@moontra/moonui-pro 2.20.1 → 2.20.3
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.
- package/dist/index.d.ts +691 -261
- package/dist/index.mjs +7418 -4934
- package/package.json +11 -5
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postbuild.js +27 -0
- package/scripts/postinstall.js +176 -23
- package/src/__tests__/use-intersection-observer.test.tsx +0 -216
- package/src/__tests__/use-local-storage.test.tsx +0 -174
- package/src/__tests__/use-pro-access.test.tsx +0 -183
- package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
- package/src/components/advanced-chart/index.tsx +0 -1242
- package/src/components/advanced-forms/index.tsx +0 -426
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -372
- package/src/components/calendar/index.tsx +0 -1073
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -462
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -222
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -344
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/metric-card.tsx +0 -343
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/data-table.test.tsx +0 -187
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/enhanced/badge.tsx +0 -191
- package/src/components/enhanced/button.tsx +0 -362
- package/src/components/enhanced/card.tsx +0 -266
- package/src/components/enhanced/dialog.tsx +0 -246
- package/src/components/enhanced/index.ts +0 -4
- package/src/components/error-boundary/index.tsx +0 -109
- package/src/components/file-upload/file-upload.test.tsx +0 -243
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -307
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -298
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -76
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -516
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -513
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -530
- package/src/components/index.ts +0 -128
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1684
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -29
- package/src/components/magnetic-button/index.tsx +0 -167
- package/src/components/memory-efficient-data/index.tsx +0 -1016
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -589
- package/src/components/performance-monitor/index.tsx +0 -794
- package/src/components/phone-number-input/index.tsx +0 -338
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
- package/src/components/rich-text-editor/index.tsx +0 -2324
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -220
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -865
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1148
- package/src/components/ui/accordion.tsx +0 -73
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -153
- package/src/components/ui/badge.tsx +0 -228
- package/src/components/ui/breadcrumb.tsx +0 -214
- package/src/components/ui/button.tsx +0 -222
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -214
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -135
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -225
- package/src/components/ui/dialog.tsx +0 -334
- package/src/components/ui/dropdown-menu.tsx +0 -218
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -190
- package/src/components/ui/input.tsx +0 -222
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -612
- package/src/components/ui/pagination.tsx +0 -123
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -374
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -329
- package/src/components/ui/tabs.tsx +0 -198
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -14
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -85
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/moonui.d.ts +0 -22
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- package/src/utils/license-validator.tsx +0 -183
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useState, useMemo } from 'react'
|
|
4
|
-
import { Column, Table } from '@tanstack/react-table'
|
|
5
|
-
import { X, Filter, Trash2, Plus } from 'lucide-react'
|
|
6
|
-
import { Button } from '../ui/button'
|
|
7
|
-
import { Input } from '../ui/input'
|
|
8
|
-
import { Label } from '../ui/label'
|
|
9
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
10
|
-
import { Switch } from '../ui/switch'
|
|
11
|
-
import { Separator } from '../ui/separator'
|
|
12
|
-
import { cn } from '../../lib/utils'
|
|
13
|
-
import { motion, AnimatePresence } from 'framer-motion'
|
|
14
|
-
|
|
15
|
-
export interface FilterCondition {
|
|
16
|
-
column: string
|
|
17
|
-
operator: FilterOperator
|
|
18
|
-
value: any
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type FilterOperator =
|
|
22
|
-
| 'equals'
|
|
23
|
-
| 'notEquals'
|
|
24
|
-
| 'contains'
|
|
25
|
-
| 'notContains'
|
|
26
|
-
| 'startsWith'
|
|
27
|
-
| 'endsWith'
|
|
28
|
-
| 'greaterThan'
|
|
29
|
-
| 'lessThan'
|
|
30
|
-
| 'greaterThanOrEqual'
|
|
31
|
-
| 'lessThanOrEqual'
|
|
32
|
-
| 'between'
|
|
33
|
-
| 'in'
|
|
34
|
-
| 'notIn'
|
|
35
|
-
| 'isNull'
|
|
36
|
-
| 'isNotNull'
|
|
37
|
-
|
|
38
|
-
export interface DataTableFilterDrawerProps<TData> {
|
|
39
|
-
table: Table<TData>
|
|
40
|
-
open: boolean
|
|
41
|
-
onOpenChange: (open: boolean) => void
|
|
42
|
-
position?: 'left' | 'right'
|
|
43
|
-
width?: string
|
|
44
|
-
filters?: FilterCondition[]
|
|
45
|
-
onFiltersChange?: (filters: FilterCondition[]) => void
|
|
46
|
-
customFilters?: React.ReactNode
|
|
47
|
-
matchAll?: boolean
|
|
48
|
-
onMatchAllChange?: (matchAll: boolean) => void
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const operatorLabels: Record<FilterOperator, string> = {
|
|
52
|
-
equals: 'Equals',
|
|
53
|
-
notEquals: 'Not equals',
|
|
54
|
-
contains: 'Contains',
|
|
55
|
-
notContains: 'Not contains',
|
|
56
|
-
startsWith: 'Starts with',
|
|
57
|
-
endsWith: 'Ends with',
|
|
58
|
-
greaterThan: 'Greater than',
|
|
59
|
-
lessThan: 'Less than',
|
|
60
|
-
greaterThanOrEqual: 'Greater than or equal',
|
|
61
|
-
lessThanOrEqual: 'Less than or equal',
|
|
62
|
-
between: 'Between',
|
|
63
|
-
in: 'In',
|
|
64
|
-
notIn: 'Not in',
|
|
65
|
-
isNull: 'Is empty',
|
|
66
|
-
isNotNull: 'Is not empty',
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function getOperatorsForColumnType(columnType?: string): FilterOperator[] {
|
|
70
|
-
switch (columnType) {
|
|
71
|
-
case 'number':
|
|
72
|
-
return ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual', 'between', 'isNull', 'isNotNull']
|
|
73
|
-
case 'date':
|
|
74
|
-
return ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual', 'between', 'isNull', 'isNotNull']
|
|
75
|
-
case 'boolean':
|
|
76
|
-
return ['equals', 'notEquals', 'isNull', 'isNotNull']
|
|
77
|
-
case 'select':
|
|
78
|
-
return ['equals', 'notEquals', 'in', 'notIn', 'isNull', 'isNotNull']
|
|
79
|
-
default: // string
|
|
80
|
-
return ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'isNull', 'isNotNull']
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function DataTableFilterDrawer<TData>({
|
|
85
|
-
table,
|
|
86
|
-
open,
|
|
87
|
-
onOpenChange,
|
|
88
|
-
position = 'right',
|
|
89
|
-
width = '400px',
|
|
90
|
-
filters: externalFilters,
|
|
91
|
-
onFiltersChange,
|
|
92
|
-
customFilters,
|
|
93
|
-
matchAll: externalMatchAll,
|
|
94
|
-
onMatchAllChange,
|
|
95
|
-
}: DataTableFilterDrawerProps<TData>) {
|
|
96
|
-
const [internalFilters, setInternalFilters] = useState<FilterCondition[]>([])
|
|
97
|
-
const [internalMatchAll, setInternalMatchAll] = useState(true)
|
|
98
|
-
|
|
99
|
-
const filters = externalFilters || internalFilters
|
|
100
|
-
const setFilters = onFiltersChange || setInternalFilters
|
|
101
|
-
const matchAll = externalMatchAll !== undefined ? externalMatchAll : internalMatchAll
|
|
102
|
-
const setMatchAll = onMatchAllChange || setInternalMatchAll
|
|
103
|
-
|
|
104
|
-
// Get filterable columns
|
|
105
|
-
const filterableColumns = useMemo(() => {
|
|
106
|
-
return table.getAllColumns().filter(column => {
|
|
107
|
-
// Skip special columns
|
|
108
|
-
if (column.id === 'select' || column.id === 'actions' || column.id === 'expander') {
|
|
109
|
-
return false
|
|
110
|
-
}
|
|
111
|
-
// Only include columns that can be filtered
|
|
112
|
-
return column.getCanFilter()
|
|
113
|
-
})
|
|
114
|
-
}, [table])
|
|
115
|
-
|
|
116
|
-
const addFilter = () => {
|
|
117
|
-
const firstColumn = filterableColumns[0]
|
|
118
|
-
if (!firstColumn) return
|
|
119
|
-
|
|
120
|
-
const newFilter: FilterCondition = {
|
|
121
|
-
column: firstColumn.id,
|
|
122
|
-
operator: 'contains',
|
|
123
|
-
value: '',
|
|
124
|
-
}
|
|
125
|
-
setFilters([...filters, newFilter])
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const updateFilter = (index: number, updates: Partial<FilterCondition>) => {
|
|
129
|
-
const newFilters = [...filters]
|
|
130
|
-
newFilters[index] = { ...newFilters[index], ...updates }
|
|
131
|
-
setFilters(newFilters)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const removeFilter = (index: number) => {
|
|
135
|
-
setFilters(filters.filter((_, i) => i !== index))
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const clearAllFilters = () => {
|
|
139
|
-
setFilters([])
|
|
140
|
-
table.resetColumnFilters()
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const applyFilters = () => {
|
|
144
|
-
// Create a custom filter function that handles all our conditions
|
|
145
|
-
const customFilterFn = (row: any, columnId: string, filterValue: any) => {
|
|
146
|
-
// Find the filter condition for this column
|
|
147
|
-
const filterCondition = filters.find(f => f.column === columnId)
|
|
148
|
-
if (!filterCondition) return true
|
|
149
|
-
|
|
150
|
-
const cellValue = row.getValue(columnId)
|
|
151
|
-
const filterVal = filterCondition.value
|
|
152
|
-
|
|
153
|
-
switch (filterCondition.operator) {
|
|
154
|
-
case 'equals':
|
|
155
|
-
return cellValue === filterVal
|
|
156
|
-
case 'notEquals':
|
|
157
|
-
return cellValue !== filterVal
|
|
158
|
-
case 'contains':
|
|
159
|
-
return String(cellValue).toLowerCase().includes(String(filterVal).toLowerCase())
|
|
160
|
-
case 'notContains':
|
|
161
|
-
return !String(cellValue).toLowerCase().includes(String(filterVal).toLowerCase())
|
|
162
|
-
case 'startsWith':
|
|
163
|
-
return String(cellValue).toLowerCase().startsWith(String(filterVal).toLowerCase())
|
|
164
|
-
case 'endsWith':
|
|
165
|
-
return String(cellValue).toLowerCase().endsWith(String(filterVal).toLowerCase())
|
|
166
|
-
case 'greaterThan':
|
|
167
|
-
return Number(cellValue) > Number(filterVal)
|
|
168
|
-
case 'lessThan':
|
|
169
|
-
return Number(cellValue) < Number(filterVal)
|
|
170
|
-
case 'greaterThanOrEqual':
|
|
171
|
-
return Number(cellValue) >= Number(filterVal)
|
|
172
|
-
case 'lessThanOrEqual':
|
|
173
|
-
return Number(cellValue) <= Number(filterVal)
|
|
174
|
-
case 'isNull':
|
|
175
|
-
return cellValue == null || cellValue === ''
|
|
176
|
-
case 'isNotNull':
|
|
177
|
-
return cellValue != null && cellValue !== ''
|
|
178
|
-
default:
|
|
179
|
-
return true
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Reset all column filters first
|
|
184
|
-
table.resetColumnFilters()
|
|
185
|
-
|
|
186
|
-
// Apply filter for each column that has a filter condition
|
|
187
|
-
const columnsWithFilters = [...new Set(filters.map(f => f.column))]
|
|
188
|
-
|
|
189
|
-
columnsWithFilters.forEach(columnId => {
|
|
190
|
-
const column = table.getColumn(columnId)
|
|
191
|
-
if (!column) return
|
|
192
|
-
|
|
193
|
-
// Set filter with custom function
|
|
194
|
-
column.setFilterValue({ custom: true, filters, matchAll })
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// If no filters, ensure all filters are cleared
|
|
198
|
-
if (filters.length === 0) {
|
|
199
|
-
table.resetColumnFilters()
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
onOpenChange(false)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return (
|
|
206
|
-
<>
|
|
207
|
-
{/* Backdrop */}
|
|
208
|
-
<AnimatePresence>
|
|
209
|
-
{open && (
|
|
210
|
-
<motion.div
|
|
211
|
-
initial={{ opacity: 0 }}
|
|
212
|
-
animate={{ opacity: 1 }}
|
|
213
|
-
exit={{ opacity: 0 }}
|
|
214
|
-
className="fixed inset-0 bg-black/20 z-40"
|
|
215
|
-
onClick={() => onOpenChange(false)}
|
|
216
|
-
/>
|
|
217
|
-
)}
|
|
218
|
-
</AnimatePresence>
|
|
219
|
-
|
|
220
|
-
{/* Drawer */}
|
|
221
|
-
<AnimatePresence>
|
|
222
|
-
{open && (
|
|
223
|
-
<motion.div
|
|
224
|
-
initial={{ x: position === 'right' ? '100%' : '-100%' }}
|
|
225
|
-
animate={{ x: 0 }}
|
|
226
|
-
exit={{ x: position === 'right' ? '100%' : '-100%' }}
|
|
227
|
-
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
|
228
|
-
className={cn(
|
|
229
|
-
"fixed top-0 bottom-0 z-50 bg-background border-l shadow-xl",
|
|
230
|
-
position === 'right' ? 'right-0' : 'left-0'
|
|
231
|
-
)}
|
|
232
|
-
style={{ width }}
|
|
233
|
-
>
|
|
234
|
-
<div className="flex flex-col h-full">
|
|
235
|
-
{/* Header */}
|
|
236
|
-
<div className="flex items-center justify-between p-4 border-b">
|
|
237
|
-
<div className="flex items-center gap-2">
|
|
238
|
-
<Filter className="h-5 w-5" />
|
|
239
|
-
<h2 className="text-lg font-semibold">Filters</h2>
|
|
240
|
-
{filters.length > 0 && (
|
|
241
|
-
<span className="text-sm text-muted-foreground">
|
|
242
|
-
({filters.length} active)
|
|
243
|
-
</span>
|
|
244
|
-
)}
|
|
245
|
-
</div>
|
|
246
|
-
<Button
|
|
247
|
-
variant="ghost"
|
|
248
|
-
size="icon"
|
|
249
|
-
onClick={() => onOpenChange(false)}
|
|
250
|
-
>
|
|
251
|
-
<X className="h-4 w-4" />
|
|
252
|
-
</Button>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{/* Content */}
|
|
256
|
-
<div className="flex-1 overflow-y-auto p-4">
|
|
257
|
-
{/* Match mode */}
|
|
258
|
-
<div className="mb-6">
|
|
259
|
-
<Label className="text-sm font-medium mb-2 block">
|
|
260
|
-
Match conditions
|
|
261
|
-
</Label>
|
|
262
|
-
<div className="flex items-center gap-2">
|
|
263
|
-
<Button
|
|
264
|
-
variant={matchAll ? 'primary' : 'outline'}
|
|
265
|
-
size="sm"
|
|
266
|
-
onClick={() => setMatchAll(true)}
|
|
267
|
-
className="flex-1"
|
|
268
|
-
>
|
|
269
|
-
Match all
|
|
270
|
-
</Button>
|
|
271
|
-
<Button
|
|
272
|
-
variant={!matchAll ? 'primary' : 'outline'}
|
|
273
|
-
size="sm"
|
|
274
|
-
onClick={() => setMatchAll(false)}
|
|
275
|
-
className="flex-1"
|
|
276
|
-
>
|
|
277
|
-
Match any
|
|
278
|
-
</Button>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
|
|
282
|
-
<Separator className="mb-6" />
|
|
283
|
-
|
|
284
|
-
{/* Custom filters */}
|
|
285
|
-
{customFilters && (
|
|
286
|
-
<>
|
|
287
|
-
{customFilters}
|
|
288
|
-
<Separator className="my-6" />
|
|
289
|
-
</>
|
|
290
|
-
)}
|
|
291
|
-
|
|
292
|
-
{/* Filter conditions */}
|
|
293
|
-
<div className="space-y-4">
|
|
294
|
-
{filters.map((filter, index) => (
|
|
295
|
-
<FilterConditionRow
|
|
296
|
-
key={index}
|
|
297
|
-
filter={filter}
|
|
298
|
-
columns={filterableColumns}
|
|
299
|
-
onUpdate={(updates) => updateFilter(index, updates)}
|
|
300
|
-
onRemove={() => removeFilter(index)}
|
|
301
|
-
/>
|
|
302
|
-
))}
|
|
303
|
-
</div>
|
|
304
|
-
|
|
305
|
-
{/* Add filter button */}
|
|
306
|
-
<Button
|
|
307
|
-
variant="outline"
|
|
308
|
-
size="sm"
|
|
309
|
-
onClick={addFilter}
|
|
310
|
-
className="w-full mt-4"
|
|
311
|
-
>
|
|
312
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
313
|
-
Add filter
|
|
314
|
-
</Button>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
{/* Footer */}
|
|
318
|
-
<div className="p-4 border-t space-y-2">
|
|
319
|
-
<div className="flex gap-2">
|
|
320
|
-
<Button
|
|
321
|
-
variant="outline"
|
|
322
|
-
onClick={clearAllFilters}
|
|
323
|
-
disabled={filters.length === 0}
|
|
324
|
-
className="flex-1"
|
|
325
|
-
>
|
|
326
|
-
<Trash2 className="h-4 w-4 mr-2" />
|
|
327
|
-
Clear all
|
|
328
|
-
</Button>
|
|
329
|
-
<Button onClick={applyFilters} className="flex-1">
|
|
330
|
-
Apply filters
|
|
331
|
-
</Button>
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
</motion.div>
|
|
336
|
-
)}
|
|
337
|
-
</AnimatePresence>
|
|
338
|
-
</>
|
|
339
|
-
)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
interface FilterConditionRowProps<TData> {
|
|
343
|
-
filter: FilterCondition
|
|
344
|
-
columns: Column<TData, any>[]
|
|
345
|
-
onUpdate: (updates: Partial<FilterCondition>) => void
|
|
346
|
-
onRemove: () => void
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function FilterConditionRow<TData>({
|
|
350
|
-
filter,
|
|
351
|
-
columns,
|
|
352
|
-
onUpdate,
|
|
353
|
-
onRemove,
|
|
354
|
-
}: FilterConditionRowProps<TData>) {
|
|
355
|
-
const selectedColumn = columns.find(col => col.id === filter.column)
|
|
356
|
-
const columnDef = selectedColumn?.columnDef as any
|
|
357
|
-
const columnType = columnDef?.meta?.filterType || 'string'
|
|
358
|
-
const availableOperators = getOperatorsForColumnType(columnType)
|
|
359
|
-
|
|
360
|
-
const needsValue = filter.operator !== 'isNull' && filter.operator !== 'isNotNull'
|
|
361
|
-
|
|
362
|
-
return (
|
|
363
|
-
<div className="space-y-2 p-3 border rounded-lg bg-muted/30">
|
|
364
|
-
{/* Column selector */}
|
|
365
|
-
<div className="flex items-center gap-2">
|
|
366
|
-
<Select
|
|
367
|
-
value={filter.column}
|
|
368
|
-
onValueChange={(value) => onUpdate({ column: value })}
|
|
369
|
-
>
|
|
370
|
-
<SelectTrigger className="flex-1">
|
|
371
|
-
<SelectValue />
|
|
372
|
-
</SelectTrigger>
|
|
373
|
-
<SelectContent>
|
|
374
|
-
{columns.map(column => {
|
|
375
|
-
const header = column.columnDef.header
|
|
376
|
-
const label = typeof header === 'function' ? column.id : header || column.id
|
|
377
|
-
|
|
378
|
-
return (
|
|
379
|
-
<SelectItem key={column.id} value={column.id}>
|
|
380
|
-
{label}
|
|
381
|
-
</SelectItem>
|
|
382
|
-
)
|
|
383
|
-
})}
|
|
384
|
-
</SelectContent>
|
|
385
|
-
</Select>
|
|
386
|
-
|
|
387
|
-
<Button
|
|
388
|
-
variant="ghost"
|
|
389
|
-
size="icon"
|
|
390
|
-
onClick={onRemove}
|
|
391
|
-
className="h-8 w-8"
|
|
392
|
-
>
|
|
393
|
-
<X className="h-4 w-4" />
|
|
394
|
-
</Button>
|
|
395
|
-
</div>
|
|
396
|
-
|
|
397
|
-
{/* Operator selector */}
|
|
398
|
-
<Select
|
|
399
|
-
value={filter.operator}
|
|
400
|
-
onValueChange={(value) => onUpdate({ operator: value as FilterOperator })}
|
|
401
|
-
>
|
|
402
|
-
<SelectTrigger>
|
|
403
|
-
<SelectValue />
|
|
404
|
-
</SelectTrigger>
|
|
405
|
-
<SelectContent>
|
|
406
|
-
{availableOperators.map(operator => (
|
|
407
|
-
<SelectItem key={operator} value={operator}>
|
|
408
|
-
{operatorLabels[operator]}
|
|
409
|
-
</SelectItem>
|
|
410
|
-
))}
|
|
411
|
-
</SelectContent>
|
|
412
|
-
</Select>
|
|
413
|
-
|
|
414
|
-
{/* Value input */}
|
|
415
|
-
{needsValue && (
|
|
416
|
-
<div>
|
|
417
|
-
{columnType === 'boolean' ? (
|
|
418
|
-
<Select
|
|
419
|
-
value={String(filter.value)}
|
|
420
|
-
onValueChange={(value) => onUpdate({ value: value === 'true' })}
|
|
421
|
-
>
|
|
422
|
-
<SelectTrigger>
|
|
423
|
-
<SelectValue />
|
|
424
|
-
</SelectTrigger>
|
|
425
|
-
<SelectContent>
|
|
426
|
-
<SelectItem value="true">True</SelectItem>
|
|
427
|
-
<SelectItem value="false">False</SelectItem>
|
|
428
|
-
</SelectContent>
|
|
429
|
-
</Select>
|
|
430
|
-
) : columnType === 'number' ? (
|
|
431
|
-
<Input
|
|
432
|
-
type="number"
|
|
433
|
-
value={filter.value || ''}
|
|
434
|
-
onChange={(e) => onUpdate({ value: e.target.value })}
|
|
435
|
-
placeholder="Enter value..."
|
|
436
|
-
/>
|
|
437
|
-
) : (
|
|
438
|
-
<Input
|
|
439
|
-
value={filter.value || ''}
|
|
440
|
-
onChange={(e) => onUpdate({ value: e.target.value })}
|
|
441
|
-
placeholder="Enter value..."
|
|
442
|
-
/>
|
|
443
|
-
)}
|
|
444
|
-
</div>
|
|
445
|
-
)}
|
|
446
|
-
</div>
|
|
447
|
-
)
|
|
448
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
2
|
-
import '@testing-library/jest-dom'
|
|
3
|
-
import { DataTable } from './index'
|
|
4
|
-
|
|
5
|
-
// Mock data for testing
|
|
6
|
-
const mockData = [
|
|
7
|
-
{ id: 1, name: 'John Doe', email: 'john@example.com', status: 'active' },
|
|
8
|
-
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'inactive' },
|
|
9
|
-
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', status: 'active' }
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
const mockColumns = [
|
|
13
|
-
{
|
|
14
|
-
accessorKey: 'name',
|
|
15
|
-
header: 'Name',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
accessorKey: 'email',
|
|
19
|
-
header: 'Email',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
accessorKey: 'status',
|
|
23
|
-
header: 'Status',
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
describe('DataTable', () => {
|
|
28
|
-
it('renders without crashing', () => {
|
|
29
|
-
render(
|
|
30
|
-
<DataTable
|
|
31
|
-
columns={mockColumns}
|
|
32
|
-
data={mockData}
|
|
33
|
-
/>
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
expect(screen.getByText('Name')).toBeInTheDocument()
|
|
37
|
-
expect(screen.getByText('Email')).toBeInTheDocument()
|
|
38
|
-
expect(screen.getByText('Status')).toBeInTheDocument()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('displays data correctly', () => {
|
|
42
|
-
render(
|
|
43
|
-
<DataTable
|
|
44
|
-
columns={mockColumns}
|
|
45
|
-
data={mockData}
|
|
46
|
-
/>
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
50
|
-
expect(screen.getByText('jane@example.com')).toBeInTheDocument()
|
|
51
|
-
expect(screen.getByText('active')).toBeInTheDocument()
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('shows search input when searchable is true', () => {
|
|
55
|
-
render(
|
|
56
|
-
<DataTable
|
|
57
|
-
columns={mockColumns}
|
|
58
|
-
data={mockData}
|
|
59
|
-
searchable
|
|
60
|
-
/>
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
expect(screen.getByPlaceholderText('Search all columns...')).toBeInTheDocument()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('shows filter button when filterable is true', () => {
|
|
67
|
-
render(
|
|
68
|
-
<DataTable
|
|
69
|
-
columns={mockColumns}
|
|
70
|
-
data={mockData}
|
|
71
|
-
filterable
|
|
72
|
-
/>
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
expect(screen.getByText('Filters')).toBeInTheDocument()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('shows export button when exportable is true', () => {
|
|
79
|
-
render(
|
|
80
|
-
<DataTable
|
|
81
|
-
columns={mockColumns}
|
|
82
|
-
data={mockData}
|
|
83
|
-
exportable
|
|
84
|
-
/>
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
expect(screen.getByText('Export')).toBeInTheDocument()
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('handles search functionality', () => {
|
|
91
|
-
render(
|
|
92
|
-
<DataTable
|
|
93
|
-
columns={mockColumns}
|
|
94
|
-
data={mockData}
|
|
95
|
-
searchable
|
|
96
|
-
/>
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
const searchInput = screen.getByPlaceholderText('Search all columns...')
|
|
100
|
-
fireEvent.change(searchInput, { target: { value: 'john' } })
|
|
101
|
-
|
|
102
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
103
|
-
// Jane Smith should be filtered out
|
|
104
|
-
expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('handles pagination', () => {
|
|
108
|
-
// Create more data for pagination testing
|
|
109
|
-
const moreData = Array.from({ length: 20 }, (_, i) => ({
|
|
110
|
-
id: i + 1,
|
|
111
|
-
name: `User ${i + 1}`,
|
|
112
|
-
email: `user${i + 1}@example.com`,
|
|
113
|
-
status: i % 2 === 0 ? 'active' : 'inactive'
|
|
114
|
-
}))
|
|
115
|
-
|
|
116
|
-
render(
|
|
117
|
-
<DataTable
|
|
118
|
-
columns={mockColumns}
|
|
119
|
-
data={moreData}
|
|
120
|
-
pagination
|
|
121
|
-
pageSize={10}
|
|
122
|
-
/>
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
expect(screen.getByText('Page 1 of 2')).toBeInTheDocument()
|
|
126
|
-
expect(screen.getByText('Rows per page')).toBeInTheDocument()
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('calls onRowSelect when row is selected', () => {
|
|
130
|
-
const mockOnRowSelect = jest.fn()
|
|
131
|
-
|
|
132
|
-
render(
|
|
133
|
-
<DataTable
|
|
134
|
-
columns={mockColumns}
|
|
135
|
-
data={mockData}
|
|
136
|
-
selectable
|
|
137
|
-
onRowSelect={mockOnRowSelect}
|
|
138
|
-
/>
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
// This would require implementing selection in the component
|
|
142
|
-
// For now, just verify the component renders with selection enabled
|
|
143
|
-
expect(screen.getByRole('table')).toBeInTheDocument()
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('calls onExport when export button is clicked', () => {
|
|
147
|
-
const mockOnExport = jest.fn()
|
|
148
|
-
|
|
149
|
-
render(
|
|
150
|
-
<DataTable
|
|
151
|
-
columns={mockColumns}
|
|
152
|
-
data={mockData}
|
|
153
|
-
exportable
|
|
154
|
-
onExport={mockOnExport}
|
|
155
|
-
/>
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
const exportButton = screen.getByText('Export')
|
|
159
|
-
fireEvent.click(exportButton)
|
|
160
|
-
|
|
161
|
-
expect(mockOnExport).toHaveBeenCalledWith(mockData)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('handles empty data state', () => {
|
|
165
|
-
render(
|
|
166
|
-
<DataTable
|
|
167
|
-
columns={mockColumns}
|
|
168
|
-
data={[]}
|
|
169
|
-
/>
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
expect(screen.getByText('No results found.')).toBeInTheDocument()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('applies custom className', () => {
|
|
176
|
-
render(
|
|
177
|
-
<DataTable
|
|
178
|
-
columns={mockColumns}
|
|
179
|
-
data={mockData}
|
|
180
|
-
className="custom-table"
|
|
181
|
-
/>
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
const tableContainer = screen.getByRole('table').closest('div')
|
|
185
|
-
expect(tableContainer).toHaveClass('custom-table')
|
|
186
|
-
})
|
|
187
|
-
})
|