fix(sites): block delete until the open work-order check succeeds

This commit is contained in:
Alexandre Brandizzi 2026-09-25 12:24:16 -03:00
parent 3ab9c9a65b
commit 9bb0416346
2 changed files with 43 additions and 3 deletions

View file

@ -21,6 +21,9 @@ interface SiteDeleteDialogProps {
onDeleted: () => void;
}
const CHECK_FAILED_MESSAGE =
"Couldn't check this site for open work orders. Try again before deleting it.";
function describeDeletion(siteCode: string, openCount: number): string {
if (openCount === 0) {
return `This will permanently remove ${siteCode} from Sites. This cannot be undone.`;
@ -39,8 +42,16 @@ export function SiteDeleteDialog({
}: SiteDeleteDialogProps) {
const navigate = useNavigate();
const deleteLocation = useDeleteLocation();
const { data: openWorkOrders, isLoading } = useSiteOpenWorkOrders(siteId, open);
const {
data: openWorkOrders,
isLoading,
isError,
isFetching,
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;
return (
<Dialog
@ -56,7 +67,8 @@ export function SiteDeleteDialog({
<DialogTitle id="site-delete-title">Delete this site?</DialogTitle>
<DialogContent>
{isLoading && <CircularProgress size={20} aria-label="Checking open work orders" />}
{!isLoading && (
{isError && <DialogContentText role="alert">{CHECK_FAILED_MESSAGE}</DialogContentText>}
{!countUnknown && (
<DialogContentText>{describeDeletion(siteCode, openCount)}</DialogContentText>
)}
</DialogContent>
@ -64,6 +76,11 @@ export function SiteDeleteDialog({
<Button variant="text" onClick={onClose} disabled={deleteLocation.isPending}>
Cancel
</Button>
{isError && (
<Button variant="outlined" disabled={isFetching} onClick={() => void refetch()}>
Try again
</Button>
)}
{openCount > 0 && (
<Button
variant="outlined"
@ -75,7 +92,7 @@ export function SiteDeleteDialog({
<Button
variant="contained"
color="error"
disabled={isLoading || deleteLocation.isPending}
disabled={countUnknown || deleteLocation.isPending}
onClick={() => deleteLocation.mutate(siteId, { onSuccess: onDeleted })}
>
{deleteLocation.isPending ? "Deleting..." : "Delete"}

View file

@ -173,6 +173,29 @@ describe("Delete a site from the detail panel", () => {
);
});
it("blocks Delete when the open work-order check fails, until a retry succeeds", async () => {
getOpenWorkOrders.mockRejectedValueOnce(new Error("Server error"));
getOpenWorkOrders.mockResolvedValueOnce({ count: 2, workOrderIds: [101, 205] });
renderAs("Admin");
const panel = await openPanel();
fireEvent.click(within(panel).getByRole("button", { name: "Delete" }));
const dialog = await screen.findByRole("dialog", { name: "Delete this site?" });
expect(await within(dialog).findByRole("alert")).toHaveTextContent(
"Couldn't check this site for open work orders. Try again before deleting it.",
);
expect(within(dialog).getByRole("button", { name: "Delete" })).toBeDisabled();
expect(within(dialog).queryByText(/This cannot be undone/)).not.toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: "Try again" }));
expect(await within(dialog).findByText(/It still has 2 open work orders/)).toBeInTheDocument();
expect(within(dialog).getByRole("button", { name: "Delete" })).toBeEnabled();
expect(within(dialog).getByRole("button", { name: "View open work orders" })).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());