feat: UsersTable and UserCardList get Email, Display name (dash when empty), Role (new RoleBadge), Invite status and Created
This commit is contained in:
1 parent
153fd8719a
commit
1ece778caf
19 files changed
+485
-204
No files matched your search
+103
-43
@@ -13,10 +13,10 @@ Feature: Users screen and InviteUserModal
|
||||
/v1/user/invite; both endpoints already exist (features/invitations.feature) and this slice
|
||||
wires the SPA to them unchanged, with no Go change
|
||||
And the list items keep the API shape: user items are kind "user" (id, username, display_name,
|
||||
email, role, status "accepted", created_at) or kind "invite" (id, email, status "pending" or
|
||||
"expired", created_at); SPA display of role, owner-only actions and the redesigned columns is
|
||||
a later SPA slice; this amendment covers the API shape only; the list is used in the order the API returns it (created date, newest first),
|
||||
and rows are keyed by kind and id, because a user and an invite can share a numeric id
|
||||
email, role "owner" or "admin", status "accepted", created_at) or kind "invite" (id, email,
|
||||
status "pending" or "expired", created_at); the list is used in the order the API returns it
|
||||
(created date, newest first), and rows are keyed by kind and id, because a user and an
|
||||
invite can share a numeric id
|
||||
And DashboardShell's navigation gains "Users" (icon users, linking to /users) between
|
||||
"Articles" and "Integration", in the sidebar and in the mobile drawer, so it lists Home,
|
||||
Articles, Users, Integration, Settings in that order; "Users" has aria-current="page" on /users; the
|
||||
@@ -24,32 +24,51 @@ Feature: Users screen and InviteUserModal
|
||||
dashboard_shell_branding.feature are amended accordingly
|
||||
And the PageHeader shows breadcrumb "Users" (mobile title "Users")
|
||||
And the page header is the title "Users" with the subtitle "Everyone who can sign in and work
|
||||
on your articles." and, on desktop (md and up), a primary "Invite user" button (icon
|
||||
user-plus) at the right of the header
|
||||
And below md, "Invite user" is instead a full-width 48 px button (icon user-plus) pinned 16 px
|
||||
from the left, right and bottom of the screen with the popover shadow, and it is hidden
|
||||
while InviteUserModal is open
|
||||
And the list is rendered as UsersTable (desktop, md and up: columns Username, Invite status,
|
||||
Created, grid minmax(0,1fr) 160px 160px, 36 px header, 44 px rows, an arrow-down icon on the
|
||||
Created header) or UserCardList (below md: 68 px minimum row height, the name line, then the
|
||||
InviteStatusBadge and "Created {date}"), both fed by the same items array; rows have no
|
||||
actions and are not clickable
|
||||
And a user row's name cell shows the display name first (medium weight, highlighted) with the
|
||||
username beside it in muted text when display_name is not empty, otherwise the username
|
||||
alone (medium weight, highlighted); an invite row shows the email address in regular weight,
|
||||
toned
|
||||
And the row whose kind is "user" and whose id equals the session user's id (from GET
|
||||
/v1/auth/me) carries a muted mono "you" tag after the name
|
||||
on your articles." and, on desktop (md and up) for the owner, a primary "Invite user" button
|
||||
(icon user-plus) at the right of the header
|
||||
And below md, for the owner, "Invite user" is instead a full-width 48 px button (icon
|
||||
user-plus) pinned 16 px from the left, right and bottom of the screen with the popover
|
||||
shadow, and it is hidden while InviteUserModal is open
|
||||
And "Invite user" (desktop header button and the mobile pinned button) is shown only when the
|
||||
session user's role from GET /v1/auth/me is "owner"; for "admin", or any other or absent
|
||||
value, neither button is rendered, the pinned button's bottom space is not reserved, and
|
||||
the "Only you" hint is hidden; this is not drawn in docs/design, so it has no visual state
|
||||
and is covered by Vitest only
|
||||
And the list is rendered as UsersTable (desktop, md and up) or UserCardList (below md), both
|
||||
fed by the same items array; rows have no actions and are not clickable
|
||||
And UsersTable has the columns Email, Display name, Role, Invite status and Created, plus an
|
||||
unlabeled, empty actions column kept so the grid matches the board: grid
|
||||
minmax(0,1.3fr) minmax(0,1fr) 84px 120px 110px 150px with a 12 px gap, a 36 px header on
|
||||
--ui-bg-elevated, 48 px rows with a top border, and an arrow-down icon on the Created header
|
||||
And UserCardList (below md) has a 68 px minimum row height: the email line (with the "you"
|
||||
tag), the display name below it only when there is one, then the RoleBadge, the
|
||||
InviteStatusBadge and "Created {date}" on one line
|
||||
And the username is not shown anywhere in the list
|
||||
And the Email cell shows the account's or invite's email address, medium weight, highlighted;
|
||||
the row whose kind is "user" and whose id equals the session user's id (from GET
|
||||
/v1/auth/me) carries a muted mono "you" tag after it
|
||||
And the Display name cell shows the item's display_name, highlighted, or a muted dash "—" when
|
||||
it is empty or absent; an invite item shows the dash unless it carries a display_name; the
|
||||
current user's row shows their saved name
|
||||
And RoleBadge is a 20 px badge, IBM Plex Mono 11 px medium, 7 px side padding, no icon, showing
|
||||
the word: owner (filled --ui-primary, --ui-bg text, --ui-primary border) or admin
|
||||
(transparent, --ui-text-toned text, --ui-border-accented border); a user row shows its
|
||||
role, and an invite row shows "admin", the role it will create
|
||||
And InviteStatusBadge shows icon and word, never color alone: Accepted (circle-check, success
|
||||
soft), Pending (mail, warning soft), Expired (clock-alert, transparent, muted, with an
|
||||
accented border)
|
||||
And the Created column shows the date as "27 Sep 2026", or "Today" when it is the current day;
|
||||
the fixture below uses absolute dates only
|
||||
And skeleton rows (5) show only after 150 ms of loading, so a fast response never flashes them
|
||||
And skeleton rows (5) show only after 150 ms of loading, so a fast response never flashes them,
|
||||
one bar per column plus a RoleBadge placeholder and an InviteStatusBadge placeholder in the
|
||||
table (md and up); the below-md card skeleton keeps the board's two-bar shape
|
||||
And the Default fixture holds 5 items, newest first: invite id 2 "sam.oliveira@renew.example"
|
||||
pending 27 Sep 2026; invite id 1 "p.huber@gmail.com" expired 3 Aug 2026; user id 3 "ines"
|
||||
with display_name "Inès Duarte" accepted 18 Jun 2026; user id 2 "jonas" accepted 2 Apr 2026;
|
||||
user id 1 "marta" accepted 12 Mar 2026; the session user is id 1 "marta"
|
||||
pending 27 Sep 2026; invite id 1 "p.huber@gmail.com" with display_name "Paul Huber" expired
|
||||
3 Aug 2026; user id 3 "ines" email "ines.duarte@renew.example" display_name "Inès Duarte"
|
||||
role admin accepted 18 Jun 2026; user id 2 "jonas" email "jonas@renew.example" no display
|
||||
name, role admin, accepted 2 Apr 2026; user id 1 "marta" email "marta@renew.example" no
|
||||
display name, role owner, accepted 12 Mar 2026; the session user is id 1 "marta", role
|
||||
"owner"
|
||||
And InviteUserModal is a UModal opened from either "Invite user" button; every time it opens it
|
||||
starts in its default state: title "Invite a user", text "We'll email them a link to choose
|
||||
a username and password. The link expires after 3 days.", an empty, focused Email field
|
||||
@@ -87,23 +106,32 @@ Feature: Users screen and InviteUserModal
|
||||
form is not disabled by an in-flight request; focus returns to the button that opened it
|
||||
And a 401 and a 403 (unverified) on either endpoint are handled entirely by useApi's global
|
||||
handling (features/api_client_global_error_handling.feature)
|
||||
And this slice does not build the accept-invite screen (/invite/accept), does not add row
|
||||
actions (remove, promote, revoke or resend), and does not change how display names look in
|
||||
the Profile group's "Display names in the Users list" state
|
||||
And this slice does not build the accept-invite screen (/invite/accept) and does not add row
|
||||
actions (Cancel invite, Renew invite); the "Display names in the Users list" state of the
|
||||
Profile group is drawn by this same list
|
||||
|
||||
Scenario: Default — the list loads newest first, with display name, "you" tag and status badges
|
||||
Given GET /v1/user/ resolves 200 with the 5-item Default fixture and the session user is "marta"
|
||||
Scenario: Default — the list loads newest first with email, display name, role, status and created
|
||||
Given GET /v1/user/ resolves 200 with the 5-item Default fixture and the session user is
|
||||
"marta" with role "owner"
|
||||
When the user navigates to /users
|
||||
Then the breadcrumb reads "Users" (mobile title "Users")
|
||||
And the header shows the title "Users", the subtitle and, on desktop, "Invite user"
|
||||
And the rows appear in exactly the fixture's order
|
||||
And the first row shows "sam.oliveira@renew.example" (toned), a Pending badge and "27 Sep 2026"
|
||||
And the second row shows "p.huber@gmail.com", an Expired badge and "3 Aug 2026"
|
||||
And the third row shows "Inès Duarte" first with "ines" beside it in muted text, an Accepted
|
||||
And the table header reads Email, Display name, Role, Invite status, Created, and an empty
|
||||
unlabeled sixth cell
|
||||
And the rows appear in exactly the fixture's order, each 48 px high
|
||||
And the first row shows "sam.oliveira@renew.example", "—" (muted), "admin", a Pending badge
|
||||
and "27 Sep 2026"
|
||||
And the second row shows "p.huber@gmail.com", "Paul Huber", "admin", an Expired badge and
|
||||
"3 Aug 2026"
|
||||
And the third row shows "ines.duarte@renew.example", "Inès Duarte", "admin", an Accepted
|
||||
badge and "18 Jun 2026"
|
||||
And the fifth row shows "marta" with the "you" tag, an Accepted badge and "12 Mar 2026"
|
||||
And below md the same rows render as cards, each with the name line, then the badge and
|
||||
"Created {date}"
|
||||
And the fourth row shows "jonas@renew.example", "—", "admin", an Accepted badge and "2 Apr 2026"
|
||||
And the fifth row shows "marta@renew.example" with the "you" tag, "—", "owner" (filled), an
|
||||
Accepted badge and "12 Mar 2026"
|
||||
And the username "ines", "jonas" or "marta" appears in no row
|
||||
And below md the same rows render as cards: the email line, the display name below it only
|
||||
for "p.huber@gmail.com" and "ines.duarte@renew.example", then the RoleBadge, the status
|
||||
badge and "Created {date}" on one line
|
||||
|
||||
Scenario: Navigation — "Users" sits between Articles and Integration
|
||||
Given the user is on /users
|
||||
@@ -115,19 +143,51 @@ Feature: Users screen and InviteUserModal
|
||||
Given GET /v1/user/ has not yet resolved
|
||||
When the user navigates to /users
|
||||
Then nothing is drawn in place of the list for the first 150 ms
|
||||
And after 150 ms 5 skeleton rows replace the list, and the list container is aria-busy="true"
|
||||
And after 150 ms 5 skeleton rows replace the list, on desktop each with one bar per column
|
||||
plus a RoleBadge and an InviteStatusBadge placeholder and below md in the board's two-bar
|
||||
card shape, and the list container is aria-busy="true"
|
||||
When GET /v1/user/ then resolves 200 with the Default fixture
|
||||
Then the skeleton rows are replaced by the Default state's rows
|
||||
|
||||
Scenario: Only you — nobody has been invited yet
|
||||
Given GET /v1/user/ resolves 200 with only user id 1 "marta", accepted 12 Mar 2026, and the
|
||||
session user is "marta"
|
||||
Given GET /v1/user/ resolves 200 with only user id 1 "marta" (email "marta@renew.example",
|
||||
role "owner"), accepted 12 Mar 2026, and the session user is "marta" with role "owner"
|
||||
When the user navigates to /users
|
||||
Then the list has one row: "marta" with the "you" tag, an Accepted badge and "12 Mar 2026"
|
||||
Then the list has one row: "marta@renew.example" with the "you" tag, "—", "owner", an
|
||||
Accepted badge and "12 Mar 2026"
|
||||
And under the list the hint reads "Invite someone so they can sign in with their own account
|
||||
instead of sharing yours."
|
||||
And "Invite user" is still available
|
||||
|
||||
Scenario: Display names in the Users list — saved names show, empty ones show a dash
|
||||
Given GET /v1/auth/me resolves 200 for user id 1 with display_name "Marta Alves" and role "owner"
|
||||
And GET /v1/user/ resolves 200 with the Default fixture where user id 1 has display_name
|
||||
"Marta Alves"
|
||||
When the user navigates to /users
|
||||
Then the row "marta@renew.example" (you) shows "Marta Alves", "owner" and an Accepted badge
|
||||
And the row "ines.duarte@renew.example" shows "Inès Duarte", "admin" and an Accepted badge
|
||||
And the row "jonas@renew.example" shows a muted "—", "admin" and an Accepted badge
|
||||
And the sidebar chip also shows "Marta Alves"
|
||||
|
||||
Scenario Outline: Non-owners cannot invite — "Invite user" and the hint are not rendered
|
||||
Given the session user is "<username>" with role "<role>" and GET /v1/user/ resolves 200
|
||||
with <fixture>
|
||||
When the user navigates to /users at desktop and at a below-md width
|
||||
Then <invite>
|
||||
And the list still renders normally
|
||||
And <hint>
|
||||
|
||||
Examples:
|
||||
| username | role | fixture | invite | hint |
|
||||
| jonas | admin | the Default fixture | no "Invite user" button is rendered, header or pinned | the pinned button's space is not reserved |
|
||||
| jonas | admin | only user id 1 "marta" and user id 2 "jonas" | no "Invite user" button is rendered, header or pinned | the "Invite someone…" hint is not shown |
|
||||
| marta | owner | the Default fixture | "Invite user" is rendered in the header (desktop) and pinned (below md) | the list is unchanged |
|
||||
|
||||
Scenario: An unknown or missing role is treated as non-owner
|
||||
Given GET /v1/auth/me resolves 200 without a role
|
||||
When the user navigates to /users
|
||||
Then no "Invite user" button is rendered
|
||||
|
||||
Scenario Outline: Error — a 500 or a network failure replaces the list with an alert and a retry
|
||||
Given GET /v1/user/ <failure>
|
||||
When the user navigates to /users
|
||||
@@ -185,8 +245,8 @@ Feature: Users screen and InviteUserModal
|
||||
"lea.martin@renew.example", status "pending", created_at now, expires_at 3 days later
|
||||
Then the modal closes and focus returns to "Invite user"
|
||||
And GET /v1/user/ is not called again
|
||||
And the list has 6 rows, the first being "lea.martin@renew.example" with a Pending badge and
|
||||
"Today", drawn with the --ui-bg-elevated background
|
||||
And the list has 6 rows, the first being "lea.martin@renew.example" with "—", "admin", a
|
||||
Pending badge and "Today", drawn with the --ui-bg-elevated background
|
||||
And the other 5 rows are unchanged and follow it in their previous order
|
||||
And a toast appears: color success, icon i-lucide-circle-check, id "invite-sent-3", title
|
||||
"Invite sent", description "lea.martin@renew.example has 3 days to accept."
|
||||
@@ -200,7 +260,7 @@ Feature: Users screen and InviteUserModal
|
||||
Scenario: Invite sent into the Only-you state keeps a coherent list
|
||||
Given the Only-you state has loaded
|
||||
When an invite for "lea.martin@renew.example" is sent and resolves 201
|
||||
Then the list shows the new Pending row above the "marta" row
|
||||
Then the list shows the new Pending row above the "marta@renew.example" row
|
||||
And the Only-you hint is no longer shown
|
||||
|
||||
Scenario Outline: Invite: already a user (409) — the field error depends on the response text
|
||||
|
||||
Reference in new issue
Block a user