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

Deleting a custom role — the `Delete` affordance on the detail screen and the reassign dialog behind it

The one destructive surface in this story: a project admin removes a role they authored, and the people holding it land somewhere deliberate rather than nowhere.

The flow

Delete sits on a custom role's detail screen only — a built-in's head carries a lock and no control at all, which is what the shipped screen already renders. Pressing it calls DELETE with no destination, which is the cheap way to learn the truth: the API answers 409 with the member count, and that count is what the dialog opens with.

  • Nobody holds the role — a plain destructive confirm, no picker.
  • Somebody holds it — the dialog names how many, and requires a destination before its confirm is enabled: another of the project's roles, built-in or custom, never this one. Confirming re-issues DELETE with the destination, and the move and the removal happen together on the server.

After a successful delete the admin is returned to the role list, which no longer contains the role and shows the destination's member count increased. That page-state routing follows the shipped settings convention rather than a hand-rolled refetch.

Where this sits beside its siblings

This card owns exactly two elements of the roles screens — the Delete button and its dialog. Create role and Edit belong to the editor card, which owns them because they are the doors into the surface it builds; this card adds neither, and the two cards touch the same two files in different places. Both read the actor's project:manage_access from the server component through the shipped projectAccessService, so neither invents a capability read.

Scope boundary

In: the Delete button on a custom role's detail screen, the reassign dialog in both its states, the client call and its refusal handling, the post-delete navigation, and the en + zh strings for all of it. Out: Create role and Edit, which the editor card owns; the cap-reached state, which sits on Create role; any change to what the screens DISPLAY about a role, which the read card owns; hiding the Roles entry from an actor who cannot use it, which is the permission-gated UI story's.

Acceptance criteria

  • A custom role's detail screen renders Delete for an actor holding project:manage_access, and renders it for nobody else; a built-in role's screen renders no delete affordance for any actor, including a workspace owner.
  • The dialog composes the shipped Modal and matches the amended asset — the role name, the member count when it is non-zero, the destination picker, and a destructive confirm — with no new dialog grammar invented.
  • With members, the confirm is disabled until a destination is chosen; the picker lists the project's other roles and never the role being deleted.
  • With no members, no picker is shown and the confirm is immediately available.
  • The member count the dialog displays comes from the 409 response body, not a second request and not a client-side count.
  • A refusal returned on confirm — the role was deleted by someone else, the destination vanished, permission was lost — surfaces as a toast with its own message and leaves the dialog open; the screen never silently no-ops.
  • After a successful delete the admin lands on the role list with the role gone and the destination's member count updated, through the shipped page-state-after-mutation routing for a settings surface.
  • The dialog is keyboard-operable and focus-managed like every other Modal in the product: focus moves in on open, Esc closes, focus returns to Delete.
  • Every string is in messages/en.json and messages/zh.json; no literal copy in the component. Inks follow the asset's measured contrast rules — --el-text-faint carries no text WCAG measures.
  • Component tests cover both dialog states, the disabled-confirm rule, the refusal path, and the built-in-role case where no button exists.

Context refs

  • design/projects/roles-permissions.mock.html panel 2 (the custom role's head with Edit / Delete) and the dialog panel the design amendment adds; design/projects/design-notes.md § Panels and its contrast table.
  • app/(authed)/settings/project/roles/_components/RoleDetail.tsx — the screen this extends.
  • app/(authed)/settings/project/workflow/_components/WorkflowEditor.tsx — the shipped ReassignModal, the in-use-count-from-the-refusal pattern, and its toast handling.
  • components/ui/Modal.tsx · components/ui/Combobox.tsx · components/ui/Button.tsx — the primitives composed.
  • motir-core/CLAUDE.md — the page-state-after-mutation routing contract for a settings surface.
  • The API card — the DELETE route, its 409 shape and the count it carries; the design amendment — the dialog this builds to.