Files
RenewCMS/features/roles_spa_screens.feature
T

271 lines
21 KiB
Gherkin

Feature: SPA Roles screens (slice 6)
As an owner or a person who manages roles
I want to list, build, preview, delete and reset roles in the app
So that I can create a Contributor role in under a minute without leaving /settings
Background:
Given the backend contract of features/roles_permissions_roles_api.feature and roles_permissions_users_api.feature
And the spec of record is docs/design/screens/roles.md sections 1 to 4, 7 and 11, the tables "No-escalation feedback" and "HTTP status to copy", and routes.md "Roles and permissions"
And slice 5 is reused, not respecified: stores/permissions.ts (can, scopeOf), test/access.ts, NoAccessState, OwnerOnlyTag, ReadOnlyBanner, ViewOnlyMark, the meta.access route guard, and useApi 403/409 handling and refusalCopy
And a refusal is plain text: role_locked, own_role, role_built_in, built_in_name, role_name_taken, role_conflict, role_has_members, escalation (X-Escalation-Key), missing_permission (X-Missing-Permission), invalid_reassign, role_not_found
And GET /v1/role items carry id, name, description, built_in, locked, version, grants [{key, scope}], members, updated_at, updated_by {id, name} or null
And GET /v1/permission answers groups [{key, items: [{key, scope_kind, owner_only, per_type}]}] and gives the order and the kind of each control, human names come from permissionNames
And the SPA lives in web/src, Nuxt UI only, design tokens only, EN and FR copy in i18n/messages.ts, no comments in code
And design copy marked TBD is not asserted by a test; it is listed in the qa as a design gap
Rule: Routes, redirect and navigation
Scenario Outline: Route access
Given the session grants <grants>
When the person opens <route>
Then <result>
Examples:
| grants | route | result |
| roles.view | /settings/roles | the Roles list renders |
| roles.view | /settings/roles/new | the page-level 403 state, URL unchanged |
| roles.view, roles.manage | /settings/roles/new | the empty editor renders |
| roles.view | /settings/roles/4 | the editor renders (read-only without manage) |
| content.read=all | /settings/roles | the page-level 403 state "Your role, Contributor, doesn't include it..." with "Go to Articles" |
| content.read=all | /settings/roles/4 | the page-level 403 state |
Scenario: /settings/permissions is retired
When the person opens /settings/permissions
Then the router replaces it with /settings/roles
And no 403 state flashes
Scenario: The sidebar and drawer gain Roles
Given the session grants roles.view
Then Settings shows an "Access" group with "Roles" (shield icon) in the sidebar and the mobile drawer
And without roles.view the entry is absent in both
And there is no "Permissions" entry anywhere
Rule: Roles list (section 1)
Scenario: Default
Given GET /v1/role returns Owner (locked), Admin, Writer, Contributor (4 members), Content lead
Then the table shows Role, Description, Members, Type with a Locked badge on Owner, "Built-in" on the three built-ins and "Custom" on the rest
And the header reads "Roles" with "Who can do what on this site. Each user has one role." and an enabled "New role"
And on mobile each role is a card
Scenario: Loading, only built-ins, error
Then the first load shows skeleton rows after 150 ms and "New role" disabled
And when the list holds only the three built-ins it reads "No custom roles yet" / "The three built-in roles cover most sites. Create a role when someone needs something in between." with "New role"
And on a 500 or network failure it reads "Couldn't load roles" / "GET /v1/role failed. Check your connection and try again." with "Try again" that re-requests the list
Scenario: No permission to edit
Given the session grants roles.view and not roles.manage
Then "New role" is disabled with the tooltip "You can see roles, but only people who manage roles can create or change them."
And a banner reads "You can see roles but not change them" / "Ask an owner or admin if a role needs to change."
Scenario Outline: Row menu
Given the person <who> opens the menu on <role>
Then the items are Edit, Duplicate and Delete, and Delete is <delete>
Examples:
| who | role | delete |
| holds roles.manage | Contributor | enabled |
| is the owner | Writer | disabled, tooltip "Built-in roles can't be deleted. Duplicate it to make a custom role." on focus and hover |
| holds roles.manage | Owner | disabled (locked) |
| holds roles.view only | Contributor | Duplicate and Delete disabled, Edit opens the read-only editor |
Rule: Role editor (section 2)
Scenario: New role (empty)
Given the person holds roles.manage
When they open /settings/roles/new
Then nothing is granted, the title is "New role", Save changes is disabled with "Name your role to save it"
And the form shows Name ("For example, Contributor"), Description (optional, "What this role is for"), Content permissions with Quick presets Read only / Write own / Full content, the grid (Read, Create, Update, Delete, Publish) with "All content types", Articles and the other types, then General permissions with "Expand all"
Scenario Outline: The grid controls follow the catalogue
Then <cell> renders as <control>
And the keyboard and announcement contract of docs/design/components.md (roving tabindex, Home and End, a polite live region "Events, Update, set to Own.") holds
Examples:
| cell | control |
| Read, Update, Delete, Publish | a radiogroup named "{Type}: {Action}" with None, Own, All |
| Create | a switch named "{Type}: Create" with On and Off |
| General rows with scope_kind none-own-all | the same None / Own / All control |
| General boolean rows | a switch with a visible On/Off label |
| Schema row of the grid | a "Schema locked" tag and no control |
Scenario: Editing a custom role
Given GET /v1/role/4 returns Contributor, 4 members, grants content.read all, content.create, content.update own, content.update:case-studies all
Then Name and Description are filled, "4 members" and "Custom" show
And the Case studies row shows "Custom" with a "Reset" link
And "Reset" removes that type's typed grants and the row follows the "All content types" row again
Scenario: Unsaved changes
When three cells change
Then "3 unsaved changes" shows with Discard and Save changes, changed cells carry a square marker and the preview tags the changed line "Changed"
And Discard restores the loaded values
Scenario: Saving then saved
When Save changes is pressed with the current version
Then PUT /v1/role/4 {name, description, grants, version} is sent
And while in flight fields dim, Save shows the loader, the bar reads "Saving…" and Discard is disabled
And on 200 the bar closes, the form takes the answer (version +1) and a toast reads "Role saved" / "Contributor is updated for 4 members."
Scenario Outline: Creating
When the person saves a new role named "Contributor" with <grants>
Then POST /v1/role is sent with only those grants and the toast reads "Role created" / "Contributor is ready. Assign it from Users."
And the router moves to /settings/roles/<new id>
Examples:
| grants |
| nothing granted |
| the Write own preset |
Scenario Outline: Presets
When the person presses <preset>
Then every type row is set to <result>, the pressed preset has aria-pressed true, and unsaved changes are counted
Examples:
| preset | result |
| Read only | Read All, Create Off, Update/Delete/Publish None |
| Write own | Read All, Create On, Update Own, Delete None, Publish None |
| Full content | Read All, Create On, Update All, Delete All, Publish All |
Scenario Outline: Errors on save
When the server answers <status> <code> to the save
Then <effect>
And the form keeps the person's edits
Examples:
| status | code | effect |
| (none) | empty name | nothing is sent; field error "Give the role a name." and "1 problem to fix" |
| 400 | invalid_role_name | the same "Give the role a name." |
| 409 | role_name_taken | field error 'A role named "Writer" already exists. Choose a different name.' and "1 problem to fix" |
| 409 | role_conflict | the Conflict state |
| 403 | missing_permission (roles.manage) | the Permission lowered state |
| 403 | escalation | the "You can't grant that" / "You can only grant what your role has." message, X-Escalation-Key names the row |
| 403 | role_locked | the screen shows "This role is locked" and the editor turns read-only |
| 403 | own_role | the screen shows "You can't change your own role" / "Ask another owner or admin." |
| 400 | built_in_name | "Built-in roles keep their name." |
Scenario: Conflict
Given Contributor was loaded at version 2 and the server answers 409 role_conflict
When the SPA re-reads GET /v1/role/4 (updated_by "Jonas Weber", updated_at 11:02)
Then it reads "This role changed while you were editing" / "Jonas Weber saved changes to Contributor at 11:02. Reload to see them, or review how they differ from your edits."
And "Reload role" replaces the form with the server role, "Review differences" is offered (copy TBD), "2 unsaved changes. Save is off until you reload" shows, and Save is disabled until a reload
Scenario: Permission lowered while open (403)
When a 403 missing_permission answers the save or a refresh
Then the page becomes read-only, "Your access changed" / "An owner changed your role while this page was open. You can still see roles, but you can't change them. Your edits weren't saved." with "Reload page"
And "Reload page" refreshes the session and the role
Scenario Outline: Read-only editor
Given the person <who> opens <role>
Then <result>
And the owner-only gate is can("roles.edit_owner_only") on a built-in role, never a role name
Examples:
| who | role | result |
| is the owner | Writer, 5 members | the warning "This role has 5 members" / "Changes apply to all of them when you save, and to the API keys they created. Built-in roles can be restored with Reset to default." and "Built-in" |
| is an admin | Writer | everything disabled, "Only the owner can edit built-in roles" / "You can read what Writer allows. To change it, duplicate it into a custom role." with "Duplicate role" |
| holds roles.view only | Contributor | fields locked, ReadOnlyBanner, no Save bar |
| holds roles.manage | their own role | read-only, "You can't change the permissions of your own role. Ask another owner or admin." |
| anyone with roles.view | Owner | "The Owner role is locked" / "It always has every permission. To hand it to someone else, transfer ownership from Users." with "Go to Users" and a Locked badge |
Scenario: General permissions
Then groups are collapsible from GET /v1/permission (Activity, Users and invites, Settings, Roles, System, Schema, Media, Authors), each reads "x of y enabled", for example "2 of 2 enabled"
And rows show the names and descriptions of the design, for example "See activity" / "Read the log of who did what."
And "Expand all" opens every group
Scenario: Owner-only items
Given the person is an admin
Then Delete a site, Edit roles with owner-only power, Apply software updates, Edit activity retention and Transfer ownership are disabled, off, with a lock and the "Owner only" tag (OwnerOnlyTag)
And no request ever sends an owner-only key on a non-Owner role
Scenario: Escalation locks
Given the session grants content.publish=own, roles.manage
Then in the editor Publish "All" is disabled with a lock, described by "You can only grant what your role has. Your role can publish only entries you created, so All isn't available."
And the popover opens on focus and on hover and "None" and "Own" stay available
And a typed override in the session (for example content.update:article=none) locks the matching cell the same way
Rule: Plain-language preview (section 3)
Scenario: Live panel
When Update changes from Own to All on a Contributor with 4 members
Then the card "What this role can do" / "Updates as you edit." shows Content ("All content types" with its sentence) and Everything else lines, a last line "No access to Activity, Users and invites, Settings, Roles." and "Applies to 4 members, and to the API keys they created."
And the changed line carries "Changed"
And sentences come from fixed templates, never from keys
Scenario Outline: Preview variants
Then <case> shows <copy>
Examples:
| case | copy |
| Contributor sentence | "Can read any entry, create entries and edit only their own. Can't delete and publish." |
| read-only built-in role | "A summary of what this role allows." and no "Updates as you edit." |
| new role, nothing granted | "No access to entries." and "No access to Schema, Media, Authors, Activity, Users and invites, Settings, Roles." |
| types sharing permissions | one line per group of types |
Scenario: Mobile sheet
Given a 390 px viewport
When "What this role can do" in the sticky bar is tapped
Then a 92% bottom sheet opens with a grabber and "Close", and the grid is one card per type
Rule: Delete and reassign (section 4)
Scenario: Blocked: has members
When Delete is chosen on Contributor (4 members)
Then the dialog "Delete Contributor?" reads "4 members have this role. Move them to another role first. Then Contributor can be deleted."
And "Move members to" offers "Choose a role", "They keep their API keys. Keys follow the new role.", and "Move 4 members and delete role" stays disabled until a role is chosen
Scenario Outline: Role picker
Then <role> is <state>
Examples:
| role | state |
| Owner | not offered (one owner) |
| Contributor | not offered (the source) |
| Writer | enabled with its summary |
| Content lead | enabled for an owner; for a caller who cannot cover it, disabled with "Your role can't assign a role with more power than yours." |
| Admin | the same rule |
Scenario: Move then delete
When the person picks Writer and confirms
Then POST /v1/role/4/reassign {target_role_id} is sent, then DELETE /v1/role/4
And on both 2xx the dialog closes and the row disappears; the failure of either keeps the dialog open with the refusal copy
Scenario Outline: Delete answers
Then <code> shows <copy>
Examples:
| code | copy |
| role_has_members | the Blocked state with the live count (a member joined meanwhile) |
| role_locked | "This role is locked" / "The Owner role can't be changed or deleted. Transfer ownership from Users instead." |
| role_built_in | "Built-in roles can't be deleted" / "Duplicate it to make a custom role." |
| escalation | "You can't grant that" / "You can only grant what your role has." |
| role_not_found | the list refreshes and the row is gone |
Scenario: Confirm: no members
When Delete is chosen on Intern (0 members)
Then "Delete Intern?" / "No one has this role. This can't be undone." / "Deleting a role doesn't affect content." with Cancel and "Delete role"; DELETE /v1/role/{id} only, no reassign call
Scenario: Built-in role dialog (component only)
Then the dialog for Writer reads "Delete Writer?" / "Built-in roles can't be deleted." / "Writer is built in" / "Duplicate it to make a custom role you can change or delete." with "Duplicate role" and "Close"
And it is not reachable by URL or menu; the spec is at component level
Rule: Reset to default
Scenario: Owner resets a built-in role
Given the owner opens an edited Writer
When they confirm the reset (copy TBD)
Then POST /v1/role/{id}/reset {version} is sent, the form takes the seeded grants and version +1, and a toast confirms (copy TBD)
Scenario Outline: Reset visibility and answers
Then <case>
Examples:
| case |
| the control is absent for a custom role, for the Owner role, and without roles.edit_owner_only |
| 409 role_conflict shows the Conflict state |
| 403 role_locked / missing_permission use the shared refusal copy |
Rule: Duplicate
Scenario: Duplicate a role
When Duplicate is chosen on Writer
Then the editor opens at /settings/roles/new prefilled with Writer's grants, an empty name and a note that nothing is saved yet (copy TBD)
And saving sends POST /v1/role with no built_in or locked field
Rule: Create a Contributor in under a minute (section 11)
Scenario: The six steps
Given the person holds roles.manage
When they press New role, type "Contributor" and "Writes posts and edits only their own", press Write own and Save changes
Then POST /v1/role carries name, description and the Write own grants
And the toast "Role created" / "Contributor is ready. Assign it from Users." shows, the bar closes and the role is in the list and in the invite picker