mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Drag any files from file manager to minicard or opened card.
Thanks to xet7 ! Fixes #2936
This commit is contained in:
parent
85ac03a892
commit
3e9481c5bd
6 changed files with 210 additions and 37 deletions
|
|
@ -296,44 +296,13 @@ Template.cardAttachmentsPopup.events({
|
||||||
const files = event.currentTarget.files;
|
const files = event.currentTarget.files;
|
||||||
if (files) {
|
if (files) {
|
||||||
let uploads = [];
|
let uploads = [];
|
||||||
for (const file of files) {
|
const uploaders = handleFileUpload(card, files);
|
||||||
const fileId = new ObjectID().toString();
|
|
||||||
let fileName = DOMPurify.sanitize(file.name);
|
|
||||||
|
|
||||||
// If sanitized filename is not same as original filename,
|
uploaders.forEach(uploader => {
|
||||||
// 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;
|
|
||||||
const uploader = Attachments.insert(
|
|
||||||
config,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
uploader.on('start', function() {
|
uploader.on('start', function() {
|
||||||
uploads.push(this);
|
uploads.push(this);
|
||||||
templateInstance.uploads.set(uploads);
|
templateInstance.uploads.set(uploads);
|
||||||
});
|
});
|
||||||
uploader.on('uploaded', (error, fileRef) => {
|
|
||||||
if (!error) {
|
|
||||||
if (fileRef.isImage) {
|
|
||||||
card.setCover(fileRef._id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
uploader.on('end', (error, fileRef) => {
|
uploader.on('end', (error, fileRef) => {
|
||||||
uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
|
uploads = uploads.filter(_upload => _upload.config.fileId != fileRef._id);
|
||||||
templateInstance.uploads.set(uploads);
|
templateInstance.uploads.set(uploads);
|
||||||
|
|
@ -341,8 +310,7 @@ Template.cardAttachmentsPopup.events({
|
||||||
Popup.back();
|
Popup.back();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
uploader.start();
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'click .js-computer-upload'(event, templateInstance) {
|
'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;
|
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
|
||||||
let pastedResults = null;
|
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(() => {
|
Template.previewClipboardImagePopup.onRendered(() => {
|
||||||
// we can paste image from clipboard
|
// we can paste image from clipboard
|
||||||
const handle = results => {
|
const handle = results => {
|
||||||
|
|
|
||||||
|
|
@ -596,3 +596,11 @@ input[type="submit"].attachment-add-link-submit {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #cecece;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import CardComments from '/models/cardComments';
|
||||||
import { ALLOWED_COLORS } from '/config/const';
|
import { ALLOWED_COLORS } from '/config/const';
|
||||||
import { UserAvatar } from '../users/userAvatar';
|
import { UserAvatar } from '../users/userAvatar';
|
||||||
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
|
import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList';
|
||||||
|
import { handleFileUpload } from './attachments';
|
||||||
|
|
||||||
const subManager = new SubsManager();
|
const subManager = new SubsManager();
|
||||||
const { calculateIndexData } = Utils;
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -580,3 +580,11 @@
|
||||||
.text-green {
|
.text-green {
|
||||||
color: #008000;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ReactiveCache } from '/imports/reactiveCache';
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
import { TAPi18n } from '/imports/i18n';
|
import { TAPi18n } from '/imports/i18n';
|
||||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
import { CustomFieldStringTemplate } from '/client/lib/customFields';
|
||||||
|
import { handleFileUpload } from './attachments';
|
||||||
|
|
||||||
// Template.cards.events({
|
// Template.cards.events({
|
||||||
// 'click .member': Popup.open('cardMember')
|
// '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 span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
|
||||||
'click .minicard-labels' : this.cardLabelsPopup,
|
'click .minicard-labels' : this.cardLabelsPopup,
|
||||||
'click .js-open-minicard-details-menu': Popup.open('minicardDetailsActions'),
|
'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);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -413,7 +413,7 @@ Boards.attachSchema(
|
||||||
* Does the board allows cover attachment on minicard?
|
* Does the board allows cover attachment on minicard?
|
||||||
*/
|
*/
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
defaultValue: false,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
allowsBadgeAttachmentOnMinicard: {
|
allowsBadgeAttachmentOnMinicard: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue