Sfoglia il codice sorgente

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

Thanks to xet7 !

Fixes #2936
Lauri Ojansivu 5 giorni fa
parent
commit
3e9481c5bd

+ 84 - 35
client/components/cards/attachments.js

@@ -296,44 +296,13 @@ Template.cardAttachmentsPopup.events({
     const files = event.currentTarget.files;
     if (files) {
       let uploads = [];
-      for (const file of files) {
-        const fileId = new ObjectID().toString();
-        let fileName = DOMPurify.sanitize(file.name);
-
-        // If sanitized filename is not same as original filename,
-        // it could be XSS that is already fixed with sanitize,
-        // or just normal mistake, so it is not a problem.
-        // That is why here is no warning.
-        if (fileName !== file.name) {
-          // If filename is empty, only in that case add some filename
-          if (fileName.length === 0) {
-            fileName = 'Empty-filename-after-sanitize.txt';
-          }
-        }
+      const uploaders = handleFileUpload(card, files);
 
-        const config = {
-          file: file,
-          fileId: fileId,
-          fileName: fileName,
-          meta: Utils.getCommonAttachmentMetaFrom(card),
-          chunkSize: 'dynamic',
-        };
-        config.meta.fileId = fileId;
-        const uploader = Attachments.insert(
-          config,
-          false,
-        );
+      uploaders.forEach(uploader => {
         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) => {
           uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
           templateInstance.uploads.set(uploads);
@@ -341,8 +310,7 @@ Template.cardAttachmentsPopup.events({
             Popup.back();
           }
         });
-        uploader.start();
-      }
+      });
     }
   },
   'click .js-computer-upload'(event, templateInstance) {
@@ -356,6 +324,87 @@ const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
 const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
 let pastedResults = null;
 
+// Shared upload logic for drag-and-drop functionality
+export function handleFileUpload(card, files) {
+  if (!files || files.length === 0) {
+    return [];
+  }
+
+  // Check if board allows attachments
+  const board = card.board();
+  if (!board || !board.allowsAttachments) {
+    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');
+    return [];
+  }
+
+  const uploads = [];
+
+  for (const file of files) {
+    // Basic file validation
+    if (!file || !file.name) {
+      console.warn('Invalid file object');
+      continue;
+    }
+
+    const fileId = new ObjectID().toString();
+    let fileName = DOMPurify.sanitize(file.name);
+
+    // If sanitized filename is not same as original filename,
+    // it could be XSS that is already fixed with sanitize,
+    // or just normal mistake, so it is not a problem.
+    // That is why here is no warning.
+    if (fileName !== file.name) {
+      // If filename is empty, only in that case add some filename
+      if (fileName.length === 0) {
+        fileName = 'Empty-filename-after-sanitize.txt';
+      }
+    }
+
+    const config = {
+      file: file,
+      fileId: fileId,
+      fileName: fileName,
+      meta: Utils.getCommonAttachmentMetaFrom(card),
+      chunkSize: 'dynamic',
+    };
+    config.meta.fileId = fileId;
+
+    try {
+      const uploader = Attachments.insert(
+        config,
+        false,
+      );
+
+      uploader.on('uploaded', (error, fileRef) => {
+        if (!error) {
+          if (fileRef.isImage) {
+            card.setCover(fileRef._id);
+          }
+        } else {
+          console.error('Upload error:', error);
+        }
+      });
+
+      uploader.on('error', (error) => {
+        console.error('Upload error:', error);
+      });
+
+      uploads.push(uploader);
+      uploader.start();
+    } catch (error) {
+      console.error('Failed to create uploader:', error);
+    }
+  }
+
+  return uploads;
+}
+
 Template.previewClipboardImagePopup.onRendered(() => {
   // we can paste image from clipboard
   const handle = results => {

+ 8 - 0
client/components/cards/cardDetails.css

@@ -596,3 +596,11 @@ input[type="submit"].attachment-add-link-submit {
   overflow: hidden;
   background-color: #cecece;
 }
+
+/* Drag and drop file upload visual feedback */
+.js-card-details.is-dragging-over {
+  border: 2px dashed #0079bf;
+  background-color: #e3f2fd !important;
+  transform: scale(1.01);
+  transition: all 0.2s ease;
+}

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

@@ -12,6 +12,7 @@ import CardComments from '/models/cardComments';
 import { ALLOWED_COLORS } from '/config/const';
 import { UserAvatar } from '../users/userAvatar';
 import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
+import { handleFileUpload } from './attachments';
 
 const subManager = new SubsManager();
 const { calculateIndexData } = Utils;
@@ -481,6 +482,59 @@ BlazeComponent.extendComponent({
             }
           }
         },
+        // Drag and drop file upload handlers
+        'dragover .js-card-details'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+        },
+        'dragenter .js-card-details'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+          const card = this.data();
+          const board = card.board();
+          // Only allow drag-and-drop if user can modify card and board allows attachments
+          if (card.canModifyCard() && board && board.allowsAttachments) {
+            // Check if the drag contains files
+            const dataTransfer = event.originalEvent.dataTransfer;
+            if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
+              $(event.currentTarget).addClass('is-dragging-over');
+            }
+          }
+        },
+        'dragleave .js-card-details'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+          $(event.currentTarget).removeClass('is-dragging-over');
+        },
+        'drop .js-card-details'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+          $(event.currentTarget).removeClass('is-dragging-over');
+
+          const card = this.data();
+          const board = card.board();
+
+          // Check permissions
+          if (!card.canModifyCard() || !board || !board.allowsAttachments) {
+            return;
+          }
+
+          // Check if this is a file drop (not a checklist item reorder)
+          const dataTransfer = event.originalEvent.dataTransfer;
+          if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
+            return;
+          }
+
+          // Check if the drop contains files (not just text/HTML)
+          if (!dataTransfer.types.includes('Files')) {
+            return;
+          }
+
+          const files = dataTransfer.files;
+          if (files && files.length > 0) {
+            handleFileUpload(card, files);
+          }
+        },
       },
     ];
   },

+ 8 - 0
client/components/cards/minicard.css

@@ -580,3 +580,11 @@
 .text-green {
   color: #008000;
 }
+
+/* Drag and drop file upload visual feedback */
+.minicard.is-dragging-over {
+  border: 2px dashed #0079bf;
+  background-color: #e3f2fd !important;
+  transform: scale(1.02);
+  transition: all 0.2s ease;
+}

+ 55 - 1
client/components/cards/minicard.js

@@ -1,6 +1,7 @@
 import { ReactiveCache } from '/imports/reactiveCache';
 import { TAPi18n } from '/imports/i18n';
-import { CustomFieldStringTemplate } from '/client/lib/customFields'
+import { CustomFieldStringTemplate } from '/client/lib/customFields';
+import { handleFileUpload } from './attachments';
 
 // Template.cards.events({
 //   'click .member': Popup.open('cardMember')
@@ -107,6 +108,59 @@ BlazeComponent.extendComponent({
         'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
         'click .minicard-labels' : this.cardLabelsPopup,
         'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
+        // Drag and drop file upload handlers
+        'dragover .minicard'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+        },
+        'dragenter .minicard'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+          const card = this.data();
+          const board = card.board();
+          // Only allow drag-and-drop if user can modify card and board allows attachments
+          if (card.canModifyCard() && board && board.allowsAttachments) {
+            // Check if the drag contains files
+            const dataTransfer = event.originalEvent.dataTransfer;
+            if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
+              $(event.currentTarget).addClass('is-dragging-over');
+            }
+          }
+        },
+        'dragleave .minicard'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+          $(event.currentTarget).removeClass('is-dragging-over');
+        },
+        'drop .minicard'(event) {
+          event.preventDefault();
+          event.stopPropagation();
+          $(event.currentTarget).removeClass('is-dragging-over');
+
+          const card = this.data();
+          const board = card.board();
+
+          // Check permissions
+          if (!card.canModifyCard() || !board || !board.allowsAttachments) {
+            return;
+          }
+
+          // Check if this is a file drop (not a card reorder)
+          const dataTransfer = event.originalEvent.dataTransfer;
+          if (!dataTransfer || !dataTransfer.files || dataTransfer.files.length === 0) {
+            return;
+          }
+
+          // Check if the drop contains files (not just text/HTML)
+          if (!dataTransfer.types.includes('Files')) {
+            return;
+          }
+
+          const files = dataTransfer.files;
+          if (files && files.length > 0) {
+            handleFileUpload(card, files);
+          }
+        },
       }
     ];
   },

+ 1 - 1
models/boards.js

@@ -413,7 +413,7 @@ Boards.attachSchema(
        * Does the board allows cover attachment on minicard?
        */
       type: Boolean,
-      defaultValue: false,
+      defaultValue: true,
     },
 
     allowsBadgeAttachmentOnMinicard: {