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:
@@ -132,10 +132,6 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
|
|||||||
deleteCampaignMutation,
|
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) => {
|
const handleDelete = (campaignId: string, campaignName: string) => {
|
||||||
confirm(
|
confirm(
|
||||||
() => {
|
() => {
|
||||||
@@ -182,9 +178,7 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasActions = accessReviewCampaigns.edges.some(
|
const hasActions = accessReviewCampaigns.edges.some(edge => edge.node.canDelete);
|
||||||
edge => edge.node.canDelete && isDeletableStatus(edge.node.status),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -215,8 +209,7 @@ export default function AccessReviewCampaignsTab({ queryRef }: Props) {
|
|||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{accessReviewCampaigns.edges.map((edge) => {
|
{accessReviewCampaigns.edges.map((edge) => {
|
||||||
const canDeleteRow
|
const canDeleteRow = edge.node.canDelete;
|
||||||
= edge.node.canDelete && isDeletableStatus(edge.node.status);
|
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
key={edge.node.id}
|
key={edge.node.id}
|
||||||
|
|||||||
@@ -216,9 +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 isCancelled = campaign.status === "CANCELLED";
|
const canDelete = campaign.canDelete;
|
||||||
const isCompleted = campaign.status === "COMPLETED";
|
|
||||||
const canDelete = campaign.canDelete && (isDraft || isCancelled || isCompleted);
|
|
||||||
|
|
||||||
const campaignIdRef = useRef(campaign.id);
|
const campaignIdRef = useRef(campaign.id);
|
||||||
|
|
||||||
|
|||||||
@@ -205,12 +205,6 @@ func (s *Service) DeleteCampaign(
|
|||||||
return fmt.Errorf("cannot load campaign: %w", err)
|
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 {
|
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,7 +30,6 @@ 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")
|
||||||
@@ -45,10 +44,6 @@ type (
|
|||||||
CampaignID gid.GID
|
CampaignID gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
CampaignNotDeletableError struct {
|
|
||||||
CampaignID gid.GID
|
|
||||||
}
|
|
||||||
|
|
||||||
CampaignNotPendingActionsError struct {
|
CampaignNotPendingActionsError struct {
|
||||||
CampaignID gid.GID
|
CampaignID gid.GID
|
||||||
}
|
}
|
||||||
@@ -89,21 +84,6 @@ 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 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 {
|
func NewCampaignNotPendingActionsError(campaignID gid.GID) error {
|
||||||
return &CampaignNotPendingActionsError{CampaignID: campaignID}
|
return &CampaignNotPendingActionsError{CampaignID: campaignID}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,15 +57,6 @@ 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 unless it is draft, cancelled, or completed",
|
|
||||||
campaignID,
|
|
||||||
),
|
|
||||||
sentinel: accessreview.ErrCampaignNotDeletable,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "not pending actions",
|
name: "not pending actions",
|
||||||
err: accessreview.NewCampaignNotPendingActionsError(campaignID),
|
err: accessreview.NewCampaignNotPendingActionsError(campaignID),
|
||||||
|
|||||||
@@ -787,10 +787,6 @@ 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