mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
feat(views): gate drag/archive in ListView/BoardView on canEditCollection (TASK-1106) (#420)
The collection page's ListView and BoardView allowed all roles to drag
items, drag-status-change, reorder groups/columns, and archive groups.
Server enforces edit per-item and per-collection on these mutations
(handlers_items.go, handlers_role_board.go) — UI now matches.
Changes:
- ListView + BoardView accept a `canEdit?: boolean` prop (default true to
preserve behavior in existing callers).
- ListView: dndzone for groups + intra-group items receives
`dragDisabled: !canEdit`. Group drag handle and archive-group button
hidden when !canEdit.
- BoardView: column-cards dndzone receives `dragDisabled: isMobile || !canEdit`.
Column-header drag (column reorder) gated via `draggable={canEdit}` and
conditional drag handlers. Column-drag-handle indicator and
archive-column button hidden when !canEdit.
- Collection page passes `canEdit={canEditThisCollection}` to both views.
Scope note: per-item drag gating (e.g. a guest with ItemGrant.edit on one
item dragging just that one card) is not implemented — svelte-dnd-action
only supports zone-level dragDisabled. Achieving per-item would require
switching to dragHandleZone+dragHandle and shipping an explicit handle UI
for everyone, which is a larger UX change. Server already enforces per-item
edit on the resulting mutations, so no security gap. Documented as a
follow-up if needed.
TableView: excluded from drag/archive scope — no drag handlers to gate.
Status-cell editing is already gated via FieldEditor's readonly prop from
TASK-1105.
Parent: PLAN-1100.
This commit is contained in:
@@ -20,9 +20,16 @@
|
||||
oncreate?: () => void;
|
||||
itemProgress?: Record<string, { total: number; done: number }>;
|
||||
progressLabel?: string;
|
||||
/**
|
||||
* canEdit gates drag-to-reorder, drag-to-status-change, column
|
||||
* reordering, and the archive-column button. See ListView.svelte
|
||||
* for the rationale (zone-level gate; per-item is a follow-up).
|
||||
* Default true preserves behavior in callers that don't pass it.
|
||||
*/
|
||||
canEdit?: boolean;
|
||||
}
|
||||
|
||||
let { items, collection, wsSlug = '', groupField = 'status', focusedItemId = null, onStatusChange, onReorder, onArchiveColumn, onGroupReorder, oncreate, itemProgress, progressLabel = 'tasks' }: Props = $props();
|
||||
let { items, collection, wsSlug = '', groupField = 'status', focusedItemId = null, onStatusChange, onReorder, onArchiveColumn, onGroupReorder, oncreate, itemProgress, progressLabel = 'tasks', canEdit = true }: Props = $props();
|
||||
|
||||
let confirmArchiveColumn = $state<string | null>(null);
|
||||
let isMobile = $state(false);
|
||||
@@ -221,17 +228,19 @@
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="column-header {columnCssClass(colValue)}"
|
||||
draggable="true"
|
||||
draggable={canEdit}
|
||||
role="toolbar"
|
||||
tabindex="0"
|
||||
ondragstart={(e) => handleColumnDragStart(e, colValue)}
|
||||
ondragend={handleColumnDragEnd}
|
||||
ondragstart={canEdit ? (e) => handleColumnDragStart(e, colValue) : undefined}
|
||||
ondragend={canEdit ? handleColumnDragEnd : undefined}
|
||||
>
|
||||
<span class="column-drag-handle" title="Drag to reorder">⠿</span>
|
||||
{#if canEdit}
|
||||
<span class="column-drag-handle" title="Drag to reorder">⠿</span>
|
||||
{/if}
|
||||
<span class="column-name">{formatLabel(colValue)}</span>
|
||||
<div class="column-actions">
|
||||
<span class="column-count">{colItems.length}</span>
|
||||
{#if onArchiveColumn && colItems.length > 0}
|
||||
{#if canEdit && onArchiveColumn && colItems.length > 0}
|
||||
{#if confirmArchiveColumn === colValue}
|
||||
<span class="archive-confirm">
|
||||
<button class="archive-yes" onclick={() => { onArchiveColumn(colItems); confirmArchiveColumn = null; }}>Archive {colItems.length}?</button>
|
||||
@@ -256,7 +265,7 @@
|
||||
type: 'board-card',
|
||||
dropTargetClasses: ['drop-target'],
|
||||
delayTouchStart: touchDragDelayMs,
|
||||
dragDisabled: isMobile
|
||||
dragDisabled: isMobile || !canEdit
|
||||
}}
|
||||
onconsider={(e) => handleConsider(colValue, e)}
|
||||
onfinalize={(e) => handleFinalize(colValue, e)}
|
||||
|
||||
@@ -22,6 +22,21 @@
|
||||
oncreate?: () => void;
|
||||
itemProgress?: Record<string, { total: number; done: number }>;
|
||||
progressLabel?: string;
|
||||
/**
|
||||
* canEdit gates drag-to-reorder, drag-to-status-change, and the
|
||||
* archive-group button. Default true preserves existing behavior in
|
||||
* call sites that don't pass it. Pass `workspaceStore.canEditCollection(collection.id)`
|
||||
* (PLAN-1100 / TASK-1106) — the gate is collection-level because
|
||||
* svelte-dnd-action only supports zone-level dragDisabled.
|
||||
*
|
||||
* Per-item gating (e.g. a guest with `ItemGrant.edit` on a single
|
||||
* item dragging just that one card) would require switching to
|
||||
* `dragHandleZone` + `dragHandle` actions, which changes the drag
|
||||
* UX for everyone (whole-card → explicit-handle). Documented as a
|
||||
* follow-up if needed; the server already enforces per-item edit
|
||||
* on the resulting mutations, so no security gap here.
|
||||
*/
|
||||
canEdit?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -37,7 +52,8 @@
|
||||
onGroupReorder,
|
||||
oncreate,
|
||||
itemProgress,
|
||||
progressLabel = 'tasks'
|
||||
progressLabel = 'tasks',
|
||||
canEdit = true
|
||||
}: Props = $props();
|
||||
|
||||
let confirmArchiveGroup = $state<string | null>(null);
|
||||
@@ -214,7 +230,8 @@
|
||||
(BUG-641): a 500ms long-press is required before drag activates,
|
||||
which matches the existing intra-group item behaviour and lets
|
||||
ordinary taps/scrolls pass through unmolested. */
|
||||
delayTouchStart: touchDragDelayMs
|
||||
delayTouchStart: touchDragDelayMs,
|
||||
dragDisabled: !canEdit
|
||||
}}
|
||||
onconsider={handleGroupConsider}
|
||||
onfinalize={handleGroupFinalize}
|
||||
@@ -231,14 +248,16 @@
|
||||
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleGroup(groupName); } }}
|
||||
aria-expanded={!collapsedGroups.has(groupName)}
|
||||
>
|
||||
<span class="group-drag-handle" title="Drag to reorder">⠿</span>
|
||||
{#if canEdit}
|
||||
<span class="group-drag-handle" title="Drag to reorder">⠿</span>
|
||||
{/if}
|
||||
<span class="collapse-icon" class:collapsed={collapsedGroups.has(groupName)}
|
||||
>▾</span
|
||||
>
|
||||
<span class="group-title">{formatLabel(groupName)}</span>
|
||||
<span class="group-actions">
|
||||
<span class="group-count">{itemCount(grpItems)}</span>
|
||||
{#if onArchiveGroup && itemCount(grpItems) > 0}
|
||||
{#if canEdit && onArchiveGroup && itemCount(grpItems) > 0}
|
||||
{#if confirmArchiveGroup === groupName}
|
||||
<span class="archive-confirm">
|
||||
<button class="archive-yes" onclick={(e) => { e.stopPropagation(); onArchiveGroup(grpItems); confirmArchiveGroup = null; }}>Archive {itemCount(grpItems)}?</button>
|
||||
@@ -264,7 +283,8 @@
|
||||
flipDurationMs,
|
||||
type: 'list-item',
|
||||
dropTargetClasses: ['drop-target'],
|
||||
delayTouchStart: touchDragDelayMs
|
||||
delayTouchStart: touchDragDelayMs,
|
||||
dragDisabled: !canEdit
|
||||
}}
|
||||
onconsider={(e) => handleConsider(groupName, e)}
|
||||
onfinalize={(e) => handleFinalize(groupName, e)}
|
||||
|
||||
@@ -1276,6 +1276,7 @@
|
||||
oncreate={canEditThisCollection ? openQuickCreate : undefined}
|
||||
{itemProgress}
|
||||
{progressLabel}
|
||||
canEdit={canEditThisCollection}
|
||||
/>
|
||||
{:else if viewMode === 'table'}
|
||||
<TableView
|
||||
@@ -1302,6 +1303,7 @@
|
||||
oncreate={canEditThisCollection ? openQuickCreate : undefined}
|
||||
{itemProgress}
|
||||
{progressLabel}
|
||||
canEdit={canEditThisCollection}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user