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:
Jakub Zych
2026-10-02 19:56:10 +02:00
parent a0c182745e
commit 69a37456f6
20 changed files with 1698 additions and 112 deletions

View File

@@ -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>