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-2485Done

Assigning a custom role — the Members picker, the `PATCH` that accepts a role definition, and the chip that names it

A role nobody can be put on is a definition, not a role. This card is the step that makes an authored role take effect for a real person: Project settings → Members, change a teammate's role, and the resolution they get on their next request is the one the admin composed.

The three layers it touches

  • The picker. ProjectMembersSettings's per-row role Combobox is built from PROJECT_ASSIGNABLE_ROLES today — three hardcoded built-ins. It is built from the project's role catalog instead: the three built-ins, then its custom roles, each labelled and each carrying the kind in words as well as in its tint. The list comes from the server page's existing read and is passed down as props — the client never fetches a role list of its own.
  • The write. PATCH /api/projects/[key]/members/[userId] accepts a role definition alongside a built-in role, and projectMembersService.setRole writes it through projectMembershipRepository.setRoleDefinition, which sets the pointer and the role tier together. Assigning a built-in clears the pointer through the same method. This service is the only single-member assignment path; the bulk move that happens when a role is deleted belongs to the role service, and neither reimplements the other.
  • The chip. A member row wearing a custom role shows that role's name in the custom tint, beside built-in rows that are unchanged — the amended access-members asset owns that grammar.

The one rule the assignment adds

A role definition must belong to this project. A cross-project or cross-workspace id is refused before any write, with the same 404-not-403 posture the members routes already take, so the endpoint cannot be used to test whether a role in another workspace exists.

Scope boundary

In: the picker, the route and service change, the repository call, the member-row chip, and the en + zh strings. Out: the role EDITOR and its doors; Delete and its dialog; the bulk reassignment a delete performs; the resolution itself, which the resolution card owns and this card only makes reachable; any change to how members are added or removed.

Acceptance criteria

  • The per-row role picker lists the three built-ins followed by the project's custom roles, in the same deterministic order the role list uses; a project with no custom roles renders exactly the picker that ships today.
  • Choosing a custom role persists it, the row's chip updates to that role's name in the custom tint, and the change survives a reload — asserted by reading the row back through the members DTO, not the database.
  • Choosing a built-in for a member who held a custom role clears the pointer and sets the tier, so no membership is ever left pointing at a role it does not hold.
  • The assignment takes effect end to end: an integration test assigns a custom role that withholds sprint:manage, then resolves that member's permissions through projectAccessService.getPermissions and shows the key is absent — the read-back proves the seam rather than the write.
  • A role definition id belonging to another project or workspace is refused with 404, before any write, and a test drives it under a foreign workspace context.
  • Only an actor holding project:manage_access may change a role — the existing gate, re-asserted rather than re-implemented; the picker is read-only for everyone else exactly as it is today.
  • projectMembersService writes role_definition_id only through projectMembershipRepository.setRoleDefinition; a guard test asserts no second write path to that column exists in lib/services/.
  • The members DTO carries what the chip needs — the role's name and whether it is built-in — and the existing members tests are extended rather than replaced.
  • Every new string is in messages/en.json and messages/zh.json.

Context refs

  • app/(authed)/settings/project/members/_components/ProjectMembersSettings.tsxroleOptions, changeRole, the row chip, and the read-only degradation the picker already implements.
  • app/api/projects/[key]/members/[userId]/route.ts · lib/services/projectMembersService.ts — the shipped PATCH and its assertPermission(…, 'project:manage_access') gate.
  • lib/projects/roles.tsPROJECT_ASSIGNABLE_ROLES and asProjectRole, the constants the picker is built from today.
  • design/projects/access-members.mock.html and the amendment adding the custom-role picker and chip; design/projects/design-notes.md § Roles & permissions (Story 6.4) for the chip grammar.
  • The schema cardsetRoleDefinition and the paired-column invariant; the read card — the catalog the picker is built from; the resolution card — what makes the assignment mean anything; the design amendment.