@moontra/moonui-pro 2.3.8 → 2.4.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.
@@ -33,18 +33,34 @@ import {
33
33
  Settings,
34
34
  Lock,
35
35
  Sparkles,
36
- Loader2
36
+ Loader2,
37
+ FileDown,
38
+ FileJson,
39
+ FileSpreadsheet
37
40
  } from 'lucide-react'
38
41
  import { cn } from '../../lib/utils'
39
42
  import { useSubscription } from '../../hooks/use-subscription'
40
43
  import { motion, AnimatePresence } from 'framer-motion'
44
+ import { DataTableColumnToggle } from './data-table-column-toggle'
45
+ import { DataTableBulkActions, type BulkAction } from './data-table-bulk-actions'
46
+ import { exportData, type ExportFormat, getVisibleColumns } from './data-table-export'
47
+ import {
48
+ DropdownMenu,
49
+ DropdownMenuContent,
50
+ DropdownMenuItem,
51
+ DropdownMenuTrigger,
52
+ } from '../ui/dropdown-menu'
41
53
 
42
54
  interface DataTableProps<TData, TValue> {
43
55
  columns: ColumnDef<TData, TValue>[]
44
56
  data: TData[]
45
57
  searchable?: boolean
46
58
  filterable?: boolean
47
- exportable?: boolean
59
+ exportable?: boolean | {
60
+ formats?: ExportFormat[]
61
+ filename?: string
62
+ onExport?: (data: TData[], format: ExportFormat) => void
63
+ }
48
64
  selectable?: boolean
49
65
  pagination?: boolean
50
66
  pageSize?: number
@@ -55,6 +71,7 @@ interface DataTableProps<TData, TValue> {
55
71
  renderSubComponent?: (props: { row: { original: TData; id: string } }) => React.ReactNode
56
72
  expandedRows?: Set<string>
57
73
  onRowExpandChange?: (expandedRows: Set<string>) => void
74
+ bulkActions?: BulkAction<TData>[]
58
75
  // Additional props for compatibility
59
76
  enableSorting?: boolean
60
77
  enableFiltering?: boolean
@@ -116,6 +133,7 @@ export function DataTable<TData, TValue>({
116
133
  renderSubComponent,
117
134
  expandedRows: controlledExpandedRows,
118
135
  onRowExpandChange,
136
+ bulkActions = [],
119
137
  features = {},
120
138
  theme = {},
121
139
  texts = {},
@@ -213,6 +231,11 @@ export function DataTable<TData, TValue>({
213
231
  pageSize: actualPageSize,
214
232
  },
215
233
  },
234
+ // Prevent re-renders on state changes
235
+ autoResetAll: false,
236
+ autoResetPageIndex: false,
237
+ autoResetExpanded: false,
238
+ getRowId: (row: TData) => (row as any).id || (row as any).orderId || Math.random().toString(),
216
239
  })
217
240
 
218
241
  React.useEffect(() => {
@@ -224,11 +247,19 @@ export function DataTable<TData, TValue>({
224
247
 
225
248
  // Memoize row model to prevent unnecessary re-renders when only expanded state changes
226
249
  const tableState = table.getState()
227
- const rows = React.useMemo(
228
- () => table.getRowModel().rows,
229
- // Only recalculate when data or filtering/sorting changes, not on expand/collapse
230
- [stableData, tableState.sorting, tableState.columnFilters, tableState.globalFilter, tableState.pagination.pageIndex, tableState.pagination.pageSize]
231
- )
250
+ const rowModel = table.getRowModel()
251
+
252
+ // Use a ref to track if rows actually changed
253
+ const rowsRef = React.useRef(rowModel.rows)
254
+ const rowsChanged = React.useMemo(() => {
255
+ const changed = rowsRef.current !== rowModel.rows
256
+ if (changed) {
257
+ rowsRef.current = rowModel.rows
258
+ }
259
+ return changed
260
+ }, [rowModel.rows])
261
+
262
+ const rows = rowsRef.current
232
263
 
233
264
  // Merge features with defaults
234
265
  const enabledFeatures = {
@@ -241,14 +272,51 @@ export function DataTable<TData, TValue>({
241
272
  export: features.export !== false || exportable,
242
273
  }
243
274
 
244
- const handleExport = () => {
275
+ const handleExport = async (format: ExportFormat) => {
276
+ const selectedRows = table.getFilteredSelectedRowModel().rows
277
+ const dataToExport = selectedRows.length > 0
278
+ ? selectedRows.map(row => row.original)
279
+ : table.getFilteredRowModel().rows.map(row => row.original)
280
+
281
+ // Use custom export handler if provided
282
+ if (typeof exportable === 'object' && exportable.onExport) {
283
+ exportable.onExport(dataToExport, format)
284
+ return
285
+ }
286
+
287
+ // Use legacy onExport if provided
245
288
  if (onExport) {
246
- const selectedRows = table.getFilteredSelectedRowModel().rows
247
- const dataToExport = selectedRows.length > 0
248
- ? selectedRows.map(row => row.original)
249
- : table.getFilteredRowModel().rows.map(row => row.original)
250
289
  onExport(dataToExport)
290
+ return
251
291
  }
292
+
293
+ // Default export behavior
294
+ const filename = typeof exportable === 'object' && exportable.filename
295
+ ? exportable.filename
296
+ : 'data-export'
297
+
298
+ const visibleColumns = getVisibleColumns(columns as any, columnVisibility)
299
+
300
+ await exportData(dataToExport as Record<string, any>[], {
301
+ format,
302
+ filename,
303
+ columns: visibleColumns,
304
+ includeHeaders: true
305
+ })
306
+ }
307
+
308
+ // Parse export options
309
+ const exportFormats: ExportFormat[] = React.useMemo(() => {
310
+ if (!exportable) return []
311
+ if (exportable === true) return ['csv', 'json']
312
+ if (typeof exportable === 'object' && exportable.formats) {
313
+ return exportable.formats
314
+ }
315
+ return ['csv', 'json']
316
+ }, [exportable])
317
+
318
+ const clearRowSelection = () => {
319
+ table.resetRowSelection()
252
320
  }
253
321
 
254
322
  return (
@@ -276,20 +344,52 @@ export function DataTable<TData, TValue>({
276
344
  Filters
277
345
  </Button>
278
346
  )}
347
+
348
+ {/* Bulk actions */}
349
+ {selectable && bulkActions.length > 0 && (
350
+ <DataTableBulkActions
351
+ selectedRows={table.getFilteredSelectedRowModel().rows.map(row => row.original)}
352
+ actions={bulkActions}
353
+ onClearSelection={clearRowSelection}
354
+ />
355
+ )}
279
356
  </div>
280
357
 
281
358
  <div className="flex items-center space-x-2">
282
- {exportable && (
283
- <Button variant="outline" size="sm" onClick={handleExport}>
284
- <span suppressHydrationWarning><Download className="mr-2 h-4 w-4" /></span>
285
- Export
286
- </Button>
359
+ {/* Export dropdown */}
360
+ {exportable && exportFormats.length > 0 && (
361
+ <DropdownMenu>
362
+ <DropdownMenuTrigger asChild>
363
+ <Button variant="outline" size="sm">
364
+ <span suppressHydrationWarning><Download className="mr-2 h-4 w-4" /></span>
365
+ Export
366
+ </Button>
367
+ </DropdownMenuTrigger>
368
+ <DropdownMenuContent align="end">
369
+ {exportFormats.includes('csv') && (
370
+ <DropdownMenuItem onClick={() => handleExport('csv')}>
371
+ <FileSpreadsheet className="mr-2 h-4 w-4" />
372
+ Export as CSV
373
+ </DropdownMenuItem>
374
+ )}
375
+ {exportFormats.includes('json') && (
376
+ <DropdownMenuItem onClick={() => handleExport('json')}>
377
+ <FileJson className="mr-2 h-4 w-4" />
378
+ Export as JSON
379
+ </DropdownMenuItem>
380
+ )}
381
+ {exportFormats.includes('xlsx') && (
382
+ <DropdownMenuItem onClick={() => handleExport('xlsx')}>
383
+ <FileDown className="mr-2 h-4 w-4" />
384
+ Export as Excel
385
+ </DropdownMenuItem>
386
+ )}
387
+ </DropdownMenuContent>
388
+ </DropdownMenu>
287
389
  )}
288
390
 
289
- <Button variant="outline" size="sm">
290
- <span suppressHydrationWarning><Settings className="mr-2 h-4 w-4" /></span>
291
- Columns
292
- </Button>
391
+ {/* Column visibility toggle */}
392
+ <DataTableColumnToggle table={table} />
293
393
  </div>
294
394
  </div>
295
395
 
@@ -355,71 +455,14 @@ export function DataTable<TData, TValue>({
355
455
  const isExpanded = enableExpandable && expandedRows.has(rowId)
356
456
 
357
457
  return (
358
- <React.Fragment key={rowId}>
359
- <tr
360
- className={cn(
361
- "border-b transition-colors hover:bg-muted/50",
362
- row.getIsSelected() && "bg-muted",
363
- isExpanded && "border-b-0"
364
- )}
365
- >
366
- {row.getVisibleCells().map((cell) => (
367
- <td key={cell.id} className="moonui-data-table-td p-4 align-middle">
368
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
369
- </td>
370
- ))}
371
- </tr>
372
-
373
- <AnimatePresence initial={false}>
374
- {isExpanded && renderSubComponent && (
375
- <motion.tr
376
- key={`${row.id}-expanded`}
377
- initial={{ height: 0, opacity: 0 }}
378
- animate={{
379
- height: "auto",
380
- opacity: 1,
381
- transition: {
382
- height: {
383
- duration: 0.3,
384
- ease: "easeOut"
385
- },
386
- opacity: {
387
- duration: 0.2,
388
- delay: 0.1
389
- }
390
- }
391
- }}
392
- exit={{
393
- height: 0,
394
- opacity: 0,
395
- transition: {
396
- height: {
397
- duration: 0.3,
398
- ease: "easeIn"
399
- },
400
- opacity: {
401
- duration: 0.2
402
- }
403
- }
404
- }}
405
- style={{ overflow: "hidden" }}
406
- className="border-b"
407
- >
408
- <td colSpan={columns.length} className="p-0">
409
- <motion.div
410
- initial={{ y: -10 }}
411
- animate={{ y: 0 }}
412
- exit={{ y: -10 }}
413
- transition={{ duration: 0.2 }}
414
- className="border-t border-border/50"
415
- >
416
- {renderSubComponent({ row: { original: row.original, id: rowId } })}
417
- </motion.div>
418
- </td>
419
- </motion.tr>
420
- )}
421
- </AnimatePresence>
422
- </React.Fragment>
458
+ <TableRow
459
+ key={rowId}
460
+ row={row}
461
+ columns={columns}
462
+ isExpanded={isExpanded}
463
+ enableExpandable={enableExpandable}
464
+ renderSubComponent={renderSubComponent}
465
+ />
423
466
  );
424
467
  })}
425
468
  </>
@@ -624,5 +667,72 @@ export function useExpandableRows(initialExpanded: Set<string> = new Set()) {
624
667
  };
625
668
  }
626
669
 
670
+ // Memoized table row component
671
+ interface TableRowProps {
672
+ row: Row<any>
673
+ columns: ColumnDef<any, any>[]
674
+ isExpanded: boolean
675
+ enableExpandable: boolean
676
+ renderSubComponent?: (props: { row: { original: any; id: string } }) => React.ReactNode
677
+ }
678
+
679
+ const TableRow = React.memo(({
680
+ row,
681
+ columns,
682
+ isExpanded,
683
+ enableExpandable,
684
+ renderSubComponent
685
+ }: TableRowProps) => {
686
+ const rowId = (row.original as any).id || row.id
687
+
688
+ return (
689
+ <>
690
+ <tr
691
+ className={cn(
692
+ "border-b transition-colors hover:bg-muted/50",
693
+ row.getIsSelected() && "bg-muted",
694
+ isExpanded && "border-b-0"
695
+ )}
696
+ >
697
+ {row.getVisibleCells().map((cell) => (
698
+ <td key={cell.id} className="moonui-data-table-td p-4 align-middle">
699
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
700
+ </td>
701
+ ))}
702
+ </tr>
703
+
704
+ {isExpanded && renderSubComponent && (
705
+ <tr className="border-b">
706
+ <td colSpan={columns.length} className="p-0 overflow-hidden">
707
+ <div
708
+ className="transition-all duration-300 ease-out"
709
+ style={{
710
+ maxHeight: isExpanded ? '1000px' : '0',
711
+ opacity: isExpanded ? 1 : 0,
712
+ }}
713
+ >
714
+ <div className="border-t border-border/50">
715
+ {renderSubComponent({ row: { original: row.original, id: rowId } })}
716
+ </div>
717
+ </div>
718
+ </td>
719
+ </tr>
720
+ )}
721
+ </>
722
+ )
723
+ }, (prevProps, nextProps) => {
724
+ // Custom comparison - only re-render if row data or expanded state changed
725
+ const prevRowId = (prevProps.row.original as any).id || prevProps.row.id
726
+ const nextRowId = (nextProps.row.original as any).id || nextProps.row.id
727
+
728
+ return prevRowId === nextRowId &&
729
+ prevProps.isExpanded === nextProps.isExpanded &&
730
+ prevProps.row.getIsSelected() === nextProps.row.getIsSelected()
731
+ })
732
+
733
+ TableRow.displayName = 'TableRow'
734
+
627
735
  // Re-export types for convenience
628
736
  export { type ColumnDef } from "@tanstack/react-table";
737
+ export type { BulkAction } from './data-table-bulk-actions';
738
+ export type { ExportFormat } from './data-table-export';
@@ -0,0 +1,141 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { buttonVariants } from "./button"
8
+
9
+ const AlertDialog = AlertDialogPrimitive.Root
10
+
11
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12
+
13
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
14
+
15
+ const AlertDialogOverlay = React.forwardRef<
16
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
17
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
18
+ >(({ className, ...props }, ref) => (
19
+ <AlertDialogPrimitive.Overlay
20
+ className={cn(
21
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
+ className
23
+ )}
24
+ {...props}
25
+ ref={ref}
26
+ />
27
+ ))
28
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29
+
30
+ const AlertDialogContent = React.forwardRef<
31
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
33
+ >(({ className, ...props }, ref) => (
34
+ <AlertDialogPortal>
35
+ <AlertDialogOverlay />
36
+ <AlertDialogPrimitive.Content
37
+ ref={ref}
38
+ className={cn(
39
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ </AlertDialogPortal>
45
+ ))
46
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47
+
48
+ const AlertDialogHeader = ({
49
+ className,
50
+ ...props
51
+ }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn(
54
+ "flex flex-col space-y-2 text-center sm:text-left",
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ )
60
+ AlertDialogHeader.displayName = "AlertDialogHeader"
61
+
62
+ const AlertDialogFooter = ({
63
+ className,
64
+ ...props
65
+ }: React.HTMLAttributes<HTMLDivElement>) => (
66
+ <div
67
+ className={cn(
68
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
69
+ className
70
+ )}
71
+ {...props}
72
+ />
73
+ )
74
+ AlertDialogFooter.displayName = "AlertDialogFooter"
75
+
76
+ const AlertDialogTitle = React.forwardRef<
77
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
78
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
79
+ >(({ className, ...props }, ref) => (
80
+ <AlertDialogPrimitive.Title
81
+ ref={ref}
82
+ className={cn("text-lg font-semibold", className)}
83
+ {...props}
84
+ />
85
+ ))
86
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87
+
88
+ const AlertDialogDescription = React.forwardRef<
89
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
90
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
91
+ >(({ className, ...props }, ref) => (
92
+ <AlertDialogPrimitive.Description
93
+ ref={ref}
94
+ className={cn("text-sm text-muted-foreground", className)}
95
+ {...props}
96
+ />
97
+ ))
98
+ AlertDialogDescription.displayName =
99
+ AlertDialogPrimitive.Description.displayName
100
+
101
+ const AlertDialogAction = React.forwardRef<
102
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
103
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
104
+ >(({ className, ...props }, ref) => (
105
+ <AlertDialogPrimitive.Action
106
+ ref={ref}
107
+ className={cn(buttonVariants(), className)}
108
+ {...props}
109
+ />
110
+ ))
111
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112
+
113
+ const AlertDialogCancel = React.forwardRef<
114
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
115
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
116
+ >(({ className, ...props }, ref) => (
117
+ <AlertDialogPrimitive.Cancel
118
+ ref={ref}
119
+ className={cn(
120
+ buttonVariants({ variant: "outline" }),
121
+ "mt-2 sm:mt-0",
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ ))
127
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128
+
129
+ export {
130
+ AlertDialog,
131
+ AlertDialogPortal,
132
+ AlertDialogOverlay,
133
+ AlertDialogTrigger,
134
+ AlertDialogContent,
135
+ AlertDialogHeader,
136
+ AlertDialogFooter,
137
+ AlertDialogTitle,
138
+ AlertDialogDescription,
139
+ AlertDialogAction,
140
+ AlertDialogCancel,
141
+ }