feat(stock): beforeunload warning when pricing draft is dirty
This commit is contained in:
parent
7aaed60860
commit
9f27e9d4f1
2 changed files with 38 additions and 0 deletions
|
|
@ -428,4 +428,25 @@ describe("StockListPage", () => {
|
||||||
expect(await screen.findByText("Missing both")).toBeInTheDocument();
|
expect(await screen.findByText("Missing both")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Stock list — beforeunload while dirty", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
_resetState();
|
||||||
|
_state.items = [
|
||||||
|
{ id: "aaaaaaaaaaaaaaa", sku: "B-001", name: "M10 Hex Bolt", selling_price: 0, cost_price: 0, markup: 0, current_qty: 5, min_qty: 0, is_active: true, supplier: "" },
|
||||||
|
];
|
||||||
|
window.localStorage.clear();
|
||||||
|
});
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
it("registers a beforeunload listener when there are dirty rows", async () => {
|
||||||
|
const addSpy = vi.spyOn(window, "addEventListener");
|
||||||
|
render(<Page />);
|
||||||
|
await screen.findByText("M10 Hex Bolt"); // wait for items
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Edit pricing/i }));
|
||||||
|
const costInput = await screen.findByLabelText(/Cost for B-001/i);
|
||||||
|
fireEvent.change(costInput, { target: { value: "5" } });
|
||||||
|
expect(addSpy).toHaveBeenCalledWith("beforeunload", expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,23 @@ export default function StockListPage() {
|
||||||
}
|
}
|
||||||
}, [pricingDraft]);
|
}, [pricingDraft]);
|
||||||
|
|
||||||
|
// Warn the operator before they navigate away with unsaved pricing
|
||||||
|
// changes. Only registered when edit mode is on AND there's actually
|
||||||
|
// something in the draft — the bar already gates the apply button
|
||||||
|
// on the same condition, so a Cmd-W / tab close here would silently
|
||||||
|
// lose the work otherwise.
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const isDirty = Object.keys(pricingDraft).length > 0 && editPricing;
|
||||||
|
if (!isDirty) return;
|
||||||
|
const handler = (e: BeforeUnloadEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = "";
|
||||||
|
};
|
||||||
|
window.addEventListener("beforeunload", handler);
|
||||||
|
return () => window.removeEventListener("beforeunload", handler);
|
||||||
|
}, [pricingDraft, editPricing]);
|
||||||
|
|
||||||
// Debounce the search input → 250ms after the last keystroke. Same
|
// Debounce the search input → 250ms after the last keystroke. Same
|
||||||
// pattern as /orders and /quotes — the operator is hunting for a
|
// pattern as /orders and /quotes — the operator is hunting for a
|
||||||
// match, not paging through.
|
// match, not paging through.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue