feat: SPA Roles screens
This commit is contained in:
1 parent
907949aa12
commit
3db490a615
47 files changed
+6947
-9
No files matched your search
@@ -0,0 +1,270 @@
|
||||
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
|
||||
Reference in new issue
Block a user