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
This commit is contained in:
gsxdsm
2026-06-09 10:57:19 -07:00
parent 47e82408af
commit fc33a425fc
4 changed files with 74 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Open the workflow editor on the selected board workflow when using the workflow-mode edit action.

View File

@@ -387,6 +387,7 @@ export function AppModals({
projectId={projectId}
initialPanel={modalManager.workflowEditorInitialPanel}
initialAction={modalManager.workflowEditorInitialAction}
initialWorkflowId={modalManager.workflowEditorInitialWorkflowId}
/>
</Suspense>
</ModalErrorBoundary>

View File

@@ -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<WorkflowNodeEditorProps, "isOpen"> & { modalRef: React.RefObject<HTMLDivElement | null> }) {
const [workflows, setWorkflows] = useState<WorkflowDefinition[]>([]);
@@ -704,6 +707,7 @@ function InnerEditor({
// "New workflow" button (NewTaskModal focus pattern).
const [createOpen, setCreateOpen] = useState(initialAction === "create");
const newWorkflowBtnRef = useRef<HTMLButtonElement>(null);
const mobileInitialWorkflowDismissedRef = useRef<string | null>(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<HTMLDivElement>(null);
useModalResizePersist(modalRef, isOpen, "fusion:workflow-node-editor-size");
if (!isOpen) return null;
return (
<ReactFlowProvider>
<InnerEditor {...rest} modalRef={modalRef} />
<InnerEditor
onClose={onClose}
addToast={addToast}
projectId={projectId}
initialPanel={initialPanel}
initialAction={initialAction}
initialWorkflowId={initialWorkflowId}
modalRef={modalRef}
/>
</ReactFlowProvider>
);
}

View File

@@ -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(<WorkflowNodeEditor isOpen onClose={() => {}} 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(<WorkflowNodeEditor isOpen onClose={() => {}} 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(<WorkflowNodeEditor isOpen onClose={() => {}} 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()]);