@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 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.1",
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.1",
15
- "@vuu-ui/vuu-protocol-types": "3.3.1"
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.1",
19
- "@vuu-ui/vuu-data-editing": "3.3.1",
20
- "@vuu-ui/vuu-data-react": "3.3.1",
21
- "@vuu-ui/vuu-data-types": "3.3.1",
22
- "@vuu-ui/vuu-table-types": "3.3.1",
23
- "@vuu-ui/vuu-popups": "3.3.1",
24
- "@vuu-ui/vuu-table": "3.3.1",
25
- "@vuu-ui/vuu-utils": "3.3.1",
26
- "@vuu-ui/vuu-ui-controls": "3.3.1",
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,2 @@
1
+ export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv } from "./export-utils.js";
2
+ export { useCsvExport } from "./useCsvExport.js";
@@ -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
- dataSource,
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
@@ -1,3 +1,4 @@
1
+ export * from "./csv-export/index.js";
1
2
  export * from "./csv-upload/index.js";
2
3
  export * from "./cell-edit-validators/index.js";
3
4
  export * from "./cell-renderers/index.js";
@@ -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,2 @@
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;
@@ -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 { type SelectedColumnsChangeHandler } from "./column-picker/useColumnPicker";
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;