Allow deleting completed access review campaigns

Completed campaigns were immutable because delete only accepted draft
or cancelled status. Extend deletion to completed records and surface
delete in the console for that status.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
Cursor Agent
2026-07-29 15:35:22 +00:00
parent 255bea4738
commit 9c1cecfc1e
5 changed files with 8 additions and 6 deletions

View File

@@ -132,9 +132,9 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
deleteCampaignMutation, deleteCampaignMutation,
); );
// Only DRAFT and CANCELLED campaigns can be deleted (enforced by the backend). // Only DRAFT, CANCELLED, and COMPLETED campaigns can be deleted (enforced by the backend).
const isDeletableStatus = (status: string) => const isDeletableStatus = (status: string) =>
status === "DRAFT" || status === "CANCELLED"; status === "DRAFT" || status === "CANCELLED" || status === "COMPLETED";
const handleDelete = (campaignId: string, campaignName: string) => { const handleDelete = (campaignId: string, campaignName: string) => {
confirm( confirm(

View File

@@ -217,7 +217,8 @@ export default function CampaignDetailPage({ queryRef }: Props) {
const isDraft = campaign.status === "DRAFT"; const isDraft = campaign.status === "DRAFT";
const isPendingActions = campaign.status === "PENDING_ACTIONS"; const isPendingActions = campaign.status === "PENDING_ACTIONS";
const isCancelled = campaign.status === "CANCELLED"; const isCancelled = campaign.status === "CANCELLED";
const canDelete = campaign.canDelete && (isDraft || isCancelled); const isCompleted = campaign.status === "COMPLETED";
const canDelete = campaign.canDelete && (isDraft || isCancelled || isCompleted);
const campaignIdRef = useRef(campaign.id); const campaignIdRef = useRef(campaign.id);

View File

@@ -206,7 +206,8 @@ func (s *Service) DeleteCampaign(
} }
if campaign.Status != coredata.AccessReviewCampaignStatusDraft && if campaign.Status != coredata.AccessReviewCampaignStatusDraft &&
campaign.Status != coredata.AccessReviewCampaignStatusCancelled { campaign.Status != coredata.AccessReviewCampaignStatusCancelled &&
campaign.Status != coredata.AccessReviewCampaignStatusCompleted {
return NewCampaignNotDeletableError(campaign.ID) return NewCampaignNotDeletableError(campaign.ID)
} }

View File

@@ -95,7 +95,7 @@ func NewCampaignNotDeletableError(campaignID gid.GID) error {
func (e *CampaignNotDeletableError) Error() string { func (e *CampaignNotDeletableError) Error() string {
return fmt.Sprintf( return fmt.Sprintf(
"access review campaign %q cannot be deleted unless it is draft or cancelled", "access review campaign %q cannot be deleted unless it is draft, cancelled, or completed",
e.CampaignID, e.CampaignID,
) )
} }

View File

@@ -61,7 +61,7 @@ func TestCampaignClientErrors(t *testing.T) {
name: "not deletable", name: "not deletable",
err: accessreview.NewCampaignNotDeletableError(campaignID), err: accessreview.NewCampaignNotDeletableError(campaignID),
wantText: fmt.Sprintf( wantText: fmt.Sprintf(
"access review campaign %q cannot be deleted unless it is draft or cancelled", "access review campaign %q cannot be deleted unless it is draft, cancelled, or completed",
campaignID, campaignID,
), ),
sentinel: accessreview.ErrCampaignNotDeletable, sentinel: accessreview.ErrCampaignNotDeletable,