@teselagen/ui 0.7.33-beta.5 → 0.7.33
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/AdvancedOptions.js +33 -0
- package/AssignDefaultsModeContext.js +22 -0
- package/CellDragHandle.js +132 -0
- package/ColumnFilterMenu.js +62 -0
- package/Columns.js +979 -0
- package/DataTable/utils/queryParams.d.ts +18 -9
- package/DisabledLoadingComponent.js +15 -0
- package/DisplayOptions.js +199 -0
- package/DropdownButton.js +36 -0
- package/DropdownCell.js +61 -0
- package/EditableCell.js +44 -0
- package/FillWindow.css +6 -0
- package/FillWindow.js +69 -0
- package/FilterAndSortMenu.js +391 -0
- package/FormSeparator.js +9 -0
- package/LoadingDots.js +14 -0
- package/MatchHeaders.js +234 -0
- package/PagingTool.js +225 -0
- package/RenderCell.js +191 -0
- package/SearchBar.js +69 -0
- package/SimpleStepViz.js +22 -0
- package/SortableColumns.js +100 -0
- package/TableFormTrackerContext.js +10 -0
- package/Tag.js +112 -0
- package/ThComponent.js +44 -0
- package/TimelineEvent.js +31 -0
- package/UploadCsvWizard.css +4 -0
- package/UploadCsvWizard.js +719 -0
- package/Uploader.js +1278 -0
- package/adHoc.js +10 -0
- package/autoTooltip.js +201 -0
- package/basicHandleActionsWithFullState.js +14 -0
- package/browserUtils.js +3 -0
- package/combineReducersWithFullState.js +14 -0
- package/commandControls.js +82 -0
- package/commandUtils.js +112 -0
- package/constants.js +1 -0
- package/convertSchema.js +69 -0
- package/customIcons.js +361 -0
- package/dataTableEnhancer.js +41 -0
- package/defaultFormatters.js +32 -0
- package/defaultValidators.js +40 -0
- package/determineBlackOrWhiteTextColor.js +4 -0
- package/editCellHelper.js +44 -0
- package/formatPasteData.js +16 -0
- package/getAllRows.js +11 -0
- package/getCellCopyText.js +7 -0
- package/getCellInfo.js +36 -0
- package/getCellVal.js +20 -0
- package/getDayjsFormatter.js +35 -0
- package/getFieldPathToField.js +7 -0
- package/getIdOrCodeOrIndex.js +9 -0
- package/getLastSelectedEntity.js +11 -0
- package/getNewEntToSelect.js +25 -0
- package/getNewName.js +31 -0
- package/getRowCopyText.js +28 -0
- package/getTableConfigFromStorage.js +5 -0
- package/getTextFromEl.js +28 -0
- package/getVals.js +8 -0
- package/handleCopyColumn.js +21 -0
- package/handleCopyHelper.js +15 -0
- package/handleCopyRows.js +23 -0
- package/handleCopyTable.js +16 -0
- package/handlerHelpers.js +24 -0
- package/hotkeyUtils.js +131 -0
- package/index.cjs.js +970 -826
- package/index.d.ts +0 -1
- package/index.es.js +970 -826
- package/index.js +196 -0
- package/isBeingCalledExcessively.js +31 -0
- package/isBottomRightCornerOfRectangle.js +20 -0
- package/isEntityClean.js +15 -0
- package/isTruthy.js +12 -0
- package/isValueEmpty.js +3 -0
- package/itemUpload.js +84 -0
- package/menuUtils.js +433 -0
- package/package.json +1 -2
- package/popoverOverflowModifiers.js +11 -0
- package/primarySelectedValue.js +1 -0
- package/pureNoFunc.js +31 -0
- package/queryParams.js +1058 -0
- package/removeCleanRows.js +22 -0
- package/renderOnDoc.js +32 -0
- package/rerenderOnWindowResize.js +26 -0
- package/rowClick.js +181 -0
- package/selection.js +8 -0
- package/showAppSpinner.js +12 -0
- package/showDialogOnDocBody.js +33 -0
- package/showProgressToast.js +22 -0
- package/sortify.js +73 -0
- package/src/DataTable/index.js +1 -1
- package/src/DataTable/utils/filterLocalEntitiesToHasura.js +14 -0
- package/src/DataTable/utils/filterLocalEntitiesToHasura.test.js +49 -0
- package/src/DataTable/utils/queryParams.js +12 -9
- package/src/DataTable/utils/tableQueryParamsToHasuraClauses.js +146 -143
- package/style.css +29 -0
- package/tagUtils.js +45 -0
- package/tgFormValues.js +35 -0
- package/tg_modalState.js +47 -0
- package/throwFormError.js +16 -0
- package/toastr.js +148 -0
- package/tryToMatchSchemas.js +264 -0
- package/typeToCommonType.js +6 -0
- package/useDeepEqualMemo.js +15 -0
- package/useDialog.js +63 -0
- package/useStableReference.js +9 -0
- package/useTableEntities.js +38 -0
- package/useTraceUpdate.js +19 -0
- package/utils.js +37 -0
- package/validateTableWideErrors.js +160 -0
- package/viewColumn.js +97 -0
- package/withField.js +20 -0
- package/withFields.js +11 -0
- package/withLocalStorage.js +11 -0
- package/withSelectTableRecords.js +43 -0
- package/withSelectedEntities.js +65 -0
- package/withStore.js +10 -0
- package/withTableParams.js +301 -0
- package/wrapDialog.js +116 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { forEach, isArray, isPlainObject, map, snakeCase } from "lodash-es";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import Fuse from "fuse.js";
|
|
4
|
+
import { editCellHelper } from "../DataTable/editCellHelper";
|
|
5
|
+
import { validateTableWideErrors } from "../DataTable/validateTableWideErrors";
|
|
6
|
+
import { isEmpty } from "lodash-es";
|
|
7
|
+
import getTextFromEl from "../utils/getTextFromEl";
|
|
8
|
+
import { min } from "lodash-es";
|
|
9
|
+
import { camelCase } from "lodash-es";
|
|
10
|
+
import { startCase } from "lodash-es";
|
|
11
|
+
|
|
12
|
+
const getSchema = data => ({
|
|
13
|
+
fields: map(data[0], (val, path) => {
|
|
14
|
+
return { path, type: "string" };
|
|
15
|
+
}),
|
|
16
|
+
userData: data
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default async function tryToMatchSchemas({
|
|
20
|
+
incomingData,
|
|
21
|
+
validateAgainstSchema
|
|
22
|
+
}) {
|
|
23
|
+
await resolveValidateAgainstSchema(validateAgainstSchema);
|
|
24
|
+
const userSchema = getSchema(incomingData);
|
|
25
|
+
|
|
26
|
+
const { searchResults, csvValidationIssue, ignoredHeadersMsg } =
|
|
27
|
+
await matchSchemas({
|
|
28
|
+
userSchema,
|
|
29
|
+
officialSchema: validateAgainstSchema
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const incomingHeadersToScores = {};
|
|
33
|
+
|
|
34
|
+
searchResults.forEach(r => {
|
|
35
|
+
r.matches.forEach(match => {
|
|
36
|
+
incomingHeadersToScores[match.item.path] =
|
|
37
|
+
incomingHeadersToScores[match.item.path] || [];
|
|
38
|
+
incomingHeadersToScores[match.item.path].push(match.score);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
searchResults.forEach(r => {
|
|
43
|
+
for (const match of r.matches) {
|
|
44
|
+
if (!incomingHeadersToScores[match.item.path]) continue;
|
|
45
|
+
const minScore = min(incomingHeadersToScores[match.item.path]);
|
|
46
|
+
if (minScore === match.score) {
|
|
47
|
+
r.topMatch = match.item.path;
|
|
48
|
+
r.matches.forEach(match => {
|
|
49
|
+
if (!incomingHeadersToScores[match.item.path]) return;
|
|
50
|
+
const arr = incomingHeadersToScores[match.item.path];
|
|
51
|
+
arr.splice(arr.indexOf(match.score), 1);
|
|
52
|
+
});
|
|
53
|
+
delete incomingHeadersToScores[match.item.path];
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const matchedHeaders = {};
|
|
59
|
+
searchResults.forEach(r => {
|
|
60
|
+
if (r.topMatch) {
|
|
61
|
+
matchedHeaders[r.path] = r.topMatch;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
ignoredHeadersMsg,
|
|
67
|
+
csvValidationIssue,
|
|
68
|
+
matchedHeaders,
|
|
69
|
+
userSchema,
|
|
70
|
+
searchResults
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function matchSchemas({ userSchema, officialSchema }) {
|
|
75
|
+
const options = {
|
|
76
|
+
includeScore: true,
|
|
77
|
+
keys: ["path", "displayName"]
|
|
78
|
+
};
|
|
79
|
+
let csvValidationIssue = false;
|
|
80
|
+
const fuse = new Fuse(userSchema.fields, options);
|
|
81
|
+
|
|
82
|
+
const matchedAltPaths = [];
|
|
83
|
+
officialSchema.fields.forEach(h => {
|
|
84
|
+
let hasMatch = false;
|
|
85
|
+
//run fuse search for results
|
|
86
|
+
let result = fuse.search(h.path) || [];
|
|
87
|
+
|
|
88
|
+
const hadNormalPathMatch = userSchema.fields.some(
|
|
89
|
+
uh => norm(uh.path) === norm(h.path)
|
|
90
|
+
);
|
|
91
|
+
//if there are any exact matches, push them onto the results array
|
|
92
|
+
userSchema.fields.forEach((uh, i) => {
|
|
93
|
+
const pathMatch = norm(uh.path) === norm(h.path);
|
|
94
|
+
const displayNameMatch =
|
|
95
|
+
h.displayName && norm(uh.path) === norm(getTextFromEl(h.displayName));
|
|
96
|
+
const hasAlternatePathMatch =
|
|
97
|
+
!hadNormalPathMatch &&
|
|
98
|
+
h.alternatePathMatch &&
|
|
99
|
+
(isArray(h.alternatePathMatch)
|
|
100
|
+
? h.alternatePathMatch
|
|
101
|
+
: [h.alternatePathMatch]
|
|
102
|
+
).find(alternatePathMatch => {
|
|
103
|
+
let altPath = alternatePathMatch;
|
|
104
|
+
if (isPlainObject(alternatePathMatch)) {
|
|
105
|
+
altPath = alternatePathMatch.path;
|
|
106
|
+
}
|
|
107
|
+
return norm(uh.path) === norm(altPath);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (hasAlternatePathMatch) {
|
|
111
|
+
matchedAltPaths.push(
|
|
112
|
+
hasAlternatePathMatch.path || hasAlternatePathMatch
|
|
113
|
+
);
|
|
114
|
+
if (hasAlternatePathMatch.format) {
|
|
115
|
+
h.format = hasAlternatePathMatch.format;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (pathMatch || displayNameMatch || hasAlternatePathMatch) {
|
|
119
|
+
result = result.filter(({ path }) => path === uh.path);
|
|
120
|
+
//add a fake perfect match result to make sure we get the match
|
|
121
|
+
result.unshift({
|
|
122
|
+
item: {
|
|
123
|
+
path: uh.path,
|
|
124
|
+
type: h.type
|
|
125
|
+
},
|
|
126
|
+
refIndex: i,
|
|
127
|
+
score: 0
|
|
128
|
+
});
|
|
129
|
+
hasMatch = true;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
h.hasMatch = hasMatch;
|
|
133
|
+
h.matches = result;
|
|
134
|
+
|
|
135
|
+
if (!hasMatch && h.isRequired) {
|
|
136
|
+
csvValidationIssue =
|
|
137
|
+
"It looks like some of the headers in your uploaded file(s) do not match the expected headers. Please look over and correct any issues with the mappings below.";
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const ignoredUserSchemaFields = [];
|
|
141
|
+
userSchema.fields.forEach(uh => {
|
|
142
|
+
if (
|
|
143
|
+
!officialSchema.fields.find(
|
|
144
|
+
h =>
|
|
145
|
+
norm(h.path) === norm(uh.path) ||
|
|
146
|
+
norm(h.displayName) === norm(uh.path) ||
|
|
147
|
+
matchedAltPaths.includes(uh.path)
|
|
148
|
+
)
|
|
149
|
+
) {
|
|
150
|
+
// check that the column does contain data (if it doesn't, it's probably a blank column)
|
|
151
|
+
if (userSchema.userData.some(e => e[uh.path])) {
|
|
152
|
+
ignoredUserSchemaFields.push(uh);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (officialSchema.coerceUserSchema) {
|
|
158
|
+
officialSchema.coerceUserSchema({ userSchema, officialSchema });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
userSchema.userData = map(userSchema.userData, e => {
|
|
162
|
+
if (!e.id) {
|
|
163
|
+
return {
|
|
164
|
+
...e,
|
|
165
|
+
id: "__generated__" + nanoid()
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return e;
|
|
169
|
+
});
|
|
170
|
+
const editableFields = officialSchema.fields.filter(f => !f.isNotEditable);
|
|
171
|
+
let hasErr;
|
|
172
|
+
if (!csvValidationIssue) {
|
|
173
|
+
userSchema.userData.some(e => {
|
|
174
|
+
return editableFields.some(columnSchema => {
|
|
175
|
+
//mutative
|
|
176
|
+
const { error } = editCellHelper({
|
|
177
|
+
entity: e,
|
|
178
|
+
columnSchema,
|
|
179
|
+
newVal: columnSchema.hasMatch
|
|
180
|
+
? e[columnSchema.matches[0]?.item?.path]
|
|
181
|
+
: undefined
|
|
182
|
+
});
|
|
183
|
+
if (error) {
|
|
184
|
+
hasErr = `${
|
|
185
|
+
columnSchema.displayName || startCase(camelCase(columnSchema.path))
|
|
186
|
+
}: ${error}`;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false;
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
if (!hasErr) {
|
|
194
|
+
hasErr = Object.values(
|
|
195
|
+
validateTableWideErrors({
|
|
196
|
+
entities: userSchema.userData,
|
|
197
|
+
schema: officialSchema,
|
|
198
|
+
optionalUserSchema: userSchema,
|
|
199
|
+
newCellValidate: {}
|
|
200
|
+
})
|
|
201
|
+
)[0];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (officialSchema.tableWideAsyncValidation) {
|
|
206
|
+
//do the table wide validation
|
|
207
|
+
const res = await officialSchema.tableWideAsyncValidation({
|
|
208
|
+
entities: userSchema.userData
|
|
209
|
+
});
|
|
210
|
+
if (!isEmpty(res)) {
|
|
211
|
+
csvValidationIssue = addSpecialPropToAsyncErrs(res);
|
|
212
|
+
}
|
|
213
|
+
//return errors on the tables
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (hasErr && !csvValidationIssue) {
|
|
217
|
+
if (hasErr.message) {
|
|
218
|
+
csvValidationIssue = hasErr;
|
|
219
|
+
} else {
|
|
220
|
+
csvValidationIssue = {
|
|
221
|
+
message: hasErr
|
|
222
|
+
};
|
|
223
|
+
// csvValidationIssue = ` Some of the data doesn't look quite right. Do these header mappings look correct?`;
|
|
224
|
+
}
|
|
225
|
+
// csvValidationIssue = `Some of the data doesn't look quite right. Do these header mappings look correct?`;
|
|
226
|
+
}
|
|
227
|
+
let ignoredHeadersMsg;
|
|
228
|
+
if (ignoredUserSchemaFields.length) {
|
|
229
|
+
ignoredHeadersMsg = `It looks like the following headers in your file didn't map to any of the accepted headers: ${ignoredUserSchemaFields
|
|
230
|
+
.map(f => f.displayName || f.path)
|
|
231
|
+
.join(", ")}`;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
searchResults: officialSchema.fields,
|
|
235
|
+
csvValidationIssue,
|
|
236
|
+
ignoredHeadersMsg
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const addSpecialPropToAsyncErrs = res => {
|
|
241
|
+
forEach(res, (v, k) => {
|
|
242
|
+
res[k] = {
|
|
243
|
+
message: v,
|
|
244
|
+
_isTableAsyncWideError: true
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
return res;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
async function resolveValidateAgainstSchema() {
|
|
251
|
+
//tnw: wip!
|
|
252
|
+
// mapSeries(validateAgainstSchema.fields, async f => {
|
|
253
|
+
// if (f.type === "dropdown") {
|
|
254
|
+
// console.log(`type:`, f.type)
|
|
255
|
+
// if (f.getValues) {
|
|
256
|
+
// f.values = await f.getValues(props);
|
|
257
|
+
// }
|
|
258
|
+
// }
|
|
259
|
+
// })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function norm(h) {
|
|
263
|
+
return snakeCase(h).toLowerCase().replace(/ /g, "");
|
|
264
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isEqual } from "lodash-es";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
export const useDeepEqualMemo = value => {
|
|
5
|
+
const ref = useRef();
|
|
6
|
+
if (!isEqual(value, ref.current)) {
|
|
7
|
+
ref.current = value;
|
|
8
|
+
}
|
|
9
|
+
return ref.current;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const useDeepEqualEffect = (effect, deps) => {
|
|
13
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
+
return useEffect(effect, useDeepEqualMemo(deps));
|
|
15
|
+
};
|
package/useDialog.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
|
|
5
|
+
const {toggleDialog, comp} = useDialog({
|
|
6
|
+
ModalComponent: SimpleInsertData,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return <div>
|
|
10
|
+
{comp} //stick the returned dialog comp somewhere in the dom! (it should not effect layout)
|
|
11
|
+
{...your code here}
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
export const useDialog = ({ ModalComponent }) => {
|
|
16
|
+
const [isOpen, setOpen] = useState(false);
|
|
17
|
+
const [additionalProps, setAdditionalProps] = useState(false);
|
|
18
|
+
const Comp = useCallback(
|
|
19
|
+
() => (
|
|
20
|
+
<ModalComponent
|
|
21
|
+
hideModal={() => {
|
|
22
|
+
setOpen(false);
|
|
23
|
+
}}
|
|
24
|
+
hideDialog={() => {
|
|
25
|
+
setOpen(false);
|
|
26
|
+
}}
|
|
27
|
+
{...additionalProps}
|
|
28
|
+
dialogProps={{
|
|
29
|
+
isOpen,
|
|
30
|
+
...additionalProps?.dialogProps
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
[ModalComponent, additionalProps, isOpen]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const showDialogPromise = useCallback(async (handlerName, moreProps = {}) => {
|
|
38
|
+
return new Promise(resolve => {
|
|
39
|
+
//return a promise that can be awaited
|
|
40
|
+
setAdditionalProps({
|
|
41
|
+
hideModal: () => {
|
|
42
|
+
//override hideModal to resolve also
|
|
43
|
+
setOpen(false);
|
|
44
|
+
resolve({});
|
|
45
|
+
},
|
|
46
|
+
hideDialog: () => {
|
|
47
|
+
//override hideModal to resolve also
|
|
48
|
+
setOpen(false);
|
|
49
|
+
resolve({});
|
|
50
|
+
},
|
|
51
|
+
[handlerName]: r => {
|
|
52
|
+
setOpen(false);
|
|
53
|
+
resolve(r || {});
|
|
54
|
+
},
|
|
55
|
+
//pass any additional props to the dialog
|
|
56
|
+
...moreProps
|
|
57
|
+
});
|
|
58
|
+
setOpen(true); //open the dialog
|
|
59
|
+
});
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
return { Comp, showDialogPromise };
|
|
63
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useDispatch, useSelector } from "react-redux";
|
|
3
|
+
import { change, initialize } from "redux-form";
|
|
4
|
+
|
|
5
|
+
export const useTableEntities = tableFormName => {
|
|
6
|
+
const dispatch = useDispatch();
|
|
7
|
+
const selectTableEntities = useCallback(
|
|
8
|
+
(entities = []) => {
|
|
9
|
+
initialize(tableFormName, {}, true, {
|
|
10
|
+
keepDirty: true,
|
|
11
|
+
updateUnregisteredFields: true,
|
|
12
|
+
keepValues: true
|
|
13
|
+
});
|
|
14
|
+
const selectedEntityIdMap = {};
|
|
15
|
+
entities.forEach(entity => {
|
|
16
|
+
selectedEntityIdMap[entity.id] = {
|
|
17
|
+
entity,
|
|
18
|
+
time: Date.now()
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
dispatch(
|
|
22
|
+
change(
|
|
23
|
+
tableFormName,
|
|
24
|
+
"reduxFormSelectedEntityIdMap",
|
|
25
|
+
selectedEntityIdMap
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
[dispatch, tableFormName]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const { allOrderedEntities, selectedEntities } = useSelector(state => ({
|
|
33
|
+
allOrderedEntities: state.form?.[tableFormName]?.values?.allOrderedEntities,
|
|
34
|
+
selectedEntities:
|
|
35
|
+
state.form?.[tableFormName]?.values?.reduxFormSelectedEntityIdMap
|
|
36
|
+
}));
|
|
37
|
+
return { selectTableEntities, allOrderedEntities, selectedEntities };
|
|
38
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
// useful for debugging rerender errors
|
|
4
|
+
export const useTraceUpdate = props => {
|
|
5
|
+
const prev = useRef(props);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
|
|
8
|
+
if (prev.current[k] !== v) {
|
|
9
|
+
ps[k] = [prev.current[k], v];
|
|
10
|
+
}
|
|
11
|
+
return ps;
|
|
12
|
+
}, {});
|
|
13
|
+
if (Object.keys(changedProps).length > 0) {
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.log("Changed props:", changedProps);
|
|
16
|
+
}
|
|
17
|
+
prev.current = props;
|
|
18
|
+
});
|
|
19
|
+
};
|
package/utils.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getIdOrCodeOrIndex } from "./getIdOrCodeOrIndex";
|
|
2
|
+
|
|
3
|
+
export const getFieldPathToIndex = schema => {
|
|
4
|
+
const fieldToIndex = {};
|
|
5
|
+
schema.fields.forEach((f, i) => {
|
|
6
|
+
fieldToIndex[f.path] = i;
|
|
7
|
+
});
|
|
8
|
+
return fieldToIndex;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const defaultParsePaste = str => {
|
|
12
|
+
return str.split(/\r\n|\n|\r/).map(row => row.split("\t"));
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getEntityIdToEntity = entities => {
|
|
16
|
+
const entityIdToEntity = {};
|
|
17
|
+
entities.forEach((e, i) => {
|
|
18
|
+
entityIdToEntity[getIdOrCodeOrIndex(e, i)] = { e, i };
|
|
19
|
+
});
|
|
20
|
+
return entityIdToEntity;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const endsWithNumber = str => {
|
|
24
|
+
return /[0-9]+$/.test(str);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getNumberStrAtEnd = str => {
|
|
28
|
+
if (endsWithNumber(str)) {
|
|
29
|
+
return str.match(/[0-9]+$/)[0];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const stripNumberAtEnd = str => {
|
|
36
|
+
return str?.replace?.(getNumberStrAtEnd(str), "");
|
|
37
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { getIdOrCodeOrIndex } from "./utils";
|
|
2
|
+
import { getCellVal } from "./getCellVal";
|
|
3
|
+
import { forEach, isArray } from "lodash-es";
|
|
4
|
+
import { startCase } from "lodash-es";
|
|
5
|
+
import { camelCase } from "lodash-es";
|
|
6
|
+
|
|
7
|
+
const uniqueMsg = "This value must be unique";
|
|
8
|
+
export function validateTableWideErrors({
|
|
9
|
+
entities,
|
|
10
|
+
schema,
|
|
11
|
+
optionalUserSchema,
|
|
12
|
+
newCellValidate
|
|
13
|
+
}) {
|
|
14
|
+
forEach(newCellValidate, (err, cellId) => {
|
|
15
|
+
if (err && err._isTableWideError) {
|
|
16
|
+
delete newCellValidate[cellId];
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
if (schema.tableWideValidation) {
|
|
20
|
+
const newErrs = schema.tableWideValidation({
|
|
21
|
+
entities
|
|
22
|
+
});
|
|
23
|
+
forEach(newErrs, (err, cellId) => {
|
|
24
|
+
newCellValidate[cellId] = {
|
|
25
|
+
message: err,
|
|
26
|
+
_isTableWideError: true
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const displayNameMap = {};
|
|
31
|
+
const fieldUpperToPath = {};
|
|
32
|
+
|
|
33
|
+
forEach(schema.fields, f => {
|
|
34
|
+
fieldUpperToPath[f.path.toUpperCase()] = f.path;
|
|
35
|
+
displayNameMap[f.path] = f.displayName || startCase(camelCase(f.path));
|
|
36
|
+
});
|
|
37
|
+
function getDisplayName(path) {
|
|
38
|
+
return displayNameMap[path] || startCase(camelCase(path));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (schema.requireAtLeastOneOf) {
|
|
42
|
+
//make sure at least one of the required fields is present
|
|
43
|
+
(isArray(schema.requireAtLeastOneOf[0])
|
|
44
|
+
? schema.requireAtLeastOneOf
|
|
45
|
+
: [schema.requireAtLeastOneOf]
|
|
46
|
+
).forEach(alo => {
|
|
47
|
+
entities.forEach(entity => {
|
|
48
|
+
if (!alo.some(path => getCellVal(entity, path))) {
|
|
49
|
+
alo.forEach(path => {
|
|
50
|
+
const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`;
|
|
51
|
+
newCellValidate[cellId] = {
|
|
52
|
+
message: `At least one of these fields must be present - ${alo
|
|
53
|
+
.map(getDisplayName)
|
|
54
|
+
.join(", ")}`,
|
|
55
|
+
_isTableWideError: true
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (schema.requireExactlyOneOf) {
|
|
63
|
+
(isArray(schema.requireExactlyOneOf[0])
|
|
64
|
+
? schema.requireExactlyOneOf
|
|
65
|
+
: [schema.requireExactlyOneOf]
|
|
66
|
+
).forEach(reqs => {
|
|
67
|
+
entities.forEach(entity => {
|
|
68
|
+
const present = reqs.filter(path => getCellVal(entity, path));
|
|
69
|
+
if (present.length !== 1) {
|
|
70
|
+
reqs.forEach(path => {
|
|
71
|
+
const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`;
|
|
72
|
+
const fields = reqs.map(getDisplayName).join(", ");
|
|
73
|
+
const err = {
|
|
74
|
+
message:
|
|
75
|
+
present.length === 0
|
|
76
|
+
? `One of these fields is required - ${fields}`
|
|
77
|
+
: `Cannot have more than one of these fields - ${fields}`,
|
|
78
|
+
_isTableWideError: true
|
|
79
|
+
};
|
|
80
|
+
newCellValidate[cellId] = err;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const requireIfs = [];
|
|
88
|
+
schema.fields.forEach(col => {
|
|
89
|
+
const { path, requireIf } = col;
|
|
90
|
+
if (requireIf) {
|
|
91
|
+
const requireIfPath = fieldUpperToPath[requireIf.toUpperCase()];
|
|
92
|
+
requireIfs.push([requireIfPath, path]);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
requireIfs.forEach(([requireIfPath, path]) => {
|
|
96
|
+
entities.forEach(entity => {
|
|
97
|
+
const requireIfVal = getCellVal(entity, requireIfPath);
|
|
98
|
+
const pathVal = getCellVal(entity, path);
|
|
99
|
+
if (requireIfVal && !pathVal) {
|
|
100
|
+
const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`;
|
|
101
|
+
newCellValidate[cellId] = {
|
|
102
|
+
message: `This field is required if ${displayNameMap[requireIfPath]} is present`,
|
|
103
|
+
_isTableWideError: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
if (schema.requireAllOrNone) {
|
|
109
|
+
(isArray(schema.requireAllOrNone[0])
|
|
110
|
+
? schema.requireAllOrNone
|
|
111
|
+
: [schema.requireAllOrNone]
|
|
112
|
+
).forEach(reqs => {
|
|
113
|
+
entities.forEach(entity => {
|
|
114
|
+
const present = reqs.filter(path => getCellVal(entity, path));
|
|
115
|
+
if (present.length && present.length !== reqs.length) {
|
|
116
|
+
reqs.forEach(path => {
|
|
117
|
+
const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`;
|
|
118
|
+
const fields = reqs.map(getDisplayName).join(", ");
|
|
119
|
+
const err = {
|
|
120
|
+
message: `Please specify either ALL of the following fields or NONE of them - ${fields}`,
|
|
121
|
+
_isTableWideError: true
|
|
122
|
+
};
|
|
123
|
+
newCellValidate[cellId] = err;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
schema.fields.forEach(col => {
|
|
131
|
+
let { path, isUnique } = col;
|
|
132
|
+
if (isUnique) {
|
|
133
|
+
if (optionalUserSchema) {
|
|
134
|
+
path = col.matches[0].item.path;
|
|
135
|
+
}
|
|
136
|
+
const existingVals = {};
|
|
137
|
+
entities.forEach(entity => {
|
|
138
|
+
const val = getCellVal(entity, path, col);
|
|
139
|
+
if (!val) return;
|
|
140
|
+
const cellId = `${getIdOrCodeOrIndex(entity)}:${path}`;
|
|
141
|
+
if (existingVals[val]) {
|
|
142
|
+
newCellValidate[cellId] = {
|
|
143
|
+
message: uniqueMsg,
|
|
144
|
+
_isTableWideError: true
|
|
145
|
+
};
|
|
146
|
+
newCellValidate[existingVals[val]] = {
|
|
147
|
+
message: uniqueMsg,
|
|
148
|
+
_isTableWideError: true
|
|
149
|
+
};
|
|
150
|
+
} else {
|
|
151
|
+
if (newCellValidate[cellId] === uniqueMsg) {
|
|
152
|
+
delete newCellValidate[cellId];
|
|
153
|
+
}
|
|
154
|
+
existingVals[val] = cellId;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return newCellValidate;
|
|
160
|
+
}
|