168 lines
16 KiB
Markdown
168 lines
16 KiB
Markdown
# State matrix and error copy
|
|
|
|
Every state is drawn at desktop and mobile on the screen boards, with final copy. Each screen's file in `screens/` has the full copy per state.
|
|
|
|
| Screen | Route | State | Trigger | Spec |
|
|
|---|---|---|---|---|
|
|
| App boot | `any verified route` | Checking session | First navigation, while GET /v1/auth/me is pending | [boot](screens/boot.md) |
|
|
| App boot | `any verified route` | Verified | 200 with is_verified: the route renders | [boot](screens/boot.md) |
|
|
| App boot | `any verified route` | Unverified | 200 with is_verified false: goes to /register/pending | [boot](screens/boot.md) |
|
|
| App boot | `any verified route` | No session | 401: goes to /login?redirect= | [boot](screens/boot.md) |
|
|
| Log in | `/login` | Default | Registration closed | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Fresh install | registration.open is true | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Validation errors | Client rules fail on submit | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Submitting | Request in flight | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Wrong credentials | 403 | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Too many attempts | 429 with Retry-After | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Session expired | ?reason=expired from a 401 | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Password changed | ?reason=password-changed | [log-in](screens/log-in.md) |
|
|
| Log in | `/login` | Server error | 500 or network | [log-in](screens/log-in.md) |
|
|
| Register | `/register` | Default | Open instance | [register](screens/register.md) |
|
|
| Register | `/register` | Validation errors | Client rules fail | [register](screens/register.md) |
|
|
| Register | `/register` | Submitting | In flight | [register](screens/register.md) |
|
|
| Register | `/register` | Registration closed | 403 | [register](screens/register.md) |
|
|
| Register | `/register` | Server error | 500 or network | [register](screens/register.md) |
|
|
| Pending | `/register/pending` | Default | After registering (email known) | [pending](screens/pending.md) |
|
|
| Pending | `/register/pending` | Email unknown | After logging in unverified | [pending](screens/pending.md) |
|
|
| Pending | `/register/pending` | Confirm cancel | Cancel registration clicked | [pending](screens/pending.md) |
|
|
| Pending | `/register/pending` | Cancelling | DELETE in flight | [pending](screens/pending.md) |
|
|
| Pending | `/register/pending` | Cancelled | 200: /register with toast | [pending](screens/pending.md) |
|
|
| Pending | `/register/pending` | Already verified | 403: /home with toast | [pending](screens/pending.md) |
|
|
| Verify | `/register/validate?c=` | Verifying | On load | [verify](screens/verify.md) |
|
|
| Verify | `/register/validate?c=` | Success | 200: /home with toast | [verify](screens/verify.md) |
|
|
| Verify | `/register/validate?c=` | Invalid or expired | 403 | [verify](screens/verify.md) |
|
|
| Verify | `/register/validate?c=` | No session | 401: inline log in, then retry | [verify](screens/verify.md) |
|
|
| Verify | `/register/validate?c=` | Missing code | No c parameter | [verify](screens/verify.md) |
|
|
| Forgot password | `/register/reset/request` | Default | Page load | [forgot-password](screens/forgot-password.md) |
|
|
| Forgot password | `/register/reset/request` | Validation | Invalid email | [forgot-password](screens/forgot-password.md) |
|
|
| Forgot password | `/register/reset/request` | Submitting | In flight | [forgot-password](screens/forgot-password.md) |
|
|
| Forgot password | `/register/reset/request` | Sent | Any 200 (neutral copy) | [forgot-password](screens/forgot-password.md) |
|
|
| Forgot password | `/register/reset/request` | Too many attempts | 429 | [forgot-password](screens/forgot-password.md) |
|
|
| Forgot password | `/register/reset/request` | Server error | 500 or network | [forgot-password](screens/forgot-password.md) |
|
|
| Reset password | `/register/reset/validate` | Default | c and email present | [reset-password](screens/reset-password.md) |
|
|
| Reset password | `/register/reset/validate` | Too short | Under 8 characters | [reset-password](screens/reset-password.md) |
|
|
| Reset password | `/register/reset/validate` | Mismatch | Confirm field differs | [reset-password](screens/reset-password.md) |
|
|
| Reset password | `/register/reset/validate` | Submitting | In flight | [reset-password](screens/reset-password.md) |
|
|
| Reset password | `/register/reset/validate` | Success | 200: /login?reason=password-changed | [reset-password](screens/reset-password.md) |
|
|
| Reset password | `/register/reset/validate` | Invalid or expired | 403 | [reset-password](screens/reset-password.md) |
|
|
| Reset password | `/register/reset/validate` | Missing parameters | No c or no email | [reset-password](screens/reset-password.md) |
|
|
| Home | `/home` | Loading | List in flight (after 150 ms) | [home](screens/home.md) |
|
|
| Home | `/home` | Default | List loaded | [home](screens/home.md) |
|
|
| Home | `/home` | Empty | Zero articles | [home](screens/home.md) |
|
|
| Home | `/home` | Error | 500 or network | [home](screens/home.md) |
|
|
| Articles | `/article` | Loading | List in flight | [articles](screens/articles.md) |
|
|
| Articles | `/article` | Default | Loaded | [articles](screens/articles.md) |
|
|
| Articles | `/article` | Empty | Zero articles | [articles](screens/articles.md) |
|
|
| Articles | `/article` | No results | Search matches nothing | [articles](screens/articles.md) |
|
|
| Articles | `/article` | Empty tab | No drafts, or nothing online | [articles](screens/articles.md) |
|
|
| Articles | `/article` | Row busy | Publish or unpublish in flight | [articles](screens/articles.md) |
|
|
| Articles | `/article` | Action failed | 500 or network: toast with Retry | [articles](screens/articles.md) |
|
|
| Articles | `/article` | Row vanished | 404 on any row action | [articles](screens/articles.md) |
|
|
| Articles | `/article` | Delete confirm, in progress, failed, done | ConfirmDialog, then DELETE: 204 or 500 | [articles](screens/articles.md) |
|
|
| New article | `modal · /article/new` | Default | Opened | [new-article](screens/new-article.md) |
|
|
| New article | `modal · /article/new` | Validation | Title under 3 or body empty | [new-article](screens/new-article.md) |
|
|
| New article | `modal · /article/new` | Submitting | POST in flight | [new-article](screens/new-article.md) |
|
|
| New article | `modal · /article/new` | Title rejected | Server 400 | [new-article](screens/new-article.md) |
|
|
| New article | `modal · /article/new` | Error | 500 or network | [new-article](screens/new-article.md) |
|
|
| New article | `modal · /article/new` | Created | 201: editor, with a toast | [new-article](screens/new-article.md) |
|
|
| Editor | `/article/:id/edit` | Loading | GET in flight | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Not found | 404 | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Saved | Clean | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Unsaved | Any edit | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Saving | PATCH in flight | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Save failed | 500 or network | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Near the limit | At least 9 000 HTML characters | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Too long | Over 10 000: Save and Publish disabled | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Preview | Switch on | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Choose, crop and resize, uploading, rejected, inserted | ImageUploader | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Publishing, unpublishing | PATCH publish or unpublish | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Leave guard | Navigating away while dirty | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Offline | navigator.onLine is false | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Deleted | 204: /article with toast | [editor](screens/editor.md) |
|
|
| Editor | `/article/:id/edit` | Session expired | 401: draft kept in sessionStorage, then /login | [editor](screens/editor.md) |
|
|
| Integration | `/integration` | Default | Page load | [integration](screens/integration.md) |
|
|
| Integration | `/integration` | Copied | Copy clicked (2 s) | [integration](screens/integration.md) |
|
|
| Users | `/users` | Default | List loaded, newest first | [users](screens/users.md) |
|
|
| Users | `/users` | Loading | First load or retry | [users](screens/users.md) |
|
|
| Users | `/users` | Only you | Nobody invited yet | [users](screens/users.md) |
|
|
| Users | `/users` | Error | 500 or network | [users](screens/users.md) |
|
|
| Users | `/users` | Invite: default, validation, sending | Invite modal opened, invalid email, POST in flight | [users](screens/users.md) |
|
|
| Users | `/users` | Invite: already a user | 409 | [users](screens/users.md) |
|
|
| Users | `/users` | Invite: failed | 500, network or mail failure | [users](screens/users.md) |
|
|
| Users | `/users` | Invite sent | 201: dialog closes, Pending row on top, toast | [users](screens/users.md) |
|
|
| Users | `/users` | Cancel invite: confirm, sending, failed | "Cancel invite" on a Pending row; DELETE in flight; 500 or network | [users](screens/users.md) |
|
|
| Users | `/users` | Cancel invite: done | 204: row removed, toast | [users](screens/users.md) |
|
|
| Users | `/users` | Renew invite: sending | "Renew invite" on an Expired row; POST in flight | [users](screens/users.md) |
|
|
| Users | `/users` | Renew invite: done | 200: row Pending on top, toast | [users](screens/users.md) |
|
|
| Users | `/users` | Renew invite: failed | 500 or network: toast, row stays Expired | [users](screens/users.md) |
|
|
| Settings | `/settings` | Default | Opened from the sidebar | [settings](screens/settings.md) |
|
|
| Profile | `/profile` | Account menu | Account chip clicked | [profile](screens/profile.md) |
|
|
| Profile | `/profile` | Loading | Opened by URL before /auth/me returns | [profile](screens/profile.md) |
|
|
| Profile | `/profile` | Default, no display name | First visit | [profile](screens/profile.md) |
|
|
| Profile | `/profile` | Editing, too long | Typing; over 60 characters | [profile](screens/profile.md) |
|
|
| Profile | `/profile` | Saving, saved, save failed | PATCH in flight, 200, 500 or network | [profile](screens/profile.md) |
|
|
| Profile | `/profile` | Reset: sending, sent, 429, failed | POST /v1/auth/password-reset | [profile](screens/profile.md) |
|
|
| Profile | `/profile` | Display names in the Users list | Accounts with a display name | [profile](screens/profile.md) |
|
|
| Accept invite | `/invite/accept?c=` | Checking the link | On load: GET invite | [accept-invite](screens/accept-invite.md) |
|
|
| Accept invite | `/invite/accept?c=` | Default, validation errors, filled, submitting | Valid, unused code; email read-only | [accept-invite](screens/accept-invite.md) |
|
|
| Accept invite | `/invite/accept?c=` | Username taken | 409 | [accept-invite](screens/accept-invite.md) |
|
|
| Accept invite | `/invite/accept?c=` | Invalid or used, expired | 404 or no c parameter, 410 | [accept-invite](screens/accept-invite.md) |
|
|
| Accept invite | `/invite/accept?c=` | Server error | 500 or network | [accept-invite](screens/accept-invite.md) |
|
|
| Accept invite | `/invite/accept?c=` | Success | 200: logged in, /home with toast | [accept-invite](screens/accept-invite.md) |
|
|
|
|
## HTTP status to copy
|
|
|
|
Response bodies are plain text and are never shown. `useApi` throws an `ApiError` with the status and a context key, and each screen maps them to this copy.
|
|
|
|
| Status | Where | Copy | Surface |
|
|
|---|---|---|---|
|
|
| 400 | Any form | The field error from the client rules. A server 400 that got through: "Something in the form isn't valid. Check the highlighted fields." | Field + alert |
|
|
| 401 | Any back-office call | Goes to /login?redirect=<path>&reason=expired. "Your session expired. Log in again to pick up where you left off." | Redirect + alert |
|
|
| 401 | Verify | "Log in to finish verifying." Inline log in, then retry | Inline form |
|
|
| 403 | Log in | "That username and password don't match. Check both and try again." | Alert |
|
|
| 403 | Register | "This site already has an owner. Log in instead." | ResultPanel |
|
|
| 403 | Any back-office call | Unverified: goes to /register/pending | Redirect |
|
|
| 403 | Verify | "This verification link doesn't work." Offers Cancel registration and start over | ResultPanel |
|
|
| 403 | Cancel registration | "Your account is already verified." Goes to /home | Toast |
|
|
| 403 | Reset confirm | "This reset link has expired." Offers Request a new link | ResultPanel |
|
|
| 409 | Invite | "This address already has an account." or, for a live invite, "There's already a pending invite for this address." | Field error |
|
|
| 404 | Accept invite | "This invite link doesn't work." Offers Go to log in | ResultPanel |
|
|
| 409 | Accept invite | "That username is taken. Try another." | Field |
|
|
| 410 | Accept invite | "This invite has expired. Ask the person who invited you to send a new one." | ResultPanel |
|
|
| 404 | Editor load | "This article doesn't exist." Offers All articles | ErrorView |
|
|
| 404 | Row or editor action | "That article no longer exists." The row is removed | Toast |
|
|
| 404 | Unknown route | "Page not found" | ErrorView |
|
|
| 429 | Profile, reset link | "Too many requests. You've asked for several links recently. Wait a few minutes, then try again." Button disabled | Note |
|
|
| 429 | Log in, reset request | "Too many attempts. For your security, wait a moment before trying again." With a countdown | Alert + disabled button |
|
|
| 500 | Save | "Save failed" + Retry. Changes are kept | SaveStatus |
|
|
| 500 | Publish / unpublish | "Couldn't publish" / "Couldn't unpublish" + Retry | Toast |
|
|
| 500 | Load users | "Couldn't load users. Check your connection and try again." + Try again | Alert |
|
|
| 500 | Cancel invite | "Couldn't cancel the invite. Try again in a moment." The dialog stays open | Notice in dialog |
|
|
| 500 | Renew invite | "Couldn't renew the invite. Try again in a moment." The row stays Expired | Toast |
|
|
| 500 | Invite | "Couldn't send the invite. Try again in a moment." The address stays | Alert in dialog |
|
|
| 500 | Save display name | "Couldn't save your display name. Your text is still here. Try again in a moment." | Alert in card |
|
|
| 500 | Send reset link (profile) | "Couldn't send the link. Check your connection and try again." | Note |
|
|
| 500 | Delete | "Couldn't delete the article, so it's still there. Try again in a moment." | Inline in dialog |
|
|
| 500 | Upload | "Upload failed. The server didn't accept the file. Your article hasn't changed." | ImageUploader |
|
|
| network | Anywhere | "Couldn't reach RenewCMS. Check your connection and try again." Offline: banner | Alert / toast / banner |
|
|
|
|
## API the design marked pending
|
|
|
|
Existing: `GET /v1/auth/me`, `GET /v1/auth/registration`, `429` with `Retry-After` on `/v1/auth/login` and `/v1/auth/password-reset`, and `/static/uploadedImages/*`.
|
|
|
|
Added by the Users and Profile screens, none of which exist yet:
|
|
|
|
- `GET /v1/user/`: the users and their invites, newest first.
|
|
- `POST /v1/user/invite`: sends an invitation email. 201, 409 for an existing account or a live pending invite.
|
|
- `DELETE /v1/user/invite/:id`: cancels a Pending invite. 204. Owner only.
|
|
- `POST /v1/user/invite/:id/renew`: replaces the code, resets the 3 days and created date, and resends the email. 200 with the invite. Owner only.
|
|
- `GET /v1/user/` and `GET /v1/auth/me` gain a `role` ("owner" or "admin"); the list gains `email` on accepted users.
|
|
- `POST /v1/user/invite` becomes owner only, and always creates an admin.
|
|
- `GET /v1/settings`: `{ "version": "..." }`.
|
|
- `PATCH /v1/user/me`: saves the display name. `GET /v1/auth/me` returns it.
|
|
- `GET /v1/auth/invite?c=CODE`: looks up the invite on load, returns `{ email }`, 404 or 410. `POST /v1/auth/invite/accept` takes `{ code, username, password }` and returns 200 with the session cookie, 409 for a taken username, 404 or 410.
|
|
|
|
Still not supported, so shown disabled or omitted:
|
|
|
|
Renaming a title (shown read-only, with an explanation in Details), resending the verification email (the pending page explains how to start over), changing the email or username, listing or deleting individual images, removing users, promoting or demoting anyone, and any role other than owner and admin. Changing the password while logged in works only through the emailed reset link on the Profile page.
|