feat: add workflow selection preservation logic and related tests

This commit is contained in:
mlogclub
2026-06-30 12:31:08 +08:00
parent d72079a580
commit 79f0c06790
6 changed files with 40 additions and 2 deletions
@@ -101,6 +101,7 @@ export function WorkflowConfigPanel({
return (
<div
data-workflow-preserve-selection
className="pointer-events-none absolute inset-y-3 right-3 z-50 flex max-w-[calc(100%-1.5rem)]"
style={{ width: panelWidth }}
>
@@ -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 (
<div
ref={editorRootRef}
data-workflow-editor-root
className="relative isolate h-full min-h-0 w-full flex-1 overflow-hidden border bg-[var(--g-editor-background)]"
onPointerDownCapture={(event) => {
if (shouldClearWorkflowSelectionOnPointerDown(event.target)) {
clearSelectionFromCanvas()
}
}}
>
<div
data-workflow-preserve-selection
className={cn(
"absolute left-3 top-3 z-50 flex max-w-[calc(100%-1.5rem)] flex-col items-start gap-1.5",
selectedNodeId && "max-w-[calc(100%-25rem)]"
@@ -348,7 +360,7 @@ function WorkflowEditorInner({
/>
</div>
<div className="absolute bottom-4 left-4 z-50">
<div data-workflow-preserve-selection className="absolute bottom-4 left-4 z-50">
<WorkflowCanvasControls
zoomPercent={zoomPercent}
onZoomIn={() => playgroundTools.zoomin(true)}
@@ -360,7 +372,7 @@ function WorkflowEditorInner({
/>
</div>
<div className="absolute bottom-4 right-4 z-50">
<div data-workflow-preserve-selection className="absolute bottom-4 right-4 z-50">
<WorkflowEditorStatus
validation={validation}
nodeCount={definition.nodes.length}
@@ -16,6 +16,7 @@ export function WorkflowNodeCard({
}) {
return (
<div
data-workflow-preserve-selection
className={cn(
"group relative rounded-lg bg-[#FFFFFF] p-0.5 transition-all",
"w-[242px]",
@@ -61,6 +61,7 @@ export function WorkflowPortNodeMenu({
return (
<div
ref={menuRef}
data-workflow-preserve-selection
className="pointer-events-auto absolute z-[80] w-64 overflow-hidden rounded-lg border border-slate-200 bg-white py-1.5 shadow-[0_14px_35px_rgba(15,23,42,0.16)]"
style={{
left: position.x,
@@ -277,6 +277,21 @@ describe("workflow branch interaction helpers", () => {
},
}), 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", () => {
@@ -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[] = []