Skip to content

feat(assistants): viewer/editor share permissions UI (#113)#384

Merged
philmerrell merged 9 commits into
developfrom
feature/collab-assistant-editing-frontend
May 24, 2026
Merged

feat(assistants): viewer/editor share permissions UI (#113)#384
philmerrell merged 9 commits into
developfrom
feature/collab-assistant-editing-frontend

Conversation

@philmerrell
Copy link
Copy Markdown
Contributor

PR 2 of 2 for #113 — consumes the backend contract shipped in #383.

Summary

  • Share dialog: per-row "Can view / Can edit" selector on existing shares, a "permission for new people" toggle that applies to all newly added emails, and `onSave` delta-detection that separates adds, removes, and permission upgrades on already-shared emails — each routed to the right backend endpoint (`POST` / `DELETE` / new `PATCH /assistants/{id}/shares`).
  • Assistant list: shared-with-me cards with `userPermission === 'editor'` now show an "Editor" badge and an Edit button alongside Chat (owners' actions unchanged).
  • Assistant form: owner-only gating on the Share button; "Shared by {owner}" banner for editors; `userPermission` tracked from the loaded assistant (with a safe owner fallback for create mode).
  • Contract shape: `AssistantSharesResponse.sharedWith` is now `ShareEntry[]` (`{email, permission}`) end-to-end. The pre-existing `sharedEmails: string[]` state in the dialog became `shares: ShareEntry[]`.

Why this lands now

PR 1 changed `AssistantSharesResponse.sharedWith` from `string[]` to `ShareEntry[]` — the SPA was reading the old shape until this PR. Merging promptly closes that gap.

Test plan

  • `npm run test:ci` → 1101 passed. New spec coverage:
    • Service: `shareAssistant` default + explicit-permission, `updateSharePermission`, `getAssistantShares` returning `ShareEntry[]`.
    • Dialog: dedicated spec for the delta algorithm (pure add, pure remove, permission upgrade on existing email, no-change baseline, mixed adds + removes + upgrades). DI tokens used over `vi.mock` per project convention.
  • `npm run build` → clean (only pre-existing budget + unused-RouterLink warnings).
  • Manual smoke (script below — `SKIP_AUTH=false` locally so I can't exercise it autonomously):

Manual test script

Setup: two accounts. Alice owns an assistant; Bob has no shares yet.

  1. Owner shares Bob as viewer
    • As Alice → open the assistant → Share → enter `bob@…` → permission toggle stays on "Can view & chat" → Save.
    • Expected: dialog closes, no error.
  2. Bob sees viewer state
    • As Bob → assistants list → "Shared with Me" section shows the assistant card with the owner badge, no "Editor" badge, no Edit button (just Chat).
  3. Owner upgrades Bob to editor
    • As Alice → Share dialog → Bob's row → flip permission select to "Can edit" → Save.
    • Network: a `PATCH /assistants/{id}/shares` should fire (not `DELETE` + `POST`).
  4. Bob sees editor state
    • As Bob → reload list → his card now shows the amber "Editor" badge and an Edit button next to Chat.
  5. Bob edits as editor
    • As Bob → click Edit → form opens with all fields editable, the amber "Shared by Alice" banner in the header, no Share button.
    • Change description, Save. Expected: 200; navigates back to list.
  6. Bob cannot change visibility
    • As Bob → set visibility (via any indirect path that triggers `visibility` on the PUT, e.g. share dialog clear-all → it tries PRIVATE). Expected: backend 400 surfaces in the dialog as an error.
  7. Mixed save
    • As Alice → Share dialog → add a new viewer email, remove an existing share, downgrade one editor → Save.
    • Expected: one `POST` (adds), one `DELETE` (removes), one `PATCH` per downgraded email. Watch network tab.

🤖 Generated with Claude Code

philmerrell and others added 9 commits May 23, 2026 18:49
Consumes the backend contract shipped in #383: surfaces a per-share
permission toggle in the share dialog, an "Editor" badge + Edit affordance
on shared-with-me cards, an editor banner on the form, and an owner-only
gate on the Share button.

- Dialog: per-row "Can view / Can edit" select on existing shares, a
  "permission for new people" toggle, and onSave delta-detection that
  distinguishes adds, removes, and permission changes on already-shared
  emails — each dispatched to the correct backend endpoint (POST /
  DELETE / PATCH).
- List: shared-with-me cards with userPermission='editor' now show an
  Editor badge and an Edit button alongside Chat.
- Form: surfaces "Shared by {owner}" banner for editors; Share button
  is owner-only.
- AssistantSharesResponse.sharedWith is now ShareEntry[] across the
  service / api / dialog (matches the backend's PR-1 shape change).
- Vitest: existing service spec migrated to the new shape; new dialog
  spec covers the delta algorithm (adds / removes / permission upgrades /
  mixed) via DI tokens per project convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings the share dialog in line with the list/form design language
captured in .claude/skills/tailwind-ui/references/app-conventions.md:
rounded-2xl, blue accent (was indigo), focus:ring-2 focus:ring-blue-500,
dark:bg-gray-800 inputs, flat <section> blocks divided by border-t.

- Header avatar: blue-100 chip (was indigo).
- URL row + add-people + current-shares are three flat sections, no
  individually-bordered cards.
- "Currently shared with" became a single rounded-2xl divide-y ul
  (was a stack of bordered rows), with an empty-state when nothing
  is shared.
- Mode toggle is now a proper segmented tablist with role="tab" and
  aria-selected, accented in blue.
- Permission default-for-new-people moved into the section header so
  it's contextually attached to the add controls.
- Save/Cancel actions reorder cleanly on mobile + desktop and use the
  shared blue/ghost button tokens.
- Search results render skeleton rows (animate-pulse) while searching,
  with role="status" sr-only text — matches the connector-chip skeleton
  pattern already in the assistant editor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
loadShares() runs in the constructor and shares() starts empty, so the
empty-state ("Not shared with anyone yet") was painting while the fetch
was in flight on dialogs for SHARED assistants. Renders a skeleton ul
matching the real row layout (email + permission select + delete) while
loadingShares() is true; suppresses the count chip until the fetch
resolves.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Shares skeleton: drop from 3 rows to 1; the loading state was visually
  heavier than the real list it was previewing.
- Tabs: selected state now flips font-medium → font-semibold alongside
  the existing blue underline + text color, so the active tab reads at a
  glance. -mb-px on each tab makes the active 2px underline overlap the
  container's 1px bottom border cleanly (no gray line peeking through).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The native chevron sat too close to the rounded-2xl edge. Switch
px-2.5 → pl-2.5 pr-7 on both permission selects (the per-share row
select and the "default for new people" select) so the chevron has
breathing room without changing the left padding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The active tab used border-blue-600 alongside a base border-transparent.
Both are border-color utilities at the same specificity, so whichever
Tailwind emitted later in the stylesheet won — in practice the
transparent base, leaving no visible underline.

Switch to bottom-only border-b-transparent / border-b-blue-600 so the
active state targets border-bottom-color exclusively; no cascade
collision with the base.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Confirmed via devtools: border-b-blue-600 was on the DOM but computed
border-bottom-color was rgba(0,0,0,0). Same-specificity class collision
with border-b-transparent — Tailwind's emit order put the base last
and the conditional class lost the cascade.

Move all active styling onto aria-selected:* utilities so the active
selector becomes [aria-selected="true"], which has attribute-selector
specificity and beats the base. As a bonus the tabs now use one
declarative class string instead of four parallel [class.x] bindings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Native select chevrons sit at a browser-fixed offset from the right edge
regardless of padding-right, so pr-7 / pr-8 just pushed the text away
from a chevron still crowded against the rounded-2xl corner.

Switch both permission selects to appearance-none + an overlaid
heroChevronDown icon. The wrapper handles positioning so the chevron
clears the rounded corner cleanly. pointer-events-none on the icon so
clicks still hit the native select.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both fell out of the share-assistant-dialog redesign on PR #384 and
will bite future work the same way if undocumented:

- Tabs: conditional [class.border-b-blue-600] loses the cascade to a
  base border-b-transparent at the same specificity. Use the aria-selected:
  variant so the active rule has attribute-selector specificity.

- Selects: native chevrons sit at a browser-fixed offset from the right
  edge regardless of padding-right; with rounded-2xl they crowd the
  corner. appearance-none + overlaid heroChevronDown gives reliable
  positioning.

Adds a Tabs subsection, a Select example under Form pages, and a
"Common gotchas" section that names both failure modes and their
DevTools symptoms.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@philmerrell philmerrell merged commit 8cae47d into develop May 24, 2026
10 checks passed
@philmerrell philmerrell deleted the feature/collab-assistant-editing-frontend branch May 24, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant