فهرست منبع

Merge pull request #4641 from mfilser/attachment_upload_progress_bar

Attachment upload progress bar + multiple files upload
Lauri Ojansivu 2 سال پیش
والد
کامیت
f320069ec8

+ 4 - 0
client/components/cards/attachments.css

@@ -1,3 +1,7 @@
+.attachment-upload {
+  text-align: center;
+  font-weight: bold;
+}
 .attachments-galery {
 .attachments-galery {
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;

+ 22 - 9
client/components/cards/attachments.jade

@@ -1,10 +1,25 @@
 template(name="cardAttachmentsPopup")
 template(name="cardAttachmentsPopup")
-  ul.pop-over-list
-    li
-      input.js-attach-file.hide(type="file" name="file" multiple)
-      a.js-computer-upload {{_ 'computer'}}
-    li
-      a.js-upload-clipboard-image {{_ 'clipboard'}}
+  if $gt uploads.length 0
+    .attachment-upload {{_ 'uploading'}}
+      table
+        tr
+          th.upload-file-name-descr {{_ 'name'}}
+          th.upload-progress-descr {{_ 'progress'}}
+          th.upload-remaining-descr {{_ 'remaining_time'}}
+          th.upload-speed-descr {{_ 'speed'}}
+        each upload in uploads
+          tr
+            td.upload-file-name-value {{upload.file.name}}
+            td.upload-progress-value {{upload.progress.get}}%
+            td.upload-remaining-value {{getEstimateTime upload}}
+            td.upload-speed-value {{getEstimateSpeed upload}}
+  else
+    ul.pop-over-list
+      li
+        input.js-attach-file.hide(type="file" name="file" multiple)
+        a.js-computer-upload {{_ 'computer'}}
+      li
+        a.js-upload-clipboard-image {{_ 'clipboard'}}
 
 
 template(name="previewClipboardImagePopup")
 template(name="previewClipboardImagePopup")
   p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}}
   p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}}
@@ -37,11 +52,9 @@ template(name="attachmentsGalery")
                   source(src="{{link}}" type="video/mp4")
                   source(src="{{link}}" type="video/mp4")
             else
             else
               span.attachment-thumbnail-ext= extension
               span.attachment-thumbnail-ext= extension
-          else
-            +spinner
         p.attachment-details
         p.attachment-details
           = name
           = name
-          span.file-size ({{fileSize size}} KB)
+          span.file-size ({{fileSize size}})
           span.attachment-details-actions
           span.attachment-details-actions
             a.js-download(href="{{link}}?download=true", download="{{name}}")
             a.js-download(href="{{link}}?download=true", download="{{name}}")
               i.fa.fa-download
               i.fa.fa-download

+ 59 - 25
client/components/cards/attachments.js

@@ -1,3 +1,6 @@
+const filesize = require('filesize');
+const prettyMilliseconds = require('pretty-ms');
+
 Template.attachmentsGalery.events({
 Template.attachmentsGalery.events({
   'click .js-add-attachment': Popup.open('cardAttachments'),
   'click .js-add-attachment': Popup.open('cardAttachments'),
   // If we let this event bubble, FlowRouter will handle it and empty the page
   // If we let this event bubble, FlowRouter will handle it and empty the page
@@ -13,37 +16,68 @@ Template.attachmentsGalery.helpers({
     return Meteor.user().isBoardAdmin();
     return Meteor.user().isBoardAdmin();
   },
   },
   fileSize(size) {
   fileSize(size) {
-    return Math.round(size / 1024);
+    const ret = filesize(size);
+    return ret;
   },
   },
 });
 });
 
 
+Template.cardAttachmentsPopup.onCreated(function() {
+  this.uploads = new ReactiveVar([]);
+});
+
+Template.cardAttachmentsPopup.helpers({
+  getEstimateTime(upload) {
+    const ret = prettyMilliseconds(upload.estimateTime.get());
+    return ret;
+  },
+  getEstimateSpeed(upload) {
+    const ret = filesize(upload.estimateSpeed.get(), {round: 0}) + "/s";
+    return ret;
+  },
+  uploads() {
+    return Template.instance().uploads.get();
+  }
+});
+
 Template.cardAttachmentsPopup.events({
 Template.cardAttachmentsPopup.events({
-  'change .js-attach-file'(event) {
+  'change .js-attach-file'(event, templateInstance) {
     const card = this;
     const card = this;
-    if (event.currentTarget.files && event.currentTarget.files[0]) {
-      const fileId = Random.id();
-      const config = {
-        file: event.currentTarget.files[0],
-        fileId: fileId,
-        meta: Utils.getCommonAttachmentMetaFrom(card),
-        chunkSize: 'dynamic',
-      };
-      config.meta.fileId = fileId;
-      const uploader = Attachments.insert(
-        config,
-        false,
-      );
-      uploader.on('uploaded', (error, fileRef) => {
-        if (!error) {
-          if (fileRef.isImage) {
-            card.setCover(fileRef._id);
+    const files = event.currentTarget.files;
+    if (files) {
+      let uploads = [];
+      for (const file of files) {
+        const fileId = Random.id();
+        const config = {
+          file: file,
+          fileId: fileId,
+          meta: Utils.getCommonAttachmentMetaFrom(card),
+          chunkSize: 'dynamic',
+        };
+        config.meta.fileId = fileId;
+        const uploader = Attachments.insert(
+          config,
+          false,
+        );
+        uploader.on('start', function() {
+          uploads.push(this);
+          templateInstance.uploads.set(uploads);
+        });
+        uploader.on('uploaded', (error, fileRef) => {
+          if (!error) {
+            if (fileRef.isImage) {
+              card.setCover(fileRef._id);
+            }
           }
           }
-        }
-      });
-      uploader.on('end', (error, fileRef) => {
-        Popup.back();
-      });
-      uploader.start();
+        });
+        uploader.on('end', (error, fileRef) => {
+          uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
+          templateInstance.uploads.set(uploads);
+          if (uploads.length == 0 ) {
+            Popup.back();
+          }
+        });
+        uploader.start();
+      }
     }
     }
   },
   },
   'click .js-computer-upload'(event, templateInstance) {
   'click .js-computer-upload'(event, templateInstance) {

+ 1 - 1
client/components/settings/adminReports.jade

@@ -88,7 +88,7 @@ template(name="filesReport")
       each att in results
       each att in results
         tr
         tr
           td {{ att.name }}
           td {{ att.name }}
-          td.right {{fileSize att.size }}
+          td.right {{ fileSize att.size }}
           td {{ att.type }}
           td {{ att.type }}
           td {{ att._id }}
           td {{ att._id }}
           td {{ att.meta.boardId }}
           td {{ att.meta.boardId }}

+ 3 - 1
client/components/settings/adminReports.js

@@ -4,6 +4,7 @@ import { CardSearchPagedComponent } from '/client/lib/cardSearch';
 import SessionData from '/models/usersessiondata';
 import SessionData from '/models/usersessiondata';
 import { QueryParams } from '/config/query-classes';
 import { QueryParams } from '/config/query-classes';
 import { OPERATOR_LIMIT } from '/config/search-const';
 import { OPERATOR_LIMIT } from '/config/search-const';
+const filesize = require('filesize');
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   subscription: null,
   subscription: null,
@@ -114,7 +115,8 @@ class AdminReport extends BlazeComponent {
   }
   }
 
 
   fileSize(size) {
   fileSize(size) {
-    return Math.round(size / 1024);
+    const ret = filesize(size);
+    return ret;
   }
   }
 
 
   abbreviate(text) {
   abbreviate(text) {

+ 2 - 2
client/components/settings/attachments.jade

@@ -52,7 +52,7 @@ template(name="moveBoardAttachments")
         th {{_ 'name'}}
         th {{_ 'name'}}
         th {{_ 'path'}}
         th {{_ 'path'}}
         th {{_ 'version-name'}}
         th {{_ 'version-name'}}
-        th {{_ 'size'}} (B)
+        th {{_ 'size'}}
         th GridFsFileId
         th GridFsFileId
         th {{_ 'storage'}}
         th {{_ 'storage'}}
         th {{_ 'action'}}
         th {{_ 'action'}}
@@ -68,7 +68,7 @@ template(name="moveAttachment")
       td {{ name }}
       td {{ name }}
       td {{ version.path }}
       td {{ version.path }}
       td {{ version.versionName }}
       td {{ version.versionName }}
-      td {{ version.size }}
+      td {{ fileSize version.size }}
       td {{ version.meta.gridFsFileId }}
       td {{ version.meta.gridFsFileId }}
       td {{ version.storageName }}
       td {{ version.storageName }}
       td
       td

+ 5 - 0
client/components/settings/attachments.js

@@ -1,4 +1,5 @@
 import Attachments, { fileStoreStrategyFactory } from '/models/attachments';
 import Attachments, { fileStoreStrategyFactory } from '/models/attachments';
+const filesize = require('filesize');
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   subscription: null,
   subscription: null,
@@ -108,6 +109,10 @@ BlazeComponent.extendComponent({
 }).register('moveBoardAttachments');
 }).register('moveBoardAttachments');
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
+  fileSize(size) {
+    const ret = filesize(size);
+    return ret;
+  },
   events() {
   events() {
     return [
     return [
       {
       {

+ 14 - 14
client/components/settings/informationBody.jade

@@ -58,38 +58,38 @@ template(name='statistics')
           td {{numFormat statistics.os.loadavg.[0]}}, {{numFormat statistics.os.loadavg.[1]}}, {{numFormat statistics.os.loadavg.[2]}}
           td {{numFormat statistics.os.loadavg.[0]}}, {{numFormat statistics.os.loadavg.[1]}}, {{numFormat statistics.os.loadavg.[2]}}
         tr
         tr
           th {{_ 'OS_Totalmem'}}
           th {{_ 'OS_Totalmem'}}
-          td {{bytesToSize statistics.os.totalmem}}
+          td {{fileSize statistics.os.totalmem}}
         tr
         tr
           th {{_ 'OS_Freemem'}}
           th {{_ 'OS_Freemem'}}
-          td {{bytesToSize statistics.os.freemem}}
+          td {{fileSize statistics.os.freemem}}
         tr
         tr
           th {{_ 'OS_Cpus'}}
           th {{_ 'OS_Cpus'}}
           td {{statistics.os.cpus.length}}
           td {{statistics.os.cpus.length}}
         unless isSandstorm
         unless isSandstorm
           tr
           tr
             th {{_ 'Node_heap_total_heap_size'}}
             th {{_ 'Node_heap_total_heap_size'}}
-            td {{bytesToSize statistics.nodeHeapStats.totalHeapSize}}
+            td {{fileSize statistics.nodeHeapStats.totalHeapSize}}
           tr
           tr
             th {{_ 'Node_heap_total_heap_size_executable'}}
             th {{_ 'Node_heap_total_heap_size_executable'}}
-            td {{bytesToSize statistics.nodeHeapStats.totalHeapSizeExecutable}}
+            td {{fileSize statistics.nodeHeapStats.totalHeapSizeExecutable}}
           tr
           tr
             th {{_ 'Node_heap_total_physical_size'}}
             th {{_ 'Node_heap_total_physical_size'}}
-            td {{bytesToSize statistics.nodeHeapStats.totalPhysicalSize}}
+            td {{fileSize statistics.nodeHeapStats.totalPhysicalSize}}
           tr
           tr
             th {{_ 'Node_heap_total_available_size'}}
             th {{_ 'Node_heap_total_available_size'}}
-            td {{bytesToSize statistics.nodeHeapStats.totalAvailableSize}}
+            td {{fileSize statistics.nodeHeapStats.totalAvailableSize}}
           tr
           tr
             th {{_ 'Node_heap_used_heap_size'}}
             th {{_ 'Node_heap_used_heap_size'}}
-            td {{bytesToSize statistics.nodeHeapStats.usedHeapSize}}
+            td {{fileSize statistics.nodeHeapStats.usedHeapSize}}
           tr
           tr
             th {{_ 'Node_heap_heap_size_limit'}}
             th {{_ 'Node_heap_heap_size_limit'}}
-            td {{bytesToSize statistics.nodeHeapStats.heapSizeLimit}}
+            td {{fileSize statistics.nodeHeapStats.heapSizeLimit}}
           tr
           tr
             th {{_ 'Node_heap_malloced_memory'}}
             th {{_ 'Node_heap_malloced_memory'}}
-            td {{bytesToSize statistics.nodeHeapStats.mallocedMemory}}
+            td {{fileSize statistics.nodeHeapStats.mallocedMemory}}
           tr
           tr
             th {{_ 'Node_heap_peak_malloced_memory'}}
             th {{_ 'Node_heap_peak_malloced_memory'}}
-            td {{bytesToSize statistics.nodeHeapStats.peakMallocedMemory}}
+            td {{fileSize statistics.nodeHeapStats.peakMallocedMemory}}
           tr
           tr
             th {{_ 'Node_heap_does_zap_garbage'}}
             th {{_ 'Node_heap_does_zap_garbage'}}
             td {{statistics.nodeHeapStats.doesZapGarbage}}
             td {{statistics.nodeHeapStats.doesZapGarbage}}
@@ -101,13 +101,13 @@ template(name='statistics')
             td {{statistics.nodeHeapStats.numberOfDetachedContexts}}
             td {{statistics.nodeHeapStats.numberOfDetachedContexts}}
           tr
           tr
             th {{_ 'Node_memory_usage_rss'}}
             th {{_ 'Node_memory_usage_rss'}}
-            td {{bytesToSize statistics.nodeMemoryUsage.rss}}
+            td {{fileSize statistics.nodeMemoryUsage.rss}}
           tr
           tr
             th {{_ 'Node_memory_usage_heap_total'}}
             th {{_ 'Node_memory_usage_heap_total'}}
-            td {{bytesToSize statistics.nodeMemoryUsage.heapTotal}}
+            td {{fileSize statistics.nodeMemoryUsage.heapTotal}}
           tr
           tr
             th {{_ 'Node_memory_usage_heap_used'}}
             th {{_ 'Node_memory_usage_heap_used'}}
-            td {{bytesToSize statistics.nodeMemoryUsage.heapUsed}}
+            td {{fileSize statistics.nodeMemoryUsage.heapUsed}}
           tr
           tr
             th {{_ 'Node_memory_usage_external'}}
             th {{_ 'Node_memory_usage_external'}}
-            td {{bytesToSize statistics.nodeMemoryUsage.external}}
+            td {{fileSize statistics.nodeMemoryUsage.external}}

+ 4 - 7
client/components/settings/informationBody.js

@@ -1,4 +1,5 @@
 import { TAPi18n } from '/imports/i18n';
 import { TAPi18n } from '/imports/i18n';
+const filesize = require('filesize');
 
 
 BlazeComponent.extendComponent({
 BlazeComponent.extendComponent({
   onCreated() {
   onCreated() {
@@ -39,12 +40,8 @@ BlazeComponent.extendComponent({
     return parseFloat(number).toFixed(2);
     return parseFloat(number).toFixed(2);
   },
   },
 
 
-  bytesToSize(bytes) {
-    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
-    if (bytes === 0) {
-      return '0 Byte';
-    }
-    const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10);
-    return `${Math.round(bytes / Math.pow(1024, i), 2)}  ${sizes[i]}`;
+  fileSize(size) {
+    const ret = filesize(size);
+    return ret;
   },
   },
 }).register('statistics');
 }).register('statistics');

+ 5 - 1
imports/i18n/data/en.i18n.json

@@ -1181,5 +1181,9 @@
   "storage": "Storage",
   "storage": "Storage",
   "action": "Action",
   "action": "Action",
   "board-title": "Board Title",
   "board-title": "Board Title",
-  "attachmentRenamePopup-title": "Rename"
+  "attachmentRenamePopup-title": "Rename",
+  "uploading": "Uploading",
+  "remaining_time": "Remaining time",
+  "speed": "Speed",
+  "progress": "Progress"
 }
 }

+ 18 - 0
package-lock.json

@@ -1791,6 +1791,11 @@
         "token-types": "^4.1.1"
         "token-types": "^4.1.1"
       }
       }
     },
     },
+    "filesize": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
+      "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ=="
+    },
     "find-up": {
     "find-up": {
       "version": "4.1.0",
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -3624,6 +3629,11 @@
         "parse5": "^7.0.0"
         "parse5": "^7.0.0"
       }
       }
     },
     },
+    "parse-ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
+      "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
+    },
     "path-exists": {
     "path-exists": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3671,6 +3681,14 @@
       "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
       "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
       "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ=="
       "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ=="
     },
     },
+    "pretty-ms": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
+      "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
+      "requires": {
+        "parse-ms": "^2.1.0"
+      }
+    },
     "process-nextick-args": {
     "process-nextick-args": {
       "version": "2.0.1",
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",

+ 2 - 0
package.json

@@ -37,6 +37,7 @@
     "exceljs": "^4.2.1",
     "exceljs": "^4.2.1",
     "fibers": "^5.0.0",
     "fibers": "^5.0.0",
     "file-type": "^16.5.4",
     "file-type": "^16.5.4",
+    "filesize": "^8.0.7",
     "i18next": "^21.6.16",
     "i18next": "^21.6.16",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "jquery": "^2.2.4",
     "jquery": "^2.2.4",
@@ -55,6 +56,7 @@
     "os": "^0.1.2",
     "os": "^0.1.2",
     "page": "^1.11.6",
     "page": "^1.11.6",
     "papaparse": "^5.3.1",
     "papaparse": "^5.3.1",
+    "pretty-ms": "^7.0.1",
     "qs": "^6.10.1",
     "qs": "^6.10.1",
     "simpl-schema": "^1.12.0",
     "simpl-schema": "^1.12.0",
     "source-map-support": "^0.5.20",
     "source-map-support": "^0.5.20",