Compare commits
1 Commits
v1.0.0-MVP
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e83e4277 |
@@ -1,5 +1,5 @@
|
||||
# Start Geutebruck API Services
|
||||
# This script starts GeViServer, SDK Bridge, and Python API
|
||||
# This script starts GeViServer, SDK Bridge, Python API, and Flutter Web App
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
@@ -15,6 +15,7 @@ $sdkBridgeExe = "$sdkBridgePath\GeViScopeBridge.exe"
|
||||
$apiPath = "C:\DEV\COPILOT\geutebruck-api\src\api"
|
||||
$venvPython = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\python.exe"
|
||||
$uvicorn = "C:\DEV\COPILOT\geutebruck-api\.venv\Scripts\uvicorn.exe"
|
||||
$flutterWebPath = "C:\DEV\COPILOT\geutebruck_app\build\web"
|
||||
|
||||
# Function to wait for port to be listening
|
||||
function Wait-ForPort {
|
||||
@@ -40,12 +41,13 @@ function Wait-ForPort {
|
||||
$geviServerRunning = Get-Process -Name "GeViServer" -ErrorAction SilentlyContinue
|
||||
$sdkBridgeRunning = Get-Process -Name "GeViScopeBridge" -ErrorAction SilentlyContinue
|
||||
$uvicornRunning = Get-Process -Name "uvicorn" -ErrorAction SilentlyContinue
|
||||
$flutterRunning = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue
|
||||
|
||||
# Start GeViServer
|
||||
if ($geviServerRunning) {
|
||||
Write-Host "[SKIP] GeViServer is already running (PID: $($geviServerRunning.Id))" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[1/3] Starting GeViServer..." -ForegroundColor Green
|
||||
Write-Host "[1/4] Starting GeViServer..." -ForegroundColor Green
|
||||
Start-Process -FilePath $geviServerExe -ArgumentList "console" -WorkingDirectory "C:\GEVISOFT" -WindowStyle Hidden
|
||||
|
||||
# Wait for GeViServer to start listening on port 7700
|
||||
@@ -65,7 +67,7 @@ if ($geviServerRunning) {
|
||||
if ($sdkBridgeRunning) {
|
||||
Write-Host "[SKIP] SDK Bridge is already running (PID: $($sdkBridgeRunning.Id))" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[2/3] Starting SDK Bridge..." -ForegroundColor Green
|
||||
Write-Host "[2/4] Starting SDK Bridge..." -ForegroundColor Green
|
||||
Start-Process -FilePath $sdkBridgeExe -WorkingDirectory $sdkBridgePath -WindowStyle Hidden
|
||||
|
||||
# Wait for SDK Bridge to start listening on port 50051
|
||||
@@ -85,7 +87,7 @@ if ($sdkBridgeRunning) {
|
||||
if ($uvicornRunning) {
|
||||
Write-Host "[SKIP] Python API is already running (PID: $($uvicornRunning.Id))" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[3/3] Starting Python API..." -ForegroundColor Green
|
||||
Write-Host "[3/4] Starting Python API..." -ForegroundColor Green
|
||||
Start-Process -FilePath $uvicorn `
|
||||
-ArgumentList "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" `
|
||||
-WorkingDirectory $apiPath `
|
||||
@@ -104,6 +106,32 @@ if ($uvicornRunning) {
|
||||
}
|
||||
}
|
||||
|
||||
# Start Flutter Web Server
|
||||
if ($flutterRunning) {
|
||||
$flutterProcess = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1 -ExpandProperty OwningProcess
|
||||
Write-Host "[SKIP] Flutter Web is already running (PID: $flutterProcess)" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[4/4] Starting Flutter Web Server..." -ForegroundColor Green
|
||||
Start-Process -FilePath "python" `
|
||||
-ArgumentList "-m", "http.server", "8081", "--bind", "0.0.0.0" `
|
||||
-WorkingDirectory $flutterWebPath `
|
||||
-WindowStyle Hidden
|
||||
|
||||
# Wait for Flutter Web to start listening on port 8081
|
||||
Write-Host " Waiting for Flutter Web to initialize" -NoNewline -ForegroundColor Gray
|
||||
if (Wait-ForPort -Port 8081 -TimeoutSeconds 10) {
|
||||
Write-Host ""
|
||||
$flutterProcess = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1 -ExpandProperty OwningProcess
|
||||
Write-Host " [OK] Flutter Web started (PID: $flutterProcess)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host ""
|
||||
Write-Host " [ERROR] Flutter Web failed to start listening on port 8081" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host "Services Started Successfully!" -ForegroundColor Green
|
||||
@@ -113,6 +141,7 @@ Write-Host "GeViServer: Running on ports 7700-7703" -ForegroundColor Cyan
|
||||
Write-Host "SDK Bridge: Running on port 50051 (gRPC)" -ForegroundColor Cyan
|
||||
Write-Host "Python API: http://localhost:8000" -ForegroundColor Cyan
|
||||
Write-Host "Swagger UI: http://localhost:8000/docs" -ForegroundColor Cyan
|
||||
Write-Host "Flutter Web: http://localhost:8081" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "To check status, run: .\status-services.ps1" -ForegroundColor Yellow
|
||||
Write-Host "To stop services, run: .\stop-services.ps1" -ForegroundColor Yellow
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stop Geutebruck API Services
|
||||
# This script stops Python API, SDK Bridge, and GeViServer
|
||||
# This script stops Flutter Web, Python API, SDK Bridge, and GeViServer
|
||||
#
|
||||
# Usage:
|
||||
# .\stop-services.ps1 # Stop all services
|
||||
@@ -20,42 +20,53 @@ if ($KeepGeViServer) {
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Stop Flutter Web Server
|
||||
$flutterPort = Get-NetTCPConnection -LocalPort 8081 -State Listen -ErrorAction SilentlyContinue
|
||||
if ($flutterPort) {
|
||||
$flutterPid = $flutterPort | Select-Object -First 1 -ExpandProperty OwningProcess
|
||||
Write-Host "[1/4] Stopping Flutter Web (PID: $flutterPid)..." -ForegroundColor Yellow
|
||||
Stop-Process -Id $flutterPid -Force
|
||||
Write-Host " [OK] Flutter Web stopped" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[1/4] Flutter Web is not running" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Stop Python API
|
||||
$uvicorn = Get-Process -Name "uvicorn"
|
||||
if ($uvicorn) {
|
||||
Write-Host "[1/3] Stopping Python API (PID: $($uvicorn.Id))..." -ForegroundColor Yellow
|
||||
Write-Host "[2/4] Stopping Python API (PID: $($uvicorn.Id))..." -ForegroundColor Yellow
|
||||
Stop-Process -Name "uvicorn" -Force
|
||||
Write-Host " [OK] Python API stopped" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[1/3] Python API is not running" -ForegroundColor Gray
|
||||
Write-Host "[2/4] Python API is not running" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Stop SDK Bridge
|
||||
$sdkBridge = Get-Process -Name "GeViScopeBridge"
|
||||
if ($sdkBridge) {
|
||||
Write-Host "[2/3] Stopping SDK Bridge (PID: $($sdkBridge.Id))..." -ForegroundColor Yellow
|
||||
Write-Host "[3/4] Stopping SDK Bridge (PID: $($sdkBridge.Id))..." -ForegroundColor Yellow
|
||||
Stop-Process -Name "GeViScopeBridge" -Force
|
||||
Write-Host " [OK] SDK Bridge stopped" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[2/3] SDK Bridge is not running" -ForegroundColor Gray
|
||||
Write-Host "[3/4] SDK Bridge is not running" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Stop GeViServer (unless -KeepGeViServer flag is set)
|
||||
if (-not $KeepGeViServer) {
|
||||
$geviServer = Get-Process -Name "GeViServer"
|
||||
if ($geviServer) {
|
||||
Write-Host "[3/3] Stopping GeViServer (PID: $($geviServer.Id))..." -ForegroundColor Yellow
|
||||
Write-Host "[4/4] Stopping GeViServer (PID: $($geviServer.Id))..." -ForegroundColor Yellow
|
||||
Stop-Process -Name "GeViServer" -Force
|
||||
Write-Host " [OK] GeViServer stopped" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[3/3] GeViServer is not running" -ForegroundColor Gray
|
||||
Write-Host "[4/4] GeViServer is not running" -ForegroundColor Gray
|
||||
}
|
||||
} else {
|
||||
$geviServer = Get-Process -Name "GeViServer"
|
||||
if ($geviServer) {
|
||||
Write-Host "[3/3] Keeping GeViServer running (PID: $($geviServer.Id))" -ForegroundColor Green
|
||||
Write-Host "[4/4] Keeping GeViServer running (PID: $($geviServer.Id))" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[3/3] GeViServer is not running (cannot keep)" -ForegroundColor Yellow
|
||||
Write-Host "[4/4] GeViServer is not running (cannot keep)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
95
geutebruck_app/lib/data/services/server_cache_service.dart
Normal file
95
geutebruck_app/lib/data/services/server_cache_service.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/action_template.dart';
|
||||
|
||||
/// Service for caching server lists to prevent categories from disappearing
|
||||
/// when SDK Bridge is temporarily unavailable during service restarts
|
||||
class ServerCacheService {
|
||||
static const String _gcoreServersKey = 'cached_gcore_servers';
|
||||
static const String _gscServersKey = 'cached_gsc_servers';
|
||||
static const String _cacheTimestampKey = 'server_cache_timestamp';
|
||||
|
||||
/// Save G-Core servers to cache
|
||||
static Future<void> cacheGCoreServers(List<ServerInfo> servers) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = jsonEncode(servers.map((s) => s.toJson()).toList());
|
||||
await prefs.setString(_gcoreServersKey, json);
|
||||
await prefs.setInt(_cacheTimestampKey, DateTime.now().millisecondsSinceEpoch);
|
||||
print('[ServerCache] Cached ${servers.length} G-Core servers');
|
||||
}
|
||||
|
||||
/// Save GSC servers to cache
|
||||
static Future<void> cacheGSCServers(List<ServerInfo> servers) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = jsonEncode(servers.map((s) => s.toJson()).toList());
|
||||
await prefs.setString(_gscServersKey, json);
|
||||
await prefs.setInt(_cacheTimestampKey, DateTime.now().millisecondsSinceEpoch);
|
||||
print('[ServerCache] Cached ${servers.length} GSC servers');
|
||||
}
|
||||
|
||||
/// Save both server lists
|
||||
static Future<void> cacheServers({
|
||||
required List<ServerInfo> gcoreServers,
|
||||
required List<ServerInfo> gscServers,
|
||||
}) async {
|
||||
await cacheGCoreServers(gcoreServers);
|
||||
await cacheGSCServers(gscServers);
|
||||
}
|
||||
|
||||
/// Load cached G-Core servers
|
||||
static Future<List<ServerInfo>> getCachedGCoreServers() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = prefs.getString(_gcoreServersKey);
|
||||
if (json == null) {
|
||||
print('[ServerCache] No cached G-Core servers found');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
final servers = list.map((item) => ServerInfo.fromJson(item as Map<String, dynamic>)).toList();
|
||||
print('[ServerCache] Loaded ${servers.length} cached G-Core servers');
|
||||
return servers;
|
||||
} catch (e) {
|
||||
print('[ServerCache] Error loading cached G-Core servers: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Load cached GSC servers
|
||||
static Future<List<ServerInfo>> getCachedGSCServers() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = prefs.getString(_gscServersKey);
|
||||
if (json == null) {
|
||||
print('[ServerCache] No cached GSC servers found');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
final list = jsonDecode(json) as List<dynamic>;
|
||||
final servers = list.map((item) => ServerInfo.fromJson(item as Map<String, dynamic>)).toList();
|
||||
print('[ServerCache] Loaded ${servers.length} cached GSC servers');
|
||||
return servers;
|
||||
} catch (e) {
|
||||
print('[ServerCache] Error loading cached GSC servers: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache age in milliseconds
|
||||
static Future<int?> getCacheAge() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final timestamp = prefs.getInt(_cacheTimestampKey);
|
||||
if (timestamp == null) return null;
|
||||
return DateTime.now().millisecondsSinceEpoch - timestamp;
|
||||
}
|
||||
|
||||
/// Clear all cached server data
|
||||
static Future<void> clearCache() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_gcoreServersKey);
|
||||
await prefs.remove(_gscServersKey);
|
||||
await prefs.remove(_cacheTimestampKey);
|
||||
print('[ServerCache] Cache cleared');
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import '../../../domain/entities/action_mapping.dart';
|
||||
import '../../../data/models/action_output.dart';
|
||||
import '../../../data/models/action_template.dart';
|
||||
import '../../../data/services/action_template_service.dart';
|
||||
import '../../../data/services/server_cache_service.dart';
|
||||
import '../../../core/constants/api_constants.dart';
|
||||
import '../../../core/storage/token_manager.dart';
|
||||
import '../../blocs/action_mapping/action_mapping_bloc.dart';
|
||||
@@ -73,6 +74,13 @@ class _ActionMappingFormScreenState extends State<ActionMappingFormScreen> {
|
||||
}
|
||||
|
||||
Future<void> _loadActionTemplates() async {
|
||||
print('[ActionTemplates] Starting to load action templates...');
|
||||
|
||||
// Load cached servers first as fallback
|
||||
final cachedGCoreServers = await ServerCacheService.getCachedGCoreServers();
|
||||
final cachedGSCServers = await ServerCacheService.getCachedGSCServers();
|
||||
print('[ActionTemplates] Cached servers - G-Core: ${cachedGCoreServers.length}, GSC: ${cachedGSCServers.length}');
|
||||
|
||||
try {
|
||||
// Get auth token from TokenManager
|
||||
final token = TokenManager().accessToken;
|
||||
@@ -85,22 +93,58 @@ class _ActionMappingFormScreenState extends State<ActionMappingFormScreen> {
|
||||
final categoriesResponse = await service.getActionCategories();
|
||||
final templates = await service.getActionTemplates();
|
||||
|
||||
// Use API data if available, otherwise use cached data
|
||||
final gcoreServers = categoriesResponse.servers.gcoreServers.isNotEmpty
|
||||
? categoriesResponse.servers.gcoreServers
|
||||
: cachedGCoreServers;
|
||||
|
||||
final gscServers = categoriesResponse.servers.gscServers.isNotEmpty
|
||||
? categoriesResponse.servers.gscServers
|
||||
: cachedGSCServers;
|
||||
|
||||
print('[ActionTemplates] API returned - G-Core: ${categoriesResponse.servers.gcoreServers.length}, GSC: ${categoriesResponse.servers.gscServers.length}');
|
||||
print('[ActionTemplates] Using - G-Core: ${gcoreServers.length}, GSC: ${gscServers.length}');
|
||||
|
||||
// Cache the server lists if we got them from API
|
||||
if (categoriesResponse.servers.gcoreServers.isNotEmpty || categoriesResponse.servers.gscServers.isNotEmpty) {
|
||||
await ServerCacheService.cacheServers(
|
||||
gcoreServers: categoriesResponse.servers.gcoreServers,
|
||||
gscServers: categoriesResponse.servers.gscServers,
|
||||
);
|
||||
print('[ActionTemplates] Cached server lists to local storage');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_categories = categoriesResponse.categories;
|
||||
_templates = templates;
|
||||
_gcoreServers = categoriesResponse.servers.gcoreServers;
|
||||
_gscServers = categoriesResponse.servers.gscServers;
|
||||
_gcoreServers = gcoreServers;
|
||||
_gscServers = gscServers;
|
||||
_isLoadingTemplates = false;
|
||||
});
|
||||
|
||||
print('[ActionTemplates] Successfully loaded templates and ${gcoreServers.length + gscServers.length} servers');
|
||||
} catch (e) {
|
||||
print('[ActionTemplates] Error loading from API: $e');
|
||||
|
||||
// Fall back to cached data
|
||||
setState(() {
|
||||
_gcoreServers = cachedGCoreServers;
|
||||
_gscServers = cachedGSCServers;
|
||||
_isLoadingTemplates = false;
|
||||
});
|
||||
|
||||
print('[ActionTemplates] Fell back to cached servers - G-Core: ${cachedGCoreServers.length}, GSC: ${cachedGSCServers.length}');
|
||||
|
||||
if (mounted) {
|
||||
final hasCache = cachedGCoreServers.isNotEmpty || cachedGSCServers.isNotEmpty;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to load action templates: $e'),
|
||||
backgroundColor: Colors.orange,
|
||||
content: Text(
|
||||
hasCache
|
||||
? 'Using cached server data (API unavailable)'
|
||||
: 'Failed to load action templates: $e',
|
||||
),
|
||||
backgroundColor: hasCache ? Colors.orange : Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,17 @@ class ActionMappingsListScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
bool _showSearch = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final Set<String> _selectedMappings = {};
|
||||
bool _selectAll = false;
|
||||
|
||||
// Filter states
|
||||
bool? _filterEnabled; // null = all, true = enabled only, false = disabled only
|
||||
String _searchQuery = '';
|
||||
|
||||
// Sort states
|
||||
int? _sortColumnIndex;
|
||||
bool _sortAscending = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -27,51 +36,113 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<ActionMapping> _getFilteredMappings(List<ActionMapping> allMappings) {
|
||||
var filtered = allMappings;
|
||||
|
||||
// Apply enabled/disabled filter
|
||||
if (_filterEnabled != null) {
|
||||
filtered = filtered.where((m) => m.enabled == _filterEnabled).toList();
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
filtered = filtered.where((m) {
|
||||
return m.name.toLowerCase().contains(query) ||
|
||||
m.inputAction.toLowerCase().contains(query) ||
|
||||
m.outputActions.any((o) => o.action.toLowerCase().contains(query)) ||
|
||||
(m.description?.toLowerCase().contains(query) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
if (_sortColumnIndex != null) {
|
||||
filtered.sort((a, b) {
|
||||
int comparison = 0;
|
||||
switch (_sortColumnIndex) {
|
||||
case 0: // Name
|
||||
comparison = a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||
break;
|
||||
case 1: // Input Action
|
||||
comparison = a.inputAction.toLowerCase().compareTo(b.inputAction.toLowerCase());
|
||||
break;
|
||||
case 2: // Output Actions
|
||||
final aOutputs = a.outputActions.map((o) => o.action).join(', ').toLowerCase();
|
||||
final bOutputs = b.outputActions.map((o) => o.action).join(', ').toLowerCase();
|
||||
comparison = aOutputs.compareTo(bOutputs);
|
||||
break;
|
||||
case 3: // Status
|
||||
comparison = a.enabled == b.enabled ? 0 : (a.enabled ? -1 : 1);
|
||||
break;
|
||||
case 4: // Executions
|
||||
comparison = a.executionCount.compareTo(b.executionCount);
|
||||
break;
|
||||
}
|
||||
return _sortAscending ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
void _toggleSelectAll(List<ActionMapping> mappings) {
|
||||
setState(() {
|
||||
if (_selectAll) {
|
||||
_selectedMappings.clear();
|
||||
} else {
|
||||
_selectedMappings.addAll(mappings.map((m) => m.id));
|
||||
}
|
||||
_selectAll = !_selectAll;
|
||||
});
|
||||
}
|
||||
|
||||
void _showBatchDeleteDialog(BuildContext context) {
|
||||
if (_selectedMappings.isEmpty) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text('Delete ${_selectedMappings.length} Action Mappings'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete ${_selectedMappings.length} selected action mapping${_selectedMappings.length != 1 ? 's' : ''}?'
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
for (final id in _selectedMappings) {
|
||||
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(id));
|
||||
}
|
||||
setState(() {
|
||||
_selectedMappings.clear();
|
||||
_selectAll = false;
|
||||
});
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text('Delete All', style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
drawer: const AppDrawer(currentRoute: '/action-mappings'),
|
||||
appBar: AppBar(
|
||||
title: _showSearch
|
||||
? TextField(
|
||||
controller: _searchController,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search action mappings...',
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(color: Colors.white70),
|
||||
),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
onChanged: (query) {
|
||||
if (query.isEmpty) {
|
||||
context.read<ActionMappingBloc>().add(const LoadActionMappings());
|
||||
} else {
|
||||
context.read<ActionMappingBloc>().add(SearchActionMappings(query));
|
||||
}
|
||||
},
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
const Icon(Icons.link, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Action Mappings'),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.link, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Action Mappings'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
// Search toggle button
|
||||
IconButton(
|
||||
icon: Icon(_showSearch ? Icons.close : Icons.search),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showSearch = !_showSearch;
|
||||
if (!_showSearch) {
|
||||
_searchController.clear();
|
||||
context.read<ActionMappingBloc>().add(const LoadActionMappings());
|
||||
}
|
||||
});
|
||||
},
|
||||
tooltip: _showSearch ? 'Close search' : 'Search',
|
||||
),
|
||||
// Sync button with dirty count badge
|
||||
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
||||
builder: (context, state) {
|
||||
@@ -158,24 +229,136 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Add Action Mapping button
|
||||
// Toolbar with search, filters, and batch actions
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context.push('/action-mappings/create');
|
||||
// Search and Add button row
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search by name, input, output, or description...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
_searchQuery = '';
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context.push('/action-mappings/create');
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Mapping'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Filter chips and batch actions
|
||||
BlocBuilder<ActionMappingBloc, ActionMappingState>(
|
||||
builder: (context, state) {
|
||||
final allMappings = state is ActionMappingLoaded ? state.mappings : <ActionMapping>[];
|
||||
final filteredMappings = _getFilteredMappings(allMappings);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Filter chips
|
||||
const Text('Filters: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
const SizedBox(width: 8),
|
||||
ChoiceChip(
|
||||
label: const Text('All'),
|
||||
selected: _filterEnabled == null,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_filterEnabled = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ChoiceChip(
|
||||
label: const Text('Enabled'),
|
||||
selected: _filterEnabled == true,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_filterEnabled = selected ? true : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ChoiceChip(
|
||||
label: const Text('Disabled'),
|
||||
selected: _filterEnabled == false,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_filterEnabled = selected ? false : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// Batch actions
|
||||
if (_selectedMappings.isNotEmpty) ...[
|
||||
Text(
|
||||
'${_selectedMappings.length} selected',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedMappings.clear();
|
||||
_selectAll = false;
|
||||
});
|
||||
},
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showBatchDeleteDialog(context),
|
||||
icon: const Icon(Icons.delete, size: 18),
|
||||
label: const Text('Delete Selected'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Action Mapping'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Action Mapping list
|
||||
// Table
|
||||
Expanded(
|
||||
child: BlocConsumer<ActionMappingBloc, ActionMappingState>(
|
||||
listener: (context, state) {
|
||||
@@ -231,9 +414,9 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
),
|
||||
);
|
||||
} else if (state is ActionMappingLoaded) {
|
||||
final mappings = state.mappings;
|
||||
final filteredMappings = _getFilteredMappings(state.mappings);
|
||||
|
||||
if (mappings.isEmpty) {
|
||||
if (filteredMappings.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -241,14 +424,18 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
Icon(Icons.link_off, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No action mappings found',
|
||||
_searchQuery.isNotEmpty || _filterEnabled != null
|
||||
? 'No matching action mappings'
|
||||
: 'No action mappings found',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Add an action mapping to get started',
|
||||
_searchQuery.isNotEmpty || _filterEnabled != null
|
||||
? 'Try adjusting your search or filters'
|
||||
: 'Add an action mapping to get started',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
@@ -258,14 +445,7 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: mappings.length,
|
||||
itemBuilder: (context, index) {
|
||||
final mapping = mappings[index];
|
||||
return _buildActionMappingCard(context, mapping);
|
||||
},
|
||||
);
|
||||
return _buildCompactTable(context, filteredMappings);
|
||||
} else if (state is ActionMappingError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -298,7 +478,6 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// Handle ActionMappingInitial or any other unknown states with a loading indicator
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
@@ -308,241 +487,193 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionMappingCard(BuildContext context, ActionMapping mapping) {
|
||||
final hasParameters = mapping.inputParameters.isNotEmpty ||
|
||||
mapping.outputActions.any((o) => o.parameters.isNotEmpty);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12.0),
|
||||
child: ExpansionTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: mapping.enabled
|
||||
? Colors.green.withOpacity(0.2)
|
||||
: Colors.grey.withOpacity(0.2),
|
||||
child: Icon(
|
||||
Icons.link,
|
||||
color: mapping.enabled ? Colors.green : Colors.grey,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
mapping.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Widget _buildCompactTable(BuildContext context, List<ActionMapping> mappings) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: DataTable(
|
||||
columnSpacing: 24,
|
||||
horizontalMargin: 12,
|
||||
headingRowHeight: 48,
|
||||
dataRowHeight: 56,
|
||||
showCheckboxColumn: true,
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: const Text('Name', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
onSort: (columnIndex, ascending) {
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: mapping.enabled ? Colors.green : Colors.grey,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
mapping.enabled ? 'Enabled' : 'Disabled',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Input Action', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
onSort: (columnIndex, ascending) {
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
},
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Output Actions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
onSort: (columnIndex, ascending) {
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
},
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Status', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
onSort: (columnIndex, ascending) {
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
},
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text('Executions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
numeric: true,
|
||||
onSort: (columnIndex, ascending) {
|
||||
setState(() {
|
||||
_sortColumnIndex = columnIndex;
|
||||
_sortAscending = ascending;
|
||||
});
|
||||
},
|
||||
),
|
||||
const DataColumn(
|
||||
label: Text('Actions', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
if (mapping.description != null && mapping.description!.isNotEmpty)
|
||||
Text(
|
||||
mapping.description!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Input: ', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
rows: mappings.map((mapping) {
|
||||
final isSelected = _selectedMappings.contains(mapping.id);
|
||||
|
||||
return DataRow(
|
||||
selected: isSelected,
|
||||
onSelectChanged: (selected) {
|
||||
setState(() {
|
||||
if (selected == true) {
|
||||
_selectedMappings.add(mapping.id);
|
||||
} else {
|
||||
_selectedMappings.remove(mapping.id);
|
||||
}
|
||||
_selectAll = _selectedMappings.length == mappings.length;
|
||||
});
|
||||
},
|
||||
cells: [
|
||||
// Name
|
||||
DataCell(
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
mapping.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (mapping.description != null && mapping.description!.isNotEmpty)
|
||||
Text(
|
||||
mapping.description!,
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Input Action
|
||||
DataCell(
|
||||
Text(
|
||||
mapping.inputAction,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (mapping.inputParameters.isNotEmpty)
|
||||
|
||||
// Output Actions
|
||||
DataCell(
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
mapping.outputActions.map((o) => o.action).join(', '),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Status
|
||||
DataCell(
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
color: mapping.enabled ? Colors.green.withOpacity(0.1) : Colors.grey.withOpacity(0.1),
|
||||
border: Border.all(
|
||||
color: mapping.enabled ? Colors.green : Colors.grey,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${mapping.inputParameters.length} param${mapping.inputParameters.length != 1 ? 's' : ''}',
|
||||
style: TextStyle(fontSize: 10, color: Colors.blue[700]),
|
||||
mapping.enabled ? 'Enabled' : 'Disabled',
|
||||
style: TextStyle(
|
||||
color: mapping.enabled ? Colors.green[700] : Colors.grey[700],
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
...mapping.outputActions.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final output = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
index == 0 ? 'Output: ' : ' ',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
output.action,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (output.parameters.isNotEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
),
|
||||
child: Text(
|
||||
'${output.parameters.length} param${output.parameters.length != 1 ? 's' : ''}',
|
||||
style: TextStyle(fontSize: 10, color: Colors.orange[700]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (mapping.executionCount > 0) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text('Executions: ${mapping.executionCount}'),
|
||||
],
|
||||
],
|
||||
),
|
||||
children: [
|
||||
if (hasParameters)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Input Parameters
|
||||
if (mapping.inputParameters.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Input Parameters',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...mapping.inputParameters.entries.map((entry) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
'${entry.key}:',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.value.toString(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
// Output Action Parameters
|
||||
...mapping.outputActions.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final output = entry.value;
|
||||
if (output.parameters.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Output Action ${index + 1}: ${output.action}',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...output.parameters.entries.map((paramEntry) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
'${paramEntry.key}:',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
paramEntry.value.toString(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
context.push('/action-mappings/edit/${mapping.id}', extra: mapping);
|
||||
},
|
||||
tooltip: 'Edit',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
onPressed: () {
|
||||
_showDeleteConfirmation(context, mapping);
|
||||
},
|
||||
tooltip: 'Delete',
|
||||
),
|
||||
],
|
||||
// Executions
|
||||
DataCell(
|
||||
Text(
|
||||
'${mapping.executionCount}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
onPressed: () {
|
||||
context.push('/action-mappings/edit/${mapping.id}', extra: mapping);
|
||||
},
|
||||
tooltip: 'Edit',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
onPressed: () {
|
||||
_showDeleteConfirmation(context, mapping);
|
||||
},
|
||||
tooltip: 'Delete',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -562,6 +693,9 @@ class _ActionMappingsListScreenState extends State<ActionMappingsListScreen> {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<ActionMappingBloc>().add(DeleteActionMappingEvent(mapping.id));
|
||||
setState(() {
|
||||
_selectedMappings.remove(mapping.id);
|
||||
});
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: const Text('Delete', style: TextStyle(color: Colors.red)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -112,6 +112,10 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
||||
_selectedActionName = actionName;
|
||||
_selectedTemplate = widget.templates[actionName];
|
||||
|
||||
print('[DEBUG] _selectAction: action=$actionName, categoryPrefix=$_categoryPrefix');
|
||||
print('[DEBUG] _selectAction: gscServers count=${widget.gscServers.length}');
|
||||
print('[DEBUG] _selectAction: gcoreServers count=${widget.gcoreServers.length}');
|
||||
|
||||
// Clear previous parameter controllers
|
||||
for (final controller in _paramControllers.values) {
|
||||
controller.dispose();
|
||||
@@ -121,6 +125,7 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
||||
|
||||
// Initialize new parameter controllers
|
||||
if (_selectedTemplate != null) {
|
||||
print('[DEBUG] _selectAction: template parameters=${_selectedTemplate!.parameters}');
|
||||
for (final param in _selectedTemplate!.parameters) {
|
||||
_paramControllers[param] = TextEditingController();
|
||||
_paramEnabled[param] = false; // Start with parameters disabled
|
||||
@@ -130,29 +135,41 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
||||
if (_categoryPrefix == 'gcore') {
|
||||
// Add G-Core server parameter
|
||||
const serverParam = 'GCoreServer';
|
||||
print('[DEBUG] Adding G-Core server parameter');
|
||||
if (!_paramControllers.containsKey(serverParam)) {
|
||||
_paramControllers[serverParam] = TextEditingController();
|
||||
print('[DEBUG] Created new controller for $serverParam');
|
||||
}
|
||||
_paramEnabled[serverParam] = true;
|
||||
// Auto-select first enabled server if available
|
||||
final enabledServers = widget.gcoreServers.where((s) => s.enabled).toList();
|
||||
if (enabledServers.isNotEmpty) {
|
||||
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
||||
print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}');
|
||||
}
|
||||
} else if (_categoryPrefix == 'gsc') {
|
||||
// Add GSC server parameter
|
||||
const serverParam = 'GscServer';
|
||||
print('[DEBUG] Adding GSC server parameter');
|
||||
if (!_paramControllers.containsKey(serverParam)) {
|
||||
_paramControllers[serverParam] = TextEditingController();
|
||||
print('[DEBUG] Created new controller for $serverParam');
|
||||
}
|
||||
_paramEnabled[serverParam] = true;
|
||||
// Auto-select first enabled server if available
|
||||
final enabledServers = widget.gscServers.where((s) => s.enabled).toList();
|
||||
print('[DEBUG] GSC enabled servers: ${enabledServers.map((s) => s.alias).toList()}');
|
||||
if (enabledServers.isNotEmpty) {
|
||||
_paramControllers[serverParam]?.text = enabledServers.first.alias;
|
||||
print('[DEBUG] Set $serverParam to ${enabledServers.first.alias}');
|
||||
} else {
|
||||
print('[DEBUG] WARNING: No enabled GSC servers found!');
|
||||
}
|
||||
}
|
||||
|
||||
print('[DEBUG] Final _paramEnabled keys: ${_paramEnabled.keys.toList()}');
|
||||
print('[DEBUG] Final _paramControllers keys: ${_paramControllers.keys.toList()}');
|
||||
|
||||
// Auto-fill caption if empty
|
||||
if (_captionController.text.isEmpty) {
|
||||
_captionController.text = actionName;
|
||||
@@ -197,13 +214,47 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate server parameter for GSC/G-Core actions
|
||||
if (_categoryPrefix == 'gsc') {
|
||||
final gscServerValue = _paramControllers['GscServer']?.text.trim() ?? '';
|
||||
if (gscServerValue.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('GSC server is required. Please configure a GeViScope server first.'),
|
||||
backgroundColor: Colors.red,
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (_categoryPrefix == 'gcore') {
|
||||
final gcoreServerValue = _paramControllers['GCoreServer']?.text.trim() ?? '';
|
||||
if (gcoreServerValue.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('G-Core server is required. Please configure a G-Core server first.'),
|
||||
backgroundColor: Colors.red,
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
print('[DEBUG] _onOk: Building parameters...');
|
||||
print('[DEBUG] _onOk: _categoryPrefix=$_categoryPrefix');
|
||||
print('[DEBUG] _onOk: _paramEnabled=${_paramEnabled}');
|
||||
|
||||
// Build parameters map from enabled parameters
|
||||
final parameters = <String, dynamic>{};
|
||||
for (final entry in _paramEnabled.entries) {
|
||||
print('[DEBUG] _onOk: Checking param ${entry.key}, enabled=${entry.value}');
|
||||
if (entry.value) {
|
||||
final value = _paramControllers[entry.key]?.text.trim() ?? '';
|
||||
print('[DEBUG] _onOk: Param ${entry.key} value="$value"');
|
||||
if (value.isNotEmpty) {
|
||||
parameters[entry.key] = value;
|
||||
print('[DEBUG] _onOk: Added param ${entry.key}=$value');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,12 +271,16 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
||||
parameters['Delay'] = delay;
|
||||
}
|
||||
|
||||
print('[DEBUG] _onOk: Final parameters=$parameters');
|
||||
|
||||
// Create ActionOutput with the actual action name (NOT the caption!)
|
||||
final result = ActionOutput(
|
||||
action: _selectedActionName!,
|
||||
parameters: parameters,
|
||||
);
|
||||
|
||||
print('[DEBUG] _onOk: Created ActionOutput: action=$_selectedActionName, parameters=$parameters');
|
||||
|
||||
Navigator.of(context).pop(result);
|
||||
}
|
||||
|
||||
@@ -237,15 +292,19 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
||||
enhanced.addAll(widget.categories);
|
||||
|
||||
// Add G-Core variants for applicable categories
|
||||
// ALWAYS show these categories - even if no servers are configured
|
||||
// User can see the category but won't be able to select a server if none exist
|
||||
for (final category in _serverCategories) {
|
||||
if (widget.categories.containsKey(category) && widget.gcoreServers.isNotEmpty) {
|
||||
if (widget.categories.containsKey(category)) {
|
||||
enhanced['G-Core: $category'] = widget.categories[category]!;
|
||||
}
|
||||
}
|
||||
|
||||
// Add GSC variants for applicable categories
|
||||
// ALWAYS show these categories - even if no servers are configured
|
||||
// User can see the category but won't be able to select a server if none exist
|
||||
for (final category in _serverCategories) {
|
||||
if (widget.categories.containsKey(category) && widget.gscServers.isNotEmpty) {
|
||||
if (widget.categories.containsKey(category)) {
|
||||
enhanced['GSC: $category'] = widget.categories[category]!;
|
||||
}
|
||||
}
|
||||
@@ -258,12 +317,15 @@ class _ActionPickerDialogState extends State<ActionPickerDialog> {
|
||||
if (displayCategory.startsWith('G-Core: ')) {
|
||||
_categoryPrefix = 'gcore';
|
||||
_selectedCategory = displayCategory.substring(8); // Remove "G-Core: "
|
||||
print('[DEBUG] Parsed category: prefix=gcore, base=$_selectedCategory');
|
||||
} else if (displayCategory.startsWith('GSC: ')) {
|
||||
_categoryPrefix = 'gsc';
|
||||
_selectedCategory = displayCategory.substring(5); // Remove "GSC: "
|
||||
print('[DEBUG] Parsed category: prefix=gsc, base=$_selectedCategory');
|
||||
} else {
|
||||
_categoryPrefix = null;
|
||||
_selectedCategory = displayCategory;
|
||||
print('[DEBUG] Parsed category: prefix=null, base=$_selectedCategory');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user