mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
feat(labels): harden Stack Labels (gate parity, abort, dry-run, cap) (#1232)
* feat(labels): harden stack-labels surface (gate parity, abort, dry-run policy, cap) - Cap labelIds array at MAX_LABELS_PER_NODE on PUT /api/stacks/:name/labels. - Hide sidebar Labels submenu and Settings mutation buttons for roles without stack:edit, matching the backend requirePermission gate. - Break the per-label bulk-action loop on req.aborted so cancelled requests release the per-node lock once the in-flight op completes. - Reset saving state on success in LabelInlineCreateForm so the form stays interactive when reused outside the kebab/context menus. - Invoke enforcePolicyPreDeploy inside the dry-run deploy branch and report blocked stacks honestly; previously dry-run skipped the gate and would predict success for stacks the real deploy would block. * fix(labels): split sidebar gate so inline create matches unscoped backend guard Codex review of #1232 surfaced a parity miss: POST /api/labels is guarded by unscoped requirePermission('stack:edit'), but the sidebar inline "New label" entry was gated by canEditLabels (per-stack scoped). An Admiral user with only scoped grants on a stack could toggle existing labels but the inline create request would 403. Splits the sidebar gate: - canEditLabels (scoped) keeps gating the Labels submenu trigger and toggle items, matching PUT /api/stacks/:name/labels. - canCreateLabels (unscoped) now gates the inline "New label" entry, matching POST /api/labels. Also restores the swallow-catch in LabelInlineCreateForm. The earlier catch-to-finally change in #1232 let the rethrow from createAndAssignLabel surface as an unhandled event-handler rejection in the browser console. Parents already toast on failure; swallowing in the form is the intended behavior with the finally reset still in place.
This commit is contained in:
@@ -20,6 +20,12 @@ export function LabelInlineCreateForm({ onSubmit, onCancel }: LabelInlineCreateF
|
||||
try {
|
||||
await onSubmit(trimmed, color);
|
||||
} catch {
|
||||
// Parents (createAndAssignLabel) toast and rethrow to signal failure;
|
||||
// swallow here so the rejection does not surface as an unhandled
|
||||
// event-handler rejection in the browser console.
|
||||
} finally {
|
||||
// Reset even on success so the form stays interactive if the parent
|
||||
// keeps it mounted (e.g. when reused outside the kebab/context menu).
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ function LabelsSub({ item, ctx }: { item: MenuItem; ctx: StackMenuCtx }) {
|
||||
);
|
||||
})}
|
||||
<ContextMenuSeparator />
|
||||
{ctx.labels.length < MAX_LABELS_PER_NODE && (
|
||||
{ctx.canCreateLabels && ctx.labels.length < MAX_LABELS_PER_NODE && (
|
||||
<ContextMenuItem onSelect={e => { e.preventDefault(); setCreating(true); }}>
|
||||
<Plus className="w-3.5 h-3.5 mr-2 text-muted-foreground" strokeWidth={1.5} />
|
||||
<span className="text-xs">New label</span>
|
||||
|
||||
@@ -63,7 +63,7 @@ function LabelsSub({ item, ctx }: { item: MenuItem; ctx: StackMenuCtx }) {
|
||||
);
|
||||
})}
|
||||
<DropdownMenuSeparator />
|
||||
{ctx.labels.length < MAX_LABELS_PER_NODE && (
|
||||
{ctx.canCreateLabels && ctx.labels.length < MAX_LABELS_PER_NODE && (
|
||||
<DropdownMenuItem onSelect={e => { e.preventDefault(); setCreating(true); }}>
|
||||
<Plus className="w-3.5 h-3.5 mr-2 text-muted-foreground" strokeWidth={1.5} />
|
||||
<span className="text-xs">New label</span>
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface StackMenuCtx {
|
||||
isPaid: boolean;
|
||||
isAdmiral: boolean;
|
||||
canDelete: boolean;
|
||||
canEditLabels: boolean;
|
||||
canCreateLabels: boolean;
|
||||
isPinned: boolean;
|
||||
labels: Label[];
|
||||
assignedLabelIds: number[];
|
||||
|
||||
Reference in New Issue
Block a user