diff --git a/lib/features/main_screen/main_content_split.dart b/lib/features/main_screen/main_content_split.dart index 00141215..2264ddf8 100644 --- a/lib/features/main_screen/main_content_split.dart +++ b/lib/features/main_screen/main_content_split.dart @@ -479,6 +479,13 @@ class MainContentSplitState extends State onRestoreLastSelectedObject: () { unawaited(_restoreLastSelectedObject()); }, + onOpenSqliteTable: ws.activeConnection == null + ? null + : (name) => widget.onSqliteObjectSelected( + ws.activeConnection!, + name, + SqliteObjectKind.table, + ), isReadOnly: ws.isReadOnly, onRequestNewConnection: widget.onRequestNewConnection, onRequestNewConnectionFromUrl: diff --git a/lib/features/main_screen/workspace_panel.dart b/lib/features/main_screen/workspace_panel.dart index ed816eb0..a6c3472e 100644 --- a/lib/features/main_screen/workspace_panel.dart +++ b/lib/features/main_screen/workspace_panel.dart @@ -66,6 +66,7 @@ class WorkspacePanel extends StatefulWidget { this.lastSelectedRedisKey, this.onNavigateHome, this.onRestoreLastSelectedObject, + this.onOpenSqliteTable, this.isReadOnly = false, this.onRequestNewConnection, this.onRequestNewConnectionFromUrl, @@ -189,6 +190,9 @@ class WorkspacePanel extends StatefulWidget { /// Callback to restore the last selected table / object view. final VoidCallback? onRestoreLastSelectedObject; + /// Opens a SQLite table chosen on the Overview page (#1816). + final void Function(String name)? onOpenSqliteTable; + /// Empty-state hero: primary CTA to add a connection. final void Function()? onRequestNewConnection; final void Function()? onRequestNewConnectionFromUrl; @@ -378,6 +382,7 @@ class _WorkspacePanelState extends State { sqlTabRequestToken: widget.sqliteSqlTabRequestToken, lastSelectedSqliteObject: widget.lastSelectedSqliteObject, onRestoreLastSelectedObject: widget.onRestoreLastSelectedObject, + onOpenTable: widget.onOpenSqliteTable, isReadOnly: widget.isReadOnly, ), object: sq == null diff --git a/lib/features/sqlite/sqlite_overview_tab.dart b/lib/features/sqlite/sqlite_overview_tab.dart index 3c6cde96..8fb3a112 100644 --- a/lib/features/sqlite/sqlite_overview_tab.dart +++ b/lib/features/sqlite/sqlite_overview_tab.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/motion/querya_hover_surface.dart'; import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:flutter/widgets.dart'; import 'dart:io'; @@ -17,10 +18,15 @@ class SqliteOverviewTab extends material.StatefulWidget { const SqliteOverviewTab({ super.key, required this.connectionRow, + this.onOpenTable, }); final ConnectionRow connectionRow; + /// Opens a table in the table browser, as choosing it in the tree does + /// (#1816). Without it the chips are a plain list. + final void Function(String name)? onOpenTable; + @override material.State createState() => _SqliteOverviewTabState(); } @@ -391,15 +397,16 @@ class _SqliteOverviewTabState extends material.State { spacing: 8, runSpacing: 8, children: _tables.map((t) { - return material.Container( + final open = widget.onOpenTable; + final chip = QueryaHoverSurface( + borderRadius: material.BorderRadius.circular(6), padding: const material.EdgeInsets.symmetric( horizontal: 10, vertical: 6), - decoration: material.BoxDecoration( - color: cs.muted.withValues(alpha: 0.4), - borderRadius: material.BorderRadius.circular(6), - border: material.Border.all( - color: cs.border.withValues(alpha: 0.3)), - ), + idleColor: cs.muted.withValues(alpha: 0.4), + hoveredColor: cs.muted.withValues(alpha: 0.7), + border: material.Border.all( + color: cs.border.withValues(alpha: 0.3)), + onTap: open == null ? null : () => open(t), child: material.Row( mainAxisSize: material.MainAxisSize.min, children: [ @@ -410,6 +417,18 @@ class _SqliteOverviewTabState extends material.State { ], ), ); + if (open == null) return chip; + return material.Tooltip( + message: 'Open $t', + excludeFromSemantics: true, + child: material.Semantics( + button: true, + label: t, + onTap: () => open(t), + excludeSemantics: true, + child: chip, + ), + ); }).toList(), ), ], diff --git a/lib/features/sqlite/sqlite_workspace_home.dart b/lib/features/sqlite/sqlite_workspace_home.dart index 140cf09b..f425ac98 100644 --- a/lib/features/sqlite/sqlite_workspace_home.dart +++ b/lib/features/sqlite/sqlite_workspace_home.dart @@ -17,6 +17,7 @@ class SqliteWorkspaceHome extends material.StatefulWidget { this.sqlTabRequestToken = 0, this.lastSelectedSqliteObject, this.onRestoreLastSelectedObject, + this.onOpenTable, this.isReadOnly = false, }); @@ -28,6 +29,9 @@ class SqliteWorkspaceHome extends material.StatefulWidget { final ({String name, SqliteObjectKind kind})? lastSelectedSqliteObject; final VoidCallback? onRestoreLastSelectedObject; + /// Opens a table from the Overview page. + final void Function(String name)? onOpenTable; + @override material.State createState() => _SqliteWorkspaceHomeState(); @@ -131,6 +135,7 @@ class _SqliteWorkspaceHomeState extends material.State { SqliteOverviewTab( key: ValueKey('sqlite_overview_${widget.connectionRow.id}'), connectionRow: widget.connectionRow, + onOpenTable: widget.onOpenTable, ), SqliteSqlWorkspace( key: ValueKey('sqlite_sql_${widget.connectionRow.id}'), diff --git a/test/features/sqlite/sqlite_overview_chips_test.dart b/test/features/sqlite/sqlite_overview_chips_test.dart new file mode 100644 index 00000000..9e650fcb --- /dev/null +++ b/test/features/sqlite/sqlite_overview_chips_test.dart @@ -0,0 +1,82 @@ +import 'dart:io'; + +import 'package:flutter/widgets.dart'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; +import 'package:querya_desktop/features/sqlite/sqlite_overview_tab.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import '../../support/querya_theme_test_shell.dart'; + +class _FakePathProvider extends PathProviderPlatform { + _FakePathProvider(this._root); + final String _root; + + @override + Future getApplicationSupportPath() async => _root; + @override + Future getTemporaryPath() async => _root; + @override + Future getApplicationDocumentsPath() async => _root; +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late Directory tempDir; + late String dbPath; + + setUpAll(() async { + sqfliteFfiInit(); + tempDir = await Directory.systemTemp.createTemp('querya_overview_chips_'); + PathProviderPlatform.instance = _FakePathProvider(tempDir.path); + await LocalDb.initFfi(); + dbPath = '${tempDir.path}/chips.db'; + final db = await databaseFactoryFfi.openDatabase(dbPath); + await db.execute('CREATE TABLE orders (id INTEGER PRIMARY KEY);'); + await db.execute('CREATE TABLE users (id INTEGER PRIMARY KEY);'); + await db.close(); + }); + + tearDownAll(() async { + await LocalDb.instance.close(); + try { + tempDir.deleteSync(recursive: true); + } catch (_) {} + }); + + testWidgets('a table chip opens that table (#1816)', (tester) async { + await tester.binding.setSurfaceSize(const Size(1000, 800)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + final opened = []; + final conn = ConnectionRow( + id: 601, + type: 'sqlite', + name: 'Chips', + host: dbPath, + createdAt: '2026-10-25T00:00:00Z', + ); + + await tester.pumpWidget( + queryaThemeTestShell( + child: SqliteOverviewTab(connectionRow: conn, onOpenTable: opened.add), + ), + ); + for (var i = 0; i < 10; i++) { + await tester.runAsync( + () => Future.delayed(const Duration(milliseconds: 50)), + ); + await tester.pump(); + } + + expect(find.text('users'), findsOneWidget); + await tester.tap(find.text('users')); + await tester.pump(); + expect(opened, ['users']); + + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(seconds: 5)); + }); +}