feat(10-03): open, edit and save a record with toast and 422 feedback

- The SPA loads the backend::lang bundle before /auth/me, sets the
  document language from meta.locale and renders plural messages with
  Intl.PluralRules; interpolate mirrors phrasebook for :name/:Name/:NAME
- Create and record routes; mapWinterUrl maps recordUrl and redirects
  onto the controller's list, create and record routes only
- List rows open their record; FormView loads the form schema and the
  record, shows context-allowed fields in the span grid, saves values
  keyed by field name and toasts the resolved saved message
- A 422 puts each message under its field (aria-invalid,
  aria-describedby), shows the plural banner, focuses the first invalid
  field in schema order and clears a field's error on change
- The D-05 registry maps text, textarea, number and dropdown; any other
  type renders the unsupported-field box with the type in DM Mono
- The admin OpenAPI document declares the write request bodies
  (AdminRecord, AdminIDsRequest) and the list filter query as a
  deepObject, so the typed client can send them
- New backend::lang form.load_failed key; boardwalk/dist rebuilt
This commit is contained in:
Jakub Zych
2026-09-27 16:43:47 +02:00
parent 453b8ae786
commit 126ca5b8ed
41 changed files with 1930 additions and 37 deletions

View File

@@ -474,7 +474,12 @@ export interface paths {
};
cookie?: never;
};
requestBody?: never;
/** @description Setting values keyed by field name */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminRecord"];
};
};
responses: {
/** @description OK */
200: {
@@ -617,6 +622,10 @@ export interface paths {
page?: number;
/** @description Records per page */
per_page?: number;
/** @description Filter values keyed by filter name, sent as filter[<name>]=<value> */
filter?: {
[key: string]: string;
};
};
header?: never;
path: {
@@ -697,7 +706,12 @@ export interface paths {
};
cookie?: never;
};
requestBody?: never;
/** @description Field values keyed by field name; relation fields carry ids */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminRecord"];
};
};
responses: {
/** @description Created */
201: {
@@ -776,7 +790,12 @@ export interface paths {
};
cookie?: never;
};
requestBody?: never;
/** @description Record ids */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminIDsRequest"];
};
};
responses: {
/** @description OK */
200: {
@@ -1299,7 +1318,12 @@ export interface paths {
};
cookie?: never;
};
requestBody?: never;
/** @description Field values keyed by field name; relation fields carry ids */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminRecord"];
};
};
responses: {
/** @description OK */
200: {
@@ -1614,7 +1638,12 @@ export interface paths {
};
cookie?: never;
};
requestBody?: never;
/** @description Related record ids */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminIDsRequest"];
};
};
responses: {
/** @description OK */
200: {
@@ -1697,7 +1726,12 @@ export interface paths {
};
cookie?: never;
};
requestBody?: never;
/** @description Related record ids */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminIDsRequest"];
};
};
responses: {
/** @description OK */
200: {
@@ -1756,6 +1790,9 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
"cabana.AdminIDsRequest": {
ids: number[];
};
"cabana.AdminLoginData": {
access_token?: string;
expires_in?: number;