Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2473Done

The work-item affordances ask the KEY — retire `canEdit` and `canManage` from the client context

Every work-item affordance in the product decides what to show from one of two booleans. canEdit answers may this person edit work items; canManage answers may this person administer the project — and canManage is what the ⋯ menu's Delete currently gates on, even though deleting a work item has had its own permission since the member-facing sweep wired work_item:delete. The affordances are right by coincidence for the three built-in roles and wrong for any role someone composes.

The client-set card put the real set in the context and kept the two booleans alive, derived, so that nothing had to change at once. This card spends that: every consumer asks can(<key>), and the booleans come off the context value.

The approach

  • Each consumer's key is READ off the write it guards, never inferred from the boolean it replaces. For each component, find the server action or route its control invokes, grep the assertPermission / assertCan* in that path, and use that key. The distinctions this surfaces are real and are the point of the card: applying an existing label to an item is work_item:edit, while managing the project's label SET is label:manage; deleting an item is work_item:delete, not project:administer.
  • The tighten-only fallback survives. useProjectAccess() outside a provider answers true for every key, so a component mounted in a unit test keeps its ungated behaviour. That property is shipped and must not regress.
  • Treatment is unchanged. This card re-points WHICH permission an affordance consults; it does not turn a disabled control into a hidden one or the reverse. The 2026-06-09 in-place treatments — the disabled Create button and its C / ⌘K shortcut, board drag with its read-only banner, the inline field pickers — stay exactly as they render today, and panel 5 of the design asset draws them under that heading for exactly this reason.
  • Then the booleans go. With no consumer left, canEdit and canManage come off the context value, so the next component cannot reach for them.

Scope boundary

In: app/(authed)/_components/ProjectAccessProvider.tsx and every component that reads useProjectAccess(), plus their tests.

Out: the nav, the palette and the settings surfaces, which sibling cards own; the SERVER-side gates, which are already correct and are what this card reads FROM; any change to whether a control is hidden or disabled.

Acceptance criteria

  • A grep of the repository for canEdit or canManage read from useProjectAccess() returns nothing, and the two fields are gone from the context value's type — so the sweep is self-recounting and a consumer added between planning and running cannot be missed by a count.
  • Every re-pointed consumer names the key its own control's server gate asserts; the PR body carries the pairing table with the file and line of each gate that justified it.
  • work_item:delete — not project:administer — gates the delete affordance wherever one is offered; a test asserts an actor holding delete but not administer is offered it.
  • Where a control's real key differs from the boolean it replaced, the change is called out individually in the PR body rather than folded into the sweep, because those are the only lines in this card that change behaviour for a built-in role.
  • useProjectAccess() with no provider still answers true for every key.
  • No control changes treatment: everything visible-and-disabled today is still visible-and-disabled, and nothing newly disappears — asserted for a viewer against the shipped board, issue-detail and list surfaces.
  • The changed files meet the repo's per-file coverage floor.

Context refs

  • app/(authed)/_components/ProjectAccessProvider.tsx — the context this card narrows to can() alone.
  • app/(authed)/items/_components/WorkItemRowActions.tsx, app/(authed)/boards/_components/BoardCard.tsx, app/(authed)/items/archived/_components/ArchivedWorkItemsList.tsx — the three consumers reading canManage, the ones whose key most likely changes.
  • app/(authed)/items/[key]/_components/CoreFieldsPanel.tsx, LabelsCard.tsx, ComponentsCard.tsx, CustomFieldsSection.tsx, app/(authed)/items/_components/IssueInlineEdit.tsx, NewIssueButton.tsx, app/(authed)/_components/CreateIssueButton.tsx, ReportButton.tsx — the canEdit consumers.
  • lib/permissions/catalog.ts — the key set, and which of them are work-item scoped.
  • lib/services/projectAccessService.tsassertPermission, the gate each consumer's key is read from.
  • The design card — panel 5, the UNCHANGED in-place treatments this card must leave alone.