@teselagen/ui 0.0.11 → 0.0.12

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