@teselagen/ui 0.3.7 → 0.3.9

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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@teselagen/ui",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "main": "./src/index.js",
5
5
  "dependencies": {
6
- "@teselagen/file-utils": "0.3.7",
6
+ "@teselagen/file-utils": "0.3.9",
7
7
  "@teselagen/bounce-loader": "0.3.7",
8
8
  "@blueprintjs/core": "3.52.0",
9
9
  "@blueprintjs/datetime": "3.23.19",
@@ -12,6 +12,7 @@
12
12
  "@teselagen/react-table": "^6.10.11",
13
13
  "axios": "^0.21.1",
14
14
  "bluebird": "3.7.2",
15
+ "buffer": "5.7.1",
15
16
  "color": "^3.2.1",
16
17
  "copy-to-clipboard": "^3.3.1",
17
18
  "dayjs": "^1.10.4",
@@ -14,13 +14,14 @@ export function CellDragHandle({
14
14
  const rowsToSelect = useRef();
15
15
  const rectangleCellPaths = useRef();
16
16
 
17
- const handleDrag = useRef(e => {
17
+ const handleDrag = useRef((e) => {
18
18
  const table = ReactDOM.findDOMNode(thisTable).querySelector(".rt-table");
19
19
  const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`);
20
20
  const [rowId, path] = cellId.split(":");
21
21
  const selectedTr = table.querySelector(
22
22
  `.rt-tr-group.with-row-data[data-test-id="${rowId}"]`
23
23
  );
24
+ if (!selectedTr) return;
24
25
  const selectedIndex = selectedTr.dataset.index;
25
26
 
26
27
  if (selectedTr && trs.length) {
@@ -59,7 +60,7 @@ export function CellDragHandle({
59
60
  //add dashed borders
60
61
 
61
62
  if (rectangleCellPaths.current) {
62
- rectangleCellPaths.current.forEach(path => {
63
+ rectangleCellPaths.current.forEach((path) => {
63
64
  changeDashedBorder(path, true);
64
65
  });
65
66
  } else {
@@ -69,7 +70,7 @@ export function CellDragHandle({
69
70
  }
70
71
  if (!isSelectedForUpdate) {
71
72
  if (rectangleCellPaths.current) {
72
- rectangleCellPaths.current.forEach(path => {
73
+ rectangleCellPaths.current.forEach((path) => {
73
74
  changeDashedBorder(path, false);
74
75
  });
75
76
  } else {
@@ -86,9 +87,9 @@ export function CellDragHandle({
86
87
  const trs = table.querySelectorAll(`.rt-tr-group.with-row-data`);
87
88
  const [, path] = cellId.split(":");
88
89
  //remove the dashed borders
89
- forEach(trs, tr => {
90
+ forEach(trs, (tr) => {
90
91
  if (rectangleCellPaths.current) {
91
- rectangleCellPaths.current.forEach(path => {
92
+ rectangleCellPaths.current.forEach((path) => {
92
93
  const el = tr.querySelector(`[data-test="tgCell_${path}"]`);
93
94
  el.parentNode.classList.remove("selectedForUpdate");
94
95
  });
@@ -100,9 +101,9 @@ export function CellDragHandle({
100
101
  document.removeEventListener("mousemove", handleDrag.current, false);
101
102
  document.removeEventListener("mouseup", mouseup.current, false);
102
103
  onDragEnd(
103
- flatMap(rowsToSelect.current, id => {
104
+ flatMap(rowsToSelect.current, (id) => {
104
105
  if (rectangleCellPaths.current) {
105
- return rectangleCellPaths.current.map(path => {
106
+ return rectangleCellPaths.current.map((path) => {
106
107
  return `${id}:${path}`;
107
108
  });
108
109
  } else {
@@ -114,7 +115,7 @@ export function CellDragHandle({
114
115
 
115
116
  return (
116
117
  <div
117
- onMouseDown={e => {
118
+ onMouseDown={(e) => {
118
119
  rowsToSelect.current = [];
119
120
  xStart.current = e.clientX;
120
121
  const { isRect, selectedPaths } = isSelectionARectangle();
@@ -64,7 +64,7 @@ const helperSchema = [
64
64
  {
65
65
  column: undefined,
66
66
  type: String,
67
- value: student => student,
67
+ value: (student) => student,
68
68
  width: 200
69
69
  }
70
70
  ];
@@ -85,7 +85,7 @@ class ValidateAgainstSchema {
85
85
  }
86
86
  const schema = convertSchema(newValidateAgainstSchema);
87
87
  if (
88
- schema.fields.some(f => {
88
+ schema.fields.some((f) => {
89
89
  if (f.path === "id") {
90
90
  return true;
91
91
  }
@@ -117,7 +117,6 @@ class ValidateAgainstSchema {
117
117
 
118
118
  // validateAgainstSchema.fields = ["hahah"];
119
119
 
120
-
121
120
  // wink wink
122
121
  const emptyPromise = Promise.resolve.bind(Promise);
123
122
 
@@ -156,11 +155,11 @@ function UploaderInner({
156
155
  const validateAgainstSchemaStore = useRef(new ValidateAgainstSchema());
157
156
  const callout =
158
157
  _callout ||
159
- (isArray(_accept) ? _accept : [_accept]).find?.(a => a?.callout)?.callout;
158
+ (isArray(_accept) ? _accept : [_accept]).find?.((a) => a?.callout)?.callout;
160
159
  const validateAgainstSchemaToUse =
161
160
  _validateAgainstSchema ||
162
161
  (isArray(_accept) ? _accept : [_accept]).find?.(
163
- a => a?.validateAgainstSchema
162
+ (a) => a?.validateAgainstSchema
164
163
  )?.validateAgainstSchema;
165
164
 
166
165
  useEffect(() => {
@@ -179,11 +178,11 @@ function UploaderInner({
179
178
  ? [_accept]
180
179
  : isArray(_accept)
181
180
  ? _accept
182
- : _accept.split(",").map(a => ({ type: a }));
181
+ : _accept.split(",").map((a) => ({ type: a }));
183
182
  if (
184
183
  validateAgainstSchemaStore.current &&
185
184
  accept &&
186
- !accept.some(a => a.type === "zip")
185
+ !accept.some((a) => a.type === "zip")
187
186
  ) {
188
187
  accept?.unshift({
189
188
  type: "zip",
@@ -196,15 +195,13 @@ function UploaderInner({
196
195
  const { showDialogPromise: showUploadCsvWizardDialog, comp } = useDialog({
197
196
  ModalComponent: UploadCsvWizardDialog
198
197
  });
199
- const {
200
- showDialogPromise: showSimpleInsertDataDialog,
201
- comp: comp2
202
- } = useDialog({
203
- ModalComponent: SimpleInsertDataDialog
204
- });
198
+ const { showDialogPromise: showSimpleInsertDataDialog, comp: comp2 } =
199
+ useDialog({
200
+ ModalComponent: SimpleInsertDataDialog
201
+ });
205
202
 
206
203
  function cleanupFiles() {
207
- filesToClean.current.forEach(file => URL.revokeObjectURL(file.preview));
204
+ filesToClean.current.forEach((file) => URL.revokeObjectURL(file.preview));
208
205
  }
209
206
  useEffect(() => {
210
207
  return () => {
@@ -221,15 +218,15 @@ function UploaderInner({
221
218
  let advancedAccept;
222
219
 
223
220
  if (Array.isArray(accept)) {
224
- if (accept.some(a => isPlainObject(a))) {
221
+ if (accept.some((a) => isPlainObject(a))) {
225
222
  //advanced accept
226
223
  advancedAccept = accept;
227
- simpleAccept = flatMap(accept, a => {
224
+ simpleAccept = flatMap(accept, (a) => {
228
225
  if (a.validateAgainstSchema) {
229
226
  if (!a.type) {
230
227
  a.type = [".csv", ".xlsx"];
231
228
  }
232
- handleManuallyEnterData = async e => {
229
+ handleManuallyEnterData = async (e) => {
233
230
  e.stopPropagation();
234
231
  const { newEntities } = await showSimpleInsertDataDialog(
235
232
  "onSimpleInsertDialogFinish",
@@ -286,37 +283,37 @@ function UploaderInner({
286
283
 
287
284
  const handleDownloadXlsxFile = async () => {
288
285
  const dataDictionarySchema = [
289
- { value: f => f.displayName || f.path, column: `Column Name` },
286
+ { value: (f) => f.displayName || f.path, column: `Column Name` },
287
+ // {
288
+ // value: f => f.isUnique ? "Unique" : "",
289
+ // column: `Unique?`
290
+ // },
290
291
  {
291
- value: f => f.isUnique,
292
- column: `Unique`
292
+ value: (f) => (f.isRequired ? "Required" : "Optional"),
293
+ column: `Required?`
293
294
  },
294
295
  {
295
- value: f => f.isRequired,
296
- column: `Required`,
297
- type: Boolean
298
- },
299
- {
300
- value: f => f.type || "text",
296
+ value: (f) =>
297
+ f.type === "dropdown" ? "text" : f.type || "text",
301
298
  column: `Data Type`
302
299
  },
303
300
  {
304
- value: f => f.description,
301
+ value: (f) => f.description,
305
302
  column: `Notes`
306
303
  },
307
304
  {
308
- value: f => f.example || f.defaultValue || "",
305
+ value: (f) => f.example || f.defaultValue || "",
309
306
  column: `Example Data`
310
307
  }
311
308
  ];
312
309
 
313
310
  const mainExampleData = {};
314
- const mainSchema = a.validateAgainstSchema.fields.map(f => {
311
+ const mainSchema = a.validateAgainstSchema.fields.map((f) => {
315
312
  mainExampleData[f.displayName || f.path] =
316
313
  f.example || f.defaultValue;
317
314
  return {
318
315
  column: f.displayName || f.path,
319
- value: v => {
316
+ value: (v) => {
320
317
  return v[f.displayName || f.path];
321
318
  }
322
319
  };
@@ -342,12 +339,12 @@ function UploaderInner({
342
339
  exampleFile: () => {
343
340
  const rows = [];
344
341
  rows.push(
345
- a.validateAgainstSchema.fields.map(f => {
342
+ a.validateAgainstSchema.fields.map((f) => {
346
343
  return `${f.displayName || f.path}`;
347
344
  })
348
345
  );
349
346
  rows.push(
350
- a.validateAgainstSchema.fields.map(f => {
347
+ a.validateAgainstSchema.fields.map((f) => {
351
348
  return `${f.example || f.defaultValue || ""}`;
352
349
  })
353
350
  );
@@ -414,16 +411,16 @@ function UploaderInner({
414
411
  const responses = [];
415
412
 
416
413
  await Promise.all(
417
- acceptedFiles.map(fileToUpload => {
414
+ acceptedFiles.map((fileToUpload) => {
418
415
  const data = new FormData();
419
416
  data.append("file", fileToUpload);
420
417
 
421
418
  return axiosInstance
422
419
  .post(action, data)
423
- .then(function(res) {
420
+ .then(function (res) {
424
421
  responses.push(res.data && res.data[0]);
425
422
  onFileSuccess(res.data[0]).then(() => {
426
- cleanedFileList = cleanedFileList.map(file => {
423
+ cleanedFileList = cleanedFileList.map((file) => {
427
424
  const fileToReturn = {
428
425
  ...file,
429
426
  ...res.data[0]
@@ -436,13 +433,13 @@ function UploaderInner({
436
433
  onChange(cleanedFileList);
437
434
  });
438
435
  })
439
- .catch(function(err) {
436
+ .catch(function (err) {
440
437
  console.error("Error uploading file:", err);
441
438
  responses.push({
442
439
  ...fileToUpload,
443
440
  error: err && err.msg ? err.msg : err
444
441
  });
445
- cleanedFileList = cleanedFileList.map(file => {
442
+ cleanedFileList = cleanedFileList.map((file) => {
446
443
  const fileToReturn = { ...file };
447
444
  if (fileToReturn.id === fileToUpload.id) {
448
445
  fileToReturn.loading = false;
@@ -458,7 +455,7 @@ function UploaderInner({
458
455
  }
459
456
  } else {
460
457
  onChange(
461
- cleanedFileList.map(function(file) {
458
+ cleanedFileList.map(function (file) {
462
459
  return {
463
460
  ...file,
464
461
  loading: false
@@ -589,7 +586,7 @@ function UploaderInner({
589
586
  : [a.type]
590
587
  : [a]
591
588
  )
592
- .map(t => {
589
+ .map((t) => {
593
590
  if (!t.startsWith) {
594
591
  console.error(`Missing type here:`, a);
595
592
  throw new Error(
@@ -627,13 +624,13 @@ function UploaderInner({
627
624
  )}
628
625
  <Dropzone
629
626
  disabled={disabled}
630
- onClick={evt => evt.preventDefault()}
627
+ onClick={(evt) => evt.preventDefault()}
631
628
  multiple={fileLimit !== 1}
632
629
  accept={
633
630
  simpleAccept
634
631
  ? simpleAccept
635
632
  .split(", ")
636
- .map(a => (a.startsWith(".") ? a : "." + a))
633
+ .map((a) => (a.startsWith(".") ? a : "." + a))
637
634
  .join(", ")
638
635
  : undefined
639
636
  }
@@ -646,9 +643,9 @@ function UploaderInner({
646
643
  file,
647
644
  simpleAccept
648
645
  ?.split(", ")
649
- ?.map(a => (a.startsWith(".") ? a : "." + a)) || []
646
+ ?.map((a) => (a.startsWith(".") ? a : "." + a)) || []
650
647
  );
651
- acceptedFiles.push(...files.map(f => f.originFileObj));
648
+ acceptedFiles.push(...files.map((f) => f.originFileObj));
652
649
  } else {
653
650
  acceptedFiles.push(file);
654
651
  }
@@ -656,11 +653,11 @@ function UploaderInner({
656
653
  cleanupFiles();
657
654
  if (rejectedFiles.length) {
658
655
  let msg = "";
659
- rejectedFiles.forEach(file => {
656
+ rejectedFiles.forEach((file) => {
660
657
  if (msg) msg += "\n";
661
658
  msg +=
662
659
  `${file.file.name}: ` +
663
- file.errors.map(err => err.message).join(", ");
660
+ file.errors.map((err) => err.message).join(", ");
664
661
  });
665
662
  window.toastr &&
666
663
  window.toastr.warning(
@@ -673,7 +670,7 @@ function UploaderInner({
673
670
  acceptedFiles = acceptedFiles.slice(0, fileLimit);
674
671
  }
675
672
 
676
- acceptedFiles.forEach(file => {
673
+ acceptedFiles.forEach((file) => {
677
674
  file.preview = URL.createObjectURL(file);
678
675
  file.loading = true;
679
676
  if (!file.id) {
@@ -684,15 +681,15 @@ function UploaderInner({
684
681
 
685
682
  if (readBeforeUpload) {
686
683
  acceptedFiles = await Promise.all(
687
- acceptedFiles.map(file => {
684
+ acceptedFiles.map((file) => {
688
685
  return new Promise((resolve, reject) => {
689
686
  const reader = new FileReader();
690
687
  reader.readAsText(file, "UTF-8");
691
- reader.onload = evt => {
688
+ reader.onload = (evt) => {
692
689
  file.parsedString = evt.target.result;
693
690
  resolve(file);
694
691
  };
695
- reader.onerror = err => {
692
+ reader.onerror = (err) => {
696
693
  console.error("err:", err);
697
694
  reject(err);
698
695
  };
@@ -700,7 +697,7 @@ function UploaderInner({
700
697
  })
701
698
  );
702
699
  }
703
- const cleanedAccepted = acceptedFiles.map(file => {
700
+ const cleanedAccepted = acceptedFiles.map((file) => {
704
701
  return {
705
702
  originFileObj: file,
706
703
  originalFileObj: file,
@@ -717,11 +714,8 @@ function UploaderInner({
717
714
  : {})
718
715
  };
719
716
  });
720
- const cleanedFileList = [
721
- ...cleanedAccepted,
722
- ...fileListToUse
723
- ].slice(0, fileLimit ? fileLimit : undefined);
724
717
 
718
+ const toKeep = [];
725
719
  if (validateAgainstSchema) {
726
720
  const filesWIssues = [];
727
721
  const filesWOIssues = [];
@@ -734,8 +728,9 @@ function UploaderInner({
734
728
  console.error("error:", error);
735
729
  window.toastr &&
736
730
  window.toastr.error(
737
- `There was an error parsing your file. Please try again. ${error.message ||
738
- error}`
731
+ `There was an error parsing your file. Please try again. ${
732
+ error.message || error
733
+ }`
739
734
  );
740
735
  return;
741
736
  }
@@ -755,91 +750,91 @@ function UploaderInner({
755
750
  userSchema,
756
751
  parsedF.data
757
752
  );
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
- )}`
753
+ } else {
754
+ toKeep.push(file);
755
+ let csvValidationIssue = _csvValidationIssue;
756
+ if (csvValidationIssue) {
757
+ if (isObject(csvValidationIssue)) {
758
+ initializeForm(
759
+ `editableCellTable${
760
+ cleanedAccepted.length > 1 ? `-${i}` : ""
761
+ }`,
762
+ {
763
+ reduxFormCellValidation: csvValidationIssue
764
+ },
765
+ {
766
+ keepDirty: true,
767
+ keepValues: true,
768
+ updateUnregisteredFields: true
769
+ }
792
770
  );
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>
771
+ const err = Object.values(csvValidationIssue)[0];
772
+ // csvValidationIssue = `It looks like there was an error with your data - \n\n${
773
+ // err && err.message ? err.message : err
774
+ // }.\n\nPlease review your headers and then correct any errors on the next page.`; //pass just the first error as a string
775
+ const errMsg =
776
+ err && err.message ? err.message : err;
777
+ if (isPlainObject(errMsg)) {
778
+ throw new Error(
779
+ `errMsg is an object ${JSON.stringify(
780
+ errMsg,
781
+ null,
782
+ 4
783
+ )}`
784
+ );
785
+ }
786
+ csvValidationIssue = (
801
787
  <div>
802
- Please review your headers and then correct any
803
- errors on the next page.
788
+ <div>
789
+ It looks like there was an error with your
790
+ data (Correct on the Review Data page):
791
+ </div>
792
+ <div style={{ color: "red" }}>{errMsg}</div>
793
+ <div>
794
+ Please review your headers and then correct
795
+ any errors on the next page.
796
+ </div>
804
797
  </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`;
798
+ );
799
+ }
800
+ filesWIssues.push({
801
+ file,
802
+ csvValidationIssue,
803
+ matchedHeaders,
804
+ userSchema,
805
+ searchResults
806
+ });
807
+ } else {
808
+ filesWOIssues.push({
809
+ file,
810
+ csvValidationIssue,
811
+ matchedHeaders,
812
+ userSchema,
813
+ searchResults
814
+ });
815
+ const newFileName = removeExt(file.name) + `.csv`;
824
816
 
825
- const { newFile, cleanedEntities } = getNewCsvFile(
826
- userSchema.userData,
827
- newFileName
828
- );
817
+ const { newFile, cleanedEntities } = getNewCsvFile(
818
+ userSchema.userData,
819
+ newFileName
820
+ );
829
821
 
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;
822
+ file.meta = parsedF.meta;
823
+ file.hasEditClick = true;
824
+ file.parsedData = cleanedEntities;
825
+ file.name = newFileName;
826
+ file.originFileObj = newFile;
827
+ file.originalFileObj = newFile;
828
+ }
836
829
  }
830
+ } else {
831
+ toKeep.push(file);
837
832
  }
838
833
  }
839
834
  if (filesWIssues.length) {
840
835
  const { file } = filesWIssues[0];
841
836
  const allFiles = [...filesWIssues, ...filesWOIssues];
842
- const doAllFilesHaveSameHeaders = allFiles.every(f => {
837
+ const doAllFilesHaveSameHeaders = allFiles.every((f) => {
843
838
  if (f.userSchema.fields && f.userSchema.fields.length) {
844
839
  return f.userSchema.fields.every((h, i) => {
845
840
  return (
@@ -899,6 +894,18 @@ function UploaderInner({
899
894
  }
900
895
  }
901
896
 
897
+ if (toKeep.length === 0) {
898
+ console.log(`asdfasdfas`);
899
+ window.toastr &&
900
+ window.toastr.error(
901
+ `It looks like there wasn't any data in your file. Please add some data and try again`
902
+ );
903
+ }
904
+ const cleanedFileList = [...toKeep, ...fileListToUse].slice(
905
+ 0,
906
+ fileLimit ? fileLimit : undefined
907
+ );
908
+
902
909
  handleSecondHalfOfUpload({ acceptedFiles, cleanedFileList });
903
910
  }
904
911
  }}
@@ -1050,29 +1057,26 @@ function UploaderInner({
1050
1057
  validateAgainstSchema
1051
1058
  });
1052
1059
 
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
- );
1060
+ const { newEntities } =
1061
+ await showSimpleInsertDataDialog(
1062
+ "onSimpleInsertDialogFinish",
1063
+ {
1064
+ dialogProps: {
1065
+ title: "Edit Data"
1066
+ },
1067
+ validateAgainstSchema,
1068
+ isEditingExistingFile: true,
1069
+ searchResults,
1070
+ matchedHeaders,
1071
+ userSchema
1072
+ }
1073
+ );
1068
1074
 
1069
1075
  if (!newEntities) {
1070
1076
  return;
1071
1077
  } else {
1072
- const {
1073
- newFile,
1074
- cleanedEntities
1075
- } = getNewCsvFile(newEntities, file.name);
1078
+ const { newFile, cleanedEntities } =
1079
+ getNewCsvFile(newEntities, file.name);
1076
1080
  // file.parsedData = newEntities;
1077
1081
  Object.assign(file, {
1078
1082
  ...newFile,
@@ -1187,7 +1191,7 @@ function getNewCsvFile(ents, fileName) {
1187
1191
  }
1188
1192
 
1189
1193
  function stripId(ents = []) {
1190
- return ents.map(ent => {
1194
+ return ents.map((ent) => {
1191
1195
  const { id, ...rest } = ent;
1192
1196
  return rest;
1193
1197
  });