Skip to content

feat(frontend): organization management UI + role-aware permission gating (stacked on #267) - #268

Open
amal66 wants to merge 3 commits into
Open-Legal-Products:mainfrom
amal66:olp-pr/organizations-ui
Open

feat(frontend): organization management UI + role-aware permission gating (stacked on #267)#268
amal66 wants to merge 3 commits into
Open-Legal-Products:mainfrom
amal66:olp-pr/organizations-ui

Conversation

@amal66

@amal66 amal66 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #267

This PR is the frontend half of the organizations/RBAC feature: it sits on top of #267's branch and carries its two commits — review the top commit only (feat(frontend): organization management UI + role-aware permission gating). If #267 merges first, this rebases to a single clean commit.

#267 shipped the tenant layer and the project role ladder server-side, but nothing in the product surfaced it: no way to create an org, no way to see members, and a UI that still derives every permission from a single is_owner boolean (including the org-admin case where the server allows an action but the client renders read-only). This PR closes those UI/UX flows.

What it adds

Settings → Organizations (new page, plus a sidebar entry)

  • Create an organization (you become its owner). The auto-provisioned personal org is deliberately hidden — it's plumbing, not a firm.
  • Member roster per org: profile display names/emails (never bare user ids), role badges, a role select (owner/admin/member) for owners/admins, remove-member and leave-org with confirmation. Server rules surface inline — including the 409 An organization must keep at least one owner. last-owner protection.
  • Teams: create/delete teams, add/remove members by email, member chips with profile names.
  • Plain members see the roster read-only — no add input, no role selects — matching the server's authorization.

Org-scoped project creation

The New Project modal gains an Organization select (default: Personal workspace) populated from GET /orgs. This is the write path that makes the whole feature real: until now nothing in the UI could create a project with an org_id.

Role-aware permission gating (replaces is_owner-only logic)

frontend/src/app/lib/permissions.ts mirrors the server's capability matrix, and detail pages derive the caller's role from the access_role field #267 added (falling back to the historic is_owner contract on list rows). One canDo(capability) seam threads through the project workspace:

Surface Now gated by
Project details edit, sharing (PeopleModal write mode) members.manage (manager+)
Folder rename / move / delete, incl. drag-and-drop structure.manage (manager+)
Folder create, document rename / move / remove-from-folder docs.organize (editor+)
Document upload / Add-documents action content.edit (editor+)
Review columns, document set, clear-cells, details, apply-workflow structure.manage (manager+)
Project / review delete owner only

Notable fixes this delivers:

  • The folder tree was previously completely unguarded — any shared member saw working rename/delete affordances whose server calls now fail; viewers saw them too. Gated with the same popup pattern used everywhere else.
  • Org admins are no longer shown a read-only project they can actually manage (the server-allows/client-forbids inconsistency).
  • OwnerOnlyPopup grows a requiredRole tier ("Only the owner or a manager can rename folders.") and now shows the owner's email when known, so a denial tells the user who to ask.
  • Review details save only sends project_id when it actually changed — moving a review is owner-only server-side, and a manager editing just the title must not 403.

Backend (small, additive)

GET /orgs/:orgId/members and GET /orgs/:orgId/teams are enriched with mirrored profile email/display_name (same source as the projects /people endpoint) so the client never has to render a UUID.

UX walkthrough (screenshots)

Captured against a live local stack (Supabase CLI + MinIO + backend + prod next start), three users in one org: an owner, an admin (→ manager role on org projects), and a plain member (→ viewer).

As the org owner

1 — Organization management (Settings → Organizations): roster with role selects and descriptions, teams with member chips.

2 — Creating a project inside the firm: the new Organization select in the New Project modal.

3 — Owner's project view: full toolset (Folder, Add documents, Actions).

4 — People with access modal.

As a plain org member (viewer)

5 — Same project, read-only: no Folder button, Add documents disabled.

6 — "View details": inputs disabled.

7 — Delete attempt: owner-only popup instead of a silent 404.

As an org admin (manager)

8 — Manager toolset: Folder and uploads available — the previous server-allows/client-forbids inconsistency is gone.

9 — "Edit details": editable, with Update action.

10 — Container deletion still owner-only, even for admins.

Testing

  • cd frontend && npm test74 passed (10 files), including new suites: the client capability matrix asserted cell-by-cell against the server's, the roleFrom fallback contract, OrganizationsPage flows (create org, roster rendering, role change, inline 409 surfacing, member read-only mode), and OwnerOnlyPopup tier copy.
  • cd frontend && npx tsc --noEmit → clean; npm run lint → 0 errors; next build → clean production build.
  • cd backend && npx vitest run307 passed (member/team enrichment covered by the orgs service suite).
  • Manually verified against a full local stack (Supabase CLI + MinIO + backend + prod next start): org create → member add → role change → viewer/editor/manager gating in the project and review UIs.

Provenance

All new code written for this PR (no fork port — the fork has no org UI). Follows the existing conventions: account settings tab rail, AccountSection glass cards, PillButton/ModalSelect/AddUserInput primitives, apiRequest client layer, the OwnerOnlyPopup guard idiom, and testing-library patterns from the frontend harness.

🤖 Generated with Claude Code

amal66 and others added 3 commits July 26, 2026 17:02
Introduce an organizations tenant layer on top of the existing per-user
model: every account gets an auto-provisioned personal org, orgs carry
owner/admin/member RBAC via org_members, and teams group members inside
an org. projects/documents/workflows/tabular_reviews gain a nullable
org_id (ON DELETE SET NULL) so org membership becomes a third access
branch alongside row ownership and shared_with emails — in the access
helpers, the overview RPCs, and the org-aware /orgs REST module.

Mechanical port of the organizations/RBAC feature from amal66/mike@main
(b3166dd) onto the upstream layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
…nches

Give the three access branches a Drive-style role ladder instead of raw
ok/isOwner flags: row owner → owner, shared_with email → editor, org
owner/admin → manager, plain org member → viewer. A single capability
matrix (lib/permissions.ts) maps roles to what routes may do — view,
content.edit, docs.organize, structure.manage, members.manage,
container.delete — and every project/document/review write route now
declares the capability it needs instead of hand-rolling an owner check.

This makes the ADR's 'org membership grants visibility, not ownership'
promise real: plain org members are read-only (previously the org branch
returned ok:true and most write routes gated on nothing beyond ok), and
org owner/admins can curate content (manage folders, sharing, review
structure) without being able to delete containers they don't own.

Notable tightenings, all fail-closed:
- folder rename/move/delete, doc-set/column edits on reviews, and
  clear-cells are manager+ (generalising the owner-only folder-delete
  gate that landed upstream in Open-Legal-Products#193)
- version pushes, edit resolution, chat, and review generation are
  editor+ (org viewers excluded)
- project PATCH (metadata + sharing) is manager+, so org admins can
  manage without owning; project/review DELETE stays owner-only
- GET /projects/:id and /people now go through checkProjectAccess (the
  roster previously 404'd for org members who could read the project)

Detail responses expose access_role alongside is_owner so the client
can render per-role affordances. can() is exhaustively unit-tested
(role × capability), and route suites cover the new gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ting

The client half of the org/RBAC feature: orgs become visible and usable,
and the UI stops deriving permissions from a single is_owner boolean.

- Settings → Organizations: create orgs, expand into a member roster
  (profile names/emails, role select with owner/admin/member, remove or
  leave with confirm; 409 last-owner protection surfaced inline) and
  teams (create/delete, add/remove members). Personal orgs stay hidden —
  they are plumbing, not a firm.
- New project modal gains an Organization select (personal workspace
  default) so org-scoped projects can actually be created; sidebar user
  menu links to Organizations.
- GET /orgs/:id/members and /teams responses enriched server-side with
  mirrored profile email/display_name (same source as /people) so the
  client never renders a bare user id.
- lib/permissions.ts mirrors the server capability matrix; detail pages
  derive the caller's role from access_role (falling back to the
  is_owner list-row contract) and gate affordances through one canDo()
  seam: project details/sharing manager+, folder rename/move/delete
  manager+, doc rename/move and folder create editor+, review
  columns/document-set/clear-cells/details manager+, container delete
  owner-only. This also closes the previously unguarded folder-tree
  actions and un-read-onlys org admins.
- OwnerOnlyPopup generalised with a requiredRole tier so denials name
  the role that can act, and now shows the owner's email when known.
- Review details save only sends project_id when it changes (moving is
  owner-only server-side; managers editing a title must not 403).
- Tests: client matrix + roleFrom contract, OrganizationsPage flows
  (create, roster, role change, error surfacing, member read-only),
  OwnerOnlyPopup tiers. Frontend suite 74 passed; backend 307 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@amal66

amal66 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Went through the full diff — the core feature here is solid, but the PR bundles several independent changes worth calling out. Breakdown:

1. Backend pagination/search/sort (the stated scope)

  • New 9-arg get_tabular_reviews_overview RPC (p_scope, p_limit, p_offset, p_search_term, p_sort_key, p_sort_direction), with the legacy 3-arg signature kept as a compatibility wrapper
  • Small parsing/validation libs (pagination.ts, search.ts, sort.ts, tabularReviewsOverview.ts) and the tabular.ts route wiring

2. Frontend lazy-load integration

  • usePaginatedTabularReviews hook (debounced search via useDebouncedValue, abort-on-query-change, retry), wired into both tabular-reviews pages and ProjectReviewsTable
  • mikeApi.ts client support for the new args

3. Perf/schema work inside the migration that goes beyond pagination

  • document_count computed once in a new review_document_counts CTE, avoiding recomputation in the new sort branches
  • tabular_cells aggregation narrowed to only reviews whose document_ids isn't a JSON array
  • pg_trgm extension + GIN trigram index on lower(title) (supports the new search, but it's schema surface of its own)

4. State-management removal in ProjectWorkspace

  • Deletes the entire projectReviews cache: state, loading flag, ensureProjectReviews, its promise ref, the prefetch call, and the optimistic prepend after creating a review
  • Consequence: reviews are refetched on every visit to the reviews tab instead of cached in the workspace context

5. Shared PageHeader search-control UX change (cross-cutting — affects every page with a header search, not just tabular reviews)

  • Adds a clear (✕) button
  • Input stays expanded whenever it has a value
  • Outside click no longer clears the query (previously it wiped it)
  • autoFocus only on explicit open
  • Regression: the input lost its disabled={disabled} — only the clear button is disabled now, so a disabled search still accepts typing

6. Drive-by logic fix in delete-selected (both pages)

  • Owned-ids filter flipped from !review || review.user_id === user.id to !!review && review.user_id === user.id — selected ids not present in the loaded list are no longer assumed deletable. Good catch on a real pre-existing bug, just worth noting it's unrelated to pagination.

Tests (supporting #1#2)

  • 4 backend unit-test files (pagination, search, sort, RPC-args parsers)
  • New Supabase integration test (tied-row pagination determinism, scope+search-before-limit, sort-before-paginate, legacy 3-arg RPC still callable), registered in test-stack.sh
  • Hook tests (selection scoping, abort-on-query-change, error + retry)

Requested changes:

  1. Split the PageHeader change (fix: JSONB array queries fail with "invalid input syntax for type json" #5) into its own PR. It silently changes search behavior on every page that uses the shared header, so it needs its own review — this PR should stay focused on the pagination approach, which is the part that needs design sign-off before being mirrored to the other views.
  2. Restore disabled={disabled} on the search input (in whichever PR ends up carrying fix: JSONB array queries fail with "invalid input syntax for type json" #5) — right now a disabled search still accepts typing.
  3. Mention Is Mike aimed at any specific territory? #3, Create Docker file and / or compose for easy deployment #4, and feat(research): add Olostep web research endpoints #6 in the PR description if you'd rather keep them here than split them too — they're reasonable changes, but as written the description doesn't cover them, and future readers of this PR won't know they shipped with it.

Happy to re-review quickly once that's done — #1 + #2 + the tests look good and I don't expect issues with the core approach.

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