feat: users row actions "Cancel invite" on Pending rows and "Renew invite" on Expired rows
This commit is contained in:
1 parent
1ece778caf
commit
142325730d
14 files changed
+1156
-35
No files matched your search
@@ -0,0 +1,174 @@
|
||||
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
|
||||
Reference in new issue
Block a user