import { useEffect, useMemo, useRef, useState, type SyntheticEvent, } from "react"; import { useNavigate } from "react-router-dom"; import { useConsoleWorkspace } from "../context.js"; import { createDraftAuthoringClient, type DraftAuthoringClient, } from "../domain/draft-authoring-client.js"; import type { CapabilityDetail } from "../domain/capability-models.js"; import { useDraftWorkspace } from "../routes/useDraftWorkspace.js"; export type CreateDraftDialogProps = { readonly capability: CapabilityDetail | null; readonly onClose: () => void; }; type DialogPhase = "idle" | "saving" | "error"; const errorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error); const draftPath = (workspaceId: string, capability: CapabilityDetail | null): string => { const path = `/console/drafts/${encodeURIComponent(workspaceId)}`; return capability === null ? path : `${path}?capability=${encodeURIComponent(capability.name)}`; }; const showModal = (dialog: HTMLDialogElement): void => { if (dialog.open) return; if (typeof dialog.showModal === "function") { try { dialog.showModal(); return; } catch { // Some test DOMs expose showModal without implementing it. } } dialog.setAttribute("open", ""); }; export const CreateDraftDialog = ({ capability, onClose, }: CreateDraftDialogProps) => { const navigate = useNavigate(); const { writeExecutor } = useConsoleWorkspace(); const drafts = useDraftWorkspace(null); const dialogRef = useRef(null); const onCloseRef = useRef(onClose); const lifecycleTokenRef = useRef(null); const nextLifecycleTokenRef = useRef(0); const requestGenerationRef = useRef(0); const client = useMemo( () => (writeExecutor ? createDraftAuthoringClient(writeExecutor) : null), [writeExecutor], ); const [workspaceId, setWorkspaceId] = useState(""); const [name, setName] = useState(""); const [title, setTitle] = useState(""); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(""); const [phase, setPhase] = useState("idle"); const [message, setMessage] = useState(null); useEffect(() => { onCloseRef.current = onClose; }, [onClose]); useEffect(() => { const dialog = dialogRef.current; if (dialog === null) return; const lifecycleToken = ++nextLifecycleTokenRef.current; lifecycleTokenRef.current = lifecycleToken; const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null; showModal(dialog); dialog.querySelector("[data-dialog-autofocus]")?.focus(); return () => { if (lifecycleTokenRef.current === lifecycleToken) { lifecycleTokenRef.current = null; } requestGenerationRef.current += 1; // Do not call dialog.close() here. React Strict Mode replays this cleanup // while the same dialog remains mounted; Chrome dispatches that native // close event asynchronously and the remounted instance can mistake it // for an operator close. Removing the DOM node closes a real unmount. if (previouslyFocused !== null && document.contains(previouslyFocused)) { previouslyFocused.focus(); } }; }, []); const invalidatePendingCreate = (): void => { requestGenerationRef.current += 1; }; const isCurrentRequest = ( generation: number, lifecycleToken: number, ): boolean => lifecycleTokenRef.current === lifecycleToken && requestGenerationRef.current === generation; const requestClose = (): void => { invalidatePendingCreate(); onCloseRef.current(); }; const handleCancel = (event: SyntheticEvent): void => { event.preventDefault(); requestClose(); }; const handleDialogClose = (): void => { if (lifecycleTokenRef.current === null) return; requestClose(); }; const createDraft = async (): Promise => { if (client === null) { setPhase("error"); setMessage("Connect to a workflow server before creating a draft."); return; } const lifecycleToken = lifecycleTokenRef.current; if (lifecycleToken === null) return; setPhase("saving"); setMessage(null); const requestGeneration = requestGenerationRef.current + 1; requestGenerationRef.current = requestGeneration; try { const created = capability === null ? await client.createEmpty({ workspaceId, name, title }) : await client.createFromCapability({ workspaceId, name, title, capabilityName: capability.name, }); if (!isCurrentRequest(requestGeneration, lifecycleToken)) return; navigate(draftPath(created.workspaceId, capability)); } catch (error: unknown) { if (!isCurrentRequest(requestGeneration, lifecycleToken)) return; setPhase("error"); setMessage(errorMessage(error)); } }; return (

{capability === null ? "Draft authoring" : "Capability handoff"}

{capability === null ? "Create a draft workspace" : "Add capability to draft"}

{capability !== null && (

Use an existing draft

{drafts.listPhase === "loading" &&

Loading draft workspaces...

} {drafts.listPhase === "error" && (

{drafts.listMessage ?? "Draft workspace list failed."}

)} {drafts.listPhase === "ready" && drafts.items.length === 0 && (

No existing draft workspaces are available.

)} {drafts.items.length > 0 && ( <> )}
)}
{ event.preventDefault(); void createDraft(); }} >

{capability === null ? "New draft" : "Create seeded draft"}

setWorkspaceId(event.target.value)} required type="text" value={workspaceId} data-dialog-autofocus="true" />
setName(event.target.value)} required type="text" value={name} />
setTitle(event.target.value)} type="text" value={title} />
{phase === "error" && message !== null &&

{message}

}
); };