@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,173 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, FormField, FormFieldLabel, ToggleButton, ToggleButtonGroup } from "@salt-ds/core";
|
|
3
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
4
|
+
import { useWindow } from "@salt-ds/window";
|
|
5
|
+
import { Icon, VuuInput } from "@vuu-ui/vuu-ui-controls";
|
|
6
|
+
import { getCalculatedColumnDetails, getDefaultAlignment, isCalculatedColumn } from "@vuu-ui/vuu-utils";
|
|
7
|
+
import { ColumnFormattingPanel } from "../column-formatting-settings/index.js";
|
|
8
|
+
import { ColumnNameLabel } from "./ColumnNameLabel.js";
|
|
9
|
+
import { useColumnSettings } from "./useColumnSettings.js";
|
|
10
|
+
import ColumnSettingsPanel from "./ColumnSettingsPanel.css";
|
|
11
|
+
const classBase = "vuuColumnSettingsPanel";
|
|
12
|
+
const getColumnLabel = (column)=>{
|
|
13
|
+
const { name, label } = column;
|
|
14
|
+
if (isCalculatedColumn(name)) return label ?? getCalculatedColumnDetails(column).name;
|
|
15
|
+
return label ?? name;
|
|
16
|
+
};
|
|
17
|
+
const ColumnSettingsPanel_ColumnSettingsPanel = ({ column: columnProp, columnModel, onClickEditCalculatedColumn, onConfigChange })=>{
|
|
18
|
+
const targetWindow = useWindow();
|
|
19
|
+
useComponentCssInjection({
|
|
20
|
+
testId: "vuu-column-settings-panel",
|
|
21
|
+
css: ColumnSettingsPanel,
|
|
22
|
+
window: targetWindow
|
|
23
|
+
});
|
|
24
|
+
const isNewCalculatedColumn = "::" === columnProp.name;
|
|
25
|
+
const { availableRenderers, column, navigateNextColumn, navigatePrevColumn, onChange, onChangeFormatting, onChangeRendering, onChangeToggleButton, onChangeType, onInputCommit } = useColumnSettings({
|
|
26
|
+
column: columnProp,
|
|
27
|
+
columnModel,
|
|
28
|
+
onConfigChange
|
|
29
|
+
});
|
|
30
|
+
const { serverDataType, align = getDefaultAlignment(serverDataType), pin, width = "" } = column;
|
|
31
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
32
|
+
className: classBase,
|
|
33
|
+
children: [
|
|
34
|
+
/*#__PURE__*/ jsx("div", {
|
|
35
|
+
className: `${classBase}-header`,
|
|
36
|
+
children: /*#__PURE__*/ jsx(ColumnNameLabel, {
|
|
37
|
+
column: column,
|
|
38
|
+
onClick: onClickEditCalculatedColumn
|
|
39
|
+
})
|
|
40
|
+
}),
|
|
41
|
+
/*#__PURE__*/ jsxs(FormField, {
|
|
42
|
+
"data-field": "column-label",
|
|
43
|
+
children: [
|
|
44
|
+
/*#__PURE__*/ jsx(FormFieldLabel, {
|
|
45
|
+
children: "Column Label"
|
|
46
|
+
}),
|
|
47
|
+
/*#__PURE__*/ jsx(VuuInput, {
|
|
48
|
+
bordered: true,
|
|
49
|
+
className: "vuuInput",
|
|
50
|
+
"data-embedded": true,
|
|
51
|
+
onChange: onChange,
|
|
52
|
+
onCommit: onInputCommit,
|
|
53
|
+
value: getColumnLabel(column)
|
|
54
|
+
})
|
|
55
|
+
]
|
|
56
|
+
}),
|
|
57
|
+
/*#__PURE__*/ jsxs(FormField, {
|
|
58
|
+
"data-field": "column-width",
|
|
59
|
+
children: [
|
|
60
|
+
/*#__PURE__*/ jsx(FormFieldLabel, {
|
|
61
|
+
children: "Column Width"
|
|
62
|
+
}),
|
|
63
|
+
/*#__PURE__*/ jsx(VuuInput, {
|
|
64
|
+
bordered: true,
|
|
65
|
+
className: "vuuInput",
|
|
66
|
+
"data-embedded": true,
|
|
67
|
+
onChange: onChange,
|
|
68
|
+
value: width,
|
|
69
|
+
onCommit: onInputCommit
|
|
70
|
+
})
|
|
71
|
+
]
|
|
72
|
+
}),
|
|
73
|
+
/*#__PURE__*/ jsxs(FormField, {
|
|
74
|
+
"data-field": "column-alignment",
|
|
75
|
+
children: [
|
|
76
|
+
/*#__PURE__*/ jsx(FormFieldLabel, {
|
|
77
|
+
children: "Alignment"
|
|
78
|
+
}),
|
|
79
|
+
/*#__PURE__*/ jsxs(ToggleButtonGroup, {
|
|
80
|
+
onChange: onChangeToggleButton,
|
|
81
|
+
value: align,
|
|
82
|
+
children: [
|
|
83
|
+
/*#__PURE__*/ jsx(ToggleButton, {
|
|
84
|
+
value: "left",
|
|
85
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
86
|
+
name: "align-left",
|
|
87
|
+
size: 16
|
|
88
|
+
})
|
|
89
|
+
}),
|
|
90
|
+
/*#__PURE__*/ jsx(ToggleButton, {
|
|
91
|
+
value: "right",
|
|
92
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
93
|
+
name: "align-right",
|
|
94
|
+
size: 16
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
]
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
}),
|
|
101
|
+
/*#__PURE__*/ jsxs(FormField, {
|
|
102
|
+
"data-field": "column-pin",
|
|
103
|
+
children: [
|
|
104
|
+
/*#__PURE__*/ jsx(FormFieldLabel, {
|
|
105
|
+
children: "Pin Column"
|
|
106
|
+
}),
|
|
107
|
+
/*#__PURE__*/ jsxs(ToggleButtonGroup, {
|
|
108
|
+
onChange: onChangeToggleButton,
|
|
109
|
+
value: pin || "false",
|
|
110
|
+
children: [
|
|
111
|
+
/*#__PURE__*/ jsx(ToggleButton, {
|
|
112
|
+
value: "left",
|
|
113
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
114
|
+
name: "pin-left",
|
|
115
|
+
size: 16
|
|
116
|
+
})
|
|
117
|
+
}),
|
|
118
|
+
/*#__PURE__*/ jsx(ToggleButton, {
|
|
119
|
+
value: "floating",
|
|
120
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
121
|
+
name: "pin-float",
|
|
122
|
+
size: 16
|
|
123
|
+
})
|
|
124
|
+
}),
|
|
125
|
+
/*#__PURE__*/ jsx(ToggleButton, {
|
|
126
|
+
value: "right",
|
|
127
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
128
|
+
name: "pin-right",
|
|
129
|
+
size: 16
|
|
130
|
+
})
|
|
131
|
+
}),
|
|
132
|
+
/*#__PURE__*/ jsx(ToggleButton, {
|
|
133
|
+
value: "",
|
|
134
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
135
|
+
name: "cross-circle",
|
|
136
|
+
size: 16
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
]
|
|
140
|
+
})
|
|
141
|
+
]
|
|
142
|
+
}),
|
|
143
|
+
/*#__PURE__*/ jsx(ColumnFormattingPanel, {
|
|
144
|
+
availableRenderers: availableRenderers,
|
|
145
|
+
column: column,
|
|
146
|
+
onChangeFormatting: onChangeFormatting,
|
|
147
|
+
onChangeRendering: onChangeRendering,
|
|
148
|
+
onChangeColumnType: onChangeType
|
|
149
|
+
}),
|
|
150
|
+
/*#__PURE__*/ jsxs("div", {
|
|
151
|
+
className: `${classBase}-buttonBar`,
|
|
152
|
+
"data-align": isNewCalculatedColumn ? "right" : void 0,
|
|
153
|
+
children: [
|
|
154
|
+
/*#__PURE__*/ jsx(Button, {
|
|
155
|
+
className: `${classBase}-buttonNavPrev`,
|
|
156
|
+
variant: "secondary",
|
|
157
|
+
"data-icon": "arrow-left",
|
|
158
|
+
onClick: navigatePrevColumn,
|
|
159
|
+
children: "PREVIOUS"
|
|
160
|
+
}),
|
|
161
|
+
/*#__PURE__*/ jsx(Button, {
|
|
162
|
+
className: `${classBase}-buttonNavNext`,
|
|
163
|
+
variant: "secondary",
|
|
164
|
+
"data-icon": "arrow-right",
|
|
165
|
+
onClick: navigateNextColumn,
|
|
166
|
+
children: "NEXT"
|
|
167
|
+
})
|
|
168
|
+
]
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
export { ColumnSettingsPanel_ColumnSettingsPanel as ColumnSettingsPanel };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { getFieldName, getRegisteredCellRenderers, getServerDataType, isValidColumnAlignment, isValidPinLocation, queryClosest, setCalculatedColumnName, updateColumnFormatting, updateColumnRenderProps, updateColumnType } from "@vuu-ui/vuu-utils";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
const integerCellRenderers = [
|
|
4
|
+
{
|
|
5
|
+
description: "Default formatter for columns with data type integer",
|
|
6
|
+
label: "Default Renderer (int, long)",
|
|
7
|
+
name: "default-int"
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
const doubleCellRenderers = [
|
|
11
|
+
{
|
|
12
|
+
description: "Default formatter for columns with data type double",
|
|
13
|
+
label: "Default Renderer (double)",
|
|
14
|
+
name: "default-double"
|
|
15
|
+
}
|
|
16
|
+
];
|
|
17
|
+
const decimalCellRenderers = [
|
|
18
|
+
{
|
|
19
|
+
description: "Default formatter for columns with data type decimal (2,4,6 or 8)",
|
|
20
|
+
label: "Default Renderer (decimal)",
|
|
21
|
+
name: "default-decimal"
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
const stringCellRenderers = [
|
|
25
|
+
{
|
|
26
|
+
description: "Default formatter for columns with data type string",
|
|
27
|
+
label: "Default Renderer (string)",
|
|
28
|
+
name: "default-string"
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
const booleanCellRenderers = [];
|
|
32
|
+
const getAvailableCellRenderers = (column)=>{
|
|
33
|
+
const serverDataType = getServerDataType(column);
|
|
34
|
+
switch(serverDataType){
|
|
35
|
+
case "char":
|
|
36
|
+
case "string":
|
|
37
|
+
return stringCellRenderers.concat(getRegisteredCellRenderers("string"));
|
|
38
|
+
case "int":
|
|
39
|
+
case "long":
|
|
40
|
+
return integerCellRenderers.concat(getRegisteredCellRenderers("int"));
|
|
41
|
+
case "double":
|
|
42
|
+
return doubleCellRenderers.concat(getRegisteredCellRenderers(column.serverDataType));
|
|
43
|
+
case "scaleddecimal2":
|
|
44
|
+
case "scaleddecimal4":
|
|
45
|
+
case "scaleddecimal6":
|
|
46
|
+
case "scaleddecimal8":
|
|
47
|
+
return decimalCellRenderers.concat(getRegisteredCellRenderers(column.serverDataType));
|
|
48
|
+
case "boolean":
|
|
49
|
+
return booleanCellRenderers.concat(getRegisteredCellRenderers("boolean"));
|
|
50
|
+
default:
|
|
51
|
+
return stringCellRenderers;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const useColumnSettings = ({ column: columnProp, columnModel })=>{
|
|
55
|
+
const [column, setColumn] = useState(columnModel.getColumn(columnProp.name));
|
|
56
|
+
useEffect(()=>{
|
|
57
|
+
setColumn(columnProp);
|
|
58
|
+
}, [
|
|
59
|
+
columnProp
|
|
60
|
+
]);
|
|
61
|
+
const availableRenderers = useMemo(()=>getAvailableCellRenderers(column), [
|
|
62
|
+
column
|
|
63
|
+
]);
|
|
64
|
+
const handleChangeToggleButton = useCallback((evt)=>{
|
|
65
|
+
const button = queryClosest(evt.target, "button");
|
|
66
|
+
if (button) {
|
|
67
|
+
const fieldName = getFieldName(button);
|
|
68
|
+
const { value } = button;
|
|
69
|
+
switch(fieldName){
|
|
70
|
+
case "column-alignment":
|
|
71
|
+
if (isValidColumnAlignment(value)) {
|
|
72
|
+
const newColumn = {
|
|
73
|
+
...column,
|
|
74
|
+
align: value
|
|
75
|
+
};
|
|
76
|
+
setColumn(newColumn);
|
|
77
|
+
columnModel.updateColumn(newColumn);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case "column-pin":
|
|
81
|
+
if (isValidPinLocation(value)) {
|
|
82
|
+
const newColumn = {
|
|
83
|
+
...column,
|
|
84
|
+
pin: value || void 0
|
|
85
|
+
};
|
|
86
|
+
setColumn(newColumn);
|
|
87
|
+
columnModel.updateColumn(newColumn);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, [
|
|
93
|
+
column,
|
|
94
|
+
columnModel
|
|
95
|
+
]);
|
|
96
|
+
const handleChange = useCallback((evt)=>{
|
|
97
|
+
const input = evt.target;
|
|
98
|
+
const fieldName = getFieldName(input);
|
|
99
|
+
const { value } = input;
|
|
100
|
+
switch(fieldName){
|
|
101
|
+
case "column-label":
|
|
102
|
+
setColumn((state)=>({
|
|
103
|
+
...state,
|
|
104
|
+
label: value
|
|
105
|
+
}));
|
|
106
|
+
break;
|
|
107
|
+
case "column-name":
|
|
108
|
+
setColumn((state)=>setCalculatedColumnName(state, value));
|
|
109
|
+
break;
|
|
110
|
+
case "column-width":
|
|
111
|
+
{
|
|
112
|
+
const numericValue = parseInt(value, 10);
|
|
113
|
+
isNaN(numericValue) ? setColumn((state)=>({
|
|
114
|
+
...state,
|
|
115
|
+
width: void 0
|
|
116
|
+
})) : setColumn((state)=>({
|
|
117
|
+
...state,
|
|
118
|
+
width: numericValue
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}, []);
|
|
124
|
+
const handleInputCommit = useCallback(()=>{
|
|
125
|
+
columnModel.updateColumn(column);
|
|
126
|
+
}, [
|
|
127
|
+
column,
|
|
128
|
+
columnModel
|
|
129
|
+
]);
|
|
130
|
+
const handleChangeCalculatedColumnName = useCallback((name)=>{
|
|
131
|
+
setColumn((state)=>({
|
|
132
|
+
...state,
|
|
133
|
+
name
|
|
134
|
+
}));
|
|
135
|
+
}, []);
|
|
136
|
+
const handleChangeRendering = useCallback((renderProps)=>{
|
|
137
|
+
if (renderProps) {
|
|
138
|
+
const newColumn = updateColumnRenderProps(column, renderProps);
|
|
139
|
+
setColumn(newColumn);
|
|
140
|
+
columnModel.updateColumn(newColumn);
|
|
141
|
+
}
|
|
142
|
+
}, [
|
|
143
|
+
column,
|
|
144
|
+
columnModel
|
|
145
|
+
]);
|
|
146
|
+
const handleChangeFormatting = useCallback((formatting)=>{
|
|
147
|
+
const newColumn = updateColumnFormatting(column, formatting);
|
|
148
|
+
setColumn(newColumn);
|
|
149
|
+
columnModel.updateColumn(newColumn);
|
|
150
|
+
}, [
|
|
151
|
+
column,
|
|
152
|
+
columnModel
|
|
153
|
+
]);
|
|
154
|
+
const handleChangeType = useCallback((type)=>{
|
|
155
|
+
const updatedColumn = updateColumnType(column, type);
|
|
156
|
+
setColumn(updatedColumn);
|
|
157
|
+
columnModel.updateColumn(updatedColumn);
|
|
158
|
+
}, [
|
|
159
|
+
column,
|
|
160
|
+
columnModel
|
|
161
|
+
]);
|
|
162
|
+
const handleChangeServerDataType = useCallback((serverDataType)=>{
|
|
163
|
+
setColumn((col)=>({
|
|
164
|
+
...col,
|
|
165
|
+
serverDataType
|
|
166
|
+
}));
|
|
167
|
+
}, []);
|
|
168
|
+
const navigateColumn = useCallback(({ moveBy })=>{
|
|
169
|
+
const index = columnModel.selectedColumns.indexOf(column) + moveBy;
|
|
170
|
+
const newColumn = columnModel.selectedColumns[index];
|
|
171
|
+
if (newColumn) setColumn(newColumn);
|
|
172
|
+
}, [
|
|
173
|
+
column,
|
|
174
|
+
columnModel
|
|
175
|
+
]);
|
|
176
|
+
const navigateNextColumn = useCallback(()=>{
|
|
177
|
+
navigateColumn({
|
|
178
|
+
moveBy: 1
|
|
179
|
+
});
|
|
180
|
+
}, [
|
|
181
|
+
navigateColumn
|
|
182
|
+
]);
|
|
183
|
+
const navigatePrevColumn = useCallback(()=>{
|
|
184
|
+
navigateColumn({
|
|
185
|
+
moveBy: -1
|
|
186
|
+
});
|
|
187
|
+
}, [
|
|
188
|
+
navigateColumn
|
|
189
|
+
]);
|
|
190
|
+
return {
|
|
191
|
+
availableRenderers,
|
|
192
|
+
column,
|
|
193
|
+
navigateNextColumn,
|
|
194
|
+
navigatePrevColumn,
|
|
195
|
+
onChange: handleChange,
|
|
196
|
+
onChangeCalculatedColumnName: handleChangeCalculatedColumnName,
|
|
197
|
+
onChangeFormatting: handleChangeFormatting,
|
|
198
|
+
onChangeRendering: handleChangeRendering,
|
|
199
|
+
onChangeServerDataType: handleChangeServerDataType,
|
|
200
|
+
onChangeToggleButton: handleChangeToggleButton,
|
|
201
|
+
onChangeType: handleChangeType,
|
|
202
|
+
onInputCommit: handleInputCommit
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
export { useColumnSettings };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.vuuCsvUpload {
|
|
3
|
+
gap: var(--salt-spacing-200);
|
|
4
|
+
display: grid;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.vuuCsvUpload-dropZone {
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: var(--salt-spacing-100);
|
|
10
|
+
min-height: calc(var(--salt-size-base) * 4);
|
|
11
|
+
padding: var(--salt-spacing-300);
|
|
12
|
+
text-align: center;
|
|
13
|
+
justify-items: center;
|
|
14
|
+
display: grid;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.vuuCsvUpload-errors {
|
|
18
|
+
border: solid 1px var(--salt-status-error-borderColor);
|
|
19
|
+
border-radius: var(--salt-size-borderRadius);
|
|
20
|
+
color: var(--salt-status-error-foreground);
|
|
21
|
+
gap: var(--salt-spacing-100);
|
|
22
|
+
padding: var(--salt-spacing-200);
|
|
23
|
+
display: grid;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
`;
|
|
28
|
+
export default css;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Dialog, DialogActions, DialogContent, DialogHeader, FileDropZone, FileDropZoneIcon, FileDropZoneTrigger } from "@salt-ds/core";
|
|
3
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
4
|
+
import { useWindow } from "@salt-ds/window";
|
|
5
|
+
import { useCallback, useState } from "react";
|
|
6
|
+
import { useCsvUpload } from "./useCsvUpload.js";
|
|
7
|
+
import CsvUpload from "./CsvUpload.css";
|
|
8
|
+
const classBase = "vuuCsvUpload";
|
|
9
|
+
const CsvUpload_CsvUpload = (props)=>{
|
|
10
|
+
const { children, dialogTitle = "Import CSV", onCancel, onClose, open } = props;
|
|
11
|
+
const isControlledOpen = void 0 !== open;
|
|
12
|
+
const [internalOpen, setInternalOpen] = useState(open ?? true);
|
|
13
|
+
const handleCancel = useCallback(()=>{
|
|
14
|
+
if (!isControlledOpen) setInternalOpen(false);
|
|
15
|
+
onCancel?.();
|
|
16
|
+
}, [
|
|
17
|
+
isControlledOpen,
|
|
18
|
+
onCancel
|
|
19
|
+
]);
|
|
20
|
+
const dialogOpen = isControlledOpen ? open : internalOpen;
|
|
21
|
+
const targetWindow = useWindow();
|
|
22
|
+
useComponentCssInjection({
|
|
23
|
+
testId: "vuu-csv-upload",
|
|
24
|
+
css: CsvUpload,
|
|
25
|
+
window: targetWindow
|
|
26
|
+
});
|
|
27
|
+
const { canImport, isProcessingFile, isImporting, importData, onDrop, onTriggerChange, schema, validation } = useCsvUpload(props);
|
|
28
|
+
const handleImport = useCallback(async ()=>{
|
|
29
|
+
await importData();
|
|
30
|
+
onClose?.();
|
|
31
|
+
}, [
|
|
32
|
+
importData,
|
|
33
|
+
onClose
|
|
34
|
+
]);
|
|
35
|
+
return /*#__PURE__*/ jsxs(Dialog, {
|
|
36
|
+
open: dialogOpen,
|
|
37
|
+
children: [
|
|
38
|
+
/*#__PURE__*/ jsx(DialogHeader, {
|
|
39
|
+
header: dialogTitle
|
|
40
|
+
}),
|
|
41
|
+
/*#__PURE__*/ jsx(DialogContent, {
|
|
42
|
+
children: /*#__PURE__*/ jsxs("div", {
|
|
43
|
+
className: classBase,
|
|
44
|
+
children: [
|
|
45
|
+
/*#__PURE__*/ jsxs(FileDropZone, {
|
|
46
|
+
className: `${classBase}-dropZone`,
|
|
47
|
+
disabled: void 0 === schema || isProcessingFile || isImporting,
|
|
48
|
+
onDrop: onDrop,
|
|
49
|
+
status: validation && validation.errors.length > 0 ? "error" : void 0,
|
|
50
|
+
children: [
|
|
51
|
+
/*#__PURE__*/ jsx(FileDropZoneIcon, {}),
|
|
52
|
+
validation && validation.errors.length > 0 ? /*#__PURE__*/ jsxs(Fragment, {
|
|
53
|
+
children: [
|
|
54
|
+
/*#__PURE__*/ jsx("div", {
|
|
55
|
+
children: "Your file contains errors"
|
|
56
|
+
}),
|
|
57
|
+
/*#__PURE__*/ jsx("div", {
|
|
58
|
+
children: " Please rectify and reupload"
|
|
59
|
+
})
|
|
60
|
+
]
|
|
61
|
+
}) : /*#__PURE__*/ jsx("div", {
|
|
62
|
+
children: "Drop a file here or"
|
|
63
|
+
}),
|
|
64
|
+
/*#__PURE__*/ jsx(FileDropZoneTrigger, {
|
|
65
|
+
accept: ".csv,text/csv",
|
|
66
|
+
onChange: onTriggerChange,
|
|
67
|
+
children: "BROWSE FILES"
|
|
68
|
+
})
|
|
69
|
+
]
|
|
70
|
+
}),
|
|
71
|
+
children
|
|
72
|
+
]
|
|
73
|
+
})
|
|
74
|
+
}),
|
|
75
|
+
/*#__PURE__*/ jsxs(DialogActions, {
|
|
76
|
+
children: [
|
|
77
|
+
/*#__PURE__*/ jsx(Button, {
|
|
78
|
+
appearance: "solid",
|
|
79
|
+
sentiment: "negative",
|
|
80
|
+
onClick: handleCancel,
|
|
81
|
+
children: "Cancel"
|
|
82
|
+
}),
|
|
83
|
+
/*#__PURE__*/ jsx(Button, {
|
|
84
|
+
disabled: !canImport,
|
|
85
|
+
appearance: "solid",
|
|
86
|
+
sentiment: "accented",
|
|
87
|
+
onClick: handleImport,
|
|
88
|
+
children: isProcessingFile ? "Validating..." : isImporting ? "Importing..." : "Import"
|
|
89
|
+
})
|
|
90
|
+
]
|
|
91
|
+
})
|
|
92
|
+
]
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
export { CsvUpload_CsvUpload as CsvUpload };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CsvUpload } from "./CsvUpload.js";
|
|
2
|
+
export { useCsvUpload } from "./useCsvUpload.js";
|
|
3
|
+
export { useCsvUploadSessionPreview } from "./useCsvUploadSessionPreview.js";
|
|
4
|
+
export { validateCsvAgainstSchema } from "./parse/csv-schema-validation.js";
|
|
5
|
+
export { parseCsv } from "./parse/csv-parse.js";
|
|
6
|
+
export { CsvParseErrorEnum, CsvValidationErrorEnum } from "./parse/csv-errors.js";
|
|
7
|
+
export { CSV_FIRST_DATA_ROW_NUMBER, CSV_HEADER_ROW_NUMBER } from "./parse/csv-constants.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CSV_HEADER_ROW_NUMBER } from "./csv-constants.js";
|
|
2
|
+
var csv_errors_CsvParseErrorEnum = /*#__PURE__*/ function(CsvParseErrorEnum) {
|
|
3
|
+
CsvParseErrorEnum["EMPTY_FILE"] = "EMPTY_FILE";
|
|
4
|
+
CsvParseErrorEnum["INVALID_SEPARATOR"] = "INVALID_SEPARATOR";
|
|
5
|
+
CsvParseErrorEnum["INVALID_FORMAT"] = "INVALID_FORMAT";
|
|
6
|
+
CsvParseErrorEnum["UNQUOTED_VALUE"] = "UNQUOTED_VALUE";
|
|
7
|
+
CsvParseErrorEnum["EMPTY_HEADER_COLUMN"] = "EMPTY_HEADER_COLUMN";
|
|
8
|
+
CsvParseErrorEnum["DUPLICATE_HEADER_COLUMN"] = "DUPLICATE_HEADER_COLUMN";
|
|
9
|
+
CsvParseErrorEnum["ROW_COLUMN_COUNT_MISMATCH"] = "ROW_COLUMN_COUNT_MISMATCH";
|
|
10
|
+
return CsvParseErrorEnum;
|
|
11
|
+
}({});
|
|
12
|
+
var csv_errors_CsvValidationErrorEnum = /*#__PURE__*/ function(CsvValidationErrorEnum) {
|
|
13
|
+
CsvValidationErrorEnum["MISSING_KEY_COLUMN"] = "MISSING_KEY_COLUMN";
|
|
14
|
+
CsvValidationErrorEnum["UNKNOWN_COLUMN"] = "UNKNOWN_COLUMN";
|
|
15
|
+
CsvValidationErrorEnum["MAX_ROWS_EXCEEDED"] = "MAX_ROWS_EXCEEDED";
|
|
16
|
+
CsvValidationErrorEnum["EMPTY_NON_STRING_VALUE"] = "EMPTY_NON_STRING_VALUE";
|
|
17
|
+
CsvValidationErrorEnum["TYPE_MISMATCH"] = "TYPE_MISMATCH";
|
|
18
|
+
return CsvValidationErrorEnum;
|
|
19
|
+
}({});
|
|
20
|
+
const createCsvErrorState = ()=>({
|
|
21
|
+
errorMap: {
|
|
22
|
+
rowErrors: {},
|
|
23
|
+
fileErrors: {}
|
|
24
|
+
},
|
|
25
|
+
errors: []
|
|
26
|
+
});
|
|
27
|
+
const addCsvFileError = (state, columnName, error, message, value = "")=>{
|
|
28
|
+
const existing = state.errorMap.fileErrors[columnName] ?? [];
|
|
29
|
+
existing.push(error);
|
|
30
|
+
state.errorMap.fileErrors[columnName] = existing;
|
|
31
|
+
state.errors.push({
|
|
32
|
+
rowNum: CSV_HEADER_ROW_NUMBER,
|
|
33
|
+
column: columnName,
|
|
34
|
+
value,
|
|
35
|
+
message,
|
|
36
|
+
errorEnum: error
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const addCsvRowError = (state, rowNum, columnName, error, message, value = "")=>{
|
|
40
|
+
const rowErrors = state.errorMap.rowErrors[rowNum] ?? {};
|
|
41
|
+
const existing = rowErrors[columnName] ?? [];
|
|
42
|
+
existing.push(error);
|
|
43
|
+
rowErrors[columnName] = existing;
|
|
44
|
+
state.errorMap.rowErrors[rowNum] = rowErrors;
|
|
45
|
+
state.errors.push({
|
|
46
|
+
rowNum,
|
|
47
|
+
column: columnName,
|
|
48
|
+
value,
|
|
49
|
+
message,
|
|
50
|
+
errorEnum: error
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const hasCsvErrors = (errorMap)=>Object.keys(errorMap.fileErrors).length > 0 || Object.keys(errorMap.rowErrors).length > 0;
|
|
54
|
+
export { addCsvFileError, addCsvRowError, createCsvErrorState, csv_errors_CsvParseErrorEnum as CsvParseErrorEnum, csv_errors_CsvValidationErrorEnum as CsvValidationErrorEnum, hasCsvErrors };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { parse } from "csv-parse/browser/esm/sync";
|
|
2
|
+
import { CsvParseErrorEnum, addCsvFileError, addCsvRowError, createCsvErrorState, hasCsvErrors } from "./csv-errors.js";
|
|
3
|
+
import { CSV_FIRST_DATA_ROW_NUMBER, CSV_HEADER_ROW_NUMBER } from "./csv-constants.js";
|
|
4
|
+
const normaliseLineEndings = (input)=>input.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
5
|
+
const quotedCsvLinePattern = /^\s*"(?:""|[^"])*"\s*(,\s*"(?:""|[^"])*"\s*)*$/;
|
|
6
|
+
const withError = (message, errorMap, errors, header = [], rows = [])=>({
|
|
7
|
+
error: {
|
|
8
|
+
errorMap,
|
|
9
|
+
errors,
|
|
10
|
+
message
|
|
11
|
+
},
|
|
12
|
+
header,
|
|
13
|
+
rows
|
|
14
|
+
});
|
|
15
|
+
const parseCsv = (csvText, options)=>{
|
|
16
|
+
const errorState = createCsvErrorState();
|
|
17
|
+
const text = normaliseLineEndings(csvText);
|
|
18
|
+
if (0 === text.trim().length) {
|
|
19
|
+
addCsvFileError(errorState, "*", CsvParseErrorEnum.EMPTY_FILE, "CSV file is empty.");
|
|
20
|
+
return withError("CSV file is empty.", errorState.errorMap, errorState.errors);
|
|
21
|
+
}
|
|
22
|
+
const allLines = text.split("\n");
|
|
23
|
+
const nonEmptyLines = allLines.filter((line)=>line.trim().length > 0);
|
|
24
|
+
if (0 === nonEmptyLines.length) {
|
|
25
|
+
addCsvFileError(errorState, "*", CsvParseErrorEnum.EMPTY_FILE, "CSV file is empty.");
|
|
26
|
+
return withError("CSV file is empty.", errorState.errorMap, errorState.errors);
|
|
27
|
+
}
|
|
28
|
+
const firstLine = nonEmptyLines[0];
|
|
29
|
+
if (firstLine.includes(";") && !firstLine.includes(",")) {
|
|
30
|
+
addCsvFileError(errorState, "*", CsvParseErrorEnum.INVALID_SEPARATOR, "CSV must use comma separators.", firstLine);
|
|
31
|
+
return withError("CSV must use comma separators.", errorState.errorMap, errorState.errors);
|
|
32
|
+
}
|
|
33
|
+
if (options?.requireQuotedValues === true) {
|
|
34
|
+
nonEmptyLines.forEach((line, lineIndex)=>{
|
|
35
|
+
if (!quotedCsvLinePattern.test(line)) if (0 === lineIndex) addCsvFileError(errorState, "*", CsvParseErrorEnum.UNQUOTED_VALUE, "CSV values must be enclosed in double quotes.", line);
|
|
36
|
+
else addCsvRowError(errorState, lineIndex + CSV_HEADER_ROW_NUMBER, "*", CsvParseErrorEnum.UNQUOTED_VALUE, "CSV values must be enclosed in double quotes.", line);
|
|
37
|
+
});
|
|
38
|
+
if (hasCsvErrors(errorState.errorMap)) return withError("CSV values must be enclosed in double quotes.", errorState.errorMap, errorState.errors);
|
|
39
|
+
}
|
|
40
|
+
let records;
|
|
41
|
+
try {
|
|
42
|
+
records = parse(text, {
|
|
43
|
+
bom: true,
|
|
44
|
+
delimiter: ",",
|
|
45
|
+
escape: '"',
|
|
46
|
+
quote: '"',
|
|
47
|
+
relax_column_count: true,
|
|
48
|
+
skip_empty_lines: true
|
|
49
|
+
});
|
|
50
|
+
} catch (err) {
|
|
51
|
+
addCsvFileError(errorState, "*", CsvParseErrorEnum.INVALID_FORMAT, err instanceof Error ? err.message : "Invalid CSV format.");
|
|
52
|
+
return withError("Invalid CSV format.", errorState.errorMap, errorState.errors);
|
|
53
|
+
}
|
|
54
|
+
if (0 === records.length) {
|
|
55
|
+
addCsvFileError(errorState, "*", CsvParseErrorEnum.EMPTY_FILE, "CSV file is empty.");
|
|
56
|
+
return withError("CSV file is empty.", errorState.errorMap, errorState.errors);
|
|
57
|
+
}
|
|
58
|
+
const header = records[0].map((value)=>value.trim());
|
|
59
|
+
if (0 === header.length) addCsvFileError(errorState, "*", CsvParseErrorEnum.EMPTY_HEADER_COLUMN, "CSV header contains empty column names.");
|
|
60
|
+
header.forEach((col, columnIndex)=>{
|
|
61
|
+
if (0 === col.length) addCsvFileError(errorState, `column_${columnIndex + 1}`, CsvParseErrorEnum.EMPTY_HEADER_COLUMN, "CSV header contains empty column names.");
|
|
62
|
+
});
|
|
63
|
+
if (hasCsvErrors(errorState.errorMap)) return withError("CSV header contains empty column names.", errorState.errorMap, errorState.errors);
|
|
64
|
+
const duplicateHeader = header.find((col, idx)=>header.indexOf(col) !== idx);
|
|
65
|
+
if (duplicateHeader) {
|
|
66
|
+
addCsvFileError(errorState, duplicateHeader, CsvParseErrorEnum.DUPLICATE_HEADER_COLUMN, `CSV header contains duplicate column '${duplicateHeader}'.`, duplicateHeader);
|
|
67
|
+
return withError(`CSV header contains duplicate column '${duplicateHeader}'.`, errorState.errorMap, errorState.errors, header, []);
|
|
68
|
+
}
|
|
69
|
+
const rows = records.slice(1);
|
|
70
|
+
rows.forEach((row, rowIndex)=>{
|
|
71
|
+
if (row.length !== header.length) addCsvRowError(errorState, rowIndex + CSV_FIRST_DATA_ROW_NUMBER, "*", CsvParseErrorEnum.ROW_COLUMN_COUNT_MISMATCH, "CSV rows must have the same number of columns as the header.", row.join(","));
|
|
72
|
+
});
|
|
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
|
+
return {
|
|
75
|
+
header,
|
|
76
|
+
rows
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export { CsvParseErrorEnum, parseCsv };
|