From 53c7981c684cae0deeb1041aeff6701bf6e7fc7b Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 17 Oct 2026 12:28:03 +0300 Subject: [PATCH 1/3] fix(ui): destructive-query dialog fits small windows and UI scale; long table names in DML preview are cut (#1642) --- .../workspace/destructive_query_dialog.dart | 33 ++++++++++++++----- .../workspace/dml_preview_dialog.dart | 17 ++++++---- .../destructive_query_dialog_test.dart | 31 +++++++++++++++++ .../workspace/dml_preview_dialog_test.dart | 26 +++++++++++++++ 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/lib/features/workspace/destructive_query_dialog.dart b/lib/features/workspace/destructive_query_dialog.dart index 794cbd89..ebd15360 100644 --- a/lib/features/workspace/destructive_query_dialog.dart +++ b/lib/features/workspace/destructive_query_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart'; import 'package:querya_desktop/core/database/destructive_sql_detector.dart'; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/theme/querya_typography.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; import 'package:querya_desktop/core/ui/querya_control_tokens.dart'; @@ -105,17 +106,32 @@ class _DestructiveQueryDialogState final isDark = theme.brightness == Brightness.dark; final isCritical = widget.result.maxRiskLevel == 'CRITICAL'; - return QueryaDialogCard( + // 680 x 540 at 1x, scaled with the UI scale, and never past the window + // minus the usual dialog margins (#1642): on a narrow window or at 1.25x+ + // the fixed size ran off the screen. The operation list scrolls. + final insets = WindowLayout.dialogSymmetricInsets(context); + final screen = material.MediaQuery.sizeOf(context); + final width = WindowLayout.scaledDialogExtent( + context, + screenExtent: screen.width, + insetTotal: insets.horizontal, + baseMax: 680, + baseMin: 540, + ); + final height = WindowLayout.scaledDialogExtent( + context, + screenExtent: screen.height, + insetTotal: insets.vertical, + baseMax: 540, + baseMin: 440, + ); + + final card = QueryaDialogCard( borderColor: cs.destructive.withValues(alpha: isDark ? 0.6 : 0.4), - constraints: const material.BoxConstraints( - minWidth: 540, - maxWidth: 680, - minHeight: 440, - maxHeight: 580, - ), + constraints: material.BoxConstraints(maxWidth: width, maxHeight: height), child: material.FocusTraversalGroup( child: material.SizedBox( - height: 540, + height: height, child: material.Padding( padding: const material.EdgeInsets.all(20), child: material.Column( @@ -339,5 +355,6 @@ class _DestructiveQueryDialogState ), ), ); + return material.Padding(padding: insets, child: card); } } diff --git a/lib/features/workspace/dml_preview_dialog.dart b/lib/features/workspace/dml_preview_dialog.dart index c741f740..4c1e777f 100644 --- a/lib/features/workspace/dml_preview_dialog.dart +++ b/lib/features/workspace/dml_preview_dialog.dart @@ -124,12 +124,17 @@ class _DmlPreviewDialogState extends material.State<_DmlPreviewDialog> { color: cs.foreground, ), const Gap(6), - Text( - widget.plan.schema != null && - widget.plan.schema!.isNotEmpty - ? '${widget.plan.schema}.${widget.plan.tableName}' - : widget.plan.tableName, - ).semiBold().small(), + // A long schema.table name is cut, not overflowed (#1642). + material.Flexible( + child: Text( + widget.plan.schema != null && + widget.plan.schema!.isNotEmpty + ? '${widget.plan.schema}.${widget.plan.tableName}' + : widget.plan.tableName, + maxLines: 1, + overflow: material.TextOverflow.ellipsis, + ).semiBold().small(), + ), ], ), ), diff --git a/test/features/workspace/destructive_query_dialog_test.dart b/test/features/workspace/destructive_query_dialog_test.dart index 748088d4..090a297c 100644 --- a/test/features/workspace/destructive_query_dialog_test.dart +++ b/test/features/workspace/destructive_query_dialog_test.dart @@ -330,5 +330,36 @@ void main() { QueryaTheme.lightDefault.colorScheme.foreground, ); }); + + testWidgets('fits a small window instead of running off it (#1642)', + (tester) async { + await tester.binding.setSurfaceSize(const Size(520, 420)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.Builder( + builder: (context) => material.ElevatedButton( + onPressed: () => showDestructiveQueryDialog( + context: context, + result: DestructiveSqlDetector.inspect('DROP TABLE legacy_users;'), + sql: 'DROP TABLE legacy_users;', + ), + child: const material.Text('Open'), + ), + ), + ), + ); + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + final card = tester.getRect(find.byType(material.FocusTraversalGroup).last); + expect(card.left, greaterThanOrEqualTo(0)); + expect(card.top, greaterThanOrEqualTo(0)); + expect(card.right, lessThanOrEqualTo(520)); + expect(card.bottom, lessThanOrEqualTo(420)); + expect(find.text('Execute Destructive Statement'), findsOneWidget); + }); }); } diff --git a/test/features/workspace/dml_preview_dialog_test.dart b/test/features/workspace/dml_preview_dialog_test.dart index cfa09de9..24f465e2 100644 --- a/test/features/workspace/dml_preview_dialog_test.dart +++ b/test/features/workspace/dml_preview_dialog_test.dart @@ -184,5 +184,31 @@ void main() { final deleteText = tester.widget(find.text('1 DELETE')); expect(deleteText.style?.color, QueryaTheme.darkDefault.workbench.destructive); }); + + testWidgets('a long schema.table name is cut instead of overflowing (#1642)', + (tester) async { + await tester.binding.setSurfaceSize(const Size(640, 700)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + final longName = 'very_long_table_name_' * 8; + final plan = TableMutationPlan( + dialect: SqlDialect.postgres, + tableName: longName, + schema: 'reporting_archive_schema', + statements: const [ + TableMutationStatement( + type: MutationType.delete, + sql: 'DELETE FROM t WHERE "id" = 3', + description: 'Delete row 3', + ), + ], + ); + + await tester.pumpWidget(buildTestDialog(plan: plan)); + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('reporting_archive_schema.$longName'), findsOneWidget); + }); }); } From 9068d07fe0598a8a162d7f6c416f10a25041aa32 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 17 Oct 2026 12:28:37 +0300 Subject: [PATCH 2/3] test(ui): find the destructive dialog by its card --- test/features/workspace/destructive_query_dialog_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/features/workspace/destructive_query_dialog_test.dart b/test/features/workspace/destructive_query_dialog_test.dart index 090a297c..ee9eaddc 100644 --- a/test/features/workspace/destructive_query_dialog_test.dart +++ b/test/features/workspace/destructive_query_dialog_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:querya_desktop/core/database/destructive_sql_detector.dart'; import 'package:querya_desktop/core/theme/querya_theme.dart'; import 'package:querya_desktop/features/workspace/destructive_query_dialog.dart'; +import 'package:querya_desktop/shared/widgets/querya_dialog_card.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide TextExtension; import '../../support/querya_theme_test_shell.dart'; @@ -354,7 +355,7 @@ void main() { await tester.pumpAndSettle(); expect(tester.takeException(), isNull); - final card = tester.getRect(find.byType(material.FocusTraversalGroup).last); + final card = tester.getRect(find.byType(QueryaDialogCard)); expect(card.left, greaterThanOrEqualTo(0)); expect(card.top, greaterThanOrEqualTo(0)); expect(card.right, lessThanOrEqualTo(520)); From fee800fd47976452bfba56712f0bf76ec951e102 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 17 Oct 2026 12:39:51 +0300 Subject: [PATCH 3/3] fix(ui): destructive-query dialog body scrolls when the window is shorter than it (#1642) --- .../workspace/destructive_query_dialog.dart | 392 +++++++++--------- 1 file changed, 199 insertions(+), 193 deletions(-) diff --git a/lib/features/workspace/destructive_query_dialog.dart b/lib/features/workspace/destructive_query_dialog.dart index ebd15360..5b6cc08c 100644 --- a/lib/features/workspace/destructive_query_dialog.dart +++ b/lib/features/workspace/destructive_query_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart'; import 'package:querya_desktop/core/database/destructive_sql_detector.dart'; +import 'package:querya_desktop/core/layout/ui_scale.dart'; import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/theme/querya_typography.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -108,7 +109,8 @@ class _DestructiveQueryDialogState // 680 x 540 at 1x, scaled with the UI scale, and never past the window // minus the usual dialog margins (#1642): on a narrow window or at 1.25x+ - // the fixed size ran off the screen. The operation list scrolls. + // the fixed size ran off the screen. The body keeps its designed height + // and scrolls when the window is shorter than that. final insets = WindowLayout.dialogSymmetricInsets(context); final screen = material.MediaQuery.sizeOf(context); final width = WindowLayout.scaledDialogExtent( @@ -130,226 +132,230 @@ class _DestructiveQueryDialogState borderColor: cs.destructive.withValues(alpha: isDark ? 0.6 : 0.4), constraints: material.BoxConstraints(maxWidth: width, maxHeight: height), child: material.FocusTraversalGroup( - child: material.SizedBox( - height: height, - child: material.Padding( - padding: const material.EdgeInsets.all(20), - child: material.Column( - mainAxisSize: material.MainAxisSize.min, - crossAxisAlignment: material.CrossAxisAlignment.stretch, - children: [ - // Header - material.Row( - children: [ - material.Container( - padding: const material.EdgeInsets.all(8), - decoration: material.BoxDecoration( - color: cs.destructive.withValues(alpha: 0.15), - borderRadius: material.BorderRadius.circular(8), - ), - child: material.Icon( - material.Icons.warning_amber_rounded, - size: 24, - color: cs.destructive, + child: material.SingleChildScrollView( + child: material.SizedBox( + height: context.scaled(540), + child: material.Padding( + padding: const material.EdgeInsets.all(20), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + // Header + material.Row( + children: [ + material.Container( + padding: const material.EdgeInsets.all(8), + decoration: material.BoxDecoration( + color: cs.destructive.withValues(alpha: 0.15), + borderRadius: material.BorderRadius.circular(8), + ), + child: material.Icon( + material.Icons.warning_amber_rounded, + size: 24, + color: cs.destructive, + ), ), - ), - const Gap(12), - material.Expanded( - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - Text( - isCritical - ? 'Critical Destructive Operation' - : 'Destructive Operation Detected', - ).semiBold().large(), - const Gap(2), - if (widget.connectionName != null) + const Gap(12), + material.Expanded( + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ Text( - 'Target connection: ${widget.connectionName}', - ).muted().small() - else - const Text( - 'This statement will permanently alter or delete database objects.', - ).muted().small(), - ], + isCritical + ? 'Critical Destructive Operation' + : 'Destructive Operation Detected', + ).semiBold().large(), + const Gap(2), + if (widget.connectionName != null) + Text( + 'Target connection: ${widget.connectionName}', + ).muted().small() + else + const Text( + 'This statement will permanently alter or delete database objects.', + ).muted().small(), + ], + ), ), - ), - ], - ), - const Gap(16), + ], + ), + const Gap(16), - // Detected operations list - material.Container( - padding: const material.EdgeInsets.all(12), - decoration: material.BoxDecoration( - color: cs.destructive.withValues( - alpha: isDark ? 0.12 : 0.06, - ), - borderRadius: material.BorderRadius.circular(8), - border: material.Border.all( + // Detected operations list + material.Container( + padding: const material.EdgeInsets.all(12), + decoration: material.BoxDecoration( color: cs.destructive.withValues( - alpha: isDark ? 0.35 : 0.25, + alpha: isDark ? 0.12 : 0.06, + ), + borderRadius: material.BorderRadius.circular(8), + border: material.Border.all( + color: cs.destructive.withValues( + alpha: isDark ? 0.35 : 0.25, + ), ), ), - ), - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - for (final op in widget.result.operations) ...[ - material.Row( - crossAxisAlignment: material.CrossAxisAlignment.start, - children: [ - material.Container( - padding: const material.EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: material.BoxDecoration( - color: cs.destructive, - borderRadius: material.BorderRadius.circular(4), - ), - child: Text( - op.type.label, - style: const TextStyle( - color: material.Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + for (final op in widget.result.operations) ...[ + material.Row( + crossAxisAlignment: + material.CrossAxisAlignment.start, + children: [ + material.Container( + padding: const material.EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: material.BoxDecoration( + color: cs.destructive, + borderRadius: + material.BorderRadius.circular(4), + ), + child: Text( + op.type.label, + style: const TextStyle( + color: material.Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), ), ), - ), - const Gap(8), - material.Expanded( - child: Text( - op.description, - style: material.TextStyle( - fontSize: 12, - color: cs.foreground, - fontWeight: material.FontWeight.w500, + const Gap(8), + material.Expanded( + child: Text( + op.description, + style: material.TextStyle( + fontSize: 12, + color: cs.foreground, + fontWeight: material.FontWeight.w500, + ), ), ), - ), - ], - ), - if (op != widget.result.operations.last) const Gap(8), + ], + ), + if (op != widget.result.operations.last) const Gap(8), + ], ], - ], + ), ), - ), - const Gap(14), + const Gap(14), - // SQL Script Preview Header - material.Row( - mainAxisAlignment: material.MainAxisAlignment.spaceBetween, - children: [ - const Text('QUERY PREVIEW').semiBold().xSmall().muted(), - material.InkWell( - onTap: _copySql, - borderRadius: material.BorderRadius.circular(4), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - child: material.Row( - mainAxisSize: material.MainAxisSize.min, - children: [ - material.Icon( - _copied - ? material.Icons.check_rounded - : material.Icons.copy_rounded, - size: 13, - color: _copied - ? material.Colors.green - : cs.mutedForeground, - ), - const Gap(4), - Text(_copied ? 'Copied' : 'Copy SQL').xSmall(), - ], + // SQL Script Preview Header + material.Row( + mainAxisAlignment: material.MainAxisAlignment.spaceBetween, + children: [ + const Text('QUERY PREVIEW').semiBold().xSmall().muted(), + material.InkWell( + onTap: _copySql, + borderRadius: material.BorderRadius.circular(4), + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + child: material.Row( + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Icon( + _copied + ? material.Icons.check_rounded + : material.Icons.copy_rounded, + size: 13, + color: _copied + ? material.Colors.green + : cs.mutedForeground, + ), + const Gap(4), + Text(_copied ? 'Copied' : 'Copy SQL').xSmall(), + ], + ), ), ), - ), - ], - ), - const Gap(6), + ], + ), + const Gap(6), - // SQL Code block container - material.Expanded( - child: material.Container( - padding: const material.EdgeInsets.all(12), - decoration: material.BoxDecoration( - color: context.workbench.editorBackground, - borderRadius: material.BorderRadius.circular(8), - border: material.Border.all( - color: cs.border.withValues(alpha: 0.6), + // SQL Code block container + material.Expanded( + child: material.Container( + padding: const material.EdgeInsets.all(12), + decoration: material.BoxDecoration( + color: context.workbench.editorBackground, + borderRadius: material.BorderRadius.circular(8), + border: material.Border.all( + color: cs.border.withValues(alpha: 0.6), + ), ), - ), - child: material.SingleChildScrollView( - child: material.SelectableText( - widget.sql, - style: material.TextStyle( - fontFamily: QueryaTypography.mono, - fontFamilyFallback: - QueryaTypography.monoFontFamilyFallback, - fontSize: 12.5, - height: 1.45, - color: cs.foreground, + child: material.SingleChildScrollView( + child: material.SelectableText( + widget.sql, + style: material.TextStyle( + fontFamily: QueryaTypography.mono, + fontFamilyFallback: + QueryaTypography.monoFontFamilyFallback, + fontSize: 12.5, + height: 1.45, + color: cs.foreground, + ), ), ), ), ), - ), - const Gap(14), - - // Confirmation Checkbox - material.Row( - children: [ - material.Checkbox( - value: _acknowledged, - onChanged: (v) => - setState(() => _acknowledged = v ?? false), - ), - const Gap(8), - material.Expanded( - child: material.GestureDetector( - onTap: () => - setState(() => _acknowledged = !_acknowledged), - child: const Text( - 'I understand that this query cannot be undone and may result in permanent data loss.', - ).small(), - ), - ), - ], - ), - const Gap(16), + const Gap(14), - // Action buttons - material.FocusTraversalGroup( - policy: material.WidgetOrderTraversalPolicy(), - child: material.Wrap( - spacing: 12, - runSpacing: 8, - alignment: material.WrapAlignment.end, - crossAxisAlignment: material.WrapCrossAlignment.center, + // Confirmation Checkbox + material.Row( children: [ - QueryaButton.ghost( - label: 'Cancel', - onPressed: () => - material.Navigator.of(context).pop(false), - size: QueryaControlSize.lg, + material.Checkbox( + value: _acknowledged, + onChanged: (v) => + setState(() => _acknowledged = v ?? false), ), - QueryaButton.destructive( - label: 'Execute Destructive Statement', - icon: material.Icons.delete_forever_rounded, - onPressed: _acknowledged - ? () => material.Navigator.of(context).pop(true) - : null, - size: QueryaControlSize.lg, + const Gap(8), + material.Expanded( + child: material.GestureDetector( + onTap: () => + setState(() => _acknowledged = !_acknowledged), + child: const Text( + 'I understand that this query cannot be undone and may result in permanent data loss.', + ).small(), + ), ), ], ), - ), - ], + const Gap(16), + + // Action buttons + material.FocusTraversalGroup( + policy: material.WidgetOrderTraversalPolicy(), + child: material.Wrap( + spacing: 12, + runSpacing: 8, + alignment: material.WrapAlignment.end, + crossAxisAlignment: material.WrapCrossAlignment.center, + children: [ + QueryaButton.ghost( + label: 'Cancel', + onPressed: () => + material.Navigator.of(context).pop(false), + size: QueryaControlSize.lg, + ), + QueryaButton.destructive( + label: 'Execute Destructive Statement', + icon: material.Icons.delete_forever_rounded, + onPressed: _acknowledged + ? () => material.Navigator.of(context).pop(true) + : null, + size: QueryaControlSize.lg, + ), + ], + ), + ), + ], + ), ), ), ),