@moontra/moonui-pro 2.0.22 → 2.1.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.
- package/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- package/src/utils/package-guard.ts +60 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
|
|
5
|
+
import { Badge } from '../ui/badge'
|
|
6
|
+
import { Button } from '../ui/button'
|
|
7
|
+
import {
|
|
8
|
+
TrendingUp,
|
|
9
|
+
TrendingDown,
|
|
10
|
+
Users,
|
|
11
|
+
DollarSign,
|
|
12
|
+
Activity,
|
|
13
|
+
BarChart3,
|
|
14
|
+
Eye,
|
|
15
|
+
Download,
|
|
16
|
+
Star,
|
|
17
|
+
Clock,
|
|
18
|
+
Calendar,
|
|
19
|
+
ArrowUpRight,
|
|
20
|
+
ArrowDownRight,
|
|
21
|
+
Minus,
|
|
22
|
+
Lock,
|
|
23
|
+
Sparkles
|
|
24
|
+
} from 'lucide-react'
|
|
25
|
+
import { cn } from '@moontra/moonui'
|
|
26
|
+
|
|
27
|
+
export interface DashboardMetric {
|
|
28
|
+
id: string
|
|
29
|
+
title: string
|
|
30
|
+
value: string | number
|
|
31
|
+
change?: {
|
|
32
|
+
value: number
|
|
33
|
+
type: 'increase' | 'decrease' | 'neutral'
|
|
34
|
+
period: string
|
|
35
|
+
}
|
|
36
|
+
icon?: React.ReactNode
|
|
37
|
+
color?: string
|
|
38
|
+
description?: string
|
|
39
|
+
trend?: number[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DashboardWidget {
|
|
43
|
+
id: string
|
|
44
|
+
title: string
|
|
45
|
+
description?: string
|
|
46
|
+
content: React.ReactNode
|
|
47
|
+
size?: 'sm' | 'md' | 'lg' | 'xl'
|
|
48
|
+
loading?: boolean
|
|
49
|
+
error?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DashboardProps {
|
|
53
|
+
metrics?: DashboardMetric[]
|
|
54
|
+
widgets?: DashboardWidget[]
|
|
55
|
+
onMetricClick?: (metric: DashboardMetric) => void
|
|
56
|
+
onWidgetAction?: (widgetId: string, action: string) => void
|
|
57
|
+
className?: string
|
|
58
|
+
showHeader?: boolean
|
|
59
|
+
title?: string
|
|
60
|
+
description?: string
|
|
61
|
+
refreshable?: boolean
|
|
62
|
+
onRefresh?: () => void
|
|
63
|
+
loading?: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const METRIC_COLORS = {
|
|
67
|
+
primary: 'text-blue-600',
|
|
68
|
+
success: 'text-green-600',
|
|
69
|
+
warning: 'text-yellow-600',
|
|
70
|
+
danger: 'text-red-600',
|
|
71
|
+
info: 'text-purple-600'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const WIDGET_SIZES = {
|
|
75
|
+
sm: 'col-span-1',
|
|
76
|
+
md: 'col-span-2',
|
|
77
|
+
lg: 'col-span-3',
|
|
78
|
+
xl: 'col-span-4'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function Dashboard({
|
|
82
|
+
metrics = [],
|
|
83
|
+
widgets = [],
|
|
84
|
+
onMetricClick,
|
|
85
|
+
onWidgetAction,
|
|
86
|
+
className,
|
|
87
|
+
showHeader = true,
|
|
88
|
+
title = 'Dashboard',
|
|
89
|
+
description = 'Overview of your key metrics and performance',
|
|
90
|
+
refreshable = true,
|
|
91
|
+
onRefresh,
|
|
92
|
+
loading = false
|
|
93
|
+
}: DashboardProps) {
|
|
94
|
+
const [refreshing, setRefreshing] = React.useState(false)
|
|
95
|
+
|
|
96
|
+
const handleRefresh = async () => {
|
|
97
|
+
if (onRefresh) {
|
|
98
|
+
setRefreshing(true)
|
|
99
|
+
try {
|
|
100
|
+
await onRefresh()
|
|
101
|
+
} finally {
|
|
102
|
+
setRefreshing(false)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const formatValue = (value: string | number): string => {
|
|
108
|
+
if (typeof value === 'number') {
|
|
109
|
+
if (value >= 1000000) {
|
|
110
|
+
return (value / 1000000).toFixed(1) + 'M'
|
|
111
|
+
} else if (value >= 1000) {
|
|
112
|
+
return (value / 1000).toFixed(1) + 'K'
|
|
113
|
+
}
|
|
114
|
+
return value.toLocaleString()
|
|
115
|
+
}
|
|
116
|
+
return value.toString()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const getChangeIcon = (type: 'increase' | 'decrease' | 'neutral') => {
|
|
120
|
+
switch (type) {
|
|
121
|
+
case 'increase':
|
|
122
|
+
return <ArrowUpRight className="h-4 w-4" />
|
|
123
|
+
case 'decrease':
|
|
124
|
+
return <ArrowDownRight className="h-4 w-4" />
|
|
125
|
+
case 'neutral':
|
|
126
|
+
return <Minus className="h-4 w-4" />
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const getChangeColor = (type: 'increase' | 'decrease' | 'neutral') => {
|
|
131
|
+
switch (type) {
|
|
132
|
+
case 'increase':
|
|
133
|
+
return 'text-green-600'
|
|
134
|
+
case 'decrease':
|
|
135
|
+
return 'text-red-600'
|
|
136
|
+
case 'neutral':
|
|
137
|
+
return 'text-gray-600'
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const renderMetricCard = (metric: DashboardMetric) => {
|
|
142
|
+
const colorClass = metric.color ? METRIC_COLORS[metric.color as keyof typeof METRIC_COLORS] : 'text-foreground'
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Card
|
|
146
|
+
key={metric.id}
|
|
147
|
+
className={cn(
|
|
148
|
+
"cursor-pointer hover:shadow-md transition-shadow",
|
|
149
|
+
onMetricClick && "hover:bg-muted/50"
|
|
150
|
+
)}
|
|
151
|
+
onClick={() => onMetricClick?.(metric)}
|
|
152
|
+
>
|
|
153
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
154
|
+
<CardTitle className="text-sm font-medium">
|
|
155
|
+
{metric.title}
|
|
156
|
+
</CardTitle>
|
|
157
|
+
<div className={cn("h-4 w-4", colorClass)}>
|
|
158
|
+
{metric.icon}
|
|
159
|
+
</div>
|
|
160
|
+
</CardHeader>
|
|
161
|
+
<CardContent>
|
|
162
|
+
<div className="text-2xl font-bold">{formatValue(metric.value)}</div>
|
|
163
|
+
{metric.change && (
|
|
164
|
+
<div className={cn(
|
|
165
|
+
"flex items-center text-xs mt-1",
|
|
166
|
+
getChangeColor(metric.change.type)
|
|
167
|
+
)}>
|
|
168
|
+
{getChangeIcon(metric.change.type)}
|
|
169
|
+
<span className="ml-1">
|
|
170
|
+
{Math.abs(metric.change.value)}% from {metric.change.period}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
{metric.description && (
|
|
175
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
176
|
+
{metric.description}
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const renderWidget = (widget: DashboardWidget) => {
|
|
185
|
+
const sizeClass = WIDGET_SIZES[widget.size || 'md']
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<Card key={widget.id} className={cn("h-fit", sizeClass)}>
|
|
189
|
+
<CardHeader>
|
|
190
|
+
<div className="flex items-center justify-between">
|
|
191
|
+
<div>
|
|
192
|
+
<CardTitle className="text-base">{widget.title}</CardTitle>
|
|
193
|
+
{widget.description && (
|
|
194
|
+
<CardDescription className="mt-1">
|
|
195
|
+
{widget.description}
|
|
196
|
+
</CardDescription>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="sm"
|
|
202
|
+
onClick={() => onWidgetAction?.(widget.id, 'menu')}
|
|
203
|
+
>
|
|
204
|
+
<BarChart3 className="h-4 w-4" />
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
</CardHeader>
|
|
208
|
+
<CardContent>
|
|
209
|
+
{widget.loading ? (
|
|
210
|
+
<div className="flex items-center justify-center h-32">
|
|
211
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
212
|
+
</div>
|
|
213
|
+
) : widget.error ? (
|
|
214
|
+
<div className="flex items-center justify-center h-32 text-destructive">
|
|
215
|
+
<p className="text-sm">{widget.error}</p>
|
|
216
|
+
</div>
|
|
217
|
+
) : (
|
|
218
|
+
widget.content
|
|
219
|
+
)}
|
|
220
|
+
</CardContent>
|
|
221
|
+
</Card>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const defaultMetrics: DashboardMetric[] = [
|
|
226
|
+
{
|
|
227
|
+
id: 'total-users',
|
|
228
|
+
title: 'Total Users',
|
|
229
|
+
value: 2543,
|
|
230
|
+
change: { value: 12, type: 'increase', period: 'last month' },
|
|
231
|
+
icon: <Users className="h-4 w-4" />,
|
|
232
|
+
color: 'primary'
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'revenue',
|
|
236
|
+
title: 'Revenue',
|
|
237
|
+
value: '$12,345',
|
|
238
|
+
change: { value: 8, type: 'increase', period: 'last month' },
|
|
239
|
+
icon: <DollarSign className="h-4 w-4" />,
|
|
240
|
+
color: 'success'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'active-sessions',
|
|
244
|
+
title: 'Active Sessions',
|
|
245
|
+
value: 1234,
|
|
246
|
+
change: { value: 2, type: 'decrease', period: 'last hour' },
|
|
247
|
+
icon: <Activity className="h-4 w-4" />,
|
|
248
|
+
color: 'warning'
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: 'conversion-rate',
|
|
252
|
+
title: 'Conversion Rate',
|
|
253
|
+
value: '3.2%',
|
|
254
|
+
change: { value: 0.3, type: 'increase', period: 'last week' },
|
|
255
|
+
icon: <TrendingUp className="h-4 w-4" />,
|
|
256
|
+
color: 'info'
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
const displayMetrics = metrics.length > 0 ? metrics : defaultMetrics
|
|
261
|
+
|
|
262
|
+
if (loading) {
|
|
263
|
+
return (
|
|
264
|
+
<div className={cn("w-full", className)}>
|
|
265
|
+
<div className="animate-pulse space-y-4">
|
|
266
|
+
<div className="h-8 bg-muted rounded w-1/4"></div>
|
|
267
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
268
|
+
{[...Array(4)].map((_, i) => (
|
|
269
|
+
<div key={i} className="h-32 bg-muted rounded"></div>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className={cn("w-full space-y-6", className)}>
|
|
279
|
+
{/* Header */}
|
|
280
|
+
{showHeader && (
|
|
281
|
+
<div className="flex items-center justify-between">
|
|
282
|
+
<div>
|
|
283
|
+
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
|
284
|
+
<p className="text-muted-foreground">{description}</p>
|
|
285
|
+
</div>
|
|
286
|
+
{refreshable && (
|
|
287
|
+
<Button
|
|
288
|
+
variant="outline"
|
|
289
|
+
onClick={handleRefresh}
|
|
290
|
+
disabled={refreshing}
|
|
291
|
+
>
|
|
292
|
+
{refreshing ? (
|
|
293
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div>
|
|
294
|
+
) : (
|
|
295
|
+
<Activity className="h-4 w-4 mr-2" />
|
|
296
|
+
)}
|
|
297
|
+
Refresh
|
|
298
|
+
</Button>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{/* Metrics Grid */}
|
|
304
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
305
|
+
{displayMetrics.map(renderMetricCard)}
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
{/* Widgets Grid */}
|
|
309
|
+
{widgets.length > 0 && (
|
|
310
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
311
|
+
{widgets.map(renderWidget)}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{/* Quick Actions */}
|
|
316
|
+
<div className="flex items-center gap-2 pt-4 border-t">
|
|
317
|
+
<Button variant="outline" size="sm">
|
|
318
|
+
<Download className="h-4 w-4 mr-2" />
|
|
319
|
+
Export Data
|
|
320
|
+
</Button>
|
|
321
|
+
<Button variant="outline" size="sm">
|
|
322
|
+
<Calendar className="h-4 w-4 mr-2" />
|
|
323
|
+
Schedule Report
|
|
324
|
+
</Button>
|
|
325
|
+
<Button variant="outline" size="sm">
|
|
326
|
+
<Star className="h-4 w-4 mr-2" />
|
|
327
|
+
Save View
|
|
328
|
+
</Button>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export default Dashboard
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
})
|