feat: Add compact table views with advanced filtering and batch operations

- Enhanced Flutter web app management in PowerShell scripts
  - Added Flutter web server to start-services.ps1 as 4th service
  - Updated stop-services.ps1 to stop Flutter web server
  - Improved service orchestration and startup sequence

- Implemented server caching for improved resilience
  - Added ServerCacheService for browser localStorage caching
  - Server lists persist across service restarts
  - Automatic fallback to cached data when API unavailable
  - Action picker categories always visible regardless of server status

- Redesigned Action Mappings view with compact table layout
  - Replaced card-based ListView with DataTable for higher density
  - Added real-time search across name, input, output, description
  - Implemented multi-filter support (status: enabled/disabled)
  - Added column sorting (name, input, output, status, executions)
  - Batch operations: select all/multiple, batch delete
  - Reduced row height from ~120px to 56px for better overview

- Redesigned Servers Management view with compact table layout
  - Replaced card-based ListView with DataTable
  - Added search by alias, host, user
  - Multi-filter support (type: all/G-Core/GeViScope, status: all/enabled/disabled)
  - Column sorting (alias, host, user, type, status)
  - Batch operations: select all/multiple, batch delete
  - Color-coded type and status badges

- Improved action picker dialog for GSC/G-Core actions
  - GSC and G-Core categories always visible
  - Server validation with clear error messages
  - Fixed duplicate checkbox issue in table headers
  - Debug logging for troubleshooting server parameter issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Administrator
2026-01-02 22:52:51 +01:00
parent 14893e62a5
commit c9e83e4277
7 changed files with 1343 additions and 621 deletions

View File

@@ -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');
}
}