@formio/js 5.0.0-rc.33 → 5.0.0-rc.35

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. package/dist/fonts/bootstrap-icons.woff +0 -0
  2. package/dist/fonts/bootstrap-icons.woff2 +0 -0
  3. package/dist/formio.builder.css +19 -15
  4. package/dist/formio.builder.min.css +1 -1
  5. package/dist/formio.embed.js +1 -1
  6. package/dist/formio.embed.min.js +1 -1
  7. package/dist/formio.embed.min.js.LICENSE.txt +1 -1
  8. package/dist/formio.form.css +19 -15
  9. package/dist/formio.form.js +496 -547
  10. package/dist/formio.form.min.css +1 -1
  11. package/dist/formio.form.min.js +1 -1
  12. package/dist/formio.form.min.js.LICENSE.txt +2 -2
  13. package/dist/formio.full.css +363 -19
  14. package/dist/formio.full.js +497 -548
  15. package/dist/formio.full.min.css +3 -3
  16. package/dist/formio.full.min.js +1 -1
  17. package/dist/formio.full.min.js.LICENSE.txt +2 -2
  18. package/dist/formio.js +67 -77
  19. package/dist/formio.min.js +1 -1
  20. package/dist/formio.min.js.LICENSE.txt +1 -1
  21. package/dist/formio.utils.js +81 -101
  22. package/dist/formio.utils.min.js +1 -1
  23. package/dist/formio.utils.min.js.LICENSE.txt +1 -1
  24. package/lib/cjs/CDN.js +12 -6
  25. package/lib/cjs/Webform.js +4 -1
  26. package/lib/cjs/Wizard.js +6 -9
  27. package/lib/cjs/components/_classes/component/Component.js +6 -1
  28. package/lib/cjs/components/_classes/nested/NestedComponent.js +1 -1
  29. package/lib/cjs/components/container/fixtures/comp4.js +45 -0
  30. package/lib/cjs/components/container/fixtures/index.js +3 -1
  31. package/lib/cjs/components/datetime/DateTime.js +6 -0
  32. package/lib/cjs/components/file/File.js +465 -215
  33. package/lib/cjs/components/file/editForm/File.edit.display.js +17 -0
  34. package/lib/cjs/components/textarea/TextArea.js +2 -2
  35. package/lib/cjs/components/textfield/TextField.js +3 -1
  36. package/lib/cjs/components/time/Time.js +3 -0
  37. package/lib/cjs/providers/storage/azure.js +6 -1
  38. package/lib/cjs/providers/storage/base64.js +1 -1
  39. package/lib/cjs/providers/storage/googleDrive.js +5 -1
  40. package/lib/cjs/providers/storage/indexeddb.js +1 -1
  41. package/lib/cjs/providers/storage/s3.js +5 -1
  42. package/lib/cjs/providers/storage/xhr.js +10 -0
  43. package/lib/mjs/CDN.js +12 -6
  44. package/lib/mjs/Webform.js +4 -1
  45. package/lib/mjs/Wizard.js +6 -9
  46. package/lib/mjs/components/_classes/component/Component.js +6 -1
  47. package/lib/mjs/components/_classes/nested/NestedComponent.js +1 -1
  48. package/lib/mjs/components/container/fixtures/comp4.js +43 -0
  49. package/lib/mjs/components/container/fixtures/index.js +2 -1
  50. package/lib/mjs/components/datetime/DateTime.js +6 -0
  51. package/lib/mjs/components/file/File.js +463 -224
  52. package/lib/mjs/components/file/editForm/File.edit.display.js +17 -0
  53. package/lib/mjs/components/textarea/TextArea.js +2 -2
  54. package/lib/mjs/components/textfield/TextField.js +6 -0
  55. package/lib/mjs/components/time/Time.js +3 -0
  56. package/lib/mjs/providers/storage/azure.js +6 -1
  57. package/lib/mjs/providers/storage/base64.js +1 -1
  58. package/lib/mjs/providers/storage/googleDrive.js +5 -1
  59. package/lib/mjs/providers/storage/indexeddb.js +1 -1
  60. package/lib/mjs/providers/storage/s3.js +5 -1
  61. package/lib/mjs/providers/storage/xhr.js +10 -0
  62. package/package.json +3 -3
@@ -28,6 +28,7 @@ if (htmlCanvasElement && !htmlCanvasElement.prototype.toBlob) {
28
28
  }
29
29
  });
30
30
  }
31
+ const createRandomString = () => Math.random().toString(36).substring(2, 15);
31
32
  export default class FileComponent extends Field {
32
33
  static schema(...extend) {
33
34
  return Field.schema({
@@ -79,8 +80,13 @@ export default class FileComponent extends Field {
79
80
  progress: progressSupported,
80
81
  };
81
82
  this.cameraMode = false;
82
- this.statuses = [];
83
83
  this.fileDropHidden = false;
84
+ this.filesToSync = {
85
+ filesToUpload: [],
86
+ filesToDelete: [],
87
+ };
88
+ this.isSyncing = false;
89
+ this.abortUploads = [];
84
90
  }
85
91
  get dataReady() {
86
92
  return this.filesReady || Promise.resolve();
@@ -125,14 +131,37 @@ export default class FileComponent extends Field {
125
131
  }
126
132
  this._fileBrowseHidden = value;
127
133
  }
134
+ get shouldSyncFiles() {
135
+ return Boolean(this.filesToSync.filesToDelete.length || this.filesToSync.filesToUpload.length);
136
+ }
137
+ get autoSync() {
138
+ return _.get(this, 'component.autoSync', false);
139
+ }
140
+ get columnsSize() {
141
+ const actionsColumn = this.disabled ? 0 : this.autoSync ? 2 : 1;
142
+ const typeColumn = this.hasTypes ? 2 : 0;
143
+ const sizeColumn = 2;
144
+ const nameColumn = 12 - actionsColumn - typeColumn - sizeColumn;
145
+ return {
146
+ name: nameColumn,
147
+ size: sizeColumn,
148
+ type: typeColumn,
149
+ actions: actionsColumn,
150
+ };
151
+ }
128
152
  render() {
153
+ const { filesToDelete, filesToUpload } = this.filesToSync;
129
154
  return super.render(this.renderTemplate('file', {
130
155
  fileSize: this.fileSize,
131
156
  files: this.dataValue || [],
132
- statuses: this.statuses,
157
+ filesToDelete,
158
+ filesToUpload,
133
159
  disabled: this.disabled,
134
160
  support: this.support,
135
- fileDropHidden: this.fileDropHidden
161
+ fileDropHidden: this.fileDropHidden,
162
+ showSyncButton: this.autoSync && (filesToDelete.length || filesToUpload.length),
163
+ isSyncing: this.isSyncing,
164
+ columns: this.columnsSize,
136
165
  }));
137
166
  }
138
167
  getVideoStream(constraints) {
@@ -198,7 +227,7 @@ export default class FileComponent extends Field {
198
227
  this.getFrame(videoPlayer)
199
228
  .then((frame) => {
200
229
  frame.name = `photo-${Date.now()}.png`;
201
- this.upload([frame]);
230
+ this.handleFilesToUpload([frame]);
202
231
  this.cameraMode = false;
203
232
  this.redraw();
204
233
  });
@@ -280,20 +309,10 @@ export default class FileComponent extends Field {
280
309
  }
281
310
  return options;
282
311
  }
283
- deleteFile(fileInfo) {
284
- const { options = {} } = this.component;
285
- if (fileInfo && (['url', 'indexeddb'].includes(this.component.storage))) {
286
- const { fileService } = this;
287
- if (fileService && typeof fileService.deleteFile === 'function') {
288
- fileService.deleteFile(fileInfo, options);
289
- }
290
- else {
291
- const formio = this.options.formio || (this.root && this.root.formio);
292
- if (formio) {
293
- formio.makeRequest('', fileInfo.url, 'delete');
294
- }
295
- }
296
- }
312
+ get actions() {
313
+ return {
314
+ abort: this.abortRequest.bind(this),
315
+ };
297
316
  }
298
317
  attach(element) {
299
318
  this.loadRefs(element, {
@@ -306,19 +325,22 @@ export default class FileComponent extends Field {
306
325
  videoPlayer: 'single',
307
326
  fileLink: 'multiple',
308
327
  removeLink: 'multiple',
309
- fileStatusRemove: 'multiple',
328
+ fileToSyncRemove: 'multiple',
310
329
  fileImage: 'multiple',
311
330
  fileType: 'multiple',
312
331
  fileProcessingLoader: 'single',
332
+ syncNow: 'single',
333
+ restoreFile: 'multiple',
334
+ progress: 'multiple',
313
335
  });
314
336
  // Ensure we have an empty input refs. We need this for the setValue method to redraw the control when it is set.
315
337
  this.refs.input = [];
316
338
  const superAttach = super.attach(element);
317
339
  if (this.refs.fileDrop) {
318
- if (!this.statuses.length) {
319
- this.refs.fileDrop.removeAttribute('hidden');
320
- }
321
- const element = this;
340
+ // if (!this.statuses.length) {
341
+ // this.refs.fileDrop.removeAttribute('hidden');
342
+ // }
343
+ const _this = this;
322
344
  this.addEventListener(this.refs.fileDrop, 'dragover', function (event) {
323
345
  this.className = 'fileSelector fileDragOver';
324
346
  event.preventDefault();
@@ -330,15 +352,18 @@ export default class FileComponent extends Field {
330
352
  this.addEventListener(this.refs.fileDrop, 'drop', function (event) {
331
353
  this.className = 'fileSelector';
332
354
  event.preventDefault();
333
- element.upload(event.dataTransfer.files);
355
+ _this.handleFilesToUpload(event.dataTransfer.files);
334
356
  });
335
357
  }
358
+ this.addEventListener(element, 'click', (event) => {
359
+ this.handleAction(event);
360
+ });
336
361
  if (this.refs.fileBrowse) {
337
362
  this.addEventListener(this.refs.fileBrowse, 'click', (event) => {
338
363
  event.preventDefault();
339
364
  this.browseFiles(this.browseOptions)
340
365
  .then((files) => {
341
- this.upload(files);
366
+ this.handleFilesToUpload(files);
342
367
  });
343
368
  });
344
369
  }
@@ -350,22 +375,27 @@ export default class FileComponent extends Field {
350
375
  });
351
376
  this.refs.removeLink.forEach((removeLink, index) => {
352
377
  this.addEventListener(removeLink, 'click', (event) => {
378
+ event.preventDefault();
353
379
  const fileInfo = this.dataValue[index];
354
- this.deleteFile(fileInfo);
380
+ this.handleFileToRemove(fileInfo);
381
+ });
382
+ });
383
+ this.refs.fileToSyncRemove.forEach((fileToSyncRemove, index) => {
384
+ this.addEventListener(fileToSyncRemove, 'click', (event) => {
355
385
  event.preventDefault();
356
- this.splice(index);
386
+ this.filesToSync.filesToUpload.splice(index, 1);
357
387
  this.redraw();
358
388
  });
359
389
  });
360
- this.refs.fileStatusRemove.forEach((fileStatusRemove, index) => {
361
- this.addEventListener(fileStatusRemove, 'click', (event) => {
390
+ this.refs.restoreFile.forEach((fileToRestore, index) => {
391
+ this.addEventListener(fileToRestore, 'click', (event) => {
362
392
  event.preventDefault();
363
- const fileUpload = this.statuses[index];
364
- _.pull(this.filesUploading, fileUpload.originalName);
365
- if (fileUpload.abort) {
366
- fileUpload.abort();
367
- }
368
- this.statuses.splice(index, 1);
393
+ const fileInfo = this.filesToSync.filesToDelete[index];
394
+ delete fileInfo.status;
395
+ delete fileInfo.message;
396
+ this.filesToSync.filesToDelete.splice(index, 1);
397
+ this.dataValue.push(fileInfo);
398
+ this.triggerChange();
369
399
  this.redraw();
370
400
  });
371
401
  });
@@ -379,7 +409,7 @@ export default class FileComponent extends Field {
379
409
  reader.onloadend = (evt) => {
380
410
  const blob = new Blob([new Uint8Array(evt.target.result)], { type: file.type });
381
411
  blob.name = file.name;
382
- this.upload([blob]);
412
+ this.handleFilesToUpload([blob]);
383
413
  };
384
414
  reader.readAsArrayBuffer(file);
385
415
  });
@@ -401,7 +431,7 @@ export default class FileComponent extends Field {
401
431
  reader.onloadend = (evt) => {
402
432
  const blob = new Blob([new Uint8Array(evt.target.result)], { type: file.type });
403
433
  blob.name = file.name;
404
- this.upload([blob]);
434
+ this.handleFilesToUpload([blob]);
405
435
  };
406
436
  reader.readAsArrayBuffer(file);
407
437
  });
@@ -431,6 +461,9 @@ export default class FileComponent extends Field {
431
461
  });
432
462
  }
433
463
  this.refs.fileType.forEach((fileType, index) => {
464
+ if (!this.dataValue[index]) {
465
+ return;
466
+ }
434
467
  this.dataValue[index].fileType = this.dataValue[index].fileType || this.component.fileTypes[0].label;
435
468
  this.addEventListener(fileType, 'change', (event) => {
436
469
  event.preventDefault();
@@ -438,6 +471,10 @@ export default class FileComponent extends Field {
438
471
  this.dataValue[index].fileType = fileType.label;
439
472
  });
440
473
  });
474
+ this.addEventListener(this.refs.syncNow, 'click', (event) => {
475
+ event.preventDefault();
476
+ this.syncFiles();
477
+ });
441
478
  const fileService = this.fileService;
442
479
  if (fileService) {
443
480
  const loadingImages = [];
@@ -553,201 +590,389 @@ export default class FileComponent extends Field {
553
590
  validateMaxSize(file, val) {
554
591
  return file.size - 0.1 <= this.translateScalars(val);
555
592
  }
556
- upload(files) {
593
+ abortRequest(id) {
594
+ const abortUpload = this.abortUploads.find(abortUpload => abortUpload.id === id);
595
+ if (abortUpload) {
596
+ abortUpload.abort();
597
+ }
598
+ }
599
+ handleAction(event) {
600
+ const target = event.target;
601
+ if (!target.id) {
602
+ return;
603
+ }
604
+ const [action, id] = target.id.split('-');
605
+ if (!action || !id || !this.actions[action]) {
606
+ return;
607
+ }
608
+ this.actions[action](id);
609
+ }
610
+ getFileName(file) {
611
+ return uniqueName(file.name, this.component.fileNameTemplate, this.evalContext());
612
+ }
613
+ getInitFileToSync(file) {
614
+ const escapedFileName = file.name ? file.name.replaceAll('<', '&lt;').replaceAll('>', '&gt;') : file.name;
615
+ return {
616
+ id: createRandomString(),
617
+ // Get a unique name for this file to keep file collisions from occurring.
618
+ dir: this.interpolate(this.component.dir || ''),
619
+ name: this.getFileName(file),
620
+ originalName: escapedFileName,
621
+ fileKey: this.component.fileKey || 'file',
622
+ storage: this.component.storage,
623
+ options: this.component.options,
624
+ file,
625
+ size: file.size,
626
+ status: 'info',
627
+ message: this.t('Processing file. Please wait...'),
628
+ hash: '',
629
+ };
630
+ }
631
+ async handleSubmissionRevisions(file) {
632
+ if (this.root.form.submissionRevisions !== 'true') {
633
+ return '';
634
+ }
635
+ const bmf = new BMF();
636
+ const hash = await new Promise((resolve, reject) => {
637
+ this.emit('fileUploadingStart');
638
+ bmf.md5(file, (err, md5) => {
639
+ if (err) {
640
+ return reject(err);
641
+ }
642
+ return resolve(md5);
643
+ });
644
+ });
645
+ this.emit('fileUploadingEnd');
646
+ return hash;
647
+ }
648
+ validateFileName(file) {
649
+ // Check if file with the same name is being uploaded
650
+ const fileWithSameNameUploading = this.filesToSync.filesToUpload
651
+ .some(fileToSync => fileToSync.file?.name === file.name);
652
+ const fileWithSameNameUploaded = this.dataValue
653
+ .some(fileStatus => fileStatus.originalName === file.name);
654
+ return fileWithSameNameUploaded || fileWithSameNameUploading
655
+ ? {
656
+ status: 'error',
657
+ message: this.t(`File with the same name is already ${fileWithSameNameUploading ? 'being ' : ''}uploaded`),
658
+ }
659
+ : {};
660
+ }
661
+ validateFileSettings(file) {
662
+ // Check file pattern
663
+ if (this.component.filePattern && !this.validatePattern(file, this.component.filePattern)) {
664
+ return {
665
+ status: 'error',
666
+ message: this.t('File is the wrong type; it must be {{ pattern }}', {
667
+ pattern: this.component.filePattern,
668
+ }),
669
+ };
670
+ }
671
+ // Check file minimum size
672
+ if (this.component.fileMinSize && !this.validateMinSize(file, this.component.fileMinSize)) {
673
+ return {
674
+ status: 'error',
675
+ message: this.t('File is too small; it must be at least {{ size }}', {
676
+ size: this.component.fileMinSize,
677
+ }),
678
+ };
679
+ }
680
+ // Check file maximum size
681
+ if (this.component.fileMaxSize && !this.validateMaxSize(file, this.component.fileMaxSize)) {
682
+ return {
683
+ status: 'error',
684
+ message: this.t('File is too big; it must be at most {{ size }}', {
685
+ size: this.component.fileMaxSize,
686
+ }),
687
+ };
688
+ }
689
+ return {};
690
+ }
691
+ validateFileService() {
692
+ const { fileService } = this;
693
+ return !fileService
694
+ ? {
695
+ status: 'error',
696
+ message: this.t('File Service not provided.'),
697
+ }
698
+ : {};
699
+ }
700
+ validateFile(file) {
701
+ const fileServiceValidation = this.validateFileService();
702
+ if (fileServiceValidation.status === 'error') {
703
+ return fileServiceValidation;
704
+ }
705
+ const fileNameValidation = this.validateFileName(file);
706
+ if (fileNameValidation.status === 'error') {
707
+ return fileNameValidation;
708
+ }
709
+ return this.validateFileSettings(file);
710
+ }
711
+ getGroupPermissions() {
712
+ let groupKey = null;
713
+ let groupPermissions = null;
714
+ //Iterate through form components to find group resource if one exists
715
+ this.root.everyComponent((element) => {
716
+ if (element.component?.submissionAccess || element.component?.defaultPermission) {
717
+ groupPermissions = !element.component.submissionAccess ? [
718
+ {
719
+ type: element.component.defaultPermission,
720
+ roles: [],
721
+ },
722
+ ] : element.component.submissionAccess;
723
+ groupPermissions.forEach((permission) => {
724
+ groupKey = ['admin', 'write', 'create'].includes(permission.type) ? element.component.key : null;
725
+ });
726
+ }
727
+ });
728
+ return { groupKey, groupPermissions };
729
+ }
730
+ async triggerFileProcessor(file) {
731
+ let processedFile = null;
732
+ if (this.root.options.fileProcessor) {
733
+ try {
734
+ if (this.refs.fileProcessingLoader) {
735
+ this.refs.fileProcessingLoader.style.display = 'block';
736
+ }
737
+ const fileProcessorHandler = fileProcessor(this.fileService, this.root.options.fileProcessor);
738
+ processedFile = await fileProcessorHandler(file, this.component.properties);
739
+ }
740
+ catch (err) {
741
+ this.fileDropHidden = false;
742
+ return {
743
+ status: 'error',
744
+ message: this.t('File processing has been failed.'),
745
+ };
746
+ }
747
+ finally {
748
+ if (this.refs.fileProcessingLoader) {
749
+ this.refs.fileProcessingLoader.style.display = 'none';
750
+ }
751
+ }
752
+ }
753
+ return {
754
+ file: processedFile,
755
+ };
756
+ }
757
+ async prepareFileToUpload(file) {
758
+ const fileToSync = this.getInitFileToSync(file);
759
+ fileToSync.hash = await this.handleSubmissionRevisions(file);
760
+ const { status, message } = this.validateFile(file);
761
+ if (status === 'error') {
762
+ fileToSync.isValidationError = true;
763
+ fileToSync.status = status;
764
+ fileToSync.message = message;
765
+ return this.filesToSync.filesToUpload.push(fileToSync);
766
+ }
767
+ if (this.component.privateDownload) {
768
+ file.private = true;
769
+ }
770
+ const { groupKey, groupPermissions } = this.getGroupPermissions();
771
+ const processedFile = await this.triggerFileProcessor(file);
772
+ if (processedFile.status === 'error') {
773
+ fileToSync.status === 'error';
774
+ fileToSync.message = processedFile.message;
775
+ return this.filesToSync.filesToUpload.push(fileToSync);
776
+ }
777
+ if (this.autoSync) {
778
+ fileToSync.message = this.t('Ready to be uploaded into storage');
779
+ }
780
+ this.filesToSync.filesToUpload.push({
781
+ ...fileToSync,
782
+ message: fileToSync.message,
783
+ file: processedFile.file || file,
784
+ url: this.interpolate(this.component.url, { file: fileToSync }),
785
+ groupPermissions,
786
+ groupResourceId: groupKey ? this.currentForm.submission.data[groupKey]._id : null,
787
+ });
788
+ }
789
+ async prepareFilesToUpload(files) {
557
790
  // Only allow one upload if not multiple.
558
791
  if (!this.component.multiple) {
559
- if (this.statuses.length) {
560
- this.statuses = [];
561
- }
562
792
  files = Array.prototype.slice.call(files, 0, 1);
563
793
  }
564
794
  if (this.component.storage && files && files.length) {
565
795
  this.fileDropHidden = true;
566
- // files is not really an array and does not have a forEach method, so fake it.
567
- /* eslint-disable max-statements */
568
- Array.prototype.forEach.call(files, async (file) => {
569
- const fileName = uniqueName(file.name, this.component.fileNameTemplate, this.evalContext());
570
- const escapedFileName = file.name ? file.name.replaceAll('<', '&lt;').replaceAll('>', '&gt;') : file.name;
571
- const fileUpload = {
572
- abort: () => null,
573
- originalName: escapedFileName,
574
- name: fileName,
575
- size: file.size,
576
- status: 'info',
577
- message: this.t('Processing file. Please wait...'),
578
- hash: '',
579
- };
580
- if (this.root.form.submissionRevisions === 'true') {
581
- this.statuses.push(fileUpload);
582
- this.redraw();
583
- const bmf = new BMF();
584
- const hash = await new Promise((resolve, reject) => {
585
- this.emit('fileUploadingStart');
586
- bmf.md5(file, (err, md5) => {
587
- if (err) {
588
- return reject(err);
589
- }
590
- return resolve(md5);
591
- });
592
- });
593
- this.emit('fileUploadingEnd');
594
- fileUpload.hash = hash;
595
- }
596
- // Check if file with the same name is being uploaded
597
- if (!this.filesUploading) {
598
- this.filesUploading = [];
599
- }
600
- const fileWithSameNameUploading = this.filesUploading.some(fileUploading => fileUploading === file.name);
601
- this.filesUploading.push(file.name);
602
- const fileWithSameNameUploaded = this.dataValue.some(fileStatus => fileStatus.originalName === file.name);
603
- const fileWithSameNameUploadedWithError = this.statuses.findIndex(fileStatus => fileStatus.originalName === file.name
604
- && fileStatus.status === 'error');
605
- if (fileWithSameNameUploaded || fileWithSameNameUploading) {
606
- fileUpload.status = 'error';
607
- fileUpload.message = this.t(`File with the same name is already ${fileWithSameNameUploading ? 'being ' : ''}uploaded`);
608
- }
609
- if (fileWithSameNameUploadedWithError !== -1) {
610
- this.statuses.splice(fileWithSameNameUploadedWithError, 1);
611
- this.redraw();
612
- }
613
- // Check file pattern
614
- if (this.component.filePattern && !this.validatePattern(file, this.component.filePattern)) {
615
- fileUpload.status = 'error';
616
- fileUpload.message = this.t('File is the wrong type; it must be {{ pattern }}', {
617
- pattern: this.component.filePattern,
618
- });
619
- }
620
- // Check file minimum size
621
- if (this.component.fileMinSize && !this.validateMinSize(file, this.component.fileMinSize)) {
622
- fileUpload.status = 'error';
623
- fileUpload.message = this.t('File is too small; it must be at least {{ size }}', {
624
- size: this.component.fileMinSize,
625
- });
626
- }
627
- // Check file maximum size
628
- if (this.component.fileMaxSize && !this.validateMaxSize(file, this.component.fileMaxSize)) {
629
- fileUpload.status = 'error';
630
- fileUpload.message = this.t('File is too big; it must be at most {{ size }}', {
631
- size: this.component.fileMaxSize,
632
- });
633
- }
634
- // Get a unique name for this file to keep file collisions from occurring.
635
- const dir = this.interpolate(this.component.dir || '');
636
- const { fileService } = this;
637
- if (!fileService) {
638
- fileUpload.status = 'error';
639
- fileUpload.message = this.t('File Service not provided.');
640
- }
641
- if (this.root.form.submissionRevisions !== 'true') {
642
- this.statuses.push(fileUpload);
643
- this.redraw();
796
+ return Promise.all([...files].map(async (file) => {
797
+ await this.prepareFileToUpload(file);
798
+ this.redraw();
799
+ }));
800
+ }
801
+ else {
802
+ return Promise.resolve();
803
+ }
804
+ }
805
+ async handleFilesToUpload(files) {
806
+ await this.prepareFilesToUpload(files);
807
+ if (!this.autoSync) {
808
+ await this.syncFiles();
809
+ }
810
+ }
811
+ prepareFileToDelete(fileInfo) {
812
+ this.filesToSync.filesToDelete.push({
813
+ ...fileInfo,
814
+ status: 'info',
815
+ message: this.autoSync
816
+ ? this.t('Ready to be removed from storage')
817
+ : this.t('Preparing file to remove'),
818
+ });
819
+ const index = this.dataValue.findIndex(file => file.name === fileInfo.name);
820
+ this.splice(index);
821
+ this.redraw();
822
+ }
823
+ handleFileToRemove(fileInfo) {
824
+ this.prepareFileToDelete(fileInfo);
825
+ if (!this.autoSync) {
826
+ this.syncFiles();
827
+ }
828
+ }
829
+ async deleteFile(fileInfo) {
830
+ const { options = {} } = this.component;
831
+ if (fileInfo && (['url', 'indexeddb', 's3', 'azure', 'googledrive'].includes(this.component.storage))) {
832
+ const { fileService } = this;
833
+ if (fileService && typeof fileService.deleteFile === 'function') {
834
+ return await fileService.deleteFile(fileInfo, options);
835
+ }
836
+ else {
837
+ const formio = this.options.formio || (this.root && this.root.formio);
838
+ if (formio) {
839
+ return await formio.makeRequest('', fileInfo.url, 'delete');
644
840
  }
645
- if (fileUpload.status !== 'error') {
646
- if (this.component.privateDownload) {
647
- file.private = true;
648
- }
649
- const { storage, options = {} } = this.component;
650
- const url = this.interpolate(this.component.url, { file: fileUpload });
651
- let groupKey = null;
652
- let groupPermissions = null;
653
- //Iterate through form components to find group resource if one exists
654
- this.root.everyComponent((element) => {
655
- if (element.component?.submissionAccess || element.component?.defaultPermission) {
656
- groupPermissions = !element.component.submissionAccess ? [
657
- {
658
- type: element.component.defaultPermission,
659
- roles: [],
660
- },
661
- ] : element.component.submissionAccess;
662
- groupPermissions.forEach((permission) => {
663
- groupKey = ['admin', 'write', 'create'].includes(permission.type) ? element.component.key : null;
664
- });
665
- }
666
- });
667
- const fileKey = this.component.fileKey || 'file';
668
- const groupResourceId = groupKey ? this.currentForm.submission.data[groupKey]._id : null;
669
- let processedFile = null;
670
- if (this.root.options.fileProcessor) {
671
- try {
672
- if (this.refs.fileProcessingLoader) {
673
- this.refs.fileProcessingLoader.style.display = 'block';
674
- }
675
- const fileProcessorHandler = fileProcessor(this.fileService, this.root.options.fileProcessor);
676
- processedFile = await fileProcessorHandler(file, this.component.properties);
677
- }
678
- catch (err) {
679
- fileUpload.status = 'error';
680
- fileUpload.message = this.t('File processing has been failed.');
681
- this.fileDropHidden = false;
682
- this.redraw();
683
- return;
684
- }
685
- finally {
686
- if (this.refs.fileProcessingLoader) {
687
- this.refs.fileProcessingLoader.style.display = 'none';
688
- }
689
- }
690
- }
691
- // Prep for a potential multipart upload
692
- let count = 0;
693
- const multipartOptions = this.component.useMultipartUpload && this.component.multipart ? {
694
- ...this.component.multipart,
695
- progressCallback: (total) => {
696
- count++;
697
- fileUpload.status = 'progress';
698
- fileUpload.progress = parseInt(100 * count / total);
699
- delete fileUpload.message;
700
- this.redraw();
701
- },
702
- changeMessage: (message) => {
703
- fileUpload.message = message;
704
- this.redraw();
705
- },
706
- } : false;
707
- fileUpload.message = this.t('Starting upload...');
708
- this.redraw();
709
- const filePromise = fileService.uploadFile(storage, processedFile || file, fileName, dir,
710
- // Progress callback
711
- (evt) => {
712
- fileUpload.status = 'progress';
713
- fileUpload.progress = parseInt(100.0 * evt.loaded / evt.total);
714
- delete fileUpload.message;
715
- this.redraw();
716
- }, url, options, fileKey, groupPermissions, groupResourceId,
717
- // Upload start callback
718
- () => {
719
- this.emit('fileUploadingStart', filePromise);
720
- }, (abort) => fileUpload.abort = abort, multipartOptions).then((fileInfo) => {
721
- const index = this.statuses.indexOf(fileUpload);
722
- if (index !== -1) {
723
- this.statuses.splice(index, 1);
724
- }
725
- fileInfo.originalName = escapedFileName;
726
- fileInfo.hash = fileUpload.hash;
727
- if (!this.hasValue()) {
728
- this.dataValue = [];
729
- }
730
- this.dataValue.push(fileInfo);
731
- _.pull(this.filesUploading, fileInfo.originalName);
732
- this.fileDropHidden = false;
733
- this.redraw();
734
- this.triggerChange();
735
- this.emit('fileUploadingEnd', filePromise);
736
- })
737
- .catch((response) => {
738
- fileUpload.status = 'error';
739
- fileUpload.message = typeof response === 'string' ? response : response.toString();
740
- delete fileUpload.progress;
741
- this.fileDropHidden = false;
742
- _.pull(this.filesUploading, file.name);
743
- this.redraw();
744
- this.emit('fileUploadingEnd', filePromise);
745
- });
841
+ }
842
+ }
843
+ }
844
+ async delete() {
845
+ if (!this.filesToSync.filesToDelete.length) {
846
+ return Promise.resolve();
847
+ }
848
+ return await Promise.all(this.filesToSync.filesToDelete.map(async (fileToSync) => {
849
+ try {
850
+ if (fileToSync.isValidationError) {
851
+ return { fileToSync };
746
852
  }
747
- else {
748
- this.filesUploading.splice(this.filesUploading.indexOf(file.name), 1);
853
+ await this.deleteFile(fileToSync);
854
+ fileToSync.status = 'success';
855
+ fileToSync.message = this.t('Succefully removed');
856
+ }
857
+ catch (response) {
858
+ fileToSync.status = 'error';
859
+ fileToSync.message = typeof response === 'string' ? response : response.toString();
860
+ }
861
+ finally {
862
+ this.redraw();
863
+ }
864
+ return { fileToSync };
865
+ }));
866
+ }
867
+ updateProgress(fileInfo, progressEvent) {
868
+ fileInfo.progress = parseInt(100.0 * progressEvent.loaded / progressEvent.total);
869
+ if (fileInfo.status !== 'progress') {
870
+ fileInfo.status = 'progress';
871
+ delete fileInfo.message;
872
+ this.redraw();
873
+ }
874
+ else {
875
+ const progress = Array.prototype.find.call(this.refs.progress, progressElement => progressElement.id === fileInfo.id);
876
+ progress.innerHTML = `<span class="visually-hidden">${fileInfo.progress}% ${this.t('Complete')}</span>`;
877
+ progress.style.width = `${fileInfo.progress}%`;
878
+ progress.ariaValueNow = fileInfo.progress.toString();
879
+ }
880
+ }
881
+ getMultipartOptions(fileToSync) {
882
+ let count = 0;
883
+ return this.component.useMultipartUpload && this.component.multipart ? {
884
+ ...this.component.multipart,
885
+ progressCallback: (total) => {
886
+ count++;
887
+ fileToSync.status = 'progress';
888
+ fileToSync.progress = parseInt(100 * count / total);
889
+ delete fileToSync.message;
890
+ this.redraw();
891
+ },
892
+ changeMessage: (message) => {
893
+ fileToSync.message = message;
894
+ this.redraw();
895
+ },
896
+ } : false;
897
+ }
898
+ async uploadFile(fileToSync) {
899
+ return await this.fileService.uploadFile(fileToSync.storage, fileToSync.file, fileToSync.name, fileToSync.dir,
900
+ // Progress callback
901
+ this.updateProgress.bind(this, fileToSync), fileToSync.url, fileToSync.options, fileToSync.fileKey, fileToSync.groupPermissions, fileToSync.groupResourceId, () => { },
902
+ // Abort upload callback
903
+ (abort) => this.abortUploads.push({
904
+ id: fileToSync.id,
905
+ abort,
906
+ }), this.getMultipartOptions(fileToSync));
907
+ }
908
+ async upload() {
909
+ if (!this.filesToSync.filesToUpload.length) {
910
+ return Promise.resolve();
911
+ }
912
+ return await Promise.all(this.filesToSync.filesToUpload.map(async (fileToSync) => {
913
+ let fileInfo = null;
914
+ try {
915
+ if (fileToSync.isValidationError) {
916
+ return {
917
+ fileToSync,
918
+ fileInfo,
919
+ };
749
920
  }
750
- });
921
+ fileInfo = await this.uploadFile(fileToSync);
922
+ fileToSync.status = 'success';
923
+ fileToSync.message = this.t('Succefully uploaded');
924
+ fileInfo.originalName = fileToSync.originalName;
925
+ fileInfo.hash = fileToSync.hash;
926
+ }
927
+ catch (response) {
928
+ fileToSync.status = 'error';
929
+ delete fileToSync.progress;
930
+ fileToSync.message = typeof response === 'string'
931
+ ? response
932
+ : response.type === 'abort'
933
+ ? this.t('Request was aborted')
934
+ : response.toString();
935
+ }
936
+ finally {
937
+ delete fileToSync.progress;
938
+ this.redraw();
939
+ }
940
+ return {
941
+ fileToSync,
942
+ fileInfo,
943
+ };
944
+ }));
945
+ }
946
+ async syncFiles() {
947
+ this.isSyncing = true;
948
+ this.fileDropHidden = true;
949
+ this.redraw();
950
+ try {
951
+ const [filesToDelete = [], filesToUpload = []] = await Promise.all([this.delete(), this.upload()]);
952
+ this.filesToSync.filesToDelete = filesToDelete
953
+ .filter(file => file.fileToSync?.status === 'error')
954
+ .map(file => file.fileToSync);
955
+ this.filesToSync.filesToUpload = filesToUpload
956
+ .filter(file => file.fileToSync?.status === 'error')
957
+ .map(file => file.fileToSync);
958
+ if (!this.hasValue()) {
959
+ this.dataValue = [];
960
+ }
961
+ const data = filesToUpload
962
+ .filter(file => file.fileToSync?.status === 'success')
963
+ .map(file => file.fileInfo);
964
+ this.dataValue.push(...data);
965
+ this.triggerChange();
966
+ return Promise.resolve();
967
+ }
968
+ catch (err) {
969
+ return Promise.reject();
970
+ }
971
+ finally {
972
+ this.isSyncing = false;
973
+ this.fileDropHidden = false;
974
+ this.abortUploads = [];
975
+ this.redraw();
751
976
  }
752
977
  }
753
978
  getFile(fileInfo) {
@@ -783,7 +1008,21 @@ export default class FileComponent extends Field {
783
1008
  this.refs.fileBrowse.focus();
784
1009
  }
785
1010
  }
786
- destroy(all = false) {
1011
+ async beforeSubmit() {
1012
+ try {
1013
+ if (!this.autoSync) {
1014
+ return Promise.resolve();
1015
+ }
1016
+ await this.syncFiles();
1017
+ return this.shouldSyncFiles
1018
+ ? Promise.reject('Synchronization is failed')
1019
+ : Promise.resolve();
1020
+ }
1021
+ catch (error) {
1022
+ return Promise.reject(error.message);
1023
+ }
1024
+ }
1025
+ destroy(all) {
787
1026
  this.stopVideo();
788
1027
  super.destroy(all);
789
1028
  }