data-table-bulk-actions.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { useState } from 'react'
  2. import { type Table } from '@tanstack/react-table'
  3. import { Trash2, CircleArrowUp, ArrowUpDown, Download } from 'lucide-react'
  4. import { toast } from 'sonner'
  5. import { sleep } from '@/utils/sleep'
  6. import { Button } from '@/components/ui/button'
  7. import {
  8. DropdownMenu,
  9. DropdownMenuContent,
  10. DropdownMenuItem,
  11. DropdownMenuTrigger,
  12. } from '@/components/ui/dropdown-menu'
  13. import {
  14. Tooltip,
  15. TooltipContent,
  16. TooltipTrigger,
  17. } from '@/components/ui/tooltip'
  18. import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
  19. import { priorities, statuses } from '../data/data'
  20. import { type Task } from '../data/schema'
  21. import { TasksMultiDeleteDialog } from './tasks-multi-delete-dialog'
  22. type DataTableBulkActionsProps<TData> = {
  23. table: Table<TData>
  24. }
  25. export function DataTableBulkActions<TData>({
  26. table,
  27. }: DataTableBulkActionsProps<TData>) {
  28. const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
  29. const selectedRows = table.getFilteredSelectedRowModel().rows
  30. const handleBulkStatusChange = (status: string) => {
  31. const selectedTasks = selectedRows.map((row) => row.original as Task)
  32. toast.promise(sleep(2000), {
  33. loading: 'Updating status...',
  34. success: () => {
  35. table.resetRowSelection()
  36. return `Status updated to "${status}" for ${selectedTasks.length} task${selectedTasks.length > 1 ? 's' : ''}.`
  37. },
  38. error: 'Error',
  39. })
  40. table.resetRowSelection()
  41. }
  42. const handleBulkPriorityChange = (priority: string) => {
  43. const selectedTasks = selectedRows.map((row) => row.original as Task)
  44. toast.promise(sleep(2000), {
  45. loading: 'Updating priority...',
  46. success: () => {
  47. table.resetRowSelection()
  48. return `Priority updated to "${priority}" for ${selectedTasks.length} task${selectedTasks.length > 1 ? 's' : ''}.`
  49. },
  50. error: 'Error',
  51. })
  52. table.resetRowSelection()
  53. }
  54. const handleBulkExport = () => {
  55. const selectedTasks = selectedRows.map((row) => row.original as Task)
  56. toast.promise(sleep(2000), {
  57. loading: 'Exporting tasks...',
  58. success: () => {
  59. table.resetRowSelection()
  60. return `Exported ${selectedTasks.length} task${selectedTasks.length > 1 ? 's' : ''} to CSV.`
  61. },
  62. error: 'Error',
  63. })
  64. table.resetRowSelection()
  65. }
  66. return (
  67. <>
  68. <BulkActionsToolbar table={table} entityName='task'>
  69. <DropdownMenu>
  70. <Tooltip>
  71. <TooltipTrigger asChild>
  72. <DropdownMenuTrigger asChild>
  73. <Button
  74. variant='outline'
  75. size='icon'
  76. className='size-8'
  77. aria-label='Update status'
  78. title='Update status'
  79. >
  80. <CircleArrowUp />
  81. <span className='sr-only'>Update status</span>
  82. </Button>
  83. </DropdownMenuTrigger>
  84. </TooltipTrigger>
  85. <TooltipContent>
  86. <p>Update status</p>
  87. </TooltipContent>
  88. </Tooltip>
  89. <DropdownMenuContent sideOffset={14}>
  90. {statuses.map((status) => (
  91. <DropdownMenuItem
  92. key={status.value}
  93. defaultValue={status.value}
  94. onClick={() => handleBulkStatusChange(status.value)}
  95. >
  96. {status.icon && (
  97. <status.icon className='text-muted-foreground size-4' />
  98. )}
  99. {status.label}
  100. </DropdownMenuItem>
  101. ))}
  102. </DropdownMenuContent>
  103. </DropdownMenu>
  104. <DropdownMenu>
  105. <Tooltip>
  106. <TooltipTrigger asChild>
  107. <DropdownMenuTrigger asChild>
  108. <Button
  109. variant='outline'
  110. size='icon'
  111. className='size-8'
  112. aria-label='Update priority'
  113. title='Update priority'
  114. >
  115. <ArrowUpDown />
  116. <span className='sr-only'>Update priority</span>
  117. </Button>
  118. </DropdownMenuTrigger>
  119. </TooltipTrigger>
  120. <TooltipContent>
  121. <p>Update priority</p>
  122. </TooltipContent>
  123. </Tooltip>
  124. <DropdownMenuContent sideOffset={14}>
  125. {priorities.map((priority) => (
  126. <DropdownMenuItem
  127. key={priority.value}
  128. defaultValue={priority.value}
  129. onClick={() => handleBulkPriorityChange(priority.value)}
  130. >
  131. {priority.icon && (
  132. <priority.icon className='text-muted-foreground size-4' />
  133. )}
  134. {priority.label}
  135. </DropdownMenuItem>
  136. ))}
  137. </DropdownMenuContent>
  138. </DropdownMenu>
  139. <Tooltip>
  140. <TooltipTrigger asChild>
  141. <Button
  142. variant='outline'
  143. size='icon'
  144. onClick={() => handleBulkExport()}
  145. className='size-8'
  146. aria-label='Export tasks'
  147. title='Export tasks'
  148. >
  149. <Download />
  150. <span className='sr-only'>Export tasks</span>
  151. </Button>
  152. </TooltipTrigger>
  153. <TooltipContent>
  154. <p>Export tasks</p>
  155. </TooltipContent>
  156. </Tooltip>
  157. <Tooltip>
  158. <TooltipTrigger asChild>
  159. <Button
  160. variant='destructive'
  161. size='icon'
  162. onClick={() => setShowDeleteConfirm(true)}
  163. className='size-8'
  164. aria-label='Delete selected tasks'
  165. title='Delete selected tasks'
  166. >
  167. <Trash2 />
  168. <span className='sr-only'>Delete selected tasks</span>
  169. </Button>
  170. </TooltipTrigger>
  171. <TooltipContent>
  172. <p>Delete selected tasks</p>
  173. </TooltipContent>
  174. </Tooltip>
  175. </BulkActionsToolbar>
  176. <TasksMultiDeleteDialog
  177. open={showDeleteConfirm}
  178. onOpenChange={setShowDeleteConfirm}
  179. table={table}
  180. />
  181. </>
  182. )
  183. }