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
|
||||
|
||||
+47
-14
@@ -16,20 +16,27 @@ The list items are keyed by `kind` and `id`.
|
||||
## Automated (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
Mock `useApi`'s `request` for `GET /v1/user/` and `POST /v1/user/invite` per case; no real backend.
|
||||
Mount UsersPage inside DashboardShell with a resolved verified session (id 1, username "marta") and
|
||||
a test router at /users. The Default fixture is the Background's 5 items; the invite fixture is
|
||||
Mount UsersPage inside DashboardShell with a resolved verified session (id 1, username "marta",
|
||||
role "owner") and a test router at /users; cases 6 to 17 use that owner session. The Default
|
||||
fixture is the Background's 5 items; the invite fixture is
|
||||
id 3, email "lea.martin@renew.example". No test freezes the system date. The specs only assert on
|
||||
the fixture's absolute dates; the sent row's "Today" label is not asserted anywhere (see the Visual
|
||||
part). Run each layout case at a desktop and a below-md width (mock `matchMedia`).
|
||||
|
||||
1. **Default** — assert breadcrumb "Users", title, subtitle, "Invite user", rows in fixture order,
|
||||
"Inès Duarte" with "ines" beside it, the "you" tag only on the "marta" row, badge words and
|
||||
the fixture's absolute dates.
|
||||
1. **Default** — assert breadcrumb "Users", title, subtitle, "Invite user", the table header
|
||||
labels (Email, Display name, Role, Invite status, Created), rows in fixture order, per row the
|
||||
email, the display name or "—", the RoleBadge word and class (owner filled, admin outlined),
|
||||
the badge word and the absolute date, "Paul Huber" on the invite row, the "you" tag only on
|
||||
"marta@renew.example", no username text in any row, and the below-md cards (display name line
|
||||
only when set).
|
||||
2. **Navigation** — assert the sidebar and the drawer list Home, Articles, Users, Integration,
|
||||
Settings in that order, and "Users" carries aria-current="page" on /users.
|
||||
3. **Loading** — unresolved `GET`; with fake timers assert nothing is drawn at 149 ms; at 150 ms
|
||||
assert 5 skeleton rows and `aria-busy="true"`; resolve and assert the Default rows.
|
||||
4. **Only you** — a single "marta" item; assert the row, the hint text and "Invite user".
|
||||
assert 5 skeleton rows and `aria-busy="true"`, and at desktop that each row has a RoleBadge
|
||||
and a status badge placeholder (below md the cards keep the two-bar shape); resolve and
|
||||
assert the Default rows.
|
||||
4. **Only you** — a single "marta" item (email, role "owner"); assert the row, the hint text and
|
||||
"Invite user".
|
||||
5. **Error, Examples-driven** — reject with `ApiError(500)`, then `NetworkError`; assert the alert
|
||||
copy, the "Try again" action, no close button and no rows; click "Try again"; assert `request`
|
||||
is called again, then resolve the Default fixture and assert its rows.
|
||||
@@ -45,8 +52,8 @@ part). Run each layout case at a desktop and a below-md width (mock `matchMedia`
|
||||
was not called a second time, the list has 6 rows with the new Pending row first and the
|
||||
others unchanged, the first row has the `--ui-bg-elevated` background class, and the toast
|
||||
was raised with id "invite-sent-3", color success, icon "i-lucide-circle-check", title
|
||||
"Invite sent" and description "lea.martin@renew.example has 3 days to accept." Assert nothing
|
||||
about the "Today" label.
|
||||
"Invite sent" and description "lea.martin@renew.example has 3 days to accept." Assert the new
|
||||
first row shows "—" and "admin", and nothing about the "Today" label.
|
||||
10. **Replacement of an expired invite** — invite "p.huber@gmail.com", resolve 201; assert exactly
|
||||
one row for that address, first and Pending.
|
||||
11. **Invite sent into Only you** — assert the new row above "marta" and the hint gone.
|
||||
@@ -65,6 +72,14 @@ part). Run each layout case at a desktop and a below-md width (mock `matchMedia`
|
||||
the same body; resolve 201 and assert the closed modal, the row and the toast.
|
||||
17. **Cancel discards** — type, click "Cancel"; assert immediate close, no `request`, an empty
|
||||
field on reopening, and focus back on the opening button.
|
||||
18. **Display names in the Users list** — the Default fixture with user 1 named "Marta Alves"
|
||||
(session `/v1/auth/me` too); assert the "marta@renew.example" row shows "Marta Alves", "owner"
|
||||
and Accepted, the "ines.duarte@renew.example" row "Inès Duarte" and "admin", the
|
||||
"jonas@renew.example" row "—" and "admin", and the sidebar chip "Marta Alves".
|
||||
19. **Non-owners, Examples-driven** — session role "admin", at desktop and below md; assert no
|
||||
"Invite user" button (header or pinned), the list still renders, no reserved space for the
|
||||
pinned button, and the hint absent for a list of only "marta" and "jonas". Also a session with
|
||||
no role (no button) and an owner control (button present in both layouts).
|
||||
|
||||
## Visual (task visual:check -- users, desktop and mobile, light and dark)
|
||||
|
||||
@@ -73,11 +88,24 @@ Screens Back-office.dc.html`, exact values in `docs/design/source/AppFrame.dc.ht
|
||||
state's mock to `web/visual/states.ts` with the board's own sample data. The gate is hard: every
|
||||
capture of every non-skipped state must be within 10 %, for both viewports and both themes.
|
||||
|
||||
- [ ] Default: the 5 board rows (`sam.oliveira@renew.example`, `p.huber@gmail.com`, `ines` with
|
||||
"Inès Duarte", `jonas`, `marta` with "you"), dates 27 Sep 2026, 3 Aug 2026, 18 Jun 2026,
|
||||
2 Apr 2026, 12 Mar 2026; session user `marta`.
|
||||
- [ ] Loading: 5 skeleton rows; the setup must give skeletons time to show (150 ms) before the capture.
|
||||
- [ ] Only you: the single `marta` row plus the hint.
|
||||
- [ ] Fixtures: `usersFixture` in `web/visual/states.ts` follows the board data: invite 2
|
||||
`sam.oliveira@renew.example` pending, no name; invite 1 `p.huber@gmail.com` with
|
||||
`display_name: 'Paul Huber'` expired; user 3 `ines.duarte@renew.example` "Inès Duarte" admin;
|
||||
user 2 `jonas@renew.example` admin, empty name; user 1 `marta@renew.example` owner, empty
|
||||
name. `verifiedSession` carries `role: 'owner'`. `profileSession` and `usersWithDisplayNames`
|
||||
are updated so "Display names in the Users list" shows "Marta Alves", owner.
|
||||
- [ ] Default: the 5 board rows by email with display names, roles and status badges, dates
|
||||
27 Sep 2026, 3 Aug 2026, 18 Jun 2026, 2 Apr 2026, 12 Mar 2026; session user `marta`. The
|
||||
board also draws Cancel/Renew buttons in the sixth column, which this slice does not
|
||||
render; if the gate fails on them, stop and ask the user (no exception without approval).
|
||||
- [ ] Loading: 5 skeleton rows, with role and status placeholders on desktop and the board's
|
||||
two-bar cards on mobile; the setup must give skeletons time to show (150 ms) before the
|
||||
capture.
|
||||
- [ ] Only you: the single `marta@renew.example` row (owner) plus the hint.
|
||||
- [ ] Profile "Display names in the Users list" (`task visual:check -- profile "Display names in
|
||||
the Users list"`): Marta Alves / Inès Duarte / dash.
|
||||
- [ ] Not drawn, no visual state: non-owner rendering (no "Invite user", no hint). Covered by
|
||||
Vitest only (case 19); do not add it to `web/visual/states.ts`.
|
||||
- [ ] Error: the alert and "Try again", no table or cards; the header button is still drawn.
|
||||
- [ ] Invite: default: modal over the Default list, empty focused Email; the mobile pinned button hidden.
|
||||
- [ ] Invite: validation: value `sam.oliveira@renew`, error copy.
|
||||
@@ -91,6 +119,9 @@ capture of every non-skipped state must be within 10 %, for both viewports and b
|
||||
- [ ] Layout, taken from the board frame (`AppFrame.dc.html`), not `components.md`: desktop header
|
||||
row is title block left and the 32 px "Invite user" button right, aligned to the bottom of
|
||||
the row; on mobile the button is 16 px from left, right and bottom, 48 px high, 18 px icon.
|
||||
- [ ] Layout, from `AppFrame.dc.html`: 36 px table header, 48 px rows, sixth 150 px track empty;
|
||||
RoleBadge 20 px high, 7 px side padding, IBM Plex Mono 11 px medium; cards show the email
|
||||
line, then the display name, then the badges line.
|
||||
- [ ] Not covered by screenshots: keyboard and focus order (Tab reaches "Invite user", the modal
|
||||
focus starts on Email, then Cancel, then "Send invite", focus is trapped, Esc closes it in
|
||||
every dismissible state and is blocked while Sending, focus returns to the opening button);
|
||||
@@ -106,6 +137,8 @@ capture of every non-skipped state must be within 10 %, for both viewports and b
|
||||
- **Real invitation email** — needs real or sandboxed SMTP (`SMTP_HOST` and the related env
|
||||
vars): the mail arrives and its link `{PUBLIC_URL}/invite/accept?c=...` carries the code. The
|
||||
link will not resolve in the SPA, because the accept screen is out of scope.
|
||||
- **Admin session** — sign in once as an admin against `task dev` and confirm there is no
|
||||
"Invite user" button and no "Only you" hint.
|
||||
- **Toast placement and stacking on a live app** — bottom right on desktop and above the pinned
|
||||
button on mobile.
|
||||
- **"Today" and the elevated row on a live app** — confirm the new row and the "Today" label
|
||||
|
||||
Reference in new issue
Block a user