@teselagen/ui 0.7.33-beta.2 → 0.7.33-beta.3

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.
Files changed (115) hide show
  1. package/package.json +1 -1
  2. package/src/style.css +10 -26
  3. package/AdvancedOptions.js +0 -33
  4. package/AssignDefaultsModeContext.js +0 -22
  5. package/CellDragHandle.js +0 -132
  6. package/ColumnFilterMenu.js +0 -62
  7. package/Columns.js +0 -979
  8. package/DisabledLoadingComponent.js +0 -15
  9. package/DisplayOptions.js +0 -199
  10. package/DropdownButton.js +0 -36
  11. package/DropdownCell.js +0 -61
  12. package/EditableCell.js +0 -44
  13. package/FillWindow.css +0 -6
  14. package/FillWindow.js +0 -69
  15. package/FilterAndSortMenu.js +0 -388
  16. package/FormSeparator.js +0 -9
  17. package/LoadingDots.js +0 -14
  18. package/MatchHeaders.js +0 -234
  19. package/PagingTool.js +0 -225
  20. package/RenderCell.js +0 -191
  21. package/SearchBar.js +0 -69
  22. package/SimpleStepViz.js +0 -22
  23. package/SortableColumns.js +0 -100
  24. package/TableFormTrackerContext.js +0 -10
  25. package/Tag.js +0 -112
  26. package/ThComponent.js +0 -44
  27. package/TimelineEvent.js +0 -31
  28. package/UploadCsvWizard.css +0 -4
  29. package/UploadCsvWizard.js +0 -719
  30. package/Uploader.js +0 -1278
  31. package/adHoc.js +0 -10
  32. package/autoTooltip.js +0 -201
  33. package/basicHandleActionsWithFullState.js +0 -14
  34. package/browserUtils.js +0 -3
  35. package/combineReducersWithFullState.js +0 -14
  36. package/commandControls.js +0 -82
  37. package/commandUtils.js +0 -112
  38. package/constants.js +0 -1
  39. package/convertSchema.js +0 -69
  40. package/customIcons.js +0 -361
  41. package/dataTableEnhancer.js +0 -41
  42. package/defaultFormatters.js +0 -32
  43. package/defaultValidators.js +0 -40
  44. package/determineBlackOrWhiteTextColor.js +0 -4
  45. package/editCellHelper.js +0 -44
  46. package/filterLocalEntitiesToHasura.js +0 -216
  47. package/formatPasteData.js +0 -16
  48. package/getAllRows.js +0 -11
  49. package/getCellCopyText.js +0 -7
  50. package/getCellInfo.js +0 -36
  51. package/getCellVal.js +0 -20
  52. package/getDayjsFormatter.js +0 -35
  53. package/getFieldPathToField.js +0 -7
  54. package/getIdOrCodeOrIndex.js +0 -9
  55. package/getLastSelectedEntity.js +0 -11
  56. package/getNewEntToSelect.js +0 -25
  57. package/getNewName.js +0 -31
  58. package/getRowCopyText.js +0 -28
  59. package/getTableConfigFromStorage.js +0 -5
  60. package/getTextFromEl.js +0 -28
  61. package/getVals.js +0 -8
  62. package/handleCopyColumn.js +0 -21
  63. package/handleCopyHelper.js +0 -15
  64. package/handleCopyRows.js +0 -23
  65. package/handleCopyTable.js +0 -16
  66. package/handlerHelpers.js +0 -24
  67. package/hotkeyUtils.js +0 -131
  68. package/index.js +0 -1
  69. package/initializeHasuraWhereAndFilter.js +0 -27
  70. package/isBeingCalledExcessively.js +0 -24
  71. package/isBottomRightCornerOfRectangle.js +0 -20
  72. package/isEntityClean.js +0 -15
  73. package/isTruthy.js +0 -12
  74. package/isValueEmpty.js +0 -3
  75. package/itemUpload.js +0 -84
  76. package/menuUtils.js +0 -433
  77. package/popoverOverflowModifiers.js +0 -11
  78. package/primarySelectedValue.js +0 -1
  79. package/pureNoFunc.js +0 -31
  80. package/queryParams.js +0 -336
  81. package/removeCleanRows.js +0 -22
  82. package/renderOnDoc.js +0 -32
  83. package/rerenderOnWindowResize.js +0 -26
  84. package/rowClick.js +0 -181
  85. package/selection.js +0 -8
  86. package/showAppSpinner.js +0 -12
  87. package/showDialogOnDocBody.js +0 -33
  88. package/showProgressToast.js +0 -22
  89. package/simplifyHasuraWhere.js +0 -80
  90. package/sortify.js +0 -73
  91. package/style.css +0 -29
  92. package/tableQueryParamsToHasuraClauses.js +0 -113
  93. package/tagUtils.js +0 -45
  94. package/tgFormValues.js +0 -35
  95. package/tg_modalState.js +0 -47
  96. package/throwFormError.js +0 -16
  97. package/toastr.js +0 -148
  98. package/tryToMatchSchemas.js +0 -264
  99. package/typeToCommonType.js +0 -6
  100. package/useDeepEqualMemo.js +0 -15
  101. package/useDialog.js +0 -63
  102. package/useStableReference.js +0 -9
  103. package/useTableEntities.js +0 -38
  104. package/useTraceUpdate.js +0 -19
  105. package/utils.js +0 -37
  106. package/validateTableWideErrors.js +0 -160
  107. package/viewColumn.js +0 -97
  108. package/withField.js +0 -20
  109. package/withFields.js +0 -11
  110. package/withLocalStorage.js +0 -11
  111. package/withSelectTableRecords.js +0 -43
  112. package/withSelectedEntities.js +0 -65
  113. package/withStore.js +0 -10
  114. package/withTableParams.js +0 -288
  115. package/wrapDialog.js +0 -116
package/Uploader.js DELETED
@@ -1,1278 +0,0 @@
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 &nbsp;
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;