From bba60bfa862cfeb63e2bd5ed9f287375413214a7 Mon Sep 17 00:00:00 2001 From: fanmmy Date: Wed, 30 Sep 2026 21:20:33 +0800 Subject: [PATCH] feat(plugins): support dynamic context menus and dialogs --- .../src/components/sidebar/ConnectionTree.vue | 29 +++- .../sidebar/SidebarTreeRuntimeHost.vue | 143 ++++++++++++++++-- ...ntimeHost.pluginContextMenuRefresh.spec.ts | 129 +++++++++++++++- .../lib/plugins/dynamicContextMenu.spec.ts | 42 +++++ .../src/lib/plugins/dynamicContextMenu.ts | 75 +++++++++ .../src/lib/sidebar/sidebarTreeRuntime.ts | 5 + apps/desktop/src/types/database.ts | 1 + .../src/plugins/manifest.rs | 36 +++++ plugins/README.md | 18 +++ plugins/manifest.schema.json | 1 + 10 files changed, 458 insertions(+), 21 deletions(-) create mode 100644 apps/desktop/src/lib/plugins/dynamicContextMenu.spec.ts create mode 100644 apps/desktop/src/lib/plugins/dynamicContextMenu.ts diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 1d05372f1..678d28324 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -1943,13 +1943,32 @@ function onNodeToggled(node: TreeNode, expanded: boolean) { syncSidebarTreeNodeExpansion(store.treeNodes, node, expanded); } +let contextMenuRequest = 0; function openSidebarContextMenu(event: MouseEvent, node: TreeNode, openContextMenu: (event: MouseEvent, itemsOverride?: ContextMenuItem[]) => void) { + event.preventDefault(); + event.stopPropagation(); + const request = ++contextMenuRequest; const items = sidebarTreeRuntime.buildContextMenu(node); - sidebarContextMenuTarget.value = createSidebarActionTarget(node); - sidebarContextMenuItems.value = items; - // Pass the current row's resolved menu atomically. Waiting for the items prop - // to flush would let the singleton menu briefly reuse the previous row menu. - openContextMenu(event, items); + const resolved = sidebarTreeRuntime.resolveContextMenu(node, items); + const show = (menuItems: ContextMenuItem[]) => { + if (request !== contextMenuRequest) return; + sidebarContextMenuTarget.value = createSidebarActionTarget(node); + sidebarContextMenuItems.value = menuItems; + // Pass the current row's resolved menu atomically, including async plugin items. + openContextMenu(event, menuItems); + }; + if (resolved instanceof Promise) { + const cancelPending = () => { + contextMenuRequest += 1; + }; + document.addEventListener("pointerdown", cancelPending, { capture: true, once: true }); + void resolved + .then(show) + .catch(() => show(items)) + .finally(() => { + document.removeEventListener("pointerdown", cancelPending, true); + }); + } else show(resolved); } function openSidebarDangerDialog(request: SidebarDangerDialogRequest) { diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 6f9b35cb0..617f28419 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -1,5 +1,8 @@ -