@vuu-ui/vuu-table-extras 3.3.2 → 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 +96 -39
- package/src/csv-export/index.js +1 -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 +11 -0
- package/types/csv-export/export-utils.d.ts +28 -6
- package/types/csv-export/index.d.ts +2 -1
- package/types/csv-export/useCsvExport.d.ts +14 -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",
|
|
@@ -6,9 +6,11 @@ const EXPORT_EXCLUDED_COLUMNS = new Set([
|
|
|
6
6
|
]);
|
|
7
7
|
const MAX_EXPORT_ROWS = 10000;
|
|
8
8
|
const CHUNK_SIZE = 1000;
|
|
9
|
+
const DEFAULT_EXPORT_TIMEOUT = 30000;
|
|
10
|
+
const DEFAULT_TEMPLATE_TIMEOUT = 10000;
|
|
9
11
|
const csvCell = (value)=>{
|
|
10
12
|
const s = null == value ? "" : String(value);
|
|
11
|
-
return s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}` : s;
|
|
13
|
+
return s.includes(",") || s.includes('"') || s.includes("\n") || s.includes("\r") ? `"${s.replace(/"/g, '""')}"` : s;
|
|
12
14
|
};
|
|
13
15
|
const triggerCsvDownload = (csv, filename)=>{
|
|
14
16
|
const blob = new Blob([
|
|
@@ -26,13 +28,27 @@ const triggerCsvDownload = (csv, filename)=>{
|
|
|
26
28
|
document.body.removeChild(a);
|
|
27
29
|
setTimeout(()=>URL.revokeObjectURL(url), 10000);
|
|
28
30
|
};
|
|
29
|
-
const exportCsvTemplate = async (dataSource,
|
|
31
|
+
const exportCsvTemplate = async (dataSource, options = {})=>{
|
|
32
|
+
const { filename = "template.csv", excludeColumns = [], columns, overrides, timeout = DEFAULT_TEMPLATE_TIMEOUT, onError, onSuccess } = options;
|
|
30
33
|
const sessionOverrides = overrides ?? (columns ? {
|
|
31
34
|
columns
|
|
32
35
|
} : void 0);
|
|
33
|
-
if (!isSessionTable(dataSource.table) && dataSource.createSessionDataSource) try {
|
|
36
|
+
if (!isSessionTable(dataSource.table) && dataSource.createSessionDataSource && "initialising" !== dataSource.status && "unsubscribed" !== dataSource.status) try {
|
|
34
37
|
const sessionDataSource = await dataSource.createSessionDataSource("Empty", "export", sessionOverrides);
|
|
35
|
-
if (sessionDataSource) return new Promise((resolve)=>{
|
|
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);
|
|
36
52
|
const excluded = new Set([
|
|
37
53
|
...EXPORT_EXCLUDED_COLUMNS,
|
|
38
54
|
...excludeColumns
|
|
@@ -46,7 +62,8 @@ const exportCsvTemplate = async (dataSource, filename = "template.csv", excludeC
|
|
|
46
62
|
const exportCols = subColumns.filter((name)=>!excluded.has(name));
|
|
47
63
|
const header = exportCols.map(csvCell).join(",");
|
|
48
64
|
triggerCsvDownload(`${header}\r\n`, filename);
|
|
49
|
-
|
|
65
|
+
cleanup();
|
|
66
|
+
onSuccess?.();
|
|
50
67
|
resolve();
|
|
51
68
|
}
|
|
52
69
|
});
|
|
@@ -62,26 +79,47 @@ const exportCsvTemplate = async (dataSource, filename = "template.csv", excludeC
|
|
|
62
79
|
const unknown = targetColumns.filter((name)=>!schemaColumnNames.has(name));
|
|
63
80
|
if (unknown.length > 0) console.warn(`[exportCsvTemplate] unknown column(s) in view tableSchema: ${unknown.join(", ")}`);
|
|
64
81
|
}
|
|
65
|
-
} else if (!schema)
|
|
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
|
+
}
|
|
66
87
|
const excluded = new Set([
|
|
67
88
|
...EXPORT_EXCLUDED_COLUMNS,
|
|
68
89
|
...excludeColumns
|
|
69
90
|
]);
|
|
70
91
|
const exportCols = targetColumns ? targetColumns.filter((name)=>!excluded.has(name)) : schema?.columns.filter((col)=>!excluded.has(col.name)).map((col)=>col.name) ?? [];
|
|
71
|
-
if (0 === exportCols.length)
|
|
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
|
+
}
|
|
72
97
|
const header = exportCols.map(csvCell).join(",");
|
|
73
98
|
triggerCsvDownload(`${header}\r\n`, filename);
|
|
99
|
+
onSuccess?.();
|
|
74
100
|
};
|
|
75
|
-
const exportSessionTableToCsv = async (dataSource,
|
|
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
|
+
}
|
|
76
109
|
let sessionDataSource;
|
|
77
110
|
if (!isSessionTable(dataSource.table) && dataSource.createSessionDataSource) try {
|
|
78
111
|
sessionDataSource = await dataSource.createSessionDataSource(copyOption, "export", overrides);
|
|
79
112
|
} catch (err) {
|
|
80
|
-
|
|
81
|
-
return;
|
|
113
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
114
|
+
if (onError) return void onError(error);
|
|
115
|
+
throw error;
|
|
82
116
|
}
|
|
83
117
|
else sessionDataSource = dataSource;
|
|
84
|
-
if (!sessionDataSource)
|
|
118
|
+
if (!sessionDataSource) {
|
|
119
|
+
const error = new Error("exportSessionTableToCsv: unable to obtain sessionDataSource");
|
|
120
|
+
if (onError) return void onError(error);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
85
123
|
const activeSessionDataSource = sessionDataSource;
|
|
86
124
|
const excluded = new Set([
|
|
87
125
|
...EXPORT_EXCLUDED_COLUMNS,
|
|
@@ -91,20 +129,31 @@ const exportSessionTableToCsv = async (dataSource, filename = "export.csv", excl
|
|
|
91
129
|
let colNameToRowIdx = {};
|
|
92
130
|
let descriptorMap = {};
|
|
93
131
|
let totalSize = 0;
|
|
94
|
-
|
|
95
|
-
|
|
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);
|
|
96
150
|
const handleMessage = (message)=>{
|
|
97
151
|
if ("subscribed" === message.type) {
|
|
98
152
|
const { columns } = message;
|
|
99
153
|
if (columnDescriptors) {
|
|
100
154
|
const subscribedSet = new Set(columns);
|
|
101
155
|
const unknown = columnDescriptors.filter((d)=>!excluded.has(d.name) && !subscribedSet.has(d.name)).map((d)=>d.name);
|
|
102
|
-
if (unknown.length > 0) {
|
|
103
|
-
activeSessionDataSource.unsubscribe();
|
|
104
|
-
onError?.(new Error(`exportToCsv: unknown column(s) in columnDescriptors: ${unknown.join(", ")}`));
|
|
105
|
-
resolve();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
156
|
+
if (unknown.length > 0) return void fail(new Error(`exportToCsv: unknown column(s) in columnDescriptors: ${unknown.join(", ")}`));
|
|
108
157
|
descriptorMap = Object.fromEntries(columnDescriptors.map((d)=>[
|
|
109
158
|
d.name,
|
|
110
159
|
d
|
|
@@ -119,37 +168,45 @@ const exportSessionTableToCsv = async (dataSource, filename = "export.csv", excl
|
|
|
119
168
|
if ("size-only" === message.mode) {
|
|
120
169
|
totalSize = message.size ?? 0;
|
|
121
170
|
if (0 === totalSize) {
|
|
122
|
-
|
|
171
|
+
cleanup();
|
|
172
|
+
const header = exportCols.map((name)=>csvCell(descriptorMap[name]?.label ?? name)).join(",");
|
|
173
|
+
triggerCsvDownload(`${header}\r\n`, filename);
|
|
123
174
|
onSuccess?.();
|
|
124
175
|
resolve();
|
|
125
176
|
return;
|
|
126
177
|
}
|
|
127
|
-
if (totalSize > maxRows) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
resolve();
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
activeSessionDataSource.range = Range(0, Math.min(CHUNK_SIZE, totalSize));
|
|
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);
|
|
134
181
|
} else if ("batch" === message.mode && message.rows) {
|
|
135
|
-
|
|
182
|
+
for (const row of message.rows){
|
|
183
|
+
const rowIndex = row[metadataKeys.IDX];
|
|
184
|
+
if ("number" == typeof rowIndex) collectedRows.set(rowIndex, row);
|
|
185
|
+
}
|
|
136
186
|
if (void 0 !== message.size) totalSize = message.size;
|
|
137
|
-
if (collectedRows.
|
|
138
|
-
|
|
187
|
+
if (collectedRows.size >= totalSize) {
|
|
188
|
+
cleanup();
|
|
139
189
|
const lines = [
|
|
140
190
|
exportCols.map((name)=>csvCell(descriptorMap[name]?.label ?? name)).join(",")
|
|
141
191
|
];
|
|
142
|
-
for
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
}
|
|
147
200
|
triggerCsvDownload(lines.join("\r\n"), filename);
|
|
148
201
|
onSuccess?.();
|
|
149
202
|
resolve();
|
|
150
|
-
} else {
|
|
151
|
-
const
|
|
152
|
-
|
|
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);
|
|
153
210
|
}
|
|
154
211
|
}
|
|
155
212
|
}
|
|
@@ -160,5 +217,5 @@ const exportSessionTableToCsv = async (dataSource, filename = "export.csv", excl
|
|
|
160
217
|
}, handleMessage);
|
|
161
218
|
});
|
|
162
219
|
};
|
|
163
|
-
const exportToCsv = async (dataSource,
|
|
220
|
+
const exportToCsv = async (dataSource, options)=>exportSessionTableToCsv(dataSource, options);
|
|
164
221
|
export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv };
|
package/src/csv-export/index.js
CHANGED
|
@@ -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
|
});
|
|
@@ -189,6 +189,15 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
|
|
|
189
189
|
onError?.(void 0);
|
|
190
190
|
await closePendingEditSession(false);
|
|
191
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
|
+
}
|
|
192
201
|
if (void 0 === schema) throw Error("Table schema is not yet available.");
|
|
193
202
|
if (void 0 === table) throw Error("CsvUpload requires dataSource.table to be defined.");
|
|
194
203
|
const fileContents = await file.text();
|
|
@@ -244,6 +253,8 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
|
|
|
244
253
|
parseOptions,
|
|
245
254
|
beginEditSession,
|
|
246
255
|
closePendingEditSession,
|
|
256
|
+
dataSource.status,
|
|
257
|
+
dataSource.table,
|
|
247
258
|
table,
|
|
248
259
|
endEditSessionAndNotify,
|
|
249
260
|
schema
|
|
@@ -5,16 +5,38 @@ export type ExportColumnDescriptor<TName extends string = string> = {
|
|
|
5
5
|
label?: string;
|
|
6
6
|
exportFormatter?: (value: unknown) => string;
|
|
7
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
|
+
}
|
|
8
30
|
/** Downloads a single-row CSV containing only the column headers, for use as an import template. */
|
|
9
|
-
export declare const exportCsvTemplate: (dataSource: DataSource,
|
|
31
|
+
export declare const exportCsvTemplate: (dataSource: DataSource, options?: ExportCsvTemplateOptions) => Promise<void>;
|
|
10
32
|
/**
|
|
11
|
-
* Subscribes to `dataSource` (creating an export session data source if a view data source is passed)
|
|
12
|
-
*
|
|
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),
|
|
13
35
|
* then triggers a browser download. The session data source is unsubscribed automatically once the download
|
|
14
36
|
* is initiated.
|
|
15
37
|
*/
|
|
16
|
-
export declare const exportSessionTableToCsv: <TName extends string = string>(dataSource: DataSource,
|
|
38
|
+
export declare const exportSessionTableToCsv: <TName extends string = string>(dataSource: DataSource, options?: ExportToCsvOptions<TName>) => Promise<void>;
|
|
17
39
|
/**
|
|
18
|
-
* Creates an export session table from `dataSource
|
|
40
|
+
* Creates an export session table from `dataSource`, collects all rows in memory, then triggers a CSV download.
|
|
19
41
|
*/
|
|
20
|
-
export declare const exportToCsv: <TName extends string = string>(dataSource: DataSource,
|
|
42
|
+
export declare const exportToCsv: <TName extends string = string>(dataSource: DataSource, options?: ExportToCsvOptions<TName>) => Promise<void>;
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv, type ExportColumnDescriptor, } from "./export-utils";
|
|
1
|
+
export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv, type ExportColumnDescriptor, type ExportCsvTemplateOptions, type ExportToCsvOptions, } from "./export-utils";
|
|
2
|
+
export { useCsvExport, type UseCsvExportProps, type UseCsvExportResult, } from "./useCsvExport";
|
|
@@ -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;
|