@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.
- package/README.md +7 -0
- package/cypress.config.ts +6 -0
- package/index.html +12 -0
- package/package.json +2 -2
- package/project.json +74 -0
- package/src/AdvancedOptions.js +33 -0
- package/src/AdvancedOptions.spec.js +24 -0
- package/src/AssignDefaultsModeContext.js +21 -0
- package/src/AsyncValidateFieldSpinner/index.js +12 -0
- package/src/BlueprintError/index.js +14 -0
- package/src/BounceLoader/index.js +16 -0
- package/src/BounceLoader/style.css +45 -0
- package/src/CollapsibleCard/index.js +92 -0
- package/src/CollapsibleCard/style.css +21 -0
- package/src/DNALoader/index.js +20 -0
- package/src/DNALoader/style.css +251 -0
- package/src/DataTable/CellDragHandle.js +130 -0
- package/src/DataTable/DisabledLoadingComponent.js +15 -0
- package/src/DataTable/DisplayOptions.js +218 -0
- package/src/DataTable/FilterAndSortMenu.js +397 -0
- package/src/DataTable/PagingTool.js +232 -0
- package/src/DataTable/SearchBar.js +57 -0
- package/src/DataTable/SortableColumns.js +53 -0
- package/src/DataTable/TableFormTrackerContext.js +10 -0
- package/src/DataTable/dataTableEnhancer.js +291 -0
- package/src/DataTable/defaultFormatters.js +32 -0
- package/src/DataTable/defaultProps.js +45 -0
- package/src/DataTable/defaultValidators.js +40 -0
- package/src/DataTable/editCellHelper.js +44 -0
- package/src/DataTable/getCellVal.js +20 -0
- package/src/DataTable/getVals.js +8 -0
- package/src/DataTable/index.js +3537 -0
- package/src/DataTable/isTruthy.js +12 -0
- package/src/DataTable/isValueEmpty.js +3 -0
- package/src/DataTable/style.css +600 -0
- package/src/DataTable/utils/computePresets.js +42 -0
- package/src/DataTable/utils/convertSchema.js +69 -0
- package/src/DataTable/utils/getIdOrCodeOrIndex.js +9 -0
- package/src/DataTable/utils/getTableConfigFromStorage.js +5 -0
- package/src/DataTable/utils/queryParams.js +1032 -0
- package/src/DataTable/utils/rowClick.js +156 -0
- package/src/DataTable/utils/selection.js +8 -0
- package/src/DataTable/utils/withSelectedEntities.js +65 -0
- package/src/DataTable/utils/withTableParams.js +328 -0
- package/src/DataTable/validateTableWideErrors.js +135 -0
- package/src/DataTable/viewColumn.js +37 -0
- package/src/DialogFooter/index.js +79 -0
- package/src/DialogFooter/style.css +9 -0
- package/src/DropdownButton.js +36 -0
- package/src/FillWindow.css +6 -0
- package/src/FillWindow.js +69 -0
- package/src/FormComponents/Uploader.js +1197 -0
- package/src/FormComponents/getNewName.js +31 -0
- package/src/FormComponents/index.js +1384 -0
- package/src/FormComponents/itemUpload.js +84 -0
- package/src/FormComponents/sortify.js +73 -0
- package/src/FormComponents/style.css +247 -0
- package/src/FormComponents/tryToMatchSchemas.js +222 -0
- package/src/FormComponents/utils.js +6 -0
- package/src/HotkeysDialog/index.js +79 -0
- package/src/HotkeysDialog/style.css +54 -0
- package/src/InfoHelper/index.js +83 -0
- package/src/InfoHelper/style.css +7 -0
- package/src/IntentText/index.js +18 -0
- package/src/Loading/index.js +74 -0
- package/src/Loading/style.css +4 -0
- package/src/MatchHeaders.js +223 -0
- package/src/MenuBar/index.js +416 -0
- package/src/MenuBar/style.css +45 -0
- package/src/PromptUnsavedChanges/index.js +40 -0
- package/src/ResizableDraggableDialog/index.js +138 -0
- package/src/ResizableDraggableDialog/style.css +42 -0
- package/src/ScrollToTop/index.js +72 -0
- package/src/SimpleStepViz.js +26 -0
- package/src/TgSelect/index.js +465 -0
- package/src/TgSelect/style.css +34 -0
- package/src/TgSuggest/index.js +121 -0
- package/src/Timeline/TimelineEvent.js +31 -0
- package/src/Timeline/index.js +22 -0
- package/src/Timeline/style.css +29 -0
- package/src/UploadCsvWizard.css +4 -0
- package/src/UploadCsvWizard.js +731 -0
- package/src/autoTooltip.js +89 -0
- package/src/constants.js +1 -0
- package/src/customIcons.js +361 -0
- package/src/enhancers/withDialog/index.js +196 -0
- package/src/enhancers/withDialog/tg_modalState.js +46 -0
- package/src/enhancers/withField.js +20 -0
- package/src/enhancers/withFields.js +11 -0
- package/src/enhancers/withLocalStorage.js +11 -0
- package/src/index.js +76 -0
- package/src/rerenderOnWindowResize.js +27 -0
- package/src/showAppSpinner.js +12 -0
- package/src/showConfirmationDialog/index.js +116 -0
- package/src/showDialogOnDocBody.js +37 -0
- package/src/style.css +214 -0
- package/src/toastr.js +92 -0
- package/src/typeToCommonType.js +6 -0
- package/src/useDialog.js +64 -0
- package/src/utils/S3Download.js +14 -0
- package/src/utils/adHoc.js +10 -0
- package/src/utils/basicHandleActionsWithFullState.js +14 -0
- package/src/utils/combineReducersWithFullState.js +14 -0
- package/src/utils/commandControls.js +83 -0
- package/src/utils/commandUtils.js +112 -0
- package/src/utils/determineBlackOrWhiteTextColor.js +4 -0
- package/src/utils/getDayjsFormatter.js +35 -0
- package/src/utils/getTextFromEl.js +28 -0
- package/src/utils/handlerHelpers.js +30 -0
- package/src/utils/hotkeyUtils.js +129 -0
- package/src/utils/menuUtils.js +402 -0
- package/src/utils/popoverOverflowModifiers.js +11 -0
- package/src/utils/pureNoFunc.js +31 -0
- package/src/utils/renderOnDoc.js +29 -0
- package/src/utils/showProgressToast.js +22 -0
- package/src/utils/tagUtils.js +45 -0
- package/src/utils/tgFormValues.js +32 -0
- package/src/utils/withSelectTableRecords.js +38 -0
- package/src/utils/withStore.js +10 -0
- package/src/wrapDialog.js +112 -0
- package/tsconfig.json +4 -0
- package/vite.config.ts +7 -0
- package/index.mjs +0 -109378
- package/index.umd.js +0 -109381
- 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
|
|
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";
|