@vuu-ui/vuu-table-extras 3.3.1 → 3.3.3
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 +32 -0
- package/package.json +12 -12
- package/src/csv-export/export-utils.js +221 -0
- package/src/csv-export/index.js +2 -0
- package/src/csv-export/useCsvExport.js +93 -0
- package/src/csv-upload/parse/csv-schema-validation.js +4 -1
- package/src/csv-upload/useCsvUpload.js +62 -5
- package/src/index.js +1 -0
- package/types/csv-export/export-utils.d.ts +42 -0
- package/types/csv-export/index.d.ts +2 -0
- package/types/csv-export/useCsvExport.d.ts +14 -0
- package/types/csv-upload/CsvUpload.d.ts +9 -1
- package/types/csv-upload/useCsvUpload.d.ts +10 -1
- package/types/index.d.ts +2 -1
- package/types/inline-add-row/useInlineAddRow.d.ts +1 -0
package/README.md
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @vuu-ui/vuu-table-extras
|
|
2
|
+
|
|
3
|
+
Extended components and utilities for Vuu tables, including CSV export, CSV upload and validation, column settings, column pickers, custom cell renderers, and table footer controls.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
### [CSV Export](./src/csv-export/README.md)
|
|
10
|
+
Standalone utilities and a React hook for exporting table data and template schemas to CSV:
|
|
11
|
+
- `exportToCsv(dataSource, options)`: Subscribes to a session table, collects rows in memory in index order, formats values, and triggers browser downloads.
|
|
12
|
+
- `exportCsvTemplate(dataSource, options)`: Generates a header-only CSV template for import workflows.
|
|
13
|
+
- `useCsvExport(dataSource)`: React hook with built-in `isExporting` state, error handling, and helper methods.
|
|
14
|
+
|
|
15
|
+
### [CSV Upload](./src/csv-upload/README.md)
|
|
16
|
+
Dialog and hook workflow for importing CSV data into Vuu tables via server-side session tables:
|
|
17
|
+
- `CsvUpload`: Complete dialog component with file drag-and-drop, client-side validation, error summaries, and staging controls.
|
|
18
|
+
- `useCsvUpload`: Headless hook managing file parsing, schema validation, session table staging, and RPC row batching.
|
|
19
|
+
- `DataUploadPreview`: Inline editable table preview for inspecting and correcting invalid staged rows before commit.
|
|
20
|
+
|
|
21
|
+
### Column Management
|
|
22
|
+
- `ColumnMenu` & `useColumnActions`: Context menus for sorting, grouping, filtering, and column configuration.
|
|
23
|
+
- `ColumnPicker` & `useTableColumnPicker`: Dialog and drawer controls for selecting and reordering visible columns.
|
|
24
|
+
- `CalculatedColumnPanel`: UI panel for defining custom expression-based columns.
|
|
25
|
+
|
|
26
|
+
### Cell Renderers & Formatters
|
|
27
|
+
- Pre-built cell renderers including `BackgroundCell`, `DropdownCell`, `IconButtonCell`, and undo support.
|
|
28
|
+
- Cell edit validators for ensuring valid data entry.
|
|
29
|
+
|
|
30
|
+
### Table Footer & Status Controls
|
|
31
|
+
- `TableFooter` & `TableFooterTray`: Status bar and pagination trays.
|
|
32
|
+
- `DataSourceStats`: Live indicators for row count, connection state, freeze status, and filter metrics.
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.3.
|
|
2
|
+
"version": "3.3.3",
|
|
3
3
|
"author": "heswell",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
@@ -11,19 +11,19 @@
|
|
|
11
11
|
"lezer-generate:column": "lezer-generator --output ./src/column-expression-input/column-language-parser/generated/column-parser.js ./src/column-expression-input/column-language-parser/grammar/column.grammar"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@vuu-ui/vuu-filter-types": "3.3.
|
|
15
|
-
"@vuu-ui/vuu-protocol-types": "3.3.
|
|
14
|
+
"@vuu-ui/vuu-filter-types": "3.3.3",
|
|
15
|
+
"@vuu-ui/vuu-protocol-types": "3.3.3"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@vuu-ui/vuu-codemirror": "3.3.
|
|
19
|
-
"@vuu-ui/vuu-data-editing": "3.3.
|
|
20
|
-
"@vuu-ui/vuu-data-react": "3.3.
|
|
21
|
-
"@vuu-ui/vuu-data-types": "3.3.
|
|
22
|
-
"@vuu-ui/vuu-table-types": "3.3.
|
|
23
|
-
"@vuu-ui/vuu-popups": "3.3.
|
|
24
|
-
"@vuu-ui/vuu-table": "3.3.
|
|
25
|
-
"@vuu-ui/vuu-utils": "3.3.
|
|
26
|
-
"@vuu-ui/vuu-ui-controls": "3.3.
|
|
18
|
+
"@vuu-ui/vuu-codemirror": "3.3.3",
|
|
19
|
+
"@vuu-ui/vuu-data-editing": "3.3.3",
|
|
20
|
+
"@vuu-ui/vuu-data-react": "3.3.3",
|
|
21
|
+
"@vuu-ui/vuu-data-types": "3.3.3",
|
|
22
|
+
"@vuu-ui/vuu-table-types": "3.3.3",
|
|
23
|
+
"@vuu-ui/vuu-popups": "3.3.3",
|
|
24
|
+
"@vuu-ui/vuu-table": "3.3.3",
|
|
25
|
+
"@vuu-ui/vuu-utils": "3.3.3",
|
|
26
|
+
"@vuu-ui/vuu-ui-controls": "3.3.3",
|
|
27
27
|
"@lezer/lr": "1.4.2",
|
|
28
28
|
"@salt-ds/core": "1.54.1",
|
|
29
29
|
"@salt-ds/lab": "1.0.0-alpha.83",
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Range, isSessionTable, metadataKeys } from "@vuu-ui/vuu-utils";
|
|
2
|
+
const EXPORT_EXCLUDED_COLUMNS = new Set([
|
|
3
|
+
"vuuMsg",
|
|
4
|
+
"vuuAction",
|
|
5
|
+
"vuuRowNum"
|
|
6
|
+
]);
|
|
7
|
+
const MAX_EXPORT_ROWS = 10000;
|
|
8
|
+
const CHUNK_SIZE = 1000;
|
|
9
|
+
const DEFAULT_EXPORT_TIMEOUT = 30000;
|
|
10
|
+
const DEFAULT_TEMPLATE_TIMEOUT = 10000;
|
|
11
|
+
const csvCell = (value)=>{
|
|
12
|
+
const s = null == value ? "" : String(value);
|
|
13
|
+
return s.includes(",") || s.includes('"') || s.includes("\n") || s.includes("\r") ? `"${s.replace(/"/g, '""')}"` : s;
|
|
14
|
+
};
|
|
15
|
+
const triggerCsvDownload = (csv, filename)=>{
|
|
16
|
+
const blob = new Blob([
|
|
17
|
+
csv
|
|
18
|
+
], {
|
|
19
|
+
type: "text/csv;charset=utf-8;"
|
|
20
|
+
});
|
|
21
|
+
const url = URL.createObjectURL(blob);
|
|
22
|
+
const a = document.createElement("a");
|
|
23
|
+
a.href = url;
|
|
24
|
+
a.download = filename;
|
|
25
|
+
a.style.display = "none";
|
|
26
|
+
document.body.appendChild(a);
|
|
27
|
+
a.click();
|
|
28
|
+
document.body.removeChild(a);
|
|
29
|
+
setTimeout(()=>URL.revokeObjectURL(url), 10000);
|
|
30
|
+
};
|
|
31
|
+
const exportCsvTemplate = async (dataSource, options = {})=>{
|
|
32
|
+
const { filename = "template.csv", excludeColumns = [], columns, overrides, timeout = DEFAULT_TEMPLATE_TIMEOUT, onError, onSuccess } = options;
|
|
33
|
+
const sessionOverrides = overrides ?? (columns ? {
|
|
34
|
+
columns
|
|
35
|
+
} : void 0);
|
|
36
|
+
if (!isSessionTable(dataSource.table) && dataSource.createSessionDataSource && "initialising" !== dataSource.status && "unsubscribed" !== dataSource.status) try {
|
|
37
|
+
const sessionDataSource = await dataSource.createSessionDataSource("Empty", "export", sessionOverrides);
|
|
38
|
+
if (sessionDataSource) return new Promise((resolve, reject)=>{
|
|
39
|
+
let timer;
|
|
40
|
+
const cleanup = ()=>{
|
|
41
|
+
if (timer) clearTimeout(timer);
|
|
42
|
+
sessionDataSource.unsubscribe();
|
|
43
|
+
};
|
|
44
|
+
if (timeout > 0) timer = setTimeout(()=>{
|
|
45
|
+
cleanup();
|
|
46
|
+
const err = new Error(`exportCsvTemplate: timed out after ${timeout}ms waiting for subscription`);
|
|
47
|
+
if (onError) {
|
|
48
|
+
onError(err);
|
|
49
|
+
resolve();
|
|
50
|
+
} else reject(err);
|
|
51
|
+
}, timeout);
|
|
52
|
+
const excluded = new Set([
|
|
53
|
+
...EXPORT_EXCLUDED_COLUMNS,
|
|
54
|
+
...excludeColumns
|
|
55
|
+
]);
|
|
56
|
+
sessionDataSource.subscribe({
|
|
57
|
+
range: Range(0, 0),
|
|
58
|
+
columns: sessionOverrides?.columns
|
|
59
|
+
}, (message)=>{
|
|
60
|
+
if ("subscribed" === message.type) {
|
|
61
|
+
const { columns: subColumns } = message;
|
|
62
|
+
const exportCols = subColumns.filter((name)=>!excluded.has(name));
|
|
63
|
+
const header = exportCols.map(csvCell).join(",");
|
|
64
|
+
triggerCsvDownload(`${header}\r\n`, filename);
|
|
65
|
+
cleanup();
|
|
66
|
+
onSuccess?.();
|
|
67
|
+
resolve();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.warn("[exportCsvTemplate] createSessionDataSource failed, falling back to tableSchema", error);
|
|
73
|
+
}
|
|
74
|
+
const schema = dataSource.tableSchema;
|
|
75
|
+
const targetColumns = columns ?? overrides?.columns;
|
|
76
|
+
if (void 0 !== targetColumns) {
|
|
77
|
+
if (schema) {
|
|
78
|
+
const schemaColumnNames = new Set(schema.columns.map((col)=>col.name));
|
|
79
|
+
const unknown = targetColumns.filter((name)=>!schemaColumnNames.has(name));
|
|
80
|
+
if (unknown.length > 0) console.warn(`[exportCsvTemplate] unknown column(s) in view tableSchema: ${unknown.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
} else if (!schema) {
|
|
83
|
+
const error = new Error("exportCsvTemplate: tableSchema not available on dataSource");
|
|
84
|
+
if (onError) return void onError(error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const excluded = new Set([
|
|
88
|
+
...EXPORT_EXCLUDED_COLUMNS,
|
|
89
|
+
...excludeColumns
|
|
90
|
+
]);
|
|
91
|
+
const exportCols = targetColumns ? targetColumns.filter((name)=>!excluded.has(name)) : schema?.columns.filter((col)=>!excluded.has(col.name)).map((col)=>col.name) ?? [];
|
|
92
|
+
if (0 === exportCols.length) {
|
|
93
|
+
const error = new Error("exportCsvTemplate: no columns available for export");
|
|
94
|
+
if (onError) return void onError(error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
const header = exportCols.map(csvCell).join(",");
|
|
98
|
+
triggerCsvDownload(`${header}\r\n`, filename);
|
|
99
|
+
onSuccess?.();
|
|
100
|
+
};
|
|
101
|
+
const exportSessionTableToCsv = async (dataSource, options = {})=>{
|
|
102
|
+
const { filename = "export.csv", copyOption = "All", excludeColumns = [], maxRows = MAX_EXPORT_ROWS, columnDescriptors, overrides, timeout = DEFAULT_EXPORT_TIMEOUT, onError, onSuccess } = options;
|
|
103
|
+
const isRemote = !!dataSource.isRemote;
|
|
104
|
+
if (isRemote && !isSessionTable(dataSource.table) && ("initialising" === dataSource.status || "unsubscribed" === dataSource.status)) {
|
|
105
|
+
const error = new Error(`exportSessionTableToCsv: dataSource must be subscribed before exporting (current status: "${dataSource.status}")`);
|
|
106
|
+
if (onError) return void onError(error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
let sessionDataSource;
|
|
110
|
+
if (!isSessionTable(dataSource.table) && dataSource.createSessionDataSource) try {
|
|
111
|
+
sessionDataSource = await dataSource.createSessionDataSource(copyOption, "export", overrides);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
114
|
+
if (onError) return void onError(error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
else sessionDataSource = dataSource;
|
|
118
|
+
if (!sessionDataSource) {
|
|
119
|
+
const error = new Error("exportSessionTableToCsv: unable to obtain sessionDataSource");
|
|
120
|
+
if (onError) return void onError(error);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const activeSessionDataSource = sessionDataSource;
|
|
124
|
+
const excluded = new Set([
|
|
125
|
+
...EXPORT_EXCLUDED_COLUMNS,
|
|
126
|
+
...excludeColumns
|
|
127
|
+
]);
|
|
128
|
+
let exportCols = [];
|
|
129
|
+
let colNameToRowIdx = {};
|
|
130
|
+
let descriptorMap = {};
|
|
131
|
+
let totalSize = 0;
|
|
132
|
+
let nextRequestedFrom = 0;
|
|
133
|
+
const collectedRows = new Map();
|
|
134
|
+
return new Promise((resolve, reject)=>{
|
|
135
|
+
let timer;
|
|
136
|
+
const cleanup = ()=>{
|
|
137
|
+
if (timer) clearTimeout(timer);
|
|
138
|
+
activeSessionDataSource.unsubscribe();
|
|
139
|
+
};
|
|
140
|
+
const fail = (err)=>{
|
|
141
|
+
cleanup();
|
|
142
|
+
if (onError) {
|
|
143
|
+
onError(err);
|
|
144
|
+
resolve();
|
|
145
|
+
} else reject(err);
|
|
146
|
+
};
|
|
147
|
+
if (timeout > 0) timer = setTimeout(()=>{
|
|
148
|
+
fail(new Error(`exportToCsv: export timed out after ${timeout}ms waiting for data`));
|
|
149
|
+
}, timeout);
|
|
150
|
+
const handleMessage = (message)=>{
|
|
151
|
+
if ("subscribed" === message.type) {
|
|
152
|
+
const { columns } = message;
|
|
153
|
+
if (columnDescriptors) {
|
|
154
|
+
const subscribedSet = new Set(columns);
|
|
155
|
+
const unknown = columnDescriptors.filter((d)=>!excluded.has(d.name) && !subscribedSet.has(d.name)).map((d)=>d.name);
|
|
156
|
+
if (unknown.length > 0) return void fail(new Error(`exportToCsv: unknown column(s) in columnDescriptors: ${unknown.join(", ")}`));
|
|
157
|
+
descriptorMap = Object.fromEntries(columnDescriptors.map((d)=>[
|
|
158
|
+
d.name,
|
|
159
|
+
d
|
|
160
|
+
]));
|
|
161
|
+
}
|
|
162
|
+
exportCols = columns.filter((name)=>!excluded.has(name));
|
|
163
|
+
colNameToRowIdx = Object.fromEntries(columns.map((name, i)=>[
|
|
164
|
+
name,
|
|
165
|
+
metadataKeys.count + i
|
|
166
|
+
]));
|
|
167
|
+
} else if ("viewport-update" === message.type) {
|
|
168
|
+
if ("size-only" === message.mode) {
|
|
169
|
+
totalSize = message.size ?? 0;
|
|
170
|
+
if (0 === totalSize) {
|
|
171
|
+
cleanup();
|
|
172
|
+
const header = exportCols.map((name)=>csvCell(descriptorMap[name]?.label ?? name)).join(",");
|
|
173
|
+
triggerCsvDownload(`${header}\r\n`, filename);
|
|
174
|
+
onSuccess?.();
|
|
175
|
+
resolve();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (totalSize > maxRows) return void fail(new Error(`exportToCsv: row count ${totalSize} exceeds the ${maxRows} row limit`));
|
|
179
|
+
nextRequestedFrom = Math.min(CHUNK_SIZE, totalSize);
|
|
180
|
+
activeSessionDataSource.range = Range(0, nextRequestedFrom);
|
|
181
|
+
} else if ("batch" === message.mode && message.rows) {
|
|
182
|
+
for (const row of message.rows){
|
|
183
|
+
const rowIndex = row[metadataKeys.IDX];
|
|
184
|
+
if ("number" == typeof rowIndex) collectedRows.set(rowIndex, row);
|
|
185
|
+
}
|
|
186
|
+
if (void 0 !== message.size) totalSize = message.size;
|
|
187
|
+
if (collectedRows.size >= totalSize) {
|
|
188
|
+
cleanup();
|
|
189
|
+
const lines = [
|
|
190
|
+
exportCols.map((name)=>csvCell(descriptorMap[name]?.label ?? name)).join(",")
|
|
191
|
+
];
|
|
192
|
+
for(let i = 0; i < totalSize; i++){
|
|
193
|
+
const row = collectedRows.get(i);
|
|
194
|
+
if (row) lines.push(exportCols.map((c)=>{
|
|
195
|
+
const raw = row[colNameToRowIdx[c]];
|
|
196
|
+
const formatter = descriptorMap[c]?.exportFormatter;
|
|
197
|
+
return csvCell(formatter ? formatter(raw) : raw);
|
|
198
|
+
}).join(","));
|
|
199
|
+
}
|
|
200
|
+
triggerCsvDownload(lines.join("\r\n"), filename);
|
|
201
|
+
onSuccess?.();
|
|
202
|
+
resolve();
|
|
203
|
+
} else if (collectedRows.size >= nextRequestedFrom && nextRequestedFrom < totalSize) {
|
|
204
|
+
const nextTo = Math.min(nextRequestedFrom + CHUNK_SIZE, totalSize);
|
|
205
|
+
const requestedFrom = nextRequestedFrom;
|
|
206
|
+
nextRequestedFrom = nextTo;
|
|
207
|
+
setTimeout(()=>{
|
|
208
|
+
activeSessionDataSource.range = Range(requestedFrom, nextTo);
|
|
209
|
+
}, 0);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
activeSessionDataSource.subscribe({
|
|
215
|
+
range: Range(0, 0),
|
|
216
|
+
columns: overrides?.columns
|
|
217
|
+
}, handleMessage);
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
const exportToCsv = async (dataSource, options)=>exportSessionTableToCsv(dataSource, options);
|
|
221
|
+
export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { exportCsvTemplate, exportToCsv } from "./export-utils.js";
|
|
3
|
+
function useCsvExport(propsOrDataSource) {
|
|
4
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
const config = propsOrDataSource && "table" in propsOrDataSource ? {
|
|
7
|
+
dataSource: propsOrDataSource
|
|
8
|
+
} : propsOrDataSource ?? {};
|
|
9
|
+
const { dataSource: defaultDataSource, onError, onSuccess } = config;
|
|
10
|
+
const exportCsv = useCallback(async (options, overrideDataSource)=>{
|
|
11
|
+
const activeDataSource = overrideDataSource ?? defaultDataSource;
|
|
12
|
+
if (!activeDataSource) {
|
|
13
|
+
const err = new Error("useCsvExport: dataSource is required to export to CSV");
|
|
14
|
+
setError(err);
|
|
15
|
+
onError?.(err);
|
|
16
|
+
options?.onError?.(err);
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
setIsExporting(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
try {
|
|
22
|
+
await exportToCsv(activeDataSource, {
|
|
23
|
+
...options,
|
|
24
|
+
onError: (err)=>{
|
|
25
|
+
setError(err);
|
|
26
|
+
onError?.(err);
|
|
27
|
+
options?.onError?.(err);
|
|
28
|
+
},
|
|
29
|
+
onSuccess: ()=>{
|
|
30
|
+
onSuccess?.();
|
|
31
|
+
options?.onSuccess?.();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const catchedError = err instanceof Error ? err : new Error(String(err));
|
|
36
|
+
setError(catchedError);
|
|
37
|
+
onError?.(catchedError);
|
|
38
|
+
options?.onError?.(catchedError);
|
|
39
|
+
throw catchedError;
|
|
40
|
+
} finally{
|
|
41
|
+
setIsExporting(false);
|
|
42
|
+
}
|
|
43
|
+
}, [
|
|
44
|
+
defaultDataSource,
|
|
45
|
+
onError,
|
|
46
|
+
onSuccess
|
|
47
|
+
]);
|
|
48
|
+
const exportTemplate = useCallback(async (options, overrideDataSource)=>{
|
|
49
|
+
const activeDataSource = overrideDataSource ?? defaultDataSource;
|
|
50
|
+
if (!activeDataSource) {
|
|
51
|
+
const err = new Error("useCsvExport: dataSource is required to export CSV template");
|
|
52
|
+
setError(err);
|
|
53
|
+
onError?.(err);
|
|
54
|
+
options?.onError?.(err);
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
setIsExporting(true);
|
|
58
|
+
setError(null);
|
|
59
|
+
try {
|
|
60
|
+
await exportCsvTemplate(activeDataSource, {
|
|
61
|
+
...options,
|
|
62
|
+
onError: (err)=>{
|
|
63
|
+
setError(err);
|
|
64
|
+
onError?.(err);
|
|
65
|
+
options?.onError?.(err);
|
|
66
|
+
},
|
|
67
|
+
onSuccess: ()=>{
|
|
68
|
+
onSuccess?.();
|
|
69
|
+
options?.onSuccess?.();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const catchedError = err instanceof Error ? err : new Error(String(err));
|
|
74
|
+
setError(catchedError);
|
|
75
|
+
onError?.(catchedError);
|
|
76
|
+
options?.onError?.(catchedError);
|
|
77
|
+
throw catchedError;
|
|
78
|
+
} finally{
|
|
79
|
+
setIsExporting(false);
|
|
80
|
+
}
|
|
81
|
+
}, [
|
|
82
|
+
defaultDataSource,
|
|
83
|
+
onError,
|
|
84
|
+
onSuccess
|
|
85
|
+
]);
|
|
86
|
+
return {
|
|
87
|
+
isExporting,
|
|
88
|
+
error,
|
|
89
|
+
exportCsv,
|
|
90
|
+
exportTemplate
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export { useCsvExport };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { getTypedValue } from "@vuu-ui/vuu-utils";
|
|
2
2
|
import { CsvValidationErrorEnum, addCsvFileError, addCsvRowError, createCsvErrorState } from "./csv-errors.js";
|
|
3
3
|
import { CSV_FIRST_DATA_ROW_NUMBER, MAX_ROWS_IN_CSV } from "./csv-constants.js";
|
|
4
|
+
const INTERNAL_KEY_COLUMNS = new Set([
|
|
5
|
+
"vuuRowNum"
|
|
6
|
+
]);
|
|
4
7
|
const validateCsvAgainstSchema = (parsed, tableSchema, options)=>{
|
|
5
8
|
const schemaColumns = new Map(tableSchema.columns.map((col)=>[
|
|
6
9
|
col.name,
|
|
@@ -8,7 +11,7 @@ const validateCsvAgainstSchema = (parsed, tableSchema, options)=>{
|
|
|
8
11
|
]));
|
|
9
12
|
const maxRows = options?.maxRows ?? MAX_ROWS_IN_CSV;
|
|
10
13
|
const errorState = createCsvErrorState();
|
|
11
|
-
if (!parsed.header.includes(tableSchema.key)) addCsvFileError(errorState, tableSchema.key, CsvValidationErrorEnum.MISSING_KEY_COLUMN, `CSV must include key column '${tableSchema.key}'.`);
|
|
14
|
+
if (tableSchema.key && !INTERNAL_KEY_COLUMNS.has(tableSchema.key) && !parsed.header.includes(tableSchema.key)) addCsvFileError(errorState, tableSchema.key, CsvValidationErrorEnum.MISSING_KEY_COLUMN, `CSV must include key column '${tableSchema.key}'.`);
|
|
12
15
|
parsed.header.forEach((column)=>{
|
|
13
16
|
if (!schemaColumns.has(column)) addCsvFileError(errorState, column, CsvValidationErrorEnum.UNKNOWN_COLUMN, `Column ${column} is not present in table schema.`, column);
|
|
14
17
|
});
|
|
@@ -1,21 +1,67 @@
|
|
|
1
1
|
import { EditSession } from "@vuu-ui/vuu-data-editing";
|
|
2
|
-
import { isRpcError, isSessionTable } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { isRpcError, isSessionTable, useData } from "@vuu-ui/vuu-utils";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { parseCsv } from "./parse/csv-parse.js";
|
|
5
5
|
import { validateCsvAgainstSchema } from "./parse/csv-schema-validation.js";
|
|
6
6
|
import { buildRowErrorMessage, createUploadError, hasFileParseErrors, isCsvParseError, mergeValidationWithParseErrors, toErrorMessage } from "./parse/csv-upload-utils.js";
|
|
7
7
|
import { CSV_FIRST_DATA_ROW_NUMBER } from "./parse/csv-constants.js";
|
|
8
|
-
const useCsvUpload = ({ dataSource, importMode = "direct", onImportSessionEnded, onImportSessionStarted, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, rowDefaults })=>{
|
|
8
|
+
const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importTable, onImportSessionEnded, onImportSessionStarted, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, rowDefaults })=>{
|
|
9
|
+
const { getServerAPI } = useData();
|
|
9
10
|
const [validation, setValidation] = useState();
|
|
10
11
|
const [sessionTable, setSessionTable] = useState();
|
|
11
12
|
const [isProcessingFile, setIsProcessingFile] = useState(false);
|
|
12
13
|
const [isImporting, setIsImporting] = useState(false);
|
|
14
|
+
const [fetchedImportSchema, setFetchedImportSchema] = useState();
|
|
15
|
+
useEffect(()=>{
|
|
16
|
+
if (importSchema || void 0 === importTable) return void setFetchedImportSchema(void 0);
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
(async ()=>{
|
|
19
|
+
try {
|
|
20
|
+
const server = await getServerAPI();
|
|
21
|
+
const tableSchema = await server.getTableSchema(importTable);
|
|
22
|
+
if (!cancelled) setFetchedImportSchema(tableSchema);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error("[useCsvUpload] failed to fetch import table schema", error);
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
return ()=>{
|
|
28
|
+
cancelled = true;
|
|
29
|
+
};
|
|
30
|
+
}, [
|
|
31
|
+
getServerAPI,
|
|
32
|
+
importSchema,
|
|
33
|
+
importTable
|
|
34
|
+
]);
|
|
35
|
+
const resolvedImportSchema = importSchema ?? fetchedImportSchema;
|
|
36
|
+
const sessionOverrides = useMemo(()=>{
|
|
37
|
+
const columns = resolvedImportSchema?.columns.map(({ name })=>name);
|
|
38
|
+
return columns || importTable ? {
|
|
39
|
+
columns,
|
|
40
|
+
table: importTable
|
|
41
|
+
} : void 0;
|
|
42
|
+
}, [
|
|
43
|
+
resolvedImportSchema,
|
|
44
|
+
importTable
|
|
45
|
+
]);
|
|
46
|
+
const importDataSource = useMemo(()=>{
|
|
47
|
+
if (!sessionOverrides) return dataSource;
|
|
48
|
+
return {
|
|
49
|
+
tableSchema: dataSource.tableSchema,
|
|
50
|
+
createSessionDataSource: async (copyOption, sessionType)=>{
|
|
51
|
+
if (!dataSource.createSessionDataSource) throw Error("[useCsvUpload] dataSource does not support createSessionDataSource");
|
|
52
|
+
return dataSource.createSessionDataSource(copyOption, sessionType, sessionOverrides);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}, [
|
|
56
|
+
dataSource,
|
|
57
|
+
sessionOverrides
|
|
58
|
+
]);
|
|
13
59
|
const editSession = useMemo(()=>new EditSession({
|
|
14
|
-
dataSource,
|
|
60
|
+
dataSource: importDataSource,
|
|
15
61
|
editSessionApi: "createSessionDataSource",
|
|
16
62
|
rowDefaults
|
|
17
63
|
}), [
|
|
18
|
-
|
|
64
|
+
importDataSource,
|
|
19
65
|
rowDefaults
|
|
20
66
|
]);
|
|
21
67
|
const ownsEditSessionRef = useRef(true);
|
|
@@ -61,7 +107,7 @@ const useCsvUpload = ({ dataSource, importMode = "direct", onImportSessionEnded,
|
|
|
61
107
|
setActiveSessionDataSource
|
|
62
108
|
]);
|
|
63
109
|
const table = dataSource.table;
|
|
64
|
-
const schema = dataSource.tableSchema;
|
|
110
|
+
const schema = importTable ? resolvedImportSchema : resolvedImportSchema ?? dataSource.tableSchema;
|
|
65
111
|
const addAllRows = useCallback(async (mergedValidation, operationId)=>{
|
|
66
112
|
const vuuMsgByRow = new Map();
|
|
67
113
|
for (const { rowNum, column, message } of mergedValidation.errors){
|
|
@@ -143,6 +189,15 @@ const useCsvUpload = ({ dataSource, importMode = "direct", onImportSessionEnded,
|
|
|
143
189
|
onError?.(void 0);
|
|
144
190
|
await closePendingEditSession(false);
|
|
145
191
|
if (operationId !== operationIdRef.current) return;
|
|
192
|
+
if (!isSessionTable(dataSource.table) && ("initialising" === dataSource.status || "unsubscribed" === dataSource.status)) {
|
|
193
|
+
const errorMessage = `CsvUpload requires dataSource to be subscribed before uploading (current status: "${dataSource.status}").`;
|
|
194
|
+
onError?.({
|
|
195
|
+
errors: {
|
|
196
|
+
validationError: createUploadError("validation", errorMessage)
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
146
201
|
if (void 0 === schema) throw Error("Table schema is not yet available.");
|
|
147
202
|
if (void 0 === table) throw Error("CsvUpload requires dataSource.table to be defined.");
|
|
148
203
|
const fileContents = await file.text();
|
|
@@ -198,6 +253,8 @@ const useCsvUpload = ({ dataSource, importMode = "direct", onImportSessionEnded,
|
|
|
198
253
|
parseOptions,
|
|
199
254
|
beginEditSession,
|
|
200
255
|
closePendingEditSession,
|
|
256
|
+
dataSource.status,
|
|
257
|
+
dataSource.table,
|
|
201
258
|
table,
|
|
202
259
|
endEditSessionAndNotify,
|
|
203
260
|
schema
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CopyOption, DataSource, SessionDataSourceOverrides } from "@vuu-ui/vuu-data-types";
|
|
2
|
+
export type ExportColumnDescriptor<TName extends string = string> = {
|
|
3
|
+
name: TName;
|
|
4
|
+
/** Override the column name used as the CSV header label. */
|
|
5
|
+
label?: string;
|
|
6
|
+
exportFormatter?: (value: unknown) => string;
|
|
7
|
+
};
|
|
8
|
+
export interface ExportToCsvOptions<TName extends string = string> {
|
|
9
|
+
filename?: string;
|
|
10
|
+
copyOption?: CopyOption;
|
|
11
|
+
excludeColumns?: string[];
|
|
12
|
+
maxRows?: number;
|
|
13
|
+
columnDescriptors?: ExportColumnDescriptor<TName>[];
|
|
14
|
+
overrides?: SessionDataSourceOverrides;
|
|
15
|
+
/** Timeout in milliseconds before the export times out. Default: 30_000ms (0 to disable). */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
onError?: (error: Error) => void;
|
|
18
|
+
onSuccess?: () => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ExportCsvTemplateOptions {
|
|
21
|
+
filename?: string;
|
|
22
|
+
excludeColumns?: string[];
|
|
23
|
+
columns?: string[];
|
|
24
|
+
overrides?: SessionDataSourceOverrides;
|
|
25
|
+
/** Timeout in milliseconds before template generation times out. Default: 10_000ms (0 to disable). */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
onError?: (error: Error) => void;
|
|
28
|
+
onSuccess?: () => void;
|
|
29
|
+
}
|
|
30
|
+
/** Downloads a single-row CSV containing only the column headers, for use as an import template. */
|
|
31
|
+
export declare const exportCsvTemplate: (dataSource: DataSource, options?: ExportCsvTemplateOptions) => Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Subscribes to `dataSource` (creating an export session data source if a view data source is passed),
|
|
34
|
+
* collects all rows in memory in index order, serialises them to CSV (excluding internal session columns),
|
|
35
|
+
* then triggers a browser download. The session data source is unsubscribed automatically once the download
|
|
36
|
+
* is initiated.
|
|
37
|
+
*/
|
|
38
|
+
export declare const exportSessionTableToCsv: <TName extends string = string>(dataSource: DataSource, options?: ExportToCsvOptions<TName>) => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Creates an export session table from `dataSource`, collects all rows in memory, then triggers a CSV download.
|
|
41
|
+
*/
|
|
42
|
+
export declare const exportToCsv: <TName extends string = string>(dataSource: DataSource, options?: ExportToCsvOptions<TName>) => Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DataSource } from "@vuu-ui/vuu-data-types";
|
|
2
|
+
import { type ExportCsvTemplateOptions, type ExportToCsvOptions } from "./export-utils";
|
|
3
|
+
export interface UseCsvExportProps {
|
|
4
|
+
dataSource?: DataSource;
|
|
5
|
+
onError?: (error: Error) => void;
|
|
6
|
+
onSuccess?: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface UseCsvExportResult {
|
|
9
|
+
isExporting: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
exportCsv: <TName extends string = string>(options?: ExportToCsvOptions<TName>, overrideDataSource?: DataSource) => Promise<void>;
|
|
12
|
+
exportTemplate: (options?: ExportCsvTemplateOptions, overrideDataSource?: DataSource) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function useCsvExport(propsOrDataSource?: DataSource | UseCsvExportProps): UseCsvExportResult;
|
|
@@ -2,7 +2,7 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import type { RowDefaultDataItemValues, EditSession } from "@vuu-ui/vuu-data-editing";
|
|
3
3
|
import type { CsvParseError, CsvParseOptions } from "./parse/csv-parse";
|
|
4
4
|
import type { CsvValidationStructuredError } from "./parse/csv-schema-validation";
|
|
5
|
-
import type { DataSource } from "@vuu-ui/vuu-data-types";
|
|
5
|
+
import type { DataSource, TableSchema } from "@vuu-ui/vuu-data-types";
|
|
6
6
|
import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
|
|
7
7
|
import type { CsvUploadTableData } from "./parse/csv-upload-utils";
|
|
8
8
|
export type CsvUploadImportedResult = {
|
|
@@ -38,6 +38,14 @@ export interface CsvUploadProps {
|
|
|
38
38
|
children?: ReactNode;
|
|
39
39
|
dataSource: DataSource;
|
|
40
40
|
embedded?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Schema of the import table, where it differs from the target table. Used to validate
|
|
43
|
+
* the CSV and to determine the session datasource columns. If omitted and importTable is
|
|
44
|
+
* provided, the schema is fetched via getTableSchema. Pass a stable reference.
|
|
45
|
+
*/
|
|
46
|
+
importSchema?: TableSchema;
|
|
47
|
+
/** Expected import table, used to validate the session table returned by the server. */
|
|
48
|
+
importTable?: VuuTable;
|
|
41
49
|
onImportSessionStarted?: (dataSource: DataSource) => void;
|
|
42
50
|
onImportSessionEnded?: (result: CsvUploadSessionEndResult) => void;
|
|
43
51
|
onError?: (result: CsvUploadErrorResult | undefined) => void;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import type { DataSource, TableSchema } from "@vuu-ui/vuu-data-types";
|
|
2
2
|
import { type RowDefaultDataItemValues } from "@vuu-ui/vuu-data-editing";
|
|
3
|
+
import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
|
|
3
4
|
import { type CsvParseOptions } from "./parse/csv-parse";
|
|
4
5
|
import { type CsvValidationResult } from "./parse/csv-schema-validation";
|
|
5
6
|
import type { CsvUploadErrorResult, CsvUploadImportedResult, CsvUploadPreviewResult, CsvUploadSessionEndResult, CsvUploadSessionTable } from "./CsvUpload";
|
|
6
7
|
export interface CsvUploadHookProps {
|
|
7
8
|
dataSource: DataSource;
|
|
8
9
|
importMode?: "direct" | "preview";
|
|
10
|
+
/**
|
|
11
|
+
* Schema of the import table, where it differs from the target table. Used to validate
|
|
12
|
+
* the CSV and to determine the session datasource columns. If omitted and importTable is
|
|
13
|
+
* provided, the schema is fetched via getTableSchema. Pass a stable reference.
|
|
14
|
+
*/
|
|
15
|
+
importSchema?: TableSchema;
|
|
16
|
+
/** Expected import table, used to validate the session table returned by the server. */
|
|
17
|
+
importTable?: VuuTable;
|
|
9
18
|
maxRows?: number;
|
|
10
19
|
onImportSessionEnded?: (result: CsvUploadSessionEndResult) => void;
|
|
11
20
|
onImportSessionStarted?: (dataSource: DataSource) => void;
|
|
@@ -29,4 +38,4 @@ export type UseCsvUploadReturn = {
|
|
|
29
38
|
schema: TableSchema | undefined;
|
|
30
39
|
validation: CsvValidationResult | undefined;
|
|
31
40
|
};
|
|
32
|
-
export declare const useCsvUpload: ({ dataSource, importMode, onImportSessionEnded, onImportSessionStarted, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, rowDefaults, }: CsvUploadHookProps) => UseCsvUploadReturn;
|
|
41
|
+
export declare const useCsvUpload: ({ dataSource, importMode, importSchema, importTable, onImportSessionEnded, onImportSessionStarted, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, rowDefaults, }: CsvUploadHookProps) => UseCsvUploadReturn;
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CalculatedColumnPanel } from "./calculated-column/CalculatedColumnPanel";
|
|
2
|
+
export * from "./csv-export";
|
|
2
3
|
export * from "./csv-upload";
|
|
3
4
|
export * from "./cell-edit-validators";
|
|
4
5
|
export * from "./cell-renderers";
|
|
@@ -10,7 +11,7 @@ export { useColumnActions } from "./column-menu/useColumnActions";
|
|
|
10
11
|
export { ColumnChangeSource, ColumnModel, isColumnAdded, isColumnRemoved, isColumnsReordered, SelectedColumnChangeType, type ColumnEvents, type ColumnsChangeHandler, } from "./column-picker/ColumnModel";
|
|
11
12
|
export { ColumnPicker, type ColumnPickerProps, } from "./column-picker/ColumnPicker";
|
|
12
13
|
export { ColumnPickerAction } from "./column-picker/ColumnPickerAction";
|
|
13
|
-
export {
|
|
14
|
+
export type { SelectedColumnsChangeHandler } from "./column-picker/useColumnPicker";
|
|
14
15
|
export { useTableColumnPicker } from "./column-picker/useTableColumnPicker";
|
|
15
16
|
export { ColumnSettingsPanel } from "./column-settings-panel/ColumnSettingsPanel";
|
|
16
17
|
export { useColumnSettings } from "./column-settings-panel/useColumnSettings";
|
|
@@ -38,6 +38,7 @@ export declare const useInlineAddRow: ({ columns }: UseInlineAddRowProps) => {
|
|
|
38
38
|
allowColumnHeaderMenu?: false;
|
|
39
39
|
colHeaderContentRenderer?: string;
|
|
40
40
|
colHeaderLabelRenderer?: string;
|
|
41
|
+
exportFormatter?: (value: unknown) => string;
|
|
41
42
|
getIcon?: (row: DataRow) => string | undefined;
|
|
42
43
|
groupable?: boolean;
|
|
43
44
|
hidden?: boolean;
|