mirror of
https://github.com/wekan/wekan.git
synced 2026-03-13 17:06:13 +01:00
Convert cardDetails, cardCustomFields, cardDate, cardTime, cardDescription, attachments, checklists, labels, minicard, resultCard, and subtasks to use native Meteor Template pattern.
1842 lines
57 KiB
JavaScript
1842 lines
57 KiB
JavaScript
import { ReactiveCache } from '/imports/reactiveCache';
|
|
import { TAPi18n } from '/imports/i18n';
|
|
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
|
import {
|
|
setupDatePicker,
|
|
datePickerRendered,
|
|
datePickerHelpers,
|
|
datePickerEvents,
|
|
} from '/client/lib/datepicker';
|
|
import {
|
|
formatDateTime,
|
|
formatDate,
|
|
formatTime,
|
|
getISOWeek,
|
|
isValidDate,
|
|
isBefore,
|
|
isAfter,
|
|
isSame,
|
|
add,
|
|
subtract,
|
|
startOf,
|
|
endOf,
|
|
format,
|
|
parseDate,
|
|
now,
|
|
createDate,
|
|
fromNow,
|
|
calendar
|
|
} from '/imports/lib/dateUtils';
|
|
import Cards from '/models/cards';
|
|
import Boards from '/models/boards';
|
|
import Checklists from '/models/checklists';
|
|
import Integrations from '/models/integrations';
|
|
import Users from '/models/users';
|
|
import Lists from '/models/lists';
|
|
import CardComments from '/models/cardComments';
|
|
import { ALLOWED_COLORS } from '/config/const';
|
|
import { UserAvatar } from '../users/userAvatar';
|
|
import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard';
|
|
import { handleFileUpload } from './attachments';
|
|
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
|
|
import {
|
|
getCurrentCardIdFromContext,
|
|
getCurrentCardFromContext,
|
|
} from '/client/lib/currentCard';
|
|
import uploadProgressManager from '../../lib/uploadProgressManager';
|
|
|
|
const subManager = new SubsManager();
|
|
const { calculateIndexData } = Utils;
|
|
|
|
function getCardId() {
|
|
return getCurrentCardIdFromContext();
|
|
}
|
|
|
|
function getBoardBodyInstance() {
|
|
const boardBodyEl = document.querySelector('.board-body');
|
|
if (boardBodyEl) {
|
|
const view = Blaze.getView(boardBodyEl);
|
|
if (view && view.templateInstance) return view.templateInstance();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getCardDetailsElement(cardId) {
|
|
if (!cardId) {
|
|
return null;
|
|
}
|
|
|
|
const cardDetailsElements = document.querySelectorAll('.js-card-details');
|
|
for (const element of cardDetailsElements) {
|
|
if (Blaze.getData(element)?._id === cardId) {
|
|
return element;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
Template.cardDetails.onCreated(function () {
|
|
this.currentBoard = Utils.getCurrentBoard();
|
|
this.isLoaded = new ReactiveVar(false);
|
|
this.infiniteScrolling = new InfiniteScrolling();
|
|
|
|
const boardBody = getBoardBodyInstance();
|
|
if (boardBody !== null) {
|
|
// Only show overlay in mobile mode, not in desktop mode
|
|
const isMobile = Utils.getMobileMode();
|
|
if (isMobile) {
|
|
boardBody.showOverlay.set(true);
|
|
}
|
|
boardBody.mouseHasEnterCardDetails = false;
|
|
}
|
|
|
|
this.calculateNextPeak = () => {
|
|
const cardElement = this.find('.js-card-details');
|
|
if (cardElement) {
|
|
const altitude = cardElement.scrollHeight;
|
|
this.infiniteScrolling.setNextPeak(altitude);
|
|
}
|
|
};
|
|
|
|
this.reachNextPeak = () => {
|
|
const activitiesEl = this.find('.activities');
|
|
if (activitiesEl) {
|
|
const view = Blaze.getView(activitiesEl);
|
|
if (view && view.templateInstance) {
|
|
const activitiesTpl = view.templateInstance();
|
|
if (activitiesTpl && activitiesTpl.loadNextPage) {
|
|
activitiesTpl.loadNextPage();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Meteor.subscribe('unsaved-edits');
|
|
});
|
|
|
|
Template.cardDetails.onRendered(function () {
|
|
this.calculateNextPeak();
|
|
if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
|
|
// Send Webhook but not create Activities records ---
|
|
const card = Template.currentData();
|
|
const userId = Meteor.userId();
|
|
const params = {
|
|
userId,
|
|
cardId: card._id,
|
|
boardId: card.boardId,
|
|
listId: card.listId,
|
|
user: ReactiveCache.getCurrentUser().username,
|
|
url: '',
|
|
};
|
|
|
|
const integrations = ReactiveCache.getIntegrations({
|
|
boardId: { $in: [card.boardId, Integrations.Const.GLOBAL_WEBHOOK_ID] },
|
|
enabled: true,
|
|
activities: { $in: ['CardDetailsRendered', 'all'] },
|
|
});
|
|
|
|
if (integrations.length > 0) {
|
|
integrations.forEach((integration) => {
|
|
Meteor.call(
|
|
'outgoingWebhooks',
|
|
integration,
|
|
'CardSelected',
|
|
params,
|
|
() => { },
|
|
);
|
|
});
|
|
}
|
|
//-------------
|
|
}
|
|
|
|
const $checklistsDom = this.$('.card-checklist-items');
|
|
|
|
$checklistsDom.sortable({
|
|
tolerance: 'pointer',
|
|
helper: 'clone',
|
|
handle: '.checklist-title',
|
|
items: '.js-checklist',
|
|
placeholder: 'checklist placeholder',
|
|
distance: 7,
|
|
start(evt, ui) {
|
|
ui.placeholder.height(ui.helper.height());
|
|
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
|
},
|
|
stop(evt, ui) {
|
|
let prevChecklist = ui.item.prev('.js-checklist').get(0);
|
|
if (prevChecklist) {
|
|
prevChecklist = Blaze.getData(prevChecklist).checklist;
|
|
}
|
|
let nextChecklist = ui.item.next('.js-checklist').get(0);
|
|
if (nextChecklist) {
|
|
nextChecklist = Blaze.getData(nextChecklist).checklist;
|
|
}
|
|
const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
|
|
|
|
$checklistsDom.sortable('cancel');
|
|
const checklist = Blaze.getData(ui.item.get(0)).checklist;
|
|
|
|
Checklists.update(checklist._id, {
|
|
$set: {
|
|
sort: sortIndex.base,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
const $subtasksDom = this.$('.card-subtasks-items');
|
|
|
|
$subtasksDom.sortable({
|
|
tolerance: 'pointer',
|
|
helper: 'clone',
|
|
handle: '.subtask-title',
|
|
items: '.js-subtasks',
|
|
placeholder: 'subtasks placeholder',
|
|
distance: 7,
|
|
start(evt, ui) {
|
|
ui.placeholder.height(ui.helper.height());
|
|
EscapeActions.executeUpTo('popup-close');
|
|
},
|
|
stop(evt, ui) {
|
|
let prevChecklist = ui.item.prev('.js-subtasks').get(0);
|
|
if (prevChecklist) {
|
|
prevChecklist = Blaze.getData(prevChecklist).subtask;
|
|
}
|
|
let nextChecklist = ui.item.next('.js-subtasks').get(0);
|
|
if (nextChecklist) {
|
|
nextChecklist = Blaze.getData(nextChecklist).subtask;
|
|
}
|
|
const sortIndex = calculateIndexData(prevChecklist, nextChecklist, 1);
|
|
|
|
$subtasksDom.sortable('cancel');
|
|
const subtask = Blaze.getData(ui.item.get(0)).subtask;
|
|
|
|
Subtasks.update(subtask._id, {
|
|
$set: {
|
|
subtaskSort: sortIndex.base,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
function userIsMember() {
|
|
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
|
}
|
|
|
|
// Disable sorting if the current user is not a board member
|
|
this.autorun(() => {
|
|
const disabled = !userIsMember();
|
|
if (
|
|
$checklistsDom.data('uiSortable') ||
|
|
$checklistsDom.data('sortable')
|
|
) {
|
|
$checklistsDom.sortable('option', 'disabled', disabled);
|
|
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
|
$checklistsDom.sortable({ handle: '.checklist-handle' });
|
|
}
|
|
}
|
|
if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
|
|
$subtasksDom.sortable('option', 'disabled', disabled);
|
|
}
|
|
});
|
|
});
|
|
|
|
Template.cardDetails.onDestroyed(function () {
|
|
const boardBody = getBoardBodyInstance();
|
|
if (boardBody === null) return;
|
|
boardBody.showOverlay.set(false);
|
|
});
|
|
|
|
Template.cardDetails.helpers({
|
|
isWatching() {
|
|
const card = Template.currentData();
|
|
if (!card || typeof card.findWatcher !== 'function') return false;
|
|
return card.findWatcher(Meteor.userId());
|
|
},
|
|
|
|
customFieldsGrid() {
|
|
return ReactiveCache.getCurrentUser().hasCustomFieldsGrid();
|
|
},
|
|
|
|
cardMaximized() {
|
|
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
|
|
},
|
|
|
|
showActivities() {
|
|
const user = ReactiveCache.getCurrentUser();
|
|
return user && user.hasShowActivities();
|
|
},
|
|
|
|
cardCollapsed() {
|
|
const user = ReactiveCache.getCurrentUser();
|
|
if (user && user.profile) {
|
|
return !!user.profile.cardCollapsed;
|
|
}
|
|
if (Users.getPublicCardCollapsed) {
|
|
const stored = Users.getPublicCardCollapsed();
|
|
if (typeof stored === 'boolean') return stored;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
presentParentTask() {
|
|
const tpl = Template.instance();
|
|
let result = tpl.currentBoard.presentParentTask;
|
|
if (result === null || result === undefined) {
|
|
result = 'no-parent';
|
|
}
|
|
return result;
|
|
},
|
|
|
|
linkForCard() {
|
|
const card = Template.currentData();
|
|
let result = '#';
|
|
if (card) {
|
|
const board = ReactiveCache.getBoard(card.boardId);
|
|
if (board) {
|
|
result = FlowRouter.path('card', {
|
|
boardId: card.boardId,
|
|
slug: board.slug,
|
|
cardId: card._id,
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
showVotingButtons() {
|
|
const card = Template.currentData();
|
|
return (
|
|
(currentUser.isBoardMember() ||
|
|
(currentUser && card.voteAllowNonBoardMembers())) &&
|
|
!card.expiredVote()
|
|
);
|
|
},
|
|
|
|
showPlanningPokerButtons() {
|
|
const card = Template.currentData();
|
|
return (
|
|
(currentUser.isBoardMember() ||
|
|
(currentUser && card.pokerAllowNonBoardMembers())) &&
|
|
!card.expiredPoker()
|
|
);
|
|
},
|
|
|
|
isVerticalScrollbars() {
|
|
const user = ReactiveCache.getCurrentUser();
|
|
return user && user.isVerticalScrollbars();
|
|
},
|
|
|
|
isCurrentListId(listId) {
|
|
const data = Template.currentData();
|
|
if (!data || typeof data.listId === 'undefined') return false;
|
|
return data.listId == listId;
|
|
},
|
|
|
|
isLoaded() {
|
|
return Template.instance().isLoaded;
|
|
},
|
|
});
|
|
|
|
Template.cardDetails.events({
|
|
[`${CSSEvents.transitionend} .js-card-details`](event, tpl) {
|
|
tpl.isLoaded.set(true);
|
|
},
|
|
[`${CSSEvents.animationend} .js-card-details`](event, tpl) {
|
|
tpl.isLoaded.set(true);
|
|
},
|
|
'scroll .js-card-details'(event, tpl) {
|
|
tpl.infiniteScrolling.checkScrollPosition(event.currentTarget, () => {
|
|
tpl.reachNextPeak();
|
|
});
|
|
},
|
|
'click .js-card-collapse-toggle'(event, tpl) {
|
|
const user = ReactiveCache.getCurrentUser();
|
|
const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
|
|
if (user) {
|
|
Meteor.call('setCardCollapsed', !currentState);
|
|
} else if (Users.setPublicCardCollapsed) {
|
|
Users.setPublicCardCollapsed(!currentState);
|
|
}
|
|
},
|
|
'mousedown .js-card-drag-handle'(event) {
|
|
event.preventDefault();
|
|
const $card = $(event.target).closest('.card-details');
|
|
const startX = event.clientX;
|
|
const startY = event.clientY;
|
|
const startLeft = $card.offset().left;
|
|
const startTop = $card.offset().top;
|
|
|
|
const onMouseMove = (e) => {
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
$card.css({
|
|
left: startLeft + deltaX + 'px',
|
|
top: startTop + deltaY + 'px'
|
|
});
|
|
};
|
|
|
|
const onMouseUp = () => {
|
|
$(document).off('mousemove', onMouseMove);
|
|
$(document).off('mouseup', onMouseUp);
|
|
};
|
|
|
|
$(document).on('mousemove', onMouseMove);
|
|
$(document).on('mouseup', onMouseUp);
|
|
},
|
|
'mousedown .js-card-title-drag-handle'(event) {
|
|
// Allow dragging from title for ReadOnly users
|
|
// Don't interfere with text selection
|
|
if (event.target.tagName === 'A' || $(event.target).closest('a').length > 0) {
|
|
return; // Don't drag if clicking on links
|
|
}
|
|
|
|
event.preventDefault();
|
|
const $card = $(event.target).closest('.card-details');
|
|
const startX = event.clientX;
|
|
const startY = event.clientY;
|
|
const startLeft = $card.offset().left;
|
|
const startTop = $card.offset().top;
|
|
|
|
const onMouseMove = (e) => {
|
|
const deltaX = e.clientX - startX;
|
|
const deltaY = e.clientY - startY;
|
|
$card.css({
|
|
left: startLeft + deltaX + 'px',
|
|
top: startTop + deltaY + 'px'
|
|
});
|
|
};
|
|
|
|
const onMouseUp = () => {
|
|
$(document).off('mousemove', onMouseMove);
|
|
$(document).off('mouseup', onMouseUp);
|
|
};
|
|
|
|
$(document).on('mousemove', onMouseMove);
|
|
$(document).on('mouseup', onMouseUp);
|
|
},
|
|
'click .js-close-card-details'(event, tpl) {
|
|
// Get board ID from either the card data or current board in session
|
|
const card = Template.currentData();
|
|
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
|
|
const cardId = card && card._id;
|
|
|
|
if (boardId) {
|
|
// In desktop mode, remove from openCards array
|
|
const isMobile = Utils.getMobileMode();
|
|
if (!isMobile && cardId) {
|
|
const openCards = Session.get('openCards') || [];
|
|
const filtered = openCards.filter(id => id !== cardId);
|
|
Session.set('openCards', filtered);
|
|
|
|
// If this was the current card, clear it
|
|
if (Session.get('currentCard') === cardId) {
|
|
Session.set('currentCard', null);
|
|
}
|
|
// Don't navigate away in desktop mode - just close the card
|
|
return;
|
|
}
|
|
|
|
// Mobile mode: Clear the current card session to close the card
|
|
Session.set('currentCard', null);
|
|
|
|
// Navigate back to board without card
|
|
const board = ReactiveCache.getBoard(boardId);
|
|
if (board) {
|
|
FlowRouter.go('board', {
|
|
id: board._id,
|
|
slug: board.slug,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
'click .js-copy-link'(event, tpl) {
|
|
event.preventDefault();
|
|
const card = Template.currentData();
|
|
const url = card.absoluteUrl();
|
|
const promise = Utils.copyTextToClipboard(url);
|
|
|
|
const $tooltip = tpl.$('.card-details-header .copied-tooltip');
|
|
Utils.showCopied(promise, $tooltip);
|
|
},
|
|
'change .js-date-format-selector'(event) {
|
|
const dateFormat = event.target.value;
|
|
Meteor.call('changeDateFormat', dateFormat);
|
|
},
|
|
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
|
|
// Mobile: switch to desktop popup view (maximize)
|
|
'click .js-mobile-switch-to-desktop'(event) {
|
|
event.preventDefault();
|
|
// Switch global mode to desktop so the card appears as desktop popup
|
|
Utils.setMobileMode(false);
|
|
},
|
|
'click .js-card-zoom-in'(event) {
|
|
event.preventDefault();
|
|
const current = Utils.getCardZoom();
|
|
const newZoom = Math.min(3.0, current + 0.1);
|
|
Utils.setCardZoom(newZoom);
|
|
},
|
|
'click .js-card-zoom-out'(event) {
|
|
event.preventDefault();
|
|
const current = Utils.getCardZoom();
|
|
const newZoom = Math.max(0.5, current - 0.1);
|
|
Utils.setCardZoom(newZoom);
|
|
},
|
|
'click .js-card-mobile-desktop-toggle'(event) {
|
|
event.preventDefault();
|
|
const currentMode = Utils.getMobileMode();
|
|
Utils.setMobileMode(!currentMode);
|
|
},
|
|
async 'submit .js-card-description'(event, tpl) {
|
|
event.preventDefault();
|
|
const description = tpl.find('.js-new-description-input').value;
|
|
const card = Template.currentData();
|
|
await card.setDescription(description);
|
|
},
|
|
async 'submit .js-card-details-title'(event, tpl) {
|
|
event.preventDefault();
|
|
const titleInput = tpl.find('.js-edit-card-title');
|
|
const title = titleInput ? titleInput.value.trim() : '';
|
|
const card = Template.currentData();
|
|
if (title) {
|
|
await card.setTitle(title);
|
|
} else {
|
|
await card.setTitle('');
|
|
}
|
|
},
|
|
'submit .js-card-details-assigner'(event, tpl) {
|
|
event.preventDefault();
|
|
const assignerInput = tpl.find('.js-edit-card-assigner');
|
|
const assigner = assignerInput ? assignerInput.value.trim() : '';
|
|
const card = Template.currentData();
|
|
if (assigner) {
|
|
card.setAssignedBy(assigner);
|
|
} else {
|
|
card.setAssignedBy('');
|
|
}
|
|
},
|
|
'submit .js-card-details-requester'(event, tpl) {
|
|
event.preventDefault();
|
|
const requesterInput = tpl.find('.js-edit-card-requester');
|
|
const requester = requesterInput ? requesterInput.value.trim() : '';
|
|
const card = Template.currentData();
|
|
if (requester) {
|
|
card.setRequestedBy(requester);
|
|
} else {
|
|
card.setRequestedBy('');
|
|
}
|
|
},
|
|
'keydown input.js-edit-card-sort'(evt, tpl) {
|
|
// enter = save
|
|
if (evt.keyCode === 13) {
|
|
tpl.find('button[type=submit]').click();
|
|
}
|
|
},
|
|
async 'submit .js-card-details-sort'(event, tpl) {
|
|
event.preventDefault();
|
|
const sortInput = tpl.find('.js-edit-card-sort');
|
|
const sort = parseFloat(sortInput ? sortInput.value.trim() : '');
|
|
if (!Number.isNaN(sort)) {
|
|
let card = Template.currentData();
|
|
await card.move(card.boardId, card.swimlaneId, card.listId, sort);
|
|
}
|
|
},
|
|
async 'change .js-select-card-details-lists'(event, tpl) {
|
|
let card = Template.currentData();
|
|
const listSelect = tpl.$('.js-select-card-details-lists')[0];
|
|
const listId = listSelect.options[listSelect.selectedIndex].value;
|
|
|
|
const minOrder = card.getMinSort(listId, card.swimlaneId);
|
|
await card.move(card.boardId, card.swimlaneId, listId, minOrder - 1);
|
|
},
|
|
'click .js-go-to-linked-card'() {
|
|
const card = Template.currentData();
|
|
Utils.goCardId(card.linkedId);
|
|
},
|
|
'click .js-member': Popup.open('cardMember'),
|
|
'click .js-add-members': Popup.open('cardMembers'),
|
|
'click .js-assignee': Popup.open('cardAssignee'),
|
|
'click .js-add-assignees': Popup.open('cardAssignees'),
|
|
'click .js-add-labels': Popup.open('cardLabels'),
|
|
'click .js-received-date': Popup.open('editCardReceivedDate'),
|
|
'click .js-start-date': Popup.open('editCardStartDate'),
|
|
'click .js-due-date': Popup.open('editCardDueDate'),
|
|
'click .js-end-date': Popup.open('editCardEndDate'),
|
|
'click .js-show-positive-votes': Popup.open('positiveVoteMembers'),
|
|
'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
|
|
'click .js-custom-fields': Popup.open('cardCustomFields'),
|
|
'mouseenter .js-card-details'(event, tpl) {
|
|
const boardBody = getBoardBodyInstance(tpl);
|
|
if (boardBody === null) return;
|
|
boardBody.showOverlay.set(true);
|
|
boardBody.mouseHasEnterCardDetails = true;
|
|
},
|
|
'mousedown .js-card-details'() {
|
|
Session.set('cardDetailsIsDragging', false);
|
|
Session.set('cardDetailsIsMouseDown', true);
|
|
},
|
|
'mousemove .js-card-details'() {
|
|
if (Session.get('cardDetailsIsMouseDown')) {
|
|
Session.set('cardDetailsIsDragging', true);
|
|
}
|
|
},
|
|
'mouseup .js-card-details'() {
|
|
Session.set('cardDetailsIsDragging', false);
|
|
Session.set('cardDetailsIsMouseDown', false);
|
|
},
|
|
async 'click #toggleHideCheckedChecklistItems'() {
|
|
const card = Template.currentData();
|
|
await card.toggleHideCheckedChecklistItems();
|
|
},
|
|
'click #toggleCustomFieldsGridButton'() {
|
|
Meteor.call('toggleCustomFieldsGrid');
|
|
},
|
|
'click .js-maximize-card-details'() {
|
|
Meteor.call('toggleCardMaximized');
|
|
autosize($('.card-details'));
|
|
},
|
|
'click .js-minimize-card-details'() {
|
|
Meteor.call('toggleCardMaximized');
|
|
autosize($('.card-details'));
|
|
},
|
|
'click .js-vote'(e) {
|
|
const card = Template.currentData();
|
|
const forIt = $(e.target).hasClass('js-vote-positive');
|
|
let newState = null;
|
|
if (
|
|
card.voteState() === null ||
|
|
(card.voteState() === false && forIt) ||
|
|
(card.voteState() === true && !forIt)
|
|
) {
|
|
newState = forIt;
|
|
}
|
|
// Use secure server method; direct client updates to vote are blocked
|
|
Meteor.call('cards.vote', card._id, newState);
|
|
},
|
|
'click .js-poker'(e) {
|
|
const card = Template.currentData();
|
|
let newState = null;
|
|
if ($(e.target).hasClass('js-poker-vote-one')) {
|
|
newState = 'one';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-two')) {
|
|
newState = 'two';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-three')) {
|
|
newState = 'three';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-five')) {
|
|
newState = 'five';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-eight')) {
|
|
newState = 'eight';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-thirteen')) {
|
|
newState = 'thirteen';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-twenty')) {
|
|
newState = 'twenty';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-forty')) {
|
|
newState = 'forty';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-one-hundred')) {
|
|
newState = 'oneHundred';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
if ($(e.target).hasClass('js-poker-vote-unsure')) {
|
|
newState = 'unsure';
|
|
Meteor.call('cards.pokerVote', card._id, newState);
|
|
}
|
|
},
|
|
'click .js-poker-finish'(e) {
|
|
if ($(e.target).hasClass('js-poker-finish')) {
|
|
e.preventDefault();
|
|
const card = Template.currentData();
|
|
const now = new Date();
|
|
Meteor.call('cards.setPokerEnd', card._id, now);
|
|
}
|
|
},
|
|
'click .js-poker-replay'(e) {
|
|
if ($(e.target).hasClass('js-poker-replay')) {
|
|
e.preventDefault();
|
|
const currentCard = Template.currentData();
|
|
Meteor.call('cards.replayPoker', currentCard._id);
|
|
Meteor.call('cards.unsetPokerEnd', currentCard._id);
|
|
Meteor.call('cards.unsetPokerEstimation', currentCard._id);
|
|
}
|
|
},
|
|
'click .js-poker-estimation'(event, tpl) {
|
|
event.preventDefault();
|
|
const card = Template.currentData();
|
|
const ruleTitle = tpl.find('#pokerEstimation').value;
|
|
if (ruleTitle !== undefined && ruleTitle !== '') {
|
|
tpl.find('#pokerEstimation').value = '';
|
|
|
|
if (ruleTitle) {
|
|
Meteor.call('cards.setPokerEstimation', card._id, parseInt(ruleTitle, 10));
|
|
} else {
|
|
Meteor.call('cards.unsetPokerEstimation', card._id);
|
|
}
|
|
}
|
|
},
|
|
// Drag and drop file upload handlers
|
|
'dragover .js-card-details'(event) {
|
|
// 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) {
|
|
const dataTransfer = event.originalEvent.dataTransfer;
|
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const card = Template.currentData();
|
|
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) {
|
|
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) {
|
|
const dataTransfer = event.originalEvent.dataTransfer;
|
|
if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
$(event.currentTarget).removeClass('is-dragging-over');
|
|
|
|
const card = Template.currentData();
|
|
const board = card.board();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
Template.cardDetails.helpers({
|
|
isPopup() {
|
|
let ret = !!Utils.getPopupCardId();
|
|
return ret;
|
|
},
|
|
isDateFormat(format) {
|
|
const currentUser = ReactiveCache.getCurrentUser();
|
|
if (!currentUser) return format === 'YYYY-MM-DD';
|
|
return currentUser.getDateFormat() === format;
|
|
},
|
|
// Upload progress helpers
|
|
hasActiveUploads() {
|
|
return uploadProgressManager.hasActiveUploads(this._id);
|
|
},
|
|
uploads() {
|
|
return uploadProgressManager.getUploadsForCard(this._id);
|
|
},
|
|
uploadCount() {
|
|
return uploadProgressManager.getUploadCountForCard(this._id);
|
|
}
|
|
});
|
|
Template.cardDetailsPopup.onDestroyed(() => {
|
|
Session.delete('popupCardId');
|
|
Session.delete('popupCardBoardId');
|
|
});
|
|
Template.cardDetailsPopup.helpers({
|
|
popupCard() {
|
|
const ret = Utils.getPopupCard();
|
|
return ret;
|
|
},
|
|
});
|
|
|
|
Template.exportCardPopup.helpers({
|
|
withApi() {
|
|
return Template.instance().apiEnabled.get();
|
|
},
|
|
exportUrlCardPDF() {
|
|
const card = getCurrentCardFromContext({ ignorePopupCard: true }) || this;
|
|
const params = {
|
|
boardId: card.boardId || Session.get('currentBoard'),
|
|
listId: card.listId,
|
|
cardId: card._id || card.cardId,
|
|
};
|
|
const queryParams = {
|
|
authToken: Accounts._storedLoginToken(),
|
|
};
|
|
return FlowRouter.path(
|
|
'/api/boards/:boardId/lists/:listId/cards/:cardId/exportPDF',
|
|
params,
|
|
queryParams,
|
|
);
|
|
},
|
|
exportFilenameCardPDF() {
|
|
const card = getCurrentCardFromContext({ ignorePopupCard: true }) || this;
|
|
return `${String(card.title || 'export-card')
|
|
.replace(/[^a-z0-9._-]+/gi, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '') || 'export-card'}.pdf`;
|
|
},
|
|
});
|
|
|
|
// only allow number input
|
|
Template.editCardSortOrderForm.onRendered(function () {
|
|
this.$('input').on("keypress paste", function (event) {
|
|
let keyCode = event.keyCode;
|
|
let charCode = String.fromCharCode(keyCode);
|
|
let regex = new RegExp('[-0-9.]');
|
|
let ret = regex.test(charCode);
|
|
// only working here, defining in events() doesn't handle the return value correctly
|
|
return ret;
|
|
});
|
|
});
|
|
|
|
// inlinedCardDescription extends the normal inlinedForm to support UnsavedEdits
|
|
// draft feature for card descriptions.
|
|
Template.inlinedCardDescription.onCreated(function () {
|
|
this.isOpen = new ReactiveVar(false);
|
|
|
|
this._getUnsavedEditKey = () => ({
|
|
fieldName: 'cardDescription',
|
|
docId: getCardId(),
|
|
});
|
|
|
|
this._getValue = () => {
|
|
const input = this.find('textarea,input[type=text]');
|
|
return this.isOpen.get() && input && input.value.replaceAll(/[ \f\r\t\v]+$/gm, '');
|
|
};
|
|
|
|
this._close = (isReset = false) => {
|
|
if (this.isOpen.get() && !isReset) {
|
|
const draft = (this._getValue() || '').trim();
|
|
const card = getCurrentCardFromContext();
|
|
if (card && draft !== card.getDescription()) {
|
|
UnsavedEdits.set(this._getUnsavedEditKey(), this._getValue());
|
|
}
|
|
}
|
|
this.isOpen.set(false);
|
|
};
|
|
|
|
this._reset = () => {
|
|
UnsavedEdits.reset(this._getUnsavedEditKey());
|
|
this._close(true);
|
|
};
|
|
});
|
|
|
|
Template.inlinedCardDescription.helpers({
|
|
isOpen() {
|
|
return Template.instance().isOpen;
|
|
},
|
|
});
|
|
|
|
Template.inlinedCardDescription.events({
|
|
'click .js-close-inlined-form'(evt, tpl) {
|
|
tpl._reset();
|
|
},
|
|
'click .js-open-inlined-form'(evt, tpl) {
|
|
evt.preventDefault();
|
|
EscapeActions.clickExecute(evt.target, 'inlinedForm');
|
|
tpl.isOpen.set(true);
|
|
},
|
|
'keydown form textarea'(evt, tpl) {
|
|
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
|
tpl.find('button[type=submit]').click();
|
|
}
|
|
},
|
|
submit(evt, tpl) {
|
|
const data = Template.currentData();
|
|
if (data.autoclose !== false) {
|
|
Tracker.afterFlush(() => {
|
|
tpl._close();
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
Template.cardDetailsActionsPopup.helpers({
|
|
isWatching() {
|
|
if (!this || typeof this.findWatcher !== 'function') return false;
|
|
return this.findWatcher(Meteor.userId());
|
|
},
|
|
|
|
isBoardAdmin() {
|
|
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
|
},
|
|
|
|
showListOnMinicard() {
|
|
return this.showListOnMinicard;
|
|
},
|
|
});
|
|
|
|
Template.cardDetailsActionsPopup.events({
|
|
'click .js-export-card': Popup.open('exportCard'),
|
|
'click .js-members': Popup.open('cardMembers'),
|
|
'click .js-assignees': Popup.open('cardAssignees'),
|
|
'click .js-attachments': Popup.open('cardAttachments'),
|
|
'click .js-start-voting': Popup.open('cardStartVoting'),
|
|
'click .js-start-planning-poker': Popup.open('cardStartPlanningPoker'),
|
|
'click .js-custom-fields': Popup.open('cardCustomFields'),
|
|
'click .js-received-date': Popup.open('editCardReceivedDate'),
|
|
'click .js-start-date': Popup.open('editCardStartDate'),
|
|
'click .js-due-date': Popup.open('editCardDueDate'),
|
|
'click .js-end-date': Popup.open('editCardEndDate'),
|
|
'click .js-spent-time': Popup.open('editCardSpentTime'),
|
|
'click .js-move-card': Popup.open('moveCard'),
|
|
'click .js-copy-card': Popup.open('copyCard'),
|
|
'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
|
|
'click .js-copy-checklist-cards': Popup.open('copyManyCards'),
|
|
'click .js-set-card-color': Popup.open('setCardColor'),
|
|
async 'click .js-move-card-to-top'(event) {
|
|
event.preventDefault();
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
const minOrder = card.getMinSort() || 0;
|
|
await card.move(card.boardId, card.swimlaneId, card.listId, minOrder - 1);
|
|
Popup.back();
|
|
},
|
|
async 'click .js-move-card-to-bottom'(event) {
|
|
event.preventDefault();
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
const maxOrder = card.getMaxSort() || 0;
|
|
await card.move(card.boardId, card.swimlaneId, card.listId, maxOrder + 1);
|
|
Popup.back();
|
|
},
|
|
'click .js-archive': Popup.afterConfirm('cardArchive', async function () {
|
|
const card = Cards.findOne(getCardId());
|
|
Popup.close();
|
|
if (!card) return;
|
|
await card.archive();
|
|
Utils.goBoardId(card.boardId);
|
|
}),
|
|
'click .js-more': Popup.open('cardMore'),
|
|
'click .js-toggle-watch-card'() {
|
|
const currentCard = Cards.findOne(getCardId());
|
|
if (!currentCard) return;
|
|
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
|
|
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
|
|
if (!err && ret) Popup.close();
|
|
});
|
|
},
|
|
'click .js-toggle-show-list-on-minicard'() {
|
|
const currentCard = Cards.findOne(getCardId());
|
|
if (!currentCard) return;
|
|
const newValue = !currentCard.showListOnMinicard;
|
|
Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } });
|
|
Popup.close();
|
|
},
|
|
});
|
|
|
|
Template.editCardTitleForm.onRendered(function () {
|
|
autosize(this.$('textarea.js-edit-card-title'));
|
|
});
|
|
|
|
Template.editCardTitleForm.events({
|
|
'click a.fa.fa-copy'(event, tpl) {
|
|
const $editor = tpl.$('textarea');
|
|
const promise = Utils.copyTextToClipboard($editor[0].value);
|
|
|
|
const $tooltip = tpl.$('.copied-tooltip');
|
|
Utils.showCopied(promise, $tooltip);
|
|
},
|
|
'keydown .js-edit-card-title'(event) {
|
|
// If enter key was pressed, submit the data
|
|
// Unless the shift key is also being pressed
|
|
if (event.keyCode === 13 && !event.shiftKey) {
|
|
$('.js-submit-edit-card-title-form').click();
|
|
}
|
|
},
|
|
});
|
|
|
|
Template.cardMembersPopup.onCreated(function () {
|
|
let currBoard = Utils.getCurrentBoard();
|
|
let members = currBoard.activeMembers();
|
|
this.members = new ReactiveVar(members);
|
|
});
|
|
|
|
Template.cardMembersPopup.events({
|
|
'click .js-select-member'(event) {
|
|
const card = getCurrentCardFromContext();
|
|
if (!card) return;
|
|
const memberId = this.userId;
|
|
card.toggleMember(memberId);
|
|
event.preventDefault();
|
|
},
|
|
'keyup .card-members-filter'(event) {
|
|
const members = filterMembers(event.target.value);
|
|
Template.instance().members.set(members);
|
|
}
|
|
});
|
|
|
|
Template.cardMembersPopup.helpers({
|
|
isCardMember() {
|
|
const card = getCurrentCardFromContext();
|
|
if (!card) return false;
|
|
const cardMembers = card.getMembers();
|
|
|
|
return _.contains(cardMembers, this.userId);
|
|
},
|
|
|
|
members() {
|
|
const members = Template.instance().members.get();
|
|
const uniqueMembers = _.uniq(members, 'userId');
|
|
return _.sortBy(uniqueMembers, member => {
|
|
const user = ReactiveCache.getUser(member.userId);
|
|
return user ? user.profile.fullname : '';
|
|
});
|
|
},
|
|
userData() {
|
|
return ReactiveCache.getUser(this.userId);
|
|
},
|
|
});
|
|
|
|
const filterMembers = (filterTerm) => {
|
|
let currBoard = Utils.getCurrentBoard();
|
|
let members = currBoard.activeMembers();
|
|
|
|
if (filterTerm) {
|
|
members = members
|
|
.map(member => ({
|
|
member,
|
|
user: ReactiveCache.getUser(member.userId)
|
|
}))
|
|
.filter(({ user }) =>
|
|
(user.profile.fullname !== undefined && user.profile.fullname.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
|
|
|| user.profile.fullname === undefined && user.profile.username !== undefined && user.profile.username.toLowerCase().indexOf(filterTerm.toLowerCase()) !== -1)
|
|
.map(({ member }) => member);
|
|
}
|
|
return members;
|
|
}
|
|
|
|
Template.editCardRequesterForm.onRendered(function () {
|
|
autosize(this.$('.js-edit-card-requester'));
|
|
});
|
|
|
|
Template.editCardRequesterForm.events({
|
|
'keydown .js-edit-card-requester'(event) {
|
|
// If enter key was pressed, submit the data
|
|
if (event.keyCode === 13) {
|
|
$('.js-submit-edit-card-requester-form').click();
|
|
}
|
|
},
|
|
});
|
|
|
|
Template.editCardAssignerForm.onRendered(function () {
|
|
autosize(this.$('.js-edit-card-assigner'));
|
|
});
|
|
|
|
Template.editCardAssignerForm.events({
|
|
'keydown .js-edit-card-assigner'(event) {
|
|
// If enter key was pressed, submit the data
|
|
if (event.keyCode === 13) {
|
|
$('.js-submit-edit-card-assigner-form').click();
|
|
}
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Helper: register standard board/swimlane/list/card dialog helpers and events
|
|
* for a template that uses BoardSwimlaneListCardDialog.
|
|
*/
|
|
function registerCardDialogTemplate(templateName) {
|
|
Template[templateName].helpers({
|
|
boards() {
|
|
return Template.instance().dialog.boards();
|
|
},
|
|
swimlanes() {
|
|
return Template.instance().dialog.swimlanes();
|
|
},
|
|
lists() {
|
|
return Template.instance().dialog.lists();
|
|
},
|
|
cards() {
|
|
return Template.instance().dialog.cards();
|
|
},
|
|
isDialogOptionBoardId(boardId) {
|
|
return Template.instance().dialog.isDialogOptionBoardId(boardId);
|
|
},
|
|
isDialogOptionSwimlaneId(swimlaneId) {
|
|
return Template.instance().dialog.isDialogOptionSwimlaneId(swimlaneId);
|
|
},
|
|
isDialogOptionListId(listId) {
|
|
return Template.instance().dialog.isDialogOptionListId(listId);
|
|
},
|
|
isDialogOptionCardId(cardId) {
|
|
return Template.instance().dialog.isDialogOptionCardId(cardId);
|
|
},
|
|
isTitleDefault(title) {
|
|
return Template.instance().dialog.isTitleDefault(title);
|
|
},
|
|
});
|
|
|
|
Template[templateName].events({
|
|
async 'click .js-done'(event, tpl) {
|
|
const dialog = tpl.dialog;
|
|
const boardSelect = tpl.$('.js-select-boards')[0];
|
|
const boardId = boardSelect?.options[boardSelect?.selectedIndex]?.value;
|
|
|
|
const listSelect = tpl.$('.js-select-lists')[0];
|
|
const listId = listSelect?.options[listSelect?.selectedIndex]?.value;
|
|
|
|
const swimlaneSelect = tpl.$('.js-select-swimlanes')[0];
|
|
const swimlaneId = swimlaneSelect?.options[swimlaneSelect?.selectedIndex]?.value;
|
|
|
|
const cardSelect = tpl.$('.js-select-cards')[0];
|
|
const cardId = cardSelect?.options?.length > 0
|
|
? cardSelect.options[cardSelect.selectedIndex].value
|
|
: null;
|
|
|
|
const options = { boardId, swimlaneId, listId, cardId };
|
|
try {
|
|
await dialog.setDone(cardId, options);
|
|
} catch (e) {
|
|
console.error('Error in card dialog operation:', e);
|
|
}
|
|
Popup.back(2);
|
|
},
|
|
'change .js-select-boards'(event, tpl) {
|
|
tpl.dialog.getBoardData($(event.currentTarget).val());
|
|
},
|
|
'change .js-select-swimlanes'(event, tpl) {
|
|
tpl.dialog.selectedSwimlaneId.set($(event.currentTarget).val());
|
|
tpl.dialog.setFirstListId();
|
|
},
|
|
'change .js-select-lists'(event, tpl) {
|
|
tpl.dialog.selectedListId.set($(event.currentTarget).val());
|
|
tpl.dialog.selectedCardId.set('');
|
|
},
|
|
'change .js-select-cards'(event, tpl) {
|
|
tpl.dialog.selectedCardId.set($(event.currentTarget).val());
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Move Card Dialog */
|
|
Template.moveCardPopup.onCreated(function () {
|
|
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
|
getDialogOptions() {
|
|
return ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
|
},
|
|
async setDone(cardId, options) {
|
|
const tpl = Template.instance();
|
|
const position = tpl.$('input[name="position"]:checked').val();
|
|
|
|
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
|
const card = Template.currentData();
|
|
let sortIndex = 0;
|
|
|
|
if (cardId) {
|
|
const targetCard = ReactiveCache.getCard(cardId);
|
|
if (targetCard) {
|
|
const targetSort = targetCard.sort || 0;
|
|
if (position === 'above') {
|
|
sortIndex = targetSort - 0.5;
|
|
} else {
|
|
sortIndex = targetSort + 0.5;
|
|
}
|
|
}
|
|
} else {
|
|
const maxSort = card.getMaxSort(options.listId, options.swimlaneId);
|
|
sortIndex = (typeof maxSort === 'number' && !Number.isNaN(maxSort)) ? maxSort + 1 : 0;
|
|
}
|
|
|
|
await card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
|
},
|
|
});
|
|
});
|
|
registerCardDialogTemplate('moveCardPopup');
|
|
|
|
/** Copy Card Dialog */
|
|
Template.copyCardPopup.onCreated(function () {
|
|
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
|
getDialogOptions() {
|
|
return ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
|
},
|
|
async setDone(cardId, options) {
|
|
const tpl = Template.instance();
|
|
const textarea = tpl.$('#copy-card-title');
|
|
const title = textarea.val().trim();
|
|
const position = tpl.$('input[name="position"]:checked').val();
|
|
|
|
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
|
const card = Template.currentData();
|
|
|
|
if (title) {
|
|
const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title});
|
|
|
|
if (newCardId) {
|
|
const newCard = ReactiveCache.getCard(newCardId);
|
|
if (newCard) {
|
|
let sortIndex = 0;
|
|
|
|
if (cardId) {
|
|
const targetCard = ReactiveCache.getCard(cardId);
|
|
if (targetCard) {
|
|
const targetSort = targetCard.sort || 0;
|
|
if (position === 'above') {
|
|
sortIndex = targetSort - 0.5;
|
|
} else {
|
|
sortIndex = targetSort + 0.5;
|
|
}
|
|
}
|
|
} else {
|
|
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
|
|
sortIndex = (typeof maxSort === 'number' && !Number.isNaN(maxSort)) ? maxSort + 1 : 0;
|
|
}
|
|
|
|
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
|
}
|
|
}
|
|
|
|
// In case the filter is active we need to add the newly inserted card in
|
|
// the list of exceptions -- cards that are not filtered. Otherwise the
|
|
// card will disappear instantly.
|
|
// See https://github.com/wekan/wekan/issues/80
|
|
Filter.addException(newCardId);
|
|
}
|
|
},
|
|
});
|
|
});
|
|
registerCardDialogTemplate('copyCardPopup');
|
|
|
|
/** Convert Checklist-Item to card dialog */
|
|
Template.convertChecklistItemToCardPopup.onCreated(function () {
|
|
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
|
getDialogOptions() {
|
|
return ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
|
},
|
|
async setDone(cardId, options) {
|
|
const tpl = Template.instance();
|
|
const textarea = tpl.$('#copy-card-title');
|
|
const title = textarea.val().trim();
|
|
const position = tpl.$('input[name="position"]:checked').val();
|
|
|
|
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
|
const card = Template.currentData();
|
|
|
|
if (title) {
|
|
const _id = Cards.insert({
|
|
title: title,
|
|
listId: options.listId,
|
|
boardId: options.boardId,
|
|
swimlaneId: options.swimlaneId,
|
|
sort: 0,
|
|
});
|
|
const newCard = ReactiveCache.getCard(_id);
|
|
|
|
let sortIndex = 0;
|
|
if (cardId) {
|
|
const targetCard = ReactiveCache.getCard(cardId);
|
|
if (targetCard) {
|
|
const targetSort = targetCard.sort || 0;
|
|
if (position === 'above') {
|
|
sortIndex = targetSort - 0.5;
|
|
} else {
|
|
sortIndex = targetSort + 0.5;
|
|
}
|
|
}
|
|
} else {
|
|
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
|
|
sortIndex = (typeof maxSort === 'number' && !Number.isNaN(maxSort)) ? maxSort + 1 : 0;
|
|
}
|
|
|
|
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
|
|
|
Filter.addException(_id);
|
|
}
|
|
},
|
|
});
|
|
});
|
|
registerCardDialogTemplate('convertChecklistItemToCardPopup');
|
|
|
|
/** Copy many cards dialog */
|
|
Template.copyManyCardsPopup.onCreated(function () {
|
|
this.dialog = new BoardSwimlaneListCardDialog(this, {
|
|
getDialogOptions() {
|
|
return ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions();
|
|
},
|
|
async setDone(cardId, options) {
|
|
const tpl = Template.instance();
|
|
const textarea = tpl.$('#copy-card-title');
|
|
const title = textarea.val().trim();
|
|
const position = tpl.$('input[name="position"]:checked').val();
|
|
|
|
ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options);
|
|
const card = Template.currentData();
|
|
|
|
if (title) {
|
|
const titleList = JSON.parse(title);
|
|
for (const obj of titleList) {
|
|
const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, false, {title: obj.title, description: obj.description});
|
|
|
|
if (newCardId) {
|
|
const newCard = ReactiveCache.getCard(newCardId);
|
|
let sortIndex = 0;
|
|
|
|
if (cardId) {
|
|
const targetCard = ReactiveCache.getCard(cardId);
|
|
if (targetCard) {
|
|
const targetSort = targetCard.sort || 0;
|
|
if (position === 'above') {
|
|
sortIndex = targetSort - 0.5;
|
|
} else {
|
|
sortIndex = targetSort + 0.5;
|
|
}
|
|
}
|
|
} else {
|
|
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
|
|
sortIndex = (typeof maxSort === 'number' && !Number.isNaN(maxSort)) ? maxSort + 1 : 0;
|
|
}
|
|
|
|
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
|
|
}
|
|
|
|
// In case the filter is active we need to add the newly inserted card in
|
|
// the list of exceptions -- cards that are not filtered. Otherwise the
|
|
// card will disappear instantly.
|
|
// See https://github.com/wekan/wekan/issues/80
|
|
Filter.addException(newCardId);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
});
|
|
registerCardDialogTemplate('copyManyCardsPopup');
|
|
|
|
Template.setCardColorPopup.onCreated(function () {
|
|
const cardId = getCardId();
|
|
this.currentCard = Cards.findOne(cardId);
|
|
this.currentColor = new ReactiveVar(this.currentCard?.color);
|
|
});
|
|
|
|
Template.setCardColorPopup.helpers({
|
|
colors() {
|
|
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
|
|
},
|
|
|
|
isSelected(color) {
|
|
const tpl = Template.instance();
|
|
if (tpl.currentColor.get() === null) {
|
|
return color === 'white';
|
|
}
|
|
return tpl.currentColor.get() === color;
|
|
},
|
|
});
|
|
|
|
Template.setCardColorPopup.events({
|
|
'click .js-palette-color'(event, tpl) {
|
|
tpl.currentColor.set(Template.currentData().color);
|
|
},
|
|
async 'click .js-submit'(event, tpl) {
|
|
event.preventDefault();
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
await card.setColor(tpl.currentColor.get());
|
|
Popup.back();
|
|
},
|
|
async 'click .js-remove-color'(event) {
|
|
event.preventDefault();
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
await card.setColor(null);
|
|
Popup.back();
|
|
},
|
|
});
|
|
|
|
Template.setSelectionColorPopup.onCreated(function () {
|
|
this.currentColor = new ReactiveVar(null);
|
|
});
|
|
|
|
Template.setSelectionColorPopup.helpers({
|
|
colors() {
|
|
return ALLOWED_COLORS.map((color) => ({ color, name: '' }));
|
|
},
|
|
|
|
isSelected(color) {
|
|
return Template.instance().currentColor.get() === color;
|
|
},
|
|
});
|
|
|
|
Template.setSelectionColorPopup.events({
|
|
'click .js-palette-color'(event, tpl) {
|
|
// Extract color from class name like "card-details-red"
|
|
const classes = $(event.currentTarget).attr('class').split(' ');
|
|
const colorClass = classes.find(cls => cls.startsWith('card-details-'));
|
|
const color = colorClass ? colorClass.replace('card-details-', '') : null;
|
|
tpl.currentColor.set(color);
|
|
},
|
|
async 'click .js-submit'(event, tpl) {
|
|
event.preventDefault();
|
|
const color = tpl.currentColor.get();
|
|
// Use MultiSelection to get selected cards and set color on each
|
|
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
|
await card.setColor(color);
|
|
}
|
|
Popup.back();
|
|
},
|
|
async 'click .js-remove-color'(event, tpl) {
|
|
event.preventDefault();
|
|
// Use MultiSelection to get selected cards and remove color from each
|
|
for (const card of ReactiveCache.getCards(MultiSelection.getMongoSelector())) {
|
|
await card.setColor(null);
|
|
}
|
|
Popup.back();
|
|
},
|
|
});
|
|
|
|
Template.cardMorePopup.onCreated(function () {
|
|
const cardId = getCardId();
|
|
this.currentCard = Cards.findOne(cardId);
|
|
this.parentBoard = new ReactiveVar(null);
|
|
this.parentCard = this.currentCard?.parentCard();
|
|
if (this.parentCard) {
|
|
const list = $('.js-field-parent-card');
|
|
list.val(this.parentCard._id);
|
|
this.parentBoard.set(this.parentCard.board()._id);
|
|
} else {
|
|
this.parentBoard.set(null);
|
|
}
|
|
|
|
this.setParentCardId = (cardId) => {
|
|
if (cardId) {
|
|
this.parentCard = ReactiveCache.getCard(cardId);
|
|
} else {
|
|
this.parentCard = null;
|
|
}
|
|
const card = Cards.findOne(getCardId());
|
|
if (card) card.setParentId(cardId);
|
|
};
|
|
});
|
|
|
|
Template.cardMorePopup.helpers({
|
|
boards() {
|
|
const ret = ReactiveCache.getBoards(
|
|
{
|
|
archived: false,
|
|
'members.userId': Meteor.userId(),
|
|
_id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() },
|
|
},
|
|
{
|
|
sort: { sort: 1 /* boards default sorting */ },
|
|
},
|
|
);
|
|
return ret;
|
|
},
|
|
|
|
cards() {
|
|
const tpl = Template.instance();
|
|
const currentId = getCardId();
|
|
if (tpl.parentBoard.get()) {
|
|
const ret = ReactiveCache.getCards({
|
|
boardId: tpl.parentBoard.get(),
|
|
_id: { $ne: currentId },
|
|
});
|
|
return ret;
|
|
} else {
|
|
return [];
|
|
}
|
|
},
|
|
|
|
isParentBoard() {
|
|
const tpl = Template.instance();
|
|
const board = Template.currentData();
|
|
if (tpl.parentBoard.get()) {
|
|
return board._id === tpl.parentBoard.get();
|
|
}
|
|
return false;
|
|
},
|
|
|
|
isParentCard() {
|
|
const tpl = Template.instance();
|
|
const card = Template.currentData();
|
|
if (tpl.parentCard) {
|
|
return card._id === tpl.parentCard;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
Template.cardMorePopup.events({
|
|
'click .js-copy-card-link-to-clipboard'(event, tpl) {
|
|
const promise = Utils.copyTextToClipboard(location.origin + document.getElementById('cardURL').value);
|
|
|
|
const $tooltip = tpl.$('.copied-tooltip');
|
|
Utils.showCopied(promise, $tooltip);
|
|
},
|
|
'click .js-delete': Popup.afterConfirm('cardDelete', function () {
|
|
const card = Cards.findOne(getCardId());
|
|
Popup.close();
|
|
if (!card) return;
|
|
// verify that there are no linked cards
|
|
if (ReactiveCache.getCards({ linkedId: card._id }).length === 0) {
|
|
Cards.remove(card._id);
|
|
} else {
|
|
// TODO: Maybe later we can list where the linked cards are.
|
|
// Now here is popup with a hint that the card cannot be deleted
|
|
// as there are linked cards.
|
|
// Related:
|
|
// client/components/lists/listHeader.js about line 248
|
|
// https://github.com/wekan/wekan/issues/2785
|
|
const message = `${TAPi18n.__(
|
|
'delete-linked-card-before-this-card',
|
|
)} linkedId: ${card._id
|
|
} at client/components/cards/cardDetails.js and https://github.com/wekan/wekan/issues/2785`;
|
|
alert(message);
|
|
}
|
|
Utils.goBoardId(card.boardId);
|
|
}),
|
|
'change .js-field-parent-board'(event, tpl) {
|
|
const selection = $(event.currentTarget).val();
|
|
const list = $('.js-field-parent-card');
|
|
if (selection === 'none') {
|
|
tpl.parentBoard.set(null);
|
|
} else {
|
|
subManager.subscribe('board', $(event.currentTarget).val(), false);
|
|
tpl.parentBoard.set(selection);
|
|
list.prop('disabled', false);
|
|
}
|
|
tpl.setParentCardId(null);
|
|
},
|
|
'change .js-field-parent-card'(event, tpl) {
|
|
const selection = $(event.currentTarget).val();
|
|
tpl.setParentCardId(selection);
|
|
},
|
|
});
|
|
|
|
Template.cardStartVotingPopup.onCreated(function () {
|
|
const cardId = getCardId();
|
|
this.currentCard = Cards.findOne(cardId);
|
|
this.voteQuestion = new ReactiveVar(this.currentCard?.voteQuestion);
|
|
});
|
|
|
|
Template.cardStartVotingPopup.helpers({
|
|
getVoteQuestion() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card && card.getVoteQuestion ? card.getVoteQuestion() : null;
|
|
},
|
|
votePublic() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card && card.votePublic ? card.votePublic() : false;
|
|
},
|
|
voteAllowNonBoardMembers() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card && card.voteAllowNonBoardMembers ? card.voteAllowNonBoardMembers() : false;
|
|
},
|
|
getVoteEnd() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card && card.getVoteEnd ? card.getVoteEnd() : null;
|
|
},
|
|
});
|
|
|
|
Template.cardStartVotingPopup.events({
|
|
'click .js-end-date': Popup.open('editVoteEndDate'),
|
|
'submit .edit-vote-question'(evt) {
|
|
evt.preventDefault();
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
const voteQuestion = evt.target.vote.value;
|
|
const publicVote = $('#vote-public').hasClass('is-checked');
|
|
const allowNonBoardMembers = $('#vote-allow-non-members').hasClass(
|
|
'is-checked',
|
|
);
|
|
const endString = card.getVoteEnd();
|
|
Meteor.call('cards.setVoteQuestion', card._id, voteQuestion, publicVote, allowNonBoardMembers);
|
|
if (endString) {
|
|
Meteor.call('cards.setVoteEnd', card._id, new Date(endString));
|
|
}
|
|
Popup.back();
|
|
},
|
|
'click .js-remove-vote': Popup.afterConfirm('deleteVote', function () {
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
Meteor.call('cards.unsetVote', card._id);
|
|
Popup.back();
|
|
}),
|
|
'click a.js-toggle-vote-public'(event) {
|
|
event.preventDefault();
|
|
$('#vote-public').toggleClass('is-checked');
|
|
},
|
|
'click a.js-toggle-vote-allow-non-members'(event) {
|
|
event.preventDefault();
|
|
$('#vote-allow-non-members').toggleClass('is-checked');
|
|
},
|
|
});
|
|
|
|
Template.positiveVoteMembersPopup.helpers({
|
|
voteMemberPositive() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card ? card.voteMemberPositive() : [];
|
|
},
|
|
});
|
|
|
|
Template.negativeVoteMembersPopup.helpers({
|
|
voteMemberNegative() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card ? card.voteMemberNegative() : [];
|
|
},
|
|
});
|
|
|
|
Template.cardDeletePopup.helpers({
|
|
archived() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card ? card.archived : false;
|
|
},
|
|
});
|
|
|
|
Template.cardArchivePopup.helpers({
|
|
archived() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card ? card.archived : false;
|
|
},
|
|
});
|
|
|
|
// editVoteEndDatePopup
|
|
Template.editVoteEndDatePopup.onCreated(function () {
|
|
const card = Cards.findOne(getCardId());
|
|
setupDatePicker(this, {
|
|
defaultTime: formatDateTime(now()),
|
|
initialDate: card?.getVoteEnd ? (card.getVoteEnd() || undefined) : undefined,
|
|
});
|
|
});
|
|
|
|
Template.editVoteEndDatePopup.onRendered(function () {
|
|
datePickerRendered(this);
|
|
});
|
|
|
|
Template.editVoteEndDatePopup.helpers(datePickerHelpers());
|
|
|
|
Template.editVoteEndDatePopup.events(datePickerEvents({
|
|
storeDate(date) {
|
|
Meteor.call('cards.setVoteEnd', this.datePicker.card._id, date);
|
|
},
|
|
deleteDate() {
|
|
Meteor.call('cards.unsetVoteEnd', this.datePicker.card._id);
|
|
},
|
|
}));
|
|
|
|
Template.cardStartPlanningPokerPopup.onCreated(function () {
|
|
const cardId = getCardId();
|
|
this.currentCard = Cards.findOne(cardId);
|
|
this.pokerQuestion = new ReactiveVar(this.currentCard?.pokerQuestion);
|
|
});
|
|
|
|
Template.cardStartPlanningPokerPopup.helpers({
|
|
getPokerQuestion() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card && card.getPokerQuestion ? card.getPokerQuestion() : null;
|
|
},
|
|
pokerAllowNonBoardMembers() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card && card.pokerAllowNonBoardMembers ? card.pokerAllowNonBoardMembers() : false;
|
|
},
|
|
getPokerEnd() {
|
|
const card = Cards.findOne(getCardId());
|
|
return card && card.getPokerEnd ? card.getPokerEnd() : null;
|
|
},
|
|
});
|
|
|
|
Template.cardStartPlanningPokerPopup.events({
|
|
'click .js-end-date': Popup.open('editPokerEndDate'),
|
|
'submit .edit-poker-question'(evt) {
|
|
evt.preventDefault();
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
const pokerQuestion = true;
|
|
const allowNonBoardMembers = $('#poker-allow-non-members').hasClass(
|
|
'is-checked',
|
|
);
|
|
const endString = card.getPokerEnd();
|
|
|
|
Meteor.call('cards.setPokerQuestion', card._id, pokerQuestion, allowNonBoardMembers);
|
|
if (endString) {
|
|
Meteor.call('cards.setPokerEnd', card._id, new Date(endString));
|
|
}
|
|
Popup.back();
|
|
},
|
|
'click .js-remove-poker': Popup.afterConfirm('deletePoker', function () {
|
|
const card = Cards.findOne(getCardId());
|
|
if (!card) return;
|
|
Meteor.call('cards.unsetPoker', card._id);
|
|
Popup.back();
|
|
}),
|
|
'click a.js-toggle-poker-allow-non-members'(event) {
|
|
event.preventDefault();
|
|
$('#poker-allow-non-members').toggleClass('is-checked');
|
|
},
|
|
});
|
|
|
|
// editPokerEndDatePopup
|
|
Template.editPokerEndDatePopup.onCreated(function () {
|
|
const card = Cards.findOne(getCardId());
|
|
setupDatePicker(this, {
|
|
defaultTime: formatDateTime(now()),
|
|
initialDate: card?.getPokerEnd ? (card.getPokerEnd() || undefined) : undefined,
|
|
});
|
|
});
|
|
|
|
Template.editPokerEndDatePopup.onRendered(function () {
|
|
datePickerRendered(this);
|
|
});
|
|
|
|
Template.editPokerEndDatePopup.helpers(datePickerHelpers());
|
|
|
|
Template.editPokerEndDatePopup.events(datePickerEvents({
|
|
storeDate(date) {
|
|
Meteor.call('cards.setPokerEnd', this.datePicker.card._id, date);
|
|
},
|
|
deleteDate() {
|
|
Meteor.call('cards.unsetPokerEnd', this.datePicker.card._id);
|
|
},
|
|
}));
|
|
|
|
// Close the card details pane by pressing escape
|
|
EscapeActions.register(
|
|
'detailsPane',
|
|
async () => {
|
|
// if card description diverges from database due to editing
|
|
// ask user whether changes should be applied
|
|
if (ReactiveCache.getCurrentUser()) {
|
|
if (ReactiveCache.getCurrentUser().profile.rescueCardDescription == true) {
|
|
const currentCard = getCurrentCardFromContext();
|
|
const cardDetailsElement = getCardDetailsElement(currentCard?._id);
|
|
const currentDescription = cardDetailsElement?.querySelector(
|
|
'.editor.js-new-description-input',
|
|
);
|
|
if (currentDescription?.value && currentCard && !(currentDescription.value === currentCard.getDescription())) {
|
|
if (confirm(TAPi18n.__('rescue-card-description-dialogue'))) {
|
|
await currentCard.setDescription(currentDescription.value);
|
|
// Save it!
|
|
console.log(currentDescription.value);
|
|
console.log("current description", currentCard.getDescription());
|
|
} else {
|
|
// Do nothing!
|
|
console.log('Description changes were not saved to the database.');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Session.get('cardDetailsIsDragging')) {
|
|
// Reset dragging status as the mouse landed outside the cardDetails template area and this will prevent a mousedown event from firing
|
|
Session.set('cardDetailsIsDragging', false);
|
|
Session.set('cardDetailsIsMouseDown', false);
|
|
|
|
} else {
|
|
// Prevent close card when the user is selecting text and moves the mouse cursor outside the card detail area
|
|
Utils.goBoardId(Session.get('currentBoard'));
|
|
}
|
|
},
|
|
() => {
|
|
return !Session.equals('currentCard', null);
|
|
},
|
|
{
|
|
noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
|
|
},
|
|
);
|
|
|
|
Template.cardAssigneesPopup.onCreated(function () {
|
|
let currBoard = Utils.getCurrentBoard();
|
|
let members = currBoard.activeMembers();
|
|
this.members = new ReactiveVar(members);
|
|
});
|
|
|
|
Template.cardAssigneesPopup.events({
|
|
'click .js-select-assignee'(event) {
|
|
const card = getCurrentCardFromContext();
|
|
if (!card) return;
|
|
const assigneeId = this.userId;
|
|
card.toggleAssignee(assigneeId);
|
|
event.preventDefault();
|
|
},
|
|
'keyup .card-assignees-filter'(event) {
|
|
const members = filterMembers(event.target.value);
|
|
Template.instance().members.set(members);
|
|
},
|
|
});
|
|
|
|
Template.cardAssigneesPopup.helpers({
|
|
isCardAssignee() {
|
|
const card = getCurrentCardFromContext();
|
|
if (!card) return false;
|
|
const cardAssignees = card.getAssignees();
|
|
|
|
return _.contains(cardAssignees, this.userId);
|
|
},
|
|
|
|
members() {
|
|
const members = Template.instance().members.get();
|
|
const uniqueMembers = _.uniq(members, 'userId');
|
|
return _.sortBy(uniqueMembers, member => {
|
|
const user = ReactiveCache.getUser(member.userId);
|
|
return user ? user.profile.fullname : '';
|
|
});
|
|
},
|
|
|
|
userData() {
|
|
return ReactiveCache.getUser(this.userId);
|
|
},
|
|
});
|
|
|
|
Template.cardAssigneePopup.helpers({
|
|
userData() {
|
|
return ReactiveCache.getUser(this.userId, {
|
|
fields: {
|
|
profile: 1,
|
|
username: 1,
|
|
},
|
|
});
|
|
},
|
|
|
|
memberType() {
|
|
const user = ReactiveCache.getUser(this.userId);
|
|
return user && user.isBoardAdmin() ? 'admin' : 'normal';
|
|
},
|
|
|
|
isCardAssignee() {
|
|
const card = getCurrentCardFromContext();
|
|
if (!card) return false;
|
|
const cardAssignees = card.getAssignees();
|
|
|
|
return _.contains(cardAssignees, this.userId);
|
|
},
|
|
|
|
user() {
|
|
return ReactiveCache.getUser(this.userId);
|
|
},
|
|
});
|
|
|
|
Template.cardAssigneePopup.events({
|
|
'click .js-remove-assignee'() {
|
|
ReactiveCache.getCard(this.cardId).unassignAssignee(this.userId);
|
|
Popup.back();
|
|
},
|
|
'click .js-edit-profile': Popup.open('editProfile'),
|
|
});
|