@vuu-ui/vuu-table-extras 4.0.0-alpha.2 → 4.0.0-alpha.21
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 +18 -17
- package/src/calculated-column/CalculatedColumnPanel.js +2 -2
- package/src/calculated-column/useEditCalculatedColumn.js +1 -1
- package/src/cell-renderers/pct-progress-cell/PctProgressCell.css.js +2 -2
- package/src/cell-renderers/progress-cell/ProgressCell.css.js +2 -2
- package/src/column-expression-input/ColumnExpressionInput.css.js +5 -5
- package/src/column-expression-input/theme.js +1 -1
- package/src/column-expression-input/useColumnExpressionSuggestionProvider.js +2 -2
- package/src/column-formatting-settings/ColumnFormattingPanel.js +12 -9
- package/src/column-formatting-settings/DateTimeFormattingSettings.js +107 -3
- package/src/column-formatting-settings/LongTypeFormattingSettings.js +8 -3
- 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/column-settings-panel/ColumnNameLabel.css.js +1 -1
- package/src/csv-export/export-utils.js +76 -20
- package/src/csv-export/index.js +1 -1
- package/src/csv-export/useCsvExport.js +9 -1
- package/src/csv-upload/CsvUpload.css.js +36 -2
- package/src/csv-upload/CsvUpload.js +107 -63
- package/src/csv-upload/parse/csv-errors.js +1 -1
- package/src/csv-upload/parse/csv-parse.js +1 -1
- package/src/csv-upload/parse/csv-schema-validation.js +46 -7
- package/src/csv-upload/parse/csv-upload-utils.js +47 -2
- package/src/csv-upload/session/index.js +2 -0
- package/src/csv-upload/session/useImportSchema.js +44 -0
- package/src/csv-upload/session/useImportSession.js +204 -0
- package/src/csv-upload/useCsvUpload.js +54 -199
- package/src/freeze-control/FreezeControl.css.js +4 -4
- package/src/freeze-control/FrozenBanner.css.js +2 -2
- package/src/tabbed-table-config-panel/TabbedTableConfigPanel.js +17 -8
- package/src/tabbed-table-config-panel/useTabbedTableConfigPanel.js +1 -1
- package/src/table-footer/TableFooter.js +3 -3
- package/types/calculated-column/CalculatedColumnPanel.d.ts +3 -3
- package/types/cell-renderers/background-cell/BackgroundCell.d.ts +1 -1
- package/types/cell-renderers/background-cell/BackgroundCellConfigurationEditor.d.ts +1 -1
- package/types/cell-renderers/button-cell/IconButtonCell.d.ts +1 -1
- package/types/cell-renderers/dropdown-cell/DropdownCell.d.ts +1 -1
- package/types/cell-renderers/lookup-cell/LookupCell.d.ts +1 -1
- package/types/cell-renderers/pct-progress-cell/PctProgressCell.d.ts +1 -1
- package/types/column-expression-input/ColumnExpressionInput.d.ts +1 -1
- package/types/column-expression-input/column-language-parser/test.d.mts +1 -1
- package/types/column-formatting-settings/BaseNumericFormattingSettings.d.ts +1 -1
- package/types/column-formatting-settings/ColumnFormattingPanel.d.ts +1 -1
- package/types/column-formatting-settings/DateTimeFormattingSettings.d.ts +1 -1
- package/types/column-formatting-settings/LongTypeFormattingSettings.d.ts +1 -2
- package/types/column-menu/ColumnMenu.d.ts +1 -1
- package/types/column-picker/ColumnModel.d.ts +4 -3
- package/types/column-picker/ColumnPicker.d.ts +4 -6
- package/types/column-picker/ColumnPickerAction.d.ts +1 -1
- package/types/column-picker/useColumnPicker.d.ts +7 -10
- package/types/column-settings-panel/ColumnNameLabel.d.ts +1 -1
- package/types/column-settings-panel/ColumnSettingsPanel.d.ts +1 -1
- package/types/column-settings-panel/useColumnSettings.d.ts +3 -3
- package/types/csv-export/export-utils.d.ts +22 -9
- package/types/csv-export/index.d.ts +1 -1
- package/types/csv-export/useCsvExport.d.ts +5 -2
- package/types/csv-upload/CsvUpload.d.ts +2 -6
- package/types/csv-upload/data-upload-preview/DataUploadPreview.d.ts +1 -1
- package/types/csv-upload/parse/csv-errors.d.ts +1 -1
- package/types/csv-upload/parse/csv-schema-validation.d.ts +8 -2
- package/types/csv-upload/parse/csv-upload-utils.d.ts +9 -0
- package/types/csv-upload/session/index.d.ts +2 -0
- package/types/csv-upload/session/useImportSchema.d.ts +13 -0
- package/types/csv-upload/session/useImportSession.d.ts +26 -0
- package/types/csv-upload/useCsvUpload.d.ts +3 -3
- package/types/datasource-stats/DatasourceStats.d.ts +1 -1
- package/types/freeze-control/FreezeControl.d.ts +1 -1
- package/types/freeze-control/FrozenBanner.d.ts +1 -1
- package/types/index.d.ts +0 -1
- package/types/inline-add-row/InlineAddRow.d.ts +1 -1
- package/types/inline-add-row/useInlineAddRow.d.ts +2 -1
- package/types/tabbed-table-config-panel/TabbedTableConfigPanel.d.ts +1 -1
- package/types/tabbed-table-config-panel/TabbedTableSettingsAction.d.ts +1 -1
- package/types/tabbed-table-config-panel/useTabbedTableConfigPanel.d.ts +4 -3
- package/types/table-footer/TableFooter.d.ts +4 -4
- package/types/table-provider/TableProvider.d.ts +1 -1
- package/types/table-settings-panel/TableSettingsPanel.d.ts +1 -1
|
@@ -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: [
|
|
@@ -13,7 +13,7 @@ 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
18
|
CsvValidationErrorEnum["CUSTOM_VALIDATION"] = "CUSTOM_VALIDATION";
|
|
19
19
|
return CsvValidationErrorEnum;
|
|
@@ -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();
|
|
@@ -21,22 +55,27 @@ 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];
|
|
29
|
-
const
|
|
30
|
-
|
|
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
|
+
}
|
|
31
70
|
let value;
|
|
32
71
|
const toType = schemaType ?? "string";
|
|
33
72
|
try {
|
|
34
73
|
value = getTypedValue(rawValue, toType);
|
|
35
74
|
} catch (err) {
|
|
36
|
-
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);
|
|
37
76
|
return;
|
|
38
77
|
}
|
|
39
|
-
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);
|
|
40
79
|
const customValidator = options?.validators?.[columnName];
|
|
41
80
|
if (customValidator) {
|
|
42
81
|
const validationResult = customValidator(rawValue, columnName, rawRow);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRpcError } from "@vuu-ui/vuu-utils";
|
|
1
|
+
import { isRpcError, metadataKeys } from "@vuu-ui/vuu-utils";
|
|
2
2
|
import { CSV_FIRST_DATA_ROW_NUMBER } from "./csv-constants.js";
|
|
3
3
|
const isRecord = (value)=>"object" == typeof value && null !== value;
|
|
4
4
|
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
@@ -56,4 +56,49 @@ const executeBatchRpcCalls = async (items, executor, errorFormatter)=>{
|
|
|
56
56
|
results
|
|
57
57
|
};
|
|
58
58
|
};
|
|
59
|
-
|
|
59
|
+
const getInlineRowErrorMessage = (result)=>{
|
|
60
|
+
if (isRecord(result) && "data" in result && isRecord(result.data)) {
|
|
61
|
+
const data = result.data;
|
|
62
|
+
const msg = data.vuuMsg ?? data.msg;
|
|
63
|
+
if ("string" == typeof msg && "" !== msg.trim()) return msg.trim();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const getRowVuuMsgError = (row, vuuMsgDataIndex, vuuRowNumDataIndex)=>{
|
|
67
|
+
if (-1 === vuuMsgDataIndex) return;
|
|
68
|
+
const msg = Array.isArray(row) ? row[vuuMsgDataIndex] : row?.vuuMsg;
|
|
69
|
+
if ("string" == typeof msg && "" !== msg.trim()) {
|
|
70
|
+
const rowNum = -1 !== vuuRowNumDataIndex && void 0 !== row[vuuRowNumDataIndex] ? Number(row[vuuRowNumDataIndex]) : Number(row[metadataKeys.IDX]) + CSV_FIRST_DATA_ROW_NUMBER;
|
|
71
|
+
return {
|
|
72
|
+
rowNum,
|
|
73
|
+
message: msg.trim()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const waitForSessionErrors = (isRemote, sessionErrors, receivedRowKeys, expectedRowCount, listeners, timeoutMs = 500)=>{
|
|
78
|
+
if (sessionErrors.size > 0 || !isRemote) return Promise.resolve([
|
|
79
|
+
...sessionErrors.values()
|
|
80
|
+
]);
|
|
81
|
+
return new Promise((resolve)=>{
|
|
82
|
+
let timeoutId;
|
|
83
|
+
const onUpdate = ()=>{
|
|
84
|
+
if (sessionErrors.size > 0 || receivedRowKeys.size >= expectedRowCount) {
|
|
85
|
+
cleanup();
|
|
86
|
+
resolve([
|
|
87
|
+
...sessionErrors.values()
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const cleanup = ()=>{
|
|
92
|
+
clearTimeout(timeoutId);
|
|
93
|
+
listeners.delete(onUpdate);
|
|
94
|
+
};
|
|
95
|
+
listeners.add(onUpdate);
|
|
96
|
+
timeoutId = setTimeout(()=>{
|
|
97
|
+
cleanup();
|
|
98
|
+
resolve([
|
|
99
|
+
...sessionErrors.values()
|
|
100
|
+
]);
|
|
101
|
+
}, timeoutMs);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
export { buildRowErrorMessage, createUploadError, executeBatchRpcCalls, getInlineRowErrorMessage, getRowVuuMsgError, hasFileParseErrors, isCsvParseError, mergeValidationWithParseErrors, normalizeTableData, toErrorMessage, toValidationErrorsFromParseRowErrors, waitForSessionErrors };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useData } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
const useImportSchema = ({ dataSource, importSchema, importTable })=>{
|
|
4
|
+
const { getServerAPI } = useData();
|
|
5
|
+
const [fetchedImportSchema, setFetchedImportSchema] = useState();
|
|
6
|
+
useEffect(()=>{
|
|
7
|
+
if (importSchema || void 0 === importTable) return void setFetchedImportSchema(void 0);
|
|
8
|
+
let cancelled = false;
|
|
9
|
+
(async ()=>{
|
|
10
|
+
try {
|
|
11
|
+
const server = await getServerAPI();
|
|
12
|
+
const tableSchema = await server.getTableSchema(importTable);
|
|
13
|
+
if (!cancelled) setFetchedImportSchema(tableSchema);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("[useImportSchema] failed to fetch import table schema", error);
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
return ()=>{
|
|
19
|
+
cancelled = true;
|
|
20
|
+
};
|
|
21
|
+
}, [
|
|
22
|
+
getServerAPI,
|
|
23
|
+
importSchema,
|
|
24
|
+
importTable
|
|
25
|
+
]);
|
|
26
|
+
const resolvedImportSchema = importSchema ?? fetchedImportSchema;
|
|
27
|
+
const sessionOverrides = useMemo(()=>{
|
|
28
|
+
const columns = resolvedImportSchema?.columns.map(({ name })=>name);
|
|
29
|
+
return columns || importTable ? {
|
|
30
|
+
columns,
|
|
31
|
+
table: importTable
|
|
32
|
+
} : void 0;
|
|
33
|
+
}, [
|
|
34
|
+
resolvedImportSchema,
|
|
35
|
+
importTable
|
|
36
|
+
]);
|
|
37
|
+
const schema = importTable ? resolvedImportSchema : resolvedImportSchema ?? dataSource.tableSchema;
|
|
38
|
+
return {
|
|
39
|
+
resolvedImportSchema,
|
|
40
|
+
schema,
|
|
41
|
+
sessionOverrides
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export { useImportSchema };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { EditSession } from "@vuu-ui/vuu-data-editing";
|
|
2
|
+
import { Range, buildColumnMap, isRpcError, isSessionTable, metadataKeys } from "@vuu-ui/vuu-utils";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { buildRowErrorMessage, getInlineRowErrorMessage, getRowVuuMsgError, toErrorMessage, waitForSessionErrors } from "../parse/csv-upload-utils.js";
|
|
5
|
+
import { CSV_FIRST_DATA_ROW_NUMBER } from "../parse/csv-constants.js";
|
|
6
|
+
const useImportSession = ({ dataSource, sessionOverrides, rowDefaults, processingPromiseRef, onImportSessionEnded })=>{
|
|
7
|
+
const sessionErrorsRef = useRef(new Map());
|
|
8
|
+
const receivedRowKeysRef = useRef(new Set());
|
|
9
|
+
const sessionRowUpdateListenersRef = useRef(new Set());
|
|
10
|
+
const importDataSource = useMemo(()=>({
|
|
11
|
+
tableSchema: dataSource.tableSchema,
|
|
12
|
+
createSessionDataSource: async (copyOption, sessionType)=>{
|
|
13
|
+
if (!dataSource.createSessionDataSource) throw Error("[useCsvUpload] dataSource does not support createSessionDataSource");
|
|
14
|
+
const sessionDs = await dataSource.createSessionDataSource(copyOption, sessionType, sessionOverrides);
|
|
15
|
+
if (!sessionDs) throw Error("[useCsvUpload] Failed to create session datasource");
|
|
16
|
+
if (!sessionDs.columns.includes("vuuMsg")) sessionDs.columns = sessionDs.columns.concat("vuuMsg");
|
|
17
|
+
const columnMap = buildColumnMap(sessionDs.columns);
|
|
18
|
+
const vuuMsgDataIndex = columnMap.vuuMsg ?? -1;
|
|
19
|
+
const vuuRowNumDataIndex = columnMap.vuuRowNum ?? -1;
|
|
20
|
+
const handleSessionMessage = (message)=>{
|
|
21
|
+
if ("viewport-update" === message.type && message.rows) {
|
|
22
|
+
for (const row of message.rows){
|
|
23
|
+
const rowKey = row[metadataKeys.KEY] ?? row[metadataKeys.IDX];
|
|
24
|
+
receivedRowKeysRef.current.add(rowKey);
|
|
25
|
+
const error = getRowVuuMsgError(row, vuuMsgDataIndex, vuuRowNumDataIndex);
|
|
26
|
+
if (error) sessionErrorsRef.current.set(rowKey, error);
|
|
27
|
+
}
|
|
28
|
+
for (const listener of sessionRowUpdateListenersRef.current)listener();
|
|
29
|
+
} else if ("viewport-clear" === message.type) {
|
|
30
|
+
sessionErrorsRef.current.clear();
|
|
31
|
+
receivedRowKeysRef.current.clear();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
if (sessionDs.isRemote) return new Promise((resolve, reject)=>{
|
|
35
|
+
const handleSubscribed = ()=>{
|
|
36
|
+
sessionDs.removeListener("subscribed", handleSubscribed);
|
|
37
|
+
resolve(sessionDs);
|
|
38
|
+
};
|
|
39
|
+
sessionDs.on("subscribed", handleSubscribed);
|
|
40
|
+
try {
|
|
41
|
+
sessionDs.subscribe({
|
|
42
|
+
range: Range(0, 0)
|
|
43
|
+
}, handleSessionMessage);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
sessionDs.removeListener("subscribed", handleSubscribed);
|
|
46
|
+
reject(err);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if ("function" == typeof sessionDs.subscribe && "initialising" === sessionDs.status) try {
|
|
50
|
+
sessionDs.subscribe({
|
|
51
|
+
range: Range(0, 0)
|
|
52
|
+
}, handleSessionMessage);
|
|
53
|
+
} catch {}
|
|
54
|
+
return sessionDs;
|
|
55
|
+
}
|
|
56
|
+
}), [
|
|
57
|
+
dataSource,
|
|
58
|
+
sessionOverrides
|
|
59
|
+
]);
|
|
60
|
+
const editSession = useMemo(()=>new EditSession({
|
|
61
|
+
dataSource: importDataSource,
|
|
62
|
+
editSessionApi: "createSessionDataSource",
|
|
63
|
+
rowDefaults
|
|
64
|
+
}), [
|
|
65
|
+
importDataSource,
|
|
66
|
+
rowDefaults
|
|
67
|
+
]);
|
|
68
|
+
const ownsEditSessionRef = useRef(true);
|
|
69
|
+
const sessionDataSourceRef = useRef(void 0);
|
|
70
|
+
const [sessionTable, setSessionTable] = useState();
|
|
71
|
+
useEffect(()=>()=>{
|
|
72
|
+
const processingPromise = processingPromiseRef?.current;
|
|
73
|
+
(async ()=>{
|
|
74
|
+
try {
|
|
75
|
+
await processingPromise;
|
|
76
|
+
} finally{
|
|
77
|
+
if (ownsEditSessionRef.current) try {
|
|
78
|
+
const sessionDs = sessionDataSourceRef.current ?? editSession.sessionDataSource;
|
|
79
|
+
if (sessionDs?.status !== "unsubscribed") await editSession.end(false);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("[useCsvUpload] failed to discard edit session during cleanup", error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
}, [
|
|
86
|
+
editSession,
|
|
87
|
+
processingPromiseRef
|
|
88
|
+
]);
|
|
89
|
+
const setActiveSessionDataSource = useCallback((sessionDataSource)=>{
|
|
90
|
+
sessionDataSourceRef.current = sessionDataSource;
|
|
91
|
+
const table = sessionDataSource?.table;
|
|
92
|
+
setSessionTable(table && isSessionTable(table) ? table : void 0);
|
|
93
|
+
}, []);
|
|
94
|
+
const endEditSessionAndNotify = useCallback(async (save, reason)=>{
|
|
95
|
+
ownsEditSessionRef.current = false;
|
|
96
|
+
const sessionDataSource = sessionDataSourceRef.current;
|
|
97
|
+
if (!sessionDataSource) throw Error("CsvUpload has no active edit session.");
|
|
98
|
+
if (!sessionDataSource.endEditSession) throw Error("CsvUpload requires the session datasource to support endEditSession.");
|
|
99
|
+
const currentSessionTable = sessionDataSource.table;
|
|
100
|
+
try {
|
|
101
|
+
await editSession.end(save);
|
|
102
|
+
} finally{
|
|
103
|
+
if ("unsubscribed" !== sessionDataSource.status && "function" == typeof sessionDataSource.unsubscribe) sessionDataSource.unsubscribe();
|
|
104
|
+
setActiveSessionDataSource(void 0);
|
|
105
|
+
}
|
|
106
|
+
onImportSessionEnded?.({
|
|
107
|
+
reason,
|
|
108
|
+
sessionTable: currentSessionTable && isSessionTable(currentSessionTable) ? currentSessionTable : void 0
|
|
109
|
+
});
|
|
110
|
+
}, [
|
|
111
|
+
editSession,
|
|
112
|
+
onImportSessionEnded,
|
|
113
|
+
setActiveSessionDataSource
|
|
114
|
+
]);
|
|
115
|
+
const beginEditSession = useCallback(async ()=>{
|
|
116
|
+
sessionErrorsRef.current.clear();
|
|
117
|
+
receivedRowKeysRef.current.clear();
|
|
118
|
+
ownsEditSessionRef.current = true;
|
|
119
|
+
const sessionDataSource = await editSession.begin("Empty", "import");
|
|
120
|
+
const sessionVuuTable = sessionDataSource?.table;
|
|
121
|
+
if (void 0 === sessionDataSource || void 0 === sessionVuuTable || !isSessionTable(sessionVuuTable)) throw Error("CsvUpload createSessionDataSource returned no session datasource.");
|
|
122
|
+
setActiveSessionDataSource(sessionDataSource);
|
|
123
|
+
return sessionDataSource;
|
|
124
|
+
}, [
|
|
125
|
+
editSession,
|
|
126
|
+
setActiveSessionDataSource
|
|
127
|
+
]);
|
|
128
|
+
const closePendingEditSession = useCallback(async (save)=>{
|
|
129
|
+
if (void 0 === sessionDataSourceRef.current) return;
|
|
130
|
+
await endEditSessionAndNotify(save, save ? "saved" : "discarded");
|
|
131
|
+
}, [
|
|
132
|
+
endEditSessionAndNotify
|
|
133
|
+
]);
|
|
134
|
+
const releaseEditSessionOwnership = useCallback(()=>{
|
|
135
|
+
ownsEditSessionRef.current = false;
|
|
136
|
+
}, []);
|
|
137
|
+
const addAllRows = useCallback(async (mergedValidation, isCancelled)=>{
|
|
138
|
+
const vuuMsgByRow = new Map();
|
|
139
|
+
for (const { rowNum, column, message } of mergedValidation.errors){
|
|
140
|
+
if (rowNum < CSV_FIRST_DATA_ROW_NUMBER) continue;
|
|
141
|
+
const existing = vuuMsgByRow.get(rowNum);
|
|
142
|
+
const columnError = `${column}: ${message}`;
|
|
143
|
+
vuuMsgByRow.set(rowNum, existing ? `${existing}; ${columnError}` : columnError);
|
|
144
|
+
}
|
|
145
|
+
const parsedRowCount = mergedValidation.rows.length;
|
|
146
|
+
const unparsedErrorRows = [
|
|
147
|
+
...vuuMsgByRow.keys()
|
|
148
|
+
].filter((rowNum)=>rowNum - CSV_FIRST_DATA_ROW_NUMBER >= parsedRowCount).map((rowNum)=>({
|
|
149
|
+
rowNum,
|
|
150
|
+
rowData: {},
|
|
151
|
+
vuuMsg: vuuMsgByRow.get(rowNum) ?? ""
|
|
152
|
+
}));
|
|
153
|
+
const allRows = [
|
|
154
|
+
...mergedValidation.rows.map((rowData, idx)=>({
|
|
155
|
+
rowNum: idx + CSV_FIRST_DATA_ROW_NUMBER,
|
|
156
|
+
rowData: rowData ?? {},
|
|
157
|
+
vuuMsg: vuuMsgByRow.get(idx + CSV_FIRST_DATA_ROW_NUMBER) ?? ""
|
|
158
|
+
})),
|
|
159
|
+
...unparsedErrorRows
|
|
160
|
+
];
|
|
161
|
+
const rpcErrors = [];
|
|
162
|
+
for (const { rowNum, rowData, vuuMsg } of allRows){
|
|
163
|
+
if (isCancelled()) return false;
|
|
164
|
+
try {
|
|
165
|
+
const payload = vuuMsg ? {
|
|
166
|
+
vuuRowNum: rowNum,
|
|
167
|
+
vuuMsg
|
|
168
|
+
} : {
|
|
169
|
+
...rowData,
|
|
170
|
+
vuuRowNum: rowNum,
|
|
171
|
+
vuuMsg
|
|
172
|
+
};
|
|
173
|
+
const result = await editSession.addRow(payload);
|
|
174
|
+
if (isRpcError(result)) throw Error(result.errorMessage);
|
|
175
|
+
if ("string" == typeof result) throw Error(result);
|
|
176
|
+
const inlineMsg = getInlineRowErrorMessage(result);
|
|
177
|
+
if (inlineMsg) sessionErrorsRef.current.set(rowNum, {
|
|
178
|
+
rowNum,
|
|
179
|
+
message: inlineMsg
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
rpcErrors.push(`${0 === rowNum ? "Header" : `Row ${rowNum}`}: ${toErrorMessage(error)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (rpcErrors.length > 0) throw Error(buildRowErrorMessage("Import failed", rpcErrors));
|
|
186
|
+
return allRows.length;
|
|
187
|
+
}, [
|
|
188
|
+
editSession
|
|
189
|
+
]);
|
|
190
|
+
const checkSessionTableErrors = useCallback(async (sessionDs, expectedRowCount)=>waitForSessionErrors(sessionDs.isRemote ?? false, sessionErrorsRef.current, receivedRowKeysRef.current, expectedRowCount, sessionRowUpdateListenersRef.current), []);
|
|
191
|
+
return {
|
|
192
|
+
addAllRows,
|
|
193
|
+
beginEditSession,
|
|
194
|
+
checkSessionTableErrors,
|
|
195
|
+
closePendingEditSession,
|
|
196
|
+
editSession,
|
|
197
|
+
endEditSessionAndNotify,
|
|
198
|
+
releaseEditSessionOwnership,
|
|
199
|
+
sessionDataSourceRef,
|
|
200
|
+
sessionTable,
|
|
201
|
+
setActiveSessionDataSource
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
export { useImportSession };
|