@startsimpli/ui 0.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/README.md +537 -0
- package/package.json +80 -0
- package/src/components/index.ts +50 -0
- package/src/components/navigation/sidebar.tsx +178 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +70 -0
- package/src/components/ui/card.tsx +68 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +12 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/index.ts +24 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/components/unified-table/UnifiedTable.tsx +553 -0
- package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
- package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
- package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
- package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
- package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
- package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
- package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
- package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
- package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
- package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
- package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
- package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
- package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
- package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
- package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
- package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
- package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
- package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
- package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
- package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
- package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
- package/src/components/unified-table/components/MobileView/README.md +411 -0
- package/src/components/unified-table/components/MobileView/index.tsx +77 -0
- package/src/components/unified-table/components/MobileView/types.ts +77 -0
- package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
- package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
- package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
- package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
- package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
- package/src/components/unified-table/hooks/index.ts +21 -0
- package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
- package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
- package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
- package/src/components/unified-table/hooks/useFilters.ts +53 -0
- package/src/components/unified-table/hooks/usePagination.ts +120 -0
- package/src/components/unified-table/hooks/useResponsive.ts +50 -0
- package/src/components/unified-table/hooks/useSelection.ts +152 -0
- package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
- package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
- package/src/components/unified-table/hooks/useTableState.ts +103 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
- package/src/components/unified-table/hooks/useTableURL.ts +301 -0
- package/src/components/unified-table/index.ts +16 -0
- package/src/components/unified-table/types.ts +393 -0
- package/src/components/unified-table/utils/export.ts +236 -0
- package/src/components/unified-table/utils/index.ts +4 -0
- package/src/components/unified-table/utils/renderers.ts +105 -0
- package/src/components/unified-table/utils/themes.ts +87 -0
- package/src/components/unified-table/utils/validation.ts +122 -0
- package/src/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/theme/contract.ts +46 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/tailwind.config.js +70 -0
- package/src/theme/tailwind.preset.ts +93 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +91 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { exportToCSV, exportToExcel, exportData, generateExportFilename } from '../../utils/export'
|
|
2
|
+
import { ColumnConfig } from '../../types'
|
|
3
|
+
import * as XLSX from 'xlsx'
|
|
4
|
+
|
|
5
|
+
// Mock the XLSX library
|
|
6
|
+
jest.mock('xlsx', () => ({
|
|
7
|
+
utils: {
|
|
8
|
+
aoa_to_sheet: jest.fn(),
|
|
9
|
+
book_new: jest.fn(),
|
|
10
|
+
book_append_sheet: jest.fn(),
|
|
11
|
+
},
|
|
12
|
+
write: jest.fn(),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
// Mock URL.createObjectURL and URL.revokeObjectURL
|
|
16
|
+
global.URL.createObjectURL = jest.fn(() => 'blob:mock-url')
|
|
17
|
+
global.URL.revokeObjectURL = jest.fn()
|
|
18
|
+
|
|
19
|
+
// Mock document methods
|
|
20
|
+
const mockClick = jest.fn()
|
|
21
|
+
const mockAppendChild = jest.fn()
|
|
22
|
+
const mockRemoveChild = jest.fn()
|
|
23
|
+
|
|
24
|
+
Object.defineProperty(document, 'createElement', {
|
|
25
|
+
writable: true,
|
|
26
|
+
value: jest.fn().mockReturnValue({
|
|
27
|
+
click: mockClick,
|
|
28
|
+
href: '',
|
|
29
|
+
download: '',
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
Object.defineProperty(document.body, 'appendChild', {
|
|
34
|
+
writable: true,
|
|
35
|
+
value: mockAppendChild,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
Object.defineProperty(document.body, 'removeChild', {
|
|
39
|
+
writable: true,
|
|
40
|
+
value: mockRemoveChild,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
interface TestData {
|
|
44
|
+
id: string
|
|
45
|
+
name: string
|
|
46
|
+
age: number
|
|
47
|
+
isActive: boolean
|
|
48
|
+
company?: {
|
|
49
|
+
name: string
|
|
50
|
+
}
|
|
51
|
+
tags?: string[]
|
|
52
|
+
createdAt: Date
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('Export Utilities', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.clearAllMocks()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const sampleData: TestData[] = [
|
|
61
|
+
{
|
|
62
|
+
id: '1',
|
|
63
|
+
name: 'John Doe',
|
|
64
|
+
age: 30,
|
|
65
|
+
isActive: true,
|
|
66
|
+
company: { name: 'Acme Corp' },
|
|
67
|
+
tags: ['developer', 'manager'],
|
|
68
|
+
createdAt: new Date('2023-01-15'),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: '2',
|
|
72
|
+
name: 'Jane Smith',
|
|
73
|
+
age: 25,
|
|
74
|
+
isActive: false,
|
|
75
|
+
company: { name: 'Tech Inc' },
|
|
76
|
+
tags: ['designer'],
|
|
77
|
+
createdAt: new Date('2023-02-20'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: '3',
|
|
81
|
+
name: 'Bob Johnson',
|
|
82
|
+
age: 35,
|
|
83
|
+
isActive: true,
|
|
84
|
+
createdAt: new Date('2023-03-10'),
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
const sampleColumns: ColumnConfig<TestData>[] = [
|
|
89
|
+
{
|
|
90
|
+
id: 'id',
|
|
91
|
+
header: 'ID',
|
|
92
|
+
accessorKey: 'id',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'name',
|
|
96
|
+
header: 'Name',
|
|
97
|
+
accessorKey: 'name',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'age',
|
|
101
|
+
header: 'Age',
|
|
102
|
+
accessorKey: 'age',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'isActive',
|
|
106
|
+
header: 'Active',
|
|
107
|
+
accessorKey: 'isActive',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'company',
|
|
111
|
+
header: 'Company',
|
|
112
|
+
accessorKey: 'company.name',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'tags',
|
|
116
|
+
header: 'Tags',
|
|
117
|
+
accessorFn: (row) => row.tags?.join(', ') || '',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'createdAt',
|
|
121
|
+
header: 'Created At',
|
|
122
|
+
accessorKey: 'createdAt',
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
describe('exportToCSV', () => {
|
|
127
|
+
it('should export data to CSV format', () => {
|
|
128
|
+
exportToCSV(sampleData, sampleColumns, 'test-export')
|
|
129
|
+
|
|
130
|
+
expect(mockClick).toHaveBeenCalled()
|
|
131
|
+
expect(mockAppendChild).toHaveBeenCalled()
|
|
132
|
+
expect(mockRemoveChild).toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should include headers by default', () => {
|
|
136
|
+
const createElementSpy = jest.spyOn(document, 'createElement')
|
|
137
|
+
exportToCSV(sampleData, sampleColumns, 'test-export')
|
|
138
|
+
|
|
139
|
+
const linkElement = createElementSpy.mock.results[0]?.value
|
|
140
|
+
expect(linkElement.download).toBe('test-export.csv')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should handle empty data', () => {
|
|
144
|
+
expect(() => {
|
|
145
|
+
exportToCSV([], sampleColumns, 'test-export')
|
|
146
|
+
}).toThrow('No data to export')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should handle empty columns', () => {
|
|
150
|
+
expect(() => {
|
|
151
|
+
exportToCSV(sampleData, [], 'test-export')
|
|
152
|
+
}).toThrow('No columns specified for export')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should properly escape CSV values with quotes and commas', () => {
|
|
156
|
+
const dataWithSpecialChars: TestData[] = [
|
|
157
|
+
{
|
|
158
|
+
id: '1',
|
|
159
|
+
name: 'John "The Boss" Doe',
|
|
160
|
+
age: 30,
|
|
161
|
+
isActive: true,
|
|
162
|
+
createdAt: new Date('2023-01-15'),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: '2',
|
|
166
|
+
name: 'Smith, Jane',
|
|
167
|
+
age: 25,
|
|
168
|
+
isActive: false,
|
|
169
|
+
createdAt: new Date('2023-02-20'),
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
expect(() => {
|
|
174
|
+
exportToCSV(dataWithSpecialChars, sampleColumns, 'test-special-chars')
|
|
175
|
+
}).not.toThrow()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should handle nested object accessors', () => {
|
|
179
|
+
expect(() => {
|
|
180
|
+
exportToCSV(sampleData, sampleColumns, 'test-nested')
|
|
181
|
+
}).not.toThrow()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should handle accessor functions', () => {
|
|
185
|
+
const columnsWithFunctions: ColumnConfig<TestData>[] = [
|
|
186
|
+
{
|
|
187
|
+
id: 'fullInfo',
|
|
188
|
+
header: 'Full Info',
|
|
189
|
+
accessorFn: (row) => `${row.name} (${row.age})`,
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
expect(() => {
|
|
194
|
+
exportToCSV(sampleData, columnsWithFunctions, 'test-accessor-fn')
|
|
195
|
+
}).not.toThrow()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should format boolean values as Yes/No', () => {
|
|
199
|
+
const booleanColumns: ColumnConfig<TestData>[] = [
|
|
200
|
+
{
|
|
201
|
+
id: 'isActive',
|
|
202
|
+
header: 'Active',
|
|
203
|
+
accessorKey: 'isActive',
|
|
204
|
+
},
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
expect(() => {
|
|
208
|
+
exportToCSV(sampleData, booleanColumns, 'test-boolean')
|
|
209
|
+
}).not.toThrow()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should handle null and undefined values', () => {
|
|
213
|
+
const dataWithNulls: TestData[] = [
|
|
214
|
+
{
|
|
215
|
+
id: '1',
|
|
216
|
+
name: 'John Doe',
|
|
217
|
+
age: 30,
|
|
218
|
+
isActive: true,
|
|
219
|
+
company: undefined,
|
|
220
|
+
createdAt: new Date('2023-01-15'),
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
expect(() => {
|
|
225
|
+
exportToCSV(dataWithNulls, sampleColumns, 'test-nulls')
|
|
226
|
+
}).not.toThrow()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should format dates as ISO strings', () => {
|
|
230
|
+
const dateColumns: ColumnConfig<TestData>[] = [
|
|
231
|
+
{
|
|
232
|
+
id: 'createdAt',
|
|
233
|
+
header: 'Created',
|
|
234
|
+
accessorKey: 'createdAt',
|
|
235
|
+
},
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
expect(() => {
|
|
239
|
+
exportToCSV(sampleData, dateColumns, 'test-dates')
|
|
240
|
+
}).not.toThrow()
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('exportToExcel', () => {
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
;(XLSX.utils.aoa_to_sheet as jest.Mock).mockReturnValue({})
|
|
247
|
+
;(XLSX.utils.book_new as jest.Mock).mockReturnValue({})
|
|
248
|
+
;(XLSX.write as jest.Mock).mockReturnValue(new ArrayBuffer(8))
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should export data to Excel format', () => {
|
|
252
|
+
exportToExcel(sampleData, sampleColumns, 'test-export')
|
|
253
|
+
|
|
254
|
+
expect(XLSX.utils.aoa_to_sheet).toHaveBeenCalled()
|
|
255
|
+
expect(XLSX.utils.book_new).toHaveBeenCalled()
|
|
256
|
+
expect(XLSX.utils.book_append_sheet).toHaveBeenCalled()
|
|
257
|
+
expect(XLSX.write).toHaveBeenCalled()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should create worksheet with correct name', () => {
|
|
261
|
+
exportToExcel(sampleData, sampleColumns, 'test-export')
|
|
262
|
+
|
|
263
|
+
expect(XLSX.utils.book_append_sheet).toHaveBeenCalledWith(
|
|
264
|
+
expect.anything(),
|
|
265
|
+
expect.anything(),
|
|
266
|
+
'Data'
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should handle empty data', () => {
|
|
271
|
+
expect(() => {
|
|
272
|
+
exportToExcel([], sampleColumns, 'test-export')
|
|
273
|
+
}).toThrow('No data to export')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should handle empty columns', () => {
|
|
277
|
+
expect(() => {
|
|
278
|
+
exportToExcel(sampleData, [], 'test-export')
|
|
279
|
+
}).toThrow('No columns specified for export')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should set column widths', () => {
|
|
283
|
+
const mockSheet = {}
|
|
284
|
+
;(XLSX.utils.aoa_to_sheet as jest.Mock).mockReturnValue(mockSheet)
|
|
285
|
+
|
|
286
|
+
exportToExcel(sampleData, sampleColumns, 'test-widths')
|
|
287
|
+
|
|
288
|
+
expect(mockSheet).toHaveProperty('!cols')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('should trigger file download', () => {
|
|
292
|
+
exportToExcel(sampleData, sampleColumns, 'test-download')
|
|
293
|
+
|
|
294
|
+
expect(mockClick).toHaveBeenCalled()
|
|
295
|
+
expect(mockAppendChild).toHaveBeenCalled()
|
|
296
|
+
expect(mockRemoveChild).toHaveBeenCalled()
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('exportData', () => {
|
|
301
|
+
it('should route to CSV export when format is csv', () => {
|
|
302
|
+
exportData(sampleData, sampleColumns, {
|
|
303
|
+
format: 'csv',
|
|
304
|
+
filename: 'test-export',
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
expect(mockClick).toHaveBeenCalled()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('should route to Excel export when format is excel', () => {
|
|
311
|
+
;(XLSX.utils.aoa_to_sheet as jest.Mock).mockReturnValue({})
|
|
312
|
+
;(XLSX.utils.book_new as jest.Mock).mockReturnValue({})
|
|
313
|
+
;(XLSX.write as jest.Mock).mockReturnValue(new ArrayBuffer(8))
|
|
314
|
+
|
|
315
|
+
exportData(sampleData, sampleColumns, {
|
|
316
|
+
format: 'excel',
|
|
317
|
+
filename: 'test-export',
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
expect(XLSX.utils.aoa_to_sheet).toHaveBeenCalled()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('should throw error for unsupported format', () => {
|
|
324
|
+
expect(() => {
|
|
325
|
+
exportData(sampleData, sampleColumns, {
|
|
326
|
+
format: 'pdf' as any,
|
|
327
|
+
filename: 'test-export',
|
|
328
|
+
})
|
|
329
|
+
}).toThrow('Unsupported export format: pdf')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should respect includeHeaders option', () => {
|
|
333
|
+
exportData(sampleData, sampleColumns, {
|
|
334
|
+
format: 'csv',
|
|
335
|
+
filename: 'test-export',
|
|
336
|
+
includeHeaders: false,
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
expect(mockClick).toHaveBeenCalled()
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
describe('generateExportFilename', () => {
|
|
344
|
+
it('should generate filename with timestamp', () => {
|
|
345
|
+
const filename = generateExportFilename('export')
|
|
346
|
+
|
|
347
|
+
expect(filename).toMatch(/^export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should handle custom base filename', () => {
|
|
351
|
+
const filename = generateExportFilename('my-custom-export')
|
|
352
|
+
|
|
353
|
+
expect(filename).toMatch(/^my-custom-export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('should generate unique filenames on consecutive calls', async () => {
|
|
357
|
+
const filename1 = generateExportFilename('export')
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
359
|
+
const filename2 = generateExportFilename('export')
|
|
360
|
+
|
|
361
|
+
// They might be the same if called within the same second
|
|
362
|
+
// Just ensure they both match the pattern
|
|
363
|
+
expect(filename1).toMatch(/^export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
|
|
364
|
+
expect(filename2).toMatch(/^export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
describe('Edge Cases', () => {
|
|
369
|
+
it('should handle large datasets', () => {
|
|
370
|
+
const largeData = Array.from({ length: 10000 }, (_, i) => ({
|
|
371
|
+
id: `${i}`,
|
|
372
|
+
name: `User ${i}`,
|
|
373
|
+
age: 20 + (i % 50),
|
|
374
|
+
isActive: i % 2 === 0,
|
|
375
|
+
createdAt: new Date('2023-01-01'),
|
|
376
|
+
}))
|
|
377
|
+
|
|
378
|
+
expect(() => {
|
|
379
|
+
exportToCSV(largeData, sampleColumns, 'large-export')
|
|
380
|
+
}).not.toThrow()
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('should handle columns with function headers', () => {
|
|
384
|
+
const columnsWithFunctionHeaders: ColumnConfig<TestData>[] = [
|
|
385
|
+
{
|
|
386
|
+
id: 'name',
|
|
387
|
+
header: () => 'Custom Name Header',
|
|
388
|
+
accessorKey: 'name',
|
|
389
|
+
},
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
expect(() => {
|
|
393
|
+
exportToCSV(sampleData, columnsWithFunctionHeaders, 'test-function-header')
|
|
394
|
+
}).not.toThrow()
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('should handle data with array values', () => {
|
|
398
|
+
const dataWithArrays: TestData[] = [
|
|
399
|
+
{
|
|
400
|
+
id: '1',
|
|
401
|
+
name: 'John Doe',
|
|
402
|
+
age: 30,
|
|
403
|
+
isActive: true,
|
|
404
|
+
tags: ['tag1', 'tag2', 'tag3'],
|
|
405
|
+
createdAt: new Date('2023-01-15'),
|
|
406
|
+
},
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
expect(() => {
|
|
410
|
+
exportToCSV(dataWithArrays, sampleColumns, 'test-arrays')
|
|
411
|
+
}).not.toThrow()
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('should handle columns without accessor', () => {
|
|
415
|
+
const columnsWithoutAccessor: ColumnConfig<TestData>[] = [
|
|
416
|
+
{
|
|
417
|
+
id: 'custom',
|
|
418
|
+
header: 'Custom Column',
|
|
419
|
+
},
|
|
420
|
+
]
|
|
421
|
+
|
|
422
|
+
expect(() => {
|
|
423
|
+
exportToCSV(sampleData, columnsWithoutAccessor, 'test-no-accessor')
|
|
424
|
+
}).not.toThrow()
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button } from '../../../ui/button'
|
|
4
|
+
import { X } from 'lucide-react'
|
|
5
|
+
import { BulkAction } from '../../types'
|
|
6
|
+
import { cn } from '../../../../lib/utils'
|
|
7
|
+
|
|
8
|
+
interface BulkActionBarProps {
|
|
9
|
+
selectedCount: number
|
|
10
|
+
selectAllPages: boolean
|
|
11
|
+
totalCount: number
|
|
12
|
+
bulkActions: BulkAction[]
|
|
13
|
+
onClearSelection: () => void
|
|
14
|
+
onExecuteAction: (action: BulkAction) => void
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function BulkActionBar({
|
|
19
|
+
selectedCount,
|
|
20
|
+
selectAllPages,
|
|
21
|
+
totalCount,
|
|
22
|
+
bulkActions,
|
|
23
|
+
onClearSelection,
|
|
24
|
+
onExecuteAction,
|
|
25
|
+
className,
|
|
26
|
+
}: BulkActionBarProps) {
|
|
27
|
+
const isVisible = selectedCount > 0
|
|
28
|
+
|
|
29
|
+
const getVariantClassName = (variant: BulkAction['variant']) => {
|
|
30
|
+
switch (variant) {
|
|
31
|
+
case 'gradient-purple':
|
|
32
|
+
return 'bg-gradient-to-r from-purple-600 to-blue-600 text-white hover:from-purple-700 hover:to-blue-700'
|
|
33
|
+
case 'gradient-green':
|
|
34
|
+
return 'bg-gradient-to-r from-green-600 to-teal-600 text-white hover:from-green-700 hover:to-teal-700'
|
|
35
|
+
case 'gradient-indigo':
|
|
36
|
+
return 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white hover:from-indigo-700 hover:to-purple-700'
|
|
37
|
+
case 'gradient-orange':
|
|
38
|
+
return 'bg-gradient-to-r from-orange-600 to-red-600 text-white hover:from-orange-700 hover:to-red-700'
|
|
39
|
+
case 'gradient-blue':
|
|
40
|
+
return 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700'
|
|
41
|
+
default:
|
|
42
|
+
return ''
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={cn(
|
|
49
|
+
'transition-all duration-200 ease-in-out overflow-hidden',
|
|
50
|
+
isVisible
|
|
51
|
+
? 'opacity-100 max-h-20'
|
|
52
|
+
: 'opacity-0 max-h-0 pointer-events-none',
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
aria-hidden={!isVisible}
|
|
56
|
+
>
|
|
57
|
+
<div className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg border">
|
|
58
|
+
{/* Selection summary */}
|
|
59
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
60
|
+
{selectAllPages ? (
|
|
61
|
+
<span className="text-blue-600">
|
|
62
|
+
All <span className="font-semibold">{totalCount}</span> items selected
|
|
63
|
+
</span>
|
|
64
|
+
) : (
|
|
65
|
+
<span>
|
|
66
|
+
{`${selectedCount} ${selectedCount === 1 ? 'item' : 'items'} selected`}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Divider */}
|
|
72
|
+
<div className="h-6 w-px bg-border mx-1" />
|
|
73
|
+
|
|
74
|
+
{/* Bulk action buttons */}
|
|
75
|
+
<div className="flex items-center gap-2 flex-1">
|
|
76
|
+
{bulkActions.map((action) => {
|
|
77
|
+
const Icon = action.icon
|
|
78
|
+
const isDisabled = action.disabled?.(new Set()) || false
|
|
79
|
+
const exceedsMaxSelection = !!(action.maxSelection && selectedCount > action.maxSelection)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Button
|
|
83
|
+
key={action.id}
|
|
84
|
+
size="sm"
|
|
85
|
+
variant={action.variant === 'default' ? 'default' : 'default'}
|
|
86
|
+
className={cn(
|
|
87
|
+
getVariantClassName(action.variant),
|
|
88
|
+
'gap-2'
|
|
89
|
+
)}
|
|
90
|
+
onClick={() => onExecuteAction(action)}
|
|
91
|
+
disabled={isDisabled || exceedsMaxSelection}
|
|
92
|
+
title={
|
|
93
|
+
exceedsMaxSelection
|
|
94
|
+
? `Maximum ${action.maxSelection} items can be selected for this action`
|
|
95
|
+
: action.label
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
<Icon className="h-4 w-4" />
|
|
99
|
+
{action.label}
|
|
100
|
+
{selectAllPages && totalCount > 0 && ` (${totalCount})`}
|
|
101
|
+
</Button>
|
|
102
|
+
)
|
|
103
|
+
})}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Clear selection button */}
|
|
107
|
+
<Button
|
|
108
|
+
variant="outline"
|
|
109
|
+
size="sm"
|
|
110
|
+
onClick={onClearSelection}
|
|
111
|
+
className="gap-2"
|
|
112
|
+
>
|
|
113
|
+
<X className="h-4 w-4" />
|
|
114
|
+
Clear
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|