Allow deleting access review campaigns in any status

Drop the backend status gate on campaign delete and show delete in
the console whenever the user has delete permission, regardless of
whether the campaign is draft, in progress, or completed.

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:44:13 +00:00
parent 9c1cecfc1e
commit b156b0333a
6 changed files with 3 additions and 51 deletions

View File

@@ -132,10 +132,6 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
deleteCampaignMutation,
);
// Only DRAFT, CANCELLED, and COMPLETED campaigns can be deleted (enforced by the backend).
const isDeletableStatus = (status: string) =>
status === "DRAFT" || status === "CANCELLED" || status === "COMPLETED";
const handleDelete = (campaignId: string, campaignName: string) => {
confirm(
() => {
@@ -182,9 +178,7 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
);
};
const hasActions = accessReviewCampaigns.edges.some(
edge => edge.node.canDelete && isDeletableStatus(edge.node.status),
);
const hasActions = accessReviewCampaigns.edges.some(edge => edge.node.canDelete);
return (
<div className="space-y-4">
@@ -215,8 +209,7 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
</Thead>
<Tbody>
{accessReviewCampaigns.edges.map((edge) => {
const canDeleteRow
= edge.node.canDelete && isDeletableStatus(edge.node.status);
const canDeleteRow = edge.node.canDelete;
return (
<Tr
key={edge.node.id}

View File

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

View File

@@ -205,12 +205,6 @@ func (s *Service) DeleteCampaign(
return fmt.Errorf("cannot load campaign: %w", err)
}
if campaign.Status != coredata.AccessReviewCampaignStatusDraft &&
campaign.Status != coredata.AccessReviewCampaignStatusCancelled &&
campaign.Status != coredata.AccessReviewCampaignStatusCompleted {
return NewCampaignNotDeletableError(campaign.ID)
}
if err := campaign.Delete(ctx, conn, scope); err != nil {
return fmt.Errorf("cannot delete campaign: %w", err)
}

View File

@@ -30,7 +30,6 @@ import (
var (
ErrCampaignMissingSources = errors.New("access review campaign missing scope sources")
ErrCampaignNotDraft = errors.New("access review campaign not draft")
ErrCampaignNotDeletable = errors.New("access review campaign not deletable")
ErrCampaignNotPendingActions = errors.New("access review campaign not pending actions")
ErrCampaignCompleted = errors.New("access review campaign completed")
ErrCampaignCancelled = errors.New("access review campaign cancelled")
@@ -45,10 +44,6 @@ type (
CampaignID gid.GID
}
CampaignNotDeletableError struct {
CampaignID gid.GID
}
CampaignNotPendingActionsError struct {
CampaignID gid.GID
}
@@ -89,21 +84,6 @@ func (e *CampaignNotDraftError) Is(target error) bool {
return target == ErrCampaignNotDraft
}
func NewCampaignNotDeletableError(campaignID gid.GID) error {
return &CampaignNotDeletableError{CampaignID: campaignID}
}
func (e *CampaignNotDeletableError) Error() string {
return fmt.Sprintf(
"access review campaign %q cannot be deleted unless it is draft, cancelled, or completed",
e.CampaignID,
)
}
func (e *CampaignNotDeletableError) Is(target error) bool {
return target == ErrCampaignNotDeletable
}
func NewCampaignNotPendingActionsError(campaignID gid.GID) error {
return &CampaignNotPendingActionsError{CampaignID: campaignID}
}

View File

@@ -57,15 +57,6 @@ func TestCampaignClientErrors(t *testing.T) {
wantText: fmt.Sprintf("access review campaign %q is not in draft", campaignID),
sentinel: accessreview.ErrCampaignNotDraft,
},
{
name: "not deletable",
err: accessreview.NewCampaignNotDeletableError(campaignID),
wantText: fmt.Sprintf(
"access review campaign %q cannot be deleted unless it is draft, cancelled, or completed",
campaignID,
),
sentinel: accessreview.ErrCampaignNotDeletable,
},
{
name: "not pending actions",
err: accessreview.NewCampaignNotPendingActionsError(campaignID),

View File

@@ -787,10 +787,6 @@ func (r *mutationResolver) DeleteAccessReviewCampaign(ctx context.Context, input
return nil, gqlutils.NotFound(ctx, err)
}
if errors.Is(err, accessreview.ErrCampaignNotDeletable) {
return nil, gqlutils.Invalid(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot delete access review campaign", log.Error(err))
return nil, gqlutils.Internal(ctx)