feat(10-03): search, sort, filter, page and bulk-delete any list
- List state (search, sort, dir, page, per_page, filter[<name>]) lives in
the URL query through parseListQuery/toListQuery; every change is a
router replace and clears the selection; search is debounced 300 ms and
resets the page
- DataTable renders the schema columns with a tri-state page checkbox,
asc/desc/none sorting with aria-sort, selected rows, a sticky header,
eight skeleton rows while loading and an empty slot
- CellValue renders text (muted dash when empty, arrays comma-joined),
datetime as YYYY-MM-DD HH:mm and switch as the Tak/Nie pills
- The heading shows the plural recordCount and the create button;
delete sits in the toolbar, disabled without a selection, and confirms
with the plural deleteConfirm in a Reka alert dialog before POSTing
bulk-delete; a 409 shows a danger toast
- FilterBar renders switch (JSON of the option value), daterange
(from..to) and scope filters (choices from filters/{scope}/options)
- Pagination shows the range, the per-page select over perPageOptions
(hidden with one choice) and a pager with ellipsis
- The tracer smoke test skips the new checkbox column; new backend::lang
list keys; boardwalk/dist rebuilt
This commit is contained in:
75
admin/src/components/ui/ConfirmDialog.vue
Normal file
75
admin/src/components/ui/ConfirmDialog.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
} from 'reka-ui'
|
||||
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(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
message: string
|
||||
title?: string
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
danger?: boolean
|
||||
}>(),
|
||||
{ title: undefined, confirmLabel: undefined, cancelLabel: undefined, danger: 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 {
|
||||
emit('update:open', open)
|
||||
if (!open) {
|
||||
emit('cancel')
|
||||
}
|
||||
}
|
||||
|
||||
function onConfirm(): void {
|
||||
emit('confirm')
|
||||
emit('update:open', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogRoot :open="open" @update:open="onOpenChange">
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="fixed inset-0 z-40 bg-overlay" />
|
||||
<AlertDialogContent
|
||||
data-confirm-dialog
|
||||
class="fixed top-1/2 left-1/2 z-50 flex w-[calc(100%-32px)] max-w-[440px] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 rounded-modal bg-surface p-6 text-text shadow-pop"
|
||||
>
|
||||
<AlertDialogTitle class="text-[20px] font-bold tracking-[-0.01em]">
|
||||
{{ title ?? t('backend::lang.form.confirm') }}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription class="text-muted">{{ message }}</AlertDialogDescription>
|
||||
<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"
|
||||
>
|
||||
{{ cancelLabel ?? t('backend::lang.form.cancel') }}
|
||||
</AlertDialogCancel>
|
||||
<button
|
||||
type="button"
|
||||
data-action="confirm"
|
||||
: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"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ confirmLabel ?? t('backend::lang.form.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
31
admin/src/components/ui/confirm.ts
Normal file
31
admin/src/components/ui/confirm.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Promise-based confirmation for ConfirmDialog: `await ask(text)` resolves
|
||||
// true on confirm and false on Anuluj, Esc or a second request.
|
||||
import { ref } from 'vue'
|
||||
|
||||
export interface ConfirmRequest {
|
||||
message: string
|
||||
confirmLabel?: string
|
||||
danger?: boolean
|
||||
}
|
||||
|
||||
export function useConfirm() {
|
||||
const request = ref<ConfirmRequest | null>(null)
|
||||
let resolver: ((ok: boolean) => void) | null = null
|
||||
|
||||
function settle(ok: boolean): void {
|
||||
const resolve = resolver
|
||||
resolver = null
|
||||
request.value = null
|
||||
resolve?.(ok)
|
||||
}
|
||||
|
||||
function ask(next: ConfirmRequest): Promise<boolean> {
|
||||
settle(false)
|
||||
request.value = next
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolver = resolve
|
||||
})
|
||||
}
|
||||
|
||||
return { request, ask, confirm: () => settle(true), cancel: () => settle(false) }
|
||||
}
|
||||
Reference in New Issue
Block a user