diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.spec.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.spec.tsx index 331b3d7..7e87a93 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.spec.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.spec.tsx @@ -243,6 +243,51 @@ describe("HiveEditorPage", () => { expect(fetchHive).toHaveBeenCalledTimes(1); }); + it("moves focus to the other history button when undo or redo runs out", async () => { + renderPage(); + await addPart("cover"); + + await userEvent.click(screen.getByRole("button", { name: "Rückgängig" })); + expect(screen.getByRole("button", { name: "Rückgängig" })).toBeDisabled(); + expect(screen.getByRole("button", { name: "Wiederholen" })).toHaveFocus(); + + await userEvent.click(screen.getByRole("button", { name: "Wiederholen" })); + expect(screen.getByRole("button", { name: "Wiederholen" })).toBeDisabled(); + expect(screen.getByRole("button", { name: "Rückgängig" })).toHaveFocus(); + }); + + it("moves focus to the next row after removing a part, else the previous one", async () => { + renderPage(); + const stack = await screen.findByRole("list", { name: "Aufbau" }); + + await userEvent.click(within(stack).getByRole("button", { name: "Entfernen: Futterzarge 2" })); + expect(document.activeElement).toHaveAttribute("data-part-id", "fe"); + + // The floor insert is the last row: focus falls back to the row above (the floor). + await userEvent.click(within(stack).getByRole("button", { name: "Entfernen: Bausperre" })); + expect(document.activeElement).toHaveAttribute("data-part-id", "f"); + }); + + it("refreshes the setup on window focus and hints when the server moved on", async () => { + renderPage(); + await addPart("cover"); + const other = sampleState(); + other.stack = other.stack.filter((p) => p.id !== "b2"); + vi.mocked(fetchHive).mockResolvedValue(setupView(other, "colony-1", 4)); + const calls = vi.mocked(fetchHive).mock.calls.length; + + act(() => void window.dispatchEvent(new Event("focus"))); + + expect(await screen.findByText(/Die Beute wurde inzwischen geändert\. Beim Speichern/)).toBeInTheDocument(); + expect(vi.mocked(fetchHive).mock.calls.length).toBeGreaterThan(calls); + }); + + it("is a main landmark", async () => { + renderPage(); + + expect(await screen.findByRole("main")).toBeInTheDocument(); + }); + it("announces undo and redo", async () => { renderPage(); await addPart("cover"); diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx index 696a1cc..900879e 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/hive/page.tsx @@ -18,7 +18,9 @@ export default function HiveEditorPage() { const connectivity = useConnectivity(); const backHref = `/colonies/${encodeURIComponent(id)}`; const colony = useQuery({ queryKey: ["colonies", id], queryFn: () => fetchColony(id) }); - const hive = useQuery({ queryKey: hiveKeys.detail(id), queryFn: () => fetchHive(id), + const hive = useQuery({ + queryKey: hiveKeys.detail(id), + queryFn: () => fetchHive(id), // A colony without a hive answers 404 for good; no point retrying. retry: (count, error) => hiveErrorCode(error) !== "HIVE_NOT_FOUND" && count < 3, }); @@ -72,6 +74,7 @@ export default function HiveEditorPage() { setup={hive.data} catalog={catalog} canManageTemplates={hasPermission("hive-systems:manage")} + onRefresh={() => void hive.refetch()} /> ); } diff --git a/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx b/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx index 444413b..724e69b 100644 --- a/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx +++ b/ApiCore-Frontend/src/app/(app)/inspections/new/hive-section.tsx @@ -140,17 +140,19 @@ export function InspectionHiveSection({ catalog, errorText, }: InspectionHiveSectionProps) { - const sectionRef = useRef(null); + const headingRef = useRef(null); + const [outlineShown, setOutlineShown] = useState(false); const [announcement, setAnnouncement] = useState(""); return ( // A portal form inside the card (frame/part sheet) must never submit the enclosing inspection form.
event.stopPropagation()} > +

+ Beute-Änderungen +

{announcement}

@@ -162,6 +164,8 @@ export function InspectionHiveSection({ mode={canEdit ? "inspection" : "view"} onOpsChange={canEdit ? onOpsChange : undefined} initialOps={canEdit ? ops : undefined} + initialShowOutline={outlineShown} + onShowOutlineChange={setOutlineShown} />
{canEdit && ops.length > 0 && base && catalog ? ( @@ -171,7 +175,7 @@ export function InspectionHiveSection({ catalog={catalog} onChange={onOpsReplace} onUndone={(text) => setAnnouncement(`Rückgängig: ${text}`)} - onEmptied={() => sectionRef.current?.focus()} + onEmptied={() => headingRef.current?.focus()} /> ) : null} {errorText ? ( diff --git a/ApiCore-Frontend/src/app/(app)/inspections/new/page-hive.spec.tsx b/ApiCore-Frontend/src/app/(app)/inspections/new/page-hive.spec.tsx index bbb7a02..3c23e30 100644 --- a/ApiCore-Frontend/src/app/(app)/inspections/new/page-hive.spec.tsx +++ b/ApiCore-Frontend/src/app/(app)/inspections/new/page-hive.spec.tsx @@ -50,10 +50,16 @@ vi.mock("@/lib/hive/api", async (importOriginal) => ({ fetchHiveSystems: vi.fn(), })); -const card = vi.hoisted(() => ({ renders: [] as { mode?: string; initialOps?: unknown[] }[] })); +const card = vi.hoisted(() => ({ renders: [] as { mode?: string; initialOps?: unknown[]; initialShowOutline?: boolean }[] })); vi.mock("@/components/hive/hive-card", () => ({ - HiveCard: (props: { mode?: string; initialOps?: unknown[]; onOpsChange?: (ops: unknown[]) => void }) => { - card.renders.push({ mode: props.mode, initialOps: props.initialOps }); + HiveCard: (props: { + mode?: string; + initialOps?: unknown[]; + initialShowOutline?: boolean; + onOpsChange?: (ops: unknown[]) => void; + onShowOutlineChange?: (shown: boolean) => void; + }) => { + card.renders.push({ mode: props.mode, initialOps: props.initialOps, initialShowOutline: props.initialShowOutline }); const fill = { op: "setState", id: "fe", key: "syrupLiters", value: 2 }; const frame = { op: "setFrame", boxId: "b1", slot: 5, type: "BROOD", name: null }; const add = { @@ -73,6 +79,9 @@ vi.mock("@/components/hive/hive-card", () => ({ + @@ -217,7 +226,7 @@ describe("NewInspectionPage: section Beute", () => { await userEvent.click(screen.getByRole("button", { name: /^Rückgängig/ })); - expect(screen.getByTestId("hive-section")).toHaveFocus(); + expect(screen.getByRole("heading", { name: "Beute-Änderungen" })).toHaveFocus(); }); it("keeps focus on a row when identical ops renumber their keys", async () => { @@ -229,6 +238,16 @@ describe("NewInspectionPage: section Beute", () => { expect(screen.getByRole("button", { name: /^Rückgängig/ })).toHaveFocus(); }); + it("keeps the card's Liste toggle across the remount after Rückgängig", async () => { + renderPage(); + await userEvent.click(await screen.findByRole("button", { name: "Test Futter" })); + await userEvent.click(screen.getByRole("button", { name: "Test Liste" })); + + await userEvent.click(screen.getByRole("button", { name: /^Rückgängig/ })); + + expect(card.renders.at(-1)?.initialShowOutline).toBe(true); + }); + it("hides a refusal once the ops changed", async () => { vi.mocked(createInspection).mockRejectedValue( new ApiError(403, "Forbidden", { code: "HIVE_FORBIDDEN" }), diff --git a/ApiCore-Frontend/src/components/hive/editor/editor-toolbar.tsx b/ApiCore-Frontend/src/components/hive/editor/editor-toolbar.tsx index 54c7200..38eff09 100644 --- a/ApiCore-Frontend/src/components/hive/editor/editor-toolbar.tsx +++ b/ApiCore-Frontend/src/components/hive/editor/editor-toolbar.tsx @@ -1,6 +1,6 @@ "use client"; -import type { ReactNode } from "react"; +import type { ReactNode, Ref } from "react"; import { LayoutTemplate, Redo2, Save, Trash2, Undo2 } from "lucide-react"; import { GLASS_BUTTON, GLASS_SURFACE } from "../hive-glass"; @@ -10,6 +10,8 @@ export interface EditorToolbarProps { title: string; canUndo: boolean; canRedo: boolean; + undoRef?: Ref; + redoRef?: Ref; onUndo: () => void; onRedo: () => void; date: string; @@ -26,19 +28,20 @@ export interface EditorToolbarProps { } /** Top bar of the editor: back, title, undo/redo, date and the three ways to finish. */ -export function EditorToolbar(props: EditorToolbarProps) { +export function EditorToolbar({ undoRef, redoRef, ...props }: EditorToolbarProps) { const { date, today, hasOps, saving } = props; const dateValid = /^\d{4}-\d{2}-\d{2}$/.test(date) && date <= today; return (
{props.back}

{props.title}

-