@nocobase/portal-template-default 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.env.example +10 -0
  2. package/Dockerfile +36 -0
  3. package/README.MD +50 -0
  4. package/components.json +25 -0
  5. package/eslint.config.js +28 -0
  6. package/index.html +39 -0
  7. package/package.json +84 -0
  8. package/pnpm-workspace.yaml +2 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/logo-dark.png +0 -0
  11. package/public/logo-mark-dark.png +0 -0
  12. package/public/logo-mark.png +0 -0
  13. package/public/logo.png +0 -0
  14. package/src/App.css +158 -0
  15. package/src/App.tsx +151 -0
  16. package/src/components/app-shell/brand.tsx +70 -0
  17. package/src/components/app-shell/breadcrumb.tsx +81 -0
  18. package/src/components/app-shell/document-title-handler.tsx +169 -0
  19. package/src/components/app-shell/error-component.tsx +134 -0
  20. package/src/components/app-shell/header.tsx +127 -0
  21. package/src/components/app-shell/layout.tsx +39 -0
  22. package/src/components/app-shell/loading-overlay.tsx +36 -0
  23. package/src/components/app-shell/sidebar.tsx +351 -0
  24. package/src/components/app-shell/user-avatar.tsx +42 -0
  25. package/src/components/app-shell/user-info.tsx +53 -0
  26. package/src/components/auth/auth-layout.tsx +82 -0
  27. package/src/components/auth/forgot-password-form.tsx +58 -0
  28. package/src/components/auth/input-password.tsx +38 -0
  29. package/src/components/auth/sign-in-form.tsx +111 -0
  30. package/src/components/auth/sign-up-form.tsx +127 -0
  31. package/src/components/data-table/data-table-filter.tsx +994 -0
  32. package/src/components/data-table/data-table-pagination.tsx +146 -0
  33. package/src/components/data-table/data-table-sorter.tsx +47 -0
  34. package/src/components/data-table/data-table.tsx +321 -0
  35. package/src/components/notifications/toaster.tsx +23 -0
  36. package/src/components/notifications/undoable-notification.tsx +84 -0
  37. package/src/components/notifications/use-notification-provider.tsx +62 -0
  38. package/src/components/resources/buttons/clone.tsx +81 -0
  39. package/src/components/resources/buttons/create.tsx +70 -0
  40. package/src/components/resources/buttons/delete.tsx +113 -0
  41. package/src/components/resources/buttons/edit.tsx +81 -0
  42. package/src/components/resources/buttons/list.tsx +70 -0
  43. package/src/components/resources/buttons/refresh.tsx +81 -0
  44. package/src/components/resources/buttons/show.tsx +81 -0
  45. package/src/components/resources/status-badge.tsx +37 -0
  46. package/src/components/resources/views/create-view.tsx +81 -0
  47. package/src/components/resources/views/edit-view.tsx +104 -0
  48. package/src/components/resources/views/list-view.tsx +86 -0
  49. package/src/components/resources/views/show-view.tsx +108 -0
  50. package/src/components/theme/theme-provider.tsx +71 -0
  51. package/src/components/theme/theme-select.tsx +97 -0
  52. package/src/components/theme/theme-toggle.tsx +66 -0
  53. package/src/components/ui/accordion.tsx +72 -0
  54. package/src/components/ui/alert-dialog.tsx +185 -0
  55. package/src/components/ui/alert.tsx +76 -0
  56. package/src/components/ui/aspect-ratio.tsx +22 -0
  57. package/src/components/ui/avatar.tsx +107 -0
  58. package/src/components/ui/badge.tsx +52 -0
  59. package/src/components/ui/breadcrumb.tsx +125 -0
  60. package/src/components/ui/button.tsx +58 -0
  61. package/src/components/ui/calendar.tsx +221 -0
  62. package/src/components/ui/card.tsx +103 -0
  63. package/src/components/ui/carousel.tsx +240 -0
  64. package/src/components/ui/chart.tsx +373 -0
  65. package/src/components/ui/checkbox.tsx +29 -0
  66. package/src/components/ui/collapsible.tsx +19 -0
  67. package/src/components/ui/command.tsx +192 -0
  68. package/src/components/ui/context-menu.tsx +271 -0
  69. package/src/components/ui/dialog.tsx +160 -0
  70. package/src/components/ui/drawer.tsx +226 -0
  71. package/src/components/ui/dropdown-menu.tsx +268 -0
  72. package/src/components/ui/form.tsx +174 -0
  73. package/src/components/ui/hover-card.tsx +49 -0
  74. package/src/components/ui/input-group.tsx +158 -0
  75. package/src/components/ui/input-otp.tsx +87 -0
  76. package/src/components/ui/input.tsx +20 -0
  77. package/src/components/ui/label.tsx +18 -0
  78. package/src/components/ui/menubar.tsx +278 -0
  79. package/src/components/ui/navigation-menu.tsx +168 -0
  80. package/src/components/ui/pagination.tsx +130 -0
  81. package/src/components/ui/popover.tsx +91 -0
  82. package/src/components/ui/progress.tsx +81 -0
  83. package/src/components/ui/radio-group.tsx +38 -0
  84. package/src/components/ui/resizable.tsx +48 -0
  85. package/src/components/ui/scroll-area.tsx +55 -0
  86. package/src/components/ui/select.tsx +199 -0
  87. package/src/components/ui/separator.tsx +25 -0
  88. package/src/components/ui/sheet.tsx +136 -0
  89. package/src/components/ui/sidebar.tsx +723 -0
  90. package/src/components/ui/skeleton.tsx +13 -0
  91. package/src/components/ui/slider.tsx +52 -0
  92. package/src/components/ui/sonner.tsx +47 -0
  93. package/src/components/ui/switch.tsx +32 -0
  94. package/src/components/ui/table.tsx +114 -0
  95. package/src/components/ui/tabs.tsx +82 -0
  96. package/src/components/ui/textarea.tsx +18 -0
  97. package/src/components/ui/toggle-group.tsx +87 -0
  98. package/src/components/ui/toggle.tsx +43 -0
  99. package/src/components/ui/tooltip.tsx +66 -0
  100. package/src/hooks/use-mobile.ts +19 -0
  101. package/src/index.tsx +17 -0
  102. package/src/lib/utils.ts +19 -0
  103. package/src/pages/blog-posts/create.tsx +177 -0
  104. package/src/pages/blog-posts/edit.tsx +197 -0
  105. package/src/pages/blog-posts/index.ts +4 -0
  106. package/src/pages/blog-posts/list.tsx +131 -0
  107. package/src/pages/blog-posts/show.tsx +81 -0
  108. package/src/pages/categories/create.tsx +73 -0
  109. package/src/pages/categories/edit.tsx +73 -0
  110. package/src/pages/categories/index.ts +4 -0
  111. package/src/pages/categories/list.tsx +86 -0
  112. package/src/pages/categories/show.tsx +37 -0
  113. package/src/pages/forgot-password/index.tsx +6 -0
  114. package/src/pages/forgotPassword/index.tsx +5 -0
  115. package/src/pages/login/index.tsx +5 -0
  116. package/src/pages/register/index.tsx +5 -0
  117. package/src/providers/auth.ts +204 -0
  118. package/src/providers/constants.ts +75 -0
  119. package/src/providers/data.ts +232 -0
  120. package/src/vite-env.d.ts +1 -0
  121. package/tsconfig.json +25 -0
  122. package/tsconfig.node.json +21 -0
  123. package/vite.config.ts +74 -0
@@ -0,0 +1,994 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import { useTranslate, type CrudOperators } from "@refinedev/core";
5
+ import type { Column, Table as ReactTable } from "@tanstack/react-table";
6
+ import type { DateRange } from "react-day-picker";
7
+ import { Check, ChevronsUpDown, ListFilter, X } from "lucide-react";
8
+
9
+ import { Button } from "@/components/ui/button";
10
+ import { Input } from "@/components/ui/input";
11
+ import { Badge } from "@/components/ui/badge";
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from "@/components/ui/popover";
17
+ import {
18
+ Command,
19
+ CommandEmpty,
20
+ CommandInput,
21
+ CommandItem,
22
+ CommandList,
23
+ } from "@/components/ui/command";
24
+ import { Separator } from "@/components/ui/separator";
25
+ import { Calendar } from "@/components/ui/calendar";
26
+ import { cn } from "@/lib/utils";
27
+
28
+ export type DataTableFilterDropdownProps<TData> = {
29
+ column: Column<TData>;
30
+ contentClassName?: string;
31
+ triggerClassName?: string;
32
+ children: (args: {
33
+ isOpen: boolean;
34
+ setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
35
+ }) => React.ReactNode;
36
+ };
37
+
38
+ export function DataTableFilterDropdown<TData>({
39
+ column,
40
+ triggerClassName,
41
+ contentClassName,
42
+ children,
43
+ }: DataTableFilterDropdownProps<TData>) {
44
+ const [isOpen, setIsOpen] = useState(false);
45
+
46
+ const isFiltered = column.getIsFiltered();
47
+
48
+ return (
49
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
50
+ <PopoverTrigger
51
+ render={
52
+ <Button
53
+ onClick={() => setIsOpen(true)}
54
+ variant="ghost"
55
+ size="icon"
56
+ className={cn(
57
+ "data-[state=open]:bg-accent",
58
+ "w-5 h-5",
59
+ {
60
+ "text-primary": isFiltered,
61
+ "text-muted-foreground": !isFiltered,
62
+ },
63
+ triggerClassName
64
+ )}
65
+ />
66
+ }
67
+ >
68
+ <ListFilter className={cn("!h-3", "!w-3")} />
69
+ </PopoverTrigger>
70
+ <PopoverContent
71
+ align="start"
72
+ className={cn("w-full", "shadow-sm", contentClassName)}
73
+ >
74
+ {children({ isOpen, setIsOpen })}
75
+ </PopoverContent>
76
+ </Popover>
77
+ );
78
+ }
79
+
80
+ type DataTableFilterDropdownActionsProps = {
81
+ className?: string;
82
+ isClearDisabled?: boolean;
83
+ isApplyDisabled?: boolean;
84
+ onClear: () => void;
85
+ onApply: () => void;
86
+ };
87
+
88
+ export function DataTableFilterDropdownActions({
89
+ className,
90
+ isClearDisabled,
91
+ isApplyDisabled,
92
+ onClear,
93
+ onApply,
94
+ }: DataTableFilterDropdownActionsProps) {
95
+ const t = useTranslate();
96
+
97
+ return (
98
+ <div
99
+ className={cn(
100
+ "flex",
101
+ "items-center",
102
+ "justify-between",
103
+ "w-full",
104
+ "gap-2",
105
+ className
106
+ )}
107
+ >
108
+ <Button
109
+ size="sm"
110
+ variant="ghost"
111
+ disabled={isClearDisabled}
112
+ className={cn(
113
+ "rounded-sm",
114
+ "text-xs",
115
+ "font-semibold",
116
+ "text-muted-foreground"
117
+ )}
118
+ onClick={() => {
119
+ onClear();
120
+ }}
121
+ >
122
+ <X className={cn("w-3.5", "h-3.5", "text-muted-foreground")} />
123
+ {t("buttons.clear", "Clear")}
124
+ </Button>
125
+
126
+ <Button
127
+ size="sm"
128
+ disabled={isApplyDisabled}
129
+ className={cn("rounded-sm", "text-xs", "font-semibold")}
130
+ onClick={() => {
131
+ onApply();
132
+ }}
133
+ >
134
+ {t("buttons.apply", "Apply")}
135
+ </Button>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ export type DataTableFilterDropdownTextProps<TData> = {
141
+ column: Column<TData>;
142
+ table: ReactTable<TData>;
143
+ defaultOperator?: CrudOperators;
144
+ operators?: CrudOperators[];
145
+ placeholder?: string;
146
+ };
147
+
148
+ export function DataTableFilterDropdownText<TData>({
149
+ column,
150
+ table,
151
+ operators = [
152
+ "eq",
153
+ "ne",
154
+ "contains",
155
+ "ncontains",
156
+ "containss",
157
+ "ncontainss",
158
+ "startswith",
159
+ "nstartswith",
160
+ "startswiths",
161
+ "nstartswiths",
162
+ "endswith",
163
+ "nendswith",
164
+ "endswiths",
165
+ "nendswiths",
166
+ "in",
167
+ "nin",
168
+ "ina",
169
+ "nina",
170
+ ],
171
+ defaultOperator = "eq",
172
+ placeholder,
173
+ }: DataTableFilterDropdownTextProps<TData>) {
174
+ const t = useTranslate();
175
+
176
+ return (
177
+ <DataTableFilterInput
178
+ column={column}
179
+ table={table}
180
+ operators={operators}
181
+ defaultOperator={defaultOperator}
182
+ renderInput={({ value, onChange }) => (
183
+ <Input
184
+ type="text"
185
+ placeholder={
186
+ placeholder ?? t("table.filter.text.placeholder", "Filter by...")
187
+ }
188
+ value={value}
189
+ onChange={(event) => {
190
+ onChange(event.target.value);
191
+ }}
192
+ />
193
+ )}
194
+ />
195
+ );
196
+ }
197
+
198
+ export type DataTableFilterDropdownNumericProps<TData> = {
199
+ column: Column<TData>;
200
+ table: ReactTable<TData>;
201
+ defaultOperator?: CrudOperators;
202
+ operators?: CrudOperators[];
203
+ placeholder?: string;
204
+ };
205
+
206
+ export function DataTableFilterDropdownNumeric<TData>({
207
+ column,
208
+ table,
209
+ operators = ["eq", "ne", "gt", "lt", "gte", "lte"],
210
+ defaultOperator = "eq",
211
+ placeholder,
212
+ }: DataTableFilterDropdownNumericProps<TData>) {
213
+ const t = useTranslate();
214
+
215
+ return (
216
+ <DataTableFilterInput
217
+ column={column}
218
+ table={table}
219
+ operators={operators}
220
+ defaultOperator={defaultOperator}
221
+ renderInput={({ value, onChange }) => (
222
+ <Input
223
+ type="number"
224
+ placeholder={
225
+ placeholder ?? t("table.filter.numeric.placeholder", "Filter by...")
226
+ }
227
+ value={value}
228
+ onChange={(event) => {
229
+ onChange(event.target.value);
230
+ }}
231
+ />
232
+ )}
233
+ />
234
+ );
235
+ }
236
+
237
+ export type DataTableFilterComboboxProps<TData> = {
238
+ column: Column<TData>;
239
+ table?: ReactTable<TData>;
240
+ options: { label: string; value: string }[];
241
+ defaultOperator?: CrudOperators;
242
+ operators?: CrudOperators[];
243
+ placeholder?: string;
244
+ noResultsText?: string;
245
+ multiple?: boolean;
246
+ };
247
+
248
+ export function DataTableFilterCombobox<TData>({
249
+ column,
250
+ table,
251
+ options,
252
+ defaultOperator = "eq",
253
+ operators = ["eq", "ne", "in", "nin"],
254
+ placeholder,
255
+ noResultsText,
256
+ multiple = false,
257
+ }: DataTableFilterComboboxProps<TData>) {
258
+ const t = useTranslate();
259
+ const [isOpen, setIsOpen] = useState(false);
260
+
261
+ return (
262
+ <DataTableFilterInput
263
+ column={column}
264
+ table={table}
265
+ operators={operators}
266
+ defaultOperator={defaultOperator}
267
+ renderInput={({ value, onChange }) => {
268
+ const currentValues = multiple
269
+ ? Array.isArray(value)
270
+ ? value
271
+ : value && typeof value === "string"
272
+ ? [value]
273
+ : []
274
+ : value && typeof value === "string"
275
+ ? [value]
276
+ : [];
277
+
278
+ const handleSelect = (optionValue: string) => {
279
+ if (multiple) {
280
+ const newValues = currentValues.includes(optionValue)
281
+ ? currentValues.filter((v) => v !== optionValue)
282
+ : [...currentValues, optionValue];
283
+ onChange(newValues);
284
+ } else {
285
+ onChange(optionValue);
286
+ setIsOpen(false);
287
+ }
288
+ };
289
+
290
+ const handleRemove = (optionValue: string) => {
291
+ if (multiple) {
292
+ const newValues = currentValues.filter((v) => v !== optionValue);
293
+ onChange(newValues);
294
+ }
295
+ };
296
+
297
+ const getDisplayText = () => {
298
+ if (currentValues.length === 0) {
299
+ return (
300
+ placeholder ?? t("table.filter.combobox.placeholder", "Select...")
301
+ );
302
+ }
303
+
304
+ if (multiple) {
305
+ return `${currentValues.length} selected`;
306
+ }
307
+
308
+ const selectedOption = options.find(
309
+ (option) => option.value === currentValues[0]
310
+ );
311
+ return selectedOption ? selectedOption.label : currentValues[0];
312
+ };
313
+
314
+ const getSelectedLabels = () => {
315
+ return currentValues.map((val) => {
316
+ const option = options.find((opt) => opt.value === val);
317
+ return { label: option ? option.label : val, value: val };
318
+ });
319
+ };
320
+
321
+ return (
322
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
323
+ <PopoverTrigger
324
+ render={
325
+ <Button
326
+ variant="outline"
327
+ role="combobox"
328
+ aria-expanded={isOpen}
329
+ className={cn(
330
+ "w-full",
331
+ "min-w-48",
332
+ "max-w-80",
333
+ "justify-start",
334
+ "h-auto",
335
+ "min-h-9"
336
+ )}
337
+ />
338
+ }
339
+ >
340
+ <div className={cn("flex", "gap-2", "w-full")}>
341
+ {multiple && currentValues.length > 0 ? (
342
+ <div className={cn("flex", "flex-wrap", "gap-1", "flex-1")}>
343
+ {getSelectedLabels()
344
+ .slice(0, 3)
345
+ .map(({ label, value: val }) => (
346
+ <Badge
347
+ key={val}
348
+ variant="outline"
349
+ className={cn(
350
+ "inline-flex",
351
+ "items-center",
352
+ "gap-0",
353
+ "h-4",
354
+ "pr-0.5",
355
+ "rounded-sm"
356
+ )}
357
+ >
358
+ <span className={cn("text-[10px]", "leading-4")}>
359
+ {label}
360
+ </span>
361
+ <span
362
+ className={cn(
363
+ "inline-flex",
364
+ "items-center",
365
+ "justify-center",
366
+ "p-0",
367
+ "w-4",
368
+ "h-full",
369
+ "text-muted-foreground",
370
+ "hover:text-destructive",
371
+ "rounded-sm",
372
+ "cursor-pointer",
373
+ "transition-colors"
374
+ )}
375
+ onClick={(e) => {
376
+ e.preventDefault();
377
+ e.stopPropagation();
378
+ handleRemove(val);
379
+ }}
380
+ >
381
+ <X className={cn("!h-2", "!w-2")} />
382
+ </span>
383
+ </Badge>
384
+ ))}
385
+ {currentValues.length > 3 && (
386
+ <span
387
+ className={cn(
388
+ "text-xs",
389
+ "text-muted-foreground",
390
+ "px-1"
391
+ )}
392
+ >
393
+ +{currentValues.length - 3} more
394
+ </span>
395
+ )}
396
+ </div>
397
+ ) : (
398
+ <span
399
+ className={cn(
400
+ "truncate",
401
+ "flex-1",
402
+ "text-start",
403
+ "text-xs",
404
+ currentValues.length === 0 && "text-muted-foreground"
405
+ )}
406
+ >
407
+ {getDisplayText()}
408
+ </span>
409
+ )}
410
+
411
+ <ChevronsUpDown
412
+ className={cn("h-4", "w-4", "shrink-0", "opacity-50")}
413
+ />
414
+ </div>
415
+ </PopoverTrigger>
416
+ <PopoverContent className={cn("w-[200px]", "p-0")} align="start">
417
+ <Command items={options}>
418
+ <CommandInput
419
+ placeholder={t("table.filter.combobox.search", "Search...")}
420
+ />
421
+ <CommandEmpty>
422
+ {noResultsText ??
423
+ t(
424
+ "table.filter.combobox.noResults",
425
+ "Results not found."
426
+ )}
427
+ </CommandEmpty>
428
+ <CommandList>
429
+ {(option: { label: string; value: string }) => (
430
+ <CommandItem
431
+ key={option.value}
432
+ value={option}
433
+ onClick={() => handleSelect(option.value)}
434
+ >
435
+ {option.label}
436
+ <Check
437
+ className={cn(
438
+ "ml-auto",
439
+ "h-4",
440
+ "w-4",
441
+ currentValues.includes(option.value)
442
+ ? "opacity-100"
443
+ : "opacity-0"
444
+ )}
445
+ />
446
+ </CommandItem>
447
+ )}
448
+ </CommandList>
449
+ </Command>
450
+ </PopoverContent>
451
+ </Popover>
452
+ );
453
+ }}
454
+ />
455
+ );
456
+ }
457
+
458
+ export type DataTableFilterDropdownDateSinglePickerProps<TData> = {
459
+ column: Column<TData>;
460
+ defaultOperator?: CrudOperators;
461
+ formatDate?: (date: Date | undefined) => string | undefined;
462
+ };
463
+
464
+ export function DataTableFilterDropdownDateSinglePicker<TData>({
465
+ column,
466
+ defaultOperator = "eq",
467
+ formatDate,
468
+ }: DataTableFilterDropdownDateSinglePickerProps<TData>) {
469
+ const columnFilterValue = column.getFilterValue() as string;
470
+
471
+ const parseDate = (value: string | undefined): Date | undefined => {
472
+ if (!value) return undefined;
473
+
474
+ const date = new Date(value);
475
+
476
+ if (Number.isNaN(date.getTime())) return undefined;
477
+ return date;
478
+ };
479
+
480
+ const [filterValue, setFilterValue] = useState<Date | undefined>(() =>
481
+ parseDate(columnFilterValue)
482
+ );
483
+
484
+ useEffect(() => {
485
+ column.columnDef.meta = {
486
+ ...column.columnDef.meta,
487
+ filterOperator: defaultOperator,
488
+ };
489
+ }, [defaultOperator, column]);
490
+
491
+ useEffect(() => {
492
+ setFilterValue(parseDate(columnFilterValue));
493
+ }, [columnFilterValue]);
494
+
495
+ const hasDate = !!filterValue;
496
+
497
+ const handleApply = () => {
498
+ if (!filterValue) return;
499
+
500
+ const value = formatDate?.(filterValue) ?? filterValue.toISOString();
501
+ column.setFilterValue(value);
502
+ };
503
+
504
+ return (
505
+ <DataTableFilterDropdown
506
+ column={column}
507
+ contentClassName={cn("w-fit", "p-0")}
508
+ >
509
+ {({ setIsOpen }) => {
510
+ return (
511
+ <div
512
+ className={cn("flex", "flex-col", "items-center")}
513
+ onKeyDown={(event) => {
514
+ if (!hasDate) return;
515
+ if (event.key === "Enter") {
516
+ handleApply();
517
+ setIsOpen(false);
518
+ }
519
+ }}
520
+ >
521
+ <Calendar
522
+ mode="single"
523
+ selected={filterValue}
524
+ onSelect={(date) => {
525
+ setFilterValue(date);
526
+ }}
527
+ />
528
+
529
+ <div className={cn("w-full")}>
530
+ <Separator />
531
+ </div>
532
+
533
+ <DataTableFilterDropdownActions
534
+ className={cn("p-4")}
535
+ isApplyDisabled={!hasDate}
536
+ onClear={() => {
537
+ column.setFilterValue(undefined);
538
+ setFilterValue(undefined);
539
+ setIsOpen(false);
540
+ }}
541
+ onApply={() => {
542
+ handleApply();
543
+ setIsOpen(false);
544
+ }}
545
+ />
546
+ </div>
547
+ );
548
+ }}
549
+ </DataTableFilterDropdown>
550
+ );
551
+ }
552
+
553
+ export type DataTableFilterDropdownDateRangePickerProps<TData> = {
554
+ column: Column<TData>;
555
+ defaultOperator?: CrudOperators;
556
+ formatDateRange?: (dateRange: DateRange | undefined) => string[] | undefined;
557
+ };
558
+
559
+ export function DataTableFilterDropdownDateRangePicker<TData>({
560
+ column,
561
+ defaultOperator = "between",
562
+ formatDateRange,
563
+ }: DataTableFilterDropdownDateRangePickerProps<TData>) {
564
+ const columnFilterValue = column.getFilterValue() as string[];
565
+
566
+ const parseDateRange = (
567
+ value: string[] | undefined
568
+ ): DateRange | undefined => {
569
+ if (!value || !Array.isArray(value) || value.length !== 2) return undefined;
570
+
571
+ const from = value[0] ? new Date(value[0]) : undefined;
572
+ const to = value[1] ? new Date(value[1]) : undefined;
573
+
574
+ if (
575
+ !from ||
576
+ !to ||
577
+ Number.isNaN(from.getTime()) ||
578
+ Number.isNaN(to.getTime())
579
+ )
580
+ return undefined;
581
+ return { from, to };
582
+ };
583
+
584
+ const [filterValue, setFilterValue] = useState<DateRange | undefined>(() =>
585
+ parseDateRange(columnFilterValue)
586
+ );
587
+
588
+ useEffect(() => {
589
+ column.columnDef.meta = {
590
+ ...column.columnDef.meta,
591
+ filterOperator: defaultOperator,
592
+ };
593
+ }, [defaultOperator, column]);
594
+
595
+ useEffect(() => {
596
+ setFilterValue(parseDateRange(columnFilterValue));
597
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- objects are always different
598
+ }, [JSON.stringify(columnFilterValue)]);
599
+
600
+ const hasDateRange = filterValue?.from && filterValue?.to;
601
+
602
+ const handleApply = () => {
603
+ if (!filterValue?.from || !filterValue?.to) return;
604
+
605
+ const values = formatDateRange?.(filterValue) ?? [
606
+ filterValue.from.toISOString(),
607
+ filterValue.to.toISOString(),
608
+ ];
609
+ column.setFilterValue(values);
610
+ };
611
+
612
+ return (
613
+ <DataTableFilterDropdown
614
+ column={column}
615
+ contentClassName={cn("w-fit", "p-0")}
616
+ >
617
+ {({ setIsOpen }) => {
618
+ return (
619
+ <div
620
+ className={cn("flex", "flex-col", "items-center")}
621
+ onKeyDown={(event) => {
622
+ if (!hasDateRange) return;
623
+ if (event.key === "Enter") {
624
+ handleApply();
625
+ setIsOpen(false);
626
+ }
627
+ }}
628
+ >
629
+ <Calendar
630
+ mode="range"
631
+ numberOfMonths={2}
632
+ selected={filterValue}
633
+ onSelect={(date) => {
634
+ setFilterValue({
635
+ from: date?.from,
636
+ to: date?.to,
637
+ });
638
+ }}
639
+ />
640
+
641
+ <div className={cn("w-full")}>
642
+ <Separator />
643
+ </div>
644
+
645
+ <DataTableFilterDropdownActions
646
+ className={cn("p-4")}
647
+ isApplyDisabled={!hasDateRange}
648
+ onClear={() => {
649
+ column.setFilterValue(undefined);
650
+ setFilterValue(undefined);
651
+ setIsOpen(false);
652
+ }}
653
+ onApply={() => {
654
+ handleApply();
655
+ setIsOpen(false);
656
+ }}
657
+ />
658
+ </div>
659
+ );
660
+ }}
661
+ </DataTableFilterDropdown>
662
+ );
663
+ }
664
+
665
+ export type DataTableFilterInputProps<TData> = {
666
+ column: Column<TData>;
667
+ table?: ReactTable<TData>;
668
+ defaultOperator?: CrudOperators;
669
+ operators?: CrudOperators[];
670
+ renderInput: (props: {
671
+ value: string | string[];
672
+ onChange: (value: string | string[]) => void;
673
+ }) => React.ReactNode;
674
+ };
675
+
676
+ export function DataTableFilterInput<TData>({
677
+ column: columnFromProps,
678
+ table: tableFromProps,
679
+ operators: operatorsFromProps,
680
+ defaultOperator: defaultOperatorFromProps,
681
+ renderInput,
682
+ }: DataTableFilterInputProps<TData>) {
683
+ const [filterValue, setFilterValue] = useState(
684
+ (columnFromProps.getFilterValue() as string | string[]) || ""
685
+ );
686
+
687
+ const [operator, setOperator] = useState<CrudOperators>(() => {
688
+ if (!tableFromProps) {
689
+ return defaultOperatorFromProps || "eq";
690
+ }
691
+
692
+ const columnFilter = tableFromProps
693
+ .getState()
694
+ .columnFilters.find((filter) => {
695
+ return filter.id === columnFromProps.id;
696
+ });
697
+
698
+ if (columnFilter && "operator" in columnFilter) {
699
+ return columnFilter.operator as CrudOperators;
700
+ }
701
+
702
+ return defaultOperatorFromProps || "eq";
703
+ });
704
+
705
+ const handleApply = () => {
706
+ columnFromProps.setFilterValue(filterValue);
707
+ };
708
+
709
+ const handleClear = () => {
710
+ columnFromProps.setFilterValue(undefined);
711
+ setFilterValue("");
712
+ };
713
+
714
+ const handleOperatorChange = (value: CrudOperators) => {
715
+ setOperator(value);
716
+ columnFromProps.columnDef.meta = {
717
+ ...columnFromProps.columnDef.meta,
718
+ filterOperator: value,
719
+ };
720
+ };
721
+
722
+ return (
723
+ <DataTableFilterDropdown column={columnFromProps}>
724
+ {({ setIsOpen }) => {
725
+ return (
726
+ <div
727
+ className={cn(
728
+ "flex",
729
+ "flex-col",
730
+ "items-center",
731
+ "gap-4",
732
+ "w-full"
733
+ )}
734
+ onKeyDown={(event) => {
735
+ if (event.key === "Enter") {
736
+ handleApply();
737
+ setIsOpen(false);
738
+ }
739
+ }}
740
+ >
741
+ <div
742
+ className={cn("grid", "grid-cols-1", "md:grid-cols-2", "gap-2")}
743
+ >
744
+ {operatorsFromProps && operatorsFromProps.length > 1 && (
745
+ <DataTableFilterOperatorSelect
746
+ value={operator}
747
+ operators={operatorsFromProps}
748
+ onValueChange={handleOperatorChange}
749
+ />
750
+ )}
751
+ {renderInput({
752
+ value: filterValue,
753
+ onChange: setFilterValue,
754
+ })}
755
+ </div>
756
+ <div className={cn("w-full")}>
757
+ <Separator />
758
+ </div>
759
+ <DataTableFilterDropdownActions
760
+ onClear={() => {
761
+ handleClear();
762
+ setIsOpen(false);
763
+ }}
764
+ onApply={() => {
765
+ handleApply();
766
+ setIsOpen(false);
767
+ }}
768
+ />
769
+ </div>
770
+ );
771
+ }}
772
+ </DataTableFilterDropdown>
773
+ );
774
+ }
775
+
776
+ const CRUD_OPERATOR_LABELS: Record<
777
+ Exclude<CrudOperators, "or" | "and">,
778
+ { i18nKey: string; defaultLabel: string }
779
+ > = {
780
+ eq: { i18nKey: "table.filter.operator.eq", defaultLabel: "Equals" },
781
+ ne: { i18nKey: "table.filter.operator.ne", defaultLabel: "Not equals" },
782
+ lt: { i18nKey: "table.filter.operator.lt", defaultLabel: "Less than" },
783
+ gt: { i18nKey: "table.filter.operator.gt", defaultLabel: "Greater than" },
784
+ lte: {
785
+ i18nKey: "table.filter.operator.lte",
786
+ defaultLabel: "Less than or equal",
787
+ },
788
+ gte: {
789
+ i18nKey: "table.filter.operator.gte",
790
+ defaultLabel: "Greater than or equal",
791
+ },
792
+ in: {
793
+ i18nKey: "table.filter.operator.in",
794
+ defaultLabel: "Includes in an array",
795
+ },
796
+ nin: {
797
+ i18nKey: "table.filter.operator.nin",
798
+ defaultLabel: "Not includes in an array",
799
+ },
800
+ ina: {
801
+ i18nKey: "table.filter.operator.ina",
802
+ defaultLabel: "Includes in an array (case sensitive)",
803
+ },
804
+ nina: {
805
+ i18nKey: "table.filter.operator.nina",
806
+ defaultLabel: "Not includes in an array (case sensitive)",
807
+ },
808
+ contains: {
809
+ i18nKey: "table.filter.operator.contains",
810
+ defaultLabel: "Contains",
811
+ },
812
+ ncontains: {
813
+ i18nKey: "table.filter.operator.ncontains",
814
+ defaultLabel: "Not contains",
815
+ },
816
+ containss: {
817
+ i18nKey: "table.filter.operator.containss",
818
+ defaultLabel: "Contains (case sensitive)",
819
+ },
820
+ ncontainss: {
821
+ i18nKey: "table.filter.operator.ncontainss",
822
+ defaultLabel: "Not contains (case sensitive)",
823
+ },
824
+ between: {
825
+ i18nKey: "table.filter.operator.between",
826
+ defaultLabel: "Between",
827
+ },
828
+ nbetween: {
829
+ i18nKey: "table.filter.operator.nbetween",
830
+ defaultLabel: "Not between",
831
+ },
832
+ null: { i18nKey: "table.filter.operator.null", defaultLabel: "Is null" },
833
+ nnull: {
834
+ i18nKey: "table.filter.operator.nnull",
835
+ defaultLabel: "Is not null",
836
+ },
837
+ startswith: {
838
+ i18nKey: "table.filter.operator.startswith",
839
+ defaultLabel: "Starts with",
840
+ },
841
+ nstartswith: {
842
+ i18nKey: "table.filter.operator.nstartswith",
843
+ defaultLabel: "Not starts with",
844
+ },
845
+ startswiths: {
846
+ i18nKey: "table.filter.operator.startswiths",
847
+ defaultLabel: "Starts with (case sensitive)",
848
+ },
849
+ nstartswiths: {
850
+ i18nKey: "table.filter.operator.nstartswiths",
851
+ defaultLabel: "Not starts with (case sensitive)",
852
+ },
853
+ endswith: {
854
+ i18nKey: "table.filter.operator.endswith",
855
+ defaultLabel: "Ends with",
856
+ },
857
+ nendswith: {
858
+ i18nKey: "table.filter.operator.nendswith",
859
+ defaultLabel: "Not ends with",
860
+ },
861
+ endswiths: {
862
+ i18nKey: "table.filter.operator.endswiths",
863
+ defaultLabel: "Ends with (case sensitive)",
864
+ },
865
+ nendswiths: {
866
+ i18nKey: "table.filter.operator.nendswiths",
867
+ defaultLabel: "Not ends with (case sensitive)",
868
+ },
869
+ eqs: {
870
+ i18nKey: "table.filter.operator.eqs",
871
+ defaultLabel: "Equals (case sensitive)",
872
+ },
873
+ nes: {
874
+ i18nKey: "table.filter.operator.nes",
875
+ defaultLabel: "Not equals (case sensitive)",
876
+ },
877
+ };
878
+
879
+ export type DataTableFilterOperatorSelectProps = {
880
+ value: CrudOperators;
881
+ onValueChange: (value: CrudOperators) => void;
882
+ operators?: CrudOperators[];
883
+ placeholder?: string;
884
+ triggerClassName?: string;
885
+ contentClassName?: string;
886
+ };
887
+
888
+ export function DataTableFilterOperatorSelect({
889
+ value,
890
+ onValueChange,
891
+ operators: operatorsFromProps,
892
+ placeholder,
893
+ triggerClassName,
894
+ contentClassName,
895
+ }: DataTableFilterOperatorSelectProps) {
896
+ const t = useTranslate();
897
+
898
+ const [open, setOpen] = useState(false);
899
+
900
+ const operators = useMemo(() => {
901
+ return Object.entries(CRUD_OPERATOR_LABELS).filter(([operator]) =>
902
+ operatorsFromProps?.includes(operator as CrudOperators)
903
+ );
904
+ }, [operatorsFromProps]);
905
+
906
+ const operatorOptions = useMemo(
907
+ () =>
908
+ operators.map(([operator, { i18nKey, defaultLabel }]) => ({
909
+ value: operator as CrudOperators,
910
+ label: t(i18nKey, defaultLabel),
911
+ })),
912
+ [operators, t]
913
+ );
914
+
915
+ const selectedLabel = t(
916
+ CRUD_OPERATOR_LABELS[value as Exclude<CrudOperators, "or" | "and">].i18nKey,
917
+ CRUD_OPERATOR_LABELS[value as Exclude<CrudOperators, "or" | "and">]
918
+ .defaultLabel
919
+ );
920
+ const placeholderText =
921
+ placeholder ?? t("table.filter.operator.placeholder", "Search operator...");
922
+ const noResultsText = t(
923
+ "table.filter.operator.noResults",
924
+ "No operator found."
925
+ );
926
+
927
+ return (
928
+ <Popover open={open} onOpenChange={setOpen}>
929
+ <PopoverTrigger
930
+ render={
931
+ <Button
932
+ variant="outline"
933
+ role="combobox"
934
+ aria-expanded={open}
935
+ className={cn(
936
+ "w-full",
937
+ "justify-between",
938
+ "truncate",
939
+ triggerClassName
940
+ )}
941
+ />
942
+ }
943
+ >
944
+ <div className={cn("truncate")}>
945
+ {selectedLabel ?? placeholderText}
946
+ </div>
947
+ <ChevronsUpDown
948
+ className={cn("ml-2", "h-4", "w-4", "shrink-0", "opacity-50")}
949
+ />
950
+ </PopoverTrigger>
951
+ <PopoverContent className={cn("p-0", contentClassName)} keepMounted>
952
+ <Command items={operatorOptions}>
953
+ <CommandInput placeholder={placeholderText} />
954
+ <CommandEmpty>{noResultsText}</CommandEmpty>
955
+ <CommandList>
956
+ {(option: { value: CrudOperators; label: string }) => (
957
+ <CommandItem
958
+ key={option.value}
959
+ value={option}
960
+ onClick={() => {
961
+ onValueChange(option.value);
962
+ setOpen(false);
963
+ }}
964
+ >
965
+ <Check
966
+ className={cn(
967
+ "mr-2",
968
+ "h-4",
969
+ "w-4",
970
+ value === option.value ? "opacity-100" : "opacity-0"
971
+ )}
972
+ />
973
+ {option.label}
974
+ </CommandItem>
975
+ )}
976
+ </CommandList>
977
+ </Command>
978
+ </PopoverContent>
979
+ </Popover>
980
+ );
981
+ }
982
+
983
+ DataTableFilterDropdown.displayName = "DataTableFilterDropdown";
984
+ DataTableFilterDropdownText.displayName = "DataTableFilterDropdownText";
985
+ DataTableFilterCombobox.displayName = "DataTableFilterCombobox";
986
+ DataTableFilterDropdownDateRangePicker.displayName =
987
+ "DataTableFilterDropdownDateRangePicker";
988
+ DataTableFilterOperatorSelect.displayName = "DataTableFilterOperatorSelect";
989
+ DataTableFilterDropdownActions.displayName = "DataTableFilterDropdownActions";
990
+ DataTableFilterDropdownNumeric.displayName = "DataTableFilterDropdownNumeric";
991
+ DataTableFilterInput.displayName = "DataTableFilterInput";
992
+ DataTableFilterOperatorSelect.displayName = "DataTableFilterOperatorSelect";
993
+ DataTableFilterDropdownDateSinglePicker.displayName =
994
+ "DataTableFilterDropdownDateSinglePicker";