Files
RenewCMS/features/users_row_actions.feature

175 lines
10 KiB
Gherkin

Feature: Users row actions: Cancel invite and Renew invite
As the owner of a RenewCMS workspace
I want to cancel a pending invite and renew an expired one from the Users list
So that I can correct a wrong address or resend a lapsed link without leaving the page
Background:
Given /users is the verified route of features/users_screen.feature, rendered as UsersPage with
UsersTable (md and up) or UserCardList (below md), per docs/design/screens/users.md
And the backend already exists (features/invite_management.feature) and this slice changes no Go:
DELETE /v1/user/invite/{id} answers 204 with an empty body, and POST /v1/user/invite/{id}/renew
answers 200 with {id, kind "invite", email, status "pending", created_at, expires_at}
And useUsers gains cancelInvite(id), which sends DELETE /v1/user/invite/{id} with no body, and
renewInvite(id), which sends POST /v1/user/invite/{id}/renew with no body
And the actions are shown only when the session user's role from GET /v1/auth/me is "owner";
for "admin", or any other or absent value, no action button is rendered and the sixth column
stays empty; this is not drawn in docs/design, so it has no visual state and is covered
by Vitest only
And the sixth column (desktop) or the last line of the card (below md) holds at most one
action: "Cancel invite" on a Pending row, "Renew invite" on an Expired row, nothing on an
Accepted row
And each action is an outline button: 28 px high on desktop (12 px medium text, 13 px icon,
9 px side padding, --ui-border-accented border, --ui-bg fill, no wrapping) and 44 px high
below md (14 px side padding, 14 px icon); UButton variant "outline" size "xs"
And "Cancel invite" has the icon user-x and text in --ui-error; "Renew invite" has the icon
refresh-cw and text --ui-text-highlighted
And below md the action is the last line of the card, under the badges line
And "Cancel invite" opens ConfirmDialog (tone "danger") through useOverlay: title "Cancel this
invite?", description "The link sent to {email} will stop working. You can invite the address
again later.", cancel button "Keep invite", confirm button "Cancel invite", pending label
"Cancelling…"; the action runs inside the dialog, so the failure notice shows in the dialog
And "Renew invite" asks nothing and sends the request at once
And a request failure is any 500, any network failure that never reaches the server, and any
other status except 401 and 403; 401 and 403 are handled entirely by useApi's global handling
(features/api_client_global_error_handling.feature); the response body text is never shown
verbatim
And the fixture is the Default fixture of features/users_screen.feature: invite id 2
"sam.oliveira@renew.example" pending 27 Sep 2026; invite id 1 "p.huber@gmail.com" with
display_name "Paul Huber" expired 3 Aug 2026; users 3, 2, 1; the session user is "marta",
role "owner"; rows are keyed by kind and id
Scenario: Default: actions sit on the right of the Pending and Expired rows only
Given GET /v1/user/ resolves 200 with the Default fixture
When the owner navigates to /users at desktop
Then the first row "sam.oliveira@renew.example" ends with "Cancel invite" (icon user-x, text --ui-error)
And the second row "p.huber@gmail.com" ends with "Renew invite" (icon refresh-cw)
And the three Accepted rows have an empty sixth cell
And below md the same two cards end with the same buttons as 44 px buttons
Scenario Outline: Non-owners see no row actions
Given the session user has role "<role>" and GET /v1/user/ resolves 200 with the Default fixture
When the user navigates to /users at desktop and at a below-md width
Then <result>
Examples:
| role | result |
| admin | no "Cancel invite" and no "Renew invite" button exists |
| (none) | no "Cancel invite" and no "Renew invite" button exists |
| owner | "Cancel invite" and "Renew invite" are present once each |
Scenario: Cancel invite: confirm
Given the Default state has loaded
When the owner activates "Cancel invite" on the "sam.oliveira@renew.example" row
Then a danger modal opens titled "Cancel this invite?"
And its text reads "The link sent to sam.oliveira@renew.example will stop working. You can invite
the address again later."
And its buttons are "Keep invite" and "Cancel invite" (danger), and no request has been sent
Scenario: Cancel invite: "Keep invite" closes the dialog and changes nothing
Given the "Cancel this invite?" dialog is open for "sam.oliveira@renew.example"
When the owner activates "Keep invite"
Then the dialog closes, DELETE /v1/user/invite/2 is never sent and all 5 rows remain
Scenario: Cancel invite: sending
Given the "Cancel this invite?" dialog is open for "sam.oliveira@renew.example"
When the owner activates "Cancel invite" and DELETE /v1/user/invite/2 has not resolved
Then DELETE /v1/user/invite/2 was sent once
And the confirm button reads "Cancelling…" with a loading indicator
And "Keep invite" is disabled
And Esc and a backdrop click do not close the dialog
Scenario Outline: Cancel invite: failed
Given the "Cancel this invite?" dialog is open for "sam.oliveira@renew.example"
When the owner activates "Cancel invite" and DELETE /v1/user/invite/2 <failure>
Then the dialog stays open, with an error notice inside it reading "Couldn't cancel the invite.
Try again in a moment."
And "Keep invite" and "Cancel invite" are enabled again
And all 5 rows remain and no toast is raised
Examples:
| failure |
| resolves with a 500 response |
| resolves with a 500 response whose body is some other text |
| never reaches the server |
| resolves with a 404 response |
Scenario: Cancel invite: retrying after a failure
Given the dialog shows the failure notice
When the owner activates "Cancel invite" again and DELETE /v1/user/invite/2 resolves 204
Then DELETE /v1/user/invite/2 was sent a second time
And the dialog closes and the row is removed
Scenario: Cancel invite: done
Given the "Cancel this invite?" dialog is open for "sam.oliveira@renew.example"
When the owner activates "Cancel invite" and DELETE /v1/user/invite/2 resolves 204
Then the dialog closes
And the row "sam.oliveira@renew.example" is gone and the other 4 rows remain in their order
And GET /v1/user/ is not called again
And a toast appears: color success, icon i-lucide-circle-check, id "invite-cancelled-2", title
"Invite cancelled", description "sam.oliveira@renew.example can no longer use the link."
Scenario: Cancel invite: an Expired row can't be cancelled from the list
Given the Default state has loaded
Then the "p.huber@gmail.com" row offers "Renew invite" and no "Cancel invite"
Scenario: Renew invite: sending
Given the Default state has loaded
When the owner activates "Renew invite" on the "p.huber@gmail.com" row and
POST /v1/user/invite/1/renew has not resolved
Then POST /v1/user/invite/1/renew was sent once with no body and no confirmation was shown
And on that row the button reads "Renewing…" with the icon loader-circle at 50 % opacity, disabled
And no other row's button changes and the "Cancel invite" button of "sam.oliveira@renew.example" stays enabled
And a second activation of the busy button sends nothing
Scenario: Renew invite: done
Given POST /v1/user/invite/1/renew was sent from the "p.huber@gmail.com" row
When it resolves 200 with id 1, kind "invite", email "p.huber@gmail.com", status "pending",
created_at now and expires_at 3 days later
Then the row moves to the top of the list as Pending, created "Today", drawn with the
--ui-bg-elevated background, keeping its display name "Paul Huber" and role "admin", and now
ends with "Cancel invite"
And "sam.oliveira@renew.example" is now the second row and the other rows are unchanged
And exactly one row exists for "p.huber@gmail.com" and GET /v1/user/ is not called again
And a toast appears: color success, icon i-lucide-circle-check, id "invite-renewed-1", title
"Invite renewed", description "p.huber@gmail.com has 3 days to accept the new link."
Scenario: Renew invite: renewing a Pending row is not offered
Given the Default state has loaded
Then the "sam.oliveira@renew.example" row offers "Cancel invite" and no "Renew invite"
Scenario Outline: Renew invite: failed
Given the Default state has loaded
When the owner activates "Renew invite" on the "p.huber@gmail.com" row and
POST /v1/user/invite/1/renew <failure>
Then a toast appears: color error, icon i-lucide-circle-x, id "invite-renew-failed-1", title
"Couldn't renew the invite", description "Try again in a moment."
And the row stays Expired in its place, with "Renew invite" enabled again
And the list is otherwise unchanged
Examples:
| failure |
| resolves with a 500 response |
| resolves with a 500 response whose body is some other text |
| never reaches the server |
| resolves with a 404 response |
Scenario: Renew invite: retrying after a failure
Given the failed toast has appeared for "p.huber@gmail.com"
When the owner activates "Renew invite" again and POST /v1/user/invite/1/renew resolves 200
Then the "Invite renewed" toast is raised and the row is Pending on top
Scenario: Renew invite: only the busy row is busy
Given the fixture also holds Expired invite id 7 "old@renew.example"
When the owner activates "Renew invite" on "p.huber@gmail.com" and it has not resolved
Then "Renew invite" on "old@renew.example" is still enabled and can be activated
Scenario: Actions after a new invite is sent
Given the Default state has loaded and an invite for "lea.martin@renew.example" was sent (201)
Then the new first row ends with "Cancel invite"
Scenario: Cancelling a just-renewed row
Given "p.huber@gmail.com" was renewed and is the first row
When the owner confirms "Cancel invite" for it and DELETE /v1/user/invite/1 resolves 204
Then the row is gone and the toast "Invite cancelled" is raised