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:
52
admin/src/state/useToasts.ts
Normal file
52
admin/src/state/useToasts.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Global toast queue (design: State Management). Toasts outlive route
|
||||
// changes because AppShell mounts the renderer, so "Zapisz i zamknij" can
|
||||
// show its toast on the list it returns to.
|
||||
import { readonly, ref } from 'vue'
|
||||
|
||||
export type ToastTone = 'success' | 'danger'
|
||||
|
||||
export interface ToastItem {
|
||||
id: number
|
||||
text: string
|
||||
tone: ToastTone
|
||||
}
|
||||
|
||||
/** Auto-dismiss delay in milliseconds (design: about 4 s). */
|
||||
export const TOAST_TIMEOUT = 4000
|
||||
|
||||
const items = ref<ToastItem[]>([])
|
||||
const timers = new Map<number, ReturnType<typeof setTimeout>>()
|
||||
let nextId = 1
|
||||
|
||||
export const toasts = readonly(items)
|
||||
|
||||
export function showToast(text: string, tone: ToastTone = 'success'): number {
|
||||
const id = nextId++
|
||||
items.value = [...items.value, { id, text, tone }]
|
||||
timers.set(
|
||||
id,
|
||||
setTimeout(() => dismissToast(id), TOAST_TIMEOUT),
|
||||
)
|
||||
return id
|
||||
}
|
||||
|
||||
export function dismissToast(id: number): void {
|
||||
const timer = timers.get(id)
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer)
|
||||
timers.delete(id)
|
||||
}
|
||||
items.value = items.value.filter((item) => item.id !== id)
|
||||
}
|
||||
|
||||
export function clearToasts(): void {
|
||||
for (const timer of timers.values()) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timers.clear()
|
||||
items.value = []
|
||||
}
|
||||
|
||||
export function useToasts() {
|
||||
return { toasts, showToast, dismissToast, clearToasts }
|
||||
}
|
||||
Reference in New Issue
Block a user