@vuu-ui/vuu-table-extras 3.3.0 → 3.3.2

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/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.3.0",
2
+ "version": "3.3.2",
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.0",
15
- "@vuu-ui/vuu-protocol-types": "3.3.0"
14
+ "@vuu-ui/vuu-filter-types": "3.3.2",
15
+ "@vuu-ui/vuu-protocol-types": "3.3.2"
16
16
  },
17
17
  "dependencies": {
18
- "@vuu-ui/vuu-codemirror": "3.3.0",
19
- "@vuu-ui/vuu-data-editing": "3.3.0",
20
- "@vuu-ui/vuu-data-react": "3.3.0",
21
- "@vuu-ui/vuu-data-types": "3.3.0",
22
- "@vuu-ui/vuu-table-types": "3.3.0",
23
- "@vuu-ui/vuu-popups": "3.3.0",
24
- "@vuu-ui/vuu-table": "3.3.0",
25
- "@vuu-ui/vuu-utils": "3.3.0",
26
- "@vuu-ui/vuu-ui-controls": "3.3.0",
18
+ "@vuu-ui/vuu-codemirror": "3.3.2",
19
+ "@vuu-ui/vuu-data-editing": "3.3.2",
20
+ "@vuu-ui/vuu-data-react": "3.3.2",
21
+ "@vuu-ui/vuu-data-types": "3.3.2",
22
+ "@vuu-ui/vuu-table-types": "3.3.2",
23
+ "@vuu-ui/vuu-popups": "3.3.2",
24
+ "@vuu-ui/vuu-table": "3.3.2",
25
+ "@vuu-ui/vuu-utils": "3.3.2",
26
+ "@vuu-ui/vuu-ui-controls": "3.3.2",
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,164 @@
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 csvCell = (value)=>{
10
+ const s = null == value ? "" : String(value);
11
+ return s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}` : s;
12
+ };
13
+ const triggerCsvDownload = (csv, filename)=>{
14
+ const blob = new Blob([
15
+ csv
16
+ ], {
17
+ type: "text/csv;charset=utf-8;"
18
+ });
19
+ const url = URL.createObjectURL(blob);
20
+ const a = document.createElement("a");
21
+ a.href = url;
22
+ a.download = filename;
23
+ a.style.display = "none";
24
+ document.body.appendChild(a);
25
+ a.click();
26
+ document.body.removeChild(a);
27
+ setTimeout(()=>URL.revokeObjectURL(url), 10000);
28
+ };
29
+ const exportCsvTemplate = async (dataSource, filename = "template.csv", excludeColumns = [], columns, overrides)=>{
30
+ const sessionOverrides = overrides ?? (columns ? {
31
+ columns
32
+ } : void 0);
33
+ if (!isSessionTable(dataSource.table) && dataSource.createSessionDataSource) try {
34
+ const sessionDataSource = await dataSource.createSessionDataSource("Empty", "export", sessionOverrides);
35
+ if (sessionDataSource) return new Promise((resolve)=>{
36
+ const excluded = new Set([
37
+ ...EXPORT_EXCLUDED_COLUMNS,
38
+ ...excludeColumns
39
+ ]);
40
+ sessionDataSource.subscribe({
41
+ range: Range(0, 0),
42
+ columns: sessionOverrides?.columns
43
+ }, (message)=>{
44
+ if ("subscribed" === message.type) {
45
+ const { columns: subColumns } = message;
46
+ const exportCols = subColumns.filter((name)=>!excluded.has(name));
47
+ const header = exportCols.map(csvCell).join(",");
48
+ triggerCsvDownload(`${header}\r\n`, filename);
49
+ sessionDataSource.unsubscribe();
50
+ resolve();
51
+ }
52
+ });
53
+ });
54
+ } catch (error) {
55
+ console.warn("[exportCsvTemplate] createSessionDataSource failed, falling back to tableSchema", error);
56
+ }
57
+ const schema = dataSource.tableSchema;
58
+ const targetColumns = columns ?? overrides?.columns;
59
+ if (void 0 !== targetColumns) {
60
+ if (schema) {
61
+ const schemaColumnNames = new Set(schema.columns.map((col)=>col.name));
62
+ const unknown = targetColumns.filter((name)=>!schemaColumnNames.has(name));
63
+ if (unknown.length > 0) console.warn(`[exportCsvTemplate] unknown column(s) in view tableSchema: ${unknown.join(", ")}`);
64
+ }
65
+ } else if (!schema) throw Error("exportCsvTemplate: tableSchema not available on dataSource");
66
+ const excluded = new Set([
67
+ ...EXPORT_EXCLUDED_COLUMNS,
68
+ ...excludeColumns
69
+ ]);
70
+ 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) throw Error("exportCsvTemplate: no columns available for export");
72
+ const header = exportCols.map(csvCell).join(",");
73
+ triggerCsvDownload(`${header}\r\n`, filename);
74
+ };
75
+ const exportSessionTableToCsv = async (dataSource, filename = "export.csv", excludeColumns = [], onError, onSuccess, maxRows = MAX_EXPORT_ROWS, columnDescriptors, copyOption = "All", overrides)=>{
76
+ let sessionDataSource;
77
+ if (!isSessionTable(dataSource.table) && dataSource.createSessionDataSource) try {
78
+ sessionDataSource = await dataSource.createSessionDataSource(copyOption, "export", overrides);
79
+ } catch (err) {
80
+ onError?.(err instanceof Error ? err : new Error(String(err)));
81
+ return;
82
+ }
83
+ else sessionDataSource = dataSource;
84
+ if (!sessionDataSource) return void onError?.(new Error("exportSessionTableToCsv: unable to obtain sessionDataSource"));
85
+ const activeSessionDataSource = sessionDataSource;
86
+ const excluded = new Set([
87
+ ...EXPORT_EXCLUDED_COLUMNS,
88
+ ...excludeColumns
89
+ ]);
90
+ let exportCols = [];
91
+ let colNameToRowIdx = {};
92
+ let descriptorMap = {};
93
+ let totalSize = 0;
94
+ const collectedRows = [];
95
+ return new Promise((resolve)=>{
96
+ const handleMessage = (message)=>{
97
+ if ("subscribed" === message.type) {
98
+ const { columns } = message;
99
+ if (columnDescriptors) {
100
+ const subscribedSet = new Set(columns);
101
+ 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
+ }
108
+ descriptorMap = Object.fromEntries(columnDescriptors.map((d)=>[
109
+ d.name,
110
+ d
111
+ ]));
112
+ }
113
+ exportCols = columns.filter((name)=>!excluded.has(name));
114
+ colNameToRowIdx = Object.fromEntries(columns.map((name, i)=>[
115
+ name,
116
+ metadataKeys.count + i
117
+ ]));
118
+ } else if ("viewport-update" === message.type) {
119
+ if ("size-only" === message.mode) {
120
+ totalSize = message.size ?? 0;
121
+ if (0 === totalSize) {
122
+ activeSessionDataSource.unsubscribe();
123
+ onSuccess?.();
124
+ resolve();
125
+ return;
126
+ }
127
+ if (totalSize > maxRows) {
128
+ activeSessionDataSource.unsubscribe();
129
+ onError?.(new Error(`exportToCsv: row count ${totalSize} exceeds the ${maxRows} row limit`));
130
+ resolve();
131
+ return;
132
+ }
133
+ activeSessionDataSource.range = Range(0, Math.min(CHUNK_SIZE, totalSize));
134
+ } else if ("batch" === message.mode && message.rows) {
135
+ collectedRows.push(...message.rows);
136
+ if (void 0 !== message.size) totalSize = message.size;
137
+ if (collectedRows.length >= totalSize) {
138
+ activeSessionDataSource.unsubscribe();
139
+ const lines = [
140
+ exportCols.map((name)=>csvCell(descriptorMap[name]?.label ?? name)).join(",")
141
+ ];
142
+ for (const row of collectedRows)lines.push(exportCols.map((c)=>{
143
+ const raw = row[colNameToRowIdx[c]];
144
+ const formatter = descriptorMap[c]?.exportFormatter;
145
+ return csvCell(formatter ? formatter(raw) : raw);
146
+ }).join(","));
147
+ triggerCsvDownload(lines.join("\r\n"), filename);
148
+ onSuccess?.();
149
+ resolve();
150
+ } else {
151
+ const nextFrom = collectedRows.length;
152
+ activeSessionDataSource.range = Range(nextFrom, Math.min(nextFrom + CHUNK_SIZE, totalSize));
153
+ }
154
+ }
155
+ }
156
+ };
157
+ activeSessionDataSource.subscribe({
158
+ range: Range(0, 0),
159
+ columns: overrides?.columns
160
+ }, handleMessage);
161
+ });
162
+ };
163
+ const exportToCsv = async (dataSource, copyOption = "All", filename = "export.csv", excludeColumns = [], onError, onSuccess, maxRows = MAX_EXPORT_ROWS, columnDescriptors, overrides)=>exportSessionTableToCsv(dataSource, filename, excludeColumns, onError, onSuccess, maxRows, columnDescriptors, copyOption, overrides);
164
+ export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv };
@@ -0,0 +1 @@
1
+ export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv } from "./export-utils.js";
@@ -1,17 +1,68 @@
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 })=>{
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);
13
- const editSession = useMemo(()=>new EditSession(dataSource), [
14
- dataSource
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
+ ]);
59
+ const editSession = useMemo(()=>new EditSession({
60
+ dataSource: importDataSource,
61
+ editSessionApi: "createSessionDataSource",
62
+ rowDefaults
63
+ }), [
64
+ importDataSource,
65
+ rowDefaults
15
66
  ]);
16
67
  const ownsEditSessionRef = useRef(true);
17
68
  const operationIdRef = useRef(0);
@@ -56,7 +107,7 @@ const useCsvUpload = ({ dataSource, importMode = "direct", onImportSessionEnded,
56
107
  setActiveSessionDataSource
57
108
  ]);
58
109
  const table = dataSource.table;
59
- const schema = dataSource.tableSchema;
110
+ const schema = importTable ? resolvedImportSchema : resolvedImportSchema ?? dataSource.tableSchema;
60
111
  const addAllRows = useCallback(async (mergedValidation, operationId)=>{
61
112
  const vuuMsgByRow = new Map();
62
113
  for (const { rowNum, column, message } of mergedValidation.errors){
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,20 @@
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
+ /** Downloads a single-row CSV containing only the column headers, for use as an import template. */
9
+ export declare const exportCsvTemplate: (dataSource: DataSource, filename?: string, excludeColumns?: string[], columns?: string[], overrides?: SessionDataSourceOverrides) => Promise<void>;
10
+ /**
11
+ * Subscribes to `dataSource` (creating an export session data source if a view data source is passed)
12
+ * with a full-range request to drain all rows, serialises them to CSV (excluding internal session columns),
13
+ * then triggers a browser download. The session data source is unsubscribed automatically once the download
14
+ * is initiated.
15
+ */
16
+ export declare const exportSessionTableToCsv: <TName extends string = string>(dataSource: DataSource, filename?: string, excludeColumns?: string[], onError?: (error: Error) => void, onSuccess?: () => void, maxRows?: number, columnDescriptors?: ExportColumnDescriptor<TName>[], copyOption?: CopyOption, overrides?: SessionDataSourceOverrides) => Promise<void>;
17
+ /**
18
+ * Creates an export session table from `dataSource` then streams all rows to a CSV download.
19
+ */
20
+ export declare const exportToCsv: <TName extends string = string>(dataSource: DataSource, copyOption?: CopyOption, filename?: string, excludeColumns?: string[], onError?: (error: Error) => void, onSuccess?: () => void, maxRows?: number, columnDescriptors?: ExportColumnDescriptor<TName>[], overrides?: SessionDataSourceOverrides) => Promise<void>;
@@ -0,0 +1 @@
1
+ export { exportCsvTemplate, exportSessionTableToCsv, exportToCsv, type ExportColumnDescriptor, } from "./export-utils";
@@ -1,9 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
- import type { DataSource } from "@vuu-ui/vuu-data-types";
3
- import type { EditSession } from "@vuu-ui/vuu-data-editing";
4
- import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
2
+ import type { RowDefaultDataItemValues, EditSession } from "@vuu-ui/vuu-data-editing";
5
3
  import type { CsvParseError, CsvParseOptions } from "./parse/csv-parse";
6
4
  import type { CsvValidationStructuredError } from "./parse/csv-schema-validation";
5
+ import type { DataSource, TableSchema } from "@vuu-ui/vuu-data-types";
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 = {
9
9
  tableData: CsvUploadTableData;
@@ -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;
@@ -51,5 +59,6 @@ export interface CsvUploadProps {
51
59
  open?: boolean;
52
60
  parseOptions?: CsvParseOptions;
53
61
  importMode?: "direct" | "preview";
62
+ rowDefaults?: RowDefaultDataItemValues;
54
63
  }
55
64
  export declare const CsvUpload: (props: CsvUploadProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,20 @@
1
1
  import type { DataSource, TableSchema } from "@vuu-ui/vuu-data-types";
2
+ import { type RowDefaultDataItemValues } from "@vuu-ui/vuu-data-editing";
3
+ import type { VuuTable } from "@vuu-ui/vuu-protocol-types";
2
4
  import { type CsvParseOptions } from "./parse/csv-parse";
3
5
  import { type CsvValidationResult } from "./parse/csv-schema-validation";
4
6
  import type { CsvUploadErrorResult, CsvUploadImportedResult, CsvUploadPreviewResult, CsvUploadSessionEndResult, CsvUploadSessionTable } from "./CsvUpload";
5
7
  export interface CsvUploadHookProps {
6
8
  dataSource: DataSource;
7
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;
8
18
  maxRows?: number;
9
19
  onImportSessionEnded?: (result: CsvUploadSessionEndResult) => void;
10
20
  onImportSessionStarted?: (dataSource: DataSource) => void;
@@ -13,6 +23,8 @@ export interface CsvUploadHookProps {
13
23
  onPreview?: (result: CsvUploadPreviewResult) => void;
14
24
  onProcessingStarted?: () => void;
15
25
  parseOptions?: CsvParseOptions;
26
+ /** Default column values applied to every addRow call. Pass a stable reference — a new object triggers EditSession recreation. */
27
+ rowDefaults?: RowDefaultDataItemValues;
16
28
  }
17
29
  export type UseCsvUploadReturn = {
18
30
  canImport: boolean;
@@ -26,4 +38,4 @@ export type UseCsvUploadReturn = {
26
38
  schema: TableSchema | undefined;
27
39
  validation: CsvValidationResult | undefined;
28
40
  };
29
- export declare const useCsvUpload: ({ dataSource, importMode, onImportSessionEnded, onImportSessionStarted, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, }: 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;