@contember/bindx-dataview 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/dist/DataGrid.d.ts +38 -0
- package/dist/DataGrid.d.ts.map +1 -0
- package/dist/DataViewContext.d.ts +39 -0
- package/dist/DataViewContext.d.ts.map +1 -0
- package/dist/DataViewKeyProvider.d.ts +8 -0
- package/dist/DataViewKeyProvider.d.ts.map +1 -0
- package/dist/HasManyDataGrid.d.ts +51 -0
- package/dist/HasManyDataGrid.d.ts.map +1 -0
- package/dist/columnLeaf.d.ts +72 -0
- package/dist/columnLeaf.d.ts.map +1 -0
- package/dist/columnTypes.d.ts +35 -0
- package/dist/columnTypes.d.ts.map +1 -0
- package/dist/columns.d.ts +115 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/contextHooks.d.ts +45 -0
- package/dist/contextHooks.d.ts.map +1 -0
- package/dist/createColumn.d.ts +37 -0
- package/dist/createColumn.d.ts.map +1 -0
- package/dist/createRelationColumn.d.ts +57 -0
- package/dist/createRelationColumn.d.ts.map +1 -0
- package/dist/dataAttribute.d.ts +6 -0
- package/dist/dataAttribute.d.ts.map +1 -0
- package/dist/export.d.ts +46 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/filterComponents.d.ts +127 -0
- package/dist/filterComponents.d.ts.map +1 -0
- package/dist/filterContext.d.ts +10 -0
- package/dist/filterContext.d.ts.map +1 -0
- package/dist/filterHooks.d.ts +74 -0
- package/dist/filterHooks.d.ts.map +1 -0
- package/dist/filterScope.d.ts +54 -0
- package/dist/filterScope.d.ts.map +1 -0
- package/dist/filterWrappers.d.ts +66 -0
- package/dist/filterWrappers.d.ts.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/infiniteLoad.d.ts +32 -0
- package/dist/infiniteLoad.d.ts.map +1 -0
- package/dist/loaderComponents.d.ts +75 -0
- package/dist/loaderComponents.d.ts.map +1 -0
- package/dist/markers.d.ts +36 -0
- package/dist/markers.d.ts.map +1 -0
- package/dist/pagingComponents.d.ts +50 -0
- package/dist/pagingComponents.d.ts.map +1 -0
- package/dist/renderCellValue.d.ts +9 -0
- package/dist/renderCellValue.d.ts.map +1 -0
- package/dist/select/MultiSelect.d.ts +27 -0
- package/dist/select/MultiSelect.d.ts.map +1 -0
- package/dist/select/Select.d.ts +27 -0
- package/dist/select/Select.d.ts.map +1 -0
- package/dist/select/SelectDataView.d.ts +46 -0
- package/dist/select/SelectDataView.d.ts.map +1 -0
- package/dist/select/SelectEachValue.d.ts +20 -0
- package/dist/select/SelectEachValue.d.ts.map +1 -0
- package/dist/select/SelectItemTrigger.d.ts +25 -0
- package/dist/select/SelectItemTrigger.d.ts.map +1 -0
- package/dist/select/SelectOption.d.ts +23 -0
- package/dist/select/SelectOption.d.ts.map +1 -0
- package/dist/select/SelectPlaceholder.d.ts +16 -0
- package/dist/select/SelectPlaceholder.d.ts.map +1 -0
- package/dist/select/index.d.ts +9 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/select/selectContext.d.ts +20 -0
- package/dist/select/selectContext.d.ts.map +1 -0
- package/dist/selectionComponents.d.ts +62 -0
- package/dist/selectionComponents.d.ts.map +1 -0
- package/dist/sortingComponents.d.ts +39 -0
- package/dist/sortingComponents.d.ts.map +1 -0
- package/dist/stateStorage.d.ts +20 -0
- package/dist/stateStorage.d.ts.map +1 -0
- package/dist/useDataGridSetup.d.ts +53 -0
- package/dist/useDataGridSetup.d.ts.map +1 -0
- package/dist/useDataView.d.ts +57 -0
- package/dist/useDataView.d.ts.map +1 -0
- package/dist/useDataViewState.d.ts +89 -0
- package/dist/useDataViewState.d.ts.map +1 -0
- package/package.json +33 -0
- package/src/DataGrid.tsx +173 -0
- package/src/DataViewContext.tsx +56 -0
- package/src/DataViewKeyProvider.tsx +16 -0
- package/src/HasManyDataGrid.tsx +319 -0
- package/src/columnLeaf.ts +148 -0
- package/src/columnTypes.ts +160 -0
- package/src/columns.tsx +306 -0
- package/src/contextHooks.ts +157 -0
- package/src/createColumn.ts +128 -0
- package/src/createRelationColumn.tsx +203 -0
- package/src/dataAttribute.ts +7 -0
- package/src/export.tsx +219 -0
- package/src/filterComponents.tsx +617 -0
- package/src/filterContext.ts +27 -0
- package/src/filterHooks.ts +386 -0
- package/src/filterScope.tsx +89 -0
- package/src/filterWrappers.tsx +187 -0
- package/src/index.ts +356 -0
- package/src/infiniteLoad.tsx +188 -0
- package/src/loaderComponents.tsx +239 -0
- package/src/markers.ts +54 -0
- package/src/pagingComponents.tsx +149 -0
- package/src/select/MultiSelect.tsx +88 -0
- package/src/select/Select.tsx +88 -0
- package/src/select/SelectDataView.tsx +231 -0
- package/src/select/SelectEachValue.tsx +34 -0
- package/src/select/SelectItemTrigger.tsx +51 -0
- package/src/select/SelectOption.tsx +40 -0
- package/src/select/SelectPlaceholder.tsx +22 -0
- package/src/select/index.ts +17 -0
- package/src/select/selectContext.ts +82 -0
- package/src/selectionComponents.tsx +160 -0
- package/src/sortingComponents.tsx +104 -0
- package/src/stateStorage.ts +188 -0
- package/src/useDataGridSetup.ts +230 -0
- package/src/useDataView.ts +148 -0
- package/src/useDataViewState.ts +404 -0
package/src/export.tsx
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataView export functionality.
|
|
3
|
+
*
|
|
4
|
+
* Provides CSV export and a trigger component that downloads all matching data.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <DataViewExportTrigger baseName="articles">
|
|
9
|
+
* <button>Export CSV</button>
|
|
10
|
+
* </DataViewExportTrigger>
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { forwardRef, type ReactElement, useCallback, useState } from 'react'
|
|
15
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
16
|
+
import { composeEventHandlers } from '@radix-ui/primitive'
|
|
17
|
+
import { useDataViewContext } from './DataViewContext.js'
|
|
18
|
+
import { useBindxContext } from '@contember/bindx-react'
|
|
19
|
+
import { buildQueryFromSelection } from '@contember/bindx'
|
|
20
|
+
import type { ListQuery, ListQueryResult } from '@contember/bindx'
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Export Factory Interface
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export interface ExportResult {
|
|
27
|
+
readonly blob: Blob
|
|
28
|
+
readonly extension: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ExportFactoryArgs {
|
|
32
|
+
readonly data: readonly Record<string, unknown>[]
|
|
33
|
+
readonly columns: readonly { name: string; fieldName: string | null }[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExportFactory {
|
|
37
|
+
create(args: ExportFactoryArgs): ExportResult
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// CSV Export Factory
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
export class CsvExportFactory implements ExportFactory {
|
|
45
|
+
create({ data, columns }: ExportFactoryArgs): ExportResult {
|
|
46
|
+
const fieldColumns = columns.filter(c => c.fieldName !== null)
|
|
47
|
+
const headers = fieldColumns.map(c => String(c.name))
|
|
48
|
+
|
|
49
|
+
const rows = data.map(row => {
|
|
50
|
+
return fieldColumns.map(col => {
|
|
51
|
+
const value = col.fieldName ? row[col.fieldName] : ''
|
|
52
|
+
return escapeCsvValue(value)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const csvContent = [
|
|
57
|
+
headers.join(','),
|
|
58
|
+
...rows.map(row => row.join(',')),
|
|
59
|
+
].join('\n')
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
blob: new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }),
|
|
63
|
+
extension: 'csv',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function flattenValue(value: unknown): string {
|
|
69
|
+
if (value === null || value === undefined) return ''
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return value.map(flattenValue).join('; ')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof value === 'object') {
|
|
76
|
+
// Nested relation — flatten by extracting scalar values
|
|
77
|
+
const parts: string[] = []
|
|
78
|
+
for (const val of Object.values(value)) {
|
|
79
|
+
if (val !== null && val !== undefined && typeof val !== 'object') {
|
|
80
|
+
parts.push(String(val))
|
|
81
|
+
} else if (typeof val === 'object') {
|
|
82
|
+
const nested = flattenValue(val)
|
|
83
|
+
if (nested) parts.push(nested)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return parts.join(' ')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return String(value)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeCsvValue(value: unknown): string {
|
|
93
|
+
const str = flattenValue(value)
|
|
94
|
+
if (str === '') return ''
|
|
95
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
96
|
+
return `"${str.replace(/"/g, '""')}"`
|
|
97
|
+
}
|
|
98
|
+
return str
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Export Trigger
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
const defaultExportFactory = new CsvExportFactory()
|
|
106
|
+
|
|
107
|
+
export interface DataViewExportTriggerProps {
|
|
108
|
+
/** Button element */
|
|
109
|
+
children: React.ReactElement
|
|
110
|
+
/** Base file name. Default: entityType-date */
|
|
111
|
+
baseName?: string
|
|
112
|
+
/** Export factory. Default: CSV */
|
|
113
|
+
exportFactory?: ExportFactory
|
|
114
|
+
/** Only export visible columns (respects selection state). Default: false */
|
|
115
|
+
onlyVisible?: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const DataViewExportTrigger = forwardRef<HTMLButtonElement, DataViewExportTriggerProps>(
|
|
119
|
+
({ baseName, exportFactory = defaultExportFactory, onlyVisible = false, ...props }, ref) => {
|
|
120
|
+
const { columns, entityType, filtering, selection, selectionMeta } = useDataViewContext()
|
|
121
|
+
const { adapter } = useBindxContext()
|
|
122
|
+
const [isExporting, setIsExporting] = useState(false)
|
|
123
|
+
|
|
124
|
+
const handleExport = useCallback(async (): Promise<void> => {
|
|
125
|
+
if (!adapter || isExporting) return
|
|
126
|
+
setIsExporting(true)
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const visibleColumns = onlyVisible
|
|
130
|
+
? columns.filter((c, i) => selection.isVisible(c.fieldName ?? `col-${i}`))
|
|
131
|
+
: columns
|
|
132
|
+
|
|
133
|
+
const listQuery: ListQuery = {
|
|
134
|
+
type: 'list',
|
|
135
|
+
entityType,
|
|
136
|
+
filter: filtering.resolvedWhere,
|
|
137
|
+
orderBy: undefined,
|
|
138
|
+
limit: undefined,
|
|
139
|
+
offset: undefined,
|
|
140
|
+
spec: buildQueryFromSelection(selectionMeta),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const results = await adapter.query([listQuery])
|
|
144
|
+
const result = results[0]
|
|
145
|
+
if (!result || result.type !== 'list') {
|
|
146
|
+
console.error('Export failed: unexpected result')
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const exportColumns = visibleColumns
|
|
151
|
+
.filter(c => c.fieldName !== null)
|
|
152
|
+
.map(c => ({ name: String(c.header ?? c.fieldName ?? ''), fieldName: c.fieldName }))
|
|
153
|
+
|
|
154
|
+
const { blob, extension } = exportFactory.create({
|
|
155
|
+
data: (result as ListQueryResult).data,
|
|
156
|
+
columns: exportColumns,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Trigger download
|
|
160
|
+
const fileName = `${baseName ?? `${entityType}-${new Date().toISOString().split('T')[0]}`}.${extension}`
|
|
161
|
+
const url = URL.createObjectURL(blob)
|
|
162
|
+
const link = document.createElement('a')
|
|
163
|
+
link.href = url
|
|
164
|
+
link.download = fileName
|
|
165
|
+
document.body.appendChild(link)
|
|
166
|
+
link.click()
|
|
167
|
+
document.body.removeChild(link)
|
|
168
|
+
URL.revokeObjectURL(url)
|
|
169
|
+
} finally {
|
|
170
|
+
setIsExporting(false)
|
|
171
|
+
}
|
|
172
|
+
}, [adapter, columns, entityType, filtering.resolvedWhere, selectionMeta, exportFactory, baseName, isExporting, onlyVisible, selection])
|
|
173
|
+
|
|
174
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Slot
|
|
178
|
+
ref={ref}
|
|
179
|
+
onClick={composeEventHandlers(onClick, handleExport)}
|
|
180
|
+
disabled={isExporting}
|
|
181
|
+
data-state={isExporting ? 'exporting' : 'idle'}
|
|
182
|
+
{...otherProps}
|
|
183
|
+
/>
|
|
184
|
+
)
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
DataViewExportTrigger.displayName = 'DataViewExportTrigger'
|
|
188
|
+
|
|
189
|
+
// ============================================================================
|
|
190
|
+
// Fetch all data hook (for custom export)
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
export interface FetchAllDataResult {
|
|
194
|
+
data: readonly Record<string, unknown>[]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function useDataViewFetchAllData(): () => Promise<FetchAllDataResult | null> {
|
|
198
|
+
const { entityType, filtering, selectionMeta } = useDataViewContext()
|
|
199
|
+
const { adapter } = useBindxContext()
|
|
200
|
+
|
|
201
|
+
return useCallback(async (): Promise<FetchAllDataResult | null> => {
|
|
202
|
+
if (!adapter) return null
|
|
203
|
+
|
|
204
|
+
const listQuery: ListQuery = {
|
|
205
|
+
type: 'list',
|
|
206
|
+
entityType,
|
|
207
|
+
filter: filtering.resolvedWhere,
|
|
208
|
+
orderBy: undefined,
|
|
209
|
+
limit: undefined,
|
|
210
|
+
offset: undefined,
|
|
211
|
+
spec: buildQueryFromSelection(selectionMeta),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const results = await adapter.query([listQuery])
|
|
215
|
+
const result = results[0]
|
|
216
|
+
if (!result || result.type !== 'list') return null
|
|
217
|
+
return { data: (result as ListQueryResult).data }
|
|
218
|
+
}, [adapter, entityType, filtering.resolvedWhere, selectionMeta])
|
|
219
|
+
}
|