feat: add template checkpoint edit dialog and vendor management components

- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints.
- Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete.
- Created VendorContext for managing vendor state across components.
- Developed VendorGeneralInfo component to display detailed vendor information.
- Introduced AedSymbol and Money components for consistent currency representation.
- Added PromptDialog for user input prompts throughout the application.
- Implemented RelationLink component for unified related-data display in CRUD tables.
- Created InspectionTemplatesClient for API interactions related to inspection templates.
This commit is contained in:
2026-05-18 12:08:42 +04:00
parent 54d11f01b4
commit 4bfd8c84a9
111 changed files with 5002 additions and 1047 deletions
@@ -0,0 +1,30 @@
import { cn } from "@/shared/lib/utils"
type AedSymbolProps = {
className?: string
/** Accessible label for screen readers. Defaults to "Dirham" (do not show "AED" alongside the symbol per CBUAE guidelines). */
title?: string
}
/**
* Official UAE Dirham symbol (CBUAE, 2025).
* Glyph source: Central Bank of the UAE, released under CC0.
* Per CBUAE guidelines: place immediately before the amount, match numeral size/style,
* preserve aspect ratio (no distortion), and do not pair with the visible text "AED".
*/
export function AedSymbol({ className, title = "Dirham" }: AedSymbolProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 870"
role="img"
aria-label={title}
className={cn("inline-block h-[0.85em] w-auto align-[-0.08em]", className)}
fill="currentColor"
preserveAspectRatio="xMidYMid meet"
>
<title>{title}</title>
<path d="m88.3 1c0.4 0.6 2.6 3.3 4.7 5.9 15.3 18.2 26.8 47.8 33 85.1 4.1 24.5 4.3 32.2 4.3 125.6v87h-41.8c-38.2 0-42.6-0.2-50.1-1.7-11.8-2.5-24-9.2-32.2-17.8-6.5-6.9-6.3-7.3-5.9 13.6 0.5 17.3 0.7 19.2 3.2 28.6 4 14.9 9.5 26 17.8 35.9 11.3 13.6 22.8 21.2 39.2 26.3 3.5 1 10.9 1.4 37.1 1.6l32.7 0.5v43.3 43.4l-46.1-0.3-46.3-0.3-8-3.2c-9.5-3.8-13.8-6.6-23.1-14.9l-6.8-6.1 0.4 19.1c0.5 17.7 0.6 19.7 3.1 28.7 8.7 31.8 29.7 54.5 57.4 61.9 6.9 1.9 9.6 2 38.5 2.4l30.9 0.4v89.6c0 54.1-0.3 94-0.8 100.8-0.5 6.2-2.1 17.8-3.5 25.9-6.5 37.3-18.2 65.4-35 83.6l-3.4 3.7h169.1c101.1 0 176.7-0.4 187.8-0.9 19.5-1 63-5.3 72.8-7.4 3.1-0.6 8.9-1.5 12.7-2.1 8.1-1.2 21.5-4 40.8-8.9 27.2-6.8 52-15.3 76.3-26.1 7.6-3.4 29.4-14.5 35.2-18 3.1-1.8 6.8-4 8.2-4.7 3.9-2.1 10.4-6.3 19.9-13.1 4.7-3.4 9.4-6.7 10.4-7.4 4.2-2.8 18.7-14.9 25.3-21 25.1-23.1 46.1-48.8 62.4-76.3 2.3-4 5.3-9 6.6-11.1 3.3-5.6 16.9-33.6 18.2-37.8 0.6-1.9 1.4-3.9 1.8-4.3 2.6-3.4 17.6-50.6 19.4-60.9 0.6-3.3 0.9-3.8 3.4-4.3 1.6-0.3 24.9-0.3 51.8-0.1 53.8 0.4 53.8 0.4 65.7 5.9 6.7 3.1 8.7 4.5 16.1 11.2 9.7 8.7 8.8 10.1 8.2-11.7-0.4-12.8-0.9-20.7-1.8-23.9-3.4-12.3-4.2-14.9-7.2-21.1-9.8-21.4-26.2-36.7-47.2-44l-8.2-3-33.4-0.4-33.3-0.5 0.4-11.7c0.4-15.4 0.4-45.9-0.1-61.6l-0.4-12.6 44.6-0.2c38.2-0.2 45.3 0 49.5 1.1 12.6 3.5 21.1 8.3 31.5 17.8l5.8 5.4v-14.8c0-17.6-0.9-25.4-4.5-37-7.1-23.5-21.1-41-41.1-51.8-13-7-13.8-7.2-58.5-7.5-26.2-0.2-39.9-0.6-40.6-1.2-0.6-0.6-1.1-1.6-1.1-2.4 0-0.8-1.5-7.1-3.5-13.9-23.4-82.7-67.1-148.4-131-197.1-8.7-6.7-30-20.8-38.6-25.6-3.3-1.9-6.9-3.9-7.8-4.5-4.2-2.3-28.3-14.1-34.3-16.6-3.6-1.6-8.3-3.6-10.4-4.4-35.3-15.3-94.5-29.8-139.7-34.3-7.4-0.7-17.2-1.8-21.7-2.2-20.4-2.3-48.7-2.6-209.4-2.6-135.8 0-169.9 0.3-169.4 1zm330.7 43.3c33.8 2 54.6 4.6 78.9 10.5 74.2 17.6 126.4 54.8 164.3 117 3.5 5.8 18.3 36 20.5 42.1 10.5 28.3 15.6 45.1 20.1 67.3 1.1 5.4 2.6 12.6 3.3 16 0.7 3.3 1 6.4 0.7 6.7-0.5 0.4-100.9 0.6-223.3 0.5l-222.5-0.2-0.3-128.5c-0.1-70.6 0-129.3 0.3-130.4l0.4-1.9h71.1c39 0 78 0.4 86.5 0.9zm297.5 350.3c0.7 4.3 0.7 77.3 0 80.9l-0.6 2.7-227.5-0.2-227.4-0.3-0.2-42.4c-0.2-23.3 0-42.7 0.2-43.1 0.3-0.5 97.2-0.8 227.7-0.8h227.2zm-10.2 171.7c0.5 1.5-1.9 13.8-6.8 33.8-5.6 22.5-13.2 45.2-20.9 62-3.8 8.6-13.3 27.2-15.6 30.7-1.1 1.6-4.3 6.7-7.1 11.2-18 28.2-43.7 53.9-73 72.9-10.7 6.8-32.7 18.4-38.6 20.2-1.2 0.3-2.5 0.9-3 1.3-0.7 0.6-9.8 4-20.4 7.8-19.5 6.9-56.6 14.4-86.4 17.5-19.3 1.9-22.4 2-96.7 2h-76.9v-129.7-129.8l220.9-0.4c121.5-0.2 221.6-0.5 222.4-0.7 0.9-0.1 1.8 0.5 2.1 1.2z" />
</svg>
)
}
@@ -1,9 +1,10 @@
"use client"
import type { ReactNode } from "react"
import { Separator } from "@/shared/components/ui/separator"
import { formatCurrency } from "@/shared/utils/formatters"
import { cn } from "@/shared/lib/utils"
import type { DocumentTotals } from "@/shared/hooks/use-document-totals"
import { Money } from "@/shared/components/money"
export type DocumentTotalsSummaryProps = {
totals: DocumentTotals
@@ -20,7 +21,7 @@ function SummaryRow({
className,
}: {
label: string
value: string
value: ReactNode
muted?: boolean
className?: string
}) {
@@ -58,7 +59,7 @@ export function DocumentTotalsSummary({
<SummaryRow
key={label}
label={label}
value={formatCurrency(amount)}
value={<Money value={amount} />}
muted
/>
))}
@@ -66,13 +67,13 @@ export function DocumentTotalsSummary({
{groupLabels && Object.keys(groupLabels).length > 0 && <Separator />}
{/* Subtotal */}
<SummaryRow label="Subtotal" value={formatCurrency(subTotal)} />
<SummaryRow label="Subtotal" value={<Money value={subTotal} />} />
{/* Line-item discount */}
{discountType === "line_item_level" && lineItemDiscount > 0 && (
<SummaryRow
label="Line Discounts"
value={`– ${formatCurrency(lineItemDiscount)}`}
value={<Money value={lineItemDiscount} prefix="– " />}
muted
/>
)}
@@ -81,7 +82,7 @@ export function DocumentTotalsSummary({
{discountType === "transaction_level" && transactionDiscount > 0 && (
<SummaryRow
label="Discount"
value={`– ${formatCurrency(transactionDiscount)}`}
value={<Money value={transactionDiscount} prefix="– " />}
muted
/>
)}
@@ -90,7 +91,7 @@ export function DocumentTotalsSummary({
{taxAmount > 0 && (
<SummaryRow
label={taxLabel ?? "Tax"}
value={`+ ${formatCurrency(taxAmount)}`}
value={<Money value={taxAmount} prefix="+ " />}
muted
/>
)}
@@ -103,7 +104,7 @@ export function DocumentTotalsSummary({
"bg-primary/10 text-primary",
)}>
<span>Total</span>
<span>{formatCurrency(total)}</span>
<span><Money value={total} /></span>
</div>
</div>
)
@@ -1,47 +0,0 @@
"use client"
import { useState } from "react"
import { Button } from "@/shared/components/ui/button"
import { Download, Loader2 } from "lucide-react"
import { toast } from "sonner"
type DownloadSampleButtonProps = {
onDownload: () => Promise<Blob>
fileName?: string
label?: string
}
export function DownloadSampleButton({
onDownload,
fileName = "import-sample",
label = "Sample",
}: DownloadSampleButtonProps) {
const [isPending, setIsPending] = useState(false)
const handleDownload = async () => {
setIsPending(true)
try {
const blob = await onDownload()
const url = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = `${fileName}.xlsx`
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
URL.revokeObjectURL(url)
toast.success("Sample downloaded successfully")
} catch (err: any) {
toast.error(err?.message ?? "Failed to download sample")
} finally {
setIsPending(false)
}
}
return (
<Button size="sm" variant="outline" disabled={isPending} onClick={handleDownload}>
{isPending ? <Loader2 className="animate-spin" /> : <Download />}
{label}
</Button>
)
}
@@ -2,7 +2,15 @@
import React, { useRef, useState } from "react"
import { Button } from "@/shared/components/ui/button"
import { Download, Loader2 } from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog"
import { Download, FileSpreadsheet, Loader2, UploadCloud } from "lucide-react"
import { toast } from "sonner"
import { ImportResultsDialog } from "@/shared/components/import-results-dialog"
import type { ImportDataResponse } from "@garage/api"
@@ -13,29 +21,70 @@ type ImportDataButtonProps = {
accept?: string
label?: string
entityLabel?: string
onDownloadSample?: () => Promise<Blob>
sampleFileName?: string
notes?: React.ReactNode[]
}
const DEFAULT_NOTES: React.ReactNode[] = [
"Download the sample file and use it as a template — keep the column headers exactly as provided.",
"Supported file formats: .xlsx, .xls and .csv.",
"Fields marked as required in the sample must not be empty.",
"Existing records will not be overwritten. Each row is imported as a new record.",
"After uploading, you'll see a results summary with any rows that failed and why.",
]
export function ImportDataButton({
onImport,
onSuccess,
accept = ".xlsx,.xls,.csv",
label = "Import",
entityLabel,
onDownloadSample,
sampleFileName,
notes,
}: ImportDataButtonProps) {
const inputRef = useRef<HTMLInputElement>(null)
const [isPending, setIsPending] = useState(false)
const [stepsOpen, setStepsOpen] = useState(false)
const [isImporting, setIsImporting] = useState(false)
const [isDownloadingSample, setIsDownloadingSample] = useState(false)
const [result, setResult] = useState<ImportDataResponse["data"] | null>(null)
const [resultOpen, setResultOpen] = useState(false)
const effectiveNotes = notes && notes.length > 0 ? notes : DEFAULT_NOTES
const entityForCopy = entityLabel ?? "records"
const handleDownloadSample = async () => {
if (!onDownloadSample) return
setIsDownloadingSample(true)
try {
const blob = await onDownloadSample()
const url = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = `${sampleFileName ?? "import-sample"}.xlsx`
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
URL.revokeObjectURL(url)
toast.success("Sample downloaded successfully")
} catch (err: any) {
toast.error(err?.message ?? "Failed to download sample")
} finally {
setIsDownloadingSample(false)
}
}
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setIsPending(true)
setIsImporting(true)
try {
const response = await onImport(file)
const data = response.data ?? { imported_count: 0, failed_count: 0, failed_rows: [] }
setResult(data)
setStepsOpen(false)
setResultOpen(true)
if (data.failed_count === 0) {
@@ -46,7 +95,7 @@ export function ImportDataButton({
} catch (err: any) {
toast.error(err?.message ?? "Failed to import data")
} finally {
setIsPending(false)
setIsImporting(false)
if (inputRef.current) inputRef.current.value = ""
}
}
@@ -63,12 +112,86 @@ export function ImportDataButton({
<Button
size="sm"
variant="outline"
disabled={isPending}
onClick={() => inputRef.current?.click()}
onClick={() => setStepsOpen(true)}
>
{isPending ? <Loader2 className="animate-spin" /> : <Download />}
<Download />
{label}
</Button>
<Dialog
open={stepsOpen}
onOpenChange={(open) => {
if (isImporting) return
setStepsOpen(open)
}}
>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Import {entityForCopy.toLowerCase()}</DialogTitle>
<DialogDescription>
Follow these steps to import {entityForCopy.toLowerCase()} from a spreadsheet.
</DialogDescription>
</DialogHeader>
{onDownloadSample && (
<div className="flex items-start gap-3 rounded-md border bg-muted/30 p-3">
<FileSpreadsheet className="mt-0.5 size-5 shrink-0 text-muted-foreground" />
<div className="flex-1">
<p className="text-sm font-medium">Step 1 — Download the sample file</p>
<p className="text-xs text-muted-foreground">
Use it as a template so columns are recognized correctly.
</p>
</div>
<Button
size="sm"
variant="outline"
disabled={isDownloadingSample}
onClick={handleDownloadSample}
>
{isDownloadingSample ? (
<Loader2 className="animate-spin" />
) : (
<Download />
)}
Sample
</Button>
</div>
)}
<div>
<p className="mb-2 text-sm font-medium">
{onDownloadSample ? "Step 2 — Notes before you upload" : "Notes before you upload"}
</p>
<ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
{effectiveNotes.map((note, idx) => (
<li key={idx}>{note}</li>
))}
</ul>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setStepsOpen(false)}
disabled={isImporting}
>
Cancel
</Button>
<Button
disabled={isImporting}
onClick={() => inputRef.current?.click()}
>
{isImporting ? (
<Loader2 className="animate-spin" />
) : (
<UploadCloud />
)}
{isImporting ? "Uploading..." : "Choose file & upload"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{result && (
<ImportResultsDialog
open={resultOpen}
@@ -0,0 +1,48 @@
import { AedSymbol } from "./aed-symbol"
import { cn } from "@/shared/lib/utils"
type MoneyProps = {
value?: number | string | null
/** Prepended before the symbol (e.g. "–" or "+"). */
prefix?: string
/** Hide the AED symbol — useful when the column header already conveys currency. */
hideSymbol?: boolean
/** Locale override for number grouping. */
locale?: string
/** Minimum/maximum fraction digits. Defaults to 2. */
fractionDigits?: number
/** Fallback rendered when value is null/undefined/NaN. */
fallback?: string
className?: string
}
function formatAmount(value: number | string | null | undefined, locale: string | undefined, digits: number): string | null {
if (value == null || value === "") return null
const num = typeof value === "string" ? Number(value) : value
if (Number.isNaN(num)) return null
return new Intl.NumberFormat(locale, {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
}).format(num)
}
export function Money({
value,
prefix,
hideSymbol = false,
locale,
fractionDigits = 2,
fallback = "—",
className,
}: MoneyProps) {
const formatted = formatAmount(value, locale, fractionDigits)
if (formatted == null) return <span className={className}>{fallback}</span>
return (
<span className={cn("inline-flex items-center gap-[0.2em] whitespace-nowrap", className)}>
{prefix ? <span>{prefix}</span> : null}
{!hideSymbol ? <AedSymbol /> : null}
<span>{formatted}</span>
</span>
)
}
@@ -0,0 +1,121 @@
"use client"
import { useEffect, useState } from "react"
import { create } from "zustand"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog"
import { Button } from "@/shared/components/ui/button"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
// ── Types ──
export type PromptOptions = {
title?: string
description?: string
label?: string
placeholder?: string
defaultValue?: string
confirmLabel?: string
cancelLabel?: string
required?: boolean
}
type PromptStore = {
open: boolean
options: PromptOptions
resolve: ((value: string | null) => void) | null
_show: (options: PromptOptions) => Promise<string | null>
_close: (value: string | null) => void
}
// ── Store ──
const usePromptStore = create<PromptStore>((set, get) => ({
open: false,
options: {},
resolve: null,
_show: (options) =>
new Promise<string | null>((resolve) => {
set({ open: true, options, resolve })
}),
_close: (value) => {
const { resolve } = get()
resolve?.(value)
set({ open: false, resolve: null })
},
}))
// ── Imperative API ──
export function prompt(options: PromptOptions = {}): Promise<string | null> {
return usePromptStore.getState()._show(options)
}
// ── Dialog component (mount once in root layout) ──
export function PromptDialog() {
const { open, options, _close } = usePromptStore()
const [value, setValue] = useState("")
// Reset value whenever the dialog opens
useEffect(() => {
if (open) setValue(options.defaultValue ?? "")
}, [open, options.defaultValue])
const submit = () => {
if (options.required !== false && value.trim() === "") return
_close(value.trim())
}
return (
<Dialog
open={open}
onOpenChange={(v) => {
if (!v) _close(null)
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{options.title ?? "Enter a value"}</DialogTitle>
{options.description && (
<DialogDescription>{options.description}</DialogDescription>
)}
</DialogHeader>
<div className="grid gap-2">
{options.label && <Label htmlFor="prompt-input">{options.label}</Label>}
<Input
id="prompt-input"
autoFocus
value={value}
placeholder={options.placeholder}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
submit()
}
}}
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => _close(null)}>
{options.cancelLabel ?? "Cancel"}
</Button>
<Button onClick={submit} disabled={options.required !== false && value.trim() === ""}>
{options.confirmLabel ?? "OK"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,78 @@
"use client"
import Link from "next/link"
import type { ComponentType, MouseEvent, ReactNode } from "react"
import { Button, buttonVariants } from "@/shared/components/ui/button"
import { cn } from "@/shared/lib/utils"
type IconComponent = ComponentType<{ className?: string }>
export type RelationLinkProps = {
/** Destination href. When falsy the chip renders as a non-interactive button-styled span. */
href?: string | null
/** Primary label rendered inside the button. */
label?: ReactNode
/** Optional icon shown before the label. */
icon?: IconComponent
/** Rendered when label is empty (with or without href). Defaults to "—". */
fallback?: ReactNode
/** Optional secondary text rendered after the label (e.g. phone, plate). */
meta?: ReactNode
className?: string
}
/**
* Unified cell for related-data columns across every CRUD table.
* Renders an outlined button-style chip either way:
* - With href: clickable next/link wrapped by a Button (asChild).
* - Without href: same outline styling on a plain span so visual columns
* stay consistent across resources that don't have detail routes.
*/
export function RelationLink({
href,
label,
icon: Icon,
fallback = "—",
meta,
className,
}: RelationLinkProps) {
const hasLabel = label != null && label !== ""
if (!hasLabel && !href) {
return <span className="text-muted-foreground">{fallback}</span>
}
const body = (
<>
{Icon ? <Icon /> : null}
<span>{hasLabel ? label : fallback}</span>
{meta ? <span className="ms-1 text-muted-foreground">· {meta}</span> : null}
</>
)
if (!href) {
return (
<span
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"cursor-default pointer-events-none",
className,
)}
>
{body}
</span>
)
}
return (
<Button
variant="outline"
size="sm"
asChild
className={className}
onClick={(event: MouseEvent<HTMLButtonElement>) => event.stopPropagation()}
>
<Link href={href}>{body}</Link>
</Button>
)
}
@@ -11,7 +11,6 @@ import {
} from "@/shared/components/ui/dialog"
import { CrudResource, type CrudResourceProps } from "@/shared/data-view/resource-page"
import type { ResourcePageClient, ResourceItem } from "@/shared/data-view/resource-page"
import { Card, CardContent } from "@/shared/components/ui/card"
export type ResourceSelectorDialogProps<TClient extends ResourcePageClient> = {
/** Dialog title shown in the header */
@@ -31,6 +30,10 @@ export type ResourceSelectorDialogProps<TClient extends ResourcePageClient> = {
* - "single": row click immediately fires onConfirm with the clicked row
*/
selectionMode?: "single" | "multi"
/** Show a search input above the table. Defaults to true. */
searchable?: boolean
/** Placeholder for the search input. */
searchPlaceholder?: string
}
export function ResourceSelectorDialog<TClient extends ResourcePageClient>({
@@ -41,6 +44,8 @@ export function ResourceSelectorDialog<TClient extends ResourcePageClient>({
crudProps,
rowKey,
selectionMode = "multi",
searchable = true,
searchPlaceholder = "Search...",
}: ResourceSelectorDialogProps<TClient>) {
type TItem = ResourceItem<TClient>
@@ -70,33 +75,36 @@ export function ResourceSelectorDialog<TClient extends ResourcePageClient>({
onOpenChange(false)
}
const sharedCrudProps = {
...crudProps,
localState: crudProps.localState ?? true,
searchable: crudProps.searchable ?? searchable,
searchPlaceholder: crudProps.searchPlaceholder ?? searchPlaceholder,
}
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) handleCancel() }}>
<DialogContent className="min-w-4xl max-h-[90vh] flex flex-col">
<DialogContent className="min-w-4xl max-h-[90vh] flex flex-col gap-3 p-4">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">{title}</DialogTitle>
<DialogTitle className="text-xl font-semibold">{title}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-auto">
<Card className="rounded-none border-0 shadow-none">
<CardContent className="p-0">
{selectionMode === "single" ? (
<CrudResource<TClient>
{...crudProps}
onRowClick={handleRowClick}
/>
) : (
<CrudResource<TClient>
{...crudProps}
tableProps={{
selection: {
onSelectionChange: handleSelectionChange,
...(rowKey ? { rowKey } : {}),
},
}}
/>
)}
</CardContent>
</Card>
<div className="flex-1 overflow-auto min-h-0">
{selectionMode === "single" ? (
<CrudResource<TClient>
{...sharedCrudProps}
onRowClick={handleRowClick}
/>
) : (
<CrudResource<TClient>
{...sharedCrudProps}
tableProps={{
selection: {
onSelectionChange: handleSelectionChange,
...(rowKey ? { rowKey } : {}),
},
}}
/>
)}
</div>
{selectionMode === "multi" && (
<DialogFooter>