175 lines
10 KiB
Gherkin
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
|