Files
summercms/docs/backend/admin-spa.md
Jakub Zych a65c670574 feat(12.1-02): read-only preview screen with a status hint and record actions
- config_form.yaml preview block (optional headerPartial), reported in the form schema as preview
- fields with context: preview show only on the preview screen and are never written
- form messages preview and edit; recordActions without a preview block stops boot
- SPA route {id}/preview, PreviewView and PreviewField, record actions in the footer
- mapWinterUrl maps preview/:id; the update form returns to the preview
- summer-callout partial style classes for status hints
- README, docs, OpenAPI document, TS types and the embedded build updated
2026-10-05 00:10:48 +02:00

47 lines
4.9 KiB
Markdown

---
title: Admin SPA
description: How boardwalk serves the embedded Vue admin SPA under backend.uri, how the SPA talks to the admin API, and how its TypeScript types come from OpenAPI.
section: backend
order: 80
---
# Admin SPA
WinterCMS renders its backend on the server with layouts, partials and the AJAX framework. SummerCMS replaces that with one Vue 3 single-page app, built once and embedded in the binary by [boardwalk](../../modules/boardwalk/README.md). Plugins do not ship admin pages: they ship YAML and, when they need them, partials and scripts, and the SPA renders every controller from the schemas the admin API serves.
## Serving
cabana mounts the SPA under the admin prefix, `backend.uri` (`/backend` by default; one or more lowercase path segments). `boardwalk.Handler` serves the build:
- Any path under the prefix that is not a file and not under `api/` returns `index.html`, so the SPA's own routes work on reload.
- Paths under `api/` that no API route matches return the admin API's JSON `not_found` error, never the SPA.
- Hashed files under `assets/` are cached for a long time; `index.html` is never cached.
- Every response carries a restrictive Content-Security-Policy, frame denial, `nosniff`, a same-origin referrer policy and `noindex, nofollow`.
The build is path-agnostic: `index.html` holds a placeholder (`boardwalk.BaseToken`) that `boardwalk.RewriteIndex` replaces with the prefix once, when the handler is built. A build without the placeholder fails the start-up.
A plugin route under the admin prefix also fails the start-up: the SPA and the admin API own that whole path.
## How the SPA talks to the server
The SPA signs in through the admin API and keeps the token in the HttpOnly cookie described on [Users and permissions](users-and-permissions.md). For each screen it loads the controller's localized schema (`schema/list`, `schema/form`), then the records, and renders the fields and columns the schema names. Strings come from `GET <prefix>/api/v1/lang`, the `backend::lang` bundle in the request locale, with CLDR plural forms.
Each record form makes a session key when it opens: 32 random bytes, base64url encoded. The form sends it in the `X-Session-Key` header with every file upload, file list and file removal, and with the final create or update save. Uploads and removals are deferred: the server holds them against the key and the admin, and the save that carries the same key commits them in its transaction. Until then the form counts as unsaved, so leaving it asks first, and a new record's files go to record id `0`. The form will not save while an upload is still in flight. Uploads use `XMLHttpRequest` for progress events and carry the same `X-Requested-With` header and cookie as every other call, plus an `X-Upload-Id` so a retry returns the already stored file. Files of a protected relation are fetched through the admin API with the key and shown from object URLs. The key travels only in headers, never in a URL. See [File uploads](forms.md#file-uploads) for the `fileupload` field.
A list whose schema carries declared bulk actions shows a "Bulk actions" menu after the selection count. The menu lists only the actions the server offered to this administrator, and its button stays disabled until a row is selected. Choosing an action always asks for confirmation; the dialog stays open while the request runs, and the list reloads afterwards. See [Bulk actions](admin-controllers.md#bulk-actions).
A list row that carries a state (deleted, negative or disabled) shows a text badge for each state after its first cell, together with a text style; the row background is never changed. See [Row state](lists-and-filters.md#row-state).
A form whose schema carries `preview` has a read-only record screen at `<controller>/<id>/preview`, next to the list (`<controller>`), the create form (`<controller>/create`) and the update form (`<controller>/<id>`). It renders the fields whose context allows `preview` as text, the status hint partial above them and, in its footer, the record actions the record response offers before the one edit button. Opening that route for a form without a preview goes to the update form. See [Preview screen](forms.md#preview-screen).
## Types from OpenAPI
The admin API is described by swag annotations in cabana. `scripts/check-admin-openapi.sh` generates the OpenAPI document (`admin/openapi/admin.json`) from them and the SPA's TypeScript types (`admin/src/api/schema.d.ts`) from the document, so the SPA's API client is checked against the server's shapes at compile time. `--check` fails when either committed file is out of date:
```sh
scripts/check-admin-openapi.sh --check
```
## Building the SPA
The SPA's source is the `admin/` Vite project. `npm --prefix admin run build` type-checks it and writes the build to `modules/boardwalk/dist`, which the next `go build` embeds. An application that only uses the framework never builds the SPA: the build is committed with the framework.