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:
5
.changeset/fn-6088-workflow-editor-selected-workflow.md
Normal file
5
.changeset/fn-6088-workflow-editor-selected-workflow.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Open the workflow editor on the selected board workflow when using the workflow-mode edit action.
|
||||
@@ -387,6 +387,7 @@ export function AppModals({
|
||||
projectId={projectId}
|
||||
initialPanel={modalManager.workflowEditorInitialPanel}
|
||||
initialAction={modalManager.workflowEditorInitialAction}
|
||||
initialWorkflowId={modalManager.workflowEditorInitialWorkflowId}
|
||||
/>
|
||||
</Suspense>
|
||||
</ModalErrorBoundary>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
Reference in New Issue
Block a user