first commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import * as React from 'react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AdminActionDialog, type StatusTone } from '@/components/admin'
|
||||
|
||||
export type BatchActionId = 'hide' | 'delete' | 'restore' | string
|
||||
|
||||
export interface BatchActionConfig {
|
||||
id: BatchActionId
|
||||
label: string
|
||||
/** When true, the confirmation requires a note before submit. */
|
||||
destructive?: boolean
|
||||
tone?: StatusTone
|
||||
confirmTitle: string
|
||||
confirmDescription: string
|
||||
confirmLabel?: string
|
||||
}
|
||||
|
||||
interface BatchOperationsToolbarProps {
|
||||
selectedCount: number
|
||||
totalCount?: number
|
||||
onSelectAll?: () => void
|
||||
onClearSelection?: () => void
|
||||
actions: BatchActionConfig[]
|
||||
onAction: (action: BatchActionConfig, remark: string) => void
|
||||
isLoading?: boolean
|
||||
pendingAction?: BatchActionId | null
|
||||
}
|
||||
|
||||
export function BatchOperationsToolbar({
|
||||
selectedCount,
|
||||
totalCount,
|
||||
onSelectAll,
|
||||
onClearSelection,
|
||||
actions,
|
||||
onAction,
|
||||
isLoading = false,
|
||||
pendingAction = null,
|
||||
}: BatchOperationsToolbarProps) {
|
||||
const [pending, setPending] = React.useState<BatchActionConfig | null>(null)
|
||||
const [remark, setRemark] = React.useState('')
|
||||
|
||||
if (selectedCount === 0) return null
|
||||
|
||||
function handleConfirm() {
|
||||
if (!pending) return
|
||||
if (pending.destructive && !remark.trim()) {
|
||||
return
|
||||
}
|
||||
onAction(pending, remark.trim())
|
||||
setPending(null)
|
||||
setRemark('')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-wrap items-center gap-3 rounded-md border border-primary/30 bg-primary/5 px-3 py-2'>
|
||||
<Badge variant='default' className='gap-1'>
|
||||
Selected {selectedCount}
|
||||
{typeof totalCount === 'number' ? ` / ${totalCount}` : ''}
|
||||
</Badge>
|
||||
{onSelectAll && (
|
||||
<Button variant='ghost' size='sm' onClick={onSelectAll}>
|
||||
Select current page
|
||||
</Button>
|
||||
)}
|
||||
{onClearSelection && (
|
||||
<Button variant='ghost' size='sm' onClick={onClearSelection}>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<div className='ml-auto flex flex-wrap items-center gap-2'>
|
||||
{actions.map((action) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
size='sm'
|
||||
variant={action.destructive ? 'destructive' : 'outline'}
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
setRemark('')
|
||||
setPending(action)
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AdminActionDialog
|
||||
open={pending !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setPending(null)
|
||||
setRemark('')
|
||||
}
|
||||
}}
|
||||
tone={pending?.tone ?? (pending?.destructive ? 'danger' : 'info')}
|
||||
title={pending?.confirmTitle ?? 'Confirm bulk action'}
|
||||
description={pending?.confirmDescription ?? ''}
|
||||
confirmLabel={pending?.confirmLabel ?? 'Confirm'}
|
||||
isLoading={isLoading && pendingAction === pending?.id}
|
||||
onConfirm={handleConfirm}
|
||||
>
|
||||
{pending?.destructive ? (
|
||||
<div className='space-y-2'>
|
||||
<label className='text-sm font-medium'>Note (required)</label>
|
||||
<textarea
|
||||
className='min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
|
||||
placeholder='Add a short reason for this action'
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-2'>
|
||||
<label className='text-sm font-medium'>Note (optional)</label>
|
||||
<textarea
|
||||
className='min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
|
||||
placeholder='Add a short note'
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AdminActionDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user