From fc33a425fc2a312f92d394ebe5c4d798ee6a1110 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 9 Jun 2026 10:57:19 -0700 Subject: [PATCH] FN-6088: open workflow editor on selected workflow Open workflow-mode edits on the workflow the user already selected. - pass the workflow editor's initial workflow id through AppModals - preselect the matching workflow in WorkflowNodeEditor before defaulting to the first workflow - skip the mobile workflow list stage when a valid initial workflow is already selected - add dashboard tests for desktop selection, missing-id fallback, and mobile deep-link behavior - add a patch changeset for @runfusion/fusion Files changed: .../fn-6088-workflow-editor-selected-workflow.md | 5 +++ packages/dashboard/app/components/AppModals.tsx | 1 + .../app/components/WorkflowNodeEditor.tsx | 37 ++++++++++++++++++++-- .../__tests__/WorkflowNodeEditor.test.tsx | 34 ++++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6088 Fusion-Task-Lineage: 88a0cd03-2112-4706-b6b5-4cbebdcae234 --- ...-6088-workflow-editor-selected-workflow.md | 5 +++ .../dashboard/app/components/AppModals.tsx | 1 + .../app/components/WorkflowNodeEditor.tsx | 37 +++++++++++++++++-- .../__tests__/WorkflowNodeEditor.test.tsx | 34 +++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-6088-workflow-editor-selected-workflow.md diff --git a/.changeset/fn-6088-workflow-editor-selected-workflow.md b/.changeset/fn-6088-workflow-editor-selected-workflow.md new file mode 100644 index 0000000000..143027f36a --- /dev/null +++ b/.changeset/fn-6088-workflow-editor-selected-workflow.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Open the workflow editor on the selected board workflow when using the workflow-mode edit action. diff --git a/packages/dashboard/app/components/AppModals.tsx b/packages/dashboard/app/components/AppModals.tsx index ca2370ccf0..61805592a1 100644 --- a/packages/dashboard/app/components/AppModals.tsx +++ b/packages/dashboard/app/components/AppModals.tsx @@ -387,6 +387,7 @@ export function AppModals({ projectId={projectId} initialPanel={modalManager.workflowEditorInitialPanel} initialAction={modalManager.workflowEditorInitialAction} + initialWorkflowId={modalManager.workflowEditorInitialWorkflowId} /> diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.tsx b/packages/dashboard/app/components/WorkflowNodeEditor.tsx index 0fe14d7ac8..cb44b6c247 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.tsx +++ b/packages/dashboard/app/components/WorkflowNodeEditor.tsx @@ -176,6 +176,8 @@ interface WorkflowNodeEditorProps { initialPanel?: "settings"; /** When "create" the editor opens with the new-workflow dialog active. */ initialAction?: "create"; + /** Workflow id to preselect when the editor opens from workflow-aware surfaces. */ + initialWorkflowId?: string; } let nodeSeq = 0; @@ -671,6 +673,7 @@ function InnerEditor({ projectId, initialPanel, initialAction, + initialWorkflowId, modalRef, }: Omit & { modalRef: React.RefObject }) { const [workflows, setWorkflows] = useState([]); @@ -704,6 +707,7 @@ function InnerEditor({ // "New workflow" button (NewTaskModal focus pattern). const [createOpen, setCreateOpen] = useState(initialAction === "create"); const newWorkflowBtnRef = useRef(null); + const mobileInitialWorkflowDismissedRef = useRef(null); // Inline-editable name/description (KTD-10). `name`/`description` mirror the // active workflow and are persisted through handleSave; `editingName`/ // `editingDescription` flag the active inline input. @@ -1006,6 +1010,9 @@ function InnerEditor({ setWorkflows(data); setActiveId((prev) => { if (prev && data.some((workflow) => workflow.id === prev)) return prev; + if (initialAction !== "create" && initialWorkflowId && data.some((workflow) => workflow.id === initialWorkflowId)) { + return initialWorkflowId; + } return isMobileViewport ? null : data[0]?.id ?? null; }); } catch (err) { @@ -1013,12 +1020,20 @@ function InnerEditor({ } finally { setLoading(false); } - }, [projectId, addToast, isMobileViewport]); + }, [projectId, addToast, isMobileViewport, initialAction, initialWorkflowId]); useEffect(() => { void loadWorkflows(); }, [loadWorkflows]); + useEffect(() => { + if (!initialWorkflowId || !isMobileViewport || !workflowListStageOpen) return; + if (activeId !== initialWorkflowId) return; + if (mobileInitialWorkflowDismissedRef.current === initialWorkflowId) return; + mobileInitialWorkflowDismissedRef.current = initialWorkflowId; + setWorkflowListStageOpen(false); + }, [activeId, initialWorkflowId, isMobileViewport, workflowListStageOpen]); + // U2/R5: fire the lazy legacy-step migration once on editor open, then reload // the workflow list so any newly created fragments / "Migrated steps" workflow // appear. Non-fatal on ANY error (incl. 404 if the route ships in a later @@ -4126,13 +4141,29 @@ function InnerEditor({ ); } -export function WorkflowNodeEditor({ isOpen, ...rest }: WorkflowNodeEditorProps) { +export function WorkflowNodeEditor({ + isOpen, + onClose, + addToast, + projectId, + initialPanel, + initialAction, + initialWorkflowId, +}: WorkflowNodeEditorProps) { const modalRef = useRef(null); useModalResizePersist(modalRef, isOpen, "fusion:workflow-node-editor-size"); if (!isOpen) return null; return ( - + ); } diff --git a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx index e125757971..2d26e30670 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx @@ -373,6 +373,40 @@ describe("WorkflowNodeEditor", () => { expect(screen.getByTestId("wf-mobile-tab-actions")).toBeInTheDocument(); }); + it("preselects the matching initial workflow id on desktop", async () => { + vi.mocked(fetchWorkflows).mockResolvedValue([def(), v2Def()]); + + render( {}} addToast={() => {}} initialWorkflowId="WF-002" />); + + expect(await screen.findByTestId("wf-workflow-name")).toHaveTextContent("Custom"); + expect(screen.queryByTestId("wf-mobile-select-note")).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "QA" })).not.toHaveClass("active"); + expect(screen.getAllByRole("button", { name: "Custom" })[0]).toHaveClass("active"); + }); + + it("falls back to the first workflow on desktop when the initial workflow id is missing", async () => { + vi.mocked(fetchWorkflows).mockResolvedValue([def(), v2Def()]); + + render( {}} addToast={() => {}} initialWorkflowId="WF-missing" />); + + expect(await screen.findByTestId("wf-workflow-name")).toHaveTextContent("QA"); + expect(screen.queryByTestId("wf-mobile-select-note")).not.toBeInTheDocument(); + expect(screen.getAllByRole("button", { name: "QA" })[0]).toHaveClass("active"); + expect(screen.getByRole("button", { name: "Custom" })).not.toHaveClass("active"); + }); + + it("skips the mobile workflow list stage when the initial workflow id is valid", async () => { + mockWorkflowEditorViewport("mobile"); + vi.mocked(fetchWorkflows).mockResolvedValue([def(), v2Def()]); + + render( {}} addToast={() => {}} initialWorkflowId="WF-002" />); + + expect(await screen.findByTestId("wf-workflow-name")).toHaveTextContent("Custom"); + expect(screen.queryByTestId("wf-mobile-select-note")).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "QA" })).not.toHaveClass("active"); + expect(screen.getAllByRole("button", { name: "Custom" })[0]).toHaveClass("active"); + }); + it("opens populated mobile workflows on the list with no preselected workflow", async () => { mockWorkflowEditorViewport("mobile"); vi.mocked(fetchWorkflows).mockResolvedValue([def(), v2Def()]);