Ver Fonte

Drag any files from file manager to minicard or opened card. Part 2.

Thanks to xet7 !

Fixes #2936
Lauri Ojansivu há 6 dias atrás
pai
commit
cdd7d69c66

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

@@ -106,6 +106,142 @@
   color: white;
   cursor: pointer;
   font-size: 4em;
+}
+
+/* Upload progress indicators for drag-and-drop uploads */
+.minicard-upload-progress,
+.card-details-upload-progress {
+  background: #f8f9fa;
+  border: 1px solid #e9ecef;
+  border-radius: 4px;
+  padding: 12px;
+  margin: 8px 0;
+  font-size: 14px;
+}
+
+.upload-progress-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+  font-weight: bold;
+  color: #495057;
+}
+
+.upload-progress-header i {
+  margin-right: 8px;
+  color: #007bff;
+}
+
+.upload-progress-item {
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 8px;
+  padding: 8px;
+  background: white;
+  border-radius: 3px;
+  border: 1px solid #dee2e6;
+}
+
+.upload-progress-item.upload-error {
+  border-color: #dc3545;
+  background: #f8d7da;
+}
+
+.upload-progress-filename {
+  font-weight: 500;
+  margin-bottom: 4px;
+  color: #495057;
+  word-break: break-all;
+}
+
+.upload-progress-bar {
+  width: 100%;
+  height: 6px;
+  background: #e9ecef;
+  border-radius: 3px;
+  overflow: hidden;
+  margin-bottom: 4px;
+}
+
+.upload-progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #007bff, #0056b3);
+  transition: width 0.3s ease;
+  border-radius: 3px;
+}
+
+.upload-progress-item.upload-error .upload-progress-fill {
+  background: #dc3545;
+}
+
+.upload-progress-error,
+.upload-progress-success {
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.upload-progress-error {
+  color: #dc3545;
+}
+
+.upload-progress-success {
+  color: #28a745;
+}
+
+.upload-progress-error i,
+.upload-progress-success i {
+  margin-right: 4px;
+}
+
+/* Minicard specific styles */
+.minicard-upload-progress {
+  margin: 4px 0;
+  padding: 8px;
+  font-size: 12px;
+}
+
+.minicard-upload-progress .upload-progress-item {
+  padding: 6px;
+  margin-bottom: 6px;
+}
+
+.minicard-upload-progress .upload-progress-filename {
+  font-size: 11px;
+}
+
+/* Card details specific styles */
+.card-details-upload-progress {
+  margin: 12px 0;
+  padding: 16px;
+}
+
+.card-details-upload-progress .upload-progress-header {
+  font-size: 16px;
+  margin-bottom: 12px;
+}
+
+.card-details-upload-progress .upload-progress-item {
+  padding: 12px;
+  margin-bottom: 10px;
+}
+
+.card-details-upload-progress .upload-progress-filename {
+  font-size: 14px;
+}
+
+/* Drag over state for minicards */
+.minicard.is-dragging-over {
+  border: 2px dashed #007bff !important;
+  background: rgba(0, 123, 255, 0.1) !important;
+}
+
+/* Drag over state for card details */
+.js-card-details.is-dragging-over {
+  border: 2px dashed #007bff !important;
+  background: rgba(0, 123, 255, 0.05) !important;
+}
   top: 0;
   right: 8px;
   position: absolute;

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

@@ -1,6 +1,7 @@
 import { ReactiveCache } from '/imports/reactiveCache';
 import { ObjectID } from 'bson';
 import DOMPurify from 'dompurify';
+import uploadProgressManager from '/client/lib/uploadProgressManager';
 
 const filesize = require('filesize');
 const prettyMilliseconds = require('pretty-ms');
@@ -333,13 +334,17 @@ export function handleFileUpload(card, files) {
   // Check if board allows attachments
   const board = card.board();
   if (!board || !board.allowsAttachments) {
-    console.warn('Attachments not allowed on this board');
+    if (process.env.DEBUG === 'true') {
+      console.warn('Attachments not allowed on this board');
+    }
     return [];
   }
 
   // Check if user can modify the card
   if (!card.canModifyCard()) {
-    console.warn('User does not have permission to modify this card');
+    if (process.env.DEBUG === 'true') {
+      console.warn('User does not have permission to modify this card');
+    }
     return [];
   }
 
@@ -348,7 +353,9 @@ export function handleFileUpload(card, files) {
   for (const file of files) {
     // Basic file validation
     if (!file || !file.name) {
-      console.warn('Invalid file object');
+      if (process.env.DEBUG === 'true') {
+        console.warn('Invalid file object');
+      }
       continue;
     }
 
@@ -381,24 +388,36 @@ export function handleFileUpload(card, files) {
         false,
       );
 
+      // Add to progress manager for tracking
+      const uploadId = uploadProgressManager.addUpload(card._id, uploader, file);
+
       uploader.on('uploaded', (error, fileRef) => {
         if (!error) {
           if (fileRef.isImage) {
             card.setCover(fileRef._id);
+            if (process.env.DEBUG === 'true') {
+              console.log(`Set cover image for card ${card._id}: ${fileRef.name}`);
+            }
           }
         } else {
-          console.error('Upload error:', error);
+          if (process.env.DEBUG === 'true') {
+            console.error('Upload error:', error);
+          }
         }
       });
 
       uploader.on('error', (error) => {
-        console.error('Upload error:', error);
+        if (process.env.DEBUG === 'true') {
+          console.error('Upload error:', error);
+        }
       });
 
       uploads.push(uploader);
       uploader.start();
     } catch (error) {
-      console.error('Failed to create uploader:', error);
+      if (process.env.DEBUG === 'true') {
+        console.error('Failed to create uploader:', error);
+      }
     }
   }
 

+ 20 - 0
client/components/cards/cardDetails.jade

@@ -65,6 +65,26 @@ template(name="cardDetails")
       else
         p.warning {{_ 'card-archived'}}
 
+    // Upload progress indicator for drag-and-drop uploads
+    if hasActiveUploads
+      .card-details-upload-progress
+        .upload-progress-header
+          i.fa.fa-upload
+          span {{_ 'uploading-files'}} ({{uploadCount}})
+        each uploads
+          .upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
+            .upload-progress-filename {{file.name}}
+            .upload-progress-bar
+              .upload-progress-fill(style="width: {{progress}}%")
+            if $eq status 'error'
+              .upload-progress-error
+                i.fa.fa-exclamation-triangle
+                span {{_ 'upload-failed'}}
+            else if $eq status 'completed'
+              .upload-progress-success
+                i.fa.fa-check
+                span {{_ 'upload-completed'}}
+
     .card-details-left
 
       .card-details-items

+ 11 - 0
client/components/cards/cardDetails.js

@@ -13,6 +13,7 @@ import { ALLOWED_COLORS } from '/config/const';
 import { UserAvatar } from '../users/userAvatar';
 import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
 import { handleFileUpload } from './attachments';
+import uploadProgressManager from '/client/lib/uploadProgressManager';
 
 const subManager = new SubsManager();
 const { calculateIndexData } = Utils;
@@ -544,6 +545,16 @@ Template.cardDetails.helpers({
   isPopup() {
     let ret = !!Utils.getPopupCardId();
     return ret;
+  },
+  // Upload progress helpers
+  hasActiveUploads() {
+    return uploadProgressManager.hasActiveUploads(this._id);
+  },
+  uploads() {
+    return uploadProgressManager.getUploadsForCard(this._id);
+  },
+  uploadCount() {
+    return uploadProgressManager.getUploadCountForCard(this._id);
   }
 });
 Template.cardDetailsPopup.onDestroyed(() => {

+ 21 - 0
client/components/cards/minicard.jade

@@ -31,6 +31,27 @@ template(name="minicard")
     if cover
       if currentBoard.allowsCoverAttachmentOnMinicard
         .minicard-cover(style="background-image: url('{{cover.link 'original'}}?dummyReloadAfterSessionEstablished={{sess}}');")
+
+    // Upload progress indicator for drag-and-drop uploads
+    if hasActiveUploads
+      .minicard-upload-progress
+        .upload-progress-header
+          i.fa.fa-upload
+          span {{_ 'uploading-files'}} ({{uploadCount}})
+        each uploads
+          .upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}")
+            .upload-progress-filename {{file.name}}
+            .upload-progress-bar
+              .upload-progress-fill(style="width: {{progress}}%")
+            if $eq status 'error'
+              .upload-progress-error
+                i.fa.fa-exclamation-triangle
+                span {{_ 'upload-failed'}}
+            else if $eq status 'completed'
+              .upload-progress-success
+                i.fa.fa-check
+                span {{_ 'upload-completed'}}
+
     .minicard-title
       if $eq 'prefix-with-full-path' currentBoard.presentParentTask
         .parent-prefix

+ 11 - 0
client/components/cards/minicard.js

@@ -2,6 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
 import { TAPi18n } from '/imports/i18n';
 import { CustomFieldStringTemplate } from '/client/lib/customFields';
 import { handleFileUpload } from './attachments';
+import uploadProgressManager from '/client/lib/uploadProgressManager';
 
 // Template.cards.events({
 //   'click .member': Popup.open('cardMember')
@@ -185,6 +186,16 @@ Template.minicard.helpers({
   },
   isWatching() {
     return this.findWatcher(Meteor.userId());
+  },
+  // Upload progress helpers
+  hasActiveUploads() {
+    return uploadProgressManager.hasActiveUploads(this._id);
+  },
+  uploads() {
+    return uploadProgressManager.getUploadsForCard(this._id);
+  },
+  uploadCount() {
+    return uploadProgressManager.getUploadCountForCard(this._id);
   }
 });
 

+ 177 - 0
client/lib/uploadProgressManager.js

@@ -0,0 +1,177 @@
+import { ReactiveVar } from 'meteor/reactive-var';
+import { Tracker } from 'meteor/tracker';
+
+/**
+ * Global upload progress manager for drag-and-drop file uploads
+ * Tracks upload progress across all cards and provides reactive data
+ */
+class UploadProgressManager {
+  constructor() {
+    // Map of cardId -> array of upload objects
+    this.cardUploads = new ReactiveVar(new Map());
+
+    // Map of uploadId -> upload object for easy lookup
+    this.uploadMap = new ReactiveVar(new Map());
+  }
+
+  /**
+   * Add a new upload to track
+   * @param {string} cardId - The card ID
+   * @param {Object} uploader - The uploader object from Attachments.insert
+   * @param {File} file - The file being uploaded
+   * @returns {string} uploadId - Unique identifier for this upload
+   */
+  addUpload(cardId, uploader, file) {
+    const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+    const upload = {
+      id: uploadId,
+      cardId: cardId,
+      file: file,
+      uploader: uploader,
+      progress: new ReactiveVar(0),
+      status: new ReactiveVar('uploading'), // 'uploading', 'completed', 'error'
+      error: new ReactiveVar(null),
+      startTime: Date.now(),
+      endTime: null
+    };
+
+    // Update card uploads
+    const currentCardUploads = this.cardUploads.get();
+    const cardUploads = currentCardUploads.get(cardId) || [];
+    cardUploads.push(upload);
+    currentCardUploads.set(cardId, cardUploads);
+    this.cardUploads.set(currentCardUploads);
+
+    // Update upload map
+    const currentUploadMap = this.uploadMap.get();
+    currentUploadMap.set(uploadId, upload);
+    this.uploadMap.set(currentUploadMap);
+
+    // Set up uploader event listeners
+    uploader.on('progress', (progress) => {
+      upload.progress.set(progress);
+    });
+
+    uploader.on('uploaded', (error, fileRef) => {
+      upload.status.set(error ? 'error' : 'completed');
+      upload.endTime = Date.now();
+      upload.error.set(error);
+
+      if (process.env.DEBUG === 'true') {
+        console.log(`Upload ${uploadId} completed:`, error ? 'error' : 'success');
+      }
+
+      // Remove from tracking after a delay to show completion
+      setTimeout(() => {
+        this.removeUpload(uploadId);
+      }, 2000);
+    });
+
+    uploader.on('error', (error) => {
+      upload.status.set('error');
+      upload.endTime = Date.now();
+      upload.error.set(error);
+
+      if (process.env.DEBUG === 'true') {
+        console.log(`Upload ${uploadId} failed:`, error);
+      }
+
+      // Remove from tracking after a delay to show error
+      setTimeout(() => {
+        this.removeUpload(uploadId);
+      }, 3000);
+    });
+
+    if (process.env.DEBUG === 'true') {
+      console.log(`Added upload ${uploadId} for card ${cardId}: ${file.name}`);
+    }
+
+    return uploadId;
+  }
+
+  /**
+   * Remove an upload from tracking
+   * @param {string} uploadId - The upload ID to remove
+   */
+  removeUpload(uploadId) {
+    const upload = this.uploadMap.get().get(uploadId);
+    if (!upload) return;
+
+    const cardId = upload.cardId;
+
+    // Remove from card uploads
+    const currentCardUploads = this.cardUploads.get();
+    const cardUploads = currentCardUploads.get(cardId) || [];
+    const filteredCardUploads = cardUploads.filter(u => u.id !== uploadId);
+
+    if (filteredCardUploads.length === 0) {
+      currentCardUploads.delete(cardId);
+    } else {
+      currentCardUploads.set(cardId, filteredCardUploads);
+    }
+    this.cardUploads.set(currentCardUploads);
+
+    // Remove from upload map
+    const currentUploadMap = this.uploadMap.get();
+    currentUploadMap.delete(uploadId);
+    this.uploadMap.set(currentUploadMap);
+
+    if (process.env.DEBUG === 'true') {
+      console.log(`Removed upload ${uploadId} from tracking`);
+    }
+  }
+
+  /**
+   * Get all uploads for a specific card
+   * @param {string} cardId - The card ID
+   * @returns {Array} Array of upload objects
+   */
+  getUploadsForCard(cardId) {
+    return this.cardUploads.get().get(cardId) || [];
+  }
+
+  /**
+   * Get upload count for a specific card
+   * @param {string} cardId - The card ID
+   * @returns {number} Number of active uploads
+   */
+  getUploadCountForCard(cardId) {
+    return this.getUploadsForCard(cardId).length;
+  }
+
+  /**
+   * Check if a card has any active uploads
+   * @param {string} cardId - The card ID
+   * @returns {boolean} True if card has active uploads
+   */
+  hasActiveUploads(cardId) {
+    return this.getUploadCountForCard(cardId) > 0;
+  }
+
+  /**
+   * Get all uploads across all cards
+   * @returns {Array} Array of all upload objects
+   */
+  getAllUploads() {
+    const allUploads = [];
+    this.cardUploads.get().forEach(cardUploads => {
+      allUploads.push(...cardUploads);
+    });
+    return allUploads;
+  }
+
+  /**
+   * Clear all uploads (useful for cleanup)
+   */
+  clearAllUploads() {
+    this.cardUploads.set(new Map());
+    this.uploadMap.set(new Map());
+  }
+}
+
+// Create global instance
+const uploadProgressManager = new UploadProgressManager();
+
+export default uploadProgressManager;
+

+ 3 - 0
imports/i18n/data/en.i18n.json

@@ -631,6 +631,9 @@
   "upload": "Upload",
   "upload-avatar": "Upload an avatar",
   "uploaded-avatar": "Uploaded an avatar",
+  "uploading-files": "Uploading files",
+  "upload-failed": "Upload failed",
+  "upload-completed": "Upload completed",
   "custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL",
   "custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL",
   "custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27",