feat(17-02): tree reorder page and singleton form in the admin
- ReorderView: indented tree with drag handle, move up/down, outdent and indent (buttons and arrow keys on the handle), pointer drag before, after or inside a row and never into its own subtree; one POST with the whole node set 400 ms after the last move, flushed on leave; a failed save restores the saved tree, a 409 reloads; loading, empty, load-failure and 403 states; partition selector - ListView: outline Reorder button before New record, passing the filtered partition - SettingsIndexView opens a singleton controller's record through GET …/singleton; FormView in singleton mode has no Delete and returns to the settings index - reorder.* and messages.list.reorder phrase keys (en, pl), smoke spec, docs
This commit is contained in:
@@ -170,14 +170,31 @@ const title = computed(() => {
|
||||
return message(forms, undefined, { name: recordName.value })
|
||||
})
|
||||
|
||||
// A singleton controller (D-11, pact.AdminSingleton) edits its one record,
|
||||
// opened from the settings index: no Delete, and back, Cancel and Save and
|
||||
// close return to the settings index.
|
||||
const singleton = computed(() => mode === 'update' && !!schema.value?.singleton)
|
||||
const SETTINGS_PATH = '/settings'
|
||||
|
||||
const subtitle = computed(() =>
|
||||
mode === 'update' ? message(schema.value?.messages.update, undefined, { name: recordName.value }) : '',
|
||||
mode === 'update' && !singleton.value ? message(schema.value?.messages.update, undefined, { name: recordName.value }) : '',
|
||||
)
|
||||
|
||||
// An update form of a record that has a preview screen is entered from it,
|
||||
// so back and Cancel return there (UI-SPEC S3); a delete still goes to the list.
|
||||
const backToPreview = computed(() => mode === 'update' && !!schema.value?.preview)
|
||||
const backPath = computed(() => (backToPreview.value ? `${listPath}/${recordId}/preview` : listPath))
|
||||
const backToPreview = computed(() => mode === 'update' && !singleton.value && !!schema.value?.preview)
|
||||
const backPath = computed(() => {
|
||||
if (singleton.value) {
|
||||
return SETTINGS_PATH
|
||||
}
|
||||
return backToPreview.value ? `${listPath}/${recordId}/preview` : listPath
|
||||
})
|
||||
const backLabel = computed(() => {
|
||||
if (singleton.value) {
|
||||
return t('backend::lang.settings.back')
|
||||
}
|
||||
return t(backToPreview.value ? 'backend::lang.form.return_to_preview' : 'backend::lang.form.return_to_list')
|
||||
})
|
||||
|
||||
const dirty = computed(
|
||||
() =>
|
||||
@@ -437,7 +454,7 @@ async function onSave(): Promise<void> {
|
||||
async function onSaveAndClose(): Promise<void> {
|
||||
const record = await save()
|
||||
if (record) {
|
||||
await go(redirectTarget('redirectClose', record.data.id))
|
||||
await go(singleton.value ? SETTINGS_PATH : redirectTarget('redirectClose', record.data.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +551,7 @@ void load()
|
||||
<Button
|
||||
variant="outline"
|
||||
data-action="back"
|
||||
:aria-label="t(backToPreview ? 'backend::lang.form.return_to_preview' : 'backend::lang.form.return_to_list')"
|
||||
:aria-label="backLabel"
|
||||
class="size-10! px-0!"
|
||||
@click="onLeave"
|
||||
>
|
||||
@@ -579,7 +596,7 @@ void load()
|
||||
class="fixed inset-x-0 bottom-0 z-30 flex items-center gap-2.5 border-t border-border bg-surface px-8 py-3.5"
|
||||
>
|
||||
<Button
|
||||
v-if="mode === 'update'"
|
||||
v-if="mode === 'update' && !singleton"
|
||||
variant="danger"
|
||||
data-action="delete"
|
||||
:icon="Trash2"
|
||||
|
||||
Reference in New Issue
Block a user