Block delete for in-progress access review campaigns

Reject deletion while a campaign is fetching sources so workers are
not racing a removed record. All other statuses remain deletable.

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:48:41 +00:00
parent b156b0333a
commit b229da0115
6 changed files with 45 additions and 3 deletions

View File

@@ -132,6 +132,8 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
deleteCampaignMutation,
);
const isDeletableStatus = (status: string) => status !== "IN_PROGRESS";
const handleDelete = (campaignId: string, campaignName: string) => {
confirm(
() => {
@@ -178,7 +180,9 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
);
};
const hasActions = accessReviewCampaigns.edges.some(edge => edge.node.canDelete);
const hasActions = accessReviewCampaigns.edges.some(
edge => edge.node.canDelete && isDeletableStatus(edge.node.status),
);
return (
<div className="space-y-4">
@@ -209,7 +213,8 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
</Thead>
<Tbody>
{accessReviewCampaigns.edges.map((edge) => {
const canDeleteRow = edge.node.canDelete;
const canDeleteRow
= edge.node.canDelete && isDeletableStatus(edge.node.status);
return (
<Tr
key={edge.node.id}

View File

@@ -216,7 +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 canDelete = campaign.canDelete;
const canDelete = campaign.canDelete && !isInProgress;
const campaignIdRef = useRef(campaign.id);

View File

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

View File

@@ -30,6 +30,7 @@ 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")
@@ -44,6 +45,10 @@ type (
CampaignID gid.GID
}
CampaignNotDeletableError struct {
CampaignID gid.GID
}
CampaignNotPendingActionsError struct {
CampaignID gid.GID
}
@@ -84,6 +89,21 @@ 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 while it is in progress",
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,6 +57,15 @@ 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 while it is in progress",
campaignID,
),
sentinel: accessreview.ErrCampaignNotDeletable,
},
{
name: "not pending actions",
err: accessreview.NewCampaignNotPendingActionsError(campaignID),

View File

@@ -787,6 +787,10 @@ 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)