@vuu-ui/vuu-table-extras 3.3.12 → 3.3.14
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 +12 -12
- package/src/column-picker/ColumnModel.js +3 -2
- package/src/csv-upload/CsvUpload.css.js +34 -0
- package/src/csv-upload/CsvUpload.js +107 -63
- package/src/csv-upload/parse/csv-parse.js +1 -1
- package/src/csv-upload/parse/csv-schema-validation.js +39 -5
- package/src/csv-upload/useCsvUpload.js +12 -14
- package/types/csv-upload/CsvUpload.d.ts +1 -5
- package/types/csv-upload/useCsvUpload.d.ts +2 -2
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.3.
|
|
2
|
+
"version": "3.3.14",
|
|
3
3
|
"author": "heswell",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
@@ -11,19 +11,19 @@
|
|
|
11
11
|
"lezer-generate:column": "lezer-generator --output ./src/column-expression-input/column-language-parser/generated/column-parser.js ./src/column-expression-input/column-language-parser/grammar/column.grammar"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@vuu-ui/vuu-filter-types": "3.3.
|
|
15
|
-
"@vuu-ui/vuu-protocol-types": "3.3.
|
|
14
|
+
"@vuu-ui/vuu-filter-types": "3.3.14",
|
|
15
|
+
"@vuu-ui/vuu-protocol-types": "3.3.14"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@vuu-ui/vuu-codemirror": "3.3.
|
|
19
|
-
"@vuu-ui/vuu-data-editing": "3.3.
|
|
20
|
-
"@vuu-ui/vuu-data-react": "3.3.
|
|
21
|
-
"@vuu-ui/vuu-data-types": "3.3.
|
|
22
|
-
"@vuu-ui/vuu-table-types": "3.3.
|
|
23
|
-
"@vuu-ui/vuu-popups": "3.3.
|
|
24
|
-
"@vuu-ui/vuu-table": "3.3.
|
|
25
|
-
"@vuu-ui/vuu-utils": "3.3.
|
|
26
|
-
"@vuu-ui/vuu-ui-controls": "3.3.
|
|
18
|
+
"@vuu-ui/vuu-codemirror": "3.3.14",
|
|
19
|
+
"@vuu-ui/vuu-data-editing": "3.3.14",
|
|
20
|
+
"@vuu-ui/vuu-data-react": "3.3.14",
|
|
21
|
+
"@vuu-ui/vuu-data-types": "3.3.14",
|
|
22
|
+
"@vuu-ui/vuu-table-types": "3.3.14",
|
|
23
|
+
"@vuu-ui/vuu-popups": "3.3.14",
|
|
24
|
+
"@vuu-ui/vuu-table": "3.3.14",
|
|
25
|
+
"@vuu-ui/vuu-utils": "3.3.14",
|
|
26
|
+
"@vuu-ui/vuu-ui-controls": "3.3.14",
|
|
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",
|
|
@@ -15,6 +15,7 @@ const isColumnAdded = (change)=>change?.type === SelectedColumnChangeType.Column
|
|
|
15
15
|
const isColumnRemoved = (change)=>change?.type === SelectedColumnChangeType.ColumnRemoved;
|
|
16
16
|
const isColumnUpdated = (change)=>change?.type === SelectedColumnChangeType.ColumnUpdated;
|
|
17
17
|
const isColumnsReordered = (change)=>change?.type === SelectedColumnChangeType.ColumnsReordered;
|
|
18
|
+
const columnDescriptorComparator = (col1, col2)=>col1.name === col2.name;
|
|
18
19
|
class ColumnModel extends EventEmitter {
|
|
19
20
|
allColumns;
|
|
20
21
|
#selectedColumns;
|
|
@@ -36,10 +37,10 @@ class ColumnModel extends EventEmitter {
|
|
|
36
37
|
}
|
|
37
38
|
addRemoveOrReorderSelectedColumns(newSelectedColumns, source) {
|
|
38
39
|
if (!itemsOrOrderChanged(this.#selectedColumns, newSelectedColumns)) throw Error("[ColumnModel] addRemoveOrReorderSelectedColumns no change detected between current and new selected columns");
|
|
39
|
-
const addedColumns = getAddedItems(this.#selectedColumns, newSelectedColumns);
|
|
40
|
+
const addedColumns = getAddedItems(this.#selectedColumns, newSelectedColumns, columnDescriptorComparator);
|
|
40
41
|
if (addedColumns.length > 0) if (1 == addedColumns.length) return void this.addItemToSelectedColumns(addedColumns[0].name, source);
|
|
41
42
|
else throw Error("[ColumnModel] addRemoveOrReorderSelectedColumns attempt to add multiple selected columns in a single call");
|
|
42
|
-
const removedColumns = getRemovedItems(this.#selectedColumns, newSelectedColumns);
|
|
43
|
+
const removedColumns = getRemovedItems(this.#selectedColumns, newSelectedColumns, columnDescriptorComparator);
|
|
43
44
|
if (removedColumns.length > 0) if (1 == removedColumns.length) return void this.removeItemFromSelectedColumns(removedColumns[0].name, source);
|
|
44
45
|
else throw Error("[ColumnModel] addRemoveOrReorderSelectedColumns attempt to remove multiple selected columns in a single call");
|
|
45
46
|
this.reorderSelectedColumns(newSelectedColumns.map(({ name })=>name), source);
|
|
@@ -14,6 +14,40 @@ const css = `
|
|
|
14
14
|
display: grid;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
.vuuCsvUpload-errorContainer {
|
|
18
|
+
margin-top: var(--salt-spacing-100);
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-width: 480px;
|
|
21
|
+
max-height: 120px;
|
|
22
|
+
padding: var(--salt-spacing-100) var(--salt-spacing-200);
|
|
23
|
+
background-color: rgba(var(--salt-palette-interact-error-background), .05);
|
|
24
|
+
border-radius: var(--salt-size-borderRadius);
|
|
25
|
+
text-align: left;
|
|
26
|
+
overflow-y: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.vuuCsvUpload-errorList {
|
|
30
|
+
padding-left: var(--salt-spacing-200);
|
|
31
|
+
margin: 0;
|
|
32
|
+
list-style-type: disc;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.vuuCsvUpload-errorItem {
|
|
36
|
+
font-size: var(--salt-text-fontSize-small);
|
|
37
|
+
color: var(--salt-status-error-foreground);
|
|
38
|
+
line-height: var(--salt-text-lineHeight-small);
|
|
39
|
+
word-break: break-word;
|
|
40
|
+
margin-bottom: var(--salt-spacing-50);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.vuuCsvUpload-importErrorItem {
|
|
44
|
+
text-align: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.vuuCsvUpload-rectifyPrompt {
|
|
48
|
+
margin-top: var(--salt-spacing-100);
|
|
49
|
+
}
|
|
50
|
+
|
|
17
51
|
.vuuCsvUpload-errors {
|
|
18
52
|
border: solid 1px var(--salt-status-error-borderColor);
|
|
19
53
|
border-radius: var(--salt-size-borderRadius);
|
|
@@ -2,12 +2,12 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
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,7 +18,23 @@ 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
|
|
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
|
+
]);
|
|
22
38
|
const hasErrors = !!error || validation && validation.errors.length > 0;
|
|
23
39
|
const isUploaded = canImport || isImporting;
|
|
24
40
|
const status = hasErrors ? "error" : isUploaded ? "success" : void 0;
|
|
@@ -37,73 +53,101 @@ const CsvUpload_CsvUpload = (props)=>{
|
|
|
37
53
|
importData,
|
|
38
54
|
onClose
|
|
39
55
|
]);
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}),
|
|
53
|
-
/*#__PURE__*/ jsx("strong", {
|
|
54
|
-
children: "Upload completed"
|
|
55
|
-
})
|
|
56
|
-
]
|
|
57
|
-
}) : /*#__PURE__*/ jsxs(Fragment, {
|
|
58
|
-
children: [
|
|
59
|
-
/*#__PURE__*/ jsx(FileDropZoneIcon, {
|
|
60
|
-
status: hasErrors ? "error" : void 0
|
|
61
|
-
}),
|
|
62
|
-
validation && validation.errors.length > 0 ? /*#__PURE__*/ jsxs(Fragment, {
|
|
63
|
-
children: [
|
|
64
|
-
/*#__PURE__*/ jsx("div", {
|
|
65
|
-
children: "Your file contains errors"
|
|
66
|
-
}),
|
|
67
|
-
fileErrors.length > 0 && /*#__PURE__*/ jsx("ul", {
|
|
68
|
-
className: `${classBase}-errorList`,
|
|
69
|
-
children: fileErrors.map((error, i)=>/*#__PURE__*/ jsx("li", {
|
|
70
|
-
className: `${classBase}-errorItem`,
|
|
71
|
-
children: error.message
|
|
72
|
-
}, `${error.column}-${error.message}-${i}`))
|
|
73
|
-
}),
|
|
74
|
-
/*#__PURE__*/ jsx("div", {
|
|
75
|
-
children: "Please rectify and reupload"
|
|
76
|
-
})
|
|
77
|
-
]
|
|
78
|
-
}) : /*#__PURE__*/ jsx("div", {
|
|
79
|
-
children: "Drop a file here or"
|
|
80
|
-
}),
|
|
81
|
-
/*#__PURE__*/ jsx(FileDropZoneTrigger, {
|
|
82
|
-
accept: ".csv,text/csv",
|
|
83
|
-
onChange: onTriggerChange,
|
|
84
|
-
children: "BROWSE FILES"
|
|
85
|
-
}),
|
|
86
|
-
/*#__PURE__*/ jsx(Text, {
|
|
87
|
-
children: "Only .csv files"
|
|
88
|
-
}),
|
|
89
|
-
children
|
|
90
|
-
]
|
|
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
|
+
})
|
|
91
68
|
})
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
if (hasErrors) {
|
|
72
|
+
const headingText = error ? "Validation failed" : "Your file contains errors";
|
|
73
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
95
74
|
children: [
|
|
75
|
+
/*#__PURE__*/ jsx("strong", {
|
|
76
|
+
children: headingText
|
|
77
|
+
}),
|
|
96
78
|
/*#__PURE__*/ jsx("div", {
|
|
97
|
-
|
|
98
|
-
|
|
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
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", {
|
|
114
|
+
className: classBase,
|
|
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"
|
|
100
124
|
}),
|
|
101
|
-
/*#__PURE__*/ jsx("
|
|
102
|
-
children:
|
|
125
|
+
/*#__PURE__*/ jsx("strong", {
|
|
126
|
+
children: "Upload completed"
|
|
103
127
|
})
|
|
104
128
|
]
|
|
105
|
-
})
|
|
106
|
-
|
|
129
|
+
}) : /*#__PURE__*/ jsxs(Fragment, {
|
|
130
|
+
children: [
|
|
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"
|
|
138
|
+
}),
|
|
139
|
+
/*#__PURE__*/ jsx(FileDropZoneTrigger, {
|
|
140
|
+
accept: ".csv,text/csv",
|
|
141
|
+
onChange: onTriggerChange,
|
|
142
|
+
children: "BROWSE FILES"
|
|
143
|
+
}),
|
|
144
|
+
/*#__PURE__*/ jsx(Text, {
|
|
145
|
+
children: "Only .csv files"
|
|
146
|
+
}),
|
|
147
|
+
children
|
|
148
|
+
]
|
|
149
|
+
})
|
|
150
|
+
})
|
|
107
151
|
});
|
|
108
152
|
const actions = /*#__PURE__*/ jsxs(DialogActions, {
|
|
109
153
|
children: [
|
|
@@ -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,6 +4,40 @@ 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,
|
|
@@ -21,22 +55,22 @@ const validateCsvAgainstSchema = (parsed, tableSchema, options)=>{
|
|
|
21
55
|
const rowNum = rowIndex + CSV_FIRST_DATA_ROW_NUMBER;
|
|
22
56
|
const typedRow = {};
|
|
23
57
|
const rawRow = parsed.header.reduce((acc, headerCol, idx)=>{
|
|
24
|
-
acc[headerCol] = rowValues[idx];
|
|
58
|
+
acc[headerCol] = rowValues[idx] ?? "";
|
|
25
59
|
return acc;
|
|
26
60
|
}, {});
|
|
27
61
|
parsed.header.forEach((columnName, columnIndex)=>{
|
|
28
|
-
const rawValue = rowValues[columnIndex];
|
|
62
|
+
const rawValue = rowValues[columnIndex] ?? "";
|
|
29
63
|
const schemaType = schemaColumns.get(columnName);
|
|
30
|
-
if (0 === rawValue.length && "string" !== schemaType) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.EMPTY_NON_STRING_VALUE, "
|
|
64
|
+
if (0 === rawValue.length && "string" !== schemaType) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.EMPTY_NON_STRING_VALUE, "This field is required and cannot be left blank", rawValue);
|
|
31
65
|
let value;
|
|
32
66
|
const toType = schemaType ?? "string";
|
|
33
67
|
try {
|
|
34
68
|
value = getTypedValue(rawValue, toType);
|
|
35
69
|
} catch (err) {
|
|
36
|
-
addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, err instanceof Error ? err.message : String(err), rawValue);
|
|
70
|
+
addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, getFriendlyErrorMessage(err instanceof Error ? err.message : String(err), rawValue, toType), rawValue);
|
|
37
71
|
return;
|
|
38
72
|
}
|
|
39
|
-
if (void 0 === value) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, `
|
|
73
|
+
if (void 0 === value) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, `'${rawValue}' is not a valid ${getFriendlyTypeName(toType)}`, rawValue);
|
|
40
74
|
const customValidator = options?.validators?.[columnName];
|
|
41
75
|
if (customValidator) {
|
|
42
76
|
const validationResult = customValidator(rawValue, columnName, rawRow);
|
|
@@ -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 = [
|
|
@@ -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)=>{
|
|
@@ -245,13 +243,10 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
|
|
|
245
243
|
});
|
|
246
244
|
if (Object.keys(schemaValidation.errorMap.fileErrors).length > 0) {
|
|
247
245
|
setValidation(schemaValidation);
|
|
246
|
+
const detailedMessage = `CSV validation failed: ${schemaValidation.errors.map((e)=>"*" === e.column ? e.message : `${e.column}: ${e.message}`).join("; ")}`;
|
|
248
247
|
handleError({
|
|
249
248
|
errors: {
|
|
250
|
-
schemaError: createUploadError("schema",
|
|
251
|
-
errorMap: schemaValidation.errorMap,
|
|
252
|
-
errors: schemaValidation.errors,
|
|
253
|
-
message: "CSV validation failed."
|
|
254
|
-
})
|
|
249
|
+
schemaError: createUploadError("schema", detailedMessage, parsedCsv.error, schemaValidation)
|
|
255
250
|
}
|
|
256
251
|
});
|
|
257
252
|
return;
|
|
@@ -259,10 +254,11 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
|
|
|
259
254
|
const mergedValidation = mergeValidationWithParseErrors(schemaValidation, parsedCsv.error);
|
|
260
255
|
setValidation(mergedValidation);
|
|
261
256
|
if (mergedValidation.rows.length > 0) try {
|
|
262
|
-
await beginEditSession();
|
|
257
|
+
const sessionDataSource = await beginEditSession();
|
|
263
258
|
if (operationId !== operationIdRef.current) return;
|
|
264
259
|
const rowsAdded = await addAllRows(mergedValidation, operationId);
|
|
265
260
|
if (!rowsAdded) return;
|
|
261
|
+
onImportSessionReady?.(sessionDataSource);
|
|
266
262
|
} catch (error) {
|
|
267
263
|
if (void 0 !== sessionDataSourceRef.current) await endEditSessionAndNotify(false, "failed");
|
|
268
264
|
else try {
|
|
@@ -290,7 +286,8 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
|
|
|
290
286
|
table,
|
|
291
287
|
endEditSessionAndNotify,
|
|
292
288
|
schema,
|
|
293
|
-
validators
|
|
289
|
+
validators,
|
|
290
|
+
onImportSessionReady
|
|
294
291
|
]);
|
|
295
292
|
const handleFiles = useCallback(async (files)=>{
|
|
296
293
|
const file = files[0];
|
|
@@ -333,10 +330,11 @@ const useCsvUpload = ({ dataSource, importMode = "direct", importSchema, importT
|
|
|
333
330
|
}, [
|
|
334
331
|
handleFiles
|
|
335
332
|
]);
|
|
336
|
-
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, [
|
|
337
334
|
isImporting,
|
|
338
335
|
isProcessingFile,
|
|
339
|
-
validation
|
|
336
|
+
validation,
|
|
337
|
+
error
|
|
340
338
|
]);
|
|
341
339
|
const importData = useCallback(async ()=>{
|
|
342
340
|
if (!canImport || void 0 === validation) return false;
|
|
@@ -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;
|
|
@@ -61,9 +61,5 @@ export interface CsvUploadProps {
|
|
|
61
61
|
importMode?: "direct" | "preview";
|
|
62
62
|
rowDefaults?: RowDefaultDataItemValues;
|
|
63
63
|
validators?: Record<string, CsvColumnValidator>;
|
|
64
|
-
/**
|
|
65
|
-
* Custom renderer for the error panel.
|
|
66
|
-
*/
|
|
67
|
-
renderError?: (error: CsvUploadErrorResult) => ReactNode;
|
|
68
64
|
}
|
|
69
65
|
export declare const CsvUpload: (props: CsvUploadProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -17,7 +17,7 @@ export interface CsvUploadHookProps {
|
|
|
17
17
|
importTable?: VuuTable;
|
|
18
18
|
maxRows?: number;
|
|
19
19
|
onImportSessionEnded?: (result: CsvUploadSessionEndResult) => void;
|
|
20
|
-
|
|
20
|
+
onImportSessionReady?: (dataSource: DataSource) => void;
|
|
21
21
|
onError?: (result: CsvUploadErrorResult | undefined) => void;
|
|
22
22
|
onImported?: (result: CsvUploadImportedResult) => void;
|
|
23
23
|
onPreview?: (result: CsvUploadPreviewResult) => void;
|
|
@@ -40,4 +40,4 @@ export type UseCsvUploadReturn = {
|
|
|
40
40
|
validation: CsvValidationResult | undefined;
|
|
41
41
|
error: CsvUploadErrorResult | undefined;
|
|
42
42
|
};
|
|
43
|
-
export declare const useCsvUpload: ({ dataSource, importMode, importSchema, importTable, onImportSessionEnded,
|
|
43
|
+
export declare const useCsvUpload: ({ dataSource, importMode, importSchema, importTable, onImportSessionEnded, onImportSessionReady, onError, onImported, onPreview, onProcessingStarted, maxRows, parseOptions, rowDefaults, validators, }: CsvUploadHookProps) => UseCsvUploadReturn;
|