fix(ui): use mutation for attachment delete

Adopt a React Query mutation for policy attachment deletion and add a pending-state test on the policies index panel. This removes local delete-loading state and keeps modal loading tied to mutation status.

Made-with: Cursor
This commit is contained in:
Lucas Song 2026-04-09 01:19:54 -07:00
parent 727a6f21b8
commit 0ba8adffed
2 changed files with 55 additions and 11 deletions

View file

@ -182,4 +182,39 @@ describe("PoliciesPanel attachment delete", () => {
expect(networkingMocks.deletePolicyAttachmentCall).toHaveBeenCalledWith("test-token", EXPECTED_ATTACHMENT_ID);
});
});
it("should show mutation pending state while attachment delete is in flight", async () => {
let resolveDelete: (() => void) | undefined;
const deletePromise = new Promise<void>((resolve) => {
resolveDelete = resolve;
});
networkingMocks.deletePolicyAttachmentCall.mockImplementationOnce(() => deletePromise);
const user = userEvent.setup();
renderWithProviders(<PoliciesPanel accessToken="test-token" userRole="Admin" />);
await waitFor(() => {
expect(networkingMocks.getPolicyAttachmentsList).toHaveBeenCalled();
});
await user.click(screen.getByRole("tab", { name: /^attachments$/i }));
await waitFor(() => {
expect(screen.getByText("test-policy")).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
const dialog = await screen.findByRole("dialog", {}, { timeout: 5000 });
const deleteButton = within(dialog).getByRole("button", { name: /^delete$/i });
await user.click(deleteButton);
await waitFor(() => {
expect(within(dialog).getByRole("button", { name: /deleting/i })).toBeDisabled();
});
resolveDelete?.();
await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});
});

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from "react";
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import { Alert } from "antd";
import { useMutation } from "@tanstack/react-query";
import MessageManager from "@/components/molecules/message_manager";
import { InfoCircleOutlined } from "@ant-design/icons";
import { isAdminRole } from "@/utils/roles";
@ -57,7 +58,6 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
const [isDeleting, setIsDeleting] = useState(false);
const [policyToDelete, setPolicyToDelete] = useState<Policy | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isDeletingAttachment, setIsDeletingAttachment] = useState(false);
const [attachmentToDelete, setAttachmentToDelete] = useState<PolicyAttachment | null>(null);
const [isDeleteAttachmentModalOpen, setIsDeleteAttachmentModalOpen] = useState(false);
const [isGuardrailSelectionModalOpen, setIsGuardrailSelectionModalOpen] = useState(false);
@ -169,6 +169,22 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
setPolicyToDelete(null);
};
const deleteAttachmentMutation = useMutation({
mutationFn: async (attachmentId: string) => {
if (!accessToken) {
throw new Error("Access token is required");
}
return deletePolicyAttachmentCall(accessToken, attachmentId);
},
onSuccess: async () => {
MessageManager.success("Attachment deleted successfully");
await fetchAttachments();
},
onError: (error) => {
console.error("Error deleting attachment:", error);
MessageManager.error("Failed to delete attachment");
},
});
const handleDeleteAttachmentClick = (attachmentId: string) => {
const attachment = attachmentsList.find((a) => a.attachment_id === attachmentId) || null;
setAttachmentToDelete(attachment);
@ -181,17 +197,10 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
};
const handleAttachmentDeleteConfirm = async () => {
if (!attachmentToDelete || !accessToken) return;
setIsDeletingAttachment(true);
if (!attachmentToDelete) return;
try {
await deletePolicyAttachmentCall(accessToken, attachmentToDelete.attachment_id);
MessageManager.success("Attachment deleted successfully");
await fetchAttachments();
} catch (error) {
console.error("Error deleting attachment:", error);
MessageManager.error("Failed to delete attachment");
await deleteAttachmentMutation.mutateAsync(attachmentToDelete.attachment_id);
} finally {
setIsDeletingAttachment(false);
setIsDeleteAttachmentModalOpen(false);
setAttachmentToDelete(null);
}
@ -621,7 +630,7 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
]}
onCancel={handleAttachmentDeleteCancel}
onOk={handleAttachmentDeleteConfirm}
confirmLoading={isDeletingAttachment}
confirmLoading={deleteAttachmentMutation.isPending}
/>
<AiSuggestionModal