@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.
Files changed (114) hide show
  1. package/dist/DataGrid.d.ts +38 -0
  2. package/dist/DataGrid.d.ts.map +1 -0
  3. package/dist/DataViewContext.d.ts +39 -0
  4. package/dist/DataViewContext.d.ts.map +1 -0
  5. package/dist/DataViewKeyProvider.d.ts +8 -0
  6. package/dist/DataViewKeyProvider.d.ts.map +1 -0
  7. package/dist/HasManyDataGrid.d.ts +51 -0
  8. package/dist/HasManyDataGrid.d.ts.map +1 -0
  9. package/dist/columnLeaf.d.ts +72 -0
  10. package/dist/columnLeaf.d.ts.map +1 -0
  11. package/dist/columnTypes.d.ts +35 -0
  12. package/dist/columnTypes.d.ts.map +1 -0
  13. package/dist/columns.d.ts +115 -0
  14. package/dist/columns.d.ts.map +1 -0
  15. package/dist/contextHooks.d.ts +45 -0
  16. package/dist/contextHooks.d.ts.map +1 -0
  17. package/dist/createColumn.d.ts +37 -0
  18. package/dist/createColumn.d.ts.map +1 -0
  19. package/dist/createRelationColumn.d.ts +57 -0
  20. package/dist/createRelationColumn.d.ts.map +1 -0
  21. package/dist/dataAttribute.d.ts +6 -0
  22. package/dist/dataAttribute.d.ts.map +1 -0
  23. package/dist/export.d.ts +46 -0
  24. package/dist/export.d.ts.map +1 -0
  25. package/dist/filterComponents.d.ts +127 -0
  26. package/dist/filterComponents.d.ts.map +1 -0
  27. package/dist/filterContext.d.ts +10 -0
  28. package/dist/filterContext.d.ts.map +1 -0
  29. package/dist/filterHooks.d.ts +74 -0
  30. package/dist/filterHooks.d.ts.map +1 -0
  31. package/dist/filterScope.d.ts +54 -0
  32. package/dist/filterScope.d.ts.map +1 -0
  33. package/dist/filterWrappers.d.ts +66 -0
  34. package/dist/filterWrappers.d.ts.map +1 -0
  35. package/dist/index.d.ts +28 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/infiniteLoad.d.ts +32 -0
  38. package/dist/infiniteLoad.d.ts.map +1 -0
  39. package/dist/loaderComponents.d.ts +75 -0
  40. package/dist/loaderComponents.d.ts.map +1 -0
  41. package/dist/markers.d.ts +36 -0
  42. package/dist/markers.d.ts.map +1 -0
  43. package/dist/pagingComponents.d.ts +50 -0
  44. package/dist/pagingComponents.d.ts.map +1 -0
  45. package/dist/renderCellValue.d.ts +9 -0
  46. package/dist/renderCellValue.d.ts.map +1 -0
  47. package/dist/select/MultiSelect.d.ts +27 -0
  48. package/dist/select/MultiSelect.d.ts.map +1 -0
  49. package/dist/select/Select.d.ts +27 -0
  50. package/dist/select/Select.d.ts.map +1 -0
  51. package/dist/select/SelectDataView.d.ts +46 -0
  52. package/dist/select/SelectDataView.d.ts.map +1 -0
  53. package/dist/select/SelectEachValue.d.ts +20 -0
  54. package/dist/select/SelectEachValue.d.ts.map +1 -0
  55. package/dist/select/SelectItemTrigger.d.ts +25 -0
  56. package/dist/select/SelectItemTrigger.d.ts.map +1 -0
  57. package/dist/select/SelectOption.d.ts +23 -0
  58. package/dist/select/SelectOption.d.ts.map +1 -0
  59. package/dist/select/SelectPlaceholder.d.ts +16 -0
  60. package/dist/select/SelectPlaceholder.d.ts.map +1 -0
  61. package/dist/select/index.d.ts +9 -0
  62. package/dist/select/index.d.ts.map +1 -0
  63. package/dist/select/selectContext.d.ts +20 -0
  64. package/dist/select/selectContext.d.ts.map +1 -0
  65. package/dist/selectionComponents.d.ts +62 -0
  66. package/dist/selectionComponents.d.ts.map +1 -0
  67. package/dist/sortingComponents.d.ts +39 -0
  68. package/dist/sortingComponents.d.ts.map +1 -0
  69. package/dist/stateStorage.d.ts +20 -0
  70. package/dist/stateStorage.d.ts.map +1 -0
  71. package/dist/useDataGridSetup.d.ts +53 -0
  72. package/dist/useDataGridSetup.d.ts.map +1 -0
  73. package/dist/useDataView.d.ts +57 -0
  74. package/dist/useDataView.d.ts.map +1 -0
  75. package/dist/useDataViewState.d.ts +89 -0
  76. package/dist/useDataViewState.d.ts.map +1 -0
  77. package/package.json +33 -0
  78. package/src/DataGrid.tsx +173 -0
  79. package/src/DataViewContext.tsx +56 -0
  80. package/src/DataViewKeyProvider.tsx +16 -0
  81. package/src/HasManyDataGrid.tsx +319 -0
  82. package/src/columnLeaf.ts +148 -0
  83. package/src/columnTypes.ts +160 -0
  84. package/src/columns.tsx +306 -0
  85. package/src/contextHooks.ts +157 -0
  86. package/src/createColumn.ts +128 -0
  87. package/src/createRelationColumn.tsx +203 -0
  88. package/src/dataAttribute.ts +7 -0
  89. package/src/export.tsx +219 -0
  90. package/src/filterComponents.tsx +617 -0
  91. package/src/filterContext.ts +27 -0
  92. package/src/filterHooks.ts +386 -0
  93. package/src/filterScope.tsx +89 -0
  94. package/src/filterWrappers.tsx +187 -0
  95. package/src/index.ts +356 -0
  96. package/src/infiniteLoad.tsx +188 -0
  97. package/src/loaderComponents.tsx +239 -0
  98. package/src/markers.ts +54 -0
  99. package/src/pagingComponents.tsx +149 -0
  100. package/src/select/MultiSelect.tsx +88 -0
  101. package/src/select/Select.tsx +88 -0
  102. package/src/select/SelectDataView.tsx +231 -0
  103. package/src/select/SelectEachValue.tsx +34 -0
  104. package/src/select/SelectItemTrigger.tsx +51 -0
  105. package/src/select/SelectOption.tsx +40 -0
  106. package/src/select/SelectPlaceholder.tsx +22 -0
  107. package/src/select/index.ts +17 -0
  108. package/src/select/selectContext.ts +82 -0
  109. package/src/selectionComponents.tsx +160 -0
  110. package/src/sortingComponents.tsx +104 -0
  111. package/src/stateStorage.ts +188 -0
  112. package/src/useDataGridSetup.ts +230 -0
  113. package/src/useDataView.ts +148 -0
  114. 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
+ }