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
65 changes: 56 additions & 9 deletions lib/features/connections/connection_creation_flow.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -74,9 +76,29 @@ Future<ConnectionRow?> 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,
Expand Down Expand Up @@ -112,19 +134,44 @@ Future<ConnectionRow?> 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<DriverContribution> 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;
}
82 changes: 82 additions & 0 deletions test/features/connections/edit_extension_connection_test.dart
Original file line number Diff line number Diff line change
@@ -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));
});
}
Loading