fix(sites): hold Delete until the reopened dialog's open work-order recheck lands

The open work-order query keeps its cached result between dialog opens, so on
reopen the background refetch ran while isLoading was false and Delete was
enabled with the previous count. A work order created at the site since the
last open could be deleted past without a warning.

Treat an in-flight fetch as an unknown count: Delete and View open work orders
wait, the stale copy is hidden and the checking spinner shows until the fresh
count arrives.
This commit is contained in:
Alexandre Brandizzi 2026-09-25 17:17:20 -03:00
parent 2281c7c82d
commit 7bde8aacf2
2 changed files with 43 additions and 4 deletions

View file

@ -51,8 +51,9 @@ export function SiteDeleteDialog({
refetch,
} = useSiteOpenWorkOrders(siteId, open);
const openCount = openWorkOrders?.count ?? 0;
// Without the count the warning cannot be shown, so Delete waits for a successful check.
const countUnknown = isLoading || isError;
// Without a current count the warning cannot be trusted, so Delete waits for a successful check.
// `isFetching` covers reopening: the count cached from the last open is stale until the refetch lands.
const countUnknown = isLoading || isFetching || isError;
// The server caps the id list; when it doesn't cover the stated count, open the site's whole board.
const openIds = openWorkOrders?.workOrderIds ?? [];
const openWorkOrdersHref =
@ -73,7 +74,9 @@ export function SiteDeleteDialog({
>
<DialogTitle id="site-delete-title">Delete this site?</DialogTitle>
<DialogContent>
{isLoading && <CircularProgress size={20} aria-label="Checking open work orders" />}
{isFetching && !isError && (
<CircularProgress size={20} aria-label="Checking open work orders" />
)}
{isError && <DialogContentText role="alert">{CHECK_FAILED_MESSAGE}</DialogContentText>}
{!countUnknown && (
<DialogContentText>{describeDeletion(siteCode, openCount)}</DialogContentText>
@ -88,7 +91,7 @@ export function SiteDeleteDialog({
Try again
</Button>
)}
{openCount > 0 && (
{!countUnknown && openCount > 0 && (
<Button variant="outlined" onClick={() => navigate(openWorkOrdersHref)}>
View open work orders
</Button>

View file

@ -229,6 +229,42 @@ describe("Delete a site from the detail panel", () => {
expect(deleteSite).not.toHaveBeenCalled();
});
it("holds Delete on reopen until the fresh open work-order count replaces the cached one", async () => {
let resolveRecheck: (value: { count: number; workOrderIds: number[] }) => void = () => {};
getOpenWorkOrders.mockResolvedValueOnce({ count: 0, workOrderIds: [] });
getOpenWorkOrders.mockReturnValueOnce(
new Promise((resolve) => {
resolveRecheck = resolve;
}),
);
renderAs("Admin");
const panel = await openPanel();
fireEvent.click(within(panel).getByRole("button", { name: "Delete" }));
let dialog = await screen.findByRole("dialog", { name: "Delete this site?" });
await within(dialog).findByText(
"This will permanently remove DAL1 from Sites. This cannot be undone.",
);
fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
await waitFor(() =>
expect(screen.queryByRole("dialog", { name: "Delete this site?" })).not.toBeInTheDocument(),
);
fireEvent.click(within(panel).getByRole("button", { name: "Delete" }));
dialog = await screen.findByRole("dialog", { name: "Delete this site?" });
await waitFor(() => expect(getOpenWorkOrders).toHaveBeenCalledTimes(2));
expect(within(dialog).getByRole("button", { name: "Delete" })).toBeDisabled();
expect(within(dialog).queryByText(/This cannot be undone/)).not.toBeInTheDocument();
expect(within(dialog).getByLabelText("Checking open work orders")).toBeInTheDocument();
resolveRecheck({ count: 1, workOrderIds: [101] });
expect(await within(dialog).findByText(/It still has 1 open work order —/)).toBeInTheDocument();
expect(within(dialog).getByRole("button", { name: "Delete" })).toBeEnabled();
expect(deleteSite).not.toHaveBeenCalled();
});
it("keeps the site and shows the server's refusal when the API answers 403", async () => {
getOpenWorkOrders.mockResolvedValue({ count: 0, workOrderIds: [] });
deleteSite.mockRejectedValue(forbidden());