Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/core/storage/local_db.dart
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,18 @@ class LocalDb {
});
}

/// Moves a connection into the folder [folderId], or out of any folder when
/// it is null. Only the folder changes: secrets and settings are untouched.
Future<void> setConnectionFolder(int connectionId, int? folderId) async {
final db = await _open();
await db.update(
'connections',
{'folder_id': folderId},
where: 'id = ?',
whereArgs: [connectionId],
);
}

Future<void> clearFolders() async {
final db = await _open();
await db.delete('folders');
Expand Down
47 changes: 47 additions & 0 deletions lib/features/connections/connections_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,46 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
}
}

/// The "Move to folder" submenu of a connection: every other folder and,
/// when the connection is in one, "No folder". Empty when there is nowhere to
/// move it.
List<MenuItem> _moveItems(ConnectionRow conn) {
final theme = Theme.of(context);
final current = conn.folderId;
final targets = <MenuItem>[
for (final name in _folders)
if (_folderIdByName[name] != current)
MenuButton(
leading: material.Icon(material.Icons.folder_outlined,
size: 18, color: theme.colorScheme.mutedForeground),
onPressed: (_) => _moveConnection(conn, _folderIdByName[name]),
child: Text(name),
),
if (current != null)
MenuButton(
leading: material.Icon(material.Icons.folder_off_outlined,
size: 18, color: theme.colorScheme.mutedForeground),
onPressed: (_) => _moveConnection(conn, null),
child: const Text('No folder'),
),
];
if (targets.isEmpty) return const [];
return [
MenuButton(
leading: material.Icon(material.Icons.drive_file_move_outlined,
size: 18, color: theme.colorScheme.mutedForeground),
subMenu: targets,
child: const Text('Move to folder'),
),
];
}

Future<void> _moveConnection(ConnectionRow conn, int? folderId) async {
if (conn.id == null) return;
await LocalDb.instance.setConnectionFolder(conn.id!, folderId);
if (mounted) await _loadData();
}

Future<void> _closeSessionsOf(int id) async {
await MongoService.instance.disconnectByConnectionId(id);
await ExtensionDriverSession.instance.disconnect(id);
Expand Down Expand Up @@ -821,6 +861,7 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
iconAsset: QueryaIcons.connectionAsset(conn.type),
onRemove: () => _removeConnection(conn.id!),
onEdit: () => _editConnection(conn),
moveItems: _moveItems(conn),
onTap: () => widget.onConnectionSelected?.call(conn),
onPostgresObjectSelected: widget.onPostgresObjectSelected,
onPostgresOpenSqlWorkspace: widget.onPostgresOpenSqlWorkspace,
Expand All @@ -835,6 +876,7 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
iconAsset: QueryaIcons.connectionAsset(conn.type),
onRemove: () => _removeConnection(conn.id!),
onEdit: () => _editConnection(conn),
moveItems: _moveItems(conn),
onTap: () => widget.onConnectionSelected?.call(conn),
onMysqlObjectSelected: widget.onMysqlObjectSelected,
onMysqlOpenSqlWorkspace: widget.onMysqlOpenSqlWorkspace,
Expand All @@ -849,6 +891,7 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
iconAsset: QueryaIcons.connectionAsset(conn.type),
onRemove: () => _removeConnection(conn.id!),
onEdit: () => _editConnection(conn),
moveItems: _moveItems(conn),
onTap: () => widget.onConnectionSelected?.call(conn),
onDatabaseTap: (db) => widget.onRedisDatabaseSelected?.call(conn, db),
isExpanded: isExpanded,
Expand All @@ -862,6 +905,7 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
iconAsset: QueryaIcons.connectionAsset(conn.type),
onRemove: () => _removeConnection(conn.id!),
onEdit: () => _editConnection(conn),
moveItems: _moveItems(conn),
onTap: () => widget.onConnectionSelected?.call(conn),
onDatabaseTap: (db) => widget.onMongoDBDatabaseSelected?.call(conn, db),
onCollectionTap: (db, col) =>
Expand All @@ -877,6 +921,7 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
iconAsset: QueryaIcons.connectionAsset(conn.type),
onRemove: () => _removeConnection(conn.id!),
onEdit: () => _editConnection(conn),
moveItems: _moveItems(conn),
onTap: () => widget.onConnectionSelected?.call(conn),
onSqliteObjectSelected: widget.onSqliteObjectSelected,
onSqliteOpenSqlWorkspace: widget.onSqliteOpenSqlWorkspace,
Expand All @@ -891,6 +936,7 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
iconAsset: QueryaIcons.connectionAsset(conn.type),
onRemove: () => _removeConnection(conn.id!),
onEdit: () => _editConnection(conn),
moveItems: _moveItems(conn),
onTap: () => widget.onConnectionSelected?.call(conn),
onObjectSelected: widget.onExtensionObjectSelected,
isExpanded: isExpanded,
Expand All @@ -904,6 +950,7 @@ class ConnectionsPanelState extends State<ConnectionsPanel> {
iconAsset: QueryaIcons.connectionAsset(conn.type),
onRemove: () => _removeConnection(conn.id!),
onEdit: () => _editConnection(conn),
moveItems: _moveItems(conn),
onTap: () => widget.onConnectionSelected?.call(conn),
);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/features/connections/connections_panel_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class _ExtensionConnectionTile extends StatefulWidget {
this.iconAsset,
required this.onRemove,
required this.onEdit,
this.moveItems = const [],
this.onTap,
this.onObjectSelected,
this.isExpanded = false,
Expand All @@ -21,6 +22,9 @@ class _ExtensionConnectionTile extends StatefulWidget {
final String? iconAsset;
final VoidCallback onRemove;
final VoidCallback onEdit;

/// "Move to folder" menu entries; empty when there is nowhere to move.
final List<MenuItem> moveItems;
final VoidCallback? onTap;

/// Fires when a table/view node is clicked in the schema tree.
Expand Down Expand Up @@ -193,6 +197,7 @@ class _ExtensionConnectionTileState extends State<_ExtensionConnectionTile> {
onPressed: (_) => widget.onEdit(),
child: const Text('Edit connection…'),
),
...widget.moveItems,
MenuButton(
leading: material.Icon(material.Icons.delete_outline_rounded,
size: 18, color: theme.colorScheme.mutedForeground),
Expand Down
5 changes: 5 additions & 0 deletions lib/features/connections/connections_panel_mongo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class _MongoConnectionTile extends StatefulWidget {
this.iconAsset,
required this.onRemove,
required this.onEdit,
this.moveItems = const [],
this.onTap,
this.onDatabaseTap,
this.onCollectionTap,
Expand All @@ -23,6 +24,9 @@ class _MongoConnectionTile extends StatefulWidget {
final String? iconAsset;
final VoidCallback onRemove;
final VoidCallback onEdit;

/// "Move to folder" menu entries; empty when there is nowhere to move.
final List<MenuItem> moveItems;
final VoidCallback? onTap;
final void Function(String database)? onDatabaseTap;
final void Function(String database, String collection)? onCollectionTap;
Expand Down Expand Up @@ -172,6 +176,7 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> {
onPressed: (_) => widget.onEdit(),
child: const Text('Edit connection…'),
),
...widget.moveItems,
MenuButton(
leading: material.Icon(material.Icons.delete_outline_rounded,
size: 18, color: theme.colorScheme.mutedForeground),
Expand Down
5 changes: 5 additions & 0 deletions lib/features/connections/connections_panel_mysql.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class _MysqlConnectionTile extends StatefulWidget {
this.iconAsset,
required this.onRemove,
required this.onEdit,
this.moveItems = const [],
this.onTap,
this.onMysqlObjectSelected,
this.onMysqlOpenSqlWorkspace,
Expand All @@ -23,6 +24,9 @@ class _MysqlConnectionTile extends StatefulWidget {
final String? iconAsset;
final VoidCallback onRemove;
final VoidCallback onEdit;

/// "Move to folder" menu entries; empty when there is nowhere to move.
final List<MenuItem> moveItems;
final VoidCallback? onTap;
final void Function(
ConnectionRow connection,
Expand Down Expand Up @@ -149,6 +153,7 @@ class _MysqlConnectionTileState extends State<_MysqlConnectionTile> {
onPressed: (_) => widget.onEdit(),
child: const Text('Edit connection…'),
),
...widget.moveItems,
MenuButton(
leading: material.Icon(material.Icons.delete_outline_rounded,
size: 18, color: theme.colorScheme.mutedForeground),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class _PostgresConnectionTile extends StatefulWidget {
this.iconAsset,
required this.onRemove,
required this.onEdit,
this.moveItems = const [],
this.onTap,
this.onPostgresObjectSelected,
this.onPostgresOpenSqlWorkspace,
Expand All @@ -23,6 +24,9 @@ class _PostgresConnectionTile extends StatefulWidget {
final String? iconAsset;
final VoidCallback onRemove;
final VoidCallback onEdit;

/// "Move to folder" menu entries; empty when there is nowhere to move.
final List<MenuItem> moveItems;
final VoidCallback? onTap;
final void Function(
ConnectionRow connection,
Expand Down Expand Up @@ -144,6 +148,7 @@ class _PostgresConnectionTileState extends State<_PostgresConnectionTile> {
onPressed: (_) => widget.onEdit(),
child: const Text('Edit connection…'),
),
...widget.moveItems,
MenuButton(
leading: material.Icon(material.Icons.delete_outline_rounded,
size: 18, color: theme.colorScheme.mutedForeground),
Expand Down
5 changes: 5 additions & 0 deletions lib/features/connections/connections_panel_redis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class _RedisConnectionTile extends StatefulWidget {
this.iconAsset,
required this.onRemove,
required this.onEdit,
this.moveItems = const [],
this.onTap,
this.onDatabaseTap,
this.isExpanded = false,
Expand All @@ -22,6 +23,9 @@ class _RedisConnectionTile extends StatefulWidget {
final String? iconAsset;
final VoidCallback onRemove;
final VoidCallback onEdit;

/// "Move to folder" menu entries; empty when there is nowhere to move.
final List<MenuItem> moveItems;
final VoidCallback? onTap;
final void Function(int database)? onDatabaseTap;
final bool isExpanded;
Expand Down Expand Up @@ -154,6 +158,7 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> {
onPressed: (_) => widget.onEdit(),
child: const Text('Edit connection…'),
),
...widget.moveItems,
MenuButton(
leading: material.Icon(material.Icons.delete_outline_rounded,
size: 18, color: theme.colorScheme.mutedForeground),
Expand Down
5 changes: 5 additions & 0 deletions lib/features/connections/connections_panel_sidebar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class _ConnectionTile extends StatelessWidget {
this.iconAsset,
required this.onRemove,
required this.onEdit,
this.moveItems = const [],
this.onTap,
});

Expand All @@ -85,6 +86,9 @@ class _ConnectionTile extends StatelessWidget {
final String? iconAsset;
final VoidCallback onRemove;
final VoidCallback onEdit;

/// "Move to folder" menu entries; empty when there is nowhere to move.
final List<MenuItem> moveItems;
final VoidCallback? onTap;

@override
Expand Down Expand Up @@ -117,6 +121,7 @@ class _ConnectionTile extends StatelessWidget {
onPressed: (_) => onEdit(),
child: const Text('Edit connection…'),
),
...moveItems,
MenuButton(
leading: material.Icon(material.Icons.delete_outline_rounded,
size: 18, color: theme.colorScheme.mutedForeground),
Expand Down
5 changes: 5 additions & 0 deletions lib/features/connections/connections_panel_sqlite.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class _SqliteConnectionTile extends StatefulWidget {
this.iconAsset,
required this.onRemove,
required this.onEdit,
this.moveItems = const [],
this.onTap,
this.onSqliteObjectSelected,
this.onSqliteOpenSqlWorkspace,
Expand All @@ -26,6 +27,9 @@ class _SqliteConnectionTile extends StatefulWidget {
final String? iconAsset;
final VoidCallback onRemove;
final VoidCallback onEdit;

/// "Move to folder" menu entries; empty when there is nowhere to move.
final List<MenuItem> moveItems;
final VoidCallback? onTap;
final void Function(
ConnectionRow connection,
Expand Down Expand Up @@ -175,6 +179,7 @@ class _SqliteConnectionTileState extends State<_SqliteConnectionTile> {
onPressed: (_) => widget.onEdit(),
child: const Text('Edit connection…'),
),
...widget.moveItems,
MenuButton(
leading: material.Icon(material.Icons.delete_outline_rounded,
size: 18, color: theme.colorScheme.mutedForeground),
Expand Down
17 changes: 17 additions & 0 deletions test/core/storage/local_db_folder_removal_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,21 @@ void main() {
await db.removeFolder('Never existed');
expect(await db.getFolderIdByName('Empty'), isNull);
});

test('setConnectionFolder moves a connection into a folder and out again',
() async {
final db = LocalDb.instance;
await db.addFolder('Mover');
final folder = await db.getFolderIdByName('Mover');
final id = await db.addConnection(row('Movable'));
await ConnectionSecretsStore.writeForConnection(id, password: 'mv-pw');

await db.setConnectionFolder(id, folder);
expect((await db.getConnectionById(id))!.folderId, folder);

await db.setConnectionFolder(id, null);
expect((await db.getConnectionById(id))!.folderId, isNull);
expect((await ConnectionSecretsStore.readForConnection(id)).password,
'mv-pw');
});
}
37 changes: 37 additions & 0 deletions test/e2e/01_connection_lifecycle/e2e_connection_crud_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flutter/gestures.dart' show kSecondaryButton;
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/storage/app_settings.dart';
import 'package:querya_desktop/core/storage/folders_storage.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/connections/connections_panel.dart';
Expand All @@ -9,6 +11,9 @@ import '../helpers/e2e_connection_helper.dart';
void main() {
final app = E2eAppHarness(prefix: 'querya_e2e_crud_');
setUpAll(app.setUpAll);
// The welcome tour opens on a first launch without connections and its
// scrim covers the sidebar.
setUpAll(() => AppSettings.instance.setHasCompletedWelcomeTour(true));
tearDownAll(app.tearDownAll);

Finder inSidebar(String text) => find.descendant(
Expand Down Expand Up @@ -92,4 +97,36 @@ void main() {
await tester.runAsync(() => FoldersStorage.instance.remove('Team B'));
await app.close(tester);
});

testWidgets('Move to folder in the connection menu puts it in the folder',
(tester) async {
await app.launch(tester);
await tester.runAsync(() => FoldersStorage.instance.add('Team C'));
final folderId = await tester
.runAsync(() => LocalDb.instance.getFolderIdByName('Team C'));
final id = await E2eConnections.add(
tester, E2eConnections.redis('E2E Mover'));
await tester.runAsync(FoldersStorage.instance.reload);
await E2eConnections.reloadSidebar(tester);

await tester.tap(inSidebar('E2E Mover').first, buttons: kSecondaryButton);
await tester.pump(const Duration(milliseconds: 300));
expect(find.text('Move to folder'), findsWidgets,
reason: 'the connection menu did not open');
await tester.tap(find.text('Move to folder').last);
await tester.pump(const Duration(milliseconds: 300));
await tester.tap(find.text('Team C').last);
await tester.pump(const Duration(milliseconds: 300));

final row =
await tester.runAsync(() => LocalDb.instance.getConnectionById(id));
expect(row!.folderId, folderId);

// Moving back out ("No folder") is covered by LocalDb.setConnectionFolder's
// test. In this widget test the entry never showed for a connection that
// is already inside a folder; the cause was not found.
await E2eConnections.remove(tester, id);
await tester.runAsync(() => FoldersStorage.instance.remove('Team C'));
await app.close(tester);
});
}
Loading