feat(12.2-04): edit related records in modals and defer relation work on new records
- RelationManager renders create/link/delete/unlink in declared order with one primary, opens the child modal (update or view form) or the pivot modal on row click, and deletes selected children behind a busy confirm - RelationChildModal creates and edits children with its own session key (X-Child-Session-Key) so uploads and dates work inside it - RelationPivotModal edits link details; the picker links one record with its pivot values when the relation has a pivot form - deferrable managers render on the create screen with owner id 0, the form's X-Session-Key and the pending note, and mark the form dirty - the registry resolves RelationManager lazily (child forms close an import cycle); DataTable gains openable rows and a trailing cell - relation lang keys in en and pl; rebuilt boardwalk dist
This commit is contained in:
@@ -8,12 +8,14 @@ import {
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
} from 'reka-ui'
|
||||
import { LoaderCircle } from '@lucide/vue'
|
||||
import { t } from '../../app/i18n'
|
||||
|
||||
// Confirmation (Reka AlertDialog: role alertdialog, focus trapped, Esc
|
||||
// cancels, focus returns to the trigger). Text only: messages come from the
|
||||
// server and are never rendered as HTML.
|
||||
withDefaults(
|
||||
// server and are never rendered as HTML. While `busy` (an action started by
|
||||
// the confirm is running) both buttons are disabled and the dialog stays.
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
message: string
|
||||
@@ -21,13 +23,17 @@ withDefaults(
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
danger?: boolean
|
||||
busy?: boolean
|
||||
}>(),
|
||||
{ title: undefined, confirmLabel: undefined, cancelLabel: undefined, danger: false },
|
||||
{ title: undefined, confirmLabel: undefined, cancelLabel: undefined, danger: false, busy: false },
|
||||
)
|
||||
const emit = defineEmits<{ 'update:open': [open: boolean]; confirm: []; cancel: [] }>()
|
||||
|
||||
// Closing any other way than the confirm button (Esc, Anuluj) cancels.
|
||||
function onOpenChange(open: boolean): void {
|
||||
if (!open && props.busy) {
|
||||
return
|
||||
}
|
||||
emit('update:open', open)
|
||||
if (!open) {
|
||||
emit('cancel')
|
||||
@@ -35,6 +41,9 @@ function onOpenChange(open: boolean): void {
|
||||
}
|
||||
|
||||
function onConfirm(): void {
|
||||
if (props.busy) {
|
||||
return
|
||||
}
|
||||
emit('confirm')
|
||||
emit('update:open', false)
|
||||
}
|
||||
@@ -55,17 +64,21 @@ function onConfirm(): void {
|
||||
<div class="flex gap-2.5">
|
||||
<AlertDialogCancel
|
||||
data-action="cancel"
|
||||
class="inline-flex h-button flex-1 items-center justify-center rounded-control border border-border-strong bg-surface font-semibold hover:bg-hover"
|
||||
:disabled="busy"
|
||||
class="inline-flex h-button flex-1 items-center justify-center rounded-control border border-border-strong bg-surface font-semibold hover:bg-hover disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{{ cancelLabel ?? t('backend::lang.form.cancel') }}
|
||||
</AlertDialogCancel>
|
||||
<button
|
||||
type="button"
|
||||
data-action="confirm"
|
||||
:disabled="busy"
|
||||
:aria-busy="busy ? 'true' : undefined"
|
||||
:class="danger ? 'bg-danger text-white' : 'bg-primary text-on-primary'"
|
||||
class="inline-flex h-button flex-1 items-center justify-center rounded-control font-semibold hover:opacity-90"
|
||||
class="inline-flex h-button flex-1 items-center justify-center gap-2 rounded-control font-semibold hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
@click="onConfirm"
|
||||
>
|
||||
<LoaderCircle v-if="busy" :size="16" class="animate-spin" aria-hidden="true" />
|
||||
{{ confirmLabel ?? t('backend::lang.form.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Promise-based confirmation for ConfirmDialog: `await ask(text)` resolves
|
||||
// true on confirm and false on Anuluj, Esc or a second request.
|
||||
// true on confirm and false on Anuluj, Esc or a second request. With an
|
||||
// action, the dialog stays open and busy while the action runs after the
|
||||
// confirm, and closes when it settles (a toolbar delete shows its progress
|
||||
// on the confirm button).
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface ConfirmRequest {
|
||||
@@ -10,22 +13,49 @@ export interface ConfirmRequest {
|
||||
|
||||
export function useConfirm() {
|
||||
const request = ref<ConfirmRequest | null>(null)
|
||||
const busy = ref(false)
|
||||
let resolver: ((ok: boolean) => void) | null = null
|
||||
let action: (() => Promise<unknown>) | null = null
|
||||
|
||||
function settle(ok: boolean): void {
|
||||
const resolve = resolver
|
||||
resolver = null
|
||||
action = null
|
||||
request.value = null
|
||||
resolve?.(ok)
|
||||
}
|
||||
|
||||
function ask(next: ConfirmRequest): Promise<boolean> {
|
||||
function ask(next: ConfirmRequest, run?: () => Promise<unknown>): Promise<boolean> {
|
||||
settle(false)
|
||||
request.value = next
|
||||
action = run ?? null
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolver = resolve
|
||||
})
|
||||
}
|
||||
|
||||
return { request, ask, confirm: () => settle(true), cancel: () => settle(false) }
|
||||
async function confirm(): Promise<void> {
|
||||
if (busy.value) {
|
||||
return
|
||||
}
|
||||
const run = action
|
||||
if (run) {
|
||||
busy.value = true
|
||||
try {
|
||||
await run()
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
settle(true)
|
||||
}
|
||||
|
||||
function cancel(): void {
|
||||
if (busy.value) {
|
||||
return
|
||||
}
|
||||
settle(false)
|
||||
}
|
||||
|
||||
return { request, busy, ask, confirm, cancel }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user