fix: close Task 7 authoring review gaps
This commit is contained in:
@@ -59,6 +59,7 @@ const detail: CapabilityDetail = {
|
||||
const controller = {
|
||||
draft,
|
||||
selection: { kind: "node", nodeId: "read" },
|
||||
insertionContext: null,
|
||||
dirty: false,
|
||||
phase: "idle",
|
||||
message: null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||
import { act, cleanup, render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CapabilityDetail } from "../domain/capability-models.js";
|
||||
@@ -63,7 +63,10 @@ const setViewport = (width: number): void => {
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => cleanup());
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
setViewport(1024);
|
||||
mockedUseAuthoringCapabilityDetail.mockReturnValue({
|
||||
@@ -195,4 +198,24 @@ describe("DraftWorkbench", () => {
|
||||
).toHaveValue(" locally edited");
|
||||
expect(container.querySelector(".draft-workbench")).toHaveAttribute("data-dirty", "true");
|
||||
});
|
||||
|
||||
it("reopens an open desktop sheet as a modal after resizing to mobile", async () => {
|
||||
setViewport(1024);
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<DraftWorkbench draft={workspace} />);
|
||||
const inspector = container.querySelector("#draft-workbench-inspector") as HTMLDialogElement;
|
||||
const showModal = vi.fn(() => inspector.setAttribute("open", ""));
|
||||
Object.defineProperty(inspector, "showModal", { configurable: true, value: showModal });
|
||||
Object.defineProperty(inspector, "close", {
|
||||
configurable: true,
|
||||
value: vi.fn(() => inspector.removeAttribute("open")),
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Open context inspector" }));
|
||||
setViewport(390);
|
||||
await act(async () => window.dispatchEvent(new Event("resize")));
|
||||
|
||||
expect(showModal).toHaveBeenCalledTimes(1);
|
||||
expect(inspector).toHaveAttribute("open", "");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ const MobileSheet = ({
|
||||
}: MobileSheetProps) => {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const wasOpen = useRef(open);
|
||||
const modalRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
@@ -72,6 +73,7 @@ const MobileSheet = ({
|
||||
} else {
|
||||
dialog.removeAttribute("open");
|
||||
}
|
||||
modalRef.current = false;
|
||||
};
|
||||
|
||||
if (!isMobile) {
|
||||
@@ -84,9 +86,16 @@ const MobileSheet = ({
|
||||
if (!dialog.open) {
|
||||
if (typeof dialog.showModal === "function") {
|
||||
dialog.showModal();
|
||||
modalRef.current = true;
|
||||
} else {
|
||||
dialog.setAttribute("open", "");
|
||||
}
|
||||
} else if (!modalRef.current && typeof dialog.showModal === "function") {
|
||||
// A desktop sheet is an open nonmodal dialog; close it before moving
|
||||
// it into the mobile top layer so native inertness and focus trapping apply.
|
||||
closeDialog();
|
||||
dialog.showModal();
|
||||
modalRef.current = true;
|
||||
}
|
||||
} else {
|
||||
closeDialog();
|
||||
|
||||
@@ -161,6 +161,26 @@ describe("useDraftAuthoring", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves selected connector context when capability selection follows", async () => {
|
||||
const initial = workspace();
|
||||
authoringClient.addCapabilityStep.mockResolvedValue(workspace({ revision: 4 }));
|
||||
const { result, rerender } = renderHook(
|
||||
({ selection }) => useDraftAuthoring({ draft: initial, initialSelection: selection }),
|
||||
{
|
||||
initialProps: {
|
||||
selection: { kind: "edge", stepId: "read", outcome: "ok" } as WorkbenchSelection,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
rerender({ selection: { kind: "capability", qualifiedName: "demo.enrich" } });
|
||||
await act(async () => result.current.addCapability(capabilityInput));
|
||||
|
||||
expect(authoringClient.addCapabilityStep).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ routeFromStep: "read", routeFromOutcome: "ok" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("updates capabilities, replaces routes, and validates against the current revision", async () => {
|
||||
const initial = workspace({ revision: 7 });
|
||||
authoringClient.updateCapabilityStep.mockResolvedValue(workspace({ revision: 8 }));
|
||||
|
||||
@@ -11,13 +11,18 @@ import {
|
||||
import type { DraftWorkspace } from "../domain/draft-workspace-models.js";
|
||||
import type { CapabilityNodeFormValue } from "./CapabilityNodeForm.js";
|
||||
import type { RouteFormValue } from "./RouteForm.js";
|
||||
import { deriveInsertionContext, type WorkbenchSelection } from "./authoring-graph.js";
|
||||
import {
|
||||
deriveInsertionContext,
|
||||
type InsertionContext,
|
||||
type WorkbenchSelection,
|
||||
} from "./authoring-graph.js";
|
||||
|
||||
export type DraftAuthoringPhase = "idle" | "saving" | "conflict" | "error";
|
||||
|
||||
export interface DraftAuthoringController {
|
||||
readonly draft: DraftWorkspace;
|
||||
readonly selection: WorkbenchSelection;
|
||||
readonly insertionContext: InsertionContext | null;
|
||||
readonly dirty: boolean;
|
||||
readonly phase: DraftAuthoringPhase;
|
||||
readonly message: string | null;
|
||||
@@ -48,6 +53,7 @@ type AuthoringState = {
|
||||
readonly draftInput: DraftWorkspace;
|
||||
readonly selection: WorkbenchSelection;
|
||||
readonly selectionInput: WorkbenchSelection;
|
||||
readonly insertionContext: InsertionContext | null;
|
||||
readonly dirty: boolean;
|
||||
readonly phase: DraftAuthoringPhase;
|
||||
readonly message: string | null;
|
||||
@@ -134,6 +140,7 @@ export const useDraftAuthoring = ({
|
||||
draftInput: initialDraft,
|
||||
selection: initialSelection,
|
||||
selectionInput: initialSelection,
|
||||
insertionContext: deriveInsertionContext(initialSelection),
|
||||
dirty: false,
|
||||
phase: "idle",
|
||||
message: null,
|
||||
@@ -148,6 +155,10 @@ export const useDraftAuthoring = ({
|
||||
const draft = adoptsDraftInput ? initialDraft : state.draft;
|
||||
const adoptsSelectionInput = !sameSelection(state.selectionInput, initialSelection);
|
||||
const selection = adoptsSelectionInput ? initialSelection : state.selection;
|
||||
const insertionContext =
|
||||
adoptsSelectionInput && initialSelection.kind === "edge"
|
||||
? deriveInsertionContext(initialSelection)
|
||||
: state.insertionContext;
|
||||
const resetGeneration =
|
||||
state.resetGeneration + (adoptsDraftInput || adoptsSelectionInput ? 1 : 0);
|
||||
const currentProvenance: Provenance = useMemo(() => ({
|
||||
@@ -157,20 +168,26 @@ export const useDraftAuthoring = ({
|
||||
readExecutor,
|
||||
}), [connectedTarget, draft.workspaceId, readExecutor, writeExecutor]);
|
||||
const currentDraftRef = useRef(draft);
|
||||
const currentSelectionRef = useRef(selection);
|
||||
const currentInsertionContextRef = useRef(insertionContext);
|
||||
const currentProvenanceRef = useRef(currentProvenance);
|
||||
|
||||
useEffect(() => {
|
||||
currentDraftRef.current = draft;
|
||||
currentSelectionRef.current = selection;
|
||||
currentInsertionContextRef.current = insertionContext;
|
||||
currentProvenanceRef.current = currentProvenance;
|
||||
}, [currentProvenance, draft, selection]);
|
||||
}, [currentProvenance, draft, insertionContext, selection]);
|
||||
|
||||
const select = useCallback((selection: WorkbenchSelection): void => {
|
||||
setState((current) => ({
|
||||
...current,
|
||||
selection,
|
||||
selectionInput: initialSelection,
|
||||
insertionContext:
|
||||
selection.kind === "edge"
|
||||
? deriveInsertionContext(selection)
|
||||
: selection.kind === "capability"
|
||||
? current.insertionContext
|
||||
: null,
|
||||
}));
|
||||
}, [initialSelection]);
|
||||
|
||||
@@ -197,6 +214,10 @@ export const useDraftAuthoring = ({
|
||||
response.status === "conflict"
|
||||
? current.selection
|
||||
: nextSelection ?? current.selection,
|
||||
insertionContext:
|
||||
response.status === "conflict" || nextSelection?.kind !== "node"
|
||||
? current.insertionContext
|
||||
: null,
|
||||
dirty: response.status === "conflict" ? true : false,
|
||||
phase: response.status === "conflict" ? "conflict" : "idle",
|
||||
message:
|
||||
@@ -278,7 +299,7 @@ export const useDraftAuthoring = ({
|
||||
const addCapability = useCallback(
|
||||
(input: CapabilityNodeFormValue): Promise<void> => {
|
||||
lastSubmissionRef.current = { kind: "add", input };
|
||||
const insertion = deriveInsertionContext(currentSelectionRef.current);
|
||||
const insertion = currentInsertionContextRef.current;
|
||||
return runMutation(
|
||||
"add",
|
||||
{ input, insertion },
|
||||
@@ -445,6 +466,7 @@ export const useDraftAuthoring = ({
|
||||
return {
|
||||
draft,
|
||||
selection,
|
||||
insertionContext,
|
||||
dirty: state.dirty,
|
||||
phase: state.phase,
|
||||
message: state.message,
|
||||
|
||||
Reference in New Issue
Block a user