diff --git a/lib/features/main_screen/workspace_panel.dart b/lib/features/main_screen/workspace_panel.dart index b3c4005..bf9f3e9 100644 --- a/lib/features/main_screen/workspace_panel.dart +++ b/lib/features/main_screen/workspace_panel.dart @@ -13,6 +13,7 @@ import 'package:flutter/material.dart' as material Widget, Column; import 'package:querya_desktop/core/extensions/extension_driver_catalog.dart'; +import 'package:querya_desktop/core/motion/querya_fade_slide.dart'; import 'package:querya_desktop/core/motion/querya_switching_body.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -175,115 +176,141 @@ class _WorkspacePanelState extends State { switch (activeConn.type) { case 'postgresql': final pg = widget.selectedPostgresObject; - driverWorkspace = pg == null - ? PostgresWorkspaceHome( - key: ValueKey('pg_home_${activeConn.id}'), - connectionRow: activeConn, - postgresSqlEditorContext: widget.postgresSqlEditorContext, - postgresSqlEditorContextToken: - widget.postgresSqlEditorContextToken, - sqlTabRequestToken: widget.postgresSqlTabRequestToken, - isReadOnly: widget.isReadOnly, - ) - : buildPostgresObjectWorkspace( - connection: activeConn, - pg: pg, - ); + driverWorkspace = _homeObjectMorph( + showingObject: pg != null, + home: PostgresWorkspaceHome( + key: ValueKey('pg_home_${activeConn.id}'), + connectionRow: activeConn, + postgresSqlEditorContext: widget.postgresSqlEditorContext, + postgresSqlEditorContextToken: + widget.postgresSqlEditorContextToken, + sqlTabRequestToken: widget.postgresSqlTabRequestToken, + isReadOnly: widget.isReadOnly, + ), + object: pg == null + ? null + : buildPostgresObjectWorkspace( + connection: activeConn, + pg: pg, + ), + ); break; case 'mysql': final my = widget.selectedMysqlObject; - if (my == null) { - driverWorkspace = MysqlWorkspaceHome( + material.Widget? mysqlObject; + if (my != null) { + if (my.kind == MysqlObjectKind.procedure || + my.kind == MysqlObjectKind.function) { + mysqlObject = MysqlRoutineView( + key: ValueKey( + 'mysql_${activeConn.id}_${my.database}_${my.name}_${my.kind}', + ), + connectionRow: activeConn, + database: my.database, + routineName: my.name, + isFunction: my.kind == MysqlObjectKind.function, + ); + } else { + mysqlObject = MysqlTableView( + key: ValueKey( + 'mysql_${activeConn.id}_${my.database}_${my.name}_${my.kind}', + ), + connectionRow: activeConn, + database: my.database, + tableName: my.name, + isView: my.kind == MysqlObjectKind.view, + ); + } + } + driverWorkspace = _homeObjectMorph( + showingObject: my != null, + home: MysqlWorkspaceHome( key: ValueKey('mysql_home_${activeConn.id}'), connectionRow: activeConn, sqlTabRequestToken: widget.mysqlSqlTabRequestToken, isReadOnly: widget.isReadOnly, - ); - } else if (my.kind == MysqlObjectKind.procedure || - my.kind == MysqlObjectKind.function) { - driverWorkspace = MysqlRoutineView( - key: ValueKey( - 'mysql_${activeConn.id}_${my.database}_${my.name}_${my.kind}', - ), - connectionRow: activeConn, - database: my.database, - routineName: my.name, - isFunction: my.kind == MysqlObjectKind.function, - ); - } else { - driverWorkspace = MysqlTableView( - key: ValueKey( - 'mysql_${activeConn.id}_${my.database}_${my.name}_${my.kind}', - ), - connectionRow: activeConn, - database: my.database, - tableName: my.name, - isView: my.kind == MysqlObjectKind.view, - ); - } + ), + object: mysqlObject, + ); break; case 'mongodb': final mongoDb = widget.selectedMongoDb; - driverWorkspace = mongoDb != null - ? MongoExplorerView( - key: ValueKey('mongo_${activeConn.id}_db_$mongoDb'), - connectionRow: activeConn, - database: mongoDb, - ) - : MongoStatsView( - key: ValueKey(activeConn.id), - connectionRow: activeConn, - ); + driverWorkspace = _homeObjectMorph( + showingObject: mongoDb != null, + home: MongoStatsView( + key: ValueKey('mongo_stats_${activeConn.id}'), + connectionRow: activeConn, + ), + object: mongoDb == null + ? null + : MongoExplorerView( + key: ValueKey('mongo_${activeConn.id}_db_$mongoDb'), + connectionRow: activeConn, + database: mongoDb, + ), + ); break; case 'redis': final redisDb = widget.selectedRedisDb; - driverWorkspace = redisDb != null - ? RedisExplorerView( - key: ValueKey('redis_${activeConn.id}_db_$redisDb'), - connectionRow: activeConn, - database: redisDb, - ) - : RedisView( - key: ValueKey(activeConn.id), - connectionRow: activeConn, - ); + driverWorkspace = _homeObjectMorph( + showingObject: redisDb != null, + home: RedisView( + key: ValueKey('redis_stats_${activeConn.id}'), + connectionRow: activeConn, + ), + object: redisDb == null + ? null + : RedisExplorerView( + key: ValueKey('redis_${activeConn.id}_db_$redisDb'), + connectionRow: activeConn, + database: redisDb, + ), + ); break; case 'sqlite': final sq = widget.selectedSqliteObject; - driverWorkspace = sq == null - ? SqliteWorkspaceHome( - key: ValueKey('sqlite_home_${activeConn.id}'), - connectionRow: activeConn, - sqlTabRequestToken: widget.sqliteSqlTabRequestToken, - isReadOnly: widget.isReadOnly, - ) - : SqliteTableView( - key: ValueKey( - 'sqlite_${activeConn.id}_${sq.name}_${sq.kind}', + driverWorkspace = _homeObjectMorph( + showingObject: sq != null, + home: SqliteWorkspaceHome( + key: ValueKey('sqlite_home_${activeConn.id}'), + connectionRow: activeConn, + sqlTabRequestToken: widget.sqliteSqlTabRequestToken, + isReadOnly: widget.isReadOnly, + ), + object: sq == null + ? null + : SqliteTableView( + key: ValueKey( + 'sqlite_${activeConn.id}_${sq.name}_${sq.kind}', + ), + connectionRow: activeConn, + tableName: sq.name, + isView: sq.kind == SqliteObjectKind.view, ), - connectionRow: activeConn, - tableName: sq.name, - isView: sq.kind == SqliteObjectKind.view, - ); + ); break; default: if (ExtensionDriverCatalog.isExtensionDriverConnection(activeConn)) { final obj = widget.selectedExtensionObject; - driverWorkspace = obj == null - ? ExtensionWorkspaceHome( - key: ValueKey('ext_home_${activeConn.id}'), - connectionRow: activeConn, - sqlTabRequestToken: widget.extensionSqlTabRequestToken, - isReadOnly: widget.isReadOnly, - ) - : ExtensionTableView( - key: ValueKey( - 'ext_table_${activeConn.id}_${obj.database}_${obj.name}', + driverWorkspace = _homeObjectMorph( + showingObject: obj != null, + home: ExtensionWorkspaceHome( + key: ValueKey('ext_home_${activeConn.id}'), + connectionRow: activeConn, + sqlTabRequestToken: widget.extensionSqlTabRequestToken, + isReadOnly: widget.isReadOnly, + ), + object: obj == null + ? null + : ExtensionTableView( + key: ValueKey( + 'ext_table_${activeConn.id}_${obj.database}_${obj.name}', + ), + connectionRow: activeConn, + database: obj.database, + tableName: obj.name, ), - connectionRow: activeConn, - database: obj.database, - tableName: obj.name, - ); + ); } break; } @@ -314,4 +341,27 @@ class _WorkspacePanelState extends State { ), ); } + + /// Home (stats / SQL) stays keep-alive; object/explorer morphs in on top. + /// + /// Object→object switches use [QueryaFadeSlide] so table/DB changes do not + /// hard-cut. Does not animate virtualized result rows inside those views. + material.Widget _homeObjectMorph({ + required bool showingObject, + required material.Widget home, + required material.Widget? object, + }) { + return QueryaSwitchingBody( + index: showingObject ? 1 : 0, + children: [ + home, + QueryaFadeSlide( + child: object ?? + const material.SizedBox.expand( + key: ValueKey('workspace_object_placeholder'), + ), + ), + ], + ); + } } diff --git a/test/features/main_screen/workspace_panel_layout_test.dart b/test/features/main_screen/workspace_panel_layout_test.dart index 38e3a40..3811132 100644 --- a/test/features/main_screen/workspace_panel_layout_test.dart +++ b/test/features/main_screen/workspace_panel_layout_test.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart' as material; import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/motion/querya_fade_slide.dart'; import 'package:querya_desktop/core/motion/querya_switching_body.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/features/main_screen/workspace_panel.dart'; +import 'package:querya_desktop/features/redis/redis_explorer_view.dart'; +import 'package:querya_desktop/features/redis/redis_view.dart'; import '../../support/layout_overflow.dart'; import '../../support/querya_theme_test_shell.dart'; @@ -18,6 +21,15 @@ void main() { createdAt: '0', ); + const redisConnection = ConnectionRow( + id: 42, + type: 'redis', + name: 'redis-test', + host: '127.0.0.1', + port: 6379, + createdAt: '0', + ); + group('WorkspacePanel layout (no connection)', () { final sizes = { 'narrow_tall': const material.Size(320, 720), @@ -75,7 +87,8 @@ void main() { ), ); - expect(find.byKey(const material.Key('workspace_run_button')), findsNothing); + expect( + find.byKey(const material.Key('workspace_run_button')), findsNothing); expect(find.text('Execute/Refresh (F5)'), findsNothing); }); @@ -93,7 +106,8 @@ void main() { ), ); - expect(find.byKey(const material.Key('workspace_run_button')), findsNothing); + expect( + find.byKey(const material.Key('workspace_run_button')), findsNothing); expect(find.text('Query History'), findsNothing); expect(find.textContaining('Coming in a future release'), findsNothing); }); @@ -109,7 +123,7 @@ void main() { ), ), ); - expect(find.byType(QueryaSwitchingBody), findsOneWidget); + expect(find.byType(QueryaSwitchingBody), findsWidgets); await pumpWidgetWithSurfaceSize( tester, @@ -123,7 +137,7 @@ void main() { ), ); await tester.pump(); - expect(find.byType(QueryaSwitchingBody), findsOneWidget); + expect(find.byType(QueryaSwitchingBody), findsWidgets); expect(find.text('Unsupported connection type'), findsOneWidget); // Back to empty — keep-alive stack stays mounted. @@ -137,7 +151,65 @@ void main() { ), ); await tester.pumpAndSettle(); - expect(find.byType(QueryaSwitchingBody), findsOneWidget); + expect(find.byType(QueryaSwitchingBody), findsWidgets); + }); + }); + + group('WorkspacePanel home↔object morph', () { + testWidgets('Redis stats↔explorer uses SwitchingBody + FadeSlide', + (tester) async { + await pumpWidgetWithSurfaceSize( + tester, + const material.Size(800, 600), + queryaThemeTestShell( + child: const material.SizedBox.expand( + child: WorkspacePanel(activeConnection: redisConnection), + ), + ), + ); + await tester.pump(); + + // Outer empty↔connected + inner home↔object (+ hero FadeSlide keep-alive). + expect(find.byType(QueryaSwitchingBody), findsNWidgets(2)); + expect(find.byType(QueryaFadeSlide), findsWidgets); + expect(find.byType(RedisView), findsOneWidget); + + await pumpWidgetWithSurfaceSize( + tester, + const material.Size(800, 600), + queryaThemeTestShell( + child: const material.SizedBox.expand( + child: WorkspacePanel( + activeConnection: redisConnection, + selectedRedisDb: 0, + ), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 50)); + + expect(find.byType(QueryaSwitchingBody), findsNWidgets(2)); + expect(find.byType(QueryaFadeSlide), findsWidgets); + expect(find.byType(RedisExplorerView), findsOneWidget); + // Home stays keep-alive under SwitchingBody. + expect(find.byType(RedisView), findsOneWidget); + + await pumpWidgetWithSurfaceSize( + tester, + const material.Size(800, 600), + queryaThemeTestShell( + child: const material.SizedBox.expand( + child: WorkspacePanel(activeConnection: redisConnection), + ), + ), + ); + // Avoid pumpAndSettle — Redis stats polling keeps a ticker alive. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 250)); + + expect(find.byType(RedisView), findsOneWidget); + expect(find.byType(QueryaSwitchingBody), findsNWidgets(2)); }); }); }