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:
@@ -132,6 +132,8 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
|
|||||||
deleteCampaignMutation,
|
deleteCampaignMutation,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isDeletableStatus = (status: string) => status !== "IN_PROGRESS";
|
||||||
|
|
||||||
const handleDelete = (campaignId: string, campaignName: string) => {
|
const handleDelete = (campaignId: string, campaignName: string) => {
|
||||||
confirm(
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -209,7 +213,8 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
|
|||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{accessReviewCampaigns.edges.map((edge) => {
|
{accessReviewCampaigns.edges.map((edge) => {
|
||||||
const canDeleteRow = edge.node.canDelete;
|
const canDeleteRow
|
||||||
|
= edge.node.canDelete && isDeletableStatus(edge.node.status);
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
key={edge.node.id}
|
key={edge.node.id}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export default function CampaignDetailPage({ queryRef }: Props) {
|
|||||||
const isInProgress = campaign.status === "IN_PROGRESS";
|
const isInProgress = campaign.status === "IN_PROGRESS";
|
||||||
const isDraft = campaign.status === "DRAFT";
|
const isDraft = campaign.status === "DRAFT";
|
||||||
const isPendingActions = campaign.status === "PENDING_ACTIONS";
|
const isPendingActions = campaign.status === "PENDING_ACTIONS";
|
||||||
const canDelete = campaign.canDelete;
|
const canDelete = campaign.canDelete && !isInProgress;
|
||||||
|
|
||||||
const campaignIdRef = useRef(campaign.id);
|
const campaignIdRef = useRef(campaign.id);
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,10 @@ func (s *Service) DeleteCampaign(
|
|||||||
return fmt.Errorf("cannot load campaign: %w", err)
|
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 {
|
if err := campaign.Delete(ctx, conn, scope); err != nil {
|
||||||
return fmt.Errorf("cannot delete campaign: %w", err)
|
return fmt.Errorf("cannot delete campaign: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrCampaignMissingSources = errors.New("access review campaign missing scope sources")
|
ErrCampaignMissingSources = errors.New("access review campaign missing scope sources")
|
||||||
ErrCampaignNotDraft = errors.New("access review campaign not draft")
|
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")
|
ErrCampaignNotPendingActions = errors.New("access review campaign not pending actions")
|
||||||
ErrCampaignCompleted = errors.New("access review campaign completed")
|
ErrCampaignCompleted = errors.New("access review campaign completed")
|
||||||
ErrCampaignCancelled = errors.New("access review campaign cancelled")
|
ErrCampaignCancelled = errors.New("access review campaign cancelled")
|
||||||
@@ -44,6 +45,10 @@ type (
|
|||||||
CampaignID gid.GID
|
CampaignID gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CampaignNotDeletableError struct {
|
||||||
|
CampaignID gid.GID
|
||||||
|
}
|
||||||
|
|
||||||
CampaignNotPendingActionsError struct {
|
CampaignNotPendingActionsError struct {
|
||||||
CampaignID gid.GID
|
CampaignID gid.GID
|
||||||
}
|
}
|
||||||
@@ -84,6 +89,21 @@ func (e *CampaignNotDraftError) Is(target error) bool {
|
|||||||
return target == ErrCampaignNotDraft
|
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 {
|
func NewCampaignNotPendingActionsError(campaignID gid.GID) error {
|
||||||
return &CampaignNotPendingActionsError{CampaignID: campaignID}
|
return &CampaignNotPendingActionsError{CampaignID: campaignID}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ func TestCampaignClientErrors(t *testing.T) {
|
|||||||
wantText: fmt.Sprintf("access review campaign %q is not in draft", campaignID),
|
wantText: fmt.Sprintf("access review campaign %q is not in draft", campaignID),
|
||||||
sentinel: accessreview.ErrCampaignNotDraft,
|
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",
|
name: "not pending actions",
|
||||||
err: accessreview.NewCampaignNotPendingActionsError(campaignID),
|
err: accessreview.NewCampaignNotPendingActionsError(campaignID),
|
||||||
|
|||||||
@@ -787,6 +787,10 @@ func (r *mutationResolver) DeleteAccessReviewCampaign(ctx context.Context, input
|
|||||||
return nil, gqlutils.NotFound(ctx, err)
|
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))
|
r.logger.ErrorCtx(ctx, "cannot delete access review campaign", log.Error(err))
|
||||||
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
|||||||
Reference in New Issue
Block a user