@vuu-ui/vuu-table-extras 2.1.19-beta.1 → 2.1.19-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -14
- package/src/calculated-column/CalculatedColumnPanel.js +95 -0
- package/src/calculated-column/useCalculatedColumnPanel.js +60 -0
- package/src/calculated-column/useEditCalculatedColumn.js +104 -0
- package/src/cell-edit-validators/CaseValidator.js +21 -0
- package/src/cell-edit-validators/PatternValidator.js +23 -0
- package/src/cell-edit-validators/index.js +2 -0
- package/src/cell-renderers/background-cell/BackgroundCell.css.js +103 -0
- package/src/cell-renderers/background-cell/BackgroundCell.js +76 -0
- package/src/cell-renderers/background-cell/BackgroundCellConfigurationEditor.css.js +9 -0
- package/src/cell-renderers/background-cell/BackgroundCellConfigurationEditor.js +71 -0
- package/src/cell-renderers/background-cell/BackgroundKeyframes.css.js +187 -0
- package/src/cell-renderers/background-cell/index.js +2 -0
- package/src/cell-renderers/background-cell/useDirection.js +25 -0
- package/src/cell-renderers/button-cell/IconButtonCell.css.js +23 -0
- package/src/cell-renderers/button-cell/IconButtonCell.js +33 -0
- package/src/cell-renderers/dropdown-cell/DropdownCell.css.js +36 -0
- package/src/cell-renderers/dropdown-cell/DropdownCell.js +84 -0
- package/src/cell-renderers/dropdown-cell/index.js +1 -0
- package/src/cell-renderers/index.js +6 -0
- package/src/cell-renderers/lookup-cell/LookupCell.js +15 -0
- package/src/cell-renderers/lookup-cell/index.js +1 -0
- package/src/cell-renderers/pct-progress-cell/PctProgressCell.css.js +31 -0
- package/src/cell-renderers/pct-progress-cell/PctProgressCell.js +49 -0
- package/src/cell-renderers/pct-progress-cell/index.js +1 -0
- package/src/cell-renderers/progress-cell/ProgressCell.css.js +43 -0
- package/src/cell-renderers/progress-cell/ProgressCell.js +74 -0
- package/src/cell-renderers/progress-cell/index.js +1 -0
- package/src/column-expression-input/ColumnExpressionInput.css.js +94 -0
- package/src/column-expression-input/ColumnExpressionInput.js +28 -0
- package/src/column-expression-input/column-function-descriptors.js +281 -0
- package/src/column-expression-input/column-language-parser/ColumnExpressionLanguage.js +18 -0
- package/src/column-expression-input/column-language-parser/ColumnExpressionTreeWalker.js +381 -0
- package/src/column-expression-input/column-language-parser/column-expression-parse-utils.js +56 -0
- package/src/column-expression-input/column-language-parser/index.js +3 -0
- package/src/column-expression-input/functionDocInfo.js +24 -0
- package/src/column-expression-input/highlighting.js +18 -0
- package/src/column-expression-input/index.js +4 -0
- package/src/column-expression-input/theme.js +57 -0
- package/src/column-expression-input/useColumnAutoComplete.js +307 -0
- package/src/column-expression-input/useColumnExpressionEditor.js +133 -0
- package/src/column-expression-input/useColumnExpressionSuggestionProvider.js +211 -0
- package/src/column-formatting-settings/BaseNumericFormattingSettings.js +96 -0
- package/src/column-formatting-settings/ColumnFormattingPanel.js +112 -0
- package/src/column-formatting-settings/DateTimeFormattingSettings.js +116 -0
- package/src/column-formatting-settings/LongTypeFormattingSettings.css.js +11 -0
- package/src/column-formatting-settings/LongTypeFormattingSettings.js +54 -0
- package/src/column-formatting-settings/index.js +3 -0
- package/src/column-menu/ColumnMenu.css.js +25 -0
- package/src/column-menu/ColumnMenu.js +69 -0
- package/src/column-menu/column-action-types.js +0 -0
- package/src/column-menu/column-menu-utils.js +318 -0
- package/src/column-menu/useColumnActions.js +77 -0
- package/src/column-picker/ColumnModel.js +113 -0
- package/src/column-picker/ColumnPicker.css.js +96 -0
- package/src/column-picker/ColumnPicker.js +204 -0
- package/src/column-picker/ColumnPickerAction.js +24 -0
- package/src/column-picker/useColumnPicker.js +42 -0
- package/src/column-picker/useTableColumnPicker.js +27 -0
- package/src/column-picker/useTableColumnPickerDeprecated.js +87 -0
- package/src/column-settings-panel/ColumnNameLabel.css.js +18 -0
- package/src/column-settings-panel/ColumnNameLabel.js +54 -0
- package/src/column-settings-panel/ColumnSettingsPanel.css.js +70 -0
- package/src/column-settings-panel/ColumnSettingsPanel.js +173 -0
- package/src/column-settings-panel/useColumnSettings.js +205 -0
- package/src/csv-upload/CsvUpload.css.js +28 -0
- package/src/csv-upload/CsvUpload.js +95 -0
- package/src/csv-upload/index.js +7 -0
- package/src/csv-upload/parse/csv-constants.js +3 -0
- package/src/csv-upload/parse/csv-errors.js +54 -0
- package/src/csv-upload/parse/csv-parse.js +79 -0
- package/src/csv-upload/parse/csv-schema-validation.js +45 -0
- package/src/csv-upload/parse/csv-upload-utils.js +59 -0
- package/src/csv-upload/parse/index.js +5 -0
- package/src/csv-upload/useCsvUpload.js +260 -0
- package/src/csv-upload/useCsvUploadSessionPreview.js +70 -0
- package/src/datasource-stats/DatasourceStats.css.js +56 -0
- package/src/datasource-stats/DatasourceStats.js +89 -0
- package/src/datasource-stats/useDatasourceStats.js +48 -0
- package/src/freeze-control/FreezeControl.css.js +108 -0
- package/src/freeze-control/FreezeControl.js +96 -0
- package/src/freeze-control/FrozenBanner.css.js +16 -0
- package/src/freeze-control/FrozenBanner.js +48 -0
- package/src/freeze-control/freezeControlBadge.js +9 -0
- package/src/freeze-control/useFreezeControl.js +68 -0
- package/src/index.js +22 -0
- package/src/tabbed-table-config-panel/TabbedTableConfigPanel.css.js +23 -0
- package/src/tabbed-table-config-panel/TabbedTableConfigPanel.js +99 -0
- package/src/tabbed-table-config-panel/TabbedTableSettingsAction.js +28 -0
- package/src/tabbed-table-config-panel/useTabbedTableConfigPanel.js +29 -0
- package/src/table-footer/TableFooter.css.js +22 -0
- package/src/table-footer/TableFooter.js +30 -0
- package/src/table-provider/TableProvider.js +33 -0
- package/src/table-settings-panel/TableSettingsPanel.css.js +70 -0
- package/src/table-settings-panel/TableSettingsPanel.js +128 -0
- package/src/table-settings-panel/useTableSettings.js +50 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getTypedValue } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { CsvValidationErrorEnum, addCsvFileError, addCsvRowError, createCsvErrorState } from "./csv-errors.js";
|
|
3
|
+
import { CSV_FIRST_DATA_ROW_NUMBER } from "./csv-constants.js";
|
|
4
|
+
const MAX_ROWS_IN_CSV = 25000;
|
|
5
|
+
const validateCsvAgainstSchema = (parsed, tableSchema, options)=>{
|
|
6
|
+
const schemaColumns = new Map(tableSchema.columns.map((col)=>[
|
|
7
|
+
col.name,
|
|
8
|
+
col.serverDataType
|
|
9
|
+
]));
|
|
10
|
+
const maxRows = options?.maxRows ?? MAX_ROWS_IN_CSV;
|
|
11
|
+
const errorState = createCsvErrorState();
|
|
12
|
+
if (!parsed.header.includes(tableSchema.key)) addCsvFileError(errorState, tableSchema.key, CsvValidationErrorEnum.MISSING_KEY_COLUMN, `CSV must include key column '${tableSchema.key}'.`);
|
|
13
|
+
parsed.header.forEach((column)=>{
|
|
14
|
+
if (!schemaColumns.has(column)) addCsvFileError(errorState, column, CsvValidationErrorEnum.UNKNOWN_COLUMN, "Column is not present in table schema.", column);
|
|
15
|
+
});
|
|
16
|
+
if (parsed.rows.length > maxRows) addCsvFileError(errorState, "*", CsvValidationErrorEnum.MAX_ROWS_EXCEEDED, `CSV row count ${parsed.rows.length} exceeds maxRows ${maxRows}.`, String(parsed.rows.length));
|
|
17
|
+
const typedRows = [];
|
|
18
|
+
parsed.rows.forEach((rowValues, rowIndex)=>{
|
|
19
|
+
const rowNum = rowIndex + CSV_FIRST_DATA_ROW_NUMBER;
|
|
20
|
+
const typedRow = {};
|
|
21
|
+
parsed.header.forEach((columnName, columnIndex)=>{
|
|
22
|
+
const rawValue = rowValues[columnIndex];
|
|
23
|
+
const schemaType = schemaColumns.get(columnName);
|
|
24
|
+
if (0 === rawValue.length && "string" !== schemaType) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.EMPTY_NON_STRING_VALUE, "Empty value is not allowed for non-string columns.", rawValue);
|
|
25
|
+
let value;
|
|
26
|
+
const toType = schemaType ?? "string";
|
|
27
|
+
try {
|
|
28
|
+
value = getTypedValue(rawValue, toType);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, err instanceof Error ? err.message : String(err), rawValue);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (void 0 === value) return void addCsvRowError(errorState, rowNum, columnName, CsvValidationErrorEnum.TYPE_MISMATCH, `Value '${rawValue}' is not a valid ${toType}`, rawValue);
|
|
34
|
+
typedRow[columnName] = value;
|
|
35
|
+
});
|
|
36
|
+
typedRows.push(typedRow);
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
columns: parsed.header,
|
|
40
|
+
errorMap: errorState.errorMap,
|
|
41
|
+
errors: errorState.errors,
|
|
42
|
+
rows: typedRows
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export { CsvValidationErrorEnum, validateCsvAgainstSchema };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isRpcError } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { CSV_FIRST_DATA_ROW_NUMBER } from "./csv-constants.js";
|
|
3
|
+
const isRecord = (value)=>"object" == typeof value && null !== value;
|
|
4
|
+
const toErrorMessage = (error)=>error instanceof Error ? error.message : String(error);
|
|
5
|
+
const isCsvParseError = (value)=>isRecord(value) && "message" in value && "string" == typeof value.message && "errors" in value && Array.isArray(value.errors) && "errorMap" in value && isRecord(value.errorMap);
|
|
6
|
+
const hasFileParseErrors = (parseError)=>Object.keys(parseError.errorMap.fileErrors).length > 0;
|
|
7
|
+
const createUploadError = (source, message, parseError, validationError)=>({
|
|
8
|
+
message,
|
|
9
|
+
parseError,
|
|
10
|
+
validationError,
|
|
11
|
+
source
|
|
12
|
+
});
|
|
13
|
+
const mergeValidationWithParseErrors = (schemaValidation, parseError)=>parseError ? {
|
|
14
|
+
...schemaValidation,
|
|
15
|
+
errors: [
|
|
16
|
+
...schemaValidation.errors,
|
|
17
|
+
...toValidationErrorsFromParseRowErrors(parseError)
|
|
18
|
+
],
|
|
19
|
+
parseRowError: parseError
|
|
20
|
+
} : schemaValidation;
|
|
21
|
+
const buildRowErrorMessage = (prefix, rowErrors)=>{
|
|
22
|
+
const details = rowErrors.slice(0, 8).join("; ");
|
|
23
|
+
const remaining = rowErrors.length - 8;
|
|
24
|
+
const suffix = remaining > 0 ? `; and ${remaining} more` : "";
|
|
25
|
+
return `${prefix} for ${rowErrors.length} row(s): ${details}${suffix}`;
|
|
26
|
+
};
|
|
27
|
+
const toValidationErrorsFromParseRowErrors = (parseError)=>parseError.errors.filter((error)=>error.rowNum >= CSV_FIRST_DATA_ROW_NUMBER);
|
|
28
|
+
const normalizeTableData = (value, fallback)=>{
|
|
29
|
+
const tableData = isRecord(value) && "data" in value ? value.data : value;
|
|
30
|
+
if (!isRecord(tableData)) return fallback;
|
|
31
|
+
const columns = tableData.columns;
|
|
32
|
+
const rows = tableData.rows;
|
|
33
|
+
if (!Array.isArray(columns) || columns.some((column)=>"string" != typeof column) || !Array.isArray(rows)) return fallback;
|
|
34
|
+
return {
|
|
35
|
+
columns,
|
|
36
|
+
rows: rows.map((row)=>{
|
|
37
|
+
if (Array.isArray(row)) return row;
|
|
38
|
+
if (!isRecord(row)) return columns.map(()=>"");
|
|
39
|
+
return columns.map((column)=>row[column] ?? "");
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const executeBatchRpcCalls = async (items, executor, errorFormatter)=>{
|
|
44
|
+
const errors = [];
|
|
45
|
+
const results = [];
|
|
46
|
+
for (const item of items)try {
|
|
47
|
+
const result = await executor(item);
|
|
48
|
+
const rpcResult = result;
|
|
49
|
+
if (isRpcError(rpcResult)) errors.push(errorFormatter(item, rpcResult.errorMessage));
|
|
50
|
+
else results.push(result);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
errors.push(errorFormatter(item, toErrorMessage(err)));
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
errors,
|
|
56
|
+
results
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
export { buildRowErrorMessage, createUploadError, executeBatchRpcCalls, hasFileParseErrors, isCsvParseError, mergeValidationWithParseErrors, normalizeTableData, toErrorMessage, toValidationErrorsFromParseRowErrors };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CSV_FIRST_DATA_ROW_NUMBER, CSV_HEADER_ROW_NUMBER } from "./csv-constants.js";
|
|
2
|
+
export { CsvParseErrorEnum, CsvValidationErrorEnum, addCsvFileError, addCsvRowError, createCsvErrorState, hasCsvErrors } from "./csv-errors.js";
|
|
3
|
+
export { parseCsv } from "./csv-parse.js";
|
|
4
|
+
export { validateCsvAgainstSchema } from "./csv-schema-validation.js";
|
|
5
|
+
export { buildRowErrorMessage, executeBatchRpcCalls, hasFileParseErrors, isCsvParseError, mergeValidationWithParseErrors, normalizeTableData, toErrorMessage, toValidationErrorsFromParseRowErrors } from "./csv-upload-utils.js";
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { isRpcSuccess, isSessionTable } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { parseCsv } from "./parse/csv-parse.js";
|
|
4
|
+
import { validateCsvAgainstSchema } from "./parse/csv-schema-validation.js";
|
|
5
|
+
import { buildRowErrorMessage, createUploadError, executeBatchRpcCalls, hasFileParseErrors, isCsvParseError, mergeValidationWithParseErrors, normalizeTableData, toErrorMessage } from "./parse/csv-upload-utils.js";
|
|
6
|
+
import { CSV_FIRST_DATA_ROW_NUMBER } from "./parse/csv-constants.js";
|
|
7
|
+
const useCsvUpload = ({ dataSource, onEditSessionEnded, onEditSessionStarted, onError, onImported, onProcessingStarted, maxRows, parseOptions })=>{
|
|
8
|
+
const [validation, setValidation] = useState();
|
|
9
|
+
const [sessionTable, setSessionTable] = useState();
|
|
10
|
+
const [isProcessingFile, setIsProcessingFile] = useState(false);
|
|
11
|
+
const [isImporting, setIsImporting] = useState(false);
|
|
12
|
+
const sessionTableRef = useRef(void 0);
|
|
13
|
+
const setActiveSessionTable = useCallback((table)=>{
|
|
14
|
+
sessionTableRef.current = table;
|
|
15
|
+
if (table) {
|
|
16
|
+
const sessionDs = dataSource.createSessionDataSource?.(table);
|
|
17
|
+
if (sessionDs) onEditSessionStarted?.(sessionDs);
|
|
18
|
+
}
|
|
19
|
+
setSessionTable(table);
|
|
20
|
+
}, [
|
|
21
|
+
dataSource,
|
|
22
|
+
onEditSessionStarted
|
|
23
|
+
]);
|
|
24
|
+
const endEditSessionAndNotify = useCallback(async (save, reason)=>{
|
|
25
|
+
if (!dataSource.endEditSession) throw Error("CsvUpload requires datasource endEditSession support.");
|
|
26
|
+
const currentSessionTable = sessionTableRef.current;
|
|
27
|
+
const result = await dataSource.endEditSession(save);
|
|
28
|
+
setActiveSessionTable(void 0);
|
|
29
|
+
onEditSessionEnded?.({
|
|
30
|
+
reason,
|
|
31
|
+
sessionTable: currentSessionTable
|
|
32
|
+
});
|
|
33
|
+
return result;
|
|
34
|
+
}, [
|
|
35
|
+
dataSource,
|
|
36
|
+
onEditSessionEnded,
|
|
37
|
+
setActiveSessionTable
|
|
38
|
+
]);
|
|
39
|
+
const table = dataSource.table;
|
|
40
|
+
const schema = dataSource.tableSchema;
|
|
41
|
+
const addAllRows = useCallback(async (mergedValidation)=>{
|
|
42
|
+
if (void 0 === table) throw Error("CsvUpload requires dataSource.table to be defined.");
|
|
43
|
+
const vuuMsgByRow = new Map();
|
|
44
|
+
for (const { rowNum, column, message } of mergedValidation.errors){
|
|
45
|
+
if (rowNum < CSV_FIRST_DATA_ROW_NUMBER) continue;
|
|
46
|
+
const existing = vuuMsgByRow.get(rowNum);
|
|
47
|
+
const columnError = `${column}: ${message}`;
|
|
48
|
+
vuuMsgByRow.set(rowNum, existing ? `${existing}; ${columnError}` : `Row ${rowNum}: ${columnError}`);
|
|
49
|
+
}
|
|
50
|
+
const parsedRowCount = mergedValidation.rows.length;
|
|
51
|
+
const unparsedErrorRows = [
|
|
52
|
+
...vuuMsgByRow.keys()
|
|
53
|
+
].filter((rowNum)=>rowNum - CSV_FIRST_DATA_ROW_NUMBER >= parsedRowCount).map((rowNum)=>({
|
|
54
|
+
rowNum,
|
|
55
|
+
rowData: {},
|
|
56
|
+
vuuMsg: vuuMsgByRow.get(rowNum) ?? ""
|
|
57
|
+
}));
|
|
58
|
+
const allRows = [
|
|
59
|
+
...mergedValidation.rows.map((rowData, idx)=>({
|
|
60
|
+
rowNum: idx + CSV_FIRST_DATA_ROW_NUMBER,
|
|
61
|
+
rowData: rowData ?? {},
|
|
62
|
+
vuuMsg: vuuMsgByRow.get(idx + CSV_FIRST_DATA_ROW_NUMBER) ?? ""
|
|
63
|
+
})),
|
|
64
|
+
...unparsedErrorRows
|
|
65
|
+
];
|
|
66
|
+
const { errors: rpcErrors } = await executeBatchRpcCalls(allRows, async ({ rowNum, rowData, vuuMsg })=>{
|
|
67
|
+
const payload = {
|
|
68
|
+
...rowData,
|
|
69
|
+
rowNum,
|
|
70
|
+
vuuMsg
|
|
71
|
+
};
|
|
72
|
+
const result = await dataSource.rpcRequest?.({
|
|
73
|
+
type: "RPC_REQUEST",
|
|
74
|
+
rpcName: "addRow",
|
|
75
|
+
params: {
|
|
76
|
+
key: String(rowNum),
|
|
77
|
+
data: payload
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (!isRpcSuccess(result)) {
|
|
81
|
+
const msg = result && "errorMessage" in result ? String(result.errorMessage) : "addRow RPC failed";
|
|
82
|
+
throw Error(msg);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}, ({ rowNum }, error)=>`Row ${rowNum}: ${error}`);
|
|
86
|
+
if (rpcErrors.length > 0) {
|
|
87
|
+
const errorMessage = buildRowErrorMessage("Import failed", rpcErrors);
|
|
88
|
+
const errors = {
|
|
89
|
+
importError: createUploadError("import", errorMessage)
|
|
90
|
+
};
|
|
91
|
+
onError?.({
|
|
92
|
+
errors
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}, [
|
|
96
|
+
dataSource,
|
|
97
|
+
onError,
|
|
98
|
+
table
|
|
99
|
+
]);
|
|
100
|
+
const beginEditSession = useCallback(async ()=>{
|
|
101
|
+
if (!dataSource.beginEditSession) throw Error("CsvUpload requires datasource beginEditSession support.");
|
|
102
|
+
const sessionDataSource = await dataSource.beginEditSession("empty-session-table");
|
|
103
|
+
const sessionVuuTable = sessionDataSource?.table;
|
|
104
|
+
if (sessionVuuTable && isSessionTable(sessionVuuTable)) setActiveSessionTable(sessionVuuTable);
|
|
105
|
+
}, [
|
|
106
|
+
dataSource,
|
|
107
|
+
setActiveSessionTable
|
|
108
|
+
]);
|
|
109
|
+
const closePendingEditSession = useCallback(async (save)=>{
|
|
110
|
+
if (void 0 === sessionTableRef.current) return;
|
|
111
|
+
await endEditSessionAndNotify(save, save ? "saved" : "discarded");
|
|
112
|
+
}, [
|
|
113
|
+
endEditSessionAndNotify
|
|
114
|
+
]);
|
|
115
|
+
const processFile = useCallback(async (file)=>{
|
|
116
|
+
setValidation(void 0);
|
|
117
|
+
onError?.(void 0);
|
|
118
|
+
await closePendingEditSession(false);
|
|
119
|
+
if (void 0 === schema) throw Error("Table schema is not yet available.");
|
|
120
|
+
if (void 0 === table) throw Error("CsvUpload requires dataSource.table to be defined.");
|
|
121
|
+
const parsedCsv = parseCsv(await file.text(), parseOptions);
|
|
122
|
+
if (parsedCsv.error && hasFileParseErrors(parsedCsv.error)) {
|
|
123
|
+
setValidation(void 0);
|
|
124
|
+
onError?.({
|
|
125
|
+
errors: {
|
|
126
|
+
validationError: createUploadError("validation", `Validation failed: ${parsedCsv.error.message}`, parsedCsv.error)
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const schemaValidation = validateCsvAgainstSchema(parsedCsv, schema, {
|
|
132
|
+
maxRows
|
|
133
|
+
});
|
|
134
|
+
if (Object.keys(schemaValidation.errorMap.fileErrors).length > 0) {
|
|
135
|
+
setValidation(schemaValidation);
|
|
136
|
+
onError?.({
|
|
137
|
+
errors: {
|
|
138
|
+
schemaError: createUploadError("schema", "CSV validation failed.", parsedCsv.error, {
|
|
139
|
+
errorMap: schemaValidation.errorMap,
|
|
140
|
+
errors: schemaValidation.errors,
|
|
141
|
+
message: "CSV validation failed."
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const mergedValidation = mergeValidationWithParseErrors(schemaValidation, parsedCsv.error);
|
|
148
|
+
setValidation(mergedValidation);
|
|
149
|
+
if (mergedValidation.rows.length > 0) {
|
|
150
|
+
await beginEditSession();
|
|
151
|
+
try {
|
|
152
|
+
await addAllRows(mergedValidation);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
await endEditSessionAndNotify(false, "failed");
|
|
155
|
+
setValidation(void 0);
|
|
156
|
+
onError?.({
|
|
157
|
+
errors: {
|
|
158
|
+
importError: createUploadError("import", `RPC import failed: ${toErrorMessage(error)}`)
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}, [
|
|
164
|
+
onError,
|
|
165
|
+
addAllRows,
|
|
166
|
+
maxRows,
|
|
167
|
+
parseOptions,
|
|
168
|
+
beginEditSession,
|
|
169
|
+
closePendingEditSession,
|
|
170
|
+
table,
|
|
171
|
+
endEditSessionAndNotify,
|
|
172
|
+
schema
|
|
173
|
+
]);
|
|
174
|
+
const handleFiles = useCallback(async (files)=>{
|
|
175
|
+
const file = files[0];
|
|
176
|
+
if (void 0 === file) return;
|
|
177
|
+
setIsProcessingFile(true);
|
|
178
|
+
onProcessingStarted?.();
|
|
179
|
+
try {
|
|
180
|
+
await processFile(file);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
setValidation(void 0);
|
|
183
|
+
setActiveSessionTable(void 0);
|
|
184
|
+
const parseError = isCsvParseError(err) ? err : void 0;
|
|
185
|
+
const errorMessage = parseError ? `Validation failed: ${parseError.message}` : `Validation failed: ${toErrorMessage(err)}`;
|
|
186
|
+
const errors = {
|
|
187
|
+
errors: {
|
|
188
|
+
validationError: createUploadError("validation", errorMessage, parseError)
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
onError?.(errors);
|
|
192
|
+
} finally{
|
|
193
|
+
setIsProcessingFile(false);
|
|
194
|
+
}
|
|
195
|
+
}, [
|
|
196
|
+
onError,
|
|
197
|
+
onProcessingStarted,
|
|
198
|
+
processFile,
|
|
199
|
+
setActiveSessionTable
|
|
200
|
+
]);
|
|
201
|
+
const onDrop = useCallback((_event, files)=>{
|
|
202
|
+
handleFiles(files);
|
|
203
|
+
}, [
|
|
204
|
+
handleFiles
|
|
205
|
+
]);
|
|
206
|
+
const onTriggerChange = useCallback((_event, files)=>{
|
|
207
|
+
handleFiles(files);
|
|
208
|
+
}, [
|
|
209
|
+
handleFiles
|
|
210
|
+
]);
|
|
211
|
+
const canImport = useMemo(()=>void 0 !== validation && 0 === validation.errors.length && validation.rows.length > 0 && !isProcessingFile && !isImporting, [
|
|
212
|
+
isImporting,
|
|
213
|
+
isProcessingFile,
|
|
214
|
+
validation
|
|
215
|
+
]);
|
|
216
|
+
const importData = useCallback(async ()=>{
|
|
217
|
+
if (!canImport || void 0 === validation) return;
|
|
218
|
+
setIsImporting(true);
|
|
219
|
+
onError?.(void 0);
|
|
220
|
+
try {
|
|
221
|
+
const rpcResult = await endEditSessionAndNotify(true, "saved");
|
|
222
|
+
const fallbackTableData = {
|
|
223
|
+
columns: validation.columns,
|
|
224
|
+
rows: validation.rows.map((row)=>validation.columns.map((column)=>row[column] ?? ""))
|
|
225
|
+
};
|
|
226
|
+
onImported?.({
|
|
227
|
+
rpcResult,
|
|
228
|
+
tableData: normalizeTableData(rpcResult, fallbackTableData)
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const errorMessage = `RPC import failed: ${String(err)}`;
|
|
232
|
+
const errors = {
|
|
233
|
+
errors: {
|
|
234
|
+
importError: createUploadError("import", errorMessage)
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
onError?.(errors);
|
|
238
|
+
} finally{
|
|
239
|
+
setIsImporting(false);
|
|
240
|
+
}
|
|
241
|
+
}, [
|
|
242
|
+
canImport,
|
|
243
|
+
endEditSessionAndNotify,
|
|
244
|
+
onError,
|
|
245
|
+
onImported,
|
|
246
|
+
validation
|
|
247
|
+
]);
|
|
248
|
+
return {
|
|
249
|
+
canImport,
|
|
250
|
+
importData,
|
|
251
|
+
isImporting,
|
|
252
|
+
isProcessingFile,
|
|
253
|
+
onDrop,
|
|
254
|
+
onTriggerChange,
|
|
255
|
+
sessionTable,
|
|
256
|
+
schema,
|
|
257
|
+
validation
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
export { useCsvUpload };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useData } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
const useCsvUploadSessionPreview = (sessionTable)=>{
|
|
4
|
+
const { VuuDataSource, getServerAPI } = useData();
|
|
5
|
+
const [previewConfig, setPreviewConfig] = useState();
|
|
6
|
+
const [previewDataSource, setPreviewDataSource] = useState();
|
|
7
|
+
const [previewError, setPreviewError] = useState();
|
|
8
|
+
const [isLoadingPreview, setIsLoadingPreview] = useState(false);
|
|
9
|
+
useEffect(()=>{
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
if (!sessionTable) {
|
|
12
|
+
setPreviewDataSource(void 0);
|
|
13
|
+
setPreviewConfig(void 0);
|
|
14
|
+
setPreviewError(void 0);
|
|
15
|
+
setIsLoadingPreview(false);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const loadSessionTablePreview = async ()=>{
|
|
19
|
+
setIsLoadingPreview(true);
|
|
20
|
+
setPreviewError(void 0);
|
|
21
|
+
try {
|
|
22
|
+
const server = await getServerAPI();
|
|
23
|
+
const schema = await server.getTableSchema(sessionTable);
|
|
24
|
+
if (cancelled) return;
|
|
25
|
+
const columnDescriptors = schema.columns.map(({ name, serverDataType })=>"vuuMsg" === name ? {
|
|
26
|
+
name,
|
|
27
|
+
serverDataType,
|
|
28
|
+
width: 300,
|
|
29
|
+
label: "Error"
|
|
30
|
+
} : {
|
|
31
|
+
name,
|
|
32
|
+
serverDataType
|
|
33
|
+
});
|
|
34
|
+
setPreviewConfig({
|
|
35
|
+
columns: columnDescriptors,
|
|
36
|
+
zebraStripes: true,
|
|
37
|
+
rowSeparators: true,
|
|
38
|
+
columnSeparators: true
|
|
39
|
+
});
|
|
40
|
+
setPreviewDataSource(new VuuDataSource({
|
|
41
|
+
columns: columnDescriptors.map(({ name })=>name),
|
|
42
|
+
table: sessionTable
|
|
43
|
+
}));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!cancelled) {
|
|
46
|
+
setPreviewDataSource(void 0);
|
|
47
|
+
setPreviewConfig(void 0);
|
|
48
|
+
setPreviewError(`Failed to load session table preview: ${String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
} finally{
|
|
51
|
+
if (!cancelled) setIsLoadingPreview(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
loadSessionTablePreview();
|
|
55
|
+
return ()=>{
|
|
56
|
+
cancelled = true;
|
|
57
|
+
};
|
|
58
|
+
}, [
|
|
59
|
+
VuuDataSource,
|
|
60
|
+
getServerAPI,
|
|
61
|
+
sessionTable
|
|
62
|
+
]);
|
|
63
|
+
return {
|
|
64
|
+
isLoadingPreview,
|
|
65
|
+
previewConfig,
|
|
66
|
+
previewDataSource,
|
|
67
|
+
previewError
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export { useCsvUploadSessionPreview };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.vuuDatasourceStats {
|
|
3
|
+
--datasourceStats-background: var(--vuuDatasourceStats-background);
|
|
4
|
+
background: var(--datasourceStats-background, var(--salt-container-secondary-background));
|
|
5
|
+
font-size: var(--vuuDatasourceStats-fontSize, 10px);
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: var(--vuuDatasourceStats-gap, var(--salt-spacing-100));
|
|
8
|
+
height: var(--vuuDatasourceStats-height, 100%);
|
|
9
|
+
padding: 0 var(--salt-spacing-200);
|
|
10
|
+
grid-template: "panel-start panel-center panel-end"
|
|
11
|
+
/ 1fr 1fr 1fr;
|
|
12
|
+
display: grid;
|
|
13
|
+
|
|
14
|
+
&.vuuDatasourceStats-withSelection {
|
|
15
|
+
--datasourceStats-background: var(--vuuDatasourceStats-withSelection-background, var(--vuuDatasourceStats-background));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
& .vuuDatasourceStats-statsPanel {
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: var(--salt-spacing-100);
|
|
21
|
+
width: fit-content;
|
|
22
|
+
display: flex;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
& .vuuDatasourceStats-rowStats {
|
|
26
|
+
grid-area: panel-start;
|
|
27
|
+
justify-self: flex-start;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
& .vuuDatasourceStats-selected {
|
|
31
|
+
margin-left: var(--salt-spacing-100);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
& .vuuDatasourceStats-label {
|
|
35
|
+
color: var(--salt-content-secondary-foreground);
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
& .vuuDatasourceStats-range {
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
& .vuuDatasourceStats-value {
|
|
44
|
+
color: var(--salt-content-primary-foreground);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
& .vuuDatasourceStats-tooltray {
|
|
48
|
+
margin: 0 var(--salt-spacing-200);
|
|
49
|
+
grid-area: panel-end;
|
|
50
|
+
justify-self: end;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
`;
|
|
56
|
+
export default css;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
4
|
+
import { useWindow } from "@salt-ds/window";
|
|
5
|
+
import { useDatasourceStats } from "./useDatasourceStats.js";
|
|
6
|
+
import DatasourceStats from "./DatasourceStats.css";
|
|
7
|
+
const classBase = "vuuDatasourceStats";
|
|
8
|
+
const numberFormatter = new Intl.NumberFormat();
|
|
9
|
+
const DataSourceStats = ({ children, className, dataSource, showRowStats = true, tooltrayActions, ...htmlAttributes })=>{
|
|
10
|
+
const targetWindow = useWindow();
|
|
11
|
+
useComponentCssInjection({
|
|
12
|
+
testId: "vuu-datasource-stats",
|
|
13
|
+
css: DatasourceStats,
|
|
14
|
+
window: targetWindow
|
|
15
|
+
});
|
|
16
|
+
const { range, selectedCount, size } = useDatasourceStats({
|
|
17
|
+
dataSource,
|
|
18
|
+
showRowStats
|
|
19
|
+
});
|
|
20
|
+
const from = numberFormatter.format(range.from + 1);
|
|
21
|
+
const to = numberFormatter.format(Math.min(range.to, size));
|
|
22
|
+
const value = numberFormatter.format(size);
|
|
23
|
+
if (0 === size) return /*#__PURE__*/ jsx("div", {
|
|
24
|
+
...htmlAttributes,
|
|
25
|
+
className: clsx(classBase, className),
|
|
26
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
27
|
+
className: `${classBase}-label`,
|
|
28
|
+
children: "No Rows to display"
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
32
|
+
...htmlAttributes,
|
|
33
|
+
className: clsx(classBase, className, {
|
|
34
|
+
[`${classBase}-withSelection`]: selectedCount > 0
|
|
35
|
+
}),
|
|
36
|
+
children: [
|
|
37
|
+
showRowStats ? /*#__PURE__*/ jsxs("div", {
|
|
38
|
+
className: `${classBase}-statsPanel ${classBase}-rowStats`,
|
|
39
|
+
children: [
|
|
40
|
+
/*#__PURE__*/ jsx("span", {
|
|
41
|
+
className: `${classBase}-label`,
|
|
42
|
+
children: "Row count"
|
|
43
|
+
}),
|
|
44
|
+
/*#__PURE__*/ jsxs("span", {
|
|
45
|
+
className: `${classBase}-range`,
|
|
46
|
+
children: [
|
|
47
|
+
/*#__PURE__*/ jsx("span", {
|
|
48
|
+
className: `${classBase}-value`,
|
|
49
|
+
children: from
|
|
50
|
+
}),
|
|
51
|
+
/*#__PURE__*/ jsx("span", {
|
|
52
|
+
className: `${classBase}-label`,
|
|
53
|
+
children: "-"
|
|
54
|
+
}),
|
|
55
|
+
/*#__PURE__*/ jsx("span", {
|
|
56
|
+
className: `${classBase}-value`,
|
|
57
|
+
children: to
|
|
58
|
+
})
|
|
59
|
+
]
|
|
60
|
+
}),
|
|
61
|
+
/*#__PURE__*/ jsx("span", {
|
|
62
|
+
className: `${classBase}-label`,
|
|
63
|
+
children: "of"
|
|
64
|
+
}),
|
|
65
|
+
/*#__PURE__*/ jsx("span", {
|
|
66
|
+
className: `${classBase}-value`,
|
|
67
|
+
children: value
|
|
68
|
+
}),
|
|
69
|
+
selectedCount > 0 ? /*#__PURE__*/ jsx("span", {
|
|
70
|
+
className: clsx(`${classBase}-label`, `${classBase}-selected`),
|
|
71
|
+
children: `${selectedCount.toLocaleString()} selected`
|
|
72
|
+
}) : null
|
|
73
|
+
]
|
|
74
|
+
}) : null,
|
|
75
|
+
selectedCount > 0 ? /*#__PURE__*/ jsx("div", {
|
|
76
|
+
className: `${classBase}-statsPanel ${classBase}-selectionActions`,
|
|
77
|
+
children: children
|
|
78
|
+
}) : null,
|
|
79
|
+
tooltrayActions ? /*#__PURE__*/ jsx("div", {
|
|
80
|
+
className: `${classBase}-statsPanel ${classBase}-tooltray`,
|
|
81
|
+
children: /*#__PURE__*/ jsx("span", {
|
|
82
|
+
className: `${classBase}-actions`,
|
|
83
|
+
children: tooltrayActions
|
|
84
|
+
})
|
|
85
|
+
}) : null
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
export { DataSourceStats };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { formatDate } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
const timeFormatter = formatDate({
|
|
4
|
+
time: "hh:mm:ss"
|
|
5
|
+
});
|
|
6
|
+
const formatTime = (ts)=>{
|
|
7
|
+
if ("number" == typeof ts) return timeFormatter(new Date(ts));
|
|
8
|
+
};
|
|
9
|
+
const useDatasourceStats = ({ dataSource })=>{
|
|
10
|
+
const [selectedCount, setSelectedCount] = useState(0);
|
|
11
|
+
const [range, setRange] = useState(dataSource.range);
|
|
12
|
+
const [size, setSize] = useState(dataSource.size);
|
|
13
|
+
const [freezeTime, setFreezeTime] = useState(formatTime(dataSource.freezeTimestamp));
|
|
14
|
+
const handleFreeze = useCallback((isFrozen, freezeTimestamp)=>{
|
|
15
|
+
isFrozen ? setFreezeTime(formatTime(freezeTimestamp)) : setFreezeTime(void 0);
|
|
16
|
+
}, []);
|
|
17
|
+
const handleRowSelection = useCallback((count)=>{
|
|
18
|
+
setSelectedCount(count);
|
|
19
|
+
}, []);
|
|
20
|
+
const handleSize = useCallback((size)=>{
|
|
21
|
+
setSize(size);
|
|
22
|
+
}, []);
|
|
23
|
+
useMemo(()=>{
|
|
24
|
+
setSize(dataSource.size);
|
|
25
|
+
dataSource.on("resize", handleSize);
|
|
26
|
+
dataSource.on("range", setRange);
|
|
27
|
+
dataSource.on("freeze", handleFreeze);
|
|
28
|
+
dataSource.on("row-selection", handleRowSelection);
|
|
29
|
+
return ()=>{
|
|
30
|
+
dataSource.removeListener("resize", handleSize);
|
|
31
|
+
dataSource.removeListener("range", setRange);
|
|
32
|
+
dataSource.removeListener("freeze", handleFreeze);
|
|
33
|
+
dataSource.removeListener("row-selection", handleRowSelection);
|
|
34
|
+
};
|
|
35
|
+
}, [
|
|
36
|
+
dataSource,
|
|
37
|
+
handleFreeze,
|
|
38
|
+
handleRowSelection,
|
|
39
|
+
handleSize
|
|
40
|
+
]);
|
|
41
|
+
return {
|
|
42
|
+
range,
|
|
43
|
+
selectedCount,
|
|
44
|
+
size,
|
|
45
|
+
freezeTime
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export { useDatasourceStats };
|