@moontra/moonui-pro 2.3.7 → 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.
- package/dist/index.d.ts +23 -3
- package/dist/index.mjs +690 -81
- package/package.json +4 -3
- package/scripts/postinstall.js +26 -0
- package/src/components/data-table/data-table-bulk-actions.tsx +204 -0
- package/src/components/data-table/data-table-column-toggle.tsx +166 -0
- package/src/components/data-table/data-table-export.ts +156 -0
- package/src/components/data-table/index.tsx +206 -86
- package/src/components/ui/alert-dialog.tsx +141 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moontra/moonui-pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"postinstall": "node scripts/postinstall.js",
|
|
33
33
|
"build": "tsup",
|
|
34
34
|
"build:dts": "tsup --dts",
|
|
35
|
-
"dev": "tsup --watch",
|
|
35
|
+
"dev": "tsup --watch --sourcemap",
|
|
36
36
|
"clean": "rm -rf dist",
|
|
37
37
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
38
38
|
"test": "jest",
|
|
@@ -75,7 +75,8 @@
|
|
|
75
75
|
"react-dom": ">=18.0.0 || ^19.0.0"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@moontra/moonui
|
|
78
|
+
"@moontra/moonui": "^2.1.9",
|
|
79
|
+
"@moontra/moonui-pro": "^2.3.8",
|
|
79
80
|
"@radix-ui/react-accordion": "^1.2.11",
|
|
80
81
|
"@radix-ui/react-avatar": "^1.1.10",
|
|
81
82
|
"@radix-ui/react-checkbox": "^1.3.2",
|
package/scripts/postinstall.js
CHANGED
|
@@ -171,6 +171,32 @@ const main = async () => {
|
|
|
171
171
|
log('🌙 MoonUI Pro Installation', colors.cyan + colors.bright);
|
|
172
172
|
log('═'.repeat(50), colors.gray);
|
|
173
173
|
|
|
174
|
+
// Check for development bypass
|
|
175
|
+
if (process.env.MOONUI_DEV_MODE === 'true' || process.env.MOONUI_SKIP_AUTH === 'true') {
|
|
176
|
+
log('🔧 Development Mode Enabled', colors.yellow + colors.bright);
|
|
177
|
+
log(' Authentication bypassed for local development', colors.yellow);
|
|
178
|
+
log(' This mode should only be used during development', colors.gray);
|
|
179
|
+
log('', '');
|
|
180
|
+
log('✅ MoonUI Pro ready for development!', colors.green);
|
|
181
|
+
log('═'.repeat(50), colors.gray);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check if running on localhost (development)
|
|
186
|
+
const isLocalDev = process.env.NODE_ENV === 'development' ||
|
|
187
|
+
process.cwd().includes('moonui') ||
|
|
188
|
+
fs.existsSync(path.join(process.cwd(), '.moonui-dev'));
|
|
189
|
+
|
|
190
|
+
if (isLocalDev && !process.env.MOONUI_FORCE_AUTH) {
|
|
191
|
+
log('🚀 Local Development Detected', colors.blue + colors.bright);
|
|
192
|
+
log(' Running in development mode without auth', colors.blue);
|
|
193
|
+
log(' To enable auth checks: MOONUI_FORCE_AUTH=true npm install', colors.gray);
|
|
194
|
+
log('', '');
|
|
195
|
+
log('✅ MoonUI Pro ready for development!', colors.green);
|
|
196
|
+
log('═'.repeat(50), colors.gray);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
174
200
|
// Check if this is a CI environment
|
|
175
201
|
const isCI = process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.GITHUB_ACTIONS;
|
|
176
202
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Loader2, MoreHorizontal, AlertTriangle } from 'lucide-react'
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from '../ui/dropdown-menu'
|
|
12
|
+
import {
|
|
13
|
+
AlertDialog,
|
|
14
|
+
AlertDialogAction,
|
|
15
|
+
AlertDialogCancel,
|
|
16
|
+
AlertDialogContent,
|
|
17
|
+
AlertDialogDescription,
|
|
18
|
+
AlertDialogFooter,
|
|
19
|
+
AlertDialogHeader,
|
|
20
|
+
AlertDialogTitle,
|
|
21
|
+
} from '../ui/alert-dialog'
|
|
22
|
+
import { Button } from '../ui/button'
|
|
23
|
+
import { Badge } from '../ui/badge'
|
|
24
|
+
import { cn } from '../../lib/utils'
|
|
25
|
+
|
|
26
|
+
export interface BulkAction<T = any> {
|
|
27
|
+
label: string
|
|
28
|
+
icon?: React.ReactNode
|
|
29
|
+
action: (selectedRows: T[]) => void | Promise<void>
|
|
30
|
+
confirmMessage?: string
|
|
31
|
+
confirmTitle?: string
|
|
32
|
+
variant?: 'default' | 'destructive'
|
|
33
|
+
disabled?: boolean | ((selectedRows: T[]) => boolean)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DataTableBulkActionsProps<T = any> {
|
|
37
|
+
selectedRows: T[]
|
|
38
|
+
actions: BulkAction<T>[]
|
|
39
|
+
onClearSelection?: () => void
|
|
40
|
+
className?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function DataTableBulkActions<T>({
|
|
44
|
+
selectedRows,
|
|
45
|
+
actions,
|
|
46
|
+
onClearSelection,
|
|
47
|
+
className
|
|
48
|
+
}: DataTableBulkActionsProps<T>) {
|
|
49
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
50
|
+
const [pendingAction, setPendingAction] = React.useState<BulkAction<T> | null>(null)
|
|
51
|
+
|
|
52
|
+
const selectedCount = selectedRows.length
|
|
53
|
+
|
|
54
|
+
const handleAction = async (action: BulkAction<T>) => {
|
|
55
|
+
if (action.confirmMessage) {
|
|
56
|
+
setPendingAction(action)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await executeAction(action)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const executeAction = async (action: BulkAction<T>) => {
|
|
64
|
+
setIsLoading(true)
|
|
65
|
+
try {
|
|
66
|
+
await action.action(selectedRows)
|
|
67
|
+
|
|
68
|
+
// Clear selection after successful action
|
|
69
|
+
if (onClearSelection) {
|
|
70
|
+
onClearSelection()
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Bulk action failed:', error)
|
|
74
|
+
// You might want to show an error toast here
|
|
75
|
+
} finally {
|
|
76
|
+
setIsLoading(false)
|
|
77
|
+
setPendingAction(null)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handleConfirm = async () => {
|
|
82
|
+
if (pendingAction) {
|
|
83
|
+
await executeAction(pendingAction)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (selectedCount === 0) {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
94
|
+
{/* Selected count badge */}
|
|
95
|
+
<Badge variant="secondary" className="gap-1">
|
|
96
|
+
<span className="font-semibold">{selectedCount}</span>
|
|
97
|
+
<span>selected</span>
|
|
98
|
+
</Badge>
|
|
99
|
+
|
|
100
|
+
{/* Bulk actions dropdown */}
|
|
101
|
+
<DropdownMenu>
|
|
102
|
+
<DropdownMenuTrigger asChild>
|
|
103
|
+
<Button
|
|
104
|
+
variant="outline"
|
|
105
|
+
size="sm"
|
|
106
|
+
disabled={isLoading}
|
|
107
|
+
className="gap-2"
|
|
108
|
+
>
|
|
109
|
+
{isLoading ? (
|
|
110
|
+
<>
|
|
111
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
112
|
+
Processing...
|
|
113
|
+
</>
|
|
114
|
+
) : (
|
|
115
|
+
<>
|
|
116
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
117
|
+
Bulk Actions
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</Button>
|
|
121
|
+
</DropdownMenuTrigger>
|
|
122
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
123
|
+
{actions.map((action, index) => {
|
|
124
|
+
const isDisabled = typeof action.disabled === 'function'
|
|
125
|
+
? action.disabled(selectedRows)
|
|
126
|
+
: action.disabled
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<DropdownMenuItem
|
|
130
|
+
key={index}
|
|
131
|
+
disabled={isDisabled || isLoading}
|
|
132
|
+
onSelect={() => handleAction(action)}
|
|
133
|
+
className={cn(
|
|
134
|
+
"cursor-pointer",
|
|
135
|
+
action.variant === 'destructive' && "text-destructive focus:text-destructive"
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{action.icon && (
|
|
139
|
+
<span className="mr-2 h-4 w-4">{action.icon}</span>
|
|
140
|
+
)}
|
|
141
|
+
{action.label}
|
|
142
|
+
</DropdownMenuItem>
|
|
143
|
+
)
|
|
144
|
+
})}
|
|
145
|
+
|
|
146
|
+
{onClearSelection && (
|
|
147
|
+
<>
|
|
148
|
+
<DropdownMenuSeparator />
|
|
149
|
+
<DropdownMenuItem
|
|
150
|
+
onSelect={onClearSelection}
|
|
151
|
+
className="cursor-pointer text-muted-foreground"
|
|
152
|
+
>
|
|
153
|
+
Clear selection
|
|
154
|
+
</DropdownMenuItem>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</DropdownMenuContent>
|
|
158
|
+
</DropdownMenu>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Confirmation dialog */}
|
|
162
|
+
<AlertDialog
|
|
163
|
+
open={!!pendingAction}
|
|
164
|
+
onOpenChange={(open) => !open && setPendingAction(null)}
|
|
165
|
+
>
|
|
166
|
+
<AlertDialogContent>
|
|
167
|
+
<AlertDialogHeader>
|
|
168
|
+
<AlertDialogTitle className="flex items-center gap-2">
|
|
169
|
+
{pendingAction?.variant === 'destructive' && (
|
|
170
|
+
<AlertTriangle className="h-5 w-5 text-destructive" />
|
|
171
|
+
)}
|
|
172
|
+
{pendingAction?.confirmTitle || 'Confirm Action'}
|
|
173
|
+
</AlertDialogTitle>
|
|
174
|
+
<AlertDialogDescription>
|
|
175
|
+
{pendingAction?.confirmMessage ||
|
|
176
|
+
`This action will affect ${selectedCount} selected item${selectedCount > 1 ? 's' : ''}. This action cannot be undone.`}
|
|
177
|
+
</AlertDialogDescription>
|
|
178
|
+
</AlertDialogHeader>
|
|
179
|
+
<AlertDialogFooter>
|
|
180
|
+
<AlertDialogCancel disabled={isLoading}>
|
|
181
|
+
Cancel
|
|
182
|
+
</AlertDialogCancel>
|
|
183
|
+
<AlertDialogAction
|
|
184
|
+
onClick={handleConfirm}
|
|
185
|
+
disabled={isLoading}
|
|
186
|
+
className={cn(
|
|
187
|
+
pendingAction?.variant === 'destructive' && "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
{isLoading ? (
|
|
191
|
+
<>
|
|
192
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
193
|
+
Processing...
|
|
194
|
+
</>
|
|
195
|
+
) : (
|
|
196
|
+
'Confirm'
|
|
197
|
+
)}
|
|
198
|
+
</AlertDialogAction>
|
|
199
|
+
</AlertDialogFooter>
|
|
200
|
+
</AlertDialogContent>
|
|
201
|
+
</AlertDialog>
|
|
202
|
+
</>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Check, Columns, Search } from 'lucide-react'
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from '../ui/dropdown-menu'
|
|
13
|
+
import { Button } from '../ui/button'
|
|
14
|
+
import { Input } from '../ui/input'
|
|
15
|
+
import { cn } from '../../lib/utils'
|
|
16
|
+
|
|
17
|
+
interface DataTableColumnToggleProps {
|
|
18
|
+
table: any // Table instance from @tanstack/react-table
|
|
19
|
+
trigger?: React.ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DataTableColumnToggle({ table, trigger }: DataTableColumnToggleProps) {
|
|
23
|
+
const [search, setSearch] = React.useState('')
|
|
24
|
+
|
|
25
|
+
// Get all columns that can be hidden
|
|
26
|
+
const columns = table
|
|
27
|
+
.getAllColumns()
|
|
28
|
+
.filter((column: any) => column.getCanHide())
|
|
29
|
+
|
|
30
|
+
// Filter columns based on search
|
|
31
|
+
const filteredColumns = React.useMemo(() => {
|
|
32
|
+
if (!search) return columns
|
|
33
|
+
|
|
34
|
+
return columns.filter((column: any) => {
|
|
35
|
+
const header = column.columnDef.header
|
|
36
|
+
const headerText = typeof header === 'string'
|
|
37
|
+
? header
|
|
38
|
+
: column.id
|
|
39
|
+
|
|
40
|
+
return headerText.toLowerCase().includes(search.toLowerCase())
|
|
41
|
+
})
|
|
42
|
+
}, [columns, search])
|
|
43
|
+
|
|
44
|
+
// Count visible columns
|
|
45
|
+
const visibleCount = columns.filter((col: any) => col.getIsVisible()).length
|
|
46
|
+
|
|
47
|
+
const handleToggleAll = (visible: boolean) => {
|
|
48
|
+
columns.forEach((column: any) => {
|
|
49
|
+
column.toggleVisibility(visible)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<DropdownMenu>
|
|
55
|
+
<DropdownMenuTrigger asChild>
|
|
56
|
+
{trigger || (
|
|
57
|
+
<Button variant="outline" size="sm" className="gap-2">
|
|
58
|
+
<Columns className="h-4 w-4" />
|
|
59
|
+
Columns
|
|
60
|
+
{visibleCount < columns.length && (
|
|
61
|
+
<span className="ml-1 rounded-full bg-primary/10 px-1.5 py-0.5 text-xs">
|
|
62
|
+
{visibleCount}/{columns.length}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</Button>
|
|
66
|
+
)}
|
|
67
|
+
</DropdownMenuTrigger>
|
|
68
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
69
|
+
<DropdownMenuLabel>Toggle Columns</DropdownMenuLabel>
|
|
70
|
+
|
|
71
|
+
{/* Search input for many columns */}
|
|
72
|
+
{columns.length > 5 && (
|
|
73
|
+
<>
|
|
74
|
+
<div className="px-2 py-1.5">
|
|
75
|
+
<div className="relative">
|
|
76
|
+
<Search className="absolute left-2 top-2.5 h-3 w-3 text-muted-foreground" />
|
|
77
|
+
<Input
|
|
78
|
+
placeholder="Search columns..."
|
|
79
|
+
value={search}
|
|
80
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
81
|
+
className="h-8 pl-7 text-xs"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<DropdownMenuSeparator />
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{/* Quick actions */}
|
|
90
|
+
<div className="flex items-center justify-between px-2 py-1.5">
|
|
91
|
+
<Button
|
|
92
|
+
variant="ghost"
|
|
93
|
+
size="sm"
|
|
94
|
+
className="h-7 text-xs"
|
|
95
|
+
onClick={() => handleToggleAll(true)}
|
|
96
|
+
>
|
|
97
|
+
Show all
|
|
98
|
+
</Button>
|
|
99
|
+
<Button
|
|
100
|
+
variant="ghost"
|
|
101
|
+
size="sm"
|
|
102
|
+
className="h-7 text-xs"
|
|
103
|
+
onClick={() => handleToggleAll(false)}
|
|
104
|
+
>
|
|
105
|
+
Hide all
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<DropdownMenuSeparator />
|
|
110
|
+
|
|
111
|
+
{/* Column list */}
|
|
112
|
+
<div className="max-h-64 overflow-y-auto">
|
|
113
|
+
{filteredColumns.length === 0 ? (
|
|
114
|
+
<div className="px-2 py-4 text-center text-xs text-muted-foreground">
|
|
115
|
+
No columns found
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
filteredColumns.map((column: any) => {
|
|
119
|
+
const isVisible = column.getIsVisible()
|
|
120
|
+
const header = column.columnDef.header
|
|
121
|
+
const headerText = typeof header === 'string'
|
|
122
|
+
? header
|
|
123
|
+
: column.id
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<DropdownMenuItem
|
|
127
|
+
key={column.id}
|
|
128
|
+
className={cn(
|
|
129
|
+
"cursor-pointer",
|
|
130
|
+
!isVisible && "text-muted-foreground"
|
|
131
|
+
)}
|
|
132
|
+
onSelect={(e) => {
|
|
133
|
+
e.preventDefault()
|
|
134
|
+
column.toggleVisibility()
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<div className="flex items-center gap-2 flex-1">
|
|
138
|
+
<div className={cn(
|
|
139
|
+
"flex h-4 w-4 items-center justify-center rounded-sm border",
|
|
140
|
+
isVisible
|
|
141
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
142
|
+
: "border-muted-foreground"
|
|
143
|
+
)}>
|
|
144
|
+
{isVisible && <Check className="h-3 w-3" />}
|
|
145
|
+
</div>
|
|
146
|
+
<span className="truncate">{headerText}</span>
|
|
147
|
+
</div>
|
|
148
|
+
</DropdownMenuItem>
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Summary */}
|
|
155
|
+
{columns.length > 0 && (
|
|
156
|
+
<>
|
|
157
|
+
<DropdownMenuSeparator />
|
|
158
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground text-center">
|
|
159
|
+
{visibleCount} of {columns.length} columns visible
|
|
160
|
+
</div>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</DropdownMenuContent>
|
|
164
|
+
</DropdownMenu>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export utilities for DataTable component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type ExportFormat = 'csv' | 'json' | 'xlsx';
|
|
6
|
+
|
|
7
|
+
export interface ExportOptions {
|
|
8
|
+
filename?: string;
|
|
9
|
+
format: ExportFormat;
|
|
10
|
+
columns?: string[];
|
|
11
|
+
includeHeaders?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert data to CSV format
|
|
16
|
+
*/
|
|
17
|
+
export function dataToCSV<T extends Record<string, any>>(
|
|
18
|
+
data: T[],
|
|
19
|
+
columns?: string[],
|
|
20
|
+
includeHeaders = true
|
|
21
|
+
): string {
|
|
22
|
+
if (data.length === 0) return '';
|
|
23
|
+
|
|
24
|
+
// Get columns from first item if not provided
|
|
25
|
+
const cols = columns || Object.keys(data[0]);
|
|
26
|
+
|
|
27
|
+
// Build CSV content
|
|
28
|
+
const rows: string[] = [];
|
|
29
|
+
|
|
30
|
+
// Add headers
|
|
31
|
+
if (includeHeaders) {
|
|
32
|
+
const headers = cols.map(col => {
|
|
33
|
+
// Handle column names with special characters
|
|
34
|
+
const value = String(col);
|
|
35
|
+
return value.includes(',') || value.includes('"') || value.includes('\n')
|
|
36
|
+
? `"${value.replace(/"/g, '""')}"`
|
|
37
|
+
: value;
|
|
38
|
+
});
|
|
39
|
+
rows.push(headers.join(','));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add data rows
|
|
43
|
+
data.forEach(item => {
|
|
44
|
+
const row = cols.map(col => {
|
|
45
|
+
const value = item[col];
|
|
46
|
+
|
|
47
|
+
// Handle different value types
|
|
48
|
+
if (value === null || value === undefined) return '';
|
|
49
|
+
if (value instanceof Date) return value.toISOString();
|
|
50
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
51
|
+
|
|
52
|
+
const stringValue = String(value);
|
|
53
|
+
return stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')
|
|
54
|
+
? `"${stringValue.replace(/"/g, '""')}"`
|
|
55
|
+
: stringValue;
|
|
56
|
+
});
|
|
57
|
+
rows.push(row.join(','));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return rows.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert data to JSON format
|
|
65
|
+
*/
|
|
66
|
+
export function dataToJSON<T>(data: T[], columns?: string[]): string {
|
|
67
|
+
if (!columns || columns.length === 0) {
|
|
68
|
+
return JSON.stringify(data, null, 2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Filter data to include only specified columns
|
|
72
|
+
const filteredData = data.map(item => {
|
|
73
|
+
const filtered: Record<string, any> = {};
|
|
74
|
+
columns.forEach(col => {
|
|
75
|
+
if (col in (item as any)) {
|
|
76
|
+
filtered[col] = (item as any)[col];
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return filtered;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return JSON.stringify(filteredData, null, 2);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Download data as a file
|
|
87
|
+
*/
|
|
88
|
+
export function downloadFile(content: string, filename: string, mimeType: string): void {
|
|
89
|
+
const blob = new Blob([content], { type: mimeType });
|
|
90
|
+
const url = URL.createObjectURL(blob);
|
|
91
|
+
const link = document.createElement('a');
|
|
92
|
+
|
|
93
|
+
link.href = url;
|
|
94
|
+
link.download = filename;
|
|
95
|
+
document.body.appendChild(link);
|
|
96
|
+
link.click();
|
|
97
|
+
|
|
98
|
+
// Cleanup
|
|
99
|
+
document.body.removeChild(link);
|
|
100
|
+
URL.revokeObjectURL(url);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Export data to the specified format
|
|
105
|
+
*/
|
|
106
|
+
export async function exportData<T extends Record<string, any>>(
|
|
107
|
+
data: T[],
|
|
108
|
+
options: ExportOptions
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
const { format, filename = 'data-export', columns, includeHeaders = true } = options;
|
|
111
|
+
|
|
112
|
+
let content: string;
|
|
113
|
+
let mimeType: string;
|
|
114
|
+
let extension: string;
|
|
115
|
+
|
|
116
|
+
switch (format) {
|
|
117
|
+
case 'csv':
|
|
118
|
+
content = dataToCSV(data, columns, includeHeaders);
|
|
119
|
+
mimeType = 'text/csv;charset=utf-8;';
|
|
120
|
+
extension = 'csv';
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case 'json':
|
|
124
|
+
content = dataToJSON(data, columns);
|
|
125
|
+
mimeType = 'application/json;charset=utf-8;';
|
|
126
|
+
extension = 'json';
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'xlsx':
|
|
130
|
+
// For XLSX, we'll need to use a library like xlsx or exceljs
|
|
131
|
+
// For now, we'll throw an error indicating it's not implemented
|
|
132
|
+
throw new Error('XLSX export requires additional dependencies. Use CSV format instead.');
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const finalFilename = `${filename}-${new Date().toISOString().split('T')[0]}.${extension}`;
|
|
139
|
+
downloadFile(content, finalFilename, mimeType);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get visible columns from column definitions
|
|
144
|
+
*/
|
|
145
|
+
export function getVisibleColumns<T>(
|
|
146
|
+
columns: Array<{ id?: string; accessorKey?: string; header?: any }>,
|
|
147
|
+
columnVisibility: Record<string, boolean>
|
|
148
|
+
): string[] {
|
|
149
|
+
return columns
|
|
150
|
+
.filter(col => {
|
|
151
|
+
const key = col.id || col.accessorKey;
|
|
152
|
+
return key && columnVisibility[key] !== false;
|
|
153
|
+
})
|
|
154
|
+
.map(col => col.id || col.accessorKey!)
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
}
|