Files
pad/web/src/lib/components/fields
xarmian a8b158829b feat(item-detail): gate write affordances on canEditItem (TASK-1105) (#419)
* feat(item-detail): gate write affordances on canEditItem (TASK-1105)

Item detail page hides title-edit, content editing, FieldEditor inputs,
delete button, and assignment dropdowns when the user lacks edit on this
specific item. Mirrors the server's per-item permission cascade so the UI
cannot show affordances the server would 403.

Per-item gate via workspaceStore.canEditItem(item) — owner → item grant →
collection grant → role + visibility → deny. Handles guests with single
ItemGrant.edit (full edit on that one item, read-only on siblings) and
the precedence regression where ItemGrant.view + CollectionGrant.edit on
the same item resolves to read-only (item grant wins per server cascade).

Changes:
- FieldEditor: new `readonly?: boolean` prop. When true, renders a unified
  display block per field type (select / checkbox / date / number / url /
  text) — same visual language as the editor's idle state, no inputs, no
  dropdowns, no mutation handlers. Documented in the component header.
- RawMarkdownEditor: new `readonly?: boolean` prop, applied to the
  underlying textarea.
- [slug]/+page.svelte: derived canEdit predicate. Title swaps from
  click-to-edit button to plain h1 when read-only. Editor passes
  editable=canEdit; EditorBubbleMenu / EditorLinkPopover only mount when
  editable. RawMarkdownEditor passes readonly. Delete button hidden.
  FieldEditor receives readonly={!canEdit}. Assignment + role dropdowns
  swap to read-only display spans.
- New CSS: .title-readonly (no hover, default cursor),
  .assignment-readonly (matches assignment-select height for layout
  stability when the user gains/loses edit permission).

Parent: PLAN-1100.

* fix(item-detail): gate Editor toolbars + ?new=1 title bypass per Codex review (round 2)

Two read-only escape hatches found by Codex re-review:

1. Editor.svelte mobile toolbar (line 818) and table toolbar (line 846)
   rendered without checking the `editable` prop. tiptap's editor instance
   correctly refuses commands when editable=false, so the buttons would
   no-op, but they still rendered and were visually misleading. Both
   toolbars now gated on `editable`.

2. The slug page's auto-start-title-edit path for ?new=1 didn't check
   canEdit. A read-only user appending ?new=1 would land on the title
   textarea (which the visible-branch gate now hides). Added canEdit to
   the auto-start condition AND to startEditTitle() itself as a defensive
   second line.

Round 1 disagreements stand: Move-to / item-links / ChildItems are
explicitly TASK-1108 sweep scope and intentionally not addressed here.

Parent: PLAN-1100. Refs TASK-1105 PR #419.

* fix(item-detail): exclude BlockDragHandle in read-only + gate Move-to / links per Codex review (round 3)

Three findings from round 3:

1. Editor's BlockDragHandle ProseMirror plugin (registered in Editor's
   extensions list) is not gated by tiptap's `editable` flag — its drag
   handle is injected into the view DOM regardless. A read-only user
   could drag blocks to dispatch transactions through onUpdate. Fix:
   conditionally include the plugin in the extensions array based on
   `editable`.

2 + 3. Move-to button and item-links add/delete affordances. These were
       originally TASK-1108 sweep scope, but Codex re-flagged them in
       round 3 despite the round-2 deferral. Absorbed into TASK-1105
       rather than burn more review rounds — the gating is mechanical
       (a few {#if canEdit} wrappers). TASK-1108 sweep will still grep
       for any remaining open-coded patterns elsewhere.

Parent: PLAN-1100. Refs TASK-1105 PR #419.

* fix(item-detail): re-key Editor on canEdit change so BlockDragHandle reattaches per Codex review (round 4)

Round 3 excluded BlockDragHandle from the editor's extensions array when
editable=false. Round 4 caught the construction-time-only nature of that
gate: on cold/direct navigation /me resolves after the editor mounts, so
canEdit starts false → editor created without BlockDragHandle → /me
resolves → canEdit flips true but the existing $effect only calls
editor.setEditable(true) and does not re-register extensions.

Fix: add canEdit to the {#key} value so the editor is reconstructed when
permission flips. Cost is a brief loss of cursor/scroll position on the
flip — acceptable since the only path that flips canEdit mid-session is
a grant change while the page is open, which is rare.

Same approach is appropriate for any future extension whose registration
is gated on `editable`.

Parent: PLAN-1100. Refs TASK-1105 PR #419.

* fix(item-detail): handle ?new=1 auto-edit reactively for slow /me per Codex review (round 5)

* fix(item-detail): always reassign pendingNewItemEdit per Codex review (round 6)
2026-05-05 09:41:12 -04:00
..