feat(SPA): schema list and the sidebar Content group
This commit is contained in:
1 parent
73a75d5cb9
commit
3c67cd2314
34 files changed
+2181
-18
No files matched your search
@@ -0,0 +1,193 @@
|
||||
Feature: Schema list and content navigation in the back-office
|
||||
As an owner or admin
|
||||
I want to see the custom and built-in content types on a Schema screen
|
||||
And as any verified user I want a Content menu with one link per custom type
|
||||
So that I can reach the content types I manage and write entries for
|
||||
|
||||
Background:
|
||||
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
|
||||
And GET /v1/content-type returns a JSON array of {id, key, name, icon, fields, created_at, updated_at}
|
||||
And GET /v1/builtin-type returns a JSON array of {key, name, fields}, for owners and admins only
|
||||
And API errors are plain text
|
||||
And the session store exposes the role of the signed-in user, and only owner and admin exist today
|
||||
And a session whose role is neither owner nor admin (absent or unknown) is a test double for the forbidden states
|
||||
And the Schema screen is the route /schema with meta auth "verified", title "Schema"
|
||||
And only owners and admins can use Schema; the SPA decides from the session role, the API still enforces it
|
||||
And the typed API client exposes listContentTypes() and listBuiltinTypes()
|
||||
And a content types store loads them once per session and exposes loading, error and items
|
||||
And this slice does not add the Vitest runner, it already exists (task test:web)
|
||||
|
||||
Scenario Outline: The Schema entry is shown to owners and admins only
|
||||
Given a verified user with role "<role>" is signed in
|
||||
When the back-office shell renders
|
||||
Then the sidebar <schema_entry> a "Schema" entry linking to /schema, with the "shapes" icon, placed after "Users"
|
||||
And no existing nav item changed
|
||||
|
||||
Examples:
|
||||
| role | schema_entry |
|
||||
| owner | shows |
|
||||
| admin | shows |
|
||||
| unknown | does not show |
|
||||
|
||||
Scenario: The Content group lists one link per custom type
|
||||
Given a verified user is signed in
|
||||
And GET /v1/content-type returns the types "events" (Events), "recipes" (Recipes) and "press-mentions" (Press mentions)
|
||||
When the back-office shell renders
|
||||
Then the sidebar shows a "Content" group under the main items
|
||||
And the group contains three links, in the API order, to /content/events, /content/recipes and /content/press-mentions
|
||||
And each row shows the type name
|
||||
And the row of the type matching the current route /content/:key is marked as the current page
|
||||
|
||||
Scenario Outline: Each type shows its icon, or the default icon
|
||||
Given a custom type "recipes" with icon <icon>
|
||||
When the sidebar renders
|
||||
Then the row for "recipes" shows <shown>
|
||||
|
||||
Examples:
|
||||
| icon | shown |
|
||||
| "chef-hat" | the icon i-lucide-chef-hat (in bundle) |
|
||||
| null | the default icon "files" |
|
||||
| "not-bundled" | the default icon "files" |
|
||||
|
||||
Scenario: A long type name is truncated in one line and keeps its full name available
|
||||
Given a custom type named "Frequently asked questions and answers"
|
||||
When the sidebar renders on desktop
|
||||
Then the label is cut with an ellipsis on one line and does not widen the sidebar
|
||||
And the full name is available in a tooltip and as the link's accessible name
|
||||
|
||||
Scenario: With more than 5 types the desktop group shows 5 and the selected one
|
||||
Given 14 custom types and the current route is /content/<selected>
|
||||
When the sidebar renders on desktop
|
||||
Then the group shows the first 5 types, plus the selected type if it is not among them
|
||||
And a "Show all 14" action follows
|
||||
When the user activates "Show all 14"
|
||||
Then all 14 types are listed
|
||||
|
||||
Scenario: With 5 types or fewer there is no "Show all"
|
||||
Given 5 custom types
|
||||
When the sidebar renders
|
||||
Then all 5 are listed and no "Show all" action appears
|
||||
|
||||
Scenario: On mobile the group is one collapsed row in the drawer
|
||||
Given 14 custom types and a 390 px wide viewport
|
||||
When the user opens the drawer
|
||||
Then the Content group shows one collapsed row "All content types" with the count 14
|
||||
When the user expands it
|
||||
Then the 14 types are listed with rows 44 px high
|
||||
|
||||
Scenario: No custom type hides the group
|
||||
Given GET /v1/content-type returns []
|
||||
When the shell renders
|
||||
Then there is no Content group and no empty placeholder in the sidebar
|
||||
|
||||
Scenario Outline: A failing content type request does not break the shell
|
||||
Given GET /v1/content-type <failure>
|
||||
When the shell renders
|
||||
Then the Content group is not shown
|
||||
And the rest of the sidebar and the page render normally
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| answers 500 |
|
||||
| fails (network) |
|
||||
|
||||
Scenario: The Content group is loaded once for the whole session
|
||||
Given the user navigates between several back-office screens
|
||||
Then GET /v1/content-type is called once and the list is shared by the sidebar and the Schema list
|
||||
|
||||
Scenario: Populated list
|
||||
Given an owner or admin is signed in
|
||||
And GET /v1/content-type returns three types
|
||||
And GET /v1/builtin-type returns articles, authors, sites, media and users
|
||||
When they open /schema
|
||||
Then the page shows the title "Schema" and the text "Content types and the fields they are made of."
|
||||
And a "New content type" action is enabled
|
||||
And a table has the columns "Name", "Key", "Fields", "Last updated"
|
||||
And each custom row shows name, key, the number of fields and the updated date as an absolute date, using the board's sample data
|
||||
And after the custom rows come the five built-in rows, each with a lock icon and a "Built-in" badge
|
||||
And a built-in row shows its field count from the length of fields and an em dash as Last updated
|
||||
And the text "Built-in types are managed by RenewCMS. You can read their fields, but not change them." is shown
|
||||
And the summary "3 content types · 5 built-in" is shown
|
||||
And below 640 px the table is a list of cards with the same content
|
||||
|
||||
Scenario: Loading
|
||||
Given GET /v1/content-type has not answered
|
||||
When an owner opens /schema
|
||||
Then no skeleton is shown during the first 150 ms
|
||||
And after 150 ms skeleton rows are shown
|
||||
And "New content type" is disabled
|
||||
And the 403 page and the empty state are not shown
|
||||
|
||||
Scenario: Empty, no custom type yet
|
||||
Given GET /v1/content-type returns []
|
||||
When an owner opens /schema
|
||||
Then the empty state "No content types yet" is shown with the text "Create a content type to manage your own kind of content, like events or recipes. The built-in types stay as they are."
|
||||
And a "New content type" action is shown and enabled
|
||||
And the five built-in types are still listed below
|
||||
|
||||
Scenario Outline: Error
|
||||
Given GET /v1/content-type <failure>
|
||||
When an owner opens /schema
|
||||
Then an alert shows "Couldn't load content types"
|
||||
And it shows "GET /v1/content-type failed. Check your connection and try again."
|
||||
And a "Try again" action is shown
|
||||
And no table is shown
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| answers 500 |
|
||||
| fails (network) |
|
||||
|
||||
Scenario: Retry recovers
|
||||
Given the list is in the Error state
|
||||
When the user activates "Try again"
|
||||
Then the skeleton rows show after 150 ms and GET /v1/content-type is called again
|
||||
And when it answers 200 the Populated state replaces the alert
|
||||
|
||||
Scenario: Forbidden for a user who is not owner or admin
|
||||
Given a verified user whose role is neither owner nor admin is signed in
|
||||
When they open /schema directly
|
||||
Then the URL stays /schema and nothing redirects
|
||||
And the page shows "You don't have access to Schema"
|
||||
And it shows "Only owners and admins can change content types. You can still add and edit entries from the Content menu."
|
||||
And a "Go to Home" action navigates to /home
|
||||
And GET /v1/builtin-type is never called
|
||||
|
||||
Scenario: The API answers 403 for the list
|
||||
Given the session says owner or admin but GET /v1/builtin-type answers 403
|
||||
When they open /schema
|
||||
Then the same "You don't have access to Schema" page is shown
|
||||
|
||||
Scenario: Built-in types fail to load while custom types load
|
||||
Given GET /v1/content-type answers 200 and GET /v1/builtin-type answers 500
|
||||
When an owner opens /schema
|
||||
Then the Error state is shown
|
||||
|
||||
Scenario: An unverified or signed-out user keeps the existing redirects
|
||||
Given the user is not verified or not signed in
|
||||
When they open /schema
|
||||
Then they are redirected to /register/pending or /login as for any other "verified" route
|
||||
|
||||
Scenario: New content type
|
||||
Given the list is Populated or Empty
|
||||
When an owner activates "New content type"
|
||||
Then the router navigates to /schema/new (the builder is a later slice)
|
||||
|
||||
Scenario: Rows are not links in this slice
|
||||
Given the list is Populated
|
||||
Then custom and built-in rows do not navigate (the editor and the read-only view are later slices)
|
||||
|
||||
Scenario Outline: The typed client maps responses
|
||||
When the client calls <call>
|
||||
And the API answers <answer>
|
||||
Then it <result>
|
||||
|
||||
Examples:
|
||||
| call | answer | result |
|
||||
| listContentTypes | 200 with icon null | returns the types, icon typed string or null |
|
||||
| listContentTypes | 200 [] | returns an empty array |
|
||||
| listContentTypes | 500 plain text | rejects with an error carrying status 500 |
|
||||
| listContentTypes | network failure | rejects with a network error |
|
||||
| listBuiltinTypes | 200 | returns {key, name, fields} items |
|
||||
| listBuiltinTypes | 403 plain text | rejects with an error carrying status 403 |
|
||||
| listBuiltinTypes | 500 plain text | rejects with an error carrying status 500 |
|
||||
@@ -0,0 +1,48 @@
|
||||
# QA: Schema list and content navigation (SPA slice 1)
|
||||
|
||||
Runner: Vitest + Vue Test Utils (already installed, `task test:web`). No Go test: there is no backend change.
|
||||
Mock `/v1` through the existing api composable pattern used by the other view specs. Mount the shell (DashboardShell) with a test router and a resolved session whose role varies. Only owner and admin exist, so the forbidden cases use a session with an unknown role.
|
||||
|
||||
## Automated
|
||||
|
||||
Fixtures: three custom types (events, recipes, press-mentions) with the board's sample data for names, field counts and dates, at least one with icon "chef-hat" and one with icon null; five built-in types (articles, authors, sites, media, users) with fields arrays.
|
||||
|
||||
Schema list, one test per design state (screens/schema.md section 1):
|
||||
1. Populated: both calls 200, role owner and role admin. Assert the title, subtitle, columns, custom rows before built-in rows, lock + "Built-in" badge on 5 rows, the built-in note, the summary "3 content types · 5 built-in", enabled "New content type". Assert the card layout below 640 px.
|
||||
2. Loading: unresolved promise with fake timers. No skeleton at 149 ms, skeleton at 150 ms, "New content type" disabled. A fast response (under 150 ms) never shows skeletons.
|
||||
3. Empty: content-type returns []. Assert the empty state copy, action, and the 5 built-in rows still listed.
|
||||
4. Error: 500 and network failure. Assert the alert copy, the "Try again" action, no table. Then retry returns 200 and the Populated state replaces the alert (the second call is made).
|
||||
5. Forbidden: unknown role. Assert the 403 page copy, "Go to Home" navigates to /home, URL stays /schema, `GET /v1/builtin-type` never called. Add a case where builtin-type answers 403 for an owner.
|
||||
6. builtin-type 500 with content-type 200: Error state.
|
||||
7. "New content type" pushes /schema/new.
|
||||
|
||||
Sidebar:
|
||||
8. Schema entry visibility for owner, admin and unknown role; position after Users.
|
||||
9. Content group: three links with the right hrefs, current row marked, hidden on empty, hidden on 500 and on network failure, no second request across route changes.
|
||||
10. Icons: "chef-hat" in the bundle renders it, null and an unbundled name render the default "files" icon.
|
||||
11. Truncation: label has the truncate classes, full name in tooltip and accessible name.
|
||||
12. More than 5 types: 14 types and a selected type beyond the fifth. Assert 5 + the selected row + "Show all 14"; after activating it all 14 show. With 5 types, no "Show all".
|
||||
13. Mobile (viewport helper in `components/__tests__/viewport.ts`): drawer shows one collapsed row "All content types" with count 14, expands to 14 rows.
|
||||
|
||||
Router and guard:
|
||||
14. `/schema` is registered with auth "verified" and title "Schema"; unverified and signed-out users get the existing redirects (extend routerGuard.spec).
|
||||
|
||||
Client and store:
|
||||
15. Typed client calls (the Examples table in the feature) incl. icon string|null typing, 403, 500, network failure.
|
||||
16. Store loads once, shares state between the sidebar and the Schema list, retry reloads.
|
||||
|
||||
## Visual (task visual:check -- schema)
|
||||
|
||||
Run `task visual:check -- schema`, every capture within 10 % at desktop and mobile, light and dark. States to set up in `web/visual/states.ts`, reusing the board's sample data (names, keys, field counts, dates, counts "3 content types · 5 built-in" taken from SchemaFrame.dc.html):
|
||||
- 1 Populated, 2 Loading, 3 Empty, 4 Error, 5 Forbidden (403).
|
||||
- State 6 (built-in read-only view) is out of scope: report it as not set up in this slice, and flag it to the user if the check demands a setup for every state.
|
||||
- None of these is clock-sensitive; if the Last updated cell turns out to be relative on the board, mark it `skip: 'clock-sensitive'`.
|
||||
- Mock every `/v1` call the screen makes, including the sidebar's GET /v1/content-type.
|
||||
- The sidebar group and drawer frames belong to the later `content` group.
|
||||
|
||||
## Manual
|
||||
|
||||
- Keyboard: tab order through sidebar, "Show all", table and "New content type"; visible focus rings.
|
||||
- Tooltip on a truncated name on hover and on focus.
|
||||
- Hover and motion of the drawer expand/collapse row.
|
||||
- With a real backend: log in as owner and admin and confirm the sidebar and /schema; create a type with icon "chef-hat" via the API and see it appear.
|
||||
Reference in new issue
Block a user