DataTableSheetRowAction.tsx200 lines · main
| 1 | import { Table } from '@tanstack/react-table' |
| 2 | import { endOfDay, endOfHour, startOfDay, startOfHour } from 'date-fns' |
| 3 | import { |
| 4 | CalendarClock, |
| 5 | CalendarDays, |
| 6 | CalendarSearch, |
| 7 | ChevronLeft, |
| 8 | ChevronRight, |
| 9 | Copy, |
| 10 | Equal, |
| 11 | Filter, |
| 12 | } from 'lucide-react' |
| 13 | import { ComponentPropsWithRef } from 'react' |
| 14 | import { |
| 15 | cn, |
| 16 | DropdownMenu, |
| 17 | DropdownMenuContent, |
| 18 | DropdownMenuGroup, |
| 19 | DropdownMenuItem, |
| 20 | DropdownMenuSeparator, |
| 21 | DropdownMenuTrigger, |
| 22 | } from 'ui' |
| 23 | |
| 24 | import { DataTableFilterField } from '@/components/ui/DataTable/DataTable.types' |
| 25 | import { useCopyToClipboard } from '@/hooks/ui/useCopyToClipboard' |
| 26 | |
| 27 | interface DataTableSheetRowActionProps< |
| 28 | TData, |
| 29 | TFields extends DataTableFilterField<TData>, |
| 30 | > extends ComponentPropsWithRef<typeof DropdownMenuTrigger> { |
| 31 | fieldValue?: TFields['value'] |
| 32 | filterFields: TFields[] |
| 33 | value: string | number |
| 34 | table: Table<TData> |
| 35 | label?: string |
| 36 | } |
| 37 | |
| 38 | export function DataTableSheetRowAction<TData, TFields extends DataTableFilterField<TData>>({ |
| 39 | fieldValue, |
| 40 | filterFields, |
| 41 | value, |
| 42 | children, |
| 43 | className, |
| 44 | table, |
| 45 | label, |
| 46 | onKeyDown, |
| 47 | ...props |
| 48 | }: DataTableSheetRowActionProps<TData, TFields>) { |
| 49 | const { copy, isCopied } = useCopyToClipboard() |
| 50 | |
| 51 | const field = !!fieldValue ? filterFields.find((f) => f.value === fieldValue) : undefined |
| 52 | const column = |
| 53 | !!fieldValue && !!field |
| 54 | ? table.getAllColumns().find((c) => c.id === fieldValue.toString()) |
| 55 | : undefined |
| 56 | |
| 57 | function renderOptions() { |
| 58 | if (!field) return null |
| 59 | switch (field.type) { |
| 60 | case 'checkbox': |
| 61 | return ( |
| 62 | <DropdownMenuItem |
| 63 | onClick={() => { |
| 64 | const filterValue = column?.getFilterValue() as undefined | Array<unknown> |
| 65 | const newValue = filterValue?.includes(value) |
| 66 | ? filterValue |
| 67 | : [...(filterValue || []), value] |
| 68 | |
| 69 | column?.setFilterValue(newValue) |
| 70 | }} |
| 71 | className="flex items-center gap-2" |
| 72 | > |
| 73 | <Filter size={12} /> |
| 74 | Add as filter for {column?.id} |
| 75 | </DropdownMenuItem> |
| 76 | ) |
| 77 | case 'input': |
| 78 | return ( |
| 79 | <DropdownMenuItem |
| 80 | onClick={() => column?.setFilterValue(value)} |
| 81 | className="flex items-center gap-2" |
| 82 | > |
| 83 | <Filter size={12} /> |
| 84 | Add as filter for {column?.id} |
| 85 | </DropdownMenuItem> |
| 86 | ) |
| 87 | case 'slider': |
| 88 | return ( |
| 89 | <DropdownMenuGroup> |
| 90 | <DropdownMenuItem |
| 91 | onClick={() => column?.setFilterValue([0, value])} |
| 92 | className="flex items-center gap-2" |
| 93 | > |
| 94 | {/* FIXME: change icon as it is not clear */} |
| 95 | <ChevronLeft size={12} /> |
| 96 | Less or equal than |
| 97 | </DropdownMenuItem> |
| 98 | <DropdownMenuItem |
| 99 | onClick={() => column?.setFilterValue([value, 5000])} |
| 100 | className="flex items-center gap-2" |
| 101 | > |
| 102 | {/* FIXME: change icon as it is not clear */} |
| 103 | <ChevronRight size={12} /> |
| 104 | Greater or equal than |
| 105 | </DropdownMenuItem> |
| 106 | <DropdownMenuItem |
| 107 | onClick={() => column?.setFilterValue([value])} |
| 108 | className="flex items-center gap-2" |
| 109 | > |
| 110 | <Equal size={12} /> |
| 111 | Equal to |
| 112 | </DropdownMenuItem> |
| 113 | </DropdownMenuGroup> |
| 114 | ) |
| 115 | case 'timerange': |
| 116 | const date = new Date(value) |
| 117 | return ( |
| 118 | <DropdownMenuGroup> |
| 119 | <DropdownMenuItem |
| 120 | onClick={() => column?.setFilterValue([date])} |
| 121 | className="flex items-center gap-2" |
| 122 | > |
| 123 | <CalendarSearch size={12} /> |
| 124 | Exact timestamp |
| 125 | </DropdownMenuItem> |
| 126 | <DropdownMenuItem |
| 127 | onClick={() => { |
| 128 | const start = startOfHour(date) |
| 129 | const end = endOfHour(date) |
| 130 | column?.setFilterValue([start, end]) |
| 131 | }} |
| 132 | className="flex items-center gap-2" |
| 133 | > |
| 134 | <CalendarClock size={12} /> |
| 135 | Same hour |
| 136 | </DropdownMenuItem> |
| 137 | <DropdownMenuItem |
| 138 | onClick={() => { |
| 139 | const start = startOfDay(date) |
| 140 | const end = endOfDay(date) |
| 141 | column?.setFilterValue([start, end]) |
| 142 | }} |
| 143 | className="flex items-center gap-2" |
| 144 | > |
| 145 | <CalendarDays size={12} /> |
| 146 | Same day |
| 147 | </DropdownMenuItem> |
| 148 | </DropdownMenuGroup> |
| 149 | ) |
| 150 | default: |
| 151 | return null |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | return ( |
| 156 | <DropdownMenu> |
| 157 | <DropdownMenuTrigger |
| 158 | className={cn( |
| 159 | 'rounded-md ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', |
| 160 | 'relative', |
| 161 | className |
| 162 | )} |
| 163 | onKeyDown={(e) => { |
| 164 | if (e.key === 'ArrowDown') { |
| 165 | // REMINDER: default behavior is to open the dropdown menu |
| 166 | // But because we use it to navigate between rows, we need to prevent it |
| 167 | // and only use "Enter" to select the option |
| 168 | e.preventDefault() |
| 169 | } |
| 170 | onKeyDown?.(e) |
| 171 | }} |
| 172 | {...props} |
| 173 | > |
| 174 | {children} |
| 175 | {isCopied ? ( |
| 176 | <div className="absolute inset-0 flex items-center justify-center rounded-md bg-surface-100/80 backdrop-blur-sm animate-in fade-in duration-150"> |
| 177 | <span className="font-mono text-xs text-foreground-light">Copied</span> |
| 178 | </div> |
| 179 | ) : null} |
| 180 | </DropdownMenuTrigger> |
| 181 | |
| 182 | <DropdownMenuContent align="end" side="bottom" className="w-48 -translate-x-4"> |
| 183 | {!!field && !!column && ( |
| 184 | <> |
| 185 | {renderOptions()} |
| 186 | <DropdownMenuSeparator /> |
| 187 | </> |
| 188 | )} |
| 189 | |
| 190 | <DropdownMenuItem |
| 191 | onClick={() => copy(String(value), { timeout: 1000 })} |
| 192 | className="flex items-center gap-2" |
| 193 | > |
| 194 | <Copy size={12} /> |
| 195 | Copy {label} |
| 196 | </DropdownMenuItem> |
| 197 | </DropdownMenuContent> |
| 198 | </DropdownMenu> |
| 199 | ) |
| 200 | } |