diff --git a/client/components/boards/boardBody.css b/client/components/boards/boardBody.css index 32770eda6..a0ebce8f4 100644 --- a/client/components/boards/boardBody.css +++ b/client/components/boards/boardBody.css @@ -496,3 +496,10 @@ font-size: 25px; cursor: pointer; } + +/* Global file drag over state for board canvas */ +.board-canvas.file-drag-over { + background-color: rgba(0, 123, 255, 0.05) !important; + border: 2px dashed #007bff !important; + transition: all 0.2s ease; +} diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index c7d77eb93..cb13abebd 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -721,6 +721,31 @@ BlazeComponent.extendComponent({ } }, 'click .js-empty-board-add-swimlane': Popup.open('swimlaneAdd'), + // Global drag and drop file upload handlers for better visual feedback + 'dragover .board-canvas'(event) { + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + event.preventDefault(); + // Add visual indicator that files can be dropped + $('.board-canvas').addClass('file-drag-over'); + } + }, + 'dragleave .board-canvas'(event) { + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + // Only remove class if we're leaving the board canvas entirely + if (!event.currentTarget.contains(event.relatedTarget)) { + $('.board-canvas').removeClass('file-drag-over'); + } + } + }, + 'drop .board-canvas'(event) { + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + event.preventDefault(); + $('.board-canvas').removeClass('file-drag-over'); + } + }, }, ]; }, diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index 18788f22d..a883877e1 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -343,7 +343,7 @@ export function handleFileUpload(card, files) { } // Check if user can modify the card - if (!card.canModifyCard()) { + if (!Utils.canModifyCard()) { if (process.env.DEBUG === 'true') { console.warn('User does not have permission to modify this card'); } diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index da95765ba..df9e33a81 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -504,55 +504,58 @@ BlazeComponent.extendComponent({ }, // Drag and drop file upload handlers 'dragover .js-card-details'(event) { - event.preventDefault(); - event.stopPropagation(); + // Only prevent default for file drags to avoid interfering with other drag operations + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + 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')) { + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + 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 (Utils.canModifyCard() && board && board.allowsAttachments) { $(event.currentTarget).addClass('is-dragging-over'); } } }, 'dragleave .js-card-details'(event) { - event.preventDefault(); - event.stopPropagation(); - $(event.currentTarget).removeClass('is-dragging-over'); + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + 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; - } + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + event.preventDefault(); + event.stopPropagation(); + $(event.currentTarget).removeClass('is-dragging-over'); - // Check if the drop contains files (not just text/HTML) - if (!dataTransfer.types.includes('Files')) { - return; - } + const card = this.data(); + const board = card.board(); - const files = dataTransfer.files; - if (files && files.length > 0) { - handleFileUpload(card, files); + // Check permissions + if (!Utils.canModifyCard() || !board || !board.allowsAttachments) { + return; + } + + // Check if this is a file drop (not a checklist item reorder) + if (!dataTransfer.files || dataTransfer.files.length === 0) { + return; + } + + const files = dataTransfer.files; + if (files && files.length > 0) { + handleFileUpload(card, files); + } } }, }, diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 2cecccd7e..91ebddc8c 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -111,55 +111,58 @@ BlazeComponent.extendComponent({ 'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'), // Drag and drop file upload handlers 'dragover .minicard'(event) { - event.preventDefault(); - event.stopPropagation(); + // Only prevent default for file drags to avoid interfering with sortable + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + 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')) { + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + 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 (Utils.canModifyCard() && board && board.allowsAttachments) { $(event.currentTarget).addClass('is-dragging-over'); } } }, 'dragleave .minicard'(event) { - event.preventDefault(); - event.stopPropagation(); - $(event.currentTarget).removeClass('is-dragging-over'); + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + 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; - } + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + event.preventDefault(); + event.stopPropagation(); + $(event.currentTarget).removeClass('is-dragging-over'); - // Check if the drop contains files (not just text/HTML) - if (!dataTransfer.types.includes('Files')) { - return; - } + const card = this.data(); + const board = card.board(); - const files = dataTransfer.files; - if (files && files.length > 0) { - handleFileUpload(card, files); + // Check permissions + if (!Utils.canModifyCard() || !board || !board.allowsAttachments) { + return; + } + + // Check if this is a file drop (not a card reorder) + if (!dataTransfer.files || dataTransfer.files.length === 0) { + return; + } + + const files = dataTransfer.files; + if (files && files.length > 0) { + handleFileUpload(card, files); + } } }, } diff --git a/client/components/lists/list.js b/client/components/lists/list.js index 6c3695ebf..da7c16e16 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -157,6 +157,15 @@ BlazeComponent.extendComponent({ } else { $cards.sortable({ handle: '.minicard', + // Prevent sortable from interfering with file drag and drop + start: function(event, ui) { + // Check if this is a file drag operation + const dataTransfer = event.originalEvent?.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + // Cancel sortable for file drags + return false; + } + }, }); }