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:
@@ -1,17 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import type { AdminRecord, ListColumn } from '../../api/types'
|
||||
import { t } from '../../app/i18n'
|
||||
|
||||
// Read-only, schema-driven table: columns come from the list schema and each
|
||||
// record is read through its column keys (D-16). Selection, sorting and
|
||||
// row links arrive with the list screens.
|
||||
defineProps<{
|
||||
// Schema-driven table: columns come from the list schema and each record is
|
||||
// read through its column keys (D-16). With rowLink a row opens its record
|
||||
// route; the first cell carries the link for keyboard users.
|
||||
const props = defineProps<{
|
||||
columns: ListColumn[]
|
||||
rows: AdminRecord[]
|
||||
loading?: boolean
|
||||
emptyText?: string
|
||||
rowLink?: (row: AdminRecord) => string | null
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function linkOf(row: AdminRecord): string | null {
|
||||
return props.rowLink ? props.rowLink(row) : null
|
||||
}
|
||||
|
||||
function openRow(event: MouseEvent, row: AdminRecord): void {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target?.closest('a, button, input, label')) {
|
||||
return
|
||||
}
|
||||
const to = linkOf(row)
|
||||
if (to) {
|
||||
void router.push(to)
|
||||
}
|
||||
}
|
||||
|
||||
function cellText(row: AdminRecord, column: ListColumn): string {
|
||||
return formatValue(row[column.key])
|
||||
}
|
||||
@@ -68,14 +87,23 @@ function rowKey(row: AdminRecord, index: number): string {
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr v-for="(row, index) in rows" :key="rowKey(row, index)" class="h-row border-b border-border">
|
||||
<tr
|
||||
v-for="(row, index) in rows"
|
||||
:key="rowKey(row, index)"
|
||||
:class="linkOf(row) ? 'cursor-pointer hover:bg-hover' : ''"
|
||||
class="h-row border-b border-border"
|
||||
@click="openRow($event, row)"
|
||||
>
|
||||
<td
|
||||
v-for="(column, columnIndex) in columns"
|
||||
:key="column.key"
|
||||
:class="columnIndex === 0 ? 'font-semibold' : 'text-muted'"
|
||||
class="px-3.5 first:pl-5 last:pr-5"
|
||||
>
|
||||
{{ cellText(row, column) }}
|
||||
<RouterLink v-if="columnIndex === 0 && linkOf(row)" :to="linkOf(row)!" class="text-text no-underline">
|
||||
{{ cellText(row, column) }}
|
||||
</RouterLink>
|
||||
<template v-else>{{ cellText(row, column) }}</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user