diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ba3a355..4a17eb4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- **Vault graph view.** A force-directed link graph of the whole vault in the + context panel — notes are nodes, wikilinks edges. Hover highlights a note's + neighbourhood; click opens it in the editor. (T-323) + ### Changed ### Fixed diff --git a/assets/i18n/en_us/builtin.graph.json b/assets/i18n/en_us/builtin.graph.json new file mode 100644 index 00000000..aa3bc6d7 --- /dev/null +++ b/assets/i18n/en_us/builtin.graph.json @@ -0,0 +1,4 @@ +{ + "tab.graph.title": { "translation": "Graph" }, + "graph.empty": { "translation": "No linked notes in this vault." } +} diff --git a/assets/i18n/nl_nl/builtin.graph.json b/assets/i18n/nl_nl/builtin.graph.json new file mode 100644 index 00000000..a6ec2737 --- /dev/null +++ b/assets/i18n/nl_nl/builtin.graph.json @@ -0,0 +1,4 @@ +{ + "tab.graph.title": { "translation": "Grafiek" }, + "graph.empty": { "translation": "Geen gekoppelde notities in deze vault." } +} diff --git a/lib/builtin/graph/src/extension.dart b/lib/builtin/graph/src/extension.dart index 32f0de21..0a1ed5c9 100644 --- a/lib/builtin/graph/src/extension.dart +++ b/lib/builtin/graph/src/extension.dart @@ -1,4 +1,7 @@ +import 'package:clide/builtin/graph/src/graph_panel.dart'; import 'package:clide/extension/extension.dart'; +import 'package:clide/kernel/kernel.dart'; +import 'package:clide/widgets/widgets.dart'; class GraphExtension extends ClideExtension { @override @@ -11,5 +14,16 @@ class GraphExtension extends ClideExtension { List get dependsOn => const ['builtin.pql']; @override - List get contributions => const []; + List get contributions => [ + TabContribution( + id: 'graph.view', + slot: Slots.contextPanel, + title: 'Graph', + titleKey: 'tab.graph.title', + i18nNamespace: id, + icon: PhosphorIcons.byName('graph'), + priority: -70, + build: (_) => const GraphPanel(), + ), + ]; } diff --git a/test/builtin/graph/graph_extension_test.dart b/test/builtin/graph/graph_extension_test.dart new file mode 100644 index 00000000..2638d6f3 --- /dev/null +++ b/test/builtin/graph/graph_extension_test.dart @@ -0,0 +1,63 @@ +/// GraphExtension (T-323) contributes the vault-graph tab into the context +/// panel — but only once its pql dependency is active (it reads link data +/// through pql). +library; + +import 'package:clide/builtin/graph/graph.dart'; +import 'package:clide/builtin/graph/src/graph_panel.dart'; +import 'package:clide/builtin/pql/pql.dart'; +import 'package:clide/extension/extension.dart'; +import 'package:clide/kernel/kernel.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/kernel_fixture.dart'; +import '../../helpers/widget_harness.dart'; + +void main() { + late KernelFixture f; + setUp(() async => f = await KernelFixture.create()); + tearDown(() => f.dispose()); + + test('declares one context-panel tab with the graph.view identity', () { + final tabs = GraphExtension().contributions.whereType().toList(); + expect(tabs, hasLength(1)); + final t = tabs.single; + expect(t.id, 'graph.view'); + expect(t.slot, Slots.contextPanel); + expect(t.title, 'Graph'); + expect(t.titleKey, 'tab.graph.title'); + expect(t.i18nNamespace, 'builtin.graph'); + }); + + testWidgets('the tab builds a GraphPanel', (tester) async { + final tab = GraphExtension().contributions.whereType().single; + late Widget built; + await tester.pumpWidget( + anchoredHarness( + f, + Builder( + builder: (ctx) { + built = tab.build(ctx); // capture without mounting — no load/ipc needed + return const SizedBox(); + }, + ), + ), + ); + expect(built, isA()); + }); + + test('registers into the context panel once activated (after its pql dep)', () async { + f.services.extensions.register(PqlExtension()); + f.services.extensions.register(GraphExtension()); + await f.services.extensions.activate('builtin.pql'); + await f.services.extensions.activate('builtin.graph'); + expect(f.services.panels.tabsFor(Slots.contextPanel).any((t) => t.id == 'graph.view'), isTrue); + }); + + test('is skipped when its pql dependency is not active', () async { + f.services.extensions.register(GraphExtension()); + await f.services.extensions.activate('builtin.graph'); // dep missing → skipped + expect(f.services.panels.tabsFor(Slots.contextPanel).any((t) => t.id == 'graph.view'), isFalse); + }); +}