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

@@ -5,6 +5,7 @@ import { api } from '../api/client'
import type { AdminRecord, ListMeta, ListSchema } from '../api/types'
import { controllerIdFromParams } from '../app/controllerRoutes'
import { t } from '../app/i18n'
import { mapWinterUrl } from '../app/winterUrl'
import DataTable from '../components/list/DataTable.vue'
import { activeEntry } from '../state/useNavigation'
@@ -41,6 +42,16 @@ const range = computed(() => {
return t('backend::lang.list.pagination_range', { from, to, total: m.total })
})
/** Record route of a row when the list declares a recordUrl (D-10). */
function rowLink(row: AdminRecord): string | null {
const recordUrl = schema.value?.recordUrl
const id = row.id
if (!recordUrl || (typeof id !== 'number' && typeof id !== 'string')) {
return null
}
return mapWinterUrl(recordUrl, controllerIdFromParams(path.value), id)
}
let generation = 0
async function load(): Promise<void> {
@@ -80,6 +91,7 @@ watch(path, load, { immediate: true, deep: true })
:rows="rows"
:loading="loading"
:empty-text="schema?.noRecordsMessage"
:row-link="rowLink"
/>
<footer class="border-t border-border px-5 py-3.5 text-[13px] text-muted">
{{ loading ? t('backend::lang.list.loading') : range }}