@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.
- package/package.json +14 -12
- package/src/calculated-column/CalculatedColumnPanel.js +2 -2
- package/src/calculated-column/useEditCalculatedColumn.js +1 -1
- package/src/column-picker/ColumnModel.js +14 -25
- package/src/column-picker/ColumnPicker.css.js +0 -86
- package/src/column-picker/ColumnPicker.js +12 -180
- package/src/column-picker/useColumnPicker.js +28 -22
- package/src/csv-upload/CsvUpload.css.js +34 -0
- package/src/csv-upload/CsvUpload.js +109 -45
- package/src/csv-upload/parse/csv-constants.js +2 -2
- package/src/csv-upload/parse/csv-errors.js +2 -1
- package/src/csv-upload/parse/csv-parse.js +1 -1
- package/src/csv-upload/parse/csv-schema-validation.js +54 -6
- package/src/csv-upload/useCsvUpload.js +16 -16
- package/src/tabbed-table-config-panel/TabbedTableConfigPanel.js +17 -8
- package/src/tabbed-table-config-panel/useTabbedTableConfigPanel.js +1 -1
- package/types/calculated-column/CalculatedColumnPanel.d.ts +2 -2
- package/types/column-picker/ColumnModel.d.ts +4 -3
- package/types/column-picker/ColumnPicker.d.ts +4 -6
- package/types/column-picker/useColumnPicker.d.ts +7 -10
- package/types/column-settings-panel/useColumnSettings.d.ts +3 -3
- package/types/csv-upload/CsvUpload.d.ts +3 -6
- package/types/csv-upload/data-upload-preview/useDataUploadPreview.d.ts +1 -1
- package/types/csv-upload/index.d.ts +1 -1
- package/types/csv-upload/parse/csv-constants.d.ts +2 -2
- package/types/csv-upload/parse/csv-errors.d.ts +3 -2
- package/types/csv-upload/parse/csv-schema-validation.d.ts +10 -2
- package/types/csv-upload/parse/index.d.ts +1 -1
- package/types/csv-upload/useCsvUpload.d.ts +4 -3
- package/types/index.d.ts +0 -1
- package/types/src/csv-upload/CsvUpload.d.ts +2 -1
- package/types/src/csv-upload/index.d.ts +1 -1
- package/types/src/csv-upload/parse/csv-constants.d.ts +2 -2
- package/types/src/csv-upload/parse/csv-errors.d.ts +2 -1
- package/types/src/csv-upload/parse/csv-schema-validation.d.ts +2 -0
- package/types/src/csv-upload/parse/index.d.ts +1 -1
- package/types/src/csv-upload/useCsvUpload.d.ts +3 -2
- 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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
81
|
-
children:
|
|
82
|
-
})
|
|
144
|
+
/*#__PURE__*/ jsx(Text, {
|
|
145
|
+
children: "Only .csv files"
|
|
146
|
+
}),
|
|
147
|
+
children
|
|
83
148
|
]
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
]
|
|
149
|
+
})
|
|
150
|
+
})
|
|
87
151
|
});
|
|
88
152
|
const actions = /*#__PURE__*/ jsxs(DialogActions, {
|
|
89
153
|
children: [
|
|
@@ -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["
|
|
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, "
|
|
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
|
|
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
|
|
26
|
-
|
|
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, `
|
|
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,
|
|
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}` :
|
|
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",
|
|
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__*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import { 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
|
2
|
-
import {
|
|
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
|
-
|
|
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<
|
|
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 =
|
|
2
|
-
export declare const CSV_FIRST_DATA_ROW_NUMBER =
|
|
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
|
-
|
|
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;
|