diff --git a/lib/features/extensions/extension_sql_workspace.dart b/lib/features/extensions/extension_sql_workspace.dart index 25dd8c25..312db97d 100644 --- a/lib/features/extensions/extension_sql_workspace.dart +++ b/lib/features/extensions/extension_sql_workspace.dart @@ -185,7 +185,14 @@ class _ExtensionSqlWorkspaceState text: text, selection: material.TextSelection.collapsed(offset: text.length), ); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to open SQL file: $e', + variant: AppToastVariant.error, + ); + } } Future _saveSqlFile() async { @@ -201,7 +208,14 @@ class _ExtensionSqlWorkspaceState final path = location?.path; if (path == null || path.isEmpty) return; await File(path).writeAsString(_sqlController.text); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to save SQL file: $e', + variant: AppToastVariant.error, + ); + } } @override diff --git a/lib/features/main_screen/querya_window_title_bar.dart b/lib/features/main_screen/querya_window_title_bar.dart index ecaac67d..64f68860 100644 --- a/lib/features/main_screen/querya_window_title_bar.dart +++ b/lib/features/main_screen/querya_window_title_bar.dart @@ -1,5 +1,6 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/layout/ui_scale.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; import 'package:querya_desktop/features/connections/driver_manager_dialog.dart'; @@ -75,7 +76,7 @@ class QueryaWindowTitleBar extends StatelessWidget { final closeButtonColors = QueryaWindowTitleBar.closeButtonColors(context); return material.Container( - height: 40, + height: context.scaled(40), color: titleBarBackground(context), child: WindowTitleBarBox( child: Row( diff --git a/lib/features/main_screen/results_tab.dart b/lib/features/main_screen/results_tab.dart index cdc66f41..77b1c308 100644 --- a/lib/features/main_screen/results_tab.dart +++ b/lib/features/main_screen/results_tab.dart @@ -9,6 +9,10 @@ import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Query output: grid, loading, error, or placeholder. /// +/// Render order in [_buildBody]: **loading** first (only when [isLoading]), +/// then **error** when [errorMessage] is non-empty (including when +/// `isLoading` is false), then status / affected / idle / grid content. +/// /// Mode changes (idle / loading / error / status / grid) morph via /// [QueryaFadeSlide]. Keys are per **mode**, not per row — so grid data updates /// and scroll rebuilds do not re-trigger the transition. diff --git a/lib/features/mongodb/mongo_stats_view.dart b/lib/features/mongodb/mongo_stats_view.dart index 50a92872..cce26a9c 100644 --- a/lib/features/mongodb/mongo_stats_view.dart +++ b/lib/features/mongodb/mongo_stats_view.dart @@ -249,7 +249,31 @@ class _MongoStatsViewState extends material.State { } final status = _serverStatus; - if (status == null) return material.Container(color: cs.background); + if (status == null) { + return material.Center( + child: material.Padding( + padding: const material.EdgeInsets.all(32), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Icon(material.Icons.error_outline_rounded, + size: 48, color: cs.destructive), + const Gap(16), + const Text('No stats available').large().semiBold(), + const Gap(8), + const Text('serverStatus returned no data.').muted().small(), + const Gap(24), + OutlineButton( + onPressed: _load, + leading: const material.Icon(material.Icons.refresh_rounded, + size: 18), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } return material.Container( color: cs.background, diff --git a/lib/features/mysql/mysql_sql_workspace.dart b/lib/features/mysql/mysql_sql_workspace.dart index 955ff704..5ece7c49 100644 --- a/lib/features/mysql/mysql_sql_workspace.dart +++ b/lib/features/mysql/mysql_sql_workspace.dart @@ -284,7 +284,14 @@ class _MysqlSqlWorkspaceState extends material.State { text: text, selection: material.TextSelection.collapsed(offset: text.length), ); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to open SQL file: $e', + variant: AppToastVariant.error, + ); + } } Future _saveSqlFile() async { @@ -299,7 +306,14 @@ class _MysqlSqlWorkspaceState extends material.State { final path = location?.path; if (path == null || path.isEmpty) return; await File(path).writeAsString(_sqlController.text); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to save SQL file: $e', + variant: AppToastVariant.error, + ); + } } Future _runTxCommand(String sql) async { diff --git a/lib/features/postgresql/postgres_sql_workspace.dart b/lib/features/postgresql/postgres_sql_workspace.dart index 464ae4df..0cd85bed 100644 --- a/lib/features/postgresql/postgres_sql_workspace.dart +++ b/lib/features/postgresql/postgres_sql_workspace.dart @@ -431,7 +431,14 @@ class _PostgresSqlWorkspaceState extends material.State { text: text, selection: material.TextSelection.collapsed(offset: text.length), ); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to open SQL file: $e', + variant: AppToastVariant.error, + ); + } } Future _saveSqlFile() async { @@ -446,7 +453,14 @@ class _PostgresSqlWorkspaceState extends material.State { final path = location?.path; if (path == null || path.isEmpty) return; await File(path).writeAsString(_sqlController.text); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to save SQL file: $e', + variant: AppToastVariant.error, + ); + } } @override diff --git a/lib/features/redis/redis_key_editor.dart b/lib/features/redis/redis_key_editor.dart index b1c6e07d..5a46f646 100644 --- a/lib/features/redis/redis_key_editor.dart +++ b/lib/features/redis/redis_key_editor.dart @@ -114,9 +114,11 @@ class _RedisKeyEditorState extends material.State { try { await widget.connection.selectDatabase(widget.database); await widget.connection.set(widget.keyName, _stringController.text); + if (!mounted) return; setState(() => _success = 'Value saved'); _clearSuccessAfterDelay(); } catch (e) { + if (!mounted) return; setState(() => _error = 'Save failed: $e'); } } @@ -127,6 +129,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.del(widget.keyName); widget.onKeyDeleted?.call(); } catch (e) { + if (!mounted) return; setState(() => _error = 'Delete failed: $e'); } } @@ -140,11 +143,13 @@ class _RedisKeyEditorState extends material.State { await widget.connection.persist(widget.keyName); } _ttl = await widget.connection.ttl(widget.keyName); + if (!mounted) return; setState(() { _success = seconds > 0 ? 'TTL set to $seconds seconds' : 'TTL removed'; }); _clearSuccessAfterDelay(); } catch (e) { + if (!mounted) return; setState(() => _error = 'TTL failed: $e'); } } @@ -156,6 +161,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.hset(widget.keyName, field, value); await _load(); } catch (e) { + if (!mounted) return; setState(() => _error = 'HSET failed: $e'); } } @@ -166,6 +172,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.hdel(widget.keyName, field); await _load(); } catch (e) { + if (!mounted) return; setState(() => _error = 'HDEL failed: $e'); } } @@ -177,6 +184,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.rpush(widget.keyName, value); await _load(); } catch (e) { + if (!mounted) return; setState(() => _error = 'RPUSH failed: $e'); } } @@ -188,6 +196,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.sadd(widget.keyName, member); await _load(); } catch (e) { + if (!mounted) return; setState(() => _error = 'SADD failed: $e'); } } @@ -198,6 +207,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.srem(widget.keyName, member); await _load(); } catch (e) { + if (!mounted) return; setState(() => _error = 'SREM failed: $e'); } } @@ -209,6 +219,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.zadd(widget.keyName, score, member); await _load(); } catch (e) { + if (!mounted) return; setState(() => _error = 'ZADD failed: $e'); } } @@ -219,6 +230,7 @@ class _RedisKeyEditorState extends material.State { await widget.connection.zrem(widget.keyName, member); await _load(); } catch (e) { + if (!mounted) return; setState(() => _error = 'ZREM failed: $e'); } } diff --git a/lib/features/redis/redis_keys_view.dart b/lib/features/redis/redis_keys_view.dart index acba5dbc..6b396aa8 100644 --- a/lib/features/redis/redis_keys_view.dart +++ b/lib/features/redis/redis_keys_view.dart @@ -80,6 +80,7 @@ class _RedisKeysViewState extends material.State { // One pipelined burst of TYPE+TTL (not N× Future.wait round-trips). List<_KeyInfo> infos; + String? typeTtlError; try { final metas = await widget.connection.typesAndTtls(keyNames); infos = [ @@ -90,11 +91,13 @@ class _RedisKeysViewState extends material.State { ttl: metas[i].ttl, ), ]; - } catch (_) { + } catch (e) { + // Still show keys with unknown type/TTL; surface the failure non-blocking. infos = [ for (final name in keyNames) _KeyInfo(name: name, type: 'unknown', ttl: -1), ]; + typeTtlError = 'Failed to load key types/TTLs: $e'; } if (!mounted) return; @@ -102,6 +105,7 @@ class _RedisKeysViewState extends material.State { _keys.addAll(infos); _cursor = nextCursor; _hasMore = nextCursor != 0; + if (typeTtlError != null) _error = typeTtlError; }); } diff --git a/lib/features/redis/redis_view.dart b/lib/features/redis/redis_view.dart index 08a9fa75..4ffa5d0a 100644 --- a/lib/features/redis/redis_view.dart +++ b/lib/features/redis/redis_view.dart @@ -226,7 +226,31 @@ class _RedisViewState extends material.State { } final info = _info; - if (info == null) return material.Container(color: cs.background); + if (info == null) { + return material.Center( + child: material.Padding( + padding: const material.EdgeInsets.all(32), + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Icon(material.Icons.error_outline_rounded, + size: 48, color: cs.destructive), + const Gap(16), + const Text('No stats available').large().semiBold(), + const Gap(8), + const Text('Redis INFO returned no data.').muted().small(), + const Gap(24), + OutlineButton( + onPressed: _load, + leading: const material.Icon(material.Icons.refresh_rounded, + size: 18), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } return material.Container( color: cs.background, diff --git a/lib/features/sqlite/sqlite_sql_workspace.dart b/lib/features/sqlite/sqlite_sql_workspace.dart index 8a98276f..5a478613 100644 --- a/lib/features/sqlite/sqlite_sql_workspace.dart +++ b/lib/features/sqlite/sqlite_sql_workspace.dart @@ -240,7 +240,14 @@ class _SqliteSqlWorkspaceState extends material.State { text: text, selection: material.TextSelection.collapsed(offset: text.length), ); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to open SQL file: $e', + variant: AppToastVariant.error, + ); + } } Future _saveSqlFile() async { @@ -255,7 +262,14 @@ class _SqliteSqlWorkspaceState extends material.State { final path = location?.path; if (path == null || path.isEmpty) return; await File(path).writeAsString(_sqlController.text); - } catch (_) {} + } catch (e) { + if (!mounted) return; + showAppToast( + context: context, + message: 'Failed to save SQL file: $e', + variant: AppToastVariant.error, + ); + } } @override diff --git a/test/features/main_screen/results_tab_test.dart b/test/features/main_screen/results_tab_test.dart index e410c655..e3bf96c1 100644 --- a/test/features/main_screen/results_tab_test.dart +++ b/test/features/main_screen/results_tab_test.dart @@ -237,6 +237,29 @@ void main() { expect(find.text('created_at'), findsOneWidget); }); + testWidgets('shows error when isLoading is false', (tester) async { + await tester.pumpWidget( + resultsShell( + child: const material.Scaffold( + body: ResultsTab( + isLoading: false, + errorMessage: 'connection refused', + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect( + find.byKey(const material.ValueKey('results_mode_error')), + findsOneWidget, + ); + expect(find.textContaining('connection refused'), findsOneWidget); + expect( + find.byKey(const material.ValueKey('results_mode_loading')), + findsNothing, + ); + }); + testWidgets('shows idle / loading / error / grid mode keys', (tester) async { await tester.pumpWidget( resultsShell(