From eedaed72966e0d46e15616c88d679915479f5468 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 24 Sep 2026 12:00:42 +0300 Subject: [PATCH 1/2] fix(grid): enforce validation on save and preserve compact JSON formatting in DataGridValuePanel (Closes #906) --- .../workspace/data_grid_value_panel.dart | 129 ++++++++++++++---- .../workspace/data_grid_value_panel_test.dart | 76 +++++++++++ 2 files changed, 180 insertions(+), 25 deletions(-) diff --git a/lib/features/workspace/data_grid_value_panel.dart b/lib/features/workspace/data_grid_value_panel.dart index 0c488f4..c6f30df 100644 --- a/lib/features/workspace/data_grid_value_panel.dart +++ b/lib/features/workspace/data_grid_value_panel.dart @@ -43,11 +43,18 @@ class _DataGridValuePanelState extends material.State { ValuePanelLanguage _selectedLanguage = ValuePanelLanguage.auto; String? _validationError; bool _wordWrap = true; + bool _preserveCompact = false; Timer? _debounceTimer; + static bool _isCompact(String input) { + final trimmed = input.trim(); + return !trimmed.contains('\n') && !trimmed.contains('\r'); + } + @override void initState() { super.initState(); + _preserveCompact = _isCompact(widget.cellValue); _controller = material.TextEditingController(text: _formatInitialValue(widget.cellValue)); _controller.addListener(_onTextChanged); _validateContent(); @@ -67,6 +74,7 @@ class _DataGridValuePanelState extends material.State { super.didUpdateWidget(oldWidget); if (oldWidget.cellValue != widget.cellValue) { _debounceTimer?.cancel(); + _preserveCompact = _isCompact(widget.cellValue); _controller.text = _formatInitialValue(widget.cellValue); _validateContent(); } @@ -81,6 +89,9 @@ class _DataGridValuePanelState extends material.State { } String _formatInitialValue(String input) { + if (_isCompact(input)) { + return input; + } final trimmed = input.trim(); if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) { @@ -119,28 +130,26 @@ class _DataGridValuePanelState extends material.State { return ValuePanelLanguage.text; } - void _validateContent() { - final text = _controller.text.trim(); - if (text.isEmpty) { - if (_validationError != null) { - setState(() => _validationError = null); - } - return; - } + String? _computeValidationError([String? textToValidate]) { + final text = (textToValidate ?? _controller.text).trim(); + if (text.isEmpty) return null; final lang = _effectiveLanguage; - String? err; - if (lang == ValuePanelLanguage.json) { try { jsonDecode(text); + return null; } catch (e) { - err = 'Invalid JSON: $e'; + return 'Invalid JSON: $e'; } } else if (lang == ValuePanelLanguage.xml) { - err = XmlHtmlFormatter.validate(text); + return XmlHtmlFormatter.validate(text); } + return null; + } + void _validateContent() { + final err = _computeValidationError(); if (err != _validationError) { setState(() => _validationError = err); } @@ -166,12 +175,45 @@ class _DataGridValuePanelState extends material.State { try { final parsed = jsonDecode(_controller.text); final compact = jsonEncode(parsed); - setState(() => _controller.text = compact); + setState(() { + _controller.text = compact; + _preserveCompact = true; + }); } catch (_) {} } else if (lang == ValuePanelLanguage.xml) { final compact = XmlHtmlFormatter.minify(_controller.text); - setState(() => _controller.text = compact); + setState(() { + _controller.text = compact; + _preserveCompact = true; + }); + } + } + + void _handleUpdateValue() { + _debounceTimer?.cancel(); + final err = _computeValidationError(); + if (err != null) { + setState(() => _validationError = err); + return; + } + + String textToSave = _controller.text; + final lang = _effectiveLanguage; + + if (_preserveCompact) { + if (lang == ValuePanelLanguage.json) { + try { + final parsed = jsonDecode(textToSave); + textToSave = jsonEncode(parsed); + } catch (_) {} + } else if (lang == ValuePanelLanguage.xml) { + try { + textToSave = XmlHtmlFormatter.minify(textToSave); + } catch (_) {} + } } + + widget.onUpdateValue?.call(textToSave); } QueryaCodeLanguage _toQueryaLanguage(ValuePanelLanguage lang) { @@ -389,17 +431,54 @@ class _DataGridValuePanelState extends material.State { ), ), ), - child: material.ElevatedButton( - onPressed: () { - widget.onUpdateValue!(_controller.text); - }, - style: material.ElevatedButton.styleFrom( - backgroundColor: cs.primary, - foregroundColor: cs.primaryForeground, - padding: const material.EdgeInsets.symmetric(vertical: 8), - minimumSize: material.Size.zero, - ), - child: const Text('Update Cell Value').small().bold(), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + if (activeLang == ValuePanelLanguage.json || activeLang == ValuePanelLanguage.xml) ...[ + material.Padding( + padding: const material.EdgeInsets.only(bottom: 6), + child: material.Row( + children: [ + material.SizedBox( + height: 18, + width: 18, + child: material.Checkbox( + value: _preserveCompact, + onChanged: (val) { + setState(() => _preserveCompact = val ?? false); + }, + materialTapTargetSize: material.MaterialTapTargetSize.shrinkWrap, + ), + ), + const Gap(6), + material.GestureDetector( + onTap: () { + setState(() => _preserveCompact = !_preserveCompact); + }, + child: Text( + 'Preserve compact formatting', + style: TextStyle( + fontSize: 11, + color: cs.foreground, + ), + ), + ), + ], + ), + ), + ], + material.ElevatedButton( + onPressed: _validationError != null ? null : _handleUpdateValue, + style: material.ElevatedButton.styleFrom( + backgroundColor: cs.primary, + foregroundColor: cs.primaryForeground, + padding: const material.EdgeInsets.symmetric(vertical: 8), + minimumSize: material.Size.zero, + ), + child: const Text('Update Cell Value').small().bold(), + ), + ], ), ), ], diff --git a/test/features/workspace/data_grid_value_panel_test.dart b/test/features/workspace/data_grid_value_panel_test.dart index d096d20..65c3ecb 100644 --- a/test/features/workspace/data_grid_value_panel_test.dart +++ b/test/features/workspace/data_grid_value_panel_test.dart @@ -97,5 +97,81 @@ void main() { await tester.pump(const Duration(milliseconds: 200)); expect(find.textContaining('Invalid JSON'), findsOneWidget); }); + + testWidgets('preserves compact JSON formatting when opened and saved', (tester) async { + String? updatedVal; + const initialJson = '{"name":"John","age":30}'; + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Material( + child: DataGridValuePanel( + columnName: 'payload', + cellValue: initialJson, + rowIndex: 0, + onClose: () {}, + onUpdateValue: (val) => updatedVal = val, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Controller should retain compact single line + final textField = tester.widget(find.byType(material.TextField)); + expect(textField.controller!.text, initialJson); + expect(find.text('Preserve compact formatting'), findsOneWidget); + + // Tap Format - text in editor becomes indented + await tester.tap(find.text('Format')); + await tester.pumpAndSettle(); + expect(textField.controller!.text, contains('\n')); + + // Save while "Preserve compact formatting" is checked -> saved value is compacted + await tester.tap(find.text('Update Cell Value')); + await tester.pumpAndSettle(); + expect(updatedVal, '{"name":"John","age":30}'); + + // Now uncheck "Preserve compact formatting" and save -> saved value is formatted + await tester.tap(find.text('Preserve compact formatting')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Update Cell Value')); + await tester.pumpAndSettle(); + expect(updatedVal, contains('\n')); + }); + + testWidgets('blocks saving when validation error exists or on immediate invalid submit', (tester) async { + String? updatedVal; + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Material( + child: DataGridValuePanel( + columnName: 'payload', + cellValue: '{"valid":true}', + rowIndex: 0, + onClose: () {}, + onUpdateValue: (val) => updatedVal = val, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Enter broken JSON and immediately click Update Cell Value before debounce + await tester.enterText(find.byType(material.TextField), '{"broken":'); + await tester.tap(find.text('Update Cell Value')); + await tester.pumpAndSettle(); + + // Should not have updated + expect(updatedVal, isNull); + // Error banner should be displayed immediately + expect(find.textContaining('Invalid JSON'), findsOneWidget); + + // Elevated button is now disabled because _validationError != null + final button = tester.widget(find.byType(material.ElevatedButton)); + expect(button.onPressed, isNull); + }); }); } From d2c30c1e1a9987894603d1094ba4b01ea67f57b6 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 24 Sep 2026 12:09:02 +0300 Subject: [PATCH 2/2] fix(grid): detect incomplete or invalid JSON and XML in DataGridValuePanel --- .../workspace/data_grid_value_panel.dart | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/features/workspace/data_grid_value_panel.dart b/lib/features/workspace/data_grid_value_panel.dart index c6f30df..e24720f 100644 --- a/lib/features/workspace/data_grid_value_panel.dart +++ b/lib/features/workspace/data_grid_value_panel.dart @@ -41,6 +41,7 @@ class DataGridValuePanel extends material.StatefulWidget { class _DataGridValuePanelState extends material.State { late final material.TextEditingController _controller; ValuePanelLanguage _selectedLanguage = ValuePanelLanguage.auto; + late ValuePanelLanguage _initialDetectedLanguage; String? _validationError; bool _wordWrap = true; bool _preserveCompact = false; @@ -51,9 +52,29 @@ class _DataGridValuePanelState extends material.State { return !trimmed.contains('\n') && !trimmed.contains('\r'); } + static ValuePanelLanguage _detectLanguage(String input) { + final trimmed = input.trim(); + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + return ValuePanelLanguage.json; + } + if (trimmed.startsWith('<')) { + return ValuePanelLanguage.xml; + } + final upper = trimmed.toUpperCase(); + if (upper.startsWith('SELECT ') || + upper.startsWith('INSERT ') || + upper.startsWith('UPDATE ') || + upper.startsWith('CREATE ') || + upper.startsWith('WITH ')) { + return ValuePanelLanguage.sql; + } + return ValuePanelLanguage.text; + } + @override void initState() { super.initState(); + _initialDetectedLanguage = _detectLanguage(widget.cellValue); _preserveCompact = _isCompact(widget.cellValue); _controller = material.TextEditingController(text: _formatInitialValue(widget.cellValue)); _controller.addListener(_onTextChanged); @@ -74,6 +95,7 @@ class _DataGridValuePanelState extends material.State { super.didUpdateWidget(oldWidget); if (oldWidget.cellValue != widget.cellValue) { _debounceTimer?.cancel(); + _initialDetectedLanguage = _detectLanguage(widget.cellValue); _preserveCompact = _isCompact(widget.cellValue); _controller.text = _formatInitialValue(widget.cellValue); _validateContent(); @@ -112,11 +134,10 @@ class _DataGridValuePanelState extends material.State { return _selectedLanguage; } final trimmed = _controller.text.trim(); - if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || - (trimmed.startsWith('[') && trimmed.endsWith(']'))) { + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { return ValuePanelLanguage.json; } - if (trimmed.startsWith('<') && trimmed.endsWith('>')) { + if (trimmed.startsWith('<')) { return ValuePanelLanguage.xml; } final upper = trimmed.toUpperCase(); @@ -127,6 +148,9 @@ class _DataGridValuePanelState extends material.State { upper.startsWith('WITH ')) { return ValuePanelLanguage.sql; } + if (_initialDetectedLanguage != ValuePanelLanguage.text && trimmed.isNotEmpty) { + return _initialDetectedLanguage; + } return ValuePanelLanguage.text; }