@vuu-ui/vuu-table-extras 4.0.0-alpha.1 → 4.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +14 -12
  2. package/src/calculated-column/CalculatedColumnPanel.js +2 -2
  3. package/src/calculated-column/useEditCalculatedColumn.js +1 -1
  4. package/src/column-picker/ColumnModel.js +14 -25
  5. package/src/column-picker/ColumnPicker.css.js +0 -86
  6. package/src/column-picker/ColumnPicker.js +12 -180
  7. package/src/column-picker/useColumnPicker.js +28 -22
  8. package/src/csv-upload/CsvUpload.css.js +34 -0
  9. package/src/csv-upload/CsvUpload.js +109 -45
  10. package/src/csv-upload/parse/csv-constants.js +2 -2
  11. package/src/csv-upload/parse/csv-errors.js +2 -1
  12. package/src/csv-upload/parse/csv-parse.js +1 -1
  13. package/src/csv-upload/parse/csv-schema-validation.js +54 -6
  14. package/src/csv-upload/useCsvUpload.js +16 -16
  15. package/src/tabbed-table-config-panel/TabbedTableConfigPanel.js +17 -8
  16. package/src/tabbed-table-config-panel/useTabbedTableConfigPanel.js +1 -1
  17. package/types/calculated-column/CalculatedColumnPanel.d.ts +2 -2
  18. package/types/column-picker/ColumnModel.d.ts +4 -3
  19. package/types/column-picker/ColumnPicker.d.ts +4 -6
  20. package/types/column-picker/useColumnPicker.d.ts +7 -10
  21. package/types/column-settings-panel/useColumnSettings.d.ts +3 -3
  22. package/types/csv-upload/CsvUpload.d.ts +3 -6
  23. package/types/csv-upload/data-upload-preview/useDataUploadPreview.d.ts +1 -1
  24. package/types/csv-upload/index.d.ts +1 -1
  25. package/types/csv-upload/parse/csv-constants.d.ts +2 -2
  26. package/types/csv-upload/parse/csv-errors.d.ts +3 -2
  27. package/types/csv-upload/parse/csv-schema-validation.d.ts +10 -2
  28. package/types/csv-upload/parse/index.d.ts +1 -1
  29. package/types/csv-upload/useCsvUpload.d.ts +4 -3
  30. package/types/index.d.ts +0 -1
  31. package/types/src/csv-upload/CsvUpload.d.ts +2 -1
  32. package/types/src/csv-upload/index.d.ts +1 -1
  33. package/types/src/csv-upload/parse/csv-constants.d.ts +2 -2
  34. package/types/src/csv-upload/parse/csv-errors.d.ts +2 -1
  35. package/types/src/csv-upload/parse/csv-schema-validation.d.ts +2 -0
  36. package/types/src/csv-upload/parse/index.d.ts +1 -1
  37. package/types/src/csv-upload/useCsvUpload.d.ts +3 -2
  38. package/types/tabbed-table-config-panel/useTabbedTableConfigPanel.d.ts +4 -3
@@ -1,13 +1,13 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { Button, Dialog, DialogActions, DialogContent, DialogHeader, FileDropZone, FileDropZoneIcon, FileDropZoneTrigger } from "@salt-ds/core";
2
+ import { Button, Dialog, DialogActions, DialogContent, DialogHeader, FileDropZone, FileDropZoneIcon, FileDropZoneTrigger, Text } from "@salt-ds/core";
3
3
  import { useComponentCssInjection } from "@salt-ds/styles";
4
4
  import { useWindow } from "@salt-ds/window";
5
- import { useCallback, useState } from "react";
5
+ import { useCallback, useMemo, useState } from "react";
6
6
  import { useCsvUpload } from "./useCsvUpload.js";
7
7
  import CsvUpload from "./CsvUpload.css";
8
8
  const classBase = "vuuCsvUpload";
9
9
  const CsvUpload_CsvUpload = (props)=>{
10
- const { children, dialogTitle = "Import CSV", embedded = false, onCancel, onClose, open, renderError } = props;
10
+ const { children, dialogTitle = "Import CSV", embedded = false, onCancel, onClose, open } = props;
11
11
  const isControlledOpen = void 0 !== open;
12
12
  const [internalOpen, setInternalOpen] = useState(open ?? true);
13
13
  const dialogOpen = isControlledOpen ? open : internalOpen;
@@ -18,6 +18,26 @@ const CsvUpload_CsvUpload = (props)=>{
18
18
  window: targetWindow
19
19
  });
20
20
  const { canImport, cancelImport, isProcessingFile, isImporting, importData, onDrop, onTriggerChange, schema, validation, error } = useCsvUpload(props);
21
+ const errorsToRender = useMemo(()=>{
22
+ if (validation?.errors && validation.errors.length > 0) return validation.errors;
23
+ const parseErrors = error?.errors.schemaError?.parseError?.errors || error?.errors.validationError?.parseError?.errors;
24
+ if (parseErrors && parseErrors.length > 0) return parseErrors;
25
+ return [];
26
+ }, [
27
+ validation,
28
+ error
29
+ ]);
30
+ const parsedSchemaErrors = useMemo(()=>{
31
+ const schemaErrorMsg = error?.errors.schemaError?.message || error?.errors.validationError?.message;
32
+ if (!schemaErrorMsg) return [];
33
+ const cleanedMsg = schemaErrorMsg.replace(/^CSV validation failed:\s*/, "");
34
+ return cleanedMsg.split(";").map((item)=>item.trim()).filter((item)=>item.length > 0);
35
+ }, [
36
+ error
37
+ ]);
38
+ const hasErrors = !!error || validation && validation.errors.length > 0;
39
+ const isUploaded = canImport || isImporting;
40
+ const status = hasErrors ? "error" : isUploaded ? "success" : void 0;
21
41
  const handleCancel = useCallback(async ()=>{
22
42
  await cancelImport();
23
43
  if (!isControlledOpen) setInternalOpen(false);
@@ -33,57 +53,101 @@ const CsvUpload_CsvUpload = (props)=>{
33
53
  importData,
34
54
  onClose
35
55
  ]);
36
- const content = /*#__PURE__*/ jsxs("div", {
56
+ const renderErrorsContent = useMemo(()=>{
57
+ if (error?.errors.importError) return /*#__PURE__*/ jsxs(Fragment, {
58
+ children: [
59
+ /*#__PURE__*/ jsx("strong", {
60
+ children: "Import failed"
61
+ }),
62
+ /*#__PURE__*/ jsx("div", {
63
+ className: `${classBase}-errorContainer`,
64
+ children: /*#__PURE__*/ jsx("div", {
65
+ className: `${classBase}-errorItem ${classBase}-importErrorItem`,
66
+ children: error.errors.importError.message
67
+ })
68
+ })
69
+ ]
70
+ });
71
+ if (hasErrors) {
72
+ const headingText = error ? "Validation failed" : "Your file contains errors";
73
+ return /*#__PURE__*/ jsxs(Fragment, {
74
+ children: [
75
+ /*#__PURE__*/ jsx("strong", {
76
+ children: headingText
77
+ }),
78
+ /*#__PURE__*/ jsx("div", {
79
+ className: `${classBase}-errorContainer`,
80
+ children: errorsToRender.length > 0 ? /*#__PURE__*/ jsx("ul", {
81
+ className: `${classBase}-errorList`,
82
+ children: errorsToRender.map((error, i)=>/*#__PURE__*/ jsxs("li", {
83
+ className: `${classBase}-errorItem`,
84
+ children: [
85
+ error.rowNum > 0 ? `Row ${error.rowNum}: ` : "",
86
+ "*" === error.column ? "" : `${error.column}: `,
87
+ error.message
88
+ ]
89
+ }, `${error.column}-${error.message}-${i}`))
90
+ }) : parsedSchemaErrors.length > 0 ? /*#__PURE__*/ jsx("ul", {
91
+ className: `${classBase}-errorList`,
92
+ children: parsedSchemaErrors.map((errMsg)=>/*#__PURE__*/ jsx("li", {
93
+ className: `${classBase}-errorItem`,
94
+ children: errMsg
95
+ }, errMsg))
96
+ }) : /*#__PURE__*/ jsx("div", {
97
+ className: `${classBase}-errorItem ${classBase}-importErrorItem`,
98
+ children: error?.errors.schemaError?.message || error?.errors.validationError?.message || "An unexpected error occurred during import"
99
+ })
100
+ })
101
+ ]
102
+ });
103
+ }
104
+ return /*#__PURE__*/ jsx("div", {
105
+ children: "Drop a file here or"
106
+ });
107
+ }, [
108
+ error,
109
+ hasErrors,
110
+ errorsToRender,
111
+ parsedSchemaErrors
112
+ ]);
113
+ const content = /*#__PURE__*/ jsx("div", {
37
114
  className: classBase,
38
- children: [
39
- /*#__PURE__*/ jsxs(FileDropZone, {
40
- className: `${classBase}-dropZone`,
41
- disabled: void 0 === schema || isProcessingFile || isImporting,
42
- onDrop: onDrop,
43
- status: validation && validation.errors.length > 0 || error ? "error" : void 0,
115
+ children: /*#__PURE__*/ jsx(FileDropZone, {
116
+ className: `${classBase}-dropZone`,
117
+ disabled: void 0 === schema || isProcessingFile || isImporting,
118
+ onDrop: onDrop,
119
+ status: status,
120
+ children: isUploaded ? /*#__PURE__*/ jsxs(Fragment, {
121
+ children: [
122
+ /*#__PURE__*/ jsx(FileDropZoneIcon, {
123
+ status: "success"
124
+ }),
125
+ /*#__PURE__*/ jsx("strong", {
126
+ children: "Upload completed"
127
+ })
128
+ ]
129
+ }) : /*#__PURE__*/ jsxs(Fragment, {
44
130
  children: [
45
- /*#__PURE__*/ jsx(FileDropZoneIcon, {}),
46
- validation && validation.errors.length > 0 ? /*#__PURE__*/ jsxs(Fragment, {
47
- children: [
48
- /*#__PURE__*/ jsx("div", {
49
- children: "Your file contains errors"
50
- }),
51
- /*#__PURE__*/ jsx("ul", {
52
- className: `${classBase}-errorList`,
53
- children: validation.errors.filter((e)=>e.column in validation.errorMap.fileErrors).map((error, i)=>/*#__PURE__*/ jsx("li", {
54
- className: `${classBase}-errorItem`,
55
- children: error.message
56
- }, `${error.column}-${error.message}-${i}`))
57
- }),
58
- /*#__PURE__*/ jsx("div", {
59
- children: "Please rectify and reupload"
60
- })
61
- ]
62
- }) : /*#__PURE__*/ jsx("div", {
63
- children: "Drop a file here or"
131
+ /*#__PURE__*/ jsx(FileDropZoneIcon, {
132
+ status: hasErrors ? "error" : void 0
133
+ }),
134
+ renderErrorsContent,
135
+ hasErrors && /*#__PURE__*/ jsx(Text, {
136
+ className: `${classBase}-rectifyPrompt`,
137
+ children: "Please rectify and reupload"
64
138
  }),
65
139
  /*#__PURE__*/ jsx(FileDropZoneTrigger, {
66
140
  accept: ".csv,text/csv",
67
141
  onChange: onTriggerChange,
68
142
  children: "BROWSE FILES"
69
- })
70
- ]
71
- }),
72
- error && (renderError ? renderError(error) : /*#__PURE__*/ jsxs("div", {
73
- className: `${classBase}-errors`,
74
- children: [
75
- /*#__PURE__*/ jsx("div", {
76
- children: /*#__PURE__*/ jsx("strong", {
77
- children: "Import Error:"
78
- })
79
143
  }),
80
- /*#__PURE__*/ jsx("div", {
81
- children: error.errors.importError?.message || error.errors.schemaError?.message || error.errors.validationError?.message || "An unexpected error occurred during import."
82
- })
144
+ /*#__PURE__*/ jsx(Text, {
145
+ children: "Only .csv files"
146
+ }),
147
+ children
83
148
  ]
84
- })),
85
- children
86
- ]
149
+ })
150
+ })
87
151
  });
88
152
  const actions = /*#__PURE__*/ jsxs(DialogActions, {
89
153
  children: [
@@ -1,4 +1,4 @@
1
- const CSV_HEADER_ROW_NUMBER = 1;
2
- const CSV_FIRST_DATA_ROW_NUMBER = 2;
1
+ const CSV_HEADER_ROW_NUMBER = 0;
2
+ const CSV_FIRST_DATA_ROW_NUMBER = 1;
3
3
  const MAX_ROWS_IN_CSV = 10000;
4
4
  export { CSV_FIRST_DATA_ROW_NUMBER, CSV_HEADER_ROW_NUMBER, MAX_ROWS_IN_CSV };
@@ -13,8 +13,9 @@ var csv_errors_CsvValidationErrorEnum = /*#__PURE__*/ function(CsvValidationErro
13
13
  CsvValidationErrorEnum["MISSING_KEY_COLUMN"] = "MISSING_KEY_COLUMN";
14
14
  CsvValidationErrorEnum["UNKNOWN_COLUMN"] = "UNKNOWN_COLUMN";
15
15
  CsvValidationErrorEnum["MAX_ROWS_EXCEEDED"] = "MAX_ROWS_EXCEEDED";
16
- CsvValidationErrorEnum["EMPTY_NON_STRING_VALUE"] = "EMPTY_NON_STRING_VALUE";
16
+ CsvValidationErrorEnum["REQUIRED_FIELD_MISSING"] = "REQUIRED_FIELD_MISSING";
17
17
  CsvValidationErrorEnum["TYPE_MISMATCH"] = "TYPE_MISMATCH";
18
+ CsvValidationErrorEnum["CUSTOM_VALIDATION"] = "CUSTOM_VALIDATION";
18
19
  return CsvValidationErrorEnum;
19
20
  }({});
20
21
  const createCsvErrorState = ()=>({
@@ -68,7 +68,7 @@ const parseCsv = (csvText, options)=>{
68
68
  }
69
69
  const rows = records.slice(1);
70
70
  rows.forEach((row, rowIndex)=>{
71
- if (row.length !== header.length) addCsvRowError(errorState, rowIndex + CSV_FIRST_DATA_ROW_NUMBER, "*", CsvParseErrorEnum.ROW_COLUMN_COUNT_MISMATCH, "CSV rows must have the same number of columns as the header.", row.join(","));
71
+ if (row.length !== header.length) addCsvRowError(errorState, rowIndex + CSV_FIRST_DATA_ROW_NUMBER, "*", CsvParseErrorEnum.ROW_COLUMN_COUNT_MISMATCH, "This row has too many or too few columns compared to the header line. Check for extra or missing commas.", row.join(","));
72
72
  });
73
73
  if (hasCsvErrors(errorState.errorMap)) return withError("CSV rows must have the same number of columns as the header.", errorState.errorMap, errorState.errors, header, rows);
74
74
  return {
@@ -4,10 +4,44 @@ import { CSV_FIRST_DATA_ROW_NUMBER, MAX_ROWS_IN_CSV } from "./csv-constants.js";
4
4
  const INTERNAL_KEY_COLUMNS = new Set([
5
5
  "vuuRowNum"
6
6
  ]);
7
+ const getFriendlyTypeName = (type)=>{
8
+ switch(type){
9
+ case "int":
10
+ case "long":
11
+ return "whole number";
12
+ case "double":
13
+ case "number":
14
+ case "decimal":
15
+ case "scaleddecimal":
16
+ case "scaleddecimal2":
17
+ case "scaleddecimal4":
18
+ case "scaleddecimal6":
19
+ case "scaleddecimal8":
20
+ return "decimal or number";
21
+ case "time":
22
+ return "time in 'HH:MM:SS' format";
23
+ case "epochtimestamp":
24
+ case "epochtimestampnano":
25
+ case "date/time":
26
+ return "date/time";
27
+ case "char":
28
+ return "single character";
29
+ case "boolean":
30
+ return "boolean (true/false)";
31
+ case "json":
32
+ return "JSON value";
33
+ default:
34
+ return type;
35
+ }
36
+ };
37
+ const getFriendlyErrorMessage = (errMessage, rawValue, toType)=>{
38
+ if (errMessage.includes("is not a valid")) return `'${rawValue}' is not a valid ${getFriendlyTypeName(toType)}`;
39
+ return errMessage;
40
+ };
7
41
  const validateCsvAgainstSchema = (parsed, tableSchema, options)=>{
8
42
  const schemaColumns = new Map(tableSchema.columns.map((col)=>[
9
43
  col.name,
10
- col.serverDataType
44
+ col
11
45
  ]));
12
46
  const maxRows = options?.maxRows ?? MAX_ROWS_IN_CSV;
13
47
  const errorState = createCsvErrorState();
@@ -20,19 +54,33 @@ const validateCsvAgainstSchema = (parsed, tableSchema, options)=>{
20
54
  parsed.rows.forEach((rowValues, rowIndex)=>{
21
55
  const rowNum = rowIndex + CSV_FIRST_DATA_ROW_NUMBER;
22
56
  const typedRow = {};
57
+ const rawRow = parsed.header.reduce((acc, headerCol, idx)=>{
58
+ acc[headerCol] = rowValues[idx] ?? "";
59
+ return acc;
60
+ }, {});
23
61
  parsed.header.forEach((columnName, columnIndex)=>{
24
- const rawValue = rowValues[columnIndex];
25
- const schemaType = schemaColumns.get(columnName);
26
- if (0 === rawValue.length && "string" !== schemaType) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.EMPTY_NON_STRING_VALUE, "Empty value is not allowed for non-string columns.", rawValue);
62
+ const rawValue = rowValues[columnIndex] ?? "";
63
+ const schemaColumn = schemaColumns.get(columnName);
64
+ const schemaType = schemaColumn?.serverDataType;
65
+ const isRequired = schemaColumn?.required === true;
66
+ if (0 === rawValue.length) {
67
+ if (isRequired) addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.REQUIRED_FIELD_MISSING, "This field is required and cannot be left blank", rawValue);
68
+ return;
69
+ }
27
70
  let value;
28
71
  const toType = schemaType ?? "string";
29
72
  try {
30
73
  value = getTypedValue(rawValue, toType);
31
74
  } catch (err) {
32
- addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, err instanceof Error ? err.message : String(err), rawValue);
75
+ addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, getFriendlyErrorMessage(err instanceof Error ? err.message : String(err), rawValue, toType), rawValue);
33
76
  return;
34
77
  }
35
- if (void 0 === value) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, `Value '${rawValue}' is not a valid ${toType}`, rawValue);
78
+ if (void 0 === value) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, `'${rawValue}' is not a valid ${getFriendlyTypeName(toType)}`, rawValue);
79
+ const customValidator = options?.validators?.[columnName];
80
+ if (customValidator) {
81
+ const validationResult = customValidator(rawValue, columnName, rawRow);
82
+ if (true !== validationResult) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.CUSTOM_VALIDATION, "string" == typeof validationResult ? validationResult : `Value '${rawValue}' failed custom validation.`, rawValue);
83
+ }
36
84
  typedRow[columnName] = value;
37
85
  });
38
86
  typedRows.push(typedRow);
@@ -5,7 +5,7 @@ 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", importSchema, importTable, onImportSessionEnded, onImportSessionStarted, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, rowDefaults })=>{
8
+ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importTable, onImportSessionEnded, onImportSessionReady, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, rowDefaults, validators })=>{
9
9
  const { getServerAPI } = useData();
10
10
  const [validation, setValidation] = useState();
11
11
  const [sessionTable, setSessionTable] = useState();
@@ -120,7 +120,7 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
120
120
  try {
121
121
  await editSession.end(save);
122
122
  } finally{
123
- if ("unsubscribed" !== sessionDataSource.status) sessionDataSource.unsubscribe();
123
+ if ("unsubscribed" !== sessionDataSource.status && "function" == typeof sessionDataSource.unsubscribe) sessionDataSource.unsubscribe();
124
124
  setActiveSessionDataSource(void 0);
125
125
  }
126
126
  onImportSessionEnded?.({
@@ -140,7 +140,7 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
140
140
  if (rowNum < CSV_FIRST_DATA_ROW_NUMBER) continue;
141
141
  const existing = vuuMsgByRow.get(rowNum);
142
142
  const columnError = `${column}: ${message}`;
143
- vuuMsgByRow.set(rowNum, existing ? `${existing}; ${columnError}` : `Row ${rowNum}: ${columnError}`);
143
+ vuuMsgByRow.set(rowNum, existing ? `${existing}; ${columnError}` : columnError);
144
144
  }
145
145
  const parsedRowCount = mergedValidation.rows.length;
146
146
  const unparsedErrorRows = [
@@ -174,7 +174,7 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
174
174
  if (isRpcError(result)) throw Error(result.errorMessage);
175
175
  if ("string" == typeof result) throw Error(result);
176
176
  } catch (error) {
177
- rpcErrors.push(`Row ${rowNum}: ${toErrorMessage(error)}`);
177
+ rpcErrors.push(`${0 === rowNum ? "Header" : `Row ${rowNum}`}: ${toErrorMessage(error)}`);
178
178
  }
179
179
  }
180
180
  if (rpcErrors.length > 0) throw Error(buildRowErrorMessage("Import failed", rpcErrors));
@@ -187,11 +187,9 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
187
187
  const sessionVuuTable = sessionDataSource?.table;
188
188
  if (void 0 === sessionDataSource || void 0 === sessionVuuTable || !isSessionTable(sessionVuuTable)) throw Error("CsvUpload createSessionDataSource returned no session datasource.");
189
189
  setActiveSessionDataSource(sessionDataSource);
190
- onImportSessionStarted?.(sessionDataSource);
191
190
  return sessionDataSource;
192
191
  }, [
193
192
  editSession,
194
- onImportSessionStarted,
195
193
  setActiveSessionDataSource
196
194
  ]);
197
195
  const closePendingEditSession = useCallback(async (save)=>{
@@ -240,17 +238,15 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
240
238
  return;
241
239
  }
242
240
  const schemaValidation = validateCsvAgainstSchema(parsedCsv, schema, {
243
- maxRows
241
+ maxRows,
242
+ validators
244
243
  });
245
244
  if (Object.keys(schemaValidation.errorMap.fileErrors).length > 0) {
246
245
  setValidation(schemaValidation);
246
+ const detailedMessage = `CSV validation failed: ${schemaValidation.errors.map((e)=>"*" === e.column ? e.message : `${e.column}: ${e.message}`).join("; ")}`;
247
247
  handleError({
248
248
  errors: {
249
- schemaError: createUploadError("schema", "CSV validation failed.", parsedCsv.error, {
250
- errorMap: schemaValidation.errorMap,
251
- errors: schemaValidation.errors,
252
- message: "CSV validation failed."
253
- })
249
+ schemaError: createUploadError("schema", detailedMessage, parsedCsv.error, schemaValidation)
254
250
  }
255
251
  });
256
252
  return;
@@ -258,10 +254,11 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
258
254
  const mergedValidation = mergeValidationWithParseErrors(schemaValidation, parsedCsv.error);
259
255
  setValidation(mergedValidation);
260
256
  if (mergedValidation.rows.length > 0) try {
261
- await beginEditSession();
257
+ const sessionDataSource = await beginEditSession();
262
258
  if (operationId !== operationIdRef.current) return;
263
259
  const rowsAdded = await addAllRows(mergedValidation, operationId);
264
260
  if (!rowsAdded) return;
261
+ onImportSessionReady?.(sessionDataSource);
265
262
  } catch (error) {
266
263
  if (void 0 !== sessionDataSourceRef.current) await endEditSessionAndNotify(false, "failed");
267
264
  else try {
@@ -288,7 +285,9 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
288
285
  dataSource.table,
289
286
  table,
290
287
  endEditSessionAndNotify,
291
- schema
288
+ schema,
289
+ validators,
290
+ onImportSessionReady
292
291
  ]);
293
292
  const handleFiles = useCallback(async (files)=>{
294
293
  const file = files[0];
@@ -331,10 +330,11 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
331
330
  }, [
332
331
  handleFiles
333
332
  ]);
334
- const canImport = useMemo(()=>void 0 !== validation && 0 === validation.errors.length && validation.rows.length > 0 && !isProcessingFile && !isImporting, [
333
+ const canImport = useMemo(()=>void 0 !== validation && 0 === validation.errors.length && validation.rows.length > 0 && !isProcessingFile && !isImporting && void 0 === error, [
335
334
  isImporting,
336
335
  isProcessingFile,
337
- validation
336
+ validation,
337
+ error
338
338
  ]);
339
339
  const importData = useCallback(async ()=>{
340
340
  if (!canImport || void 0 === validation) return false;
@@ -1,5 +1,5 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Tab, TabBar, TabList, TabPanel, TabTrigger, Tabs } from "@salt-ds/core";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Button, Tab, TabBar, TabList, TabPanel, TabTrigger, Tabs } from "@salt-ds/core";
3
3
  import { useComponentCssInjection } from "@salt-ds/styles";
4
4
  import { useWindow } from "@salt-ds/window";
5
5
  import clsx from "clsx";
@@ -78,12 +78,21 @@ const TabbedTableConfigPanel_TabbedTableConfigPanel = ({ allowCreateCalculatedCo
78
78
  }),
79
79
  /*#__PURE__*/ jsx(TabPanel, {
80
80
  value: "table-columns",
81
- children: /*#__PURE__*/ jsx(ColumnPicker, {
82
- allowCreateCalculatedColumn: allowCreateCalculatedColumn,
83
- columnModel: columnModel,
84
- onClickCreateCalculatedColumn: onCreateCalculatedColumn,
85
- onSelectionChange: handleSelectionChange,
86
- selected: columns
81
+ children: /*#__PURE__*/ jsxs(Fragment, {
82
+ children: [
83
+ /*#__PURE__*/ jsx(ColumnPicker, {
84
+ columnModel: columnModel,
85
+ onSelectionChange: handleSelectionChange,
86
+ selected: columns
87
+ }),
88
+ allowCreateCalculatedColumn ? /*#__PURE__*/ jsx("div", {
89
+ className: `${classBase}-column-buttons`,
90
+ children: /*#__PURE__*/ jsx(Button, {
91
+ onClick: onCreateCalculatedColumn,
92
+ children: "Create calculated column"
93
+ })
94
+ }) : null
95
+ ]
87
96
  })
88
97
  }),
89
98
  /*#__PURE__*/ jsx(TabPanel, {
@@ -1,6 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { useCallback, useRef } from "react";
3
2
  import { useContextPanel } from "@vuu-ui/vuu-ui-controls";
3
+ import { useCallback, useRef } from "react";
4
4
  import { TabbedTableConfigPanel } from "./TabbedTableConfigPanel.js";
5
5
  const useTabbedTableConfigPanel = ({ allowCreateCalculatedColumn, columnModel, config, onDisplayAttributeChange, vuuTable })=>{
6
6
  const showContextPanel = useContextPanel();
@@ -1,6 +1,6 @@
1
- import { HTMLAttributes } from "react";
2
- import { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3
1
  import { VuuColumnDataType, VuuTable } from "@vuu-ui/vuu-protocol-types";
2
+ import { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3
+ import { HTMLAttributes } from "react";
4
4
  import { ColumnModel } from "../column-picker/ColumnModel";
5
5
  export interface CalculatedColumnPanelProps extends HTMLAttributes<HTMLDivElement> {
6
6
  column: ColumnDescriptor;
@@ -58,12 +58,13 @@ export declare class ColumnModel extends EventEmitter<ColumnEvents> {
58
58
  * Columns already selected and rendered in Table.
59
59
  */
60
60
  selectedColumns: readonly ColumnDescriptor[]);
61
- get availableColumns(): ColumnDescriptor[];
62
- set searchPattern(pattern: string);
63
- get searchPattern(): string;
64
61
  get selectedColumns(): readonly ColumnDescriptor[];
65
62
  setSelectedColumns(selectedColumns: readonly ColumnDescriptor[], source: ColumnChangeSource, changeDescriptor?: SelectedColumnChangeDescriptor): void;
66
63
  getColumn(name: string): ColumnDescriptor;
64
+ /**
65
+ * Introduced for use by ColumnPicker
66
+ */
67
+ addRemoveOrReorderSelectedColumns(newSelectedColumns: readonly ColumnDescriptor[], source: ColumnChangeSource): void;
67
68
  addItemToSelectedColumns(name: string, source: ColumnChangeSource): void;
68
69
  removeItemFromSelectedColumns(name: string, source: ColumnChangeSource): void;
69
70
  reorderSelectedColumns(orderedColumnNames: string[], source: ColumnChangeSource): void;
@@ -1,10 +1,8 @@
1
- import { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
2
- import { HTMLAttributes, MouseEventHandler } from "react";
3
- import { ListBoxProps } from "@salt-ds/core";
4
- import { ColumnPickerHookProps } from "./useColumnPicker";
1
+ import type { ListBoxProps } from "@salt-ds/core";
2
+ import type { ColumnDescriptor } from "@vuu-ui/vuu-table-types";
3
+ import { type HTMLAttributes } from "react";
4
+ import { type ColumnPickerHookProps } from "./useColumnPicker";
5
5
  export declare const classBaseListItem = "vuuColumnPickerListItem";
6
6
  export interface ColumnPickerProps extends ColumnPickerHookProps, Pick<ListBoxProps<ColumnDescriptor>, "selected" | "onSelectionChange">, HTMLAttributes<HTMLDivElement> {
7
- allowCreateCalculatedColumn?: boolean;
8
- onClickCreateCalculatedColumn?: MouseEventHandler<HTMLButtonElement>;
9
7
  }
10
8
  export declare const ColumnPicker: import("react").ForwardRefExoticComponent<ColumnPickerProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,16 +1,13 @@
1
- import { ColumnDescriptor, TableSelectionModel } from "@vuu-ui/vuu-table-types";
2
- import { FormEventHandler, MouseEventHandler } from "react";
3
- import { ColumnModel, SelectedColumnChangeType } from "./ColumnModel";
1
+ import { TableSelectionModel } from "@vuu-ui/vuu-table-types";
2
+ import { ItemDescriptor } from "@vuu-ui/vuu-ui-controls";
3
+ import { ColumnModel } from "./ColumnModel";
4
4
  export type ColumnSelectionModel = Extract<TableSelectionModel, "none" | "single">;
5
- export type SelectedColumnsChangeHandler = (columns: ColumnDescriptor[], changeType: SelectedColumnChangeType) => void;
6
5
  export interface ColumnPickerHookProps {
7
6
  columnModel: ColumnModel;
8
7
  }
9
8
  export declare const useColumnPicker: ({ columnModel: model, }: ColumnPickerHookProps) => {
10
- availableColumns: ColumnDescriptor[];
11
- onAddItemToSelectedList: MouseEventHandler<HTMLButtonElement>;
12
- onRemoveItemFromSelectedList: MouseEventHandler<HTMLButtonElement>;
13
- onChangeSearchInput: FormEventHandler;
14
- searchText: string;
15
- selectedColumns: readonly ColumnDescriptor[];
9
+ allItems: ItemDescriptor[];
10
+ selectedItems: ItemDescriptor[];
11
+ oneOrMoreColumnsIsCalculated: boolean;
12
+ handleSelectedItemsChange: (newSelectedItems: readonly ItemDescriptor[]) => void;
16
13
  };
@@ -1,8 +1,8 @@
1
- import { ColumnDescriptor, TableConfig, ColumnTypeFormatting } from "@vuu-ui/vuu-table-types";
2
- import { CellRendererDescriptor, ColumnRenderPropsChangeHandler } from "@vuu-ui/vuu-utils";
1
+ import { ColumnDescriptor, ColumnTypeFormatting, TableConfig } from "@vuu-ui/vuu-table-types";
2
+ import { DataValueTypeSimple } from "@vuu-ui/vuu-data-types";
3
3
  import { VuuColumnDataType, VuuTable } from "@vuu-ui/vuu-protocol-types";
4
+ import { CellRendererDescriptor, ColumnRenderPropsChangeHandler } from "@vuu-ui/vuu-utils";
4
5
  import { FormEventHandler } from "react";
5
- import { DataValueTypeSimple } from "@vuu-ui/vuu-data-types";
6
6
  import { ColumnModel } from "../column-picker/ColumnModel";
7
7
  /**
8
8
  * Describes the props for a Column Configuration Editor, for which
@@ -1,7 +1,7 @@
1
1
  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
- import type { CsvValidationStructuredError } from "./parse/csv-schema-validation";
4
+ import type { CsvValidationStructuredError, CsvColumnValidator } from "./parse/csv-schema-validation";
5
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";
@@ -46,7 +46,7 @@ export interface CsvUploadProps {
46
46
  importSchema?: TableSchema;
47
47
  /** Expected import table, used to validate the session table returned by the server. */
48
48
  importTable?: VuuTable;
49
- onImportSessionStarted?: (dataSource: DataSource) => void;
49
+ onImportSessionReady?: (dataSource: DataSource) => void;
50
50
  onImportSessionEnded?: (result: CsvUploadSessionEndResult) => void;
51
51
  onError?: (result: CsvUploadErrorResult | undefined) => void;
52
52
  onImported?: (result: CsvUploadImportedResult) => void;
@@ -60,9 +60,6 @@ export interface CsvUploadProps {
60
60
  parseOptions?: CsvParseOptions;
61
61
  importMode?: "direct" | "preview";
62
62
  rowDefaults?: RowDefaultDataItemValues;
63
- /**
64
- * Custom renderer for the error panel.
65
- */
66
- renderError?: (error: CsvUploadErrorResult) => ReactNode;
63
+ validators?: Record<string, CsvColumnValidator>;
67
64
  }
68
65
  export declare const CsvUpload: (props: CsvUploadProps) => import("react").JSX.Element;
@@ -11,7 +11,7 @@ export declare const useDataUploadPreview: ({ dataSource, editSession, onClose,
11
11
  canSave: boolean;
12
12
  hasSelection: boolean;
13
13
  onCancel: () => Promise<void>;
14
- onDelete: () => Promise<void>;
14
+ onDelete: () => Promise<import("@vuu-ui/vuu-protocol-types").RpcResult>;
15
15
  onSave: (force?: boolean) => Promise<void>;
16
16
  sessionError: string | undefined;
17
17
  };
@@ -1,7 +1,7 @@
1
1
  export { CsvUpload, type CsvUploadError, type CsvUploadProps, type CsvUploadErrorResult, type CsvUploadErrors, type CsvUploadPhase, type CsvUploadImportedResult, type CsvUploadPreviewResult, type CsvUploadSessionEndReason, type CsvUploadSessionEndResult, type CsvUploadSessionTable, } from "./CsvUpload";
2
2
  export { useCsvUpload, type CsvUploadHookProps, type UseCsvUploadReturn, } from "./useCsvUpload";
3
3
  export { DataUploadPreview, type DataUploadPreviewProps, } from "./data-upload-preview/DataUploadPreview";
4
- export { validateCsvAgainstSchema, type CsvValidationOptions, type CsvValidationResult, } from "./parse/csv-schema-validation";
4
+ export { validateCsvAgainstSchema, type CsvColumnValidator, type CsvValidationOptions, type CsvValidationResult, } from "./parse/csv-schema-validation";
5
5
  export { parseCsv, type CsvParseOptions, type CsvParseResult, } from "./parse/csv-parse";
6
6
  export { CsvValidationErrorEnum, CsvParseErrorEnum, type CsvError, type CsvErrorMap, type CsvStructuredError, type CsvValidationError, type CsvValidationErrorMap, type CsvValidationErrorType, type CsvValidationStructuredError, type CsvParseError, type CsvParseErrorMap, } from "./parse/csv-errors";
7
7
  export { CSV_FIRST_DATA_ROW_NUMBER, CSV_HEADER_ROW_NUMBER, } from "./parse/csv-constants";
@@ -1,3 +1,3 @@
1
- export declare const CSV_HEADER_ROW_NUMBER = 1;
2
- export declare const CSV_FIRST_DATA_ROW_NUMBER = 2;
1
+ export declare const CSV_HEADER_ROW_NUMBER = 0;
2
+ export declare const CSV_FIRST_DATA_ROW_NUMBER = 1;
3
3
  export declare const MAX_ROWS_IN_CSV = 10000;
@@ -11,8 +11,9 @@ export declare enum CsvValidationErrorEnum {
11
11
  MISSING_KEY_COLUMN = "MISSING_KEY_COLUMN",
12
12
  UNKNOWN_COLUMN = "UNKNOWN_COLUMN",
13
13
  MAX_ROWS_EXCEEDED = "MAX_ROWS_EXCEEDED",
14
- EMPTY_NON_STRING_VALUE = "EMPTY_NON_STRING_VALUE",
15
- TYPE_MISMATCH = "TYPE_MISMATCH"
14
+ REQUIRED_FIELD_MISSING = "REQUIRED_FIELD_MISSING",
15
+ TYPE_MISMATCH = "TYPE_MISMATCH",
16
+ CUSTOM_VALIDATION = "CUSTOM_VALIDATION"
16
17
  }
17
18
  export type CsvErrorMap<TError extends string> = {
18
19
  rowErrors: Record<number, Record<string, TError[]>>;
@@ -1,8 +1,15 @@
1
- import type { TableSchema } from "@vuu-ui/vuu-data-types";
1
+ import type { TableSchema, SchemaColumn } from "@vuu-ui/vuu-data-types";
2
2
  import type { VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
3
3
  import type { CsvValidationError, CsvValidationErrorMap, CsvParseError } from "./csv-errors";
4
4
  import type { CsvParseResult } from "./csv-parse";
5
5
  export { CsvValidationErrorEnum, type CsvValidationError, type CsvValidationErrorMap, type CsvValidationErrorType, type CsvValidationStructuredError, } from "./csv-errors";
6
+ export interface ImportSchemaColumn extends SchemaColumn {
7
+ required?: boolean;
8
+ }
9
+ export interface ImportTableSchema extends Omit<TableSchema, "columns"> {
10
+ readonly columns: readonly ImportSchemaColumn[];
11
+ }
12
+ export type CsvColumnValidator = (value: string, columnName: string, row: Record<string, string>) => boolean | string;
6
13
  export type CsvValidationResult = {
7
14
  columns: string[];
8
15
  errorMap: CsvValidationErrorMap;
@@ -12,5 +19,6 @@ export type CsvValidationResult = {
12
19
  };
13
20
  export type CsvValidationOptions = {
14
21
  maxRows?: number;
22
+ validators?: Record<string, CsvColumnValidator>;
15
23
  };
16
- export declare const validateCsvAgainstSchema: (parsed: CsvParseResult, tableSchema: TableSchema, options?: CsvValidationOptions) => CsvValidationResult;
24
+ export declare const validateCsvAgainstSchema: (parsed: CsvParseResult, tableSchema: TableSchema | ImportTableSchema, options?: CsvValidationOptions) => CsvValidationResult;