wekan/client/components/cards/cardDetails.js
Harry Adel d3625db755 Migrate card components from BlazeComponent to Template
Convert cardDetails, cardCustomFields, cardDate, cardTime,
cardDescription, attachments, checklists, labels, minicard,
resultCard, and subtasks to use native Meteor Template pattern.
2026-03-08 11:04:53 +02:00

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'),
});