@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
package/Uploader.js
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
Callout,
|
|
11
|
+
Classes,
|
|
12
|
+
Colors,
|
|
13
|
+
Icon,
|
|
14
|
+
Menu,
|
|
15
|
+
MenuItem,
|
|
16
|
+
Popover,
|
|
17
|
+
Position,
|
|
18
|
+
Tooltip
|
|
19
|
+
} from "@blueprintjs/core";
|
|
20
|
+
import Dropzone from "react-dropzone";
|
|
21
|
+
import classnames from "classnames";
|
|
22
|
+
import { nanoid } from "nanoid";
|
|
23
|
+
import papaparse, { unparse } from "papaparse";
|
|
24
|
+
import downloadjs from "downloadjs";
|
|
25
|
+
import UploadCsvWizardDialog, {
|
|
26
|
+
SimpleInsertDataDialog
|
|
27
|
+
} from "../UploadCsvWizard";
|
|
28
|
+
import { useDialog } from "../useDialog";
|
|
29
|
+
import {
|
|
30
|
+
filterFilesInZip,
|
|
31
|
+
isCsvOrExcelFile,
|
|
32
|
+
isZipFile,
|
|
33
|
+
parseCsvOrExcelFile,
|
|
34
|
+
removeExt
|
|
35
|
+
} from "@teselagen/file-utils";
|
|
36
|
+
import tryToMatchSchemas from "./tryToMatchSchemas";
|
|
37
|
+
import { isArray, isFunction, isPlainObject, noop } from "lodash-es";
|
|
38
|
+
import { flatMap } from "lodash-es";
|
|
39
|
+
import urljoin from "url-join";
|
|
40
|
+
import popoverOverflowModifiers from "../utils/popoverOverflowModifiers";
|
|
41
|
+
import writeXlsxFile from "write-excel-file";
|
|
42
|
+
import { startCase } from "lodash-es";
|
|
43
|
+
import { getNewName } from "./getNewName";
|
|
44
|
+
import { isObject } from "lodash-es";
|
|
45
|
+
import { change, touch, initialize } from "redux-form";
|
|
46
|
+
import classNames from "classnames";
|
|
47
|
+
import convertSchema from "../DataTable/utils/convertSchema";
|
|
48
|
+
import { LoadingDots } from "./LoadingDots";
|
|
49
|
+
import { useDispatch } from "react-redux";
|
|
50
|
+
import { flushSync } from "react-dom";
|
|
51
|
+
import { useStableReference } from "../utils/hooks/useStableReference";
|
|
52
|
+
|
|
53
|
+
const manualEnterMessage = "Build CSV File";
|
|
54
|
+
const manualEnterSubMessage = "Paste or type data to build a CSV file";
|
|
55
|
+
|
|
56
|
+
const helperText = [
|
|
57
|
+
`How to Use This Template to Upload New Data`,
|
|
58
|
+
`1. Go to the first tab and delete the example data.`,
|
|
59
|
+
`2. Input your rows of data organized under the appropriate columns. If you're confused about a column name, go to the "Column Info" tab for clarification.`,
|
|
60
|
+
`3. Save the file.`,
|
|
61
|
+
`4. Return to the interface from which you dowloaded this template.`,
|
|
62
|
+
`5. Upload the completed file.`
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const helperSchema = [
|
|
66
|
+
{
|
|
67
|
+
column: undefined,
|
|
68
|
+
type: String,
|
|
69
|
+
value: student => student,
|
|
70
|
+
width: 200
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const setValidateAgainstSchema = newValidateAgainstSchema => {
|
|
75
|
+
if (!newValidateAgainstSchema) return;
|
|
76
|
+
const schema = convertSchema(newValidateAgainstSchema);
|
|
77
|
+
if (
|
|
78
|
+
schema.fields.some(f => {
|
|
79
|
+
if (f.path === "id") {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
})
|
|
84
|
+
) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Uploader was passed a validateAgainstSchema with a fields array that contains a field with a path of "id". This is not allowed.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return schema;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getFileDownloadAttr = exampleFile => {
|
|
93
|
+
const baseUrl = window?.frontEndConfig?.serverBasePath || "";
|
|
94
|
+
return isFunction(exampleFile)
|
|
95
|
+
? { onClick: exampleFile }
|
|
96
|
+
: exampleFile && {
|
|
97
|
+
target: "_blank",
|
|
98
|
+
download: true,
|
|
99
|
+
href:
|
|
100
|
+
exampleFile.startsWith("https") || exampleFile.startsWith("www")
|
|
101
|
+
? exampleFile
|
|
102
|
+
: baseUrl
|
|
103
|
+
? urljoin(baseUrl, "exampleFiles", exampleFile)
|
|
104
|
+
: exampleFile
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const stripId = (ents = []) =>
|
|
109
|
+
ents.map(ent => {
|
|
110
|
+
const { id, ...rest } = ent;
|
|
111
|
+
return rest;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const getNewCsvFile = (ents, fileName) => {
|
|
115
|
+
const strippedEnts = stripId(ents);
|
|
116
|
+
return {
|
|
117
|
+
newFile: new File([papaparse.unparse(strippedEnts)], fileName),
|
|
118
|
+
cleanedEntities: strippedEnts
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const trimFiles = (incomingFiles, fileLimit) => {
|
|
123
|
+
if (fileLimit) {
|
|
124
|
+
if (incomingFiles.length > fileLimit) {
|
|
125
|
+
window.toastr &&
|
|
126
|
+
window.toastr.warning(
|
|
127
|
+
`Detected additional files in your upload that we are ignoring. You can only upload ${fileLimit} file${
|
|
128
|
+
fileLimit > 1 ? "s" : ""
|
|
129
|
+
} at a time.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return incomingFiles.slice(0, fileLimit);
|
|
133
|
+
}
|
|
134
|
+
return incomingFiles;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const InnerDropZone = ({
|
|
138
|
+
getRootProps,
|
|
139
|
+
getInputProps,
|
|
140
|
+
isDragAccept,
|
|
141
|
+
isDragReject,
|
|
142
|
+
isDragActive,
|
|
143
|
+
className,
|
|
144
|
+
minimal,
|
|
145
|
+
dropzoneDisabled,
|
|
146
|
+
contentOverride,
|
|
147
|
+
simpleAccept,
|
|
148
|
+
innerIcon,
|
|
149
|
+
innerText,
|
|
150
|
+
validateAgainstSchema,
|
|
151
|
+
handleManuallyEnterData,
|
|
152
|
+
noBuildCsvOption,
|
|
153
|
+
showFilesCount,
|
|
154
|
+
fileList
|
|
155
|
+
// isDragActive
|
|
156
|
+
// isDragReject
|
|
157
|
+
// isDragAccept
|
|
158
|
+
}) => (
|
|
159
|
+
<section>
|
|
160
|
+
<div
|
|
161
|
+
{...getRootProps()}
|
|
162
|
+
className={classnames("tg-dropzone", className, {
|
|
163
|
+
"tg-dropzone-minimal": minimal,
|
|
164
|
+
"tg-dropzone-active": isDragActive,
|
|
165
|
+
"tg-dropzone-reject": isDragReject, // tnr: the acceptClassName/rejectClassName doesn't work with file extensions (only mimetypes are supported when dragging). Thus we'll just always turn the drop area blue when dragging and let the filtering occur on drop. See https://github.com/react-dropzone/react-dropzone/issues/888#issuecomment-773938074
|
|
166
|
+
"tg-dropzone-accept": isDragAccept,
|
|
167
|
+
"tg-dropzone-disabled": dropzoneDisabled,
|
|
168
|
+
"bp3-disabled": dropzoneDisabled
|
|
169
|
+
})}
|
|
170
|
+
>
|
|
171
|
+
<input {...getInputProps()} />
|
|
172
|
+
{contentOverride || (
|
|
173
|
+
<div
|
|
174
|
+
title={
|
|
175
|
+
simpleAccept
|
|
176
|
+
? "Accepts only the following file types: " + simpleAccept
|
|
177
|
+
: "Accepts any file input"
|
|
178
|
+
}
|
|
179
|
+
className="tg-upload-inner"
|
|
180
|
+
>
|
|
181
|
+
{innerIcon || <Icon icon="upload" iconSize={minimal ? 15 : 30} />}
|
|
182
|
+
{innerText || (minimal ? "Upload" : "Click or drag to upload")}
|
|
183
|
+
{validateAgainstSchema && !noBuildCsvOption && (
|
|
184
|
+
<div
|
|
185
|
+
style={{
|
|
186
|
+
textAlign: "center",
|
|
187
|
+
// fontSize: 18,
|
|
188
|
+
marginTop: 7,
|
|
189
|
+
marginBottom: 5
|
|
190
|
+
}}
|
|
191
|
+
onClick={handleManuallyEnterData}
|
|
192
|
+
className="link-button"
|
|
193
|
+
>
|
|
194
|
+
...or {manualEnterMessage}
|
|
195
|
+
{/* <div
|
|
196
|
+
style={{
|
|
197
|
+
fontSize: 11,
|
|
198
|
+
color: Colors.GRAY3,
|
|
199
|
+
fontStyle: "italic"
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{manualEnterSubMessage}
|
|
203
|
+
</div> */}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{showFilesCount ? (
|
|
211
|
+
<div className="tg-upload-file-list-counter">
|
|
212
|
+
Files: {fileList ? fileList.length : 0}
|
|
213
|
+
</div>
|
|
214
|
+
) : null}
|
|
215
|
+
</section>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const onFileSuccessDefault = async () => {
|
|
219
|
+
return;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const Uploader = ({
|
|
223
|
+
accept: __accept,
|
|
224
|
+
action,
|
|
225
|
+
autoUnzip,
|
|
226
|
+
beforeUpload,
|
|
227
|
+
callout: _callout,
|
|
228
|
+
className = "",
|
|
229
|
+
contentOverride: maybeContentOverride,
|
|
230
|
+
disabled,
|
|
231
|
+
dropzoneProps = {},
|
|
232
|
+
fileLimit,
|
|
233
|
+
fileList, //list of files with options: {name, loading, error, url, originalName, downloadName}
|
|
234
|
+
innerIcon,
|
|
235
|
+
innerText,
|
|
236
|
+
meta: { form: formName } = {},
|
|
237
|
+
minimal,
|
|
238
|
+
name,
|
|
239
|
+
noBuildCsvOption,
|
|
240
|
+
noRedux = true,
|
|
241
|
+
onChange: _onChange = noop, //this is almost always getting passed by redux-form, no need to pass this handler manually
|
|
242
|
+
onFieldSubmit = noop, //called when all files have successfully uploaded
|
|
243
|
+
onFileClick, // called when a file link in the filelist is clicked
|
|
244
|
+
onFileSuccess = onFileSuccessDefault, //called each time a file is finished and before the file.loading gets set to false, needs to return a promise!
|
|
245
|
+
onPreviewClick,
|
|
246
|
+
onRemove = noop, //called when a file has been selected to be removed
|
|
247
|
+
overflowList,
|
|
248
|
+
readBeforeUpload, //read the file using the browser's FileReader before passing it to onChange and/or uploading it
|
|
249
|
+
showFilesCount,
|
|
250
|
+
showUploadList = true,
|
|
251
|
+
threeDotMenuItems,
|
|
252
|
+
validateAgainstSchema: _validateAgainstSchema
|
|
253
|
+
}) => {
|
|
254
|
+
const dispatch = useDispatch();
|
|
255
|
+
const [acceptLoading, setAcceptLoading] = useState(true); // set to true by default to prevent the dropzone from being actionable until the accept is resolved
|
|
256
|
+
const [resolvedAccept, setResolvedAccept] = useState();
|
|
257
|
+
const [loading, setLoading] = useState(false);
|
|
258
|
+
const filesToClean = useRef([]);
|
|
259
|
+
|
|
260
|
+
// We do this because we don't want functions influencing on the dependencies
|
|
261
|
+
const stableOnChange = useStableReference(_onChange);
|
|
262
|
+
const stableBeforeUpload = useStableReference(beforeUpload);
|
|
263
|
+
// onChange received from redux-form is not working anymore,
|
|
264
|
+
// so we need to overwrite it for redux to works.
|
|
265
|
+
const onChange = useCallback(
|
|
266
|
+
val => {
|
|
267
|
+
flushSync(() => {
|
|
268
|
+
if (noRedux) {
|
|
269
|
+
return stableOnChange.current(val);
|
|
270
|
+
}
|
|
271
|
+
dispatch(touch(formName, name));
|
|
272
|
+
dispatch(change(formName, name, val));
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
[dispatch, formName, name, noRedux, stableOnChange]
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const handleSecondHalfOfUpload = useCallback(
|
|
279
|
+
async ({ acceptedFiles, cleanedFileList }) => {
|
|
280
|
+
// This onChange is not changing things, we need to check whether the error is here or later
|
|
281
|
+
onChange(cleanedFileList); //tnw: this line is necessary, if you want to clear the file list in the beforeUpload, call onChange([])
|
|
282
|
+
// beforeUpload is called, otherwise beforeUpload will not be able to truly cancel the upload
|
|
283
|
+
const keepGoing = stableBeforeUpload.current
|
|
284
|
+
? await stableBeforeUpload.current(cleanedFileList, onChange)
|
|
285
|
+
: true;
|
|
286
|
+
if (!keepGoing) return;
|
|
287
|
+
|
|
288
|
+
if (action) {
|
|
289
|
+
const responses = [];
|
|
290
|
+
await Promise.all(
|
|
291
|
+
acceptedFiles.map(async fileToUpload => {
|
|
292
|
+
const data = new FormData();
|
|
293
|
+
data.append("file", fileToUpload);
|
|
294
|
+
try {
|
|
295
|
+
const res = await (window.serverApi
|
|
296
|
+
? window.serverApi.post(action, data)
|
|
297
|
+
: fetch(action, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: data
|
|
300
|
+
}));
|
|
301
|
+
responses.push(res.data && res.data[0]);
|
|
302
|
+
onFileSuccess(res.data[0]).then(() => {
|
|
303
|
+
cleanedFileList = cleanedFileList.map(file => {
|
|
304
|
+
const fileToReturn = {
|
|
305
|
+
...file,
|
|
306
|
+
...res.data[0]
|
|
307
|
+
};
|
|
308
|
+
if (fileToReturn.id === fileToUpload.id) {
|
|
309
|
+
fileToReturn.loading = false;
|
|
310
|
+
}
|
|
311
|
+
return fileToReturn;
|
|
312
|
+
});
|
|
313
|
+
onChange(cleanedFileList);
|
|
314
|
+
});
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error("Error uploading file:", err);
|
|
317
|
+
responses.push({
|
|
318
|
+
...fileToUpload,
|
|
319
|
+
error: err && err.msg ? err.msg : err
|
|
320
|
+
});
|
|
321
|
+
cleanedFileList = cleanedFileList.map(file => {
|
|
322
|
+
const fileToReturn = { ...file };
|
|
323
|
+
if (fileToReturn.id === fileToUpload.id) {
|
|
324
|
+
fileToReturn.loading = false;
|
|
325
|
+
fileToReturn.error = true;
|
|
326
|
+
}
|
|
327
|
+
return fileToReturn;
|
|
328
|
+
});
|
|
329
|
+
onChange(cleanedFileList);
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
onFieldSubmit(responses);
|
|
334
|
+
} else {
|
|
335
|
+
onChange(
|
|
336
|
+
cleanedFileList.map(function (file) {
|
|
337
|
+
return {
|
|
338
|
+
...file,
|
|
339
|
+
loading: false
|
|
340
|
+
};
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
setLoading(false);
|
|
345
|
+
},
|
|
346
|
+
[action, stableBeforeUpload, onChange, onFieldSubmit, onFileSuccess]
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const isAcceptPromise = useMemo(
|
|
350
|
+
() =>
|
|
351
|
+
__accept?.then ||
|
|
352
|
+
(Array.isArray(__accept) ? __accept.some(acc => acc?.then) : false),
|
|
353
|
+
[__accept]
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const _accept = useMemo(() => {
|
|
357
|
+
if (resolvedAccept) {
|
|
358
|
+
return resolvedAccept;
|
|
359
|
+
}
|
|
360
|
+
if (isAcceptPromise && !resolvedAccept) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
return __accept;
|
|
364
|
+
}, [__accept, isAcceptPromise, resolvedAccept]);
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
if (isAcceptPromise) {
|
|
367
|
+
setAcceptLoading(true);
|
|
368
|
+
Promise.allSettled(Array.isArray(__accept) ? __accept : [__accept]).then(
|
|
369
|
+
results => {
|
|
370
|
+
const resolved = flatMap(results, r => r.value);
|
|
371
|
+
setResolvedAccept(resolved);
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
// give the resolved accept a full JS cycle to update before allowing actionability on the dropzone
|
|
374
|
+
setAcceptLoading(false);
|
|
375
|
+
}, 0);
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
// set this to false since it is defaulted to true
|
|
380
|
+
setAcceptLoading(false);
|
|
381
|
+
}
|
|
382
|
+
}, [__accept, isAcceptPromise]);
|
|
383
|
+
|
|
384
|
+
let dropzoneDisabled = disabled;
|
|
385
|
+
if (acceptLoading) dropzoneDisabled = true;
|
|
386
|
+
|
|
387
|
+
const accept = useMemo(
|
|
388
|
+
() =>
|
|
389
|
+
!_accept
|
|
390
|
+
? undefined
|
|
391
|
+
: isAcceptPromise && !resolvedAccept
|
|
392
|
+
? []
|
|
393
|
+
: isPlainObject(_accept)
|
|
394
|
+
? [_accept]
|
|
395
|
+
: isArray(_accept)
|
|
396
|
+
? _accept
|
|
397
|
+
: _accept.split(",").map(acc => ({ type: acc })),
|
|
398
|
+
[_accept, isAcceptPromise, resolvedAccept]
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const callout = _callout || accept?.find?.(a => a?.callout)?.callout;
|
|
402
|
+
|
|
403
|
+
const validateAgainstSchema = useMemo(
|
|
404
|
+
() =>
|
|
405
|
+
setValidateAgainstSchema(
|
|
406
|
+
_validateAgainstSchema ||
|
|
407
|
+
accept?.find?.(a => a?.validateAgainstSchema)?.validateAgainstSchema
|
|
408
|
+
),
|
|
409
|
+
[_validateAgainstSchema, accept]
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (
|
|
413
|
+
(validateAgainstSchema || autoUnzip) &&
|
|
414
|
+
accept &&
|
|
415
|
+
!accept.some(a => a.type === "zip")
|
|
416
|
+
) {
|
|
417
|
+
accept?.unshift({
|
|
418
|
+
type: "zip",
|
|
419
|
+
description: "Any of the following types, just compressed"
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const { showDialogPromise: showUploadCsvWizardDialog, Comp } = useDialog({
|
|
424
|
+
ModalComponent: UploadCsvWizardDialog
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const { showDialogPromise: showSimpleInsertDataDialog, Comp: Comp2 } =
|
|
428
|
+
useDialog({
|
|
429
|
+
ModalComponent: SimpleInsertDataDialog
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
function cleanupFiles() {
|
|
433
|
+
filesToClean.current.forEach(file => URL.revokeObjectURL(file.preview));
|
|
434
|
+
}
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
return () => {
|
|
437
|
+
cleanupFiles();
|
|
438
|
+
};
|
|
439
|
+
}, []);
|
|
440
|
+
|
|
441
|
+
let contentOverride = maybeContentOverride;
|
|
442
|
+
if (contentOverride && typeof contentOverride === "function") {
|
|
443
|
+
contentOverride = contentOverride({ loading });
|
|
444
|
+
}
|
|
445
|
+
let simpleAccept;
|
|
446
|
+
let handleManuallyEnterData;
|
|
447
|
+
let advancedAccept;
|
|
448
|
+
|
|
449
|
+
if (Array.isArray(accept)) {
|
|
450
|
+
if (accept.some(acc => isPlainObject(acc))) {
|
|
451
|
+
//advanced accept
|
|
452
|
+
advancedAccept = accept;
|
|
453
|
+
simpleAccept = flatMap(accept, acc => {
|
|
454
|
+
if (acc.validateAgainstSchema) {
|
|
455
|
+
if (!acc.type) {
|
|
456
|
+
acc.type = [".csv", ".xlsx"];
|
|
457
|
+
}
|
|
458
|
+
handleManuallyEnterData = async e => {
|
|
459
|
+
e.stopPropagation();
|
|
460
|
+
const { newEntities, fileName } = await showSimpleInsertDataDialog(
|
|
461
|
+
"onSimpleInsertDialogFinish",
|
|
462
|
+
{
|
|
463
|
+
validateAgainstSchema
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
if (!newEntities) return;
|
|
467
|
+
//check existing files to make sure the new file name gets incremented if necessary
|
|
468
|
+
// fileList
|
|
469
|
+
const newFileName = getNewName(fileListToUse, fileName);
|
|
470
|
+
const { newFile, cleanedEntities } = getNewCsvFile(
|
|
471
|
+
newEntities,
|
|
472
|
+
newFileName
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const file = {
|
|
476
|
+
...newFile,
|
|
477
|
+
parsedData: cleanedEntities,
|
|
478
|
+
meta: {
|
|
479
|
+
fields: validateAgainstSchema.fields.map(({ path }) => path)
|
|
480
|
+
},
|
|
481
|
+
name: newFileName,
|
|
482
|
+
originFileObj: newFile,
|
|
483
|
+
originalFileObj: newFile,
|
|
484
|
+
id: nanoid(),
|
|
485
|
+
hasEditClick: true
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const cleanedFileList = [file, ...fileListToUse].slice(
|
|
489
|
+
0,
|
|
490
|
+
fileLimit ? fileLimit : undefined
|
|
491
|
+
);
|
|
492
|
+
handleSecondHalfOfUpload({
|
|
493
|
+
acceptedFiles: cleanedFileList,
|
|
494
|
+
cleanedFileList
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
window.toastr.success(`File Added`);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const nameToUse =
|
|
501
|
+
startCase(
|
|
502
|
+
removeExt(
|
|
503
|
+
validateAgainstSchema.fileName || validateAgainstSchema.name
|
|
504
|
+
)
|
|
505
|
+
) || "Example";
|
|
506
|
+
|
|
507
|
+
const handleDownloadXlsxFile = async () => {
|
|
508
|
+
const dataDictionarySchema = [
|
|
509
|
+
{ value: f => f.displayName || f.path, column: `Column Name` },
|
|
510
|
+
// {
|
|
511
|
+
// value: f => f.isUnique ? "Unique" : "",
|
|
512
|
+
// column: `Unique?`
|
|
513
|
+
// },
|
|
514
|
+
{
|
|
515
|
+
value: f => (f.isRequired ? "Required" : "Optional"),
|
|
516
|
+
column: `Required?`
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
value: f => (f.type === "dropdown" ? "text" : f.type || "text"),
|
|
520
|
+
column: `Data Type`
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
value: f => f.description,
|
|
524
|
+
column: `Notes`
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
value: f => f.example || f.defaultValue || "",
|
|
528
|
+
column: `Example Data`
|
|
529
|
+
}
|
|
530
|
+
];
|
|
531
|
+
|
|
532
|
+
const mainExampleData = {};
|
|
533
|
+
const fieldsToUse = [
|
|
534
|
+
...validateAgainstSchema.fields,
|
|
535
|
+
...(validateAgainstSchema.exampleDownloadFields ?? [])
|
|
536
|
+
];
|
|
537
|
+
const mainSchema = fieldsToUse.map(f => {
|
|
538
|
+
mainExampleData[f.displayName || f.path] =
|
|
539
|
+
f.example || f.defaultValue;
|
|
540
|
+
return {
|
|
541
|
+
column: f.displayName || f.path,
|
|
542
|
+
value: v => {
|
|
543
|
+
return v[f.displayName || f.path];
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
const blobFile = await writeXlsxFile(
|
|
548
|
+
[[mainExampleData], fieldsToUse, helperText],
|
|
549
|
+
{
|
|
550
|
+
headerStyle: {
|
|
551
|
+
fontWeight: "bold"
|
|
552
|
+
},
|
|
553
|
+
schema: [mainSchema, dataDictionarySchema, helperSchema],
|
|
554
|
+
sheets: [nameToUse, "Column Info", "Upload Instructions"],
|
|
555
|
+
filePath: "file.xlsx"
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
downloadjs(blobFile, `${nameToUse}.xlsx`, "xlsx");
|
|
559
|
+
};
|
|
560
|
+
// handleDownloadXlsxFile()
|
|
561
|
+
acc.exampleFiles = [
|
|
562
|
+
// ...(a.exampleFile ? [a.exampleFile] : []),
|
|
563
|
+
{
|
|
564
|
+
description: "Download Example CSV File",
|
|
565
|
+
exampleFile: () => {
|
|
566
|
+
const rows = [];
|
|
567
|
+
const schemaToUse = [
|
|
568
|
+
...acc.validateAgainstSchema.fields,
|
|
569
|
+
...(acc.validateAgainstSchema.exampleDownloadFields ?? [])
|
|
570
|
+
];
|
|
571
|
+
rows.push(
|
|
572
|
+
schemaToUse.map(f => {
|
|
573
|
+
return `${f.displayName || f.path}`;
|
|
574
|
+
})
|
|
575
|
+
);
|
|
576
|
+
rows.push(
|
|
577
|
+
schemaToUse.map(f => {
|
|
578
|
+
return `${f.example || f.defaultValue || ""}`;
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
const csv = unparse(rows);
|
|
582
|
+
|
|
583
|
+
const downloadFn = window.Cypress?.downloadTest || downloadjs;
|
|
584
|
+
downloadFn(csv, `${nameToUse}.csv`, "csv");
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
description: "Download Example XLSX File",
|
|
589
|
+
subtext: "Includes Upload Instructions and Column Info",
|
|
590
|
+
exampleFile: handleDownloadXlsxFile
|
|
591
|
+
},
|
|
592
|
+
...(noBuildCsvOption
|
|
593
|
+
? []
|
|
594
|
+
: [
|
|
595
|
+
{
|
|
596
|
+
description: manualEnterMessage,
|
|
597
|
+
subtext: manualEnterSubMessage,
|
|
598
|
+
icon: "manually-entered-data",
|
|
599
|
+
exampleFile: handleManuallyEnterData
|
|
600
|
+
}
|
|
601
|
+
])
|
|
602
|
+
];
|
|
603
|
+
delete acc.exampleFile;
|
|
604
|
+
}
|
|
605
|
+
if (acc.type) return acc.type;
|
|
606
|
+
return acc;
|
|
607
|
+
});
|
|
608
|
+
simpleAccept = simpleAccept.join(", ");
|
|
609
|
+
} else {
|
|
610
|
+
simpleAccept = accept.join(", ");
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
simpleAccept = accept;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const fileListToUse = fileList ? fileList : [];
|
|
617
|
+
|
|
618
|
+
return (
|
|
619
|
+
<>
|
|
620
|
+
{callout && (
|
|
621
|
+
<Callout style={{ marginBottom: 5 }} intent="primary">
|
|
622
|
+
{callout}
|
|
623
|
+
</Callout>
|
|
624
|
+
)}
|
|
625
|
+
<div
|
|
626
|
+
className="tg-uploader-outer"
|
|
627
|
+
style={{
|
|
628
|
+
width: minimal ? undefined : "100%",
|
|
629
|
+
display: "flex",
|
|
630
|
+
height: "fit-content"
|
|
631
|
+
}}
|
|
632
|
+
>
|
|
633
|
+
<Comp />
|
|
634
|
+
<Comp2 />
|
|
635
|
+
<div
|
|
636
|
+
className="tg-uploader-inner"
|
|
637
|
+
style={{ width: "100%", height: "fit-content", minWidth: 0 }}
|
|
638
|
+
>
|
|
639
|
+
{(simpleAccept || acceptLoading) && (
|
|
640
|
+
<div
|
|
641
|
+
className={Classes.TEXT_MUTED}
|
|
642
|
+
style={{ fontSize: 11, marginBottom: 5 }}
|
|
643
|
+
>
|
|
644
|
+
{advancedAccept && !acceptLoading ? (
|
|
645
|
+
<div>
|
|
646
|
+
Accepts
|
|
647
|
+
<span>
|
|
648
|
+
{advancedAccept.map((acc, i) => {
|
|
649
|
+
const disabled = !(
|
|
650
|
+
acc.description ||
|
|
651
|
+
acc.exampleFile ||
|
|
652
|
+
acc.exampleFiles
|
|
653
|
+
);
|
|
654
|
+
const PopOrTooltip = acc.exampleFiles ? Popover : Tooltip;
|
|
655
|
+
const hasDownload = acc.exampleFile || acc.exampleFiles;
|
|
656
|
+
const CustomTag = !hasDownload ? "span" : "a";
|
|
657
|
+
return (
|
|
658
|
+
<PopOrTooltip
|
|
659
|
+
key={i}
|
|
660
|
+
interactionKind="hover"
|
|
661
|
+
disabled={disabled}
|
|
662
|
+
modifiers={popoverOverflowModifiers}
|
|
663
|
+
content={
|
|
664
|
+
acc.exampleFiles ? (
|
|
665
|
+
<Menu>
|
|
666
|
+
{acc.exampleFiles.map(
|
|
667
|
+
(
|
|
668
|
+
{ description, subtext, exampleFile, icon },
|
|
669
|
+
i
|
|
670
|
+
) => (
|
|
671
|
+
<MenuItem
|
|
672
|
+
icon={icon || "download"}
|
|
673
|
+
intent="primary"
|
|
674
|
+
text={
|
|
675
|
+
subtext ? (
|
|
676
|
+
<div>
|
|
677
|
+
<div>{description}</div>
|
|
678
|
+
<div
|
|
679
|
+
style={{
|
|
680
|
+
fontSize: 11,
|
|
681
|
+
fontStyle: "italic",
|
|
682
|
+
color: Colors.GRAY3
|
|
683
|
+
}}
|
|
684
|
+
>
|
|
685
|
+
{subtext}
|
|
686
|
+
</div>{" "}
|
|
687
|
+
</div>
|
|
688
|
+
) : (
|
|
689
|
+
description
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
{...getFileDownloadAttr(exampleFile)}
|
|
693
|
+
key={i}
|
|
694
|
+
/>
|
|
695
|
+
)
|
|
696
|
+
)}
|
|
697
|
+
</Menu>
|
|
698
|
+
) : (
|
|
699
|
+
<div
|
|
700
|
+
style={{
|
|
701
|
+
maxWidth: 400,
|
|
702
|
+
wordBreak: "break-word"
|
|
703
|
+
}}
|
|
704
|
+
>
|
|
705
|
+
{acc.description ? (
|
|
706
|
+
<div
|
|
707
|
+
style={{
|
|
708
|
+
marginBottom: 4,
|
|
709
|
+
fontStyle: "italic"
|
|
710
|
+
}}
|
|
711
|
+
>
|
|
712
|
+
{acc.description}
|
|
713
|
+
</div>
|
|
714
|
+
) : (
|
|
715
|
+
""
|
|
716
|
+
)}
|
|
717
|
+
{acc.exampleFile &&
|
|
718
|
+
(acc.isTemplate
|
|
719
|
+
? "Download Example Template"
|
|
720
|
+
: "Download Example File")}
|
|
721
|
+
</div>
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
>
|
|
725
|
+
<CustomTag
|
|
726
|
+
className="tgFileTypeDescriptor"
|
|
727
|
+
style={{ marginRight: 10, cursor: "pointer" }}
|
|
728
|
+
{...getFileDownloadAttr(acc.exampleFile)}
|
|
729
|
+
>
|
|
730
|
+
{(acc.type
|
|
731
|
+
? isArray(acc.type)
|
|
732
|
+
? acc.type
|
|
733
|
+
: [acc.type]
|
|
734
|
+
: [acc]
|
|
735
|
+
)
|
|
736
|
+
.map(t => {
|
|
737
|
+
if (!t.startsWith) {
|
|
738
|
+
console.error(`Missing type here:`, acc);
|
|
739
|
+
throw new Error(
|
|
740
|
+
`Missing "type" here: ${JSON.stringify(
|
|
741
|
+
acc,
|
|
742
|
+
null,
|
|
743
|
+
4
|
|
744
|
+
)}`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
return t.startsWith(".") ? t : "." + t;
|
|
748
|
+
})
|
|
749
|
+
.join(", ")}
|
|
750
|
+
|
|
751
|
+
{hasDownload && (
|
|
752
|
+
<Icon
|
|
753
|
+
style={{
|
|
754
|
+
marginTop: 3,
|
|
755
|
+
marginLeft: 3
|
|
756
|
+
}}
|
|
757
|
+
size={10}
|
|
758
|
+
icon="download"
|
|
759
|
+
/>
|
|
760
|
+
)}
|
|
761
|
+
</CustomTag>
|
|
762
|
+
</PopOrTooltip>
|
|
763
|
+
);
|
|
764
|
+
})}
|
|
765
|
+
</span>
|
|
766
|
+
</div>
|
|
767
|
+
) : acceptLoading ? (
|
|
768
|
+
// make the dots below "load"
|
|
769
|
+
|
|
770
|
+
<>
|
|
771
|
+
Accept Loading
|
|
772
|
+
<LoadingDots />
|
|
773
|
+
</>
|
|
774
|
+
) : (
|
|
775
|
+
<>Accepts {simpleAccept}</>
|
|
776
|
+
)}
|
|
777
|
+
</div>
|
|
778
|
+
)}
|
|
779
|
+
<Dropzone
|
|
780
|
+
disabled={dropzoneDisabled}
|
|
781
|
+
onClick={evt => evt.preventDefault()}
|
|
782
|
+
multiple={fileLimit !== 1}
|
|
783
|
+
accept={
|
|
784
|
+
simpleAccept
|
|
785
|
+
? simpleAccept
|
|
786
|
+
.split(", ")
|
|
787
|
+
.map(acc => (acc.startsWith(".") ? acc : "." + acc))
|
|
788
|
+
.join(", ")
|
|
789
|
+
: undefined
|
|
790
|
+
}
|
|
791
|
+
onDrop={async (_acceptedFiles, rejectedFiles) => {
|
|
792
|
+
let acceptedFiles = [];
|
|
793
|
+
for (const file of _acceptedFiles) {
|
|
794
|
+
if ((validateAgainstSchema || autoUnzip) && isZipFile(file)) {
|
|
795
|
+
const files = await filterFilesInZip(
|
|
796
|
+
file,
|
|
797
|
+
simpleAccept
|
|
798
|
+
?.split(", ")
|
|
799
|
+
?.map(acc => (acc.startsWith(".") ? acc : "." + acc)) ||
|
|
800
|
+
[]
|
|
801
|
+
);
|
|
802
|
+
acceptedFiles.push(...files.map(f => f.originFileObj));
|
|
803
|
+
} else {
|
|
804
|
+
acceptedFiles.push(file);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
cleanupFiles();
|
|
808
|
+
if (rejectedFiles.length) {
|
|
809
|
+
let msg = "";
|
|
810
|
+
rejectedFiles.forEach(file => {
|
|
811
|
+
if (msg) msg += "\n";
|
|
812
|
+
msg +=
|
|
813
|
+
`${file.file.name}: ` +
|
|
814
|
+
file.errors.map(err => err.message).join(", ");
|
|
815
|
+
});
|
|
816
|
+
window.toastr &&
|
|
817
|
+
window.toastr.warning(
|
|
818
|
+
<div className="preserve-newline">{msg}</div>
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
if (!acceptedFiles.length) return;
|
|
822
|
+
setLoading(true);
|
|
823
|
+
acceptedFiles = trimFiles(acceptedFiles, fileLimit);
|
|
824
|
+
|
|
825
|
+
acceptedFiles.forEach(file => {
|
|
826
|
+
file.preview = URL.createObjectURL(file);
|
|
827
|
+
file.loading = true;
|
|
828
|
+
if (!file.id) {
|
|
829
|
+
file.id = nanoid();
|
|
830
|
+
}
|
|
831
|
+
filesToClean.current.push(file);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if (readBeforeUpload) {
|
|
835
|
+
acceptedFiles = await Promise.all(
|
|
836
|
+
acceptedFiles.map(file => {
|
|
837
|
+
return new Promise((resolve, reject) => {
|
|
838
|
+
const reader = new FileReader();
|
|
839
|
+
reader.readAsText(file, "UTF-8");
|
|
840
|
+
reader.onload = evt => {
|
|
841
|
+
file.parsedString = evt.target.result;
|
|
842
|
+
resolve(file);
|
|
843
|
+
};
|
|
844
|
+
reader.onerror = err => {
|
|
845
|
+
console.error("err:", err);
|
|
846
|
+
reject(err);
|
|
847
|
+
};
|
|
848
|
+
});
|
|
849
|
+
})
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const cleanedAccepted = acceptedFiles.map(file => {
|
|
853
|
+
return {
|
|
854
|
+
originFileObj: file,
|
|
855
|
+
originalFileObj: file,
|
|
856
|
+
id: file.id,
|
|
857
|
+
lastModified: file.lastModified,
|
|
858
|
+
lastModifiedDate: file.lastModifiedDate,
|
|
859
|
+
loading: file.loading,
|
|
860
|
+
name: file.name,
|
|
861
|
+
preview: file.preview,
|
|
862
|
+
size: file.size,
|
|
863
|
+
type: file.type,
|
|
864
|
+
...(file.parsedString
|
|
865
|
+
? { parsedString: file.parsedString }
|
|
866
|
+
: {})
|
|
867
|
+
};
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
const toKeep = [];
|
|
871
|
+
if (validateAgainstSchema) {
|
|
872
|
+
const filesWIssues = [];
|
|
873
|
+
const filesWOIssues = [];
|
|
874
|
+
for (const [i, file] of cleanedAccepted.entries()) {
|
|
875
|
+
if (isCsvOrExcelFile(file)) {
|
|
876
|
+
let parsedF;
|
|
877
|
+
try {
|
|
878
|
+
parsedF = await parseCsvOrExcelFile(file, {
|
|
879
|
+
csvParserOptions: isFunction(
|
|
880
|
+
validateAgainstSchema.csvParserOptions
|
|
881
|
+
)
|
|
882
|
+
? validateAgainstSchema.csvParserOptions({
|
|
883
|
+
validateAgainstSchema
|
|
884
|
+
})
|
|
885
|
+
: validateAgainstSchema.csvParserOptions
|
|
886
|
+
});
|
|
887
|
+
} catch (error) {
|
|
888
|
+
console.error("error:", error);
|
|
889
|
+
window.toastr &&
|
|
890
|
+
window.toastr.error(
|
|
891
|
+
`There was an error parsing your file. Please try again. ${
|
|
892
|
+
error.message || error
|
|
893
|
+
}`
|
|
894
|
+
);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const {
|
|
899
|
+
csvValidationIssue: _csvValidationIssue,
|
|
900
|
+
matchedHeaders,
|
|
901
|
+
userSchema,
|
|
902
|
+
searchResults,
|
|
903
|
+
ignoredHeadersMsg
|
|
904
|
+
} = await tryToMatchSchemas({
|
|
905
|
+
incomingData: parsedF.data,
|
|
906
|
+
validateAgainstSchema
|
|
907
|
+
});
|
|
908
|
+
if (userSchema?.userData?.length === 0) {
|
|
909
|
+
console.error(
|
|
910
|
+
`userSchema, parsedF.data:`,
|
|
911
|
+
userSchema,
|
|
912
|
+
parsedF.data
|
|
913
|
+
);
|
|
914
|
+
} else {
|
|
915
|
+
toKeep.push(file);
|
|
916
|
+
let csvValidationIssue = _csvValidationIssue;
|
|
917
|
+
if (csvValidationIssue) {
|
|
918
|
+
if (isObject(csvValidationIssue)) {
|
|
919
|
+
dispatch(
|
|
920
|
+
initialize(
|
|
921
|
+
`editableCellTable${
|
|
922
|
+
cleanedAccepted.length > 1 ? `-${i}` : ""
|
|
923
|
+
}`,
|
|
924
|
+
{
|
|
925
|
+
reduxFormCellValidation: csvValidationIssue
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
keepDirty: true,
|
|
929
|
+
keepValues: true,
|
|
930
|
+
updateUnregisteredFields: true
|
|
931
|
+
}
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
const err = Object.values(csvValidationIssue)[0];
|
|
935
|
+
// csvValidationIssue = `It looks like there was an error with your data - \n\n${
|
|
936
|
+
// err && err.message ? err.message : err
|
|
937
|
+
// }.\n\nPlease review your headers and then correct any errors on the next page.`; //pass just the first error as a string
|
|
938
|
+
const errMsg = err && err.message ? err.message : err;
|
|
939
|
+
if (isPlainObject(errMsg)) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`errMsg is an object ${JSON.stringify(
|
|
942
|
+
errMsg,
|
|
943
|
+
null,
|
|
944
|
+
4
|
|
945
|
+
)}`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
csvValidationIssue = (
|
|
949
|
+
<div>
|
|
950
|
+
<div>
|
|
951
|
+
It looks like there was an error with your data
|
|
952
|
+
(Correct on the Review Data page):
|
|
953
|
+
</div>
|
|
954
|
+
<div style={{ color: "red" }}>{errMsg}</div>
|
|
955
|
+
<div>
|
|
956
|
+
Please review your headers and then correct any
|
|
957
|
+
errors on the next page.
|
|
958
|
+
</div>
|
|
959
|
+
</div>
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
filesWIssues.push({
|
|
963
|
+
file,
|
|
964
|
+
csvValidationIssue,
|
|
965
|
+
ignoredHeadersMsg,
|
|
966
|
+
matchedHeaders,
|
|
967
|
+
userSchema,
|
|
968
|
+
searchResults
|
|
969
|
+
});
|
|
970
|
+
} else {
|
|
971
|
+
filesWOIssues.push({
|
|
972
|
+
file,
|
|
973
|
+
csvValidationIssue,
|
|
974
|
+
ignoredHeadersMsg,
|
|
975
|
+
matchedHeaders,
|
|
976
|
+
userSchema,
|
|
977
|
+
searchResults
|
|
978
|
+
});
|
|
979
|
+
const newFileName = removeExt(file.name) + `.csv`;
|
|
980
|
+
|
|
981
|
+
const { newFile, cleanedEntities } = getNewCsvFile(
|
|
982
|
+
userSchema.userData,
|
|
983
|
+
newFileName
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
file.meta = parsedF.meta;
|
|
987
|
+
file.hasEditClick = true;
|
|
988
|
+
file.parsedData = cleanedEntities;
|
|
989
|
+
file.name = newFileName;
|
|
990
|
+
file.originFileObj = newFile;
|
|
991
|
+
file.originalFileObj = newFile;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
toKeep.push(file);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (filesWIssues.length) {
|
|
999
|
+
const { file } = filesWIssues[0];
|
|
1000
|
+
const allFiles = [...filesWIssues, ...filesWOIssues];
|
|
1001
|
+
const doAllFilesHaveSameHeaders = allFiles.every(f => {
|
|
1002
|
+
if (f.userSchema.fields && f.userSchema.fields.length) {
|
|
1003
|
+
return f.userSchema.fields.every((h, i) => {
|
|
1004
|
+
return h.path === allFiles[0].userSchema.fields[i].path;
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
return false;
|
|
1008
|
+
});
|
|
1009
|
+
const multipleFiles = allFiles.length > 1;
|
|
1010
|
+
const { res } = await showUploadCsvWizardDialog(
|
|
1011
|
+
"onUploadWizardFinish",
|
|
1012
|
+
{
|
|
1013
|
+
dialogProps: {
|
|
1014
|
+
title: `Fix Up File${multipleFiles ? "s" : ""} ${
|
|
1015
|
+
multipleFiles ? "" : file.name ? `"${file.name}"` : ""
|
|
1016
|
+
}`
|
|
1017
|
+
},
|
|
1018
|
+
doAllFilesHaveSameHeaders,
|
|
1019
|
+
filesWIssues: allFiles,
|
|
1020
|
+
validateAgainstSchema
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
if (!res) {
|
|
1025
|
+
window.toastr.warning(`File Upload Aborted`);
|
|
1026
|
+
return;
|
|
1027
|
+
} else {
|
|
1028
|
+
allFiles.forEach(({ file }, i) => {
|
|
1029
|
+
const newEntities = res[i];
|
|
1030
|
+
// const newFileName = removeExt(file.name) + `_updated.csv`;
|
|
1031
|
+
//swap out file with a new csv file
|
|
1032
|
+
const { newFile, cleanedEntities } = getNewCsvFile(
|
|
1033
|
+
newEntities,
|
|
1034
|
+
file.name
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
file.hasEditClick = true;
|
|
1038
|
+
file.parsedData = cleanedEntities;
|
|
1039
|
+
file.originFileObj = newFile;
|
|
1040
|
+
file.originalFileObj = newFile;
|
|
1041
|
+
});
|
|
1042
|
+
setTimeout(() => {
|
|
1043
|
+
//inside a timeout for cypress purposes
|
|
1044
|
+
window.toastr.success(
|
|
1045
|
+
`Added Fixed Up File${
|
|
1046
|
+
allFiles.length > 1 ? "s" : ""
|
|
1047
|
+
} ${allFiles.map(({ file }) => file.name).join(", ")}`
|
|
1048
|
+
);
|
|
1049
|
+
}, 200);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} else {
|
|
1053
|
+
toKeep.push(...cleanedAccepted);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (toKeep.length === 0) {
|
|
1057
|
+
window.toastr &&
|
|
1058
|
+
window.toastr.error(
|
|
1059
|
+
`It looks like there wasn't any data in your file. Please add some data and try again`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
const cleanedFileList = trimFiles(
|
|
1063
|
+
[...toKeep, ...fileListToUse],
|
|
1064
|
+
fileLimit
|
|
1065
|
+
);
|
|
1066
|
+
handleSecondHalfOfUpload({ acceptedFiles, cleanedFileList });
|
|
1067
|
+
}}
|
|
1068
|
+
{...dropzoneProps}
|
|
1069
|
+
>
|
|
1070
|
+
{({
|
|
1071
|
+
getRootProps,
|
|
1072
|
+
getInputProps,
|
|
1073
|
+
isDragAccept,
|
|
1074
|
+
isDragReject,
|
|
1075
|
+
isDragActive
|
|
1076
|
+
}) => (
|
|
1077
|
+
<InnerDropZone
|
|
1078
|
+
getRootProps={getRootProps}
|
|
1079
|
+
getInputProps={getInputProps}
|
|
1080
|
+
isDragAccept={isDragAccept}
|
|
1081
|
+
isDragReject={isDragReject}
|
|
1082
|
+
isDragActive={isDragActive}
|
|
1083
|
+
className={className}
|
|
1084
|
+
minimal={minimal}
|
|
1085
|
+
dropzoneDisabled={dropzoneDisabled}
|
|
1086
|
+
contentOverride={contentOverride}
|
|
1087
|
+
simpleAccept={simpleAccept}
|
|
1088
|
+
innerIcon={innerIcon}
|
|
1089
|
+
innerText={innerText}
|
|
1090
|
+
validateAgainstSchema={validateAgainstSchema}
|
|
1091
|
+
handleManuallyEnterData={handleManuallyEnterData}
|
|
1092
|
+
noBuildCsvOption={noBuildCsvOption}
|
|
1093
|
+
showFilesCount={showFilesCount}
|
|
1094
|
+
fileList={fileList}
|
|
1095
|
+
/>
|
|
1096
|
+
)}
|
|
1097
|
+
</Dropzone>
|
|
1098
|
+
{/* {validateAgainstSchema && <CsvWizardHelper bindToggle={{}} validateAgainstSchema={validateAgainstSchema}></CsvWizardHelper>} */}
|
|
1099
|
+
|
|
1100
|
+
{fileList && showUploadList && !minimal && !!fileList.length && (
|
|
1101
|
+
<div
|
|
1102
|
+
className={classNames(
|
|
1103
|
+
"tg-upload-file-list-holder",
|
|
1104
|
+
overflowList ? "tg-upload-file-list-item-overflow" : null
|
|
1105
|
+
)}
|
|
1106
|
+
>
|
|
1107
|
+
{fileList.map((file, index) => {
|
|
1108
|
+
const {
|
|
1109
|
+
loading,
|
|
1110
|
+
error,
|
|
1111
|
+
name,
|
|
1112
|
+
originalName,
|
|
1113
|
+
url,
|
|
1114
|
+
downloadName,
|
|
1115
|
+
hasEditClick
|
|
1116
|
+
} = file;
|
|
1117
|
+
let icon;
|
|
1118
|
+
if (loading) {
|
|
1119
|
+
icon = "repeat";
|
|
1120
|
+
} else if (error) {
|
|
1121
|
+
icon = "error";
|
|
1122
|
+
} else {
|
|
1123
|
+
if (onPreviewClick) {
|
|
1124
|
+
icon = "eye-open";
|
|
1125
|
+
} else if (hasEditClick) {
|
|
1126
|
+
icon = "edit";
|
|
1127
|
+
} else {
|
|
1128
|
+
icon = "saved";
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return (
|
|
1132
|
+
<div
|
|
1133
|
+
key={index}
|
|
1134
|
+
className="tg-upload-file-list-item"
|
|
1135
|
+
style={{ display: "flex", width: "100%" }}
|
|
1136
|
+
>
|
|
1137
|
+
<div
|
|
1138
|
+
style={{
|
|
1139
|
+
display: "flex",
|
|
1140
|
+
justifyContent: "space-between",
|
|
1141
|
+
width: "100%"
|
|
1142
|
+
}}
|
|
1143
|
+
>
|
|
1144
|
+
<span style={{ display: "flex" }}>
|
|
1145
|
+
<Icon
|
|
1146
|
+
className={classnames({
|
|
1147
|
+
"tg-spin": loading,
|
|
1148
|
+
"tg-upload-file-list-item-preview": onPreviewClick,
|
|
1149
|
+
"tg-upload-file-list-item-edit": hasEditClick,
|
|
1150
|
+
clickableIcon: onPreviewClick || hasEditClick
|
|
1151
|
+
})}
|
|
1152
|
+
data-tip={
|
|
1153
|
+
hasEditClick
|
|
1154
|
+
? "Edit"
|
|
1155
|
+
: onPreviewClick
|
|
1156
|
+
? "Preview"
|
|
1157
|
+
: undefined
|
|
1158
|
+
}
|
|
1159
|
+
style={{ marginRight: 5 }}
|
|
1160
|
+
icon={icon}
|
|
1161
|
+
onClick={async () => {
|
|
1162
|
+
if (hasEditClick) {
|
|
1163
|
+
const {
|
|
1164
|
+
// csvValidationIssue: _csvValidationIssue,
|
|
1165
|
+
matchedHeaders,
|
|
1166
|
+
userSchema,
|
|
1167
|
+
searchResults
|
|
1168
|
+
} = await tryToMatchSchemas({
|
|
1169
|
+
incomingData: file.parsedData,
|
|
1170
|
+
validateAgainstSchema
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
const { newEntities, fileName } =
|
|
1174
|
+
await showSimpleInsertDataDialog(
|
|
1175
|
+
"onSimpleInsertDialogFinish",
|
|
1176
|
+
{
|
|
1177
|
+
dialogProps: {
|
|
1178
|
+
title: "Edit Data"
|
|
1179
|
+
},
|
|
1180
|
+
initialValues: {
|
|
1181
|
+
fileName: removeExt(file.name)
|
|
1182
|
+
},
|
|
1183
|
+
validateAgainstSchema,
|
|
1184
|
+
isEditingExistingFile: true,
|
|
1185
|
+
searchResults,
|
|
1186
|
+
matchedHeaders,
|
|
1187
|
+
userSchema
|
|
1188
|
+
}
|
|
1189
|
+
);
|
|
1190
|
+
if (!newEntities) {
|
|
1191
|
+
return;
|
|
1192
|
+
} else {
|
|
1193
|
+
const { newFile, cleanedEntities } =
|
|
1194
|
+
getNewCsvFile(newEntities, fileName);
|
|
1195
|
+
const tmpFile = Object.assign({}, file, {
|
|
1196
|
+
...newFile,
|
|
1197
|
+
originFileObj: newFile,
|
|
1198
|
+
originalFileObj: newFile,
|
|
1199
|
+
parsedData: cleanedEntities
|
|
1200
|
+
});
|
|
1201
|
+
tmpFile.name = newFile.name;
|
|
1202
|
+
const tmpFileList = [...fileList];
|
|
1203
|
+
tmpFileList[index] = tmpFile;
|
|
1204
|
+
handleSecondHalfOfUpload({
|
|
1205
|
+
acceptedFiles: tmpFileList,
|
|
1206
|
+
cleanedFileList: tmpFileList
|
|
1207
|
+
});
|
|
1208
|
+
window.toastr.success(`File Updated`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (onPreviewClick) {
|
|
1212
|
+
onPreviewClick(file, index, fileList);
|
|
1213
|
+
}
|
|
1214
|
+
}}
|
|
1215
|
+
/>
|
|
1216
|
+
<a
|
|
1217
|
+
name={name || originalName}
|
|
1218
|
+
{...(url && !onFileClick
|
|
1219
|
+
? { download: true, href: url }
|
|
1220
|
+
: {})}
|
|
1221
|
+
/* eslint-disable react/jsx-no-bind*/
|
|
1222
|
+
onClick={() => {
|
|
1223
|
+
if (onFileClick) {
|
|
1224
|
+
onFileClick(file);
|
|
1225
|
+
} else {
|
|
1226
|
+
//handle default download
|
|
1227
|
+
if (file.originFileObj) {
|
|
1228
|
+
downloadjs(file.originFileObj, file.name);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}}
|
|
1232
|
+
/* eslint-enable react/jsx-no-bind*/
|
|
1233
|
+
{...(downloadName ? { download: downloadName } : {})}
|
|
1234
|
+
>
|
|
1235
|
+
{" "}
|
|
1236
|
+
{name || originalName}{" "}
|
|
1237
|
+
</a>
|
|
1238
|
+
</span>
|
|
1239
|
+
{!loading && (
|
|
1240
|
+
<Icon
|
|
1241
|
+
onClick={() => {
|
|
1242
|
+
onRemove(file, index, fileList);
|
|
1243
|
+
onChange(
|
|
1244
|
+
fileList.filter((file, index2) => {
|
|
1245
|
+
return index2 !== index;
|
|
1246
|
+
})
|
|
1247
|
+
);
|
|
1248
|
+
}}
|
|
1249
|
+
iconSize={16}
|
|
1250
|
+
icon="cross"
|
|
1251
|
+
className="tg-upload-file-list-item-close clickableIcon"
|
|
1252
|
+
/>
|
|
1253
|
+
)}
|
|
1254
|
+
</div>
|
|
1255
|
+
</div>
|
|
1256
|
+
);
|
|
1257
|
+
})}
|
|
1258
|
+
</div>
|
|
1259
|
+
)}
|
|
1260
|
+
</div>
|
|
1261
|
+
{threeDotMenuItems && (
|
|
1262
|
+
<div className="tg-dropzone-extra-options">
|
|
1263
|
+
<Popover
|
|
1264
|
+
autoFocus={false}
|
|
1265
|
+
minimal
|
|
1266
|
+
content={<Menu>{threeDotMenuItems}</Menu>}
|
|
1267
|
+
position={Position.BOTTOM_RIGHT}
|
|
1268
|
+
>
|
|
1269
|
+
<Button minimal icon="more" />
|
|
1270
|
+
</Popover>
|
|
1271
|
+
</div>
|
|
1272
|
+
)}
|
|
1273
|
+
</div>
|
|
1274
|
+
</>
|
|
1275
|
+
);
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
export default Uploader;
|