DataTableSheetRowAction.tsx200 lines · main
1import { Table } from '@tanstack/react-table'
2import { endOfDay, endOfHour, startOfDay, startOfHour } from 'date-fns'
3import {
4 CalendarClock,
5 CalendarDays,
6 CalendarSearch,
7 ChevronLeft,
8 ChevronRight,
9 Copy,
10 Equal,
11 Filter,
12} from 'lucide-react'
13import { ComponentPropsWithRef } from 'react'
14import {
15 cn,
16 DropdownMenu,
17 DropdownMenuContent,
18 DropdownMenuGroup,
19 DropdownMenuItem,
20 DropdownMenuSeparator,
21 DropdownMenuTrigger,
22} from 'ui'
23
24import { DataTableFilterField } from '@/components/ui/DataTable/DataTable.types'
25import { useCopyToClipboard } from '@/hooks/ui/useCopyToClipboard'
26
27interface 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
38export 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}