drawings: on a rev mismatch, Clerical can keep the retired rev VISUAL calls (#41)
ci / server (pull_request) Failing after 17s
ci / frontend (pull_request) Successful in 42s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Skipped

When VISUAL calls a rev that M-Files has retired, the draft is still
flagged rev_mismatch, but the drawing job now also copies the rev VISUAL
calls (MFilesKit 0.1.3 DrawingLookup.revision) into drawing_revisions and
visual.called. A Clerical Supervisor picks either rev on the packet:
"Use latest rev N" as before, or "Use rev V as VISUAL calls it"
(rev-approve with use=called), with a reason and a signature.

The kept rev holds while M-Files' Effective rev stays the one chosen
against: no flag, no change order. A newer Effective rev flags the
packet again; a new VISUAL rev clears the choice.

Migration 0009 adds drawing_revisions. Needs mfileskit 0.1.3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 13:39:14 -06:00
co-authored by Claude Opus 5.5
parent 4c0cba4da5
commit bb8eb17748
21 changed files with 547 additions and 55 deletions
+25
View File
@@ -116,6 +116,31 @@ describe("Packet page, VISUAL box", () => {
expect(await screen.findByText("Rev 3 approved and attached.")).toBeInTheDocument();
});
it("offers the retired rev VISUAL calls next to the latest, and keeps it with use=called", async () => {
const called = { rev: "2", controlled: "Retired", state: "Superseded", name: "M00543 r2.pdf", asset_id: "as-r2", pages: 1 };
const draft = visualDraft({ visual: { ...visualDraft().visual, staged_rev: { rev: 3, name: "M00543 r3.pdf", asset_id: "as-r3" }, called } });
const kept = visualDraft({
version: 4,
flags: [],
flag: null,
drawing_state: "attached",
visual: { rev_ok: { rev: "2", vis_rev: "2", use: "called", latest: "3", controlled: "Retired", reason: "Lot 1 builds to rev 2" } },
});
const fetch = mockFetch({ ...base(draft, LEO), "POST /api/packets/1/rev-approve": { body: kept } });
renderApp("/packets/1");
expect(await screen.findByRole("button", { name: "Use latest rev 3" })).toBeInTheDocument();
expect(screen.getByText(/retired \(Superseded\) in M-Files/)).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Use rev 2 as VISUAL calls it" }));
const ask = await screen.findByRole("dialog", { name: "Use rev 2 for M00543" });
fireEvent.change(within(ask).getByLabelText("Reason"), { target: { value: "Lot 1 builds to rev 2" } });
fireEvent.click(within(ask).getByRole("button", { name: "Use rev 2" }));
await sign();
expect(callsTo(fetch, "POST", "/api/packets/1/rev-approve")).toEqual([
{ reason: "Lot 1 builds to rev 2", use: "called", sign: { factor: "password", secret: "pw-1" } },
]);
expect(await screen.findByText("Rev 2 attached as VISUAL calls it.")).toBeInTheDocument();
});
it("uploads a hand BOM as multipart with the signature fields", async () => {
const fetch = mockFetch({
...base(visualDraft()),