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

`getRoleCatalog` returns the project's OWN roles — the widened `RoleDTO`, the provenance, and the two shipped screens rendering them read-only

The read the Roles & permissions screens already use, grown to carry the project's own roles. getRoleCatalog was deliberately built as a project-scoped SERVICE read rather than a static import, with its own note saying "MOTIR-2257 will read the project's own roles here, which is the reason the projectId is threaded at all" — this is that card. The two screens likewise resolve a role by looking it up in catalog.roles, so a custom role reaches the detail page with no routing change.

The DTO widens, and that is the ripple

RoleDTO today is built-in-shaped: role: ProjectRole doubles as identity and URL segment, and labelKey / descriptionKey assume every name is an i18n string. A custom role has a name a human typed. So:

  • key: string replaces role as the identity and the [roleKey] URL segment — admin / member / viewer for a built-in, the definition's id for a custom role.
  • builtInRole: ProjectRole | null carries the enum where it still exists, which is what the icon map and the tint choice key off.
  • labelKey / descriptionKey become nullable, and name / description are added — a built-in carries the i18n keys, a custom role carries the literal strings. The client renders whichever is present, because a built-in's copy must stay translatable and an author's name must never be run through a translation lookup.
  • No basedOn and no ±N delta (Yue, 2026-08-09). An earlier revision carried both to feed a Based on Member · −2 chip; nothing records which built-in seeded a role, so neither the DTO nor the screens have one. A role's N of M is the comparison that survives.
  • builtIn, permissions, memberCount, roleGatedPermissionCount and levelGatedDomains keep their present meaning.

Ordering is deterministic: the three built-ins in their canonical order, then the project's custom roles by name. A list whose order depends on insertion reshuffles between requests for no reason a reader can see, which is the same argument the DTO already makes about permission order.

Member counts stay ONE grouped read. The shipped countByRole covers built-in memberships; countByRoleDefinition covers custom ones. Two grouped reads for the whole catalog, never one query per role — and both under the same workspace context the shipped call already establishes.

Scope boundary

In: lib/dto/permissions.ts, lib/mappers/permissionMappers.ts, projectAccessService.getRoleCatalog, and the mechanical adaptation of RoleList, RoleDetail, PermissionGroups and the [roleKey] page to the widened DTO so they render a custom role correctly read-only — with the Custom chip and the Based on … provenance chip and their en + zh strings. Out: every WRITE affordance — Create role, Edit, Delete, the delete dialog and the cap state are the next card's, and this one adds no button; the editor page; the Members picker; any change to which permissions a role holds.

Acceptance criteria

  • getRoleCatalog(projectId, ctx) returns the three built-ins followed by that project's ProjectRoleDefinition rows mapped to RoleDTO, in the deterministic order above; a project with no custom roles returns exactly what it returns today, asserted against the existing expectations rather than rewritten ones.
  • The gate still runs before any read: a project in another workspace raises ProjectNotFoundError, and its custom roles are never counted or returned — asserted under a foreign workspace GUC, not only by a service-level check.
  • Every role's memberCount is real, from two grouped reads for the whole catalog; a test with three custom roles asserts the query count does not scale with the number of roles.
  • A custom role's basedOn and its ±N delta are on the DTO and computed in the mapper against BUILTIN_ROLE_PERMISSIONS; a component never derives it.
  • RoleList and RoleDetail render a custom role: its literal name and description, the Custom chip in the tint the design pairs with it, its N of M permissions and its member count — with the tile glyph taken from panel 0 of the mock rather than chosen fresh, and the built-in rows visually unchanged. No role renders a Based on … chip, which a test pins.
  • The ROLE_ICON map keyed by ProjectRole no longer breaks totality — a custom role has a defined tile treatment, and the code does not index a Record<ProjectRole, …> with a string that may not be one.
  • /settings/project/roles/[roleKey] resolves a custom role by its id and 404s an unknown segment, with no change to how the page looks the role up.
  • The new i18n key — the Custom chip — lands in messages/en.json and messages/zh.json; the catalog-parity test stays green.
  • Both screens stay read-only for every actor, including a project admin: nothing in this card renders a control.
  • The existing screen tests (tests/settings/rolesPermissionsScreens.test.tsx) and mapper tests (tests/permissions/roleCatalogMappers.test.ts) are extended rather than replaced, and the custom-role cases are added to tests/permissions/getPermissions.integration.test.ts's getRoleCatalog block.

Context refs

  • lib/services/projectAccessService.tsgetRoleCatalog and its two notes on why it is project-scoped and why the gate runs before the count.
  • lib/dto/permissions.tsRoleDTO, RoleCatalogDTO, PermissionDomainDTO and the catalog-order rule; lib/mappers/permissionMappers.tstoRoleCatalogDTO.
  • app/(authed)/settings/project/roles/_components/RoleList.tsx · RoleDetail.tsx · PermissionGroups.tsx · app/(authed)/settings/project/roles/[roleKey]/page.tsx — the four consumers, including ROLE_ICON and the AA-driven ink rules in RoleList's header comment.
  • design/projects/roles-permissions.mock.html panels 0 and 2 + the design-notes.md § Primitives composed — the custom-role tile, the Custom chip tint and the provenance chip.
  • lib/repositories/projectMembershipRepository.tscountByRole and the countByRoleDefinition the schema card adds.
  • messages/en.json / messages/zh.json — the settings.rolesPage and settings.roles namespaces.
  • The card that grew this read last and the screens it feeds; the schema card supplying the rows.