Skip to content

Commit 094cfda

Browse files
Merge pull request #1327 from QueryaHub/fix/1314-edit-extension-connection
fix(connections): editing a connection of a missing extension or driver explains why nothing opens (#1314)
2 parents 0d0dbd9 + 50ffd9a commit 094cfda

2 files changed

Lines changed: 138 additions & 9 deletions

File tree

‎lib/features/connections/connection_creation_flow.dart‎

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/foundation.dart' show visibleForTesting;
12
import 'package:flutter/material.dart' as material;
23
import 'package:querya_desktop/core/extensions/extension_driver_catalog.dart';
34
import 'package:querya_desktop/core/extensions/models/extension_contributions.dart';
@@ -9,6 +10,7 @@ import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart';
910
import 'package:querya_desktop/features/mysql/mysql_connection_form.dart';
1011
import 'package:querya_desktop/features/postgresql/postgresql_connection_form.dart';
1112
import 'package:querya_desktop/features/redis/redis_connection_form.dart';
13+
import 'package:querya_desktop/shared/widgets/app_toast.dart';
1214

1315
export 'package:querya_desktop/features/connections/connection_edit_secrets.dart';
1416

@@ -74,9 +76,29 @@ Future<ConnectionRow?> promptEditConnection(
7476

7577
if (ExtensionDriverCatalog.isExtensionDriverConnection(existing)) {
7678
final manifest = ExtensionDriverCatalog.manifestForConnection(existing);
77-
if (manifest == null) return null;
78-
final driver = _driverForConnection(existing, manifest.contributedDrivers);
79-
if (driver == null) return null;
79+
if (manifest == null) {
80+
// Not a silent nothing (#1314): say what is missing.
81+
final who = existing.extensionId?.trim().isNotEmpty == true
82+
? 'The extension "${existing.extensionId!.trim()}"'
83+
: 'The extension for "${existing.type}" connections';
84+
_explain(
85+
dialogContext,
86+
'$who that provides "${existing.name}" is not installed or is '
87+
'disabled. Install or enable it in the extension manager, then edit '
88+
'the connection.',
89+
);
90+
return null;
91+
}
92+
final driver = driverForConnection(existing, manifest.contributedDrivers);
93+
if (driver == null) {
94+
_explain(
95+
dialogContext,
96+
'The extension "${manifest.id}" no longer provides the driver '
97+
'"${existing.type}" that "${existing.name}" uses. Update or '
98+
'reinstall the extension to edit it.',
99+
);
100+
return null;
101+
}
80102
return showExtensionConnectionForm(
81103
dialogContext,
82104
manifest: manifest,
@@ -112,19 +134,44 @@ Future<ConnectionRow?> promptEditConnection(
112134
folderId: existing.folderId,
113135
initial: existing,
114136
),
115-
_ => null,
137+
_ => _explainUnknownType(dialogContext, existing),
116138
};
117139
}
118140

119-
DriverContribution? _driverForConnection(
141+
ConnectionRow? _explainUnknownType(
142+
material.BuildContext context,
143+
ConnectionRow existing,
144+
) {
145+
_explain(
146+
context,
147+
'Unknown connection type "${existing.type}": "${existing.name}" cannot '
148+
'be edited here.',
149+
);
150+
return null;
151+
}
152+
153+
/// A short error toast on [context], when it is still mounted.
154+
void _explain(material.BuildContext context, String message) {
155+
if (!context.mounted) return;
156+
showAppToast(
157+
context: context,
158+
variant: AppToastVariant.error,
159+
message: message,
160+
);
161+
}
162+
163+
/// The driver of [row] in [drivers]: the one whose id is the row's type, or,
164+
/// when the extension contributes exactly one, that one. Never a different
165+
/// driver of an extension that has several (#1314).
166+
@visibleForTesting
167+
DriverContribution? driverForConnection(
120168
ConnectionRow row,
121169
Iterable<DriverContribution> drivers,
122170
) {
123171
final type = row.type.trim().toLowerCase();
124-
DriverContribution? first;
125-
for (final driver in drivers) {
126-
first ??= driver;
172+
final all = drivers.toList();
173+
for (final driver in all) {
127174
if (driver.driverId.trim().toLowerCase() == type) return driver;
128175
}
129-
return first;
176+
return all.length == 1 ? all.single : null;
130177
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'package:flutter/material.dart' as material;
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:querya_desktop/core/extensions/models/extension_contributions.dart';
4+
import 'package:querya_desktop/core/storage/local_db.dart';
5+
import 'package:querya_desktop/features/connections/connection_creation_flow.dart';
6+
7+
import '../../support/querya_theme_test_shell.dart';
8+
9+
/// #1314: editing a connection whose extension or driver is missing says so.
10+
ConnectionRow _row({String? extensionId, String type = 'clickhouse'}) =>
11+
ConnectionRow(
12+
id: 7,
13+
type: type,
14+
name: 'Warehouse',
15+
extensionId: extensionId,
16+
createdAt: '2026-01-01T00:00:00Z',
17+
);
18+
19+
void main() {
20+
group('driverForConnection', () {
21+
const a = DriverContribution(driverId: 'clickhouse', displayName: 'CH');
22+
const b = DriverContribution(driverId: 'duckdb', displayName: 'Duck');
23+
24+
test('the driver of the row\'s type', () {
25+
expect(driverForConnection(_row(type: 'duckdb'), [a, b]), b);
26+
expect(driverForConnection(_row(type: 'ClickHouse'), [a, b]), a);
27+
});
28+
29+
test('the only driver of an extension, whatever the row says', () {
30+
expect(driverForConnection(_row(type: 'legacy'), [a]), a);
31+
});
32+
33+
test('never another driver of an extension that has several', () {
34+
expect(driverForConnection(_row(type: 'legacy'), [a, b]), isNull);
35+
expect(driverForConnection(_row(), const []), isNull);
36+
});
37+
});
38+
39+
testWidgets('editing a connection of a missing extension explains it',
40+
(t) async {
41+
ConnectionRow? result = _row();
42+
await t.pumpWidget(queryaThemeTestShell(
43+
child: material.Builder(
44+
builder: (context) => material.TextButton(
45+
onPressed: () async => result = await promptEditConnection(
46+
context, _row(extensionId: 'acme.warehouse-driver')),
47+
child: const material.Text('edit'),
48+
),
49+
),
50+
));
51+
52+
await t.tap(find.text('edit'));
53+
await t.pump();
54+
await t.pump(const Duration(milliseconds: 300));
55+
56+
expect(result, isNull);
57+
expect(find.textContaining('acme.warehouse-driver'), findsOneWidget);
58+
expect(find.textContaining('not installed or is disabled'),
59+
findsOneWidget);
60+
await t.pump(const Duration(seconds: 6));
61+
});
62+
63+
testWidgets('an unknown connection type explains it too', (t) async {
64+
await t.pumpWidget(queryaThemeTestShell(
65+
child: material.Builder(
66+
builder: (context) => material.TextButton(
67+
onPressed: () => promptEditConnection(context, _row(type: 'cobol')),
68+
child: const material.Text('edit'),
69+
),
70+
),
71+
));
72+
73+
await t.tap(find.text('edit'));
74+
await t.pump();
75+
await t.pump(const Duration(milliseconds: 300));
76+
77+
// `cobol` is no built-in type and no extension has it.
78+
expect(find.textContaining('Unknown connection type "cobol"'),
79+
findsOneWidget);
80+
await t.pump(const Duration(seconds: 6));
81+
});
82+
}

0 commit comments

Comments
 (0)