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:
48
admin/src/components/form/FormGrid.vue
Normal file
48
admin/src/components/form/FormGrid.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminRecord, ControllerParams, FormField as FormFieldSchema, RecordMeta } from '../../api/types'
|
||||
import FormField from './FormField.vue'
|
||||
|
||||
// Two-column form grid (design screen 4): span left -> column 1, right ->
|
||||
// column 2, full (Winter's default) -> the whole row, auto and row -> the next
|
||||
// free slot. Below 600px everything is one column. Gaps are 22px x 24px.
|
||||
defineProps<{
|
||||
fields: FormFieldSchema[]
|
||||
values: AdminRecord
|
||||
errors: Record<string, string[]>
|
||||
labels?: RecordMeta['labels']
|
||||
source?: ControllerParams | null
|
||||
idPrefix?: string
|
||||
}>()
|
||||
const emit = defineEmits<{ update: [name: string, value: unknown] }>()
|
||||
|
||||
function spanClass(span: string | undefined): string {
|
||||
switch (span) {
|
||||
case 'left':
|
||||
return 'min-[600px]:col-start-1'
|
||||
case 'right':
|
||||
return 'min-[600px]:col-start-2'
|
||||
case 'auto':
|
||||
case 'row':
|
||||
return ''
|
||||
default:
|
||||
return 'col-span-full'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-x-6 gap-y-[22px] min-[600px]:grid-cols-2">
|
||||
<FormField
|
||||
v-for="field in fields"
|
||||
:key="field.name"
|
||||
:class="spanClass(field.span)"
|
||||
:field="field"
|
||||
:model-value="values[field.name]"
|
||||
:errors="errors[field.name]"
|
||||
:labels="labels?.[field.name]"
|
||||
:source="source"
|
||||
:id-prefix="idPrefix"
|
||||
@update:model-value="(value: unknown) => emit('update', field.name, value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user