diff --git a/lib/features/connections/connection_creation_flow.dart b/lib/features/connections/connection_creation_flow.dart index 5d36f02d..6f4d61e8 100644 --- a/lib/features/connections/connection_creation_flow.dart +++ b/lib/features/connections/connection_creation_flow.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/extensions/extension_driver_catalog.dart'; import 'package:querya_desktop/core/extensions/models/extension_contributions.dart'; @@ -9,6 +10,7 @@ import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart'; import 'package:querya_desktop/features/mysql/mysql_connection_form.dart'; import 'package:querya_desktop/features/postgresql/postgresql_connection_form.dart'; import 'package:querya_desktop/features/redis/redis_connection_form.dart'; +import 'package:querya_desktop/shared/widgets/app_toast.dart'; export 'package:querya_desktop/features/connections/connection_edit_secrets.dart'; @@ -74,9 +76,29 @@ Future promptEditConnection( if (ExtensionDriverCatalog.isExtensionDriverConnection(existing)) { final manifest = ExtensionDriverCatalog.manifestForConnection(existing); - if (manifest == null) return null; - final driver = _driverForConnection(existing, manifest.contributedDrivers); - if (driver == null) return null; + if (manifest == null) { + // Not a silent nothing (#1314): say what is missing. + final who = existing.extensionId?.trim().isNotEmpty == true + ? 'The extension "${existing.extensionId!.trim()}"' + : 'The extension for "${existing.type}" connections'; + _explain( + dialogContext, + '$who that provides "${existing.name}" is not installed or is ' + 'disabled. Install or enable it in the extension manager, then edit ' + 'the connection.', + ); + return null; + } + final driver = driverForConnection(existing, manifest.contributedDrivers); + if (driver == null) { + _explain( + dialogContext, + 'The extension "${manifest.id}" no longer provides the driver ' + '"${existing.type}" that "${existing.name}" uses. Update or ' + 'reinstall the extension to edit it.', + ); + return null; + } return showExtensionConnectionForm( dialogContext, manifest: manifest, @@ -112,19 +134,44 @@ Future promptEditConnection( folderId: existing.folderId, initial: existing, ), - _ => null, + _ => _explainUnknownType(dialogContext, existing), }; } -DriverContribution? _driverForConnection( +ConnectionRow? _explainUnknownType( + material.BuildContext context, + ConnectionRow existing, +) { + _explain( + context, + 'Unknown connection type "${existing.type}": "${existing.name}" cannot ' + 'be edited here.', + ); + return null; +} + +/// A short error toast on [context], when it is still mounted. +void _explain(material.BuildContext context, String message) { + if (!context.mounted) return; + showAppToast( + context: context, + variant: AppToastVariant.error, + message: message, + ); +} + +/// The driver of [row] in [drivers]: the one whose id is the row's type, or, +/// when the extension contributes exactly one, that one. Never a different +/// driver of an extension that has several (#1314). +@visibleForTesting +DriverContribution? driverForConnection( ConnectionRow row, Iterable drivers, ) { final type = row.type.trim().toLowerCase(); - DriverContribution? first; - for (final driver in drivers) { - first ??= driver; + final all = drivers.toList(); + for (final driver in all) { if (driver.driverId.trim().toLowerCase() == type) return driver; } - return first; + return all.length == 1 ? all.single : null; } diff --git a/test/features/connections/edit_extension_connection_test.dart b/test/features/connections/edit_extension_connection_test.dart new file mode 100644 index 00000000..8a388767 --- /dev/null +++ b/test/features/connections/edit_extension_connection_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/extensions/models/extension_contributions.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; +import 'package:querya_desktop/features/connections/connection_creation_flow.dart'; + +import '../../support/querya_theme_test_shell.dart'; + +/// #1314: editing a connection whose extension or driver is missing says so. +ConnectionRow _row({String? extensionId, String type = 'clickhouse'}) => + ConnectionRow( + id: 7, + type: type, + name: 'Warehouse', + extensionId: extensionId, + createdAt: '2026-01-01T00:00:00Z', + ); + +void main() { + group('driverForConnection', () { + const a = DriverContribution(driverId: 'clickhouse', displayName: 'CH'); + const b = DriverContribution(driverId: 'duckdb', displayName: 'Duck'); + + test('the driver of the row\'s type', () { + expect(driverForConnection(_row(type: 'duckdb'), [a, b]), b); + expect(driverForConnection(_row(type: 'ClickHouse'), [a, b]), a); + }); + + test('the only driver of an extension, whatever the row says', () { + expect(driverForConnection(_row(type: 'legacy'), [a]), a); + }); + + test('never another driver of an extension that has several', () { + expect(driverForConnection(_row(type: 'legacy'), [a, b]), isNull); + expect(driverForConnection(_row(), const []), isNull); + }); + }); + + testWidgets('editing a connection of a missing extension explains it', + (t) async { + ConnectionRow? result = _row(); + await t.pumpWidget(queryaThemeTestShell( + child: material.Builder( + builder: (context) => material.TextButton( + onPressed: () async => result = await promptEditConnection( + context, _row(extensionId: 'acme.warehouse-driver')), + child: const material.Text('edit'), + ), + ), + )); + + await t.tap(find.text('edit')); + await t.pump(); + await t.pump(const Duration(milliseconds: 300)); + + expect(result, isNull); + expect(find.textContaining('acme.warehouse-driver'), findsOneWidget); + expect(find.textContaining('not installed or is disabled'), + findsOneWidget); + await t.pump(const Duration(seconds: 6)); + }); + + testWidgets('an unknown connection type explains it too', (t) async { + await t.pumpWidget(queryaThemeTestShell( + child: material.Builder( + builder: (context) => material.TextButton( + onPressed: () => promptEditConnection(context, _row(type: 'cobol')), + child: const material.Text('edit'), + ), + ), + )); + + await t.tap(find.text('edit')); + await t.pump(); + await t.pump(const Duration(milliseconds: 300)); + + // `cobol` is no built-in type and no extension has it. + expect(find.textContaining('Unknown connection type "cobol"'), + findsOneWidget); + await t.pump(const Duration(seconds: 6)); + }); +}