From 79f0c067909014b3482f462cc970ed92966a9a26 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Tue, 30 Jun 2026 12:31:08 +0800 Subject: [PATCH] feat: add workflow selection preservation logic and related tests --- .../_components/workflow-config-sidebar.tsx | 1 + .../ai-workflows/_components/workflow-editor.tsx | 16 ++++++++++++++-- .../_components/workflow-node-card.tsx | 1 + .../_components/workflow-port-node-menu.tsx | 1 + .../_components/workflow-utils.test.mjs | 15 +++++++++++++++ .../ai-workflows/_components/workflow-utils.ts | 8 ++++++++ 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx b/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx index a9240cd..085d3f0 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx @@ -101,6 +101,7 @@ export function WorkflowConfigPanel({ return (
diff --git a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx index 402ea7d..1c8d9de 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx @@ -41,6 +41,7 @@ import { import { createWorkflowNodeFromSpec, deleteWorkflowNode, + shouldClearWorkflowSelectionOnPointerDown, updateWorkflowNodeData, validateWorkflowDefinition, type WorkflowNodeData, @@ -319,13 +320,24 @@ function WorkflowEditorInner({ onSelectBranch(null) } + const clearSelectionFromCanvas = () => { + setNodeMenu(null) + closeConfigPanel() + } + return (
{ + if (shouldClearWorkflowSelectionOnPointerDown(event.target)) { + clearSelectionFromCanvas() + } + }} >
-
+
playgroundTools.zoomin(true)} @@ -360,7 +372,7 @@ function WorkflowEditorInner({ />
-
+
{ }, }), false) }) + + it("clears workflow selection only when clicking outside preserved regions", async () => { + const { shouldClearWorkflowSelectionOnPointerDown } = await loadModule() + + assert.equal(shouldClearWorkflowSelectionOnPointerDown({ + closest(selector) { + return selector === "[data-workflow-preserve-selection]" ? {} : null + }, + }), false) + assert.equal(shouldClearWorkflowSelectionOnPointerDown({ + closest() { + return null + }, + }), true) + }) }) describe("workflow definition mutations", () => { diff --git a/web/app/dashboard/ai-workflows/_components/workflow-utils.ts b/web/app/dashboard/ai-workflows/_components/workflow-utils.ts index e567f34..6c83bd5 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-utils.ts +++ b/web/app/dashboard/ai-workflows/_components/workflow-utils.ts @@ -131,6 +131,14 @@ export function isBranchRowActionTarget(target: EventTarget | null): boolean { return typeof maybeElement?.closest === "function" && maybeElement.closest("button") !== null } +export function shouldClearWorkflowSelectionOnPointerDown(target: EventTarget | null): boolean { + const maybeElement = target as { closest?: (selector: string) => Element | null } | null + if (typeof maybeElement?.closest !== "function") { + return false + } + return maybeElement.closest("[data-workflow-preserve-selection]") === null +} + export function getNodeTitle( node: AIWorkflowDefinition["nodes"][number] | undefined, specs: AIWorkflowNodeSpec[] = []