feat(10-03): tabs, toggles, relation fields, form lifecycle and settings pages
- FormTabs groups fields by tab (untabbed fields in the default tab) as a
segmented tablist; a tab holding invalid fields after a 422 shows a
count badge and the form switches to the first invalid field
- switch, checkbox and relation join the renderer registry: toggle cards
(role=switch, 20px checkbox) keep a numeric value numeric; relation
fields are read-only labels from meta.labels, a searchable single
select over fields/{field}/options with emptyOption first, or ordered
removable chips with an appending search (300 ms debounce, 20 per
page, more on scroll or the more action)
- FormView gets the back button, record title and update subtitle, a
sticky footer (Usuń with deleteConfirm then DELETE, Anuluj, Zapisz i
zamknij to the mapped redirectClose, Zapisz), and asks before leaving a
dirty form on any route change plus a beforeunload guard
- Settings: the rail pins Ustawienia to the bottom when /settings is
non-empty; /settings lists pages by category; /settings/:code renders
the settings schema through FormGrid and the registry, PUTs the values
and maps a 422 like the record form
- New backend::lang keys (form.more_options, tab_default, discard,
settings.back); form and settings smoke tests; boardwalk/dist rebuilt
This commit is contained in:
47
admin/src/state/useSettings.ts
Normal file
47
admin/src/state/useSettings.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Settings pages the admin may manage (D-21), server-filtered like the
|
||||
// navigation. The rail shows "Ustawienia" only when this list is non-empty.
|
||||
import { computed, readonly, ref } from 'vue'
|
||||
import { api } from '../api/client'
|
||||
import type { SettingsEntry } from '../api/types'
|
||||
|
||||
const entries = ref<SettingsEntry[]>([])
|
||||
|
||||
export const settings = readonly(entries)
|
||||
|
||||
export const hasSettings = computed(() => entries.value.length > 0)
|
||||
|
||||
export async function loadSettings(): Promise<SettingsEntry[]> {
|
||||
const { data, response } = await api.GET('/settings')
|
||||
entries.value = response.ok && data ? data.data : []
|
||||
return entries.value
|
||||
}
|
||||
|
||||
export function setSettings(next: SettingsEntry[]): void {
|
||||
entries.value = next
|
||||
}
|
||||
|
||||
export function settingsEntry(code: string): SettingsEntry | null {
|
||||
return entries.value.find((entry) => entry.code === code) ?? null
|
||||
}
|
||||
|
||||
/** Entries grouped by category, categories in first-seen order, by order. */
|
||||
export const settingsGroups = computed(() => {
|
||||
const sorted = entries.value
|
||||
.map((entry, index) => ({ entry, index }))
|
||||
.sort((a, b) => a.entry.order - b.entry.order || a.index - b.index)
|
||||
.map(({ entry }) => entry)
|
||||
const groups: Array<{ category: string; entries: SettingsEntry[] }> = []
|
||||
for (const entry of sorted) {
|
||||
let group = groups.find((item) => item.category === entry.category)
|
||||
if (!group) {
|
||||
group = { category: entry.category, entries: [] }
|
||||
groups.push(group)
|
||||
}
|
||||
group.entries.push(entry)
|
||||
}
|
||||
return groups
|
||||
})
|
||||
|
||||
export function useSettings() {
|
||||
return { settings, hasSettings, settingsGroups, loadSettings, settingsEntry }
|
||||
}
|
||||
Reference in New Issue
Block a user