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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) 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.form.css +19 -15
  8. package/dist/formio.form.js +19 -19
  9. package/dist/formio.form.min.css +1 -1
  10. package/dist/formio.form.min.js +1 -1
  11. package/dist/formio.form.min.js.LICENSE.txt +1 -1
  12. package/dist/formio.full.css +363 -19
  13. package/dist/formio.full.js +20 -20
  14. package/dist/formio.full.min.css +3 -3
  15. package/dist/formio.full.min.js +1 -1
  16. package/dist/formio.full.min.js.LICENSE.txt +1 -1
  17. package/dist/formio.js +8 -8
  18. package/dist/formio.min.js +1 -1
  19. package/lib/cjs/CDN.js +12 -6
  20. package/lib/cjs/Webform.js +4 -1
  21. package/lib/cjs/Wizard.js +6 -9
  22. package/lib/cjs/components/_classes/component/Component.js +6 -1
  23. package/lib/cjs/components/_classes/nested/NestedComponent.js +1 -1
  24. package/lib/cjs/components/container/fixtures/comp4.js +45 -0
  25. package/lib/cjs/components/container/fixtures/index.js +3 -1
  26. package/lib/cjs/components/datetime/DateTime.js +6 -0
  27. package/lib/cjs/components/file/File.js +465 -215
  28. package/lib/cjs/components/file/editForm/File.edit.display.js +17 -0
  29. package/lib/cjs/components/textarea/TextArea.js +2 -2
  30. package/lib/cjs/components/textfield/TextField.js +3 -1
  31. package/lib/cjs/components/time/Time.js +3 -0
  32. package/lib/cjs/providers/storage/azure.js +6 -1
  33. package/lib/cjs/providers/storage/base64.js +1 -1
  34. package/lib/cjs/providers/storage/googleDrive.js +5 -1
  35. package/lib/cjs/providers/storage/indexeddb.js +1 -1
  36. package/lib/cjs/providers/storage/s3.js +5 -1
  37. package/lib/cjs/providers/storage/xhr.js +10 -0
  38. package/lib/mjs/CDN.js +12 -6
  39. package/lib/mjs/Webform.js +4 -1
  40. package/lib/mjs/Wizard.js +6 -9
  41. package/lib/mjs/components/_classes/component/Component.js +6 -1
  42. package/lib/mjs/components/_classes/nested/NestedComponent.js +1 -1
  43. package/lib/mjs/components/container/fixtures/comp4.js +43 -0
  44. package/lib/mjs/components/container/fixtures/index.js +2 -1
  45. package/lib/mjs/components/datetime/DateTime.js +6 -0
  46. package/lib/mjs/components/file/File.js +463 -224
  47. package/lib/mjs/components/file/editForm/File.edit.display.js +17 -0
  48. package/lib/mjs/components/textarea/TextArea.js +2 -2
  49. package/lib/mjs/components/textfield/TextField.js +6 -0
  50. package/lib/mjs/components/time/Time.js +3 -0
  51. package/lib/mjs/providers/storage/azure.js +6 -1
  52. package/lib/mjs/providers/storage/base64.js +1 -1
  53. package/lib/mjs/providers/storage/googleDrive.js +5 -1
  54. package/lib/mjs/providers/storage/indexeddb.js +1 -1
  55. package/lib/mjs/providers/storage/s3.js +5 -1
  56. package/lib/mjs/providers/storage/xhr.js +10 -0
  57. package/package.json +2 -2
@@ -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
  }