diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index a883877e1..3dd13684f 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -495,9 +495,9 @@ Template.previewClipboardImagePopup.events({ }, }); -BlazeComponent.extendComponent({ +Template.attachmentActionsPopup.helpers({ isCover() { - const ret = ReactiveCache.getCard(this.data().meta.cardId).coverId == this.data()._id; + const ret = ReactiveCache.getCard(this.meta.cardId).coverId == this._id; return ret; }, isBackgroundImage() { @@ -505,78 +505,72 @@ BlazeComponent.extendComponent({ //return currentBoard.backgroundImageURL === $(".attachment-thumbnail-img").attr("src"); return false; }, - events() { - return [ - { - 'click .js-add-cover'() { - ReactiveCache.getCard(this.data().meta.cardId).setCover(this.data()._id); - Popup.back(); - }, - 'click .js-remove-cover'() { - ReactiveCache.getCard(this.data().meta.cardId).unsetCover(); - Popup.back(); - }, - 'click .js-add-background-image'() { - const currentBoard = Utils.getCurrentBoard(); - currentBoard.setBackgroundImageURL(attachmentActionsLink); - Utils.setBackgroundImage(attachmentActionsLink); - Popup.back(); - event.preventDefault(); - }, - 'click .js-remove-background-image'() { - const currentBoard = Utils.getCurrentBoard(); - currentBoard.setBackgroundImageURL(""); - Utils.setBackgroundImage(""); - Popup.back(); - Utils.reload(); - event.preventDefault(); - }, - 'click .js-move-storage-fs'() { - Meteor.call('moveAttachmentToStorage', this.data()._id, "fs"); - Popup.back(); - }, - 'click .js-move-storage-gridfs'() { - Meteor.call('moveAttachmentToStorage', this.data()._id, "gridfs"); - Popup.back(); - }, - 'click .js-move-storage-s3'() { - Meteor.call('moveAttachmentToStorage', this.data()._id, "s3"); - Popup.back(); - }, - } - ] - } -}).register('attachmentActionsPopup'); +}); -BlazeComponent.extendComponent({ +Template.attachmentActionsPopup.events({ + 'click .js-add-cover'() { + ReactiveCache.getCard(this.meta.cardId).setCover(this._id); + Popup.back(); + }, + 'click .js-remove-cover'() { + ReactiveCache.getCard(this.meta.cardId).unsetCover(); + Popup.back(); + }, + 'click .js-add-background-image'(event) { + const currentBoard = Utils.getCurrentBoard(); + currentBoard.setBackgroundImageURL(attachmentActionsLink); + Utils.setBackgroundImage(attachmentActionsLink); + Popup.back(); + event.preventDefault(); + }, + 'click .js-remove-background-image'(event) { + const currentBoard = Utils.getCurrentBoard(); + currentBoard.setBackgroundImageURL(""); + Utils.setBackgroundImage(""); + Popup.back(); + Utils.reload(); + event.preventDefault(); + }, + 'click .js-move-storage-fs'() { + Meteor.call('moveAttachmentToStorage', this._id, "fs"); + Popup.back(); + }, + 'click .js-move-storage-gridfs'() { + Meteor.call('moveAttachmentToStorage', this._id, "gridfs"); + Popup.back(); + }, + 'click .js-move-storage-s3'() { + Meteor.call('moveAttachmentToStorage', this._id, "s3"); + Popup.back(); + }, +}); + +Template.attachmentRenamePopup.helpers({ getNameWithoutExtension() { - const ret = this.data().name.replace(new RegExp("\." + this.data().extension + "$"), ""); + const ret = this.name.replace(new RegExp("\." + this.extension + "$"), ""); return ret; }, - events() { - return [ - { - 'keydown input.js-edit-attachment-name'(evt) { - // enter = save - if (evt.keyCode === 13) { - this.find('button[type=submit]').click(); - } - }, - 'click button.js-submit-edit-attachment-name'(event) { - // save button pressed - event.preventDefault(); - const name = this.$('.js-edit-attachment-name')[0] - .value - .trim() + this.data().extensionWithDot; - if (name === sanitizeText(name)) { - Meteor.call('renameAttachment', this.data()._id, name); - } - Popup.back(); - }, - } - ] - } -}).register('attachmentRenamePopup'); +}); + +Template.attachmentRenamePopup.events({ + 'keydown input.js-edit-attachment-name'(evt, tpl) { + // enter = save + if (evt.keyCode === 13) { + tpl.find('button[type=submit]').click(); + } + }, + 'click button.js-submit-edit-attachment-name'(event, tpl) { + // save button pressed + event.preventDefault(); + const name = tpl.$('.js-edit-attachment-name')[0] + .value + .trim() + this.extensionWithDot; + if (name === sanitizeText(name)) { + Meteor.call('renameAttachment', this._id, name); + } + Popup.back(); + }, +}); // Template helpers for attachment migration status Template.registerHelper('attachmentMigrationStatus', function(attachmentId) { diff --git a/client/components/cards/cardCustomFields.js b/client/components/cards/cardCustomFields.js index cb522c408..297661999 100644 --- a/client/components/cards/cardCustomFields.js +++ b/client/components/cards/cardCustomFields.js @@ -1,5 +1,10 @@ import { TAPi18n } from '/imports/i18n'; -import { DatePicker } from '/client/lib/datepicker'; +import { + setupDatePicker, + datePickerRendered, + datePickerHelpers, + datePickerEvents, +} from '/client/lib/datepicker'; import { ReactiveCache } from '/imports/reactiveCache'; import { formatDateTime, @@ -22,12 +27,13 @@ import { fromNow, calendar } from '/imports/lib/dateUtils'; -import Cards from '/models/cards'; import { CustomFieldStringTemplate } from '/client/lib/customFields' +import { getCurrentCardFromContext } from '/client/lib/currentCard'; Template.cardCustomFieldsPopup.helpers({ hasCustomField() { - const card = Utils.getCurrentCard(); + const card = getCurrentCardFromContext(); + if (!card) return false; const customFieldId = this._id; return card.customFieldIndex(customFieldId) > -1; }, @@ -35,7 +41,8 @@ Template.cardCustomFieldsPopup.helpers({ Template.cardCustomFieldsPopup.events({ 'click .js-select-field'(event) { - const card = Utils.getCurrentCard(); + const card = getCurrentCardFromContext(); + if (!card) return; const customFieldId = this._id; card.toggleCustomField(customFieldId); event.preventDefault(); @@ -48,305 +55,280 @@ Template.cardCustomFieldsPopup.events({ }); // cardCustomField -const CardCustomField = BlazeComponent.extendComponent({ +Template.cardCustomField.helpers({ getTemplate() { - return `cardCustomField-${this.data().definition.type}`; - }, - - onCreated() { - const self = this; - self.card = Utils.getCurrentCard(); - self.customFieldId = this.data()._id; + return `cardCustomField-${this.definition.type}`; }, }); -CardCustomField.register('cardCustomField'); + +Template.cardCustomField.onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; +}); // cardCustomField-text -(class extends CardCustomField { - onCreated() { - super.onCreated(); - } +Template['cardCustomField-text'].onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; +}); - events() { - return [ - { - 'submit .js-card-customfield-text'(event) { - event.preventDefault(); - const value = this.currentComponent().getValue(); - this.card.setCustomField(this.customFieldId, value); - }, - }, - ]; - } -}.register('cardCustomField-text')); +Template['cardCustomField-text'].events({ + 'submit .js-card-customfield-text'(event, tpl) { + event.preventDefault(); + const value = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val(); + tpl.card.setCustomField(tpl.customFieldId, value); + }, +}); // cardCustomField-number -(class extends CardCustomField { - onCreated() { - super.onCreated(); - } +Template['cardCustomField-number'].onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; +}); - events() { - return [ - { - 'submit .js-card-customfield-number'(event) { - event.preventDefault(); - const value = parseInt(this.find('input').value, 10); - this.card.setCustomField(this.customFieldId, value); - }, - }, - ]; - } -}.register('cardCustomField-number')); +Template['cardCustomField-number'].events({ + 'submit .js-card-customfield-number'(event, tpl) { + event.preventDefault(); + const value = parseInt(tpl.find('input').value, 10); + tpl.card.setCustomField(tpl.customFieldId, value); + }, +}); // cardCustomField-checkbox -(class extends CardCustomField { - onCreated() { - super.onCreated(); - } +Template['cardCustomField-checkbox'].onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; +}); - toggleItem() { - this.card.setCustomField(this.customFieldId, !this.data().value); - } - - events() { - return [ - { - 'click .js-checklist-item .check-box-unicode': this.toggleItem, - 'click .js-checklist-item .check-box-container': this.toggleItem, - }, - ]; - } -}.register('cardCustomField-checkbox')); +Template['cardCustomField-checkbox'].events({ + 'click .js-checklist-item .check-box-unicode'(event, tpl) { + tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value); + }, + 'click .js-checklist-item .check-box-container'(event, tpl) { + tpl.card.setCustomField(tpl.customFieldId, !Template.currentData().value); + }, +}); // cardCustomField-currency -(class extends CardCustomField { - onCreated() { - super.onCreated(); - - this.currencyCode = this.data().definition.settings.currencyCode; - } +Template['cardCustomField-currency'].onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; + this.currencyCode = Template.currentData().definition.settings.currencyCode; +}); +Template['cardCustomField-currency'].helpers({ formattedValue() { const locale = TAPi18n.getLanguage(); - + const tpl = Template.instance(); return new Intl.NumberFormat(locale, { style: 'currency', - currency: this.currencyCode, - }).format(this.data().value); - } + currency: tpl.currencyCode, + }).format(this.value); + }, +}); - events() { - return [ - { - 'submit .js-card-customfield-currency'(event) { - event.preventDefault(); - // To allow input separated by comma, the comma is replaced by a period. - const value = Number(this.find('input').value.replace(/,/i, '.'), 10); - this.card.setCustomField(this.customFieldId, value); - }, - }, - ]; - } -}.register('cardCustomField-currency')); +Template['cardCustomField-currency'].events({ + 'submit .js-card-customfield-currency'(event, tpl) { + event.preventDefault(); + // To allow input separated by comma, the comma is replaced by a period. + const value = Number(tpl.find('input').value.replace(/,/i, '.'), 10); + tpl.card.setCustomField(tpl.customFieldId, value); + }, +}); // cardCustomField-date -(class extends CardCustomField { - onCreated() { - super.onCreated(); - const self = this; - self.date = ReactiveVar(); - self.now = ReactiveVar(now()); - window.setInterval(() => { - self.now.set(now()); - }, 60000); +Template['cardCustomField-date'].onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; + const self = this; + self.date = ReactiveVar(); + self.now = ReactiveVar(now()); + window.setInterval(() => { + self.now.set(now()); + }, 60000); - self.autorun(() => { - self.date.set(new Date(self.data().value)); - }); - } + self.autorun(() => { + self.date.set(new Date(Template.currentData().value)); + }); +}); +Template['cardCustomField-date'].helpers({ showWeek() { - return getISOWeek(this.date.get()).toString(); - } - + return getISOWeek(Template.instance().date.get()).toString(); + }, showWeekOfYear() { const user = ReactiveCache.getCurrentUser(); if (!user) { - // For non-logged-in users, week of year is not shown return false; } return user.isShowWeekOfYear(); - } - + }, showDate() { const currentUser = ReactiveCache.getCurrentUser(); const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } - + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, showISODate() { - return this.date.get().toISOString(); - } - + return Template.instance().date.get().toISOString(); + }, classes() { + const tpl = Template.instance(); if ( - isBefore(this.date.get(), this.now.get(), 'minute') && - isBefore(this.now.get(), this.data().value, 'minute') + isBefore(tpl.date.get(), tpl.now.get(), 'minute') && + isBefore(tpl.now.get(), this.value, 'minute') ) { return 'current'; } return ''; - } - + }, showTitle() { - return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`; - } + return `${TAPi18n.__('card-start-on')} ${Template.instance().date.get().toLocaleString()}`; + }, +}); - events() { - return [ - { - 'click .js-edit-date': Popup.open('cardCustomField-date'), - }, - ]; - } -}.register('cardCustomField-date')); +Template['cardCustomField-date'].events({ + 'click .js-edit-date': Popup.open('cardCustomField-date'), +}); // cardCustomField-datePopup -(class extends DatePicker { - onCreated() { - super.onCreated(); - const self = this; - self.card = Utils.getCurrentCard(); - self.customFieldId = this.data()._id; - this.data().value && this.date.set(new Date(this.data().value)); - } +Template['cardCustomField-datePopup'].onCreated(function () { + const data = Template.currentData(); + setupDatePicker(this, { + initialDate: data.value ? data.value : undefined, + }); + // Override card and store customFieldId for store/delete callbacks + this.datePicker.card = getCurrentCardFromContext(); + this.customFieldId = data._id; +}); - _storeDate(date) { - this.card.setCustomField(this.customFieldId, date); - } +Template['cardCustomField-datePopup'].onRendered(function () { + datePickerRendered(this); +}); - _deleteDate() { - this.card.setCustomField(this.customFieldId, ''); - } -}.register('cardCustomField-datePopup')); +Template['cardCustomField-datePopup'].helpers(datePickerHelpers()); + +Template['cardCustomField-datePopup'].events(datePickerEvents({ + storeDate(date) { + this.datePicker.card.setCustomField(this.customFieldId, date); + }, + deleteDate() { + this.datePicker.card.setCustomField(this.customFieldId, ''); + }, +})); // cardCustomField-dropdown -(class extends CardCustomField { - onCreated() { - super.onCreated(); - this._items = this.data().definition.settings.dropdownItems; - this.items = this._items.slice(0); - this.items.unshift({ - _id: '', - name: TAPi18n.__('custom-field-dropdown-none'), - }); - } +Template['cardCustomField-dropdown'].onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; + this._items = Template.currentData().definition.settings.dropdownItems; + this.items = this._items.slice(0); + this.items.unshift({ + _id: '', + name: TAPi18n.__('custom-field-dropdown-none'), + }); +}); +Template['cardCustomField-dropdown'].helpers({ + items() { + return Template.instance().items; + }, selectedItem() { - const selected = this._items.find(item => { - return item._id === this.data().value; + const tpl = Template.instance(); + const selected = tpl._items.find(item => { + return item._id === this.value; }); return selected ? selected.name : TAPi18n.__('custom-field-dropdown-unknown'); - } + }, +}); - events() { - return [ - { - 'submit .js-card-customfield-dropdown'(event) { - event.preventDefault(); - const value = this.find('select').value; - this.card.setCustomField(this.customFieldId, value); - }, - }, - ]; - } -}.register('cardCustomField-dropdown')); +Template['cardCustomField-dropdown'].events({ + 'submit .js-card-customfield-dropdown'(event, tpl) { + event.preventDefault(); + const value = tpl.find('select').value; + tpl.card.setCustomField(tpl.customFieldId, value); + }, +}); // cardCustomField-stringtemplate -class CardCustomFieldStringTemplate extends CardCustomField { - onCreated() { - super.onCreated(); - - this.customField = new CustomFieldStringTemplate(this.data().definition); - - this.stringtemplateItems = new ReactiveVar(this.data().value ?? []); - } +Template['cardCustomField-stringtemplate'].onCreated(function () { + this.card = getCurrentCardFromContext(); + this.customFieldId = Template.currentData()._id; + this.customField = new CustomFieldStringTemplate(Template.currentData().definition); + this.stringtemplateItems = new ReactiveVar(Template.currentData().value ?? []); +}); +Template['cardCustomField-stringtemplate'].helpers({ formattedValue() { - const ret = this.customField.getFormattedValue(this.data().value); + const tpl = Template.instance(); + const ret = tpl.customField.getFormattedValue(this.value); return ret; - } + }, + stringtemplateItems() { + return Template.instance().stringtemplateItems.get(); + }, +}); - getItems() { - return Array.from(this.findAll('input')) - .map(input => input.value) - .filter(value => !!value.trim()); - } +Template['cardCustomField-stringtemplate'].events({ + 'submit .js-card-customfield-stringtemplate'(event, tpl) { + event.preventDefault(); + const items = tpl.stringtemplateItems.get(); + tpl.card.setCustomField(tpl.customFieldId, items); + }, - events() { - return [ - { - 'submit .js-card-customfield-stringtemplate'(event) { - event.preventDefault(); - const items = this.stringtemplateItems.get(); - this.card.setCustomField(this.customFieldId, items); - }, + 'keydown .js-card-customfield-stringtemplate-item'(event, tpl) { + if (event.keyCode === 13) { + event.preventDefault(); - 'keydown .js-card-customfield-stringtemplate-item'(event) { - if (event.keyCode === 13) { - event.preventDefault(); + if (event.target.value.trim() || event.metaKey || event.ctrlKey) { + const inputLast = tpl.find('input.last'); - if (event.target.value.trim() || event.metaKey || event.ctrlKey) { - const inputLast = this.find('input.last'); + let items = Array.from(tpl.findAll('input')) + .map(input => input.value) + .filter(value => !!value.trim()); - let items = this.getItems(); + if (event.target === inputLast) { + inputLast.value = ''; + } else if (event.target.nextSibling === inputLast) { + inputLast.focus(); + } else { + event.target.blur(); - if (event.target === inputLast) { - inputLast.value = ''; - } else if (event.target.nextSibling === inputLast) { - inputLast.focus(); - } else { - event.target.blur(); + const idx = Array.from(tpl.findAll('input')).indexOf( + event.target, + ); + items.splice(idx + 1, 0, ''); - const idx = Array.from(this.findAll('input')).indexOf( - event.target, - ); - items.splice(idx + 1, 0, ''); + Tracker.afterFlush(() => { + const element = tpl.findAll('input')[idx + 1]; + element.focus(); + element.value = ''; + }); + } - Tracker.afterFlush(() => { - const element = this.findAll('input')[idx + 1]; - element.focus(); - element.value = ''; - }); - } + tpl.stringtemplateItems.set(items); + } + if (event.metaKey || event.ctrlKey) { + tpl.find('button[type=submit]').click(); + } + } + }, - this.stringtemplateItems.set(items); - } - if (event.metaKey || event.ctrlKey) { - this.find('button[type=submit]').click(); - } - } - }, + 'blur .js-card-customfield-stringtemplate-item'(event, tpl) { + if ( + !event.target.value.trim() || + event.target === tpl.find('input.last') + ) { + const items = Array.from(tpl.findAll('input')) + .map(input => input.value) + .filter(value => !!value.trim()); + tpl.stringtemplateItems.set(items); + tpl.find('input.last').value = ''; + } + }, - 'blur .js-card-customfield-stringtemplate-item'(event) { - if ( - !event.target.value.trim() || - event.target === this.find('input.last') - ) { - const items = this.getItems(); - this.stringtemplateItems.set(items); - this.find('input.last').value = ''; - } - }, - - 'click .js-close-inlined-form'(event) { - this.stringtemplateItems.set(this.data().value ?? []); - }, - }, - ]; - } -} -CardCustomFieldStringTemplate.register('cardCustomField-stringtemplate'); + 'click .js-close-inlined-form'(event, tpl) { + tpl.stringtemplateItems.set(Template.currentData().value ?? []); + }, +}); diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js index 386c78467..b8bce20eb 100644 --- a/client/components/cards/cardDate.js +++ b/client/components/cards/cardDate.js @@ -1,5 +1,11 @@ import { TAPi18n } from '/imports/i18n'; -import { DatePicker } from '/client/lib/datepicker'; +import { ReactiveCache } from '/imports/reactiveCache'; +import { + setupDatePicker, + datePickerRendered, + datePickerHelpers, + datePickerEvents, +} from '/client/lib/datepicker'; import { formatDateTime, formatDate, @@ -23,128 +29,159 @@ import { diff } from '/imports/lib/dateUtils'; +// --- DatePicker popups (edit date forms) --- + // editCardReceivedDatePopup -(class extends DatePicker { - onCreated() { - super.onCreated(formatDateTime(now())); - this.data().getReceived() && - this.date.set(new Date(this.data().getReceived())); - } - - _storeDate(date) { - this.card.setReceived(formatDateTime(date)); - } - - _deleteDate() { - this.card.unsetReceived(); - } -}.register('editCardReceivedDatePopup')); - -// editCardStartDatePopup -(class extends DatePicker { - onCreated() { - super.onCreated(formatDateTime(now())); - this.data().getStart() && this.date.set(new Date(this.data().getStart())); - } - - _storeDate(date) { - this.card.setStart(formatDateTime(date)); - } - - _deleteDate() { - this.card.unsetStart(); - } -}.register('editCardStartDatePopup')); - -// editCardDueDatePopup -(class extends DatePicker { - onCreated() { - super.onCreated('1970-01-01 17:00:00'); - this.data().getDue() && this.date.set(new Date(this.data().getDue())); - } - - _storeDate(date) { - this.card.setDue(formatDateTime(date)); - } - - _deleteDate() { - this.card.unsetDue(); - } -}.register('editCardDueDatePopup')); - -// editCardEndDatePopup -(class extends DatePicker { - onCreated() { - super.onCreated(formatDateTime(now())); - this.data().getEnd() && this.date.set(new Date(this.data().getEnd())); - } - - _storeDate(date) { - this.card.setEnd(formatDateTime(date)); - } - - _deleteDate() { - this.card.unsetEnd(); - } -}.register('editCardEndDatePopup')); - -// Display received, start, due & end dates -const CardDate = BlazeComponent.extendComponent({ - template() { - return 'dateBadge'; - }, - - onCreated() { - const self = this; - self.date = ReactiveVar(); - self.now = ReactiveVar(now()); - window.setInterval(() => { - self.now.set(now()); - }, 60000); - }, - - showWeek() { - return getISOWeek(this.date.get()).toString(); - }, - - showWeekOfYear() { - const user = ReactiveCache.getCurrentUser(); - if (!user) { - // For non-logged-in users, week of year is not shown - return false; - } - return user.isShowWeekOfYear(); - }, - - showDate() { - const currentUser = ReactiveCache.getCurrentUser(); - const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - }, - - showISODate() { - return this.date.get().toISOString(); - }, +Template.editCardReceivedDatePopup.onCreated(function () { + const card = Template.currentData(); + setupDatePicker(this, { + defaultTime: formatDateTime(now()), + initialDate: card.getReceived() ? card.getReceived() : undefined, + }); }); -class CardReceivedDate extends CardDate { - onCreated() { - super.onCreated(); - const self = this; - self.autorun(() => { - self.date.set(new Date(self.data().getReceived())); - }); - } +Template.editCardReceivedDatePopup.onRendered(function () { + datePickerRendered(this); +}); +Template.editCardReceivedDatePopup.helpers(datePickerHelpers()); + +Template.editCardReceivedDatePopup.events(datePickerEvents({ + storeDate(date) { + this.datePicker.card.setReceived(formatDateTime(date)); + }, + deleteDate() { + this.datePicker.card.unsetReceived(); + }, +})); + +// editCardStartDatePopup +Template.editCardStartDatePopup.onCreated(function () { + const card = Template.currentData(); + setupDatePicker(this, { + defaultTime: formatDateTime(now()), + initialDate: card.getStart() ? card.getStart() : undefined, + }); +}); + +Template.editCardStartDatePopup.onRendered(function () { + datePickerRendered(this); +}); + +Template.editCardStartDatePopup.helpers(datePickerHelpers()); + +Template.editCardStartDatePopup.events(datePickerEvents({ + storeDate(date) { + this.datePicker.card.setStart(formatDateTime(date)); + }, + deleteDate() { + this.datePicker.card.unsetStart(); + }, +})); + +// editCardDueDatePopup +Template.editCardDueDatePopup.onCreated(function () { + const card = Template.currentData(); + setupDatePicker(this, { + defaultTime: '1970-01-01 17:00:00', + initialDate: card.getDue() ? card.getDue() : undefined, + }); +}); + +Template.editCardDueDatePopup.onRendered(function () { + datePickerRendered(this); +}); + +Template.editCardDueDatePopup.helpers(datePickerHelpers()); + +Template.editCardDueDatePopup.events(datePickerEvents({ + storeDate(date) { + this.datePicker.card.setDue(formatDateTime(date)); + }, + deleteDate() { + this.datePicker.card.unsetDue(); + }, +})); + +// editCardEndDatePopup +Template.editCardEndDatePopup.onCreated(function () { + const card = Template.currentData(); + setupDatePicker(this, { + defaultTime: formatDateTime(now()), + initialDate: card.getEnd() ? card.getEnd() : undefined, + }); +}); + +Template.editCardEndDatePopup.onRendered(function () { + datePickerRendered(this); +}); + +Template.editCardEndDatePopup.helpers(datePickerHelpers()); + +Template.editCardEndDatePopup.events(datePickerEvents({ + storeDate(date) { + this.datePicker.card.setEnd(formatDateTime(date)); + }, + deleteDate() { + this.datePicker.card.unsetEnd(); + }, +})); + +// --- Card date badge display helpers --- + +// Shared onCreated logic for card date badge templates +function cardDateOnCreated(tpl) { + tpl.date = new ReactiveVar(); + tpl.now = new ReactiveVar(now()); + window.setInterval(() => { + tpl.now.set(now()); + }, 60000); +} + +// Shared helpers for card date badge templates +function cardDateHelpers(extraHelpers) { + const base = { + showWeek() { + return getISOWeek(Template.instance().date.get()).toString(); + }, + showWeekOfYear() { + const user = ReactiveCache.getCurrentUser(); + if (!user) { + return false; + } + return user.isShowWeekOfYear(); + }, + showDate() { + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, + showISODate() { + return Template.instance().date.get().toISOString(); + }, + }; + return Object.assign(base, extraHelpers); +} + +// cardReceivedDate +Template.cardReceivedDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getReceived())); + }); +}); + +Template.cardReceivedDate.helpers(cardDateHelpers({ classes() { + const tpl = Template.instance(); let classes = 'received-date '; - const dueAt = this.data().getDue(); - const endAt = this.data().getEnd(); - const startAt = this.data().getStart(); - const theDate = this.date.get(); - const now = this.now.get(); + const data = Template.currentData(); + const dueAt = data.getDue(); + const endAt = data.getEnd(); + const startAt = data.getStart(); + const theDate = tpl.date.get(); - // Received date logic: if received date is after start, due, or end dates, it's overdue if ( (startAt && isAfter(theDate, startAt)) || (endAt && isAfter(theDate, endAt)) || @@ -155,332 +192,453 @@ class CardReceivedDate extends CardDate { classes += 'not-due'; } return classes; - } - + }, showTitle() { + const tpl = Template.instance(); const currentUser = ReactiveCache.getCurrentUser(); const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); return `${TAPi18n.__('card-received-on')} ${formattedDate}`; - } + }, +})); - events() { - return super.events().concat({ - 'click .js-edit-date': Popup.open('editCardReceivedDate'), - }); - } -} -CardReceivedDate.register('cardReceivedDate'); +Template.cardReceivedDate.events({ + 'click .js-edit-date': Popup.open('editCardReceivedDate'), +}); -class CardStartDate extends CardDate { - onCreated() { - super.onCreated(); - const self = this; - self.autorun(() => { - self.date.set(new Date(self.data().getStart())); - }); - } +// cardStartDate +Template.cardStartDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getStart())); + }); +}); +Template.cardStartDate.helpers(cardDateHelpers({ classes() { + const tpl = Template.instance(); let classes = 'start-date '; - const dueAt = this.data().getDue(); - const endAt = this.data().getEnd(); - const theDate = this.date.get(); - const now = this.now.get(); + const data = Template.currentData(); + const dueAt = data.getDue(); + const endAt = data.getEnd(); + const theDate = tpl.date.get(); + const nowVal = tpl.now.get(); - // Start date logic: if start date is after due or end dates, it's overdue if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) { classes += 'overdue'; - } else if (isAfter(theDate, now)) { - // Start date is in the future - not due yet + } else if (isAfter(theDate, nowVal)) { classes += 'not-due'; } else { - // Start date is today or in the past - current/active classes += 'current'; } return classes; - } - + }, showTitle() { + const tpl = Template.instance(); const currentUser = ReactiveCache.getCurrentUser(); const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); return `${TAPi18n.__('card-start-on')} ${formattedDate}`; - } + }, +})); - events() { - return super.events().concat({ - 'click .js-edit-date': Popup.open('editCardStartDate'), - }); - } -} -CardStartDate.register('cardStartDate'); +Template.cardStartDate.events({ + 'click .js-edit-date': Popup.open('editCardStartDate'), +}); -class CardDueDate extends CardDate { - onCreated() { - super.onCreated(); - const self = this; - self.autorun(() => { - self.date.set(new Date(self.data().getDue())); - }); - } +// cardDueDate +Template.cardDueDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getDue())); + }); +}); +Template.cardDueDate.helpers(cardDateHelpers({ classes() { + const tpl = Template.instance(); let classes = 'due-date '; - const endAt = this.data().getEnd(); - const theDate = this.date.get(); - const now = this.now.get(); + const data = Template.currentData(); + const endAt = data.getEnd(); + const theDate = tpl.date.get(); + const nowVal = tpl.now.get(); - // If there's an end date and it's before the due date, task is completed early if (endAt && isBefore(endAt, theDate)) { classes += 'completed-early'; - } - // If there's an end date, don't show due date status since task is completed - else if (endAt) { + } else if (endAt) { classes += 'completed'; - } - // Due date logic based on current time - else { - const daysDiff = diff(theDate, now, 'days'); + } else { + const daysDiff = diff(theDate, nowVal, 'days'); if (daysDiff < 0) { - // Due date is in the past - overdue classes += 'overdue'; } else if (daysDiff <= 1) { - // Due today or tomorrow - due soon classes += 'due-soon'; } else { - // Due date is more than 1 day away - not due yet classes += 'not-due'; } } return classes; - } - + }, showTitle() { + const tpl = Template.instance(); const currentUser = ReactiveCache.getCurrentUser(); const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); return `${TAPi18n.__('card-due-on')} ${formattedDate}`; - } + }, +})); - events() { - return super.events().concat({ - 'click .js-edit-date': Popup.open('editCardDueDate'), - }); - } -} -CardDueDate.register('cardDueDate'); +Template.cardDueDate.events({ + 'click .js-edit-date': Popup.open('editCardDueDate'), +}); -class CardEndDate extends CardDate { - onCreated() { - super.onCreated(); - const self = this; - self.autorun(() => { - self.date.set(new Date(self.data().getEnd())); - }); - } +// cardEndDate +Template.cardEndDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getEnd())); + }); +}); +Template.cardEndDate.helpers(cardDateHelpers({ classes() { + const tpl = Template.instance(); let classes = 'end-date '; - const dueAt = this.data().getDue(); - const theDate = this.date.get(); + const data = Template.currentData(); + const dueAt = data.getDue(); + const theDate = tpl.date.get(); if (!dueAt) { - // No due date set - just show as completed classes += 'completed'; } else if (isBefore(theDate, dueAt)) { - // End date is before due date - completed early classes += 'completed-early'; } else if (isAfter(theDate, dueAt)) { - // End date is after due date - completed late classes += 'completed-late'; } else { - // End date equals due date - completed on time classes += 'completed-on-time'; } return classes; - } - + }, showTitle() { - return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`; - } + const tpl = Template.instance(); + return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`; + }, +})); - events() { - return super.events().concat({ - 'click .js-edit-date': Popup.open('editCardEndDate'), - }); - } -} -CardEndDate.register('cardEndDate'); +Template.cardEndDate.events({ + 'click .js-edit-date': Popup.open('editCardEndDate'), +}); -class CardCustomFieldDate extends CardDate { - template() { - return 'dateCustomField'; - } - - onCreated() { - super.onCreated(); - const self = this; - self.autorun(() => { - self.date.set(new Date(self.data().value)); - }); - } - - showWeek() { - return getISOWeek(this.date.get()).toString(); - } - - showWeekOfYear() { - const user = ReactiveCache.getCurrentUser(); - if (!user) { - // For non-logged-in users, week of year is not shown - return false; - } - return user.isShowWeekOfYear(); - } +// cardCustomFieldDate +Template.cardCustomFieldDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().value)); + }); +}); +Template.cardCustomFieldDate.helpers(cardDateHelpers({ showDate() { + const tpl = Template.instance(); // this will start working once mquandalle:moment // is updated to at least moment.js 2.10.5 // until then, the date is displayed in the "L" format - return this.date.get().calendar(null, { + return tpl.date.get().calendar(null, { sameElse: 'llll', }); - } - + }, showTitle() { + const tpl = Template.instance(); const currentUser = ReactiveCache.getCurrentUser(); const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); return `${formattedDate}`; - } - + }, classes() { return 'customfield-date'; - } + }, +})); - events() { - return []; - } -} -CardCustomFieldDate.register('cardCustomFieldDate'); +// --- Minicard date templates --- -(class extends CardReceivedDate { - template() { - return 'minicardReceivedDate'; - } +// minicardReceivedDate +Template.minicardReceivedDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getReceived())); + }); +}); - showDate() { - const currentUser = ReactiveCache.getCurrentUser(); - const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } -}.register('minicardReceivedDate')); - -(class extends CardStartDate { - template() { - return 'minicardStartDate'; - } - - showDate() { - const currentUser = ReactiveCache.getCurrentUser(); - const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } -}.register('minicardStartDate')); - -(class extends CardDueDate { - template() { - return 'minicardDueDate'; - } - - showDate() { - const currentUser = ReactiveCache.getCurrentUser(); - const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } -}.register('minicardDueDate')); - -(class extends CardEndDate { - template() { - return 'minicardEndDate'; - } - - showDate() { - const currentUser = ReactiveCache.getCurrentUser(); - const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } -}.register('minicardEndDate')); - -(class extends CardCustomFieldDate { - template() { - return 'minicardCustomFieldDate'; - } - - showDate() { - const currentUser = ReactiveCache.getCurrentUser(); - const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } -}.register('minicardCustomFieldDate')); - -class VoteEndDate extends CardDate { - onCreated() { - super.onCreated(); - const self = this; - self.autorun(() => { - self.date.set(new Date(self.data().getVoteEnd())); - }); - } +Template.minicardReceivedDate.helpers(cardDateHelpers({ classes() { - const classes = 'end-date' + ' '; + const tpl = Template.instance(); + let classes = 'received-date '; + const data = Template.currentData(); + const dueAt = data.getDue(); + const endAt = data.getEnd(); + const startAt = data.getStart(); + const theDate = tpl.date.get(); + + if ( + (startAt && isAfter(theDate, startAt)) || + (endAt && isAfter(theDate, endAt)) || + (dueAt && isAfter(theDate, dueAt)) + ) { + classes += 'overdue'; + } else { + classes += 'not-due'; + } return classes; - } + }, + showTitle() { + const tpl = Template.instance(); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); + return `${TAPi18n.__('card-received-on')} ${formattedDate}`; + }, showDate() { const currentUser = ReactiveCache.getCurrentUser(); const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } - showTitle() { - return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`; - } + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, +})); - events() { - return super.events().concat({ - 'click .js-edit-date': Popup.open('editVoteEndDate'), - }); - } -} -VoteEndDate.register('voteEndDate'); +Template.minicardReceivedDate.events({ + 'click .js-edit-date': Popup.open('editCardReceivedDate'), +}); -class PokerEndDate extends CardDate { - onCreated() { - super.onCreated(); - const self = this; - self.autorun(() => { - self.date.set(new Date(self.data().getPokerEnd())); - }); - } +// minicardStartDate +Template.minicardStartDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getStart())); + }); +}); + +Template.minicardStartDate.helpers(cardDateHelpers({ classes() { - const classes = 'end-date' + ' '; + const tpl = Template.instance(); + let classes = 'start-date '; + const data = Template.currentData(); + const dueAt = data.getDue(); + const endAt = data.getEnd(); + const theDate = tpl.date.get(); + const nowVal = tpl.now.get(); + + if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) { + classes += 'overdue'; + } else if (isAfter(theDate, nowVal)) { + classes += 'not-due'; + } else { + classes += 'current'; + } return classes; - } + }, + showTitle() { + const tpl = Template.instance(); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); + return `${TAPi18n.__('card-start-on')} ${formattedDate}`; + }, showDate() { const currentUser = ReactiveCache.getCurrentUser(); const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; - return formatDateByUserPreference(this.date.get(), dateFormat, true); - } - showTitle() { - return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`; - } + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, +})); - events() { - return super.events().concat({ - 'click .js-edit-date': Popup.open('editPokerEndDate'), - }); - } -} -PokerEndDate.register('pokerEndDate'); +Template.minicardStartDate.events({ + 'click .js-edit-date': Popup.open('editCardStartDate'), +}); + +// minicardDueDate +Template.minicardDueDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getDue())); + }); +}); + +Template.minicardDueDate.helpers(cardDateHelpers({ + classes() { + const tpl = Template.instance(); + let classes = 'due-date '; + const data = Template.currentData(); + const endAt = data.getEnd(); + const theDate = tpl.date.get(); + const nowVal = tpl.now.get(); + + if (endAt && isBefore(endAt, theDate)) { + classes += 'completed-early'; + } else if (endAt) { + classes += 'completed'; + } else { + const daysDiff = diff(theDate, nowVal, 'days'); + + if (daysDiff < 0) { + classes += 'overdue'; + } else if (daysDiff <= 1) { + classes += 'due-soon'; + } else { + classes += 'not-due'; + } + } + + return classes; + }, + showTitle() { + const tpl = Template.instance(); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); + return `${TAPi18n.__('card-due-on')} ${formattedDate}`; + }, + showDate() { + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, +})); + +Template.minicardDueDate.events({ + 'click .js-edit-date': Popup.open('editCardDueDate'), +}); + +// minicardEndDate +Template.minicardEndDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getEnd())); + }); +}); + +Template.minicardEndDate.helpers(cardDateHelpers({ + classes() { + const tpl = Template.instance(); + let classes = 'end-date '; + const data = Template.currentData(); + const dueAt = data.getDue(); + const theDate = tpl.date.get(); + + if (!dueAt) { + classes += 'completed'; + } else if (isBefore(theDate, dueAt)) { + classes += 'completed-early'; + } else if (isAfter(theDate, dueAt)) { + classes += 'completed-late'; + } else { + classes += 'completed-on-time'; + } + return classes; + }, + showTitle() { + const tpl = Template.instance(); + return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`; + }, + showDate() { + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, +})); + +Template.minicardEndDate.events({ + 'click .js-edit-date': Popup.open('editCardEndDate'), +}); + +// minicardCustomFieldDate +Template.minicardCustomFieldDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().value)); + }); +}); + +Template.minicardCustomFieldDate.helpers(cardDateHelpers({ + showDate() { + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, + showTitle() { + const tpl = Template.instance(); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(tpl.date.get(), dateFormat, true); + return `${formattedDate}`; + }, + classes() { + return 'customfield-date'; + }, +})); + +// --- Vote and Poker end date badge templates --- + +// voteEndDate +Template.voteEndDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getVoteEnd())); + }); +}); + +Template.voteEndDate.helpers(cardDateHelpers({ + classes() { + return 'end-date '; + }, + showDate() { + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, + showTitle() { + const tpl = Template.instance(); + return `${TAPi18n.__('card-end-on')} ${tpl.date.get().toLocaleString()}`; + }, +})); + +Template.voteEndDate.events({ + 'click .js-edit-date': Popup.open('editVoteEndDate'), +}); + +// pokerEndDate +Template.pokerEndDate.onCreated(function () { + cardDateOnCreated(this); + const self = this; + self.autorun(() => { + self.date.set(new Date(Template.currentData().getPokerEnd())); + }); +}); + +Template.pokerEndDate.helpers(cardDateHelpers({ + classes() { + return 'end-date '; + }, + showDate() { + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(Template.instance().date.get(), dateFormat, true); + }, + showTitle() { + const tpl = Template.instance(); + return `${TAPi18n.__('card-end-on')} ${format(tpl.date.get(), 'LLLL')}`; + }, +})); + +Template.pokerEndDate.events({ + 'click .js-edit-date': Popup.open('editPokerEndDate'), +}); diff --git a/client/components/cards/cardDescription.js b/client/components/cards/cardDescription.js index 8cf00f184..1ce48d4e9 100644 --- a/client/components/cards/cardDescription.js +++ b/client/components/cards/cardDescription.js @@ -1,37 +1,29 @@ const descriptionFormIsOpen = new ReactiveVar(false); -BlazeComponent.extendComponent({ - onDestroyed() { - descriptionFormIsOpen.set(false); - $('.note-popover').hide(); - }, +Template.descriptionForm.onDestroyed(function () { + descriptionFormIsOpen.set(false); + $('.note-popover').hide(); +}); +Template.descriptionForm.helpers({ descriptionFormIsOpen() { return descriptionFormIsOpen.get(); }, +}); - getInput() { - return this.$('.js-new-description-input'); +Template.descriptionForm.events({ + async 'submit .js-card-description'(event, tpl) { + event.preventDefault(); + const description = tpl.currentComponent ? tpl.currentComponent().getValue() : tpl.$('textarea').val(); + await this.setDescription(description); }, - - events() { - return [ - { - async 'submit .js-card-description'(event) { - event.preventDefault(); - const description = this.currentComponent().getValue(); - await this.data().setDescription(description); - }, - // Pressing Ctrl+Enter should submit the form - 'keydown form textarea'(evt) { - if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { - const submitButton = this.find('button[type=submit]'); - if (submitButton) { - submitButton.click(); - } - } - }, - }, - ]; + // Pressing Ctrl+Enter should submit the form + 'keydown form textarea'(evt, tpl) { + if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { + const submitButton = tpl.find('button[type=submit]'); + if (submitButton) { + submitButton.click(); + } + } }, -}).register('descriptionForm'); +}); diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index cb40ceec5..946d660f2 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -1,7 +1,12 @@ import { ReactiveCache } from '/imports/reactiveCache'; import { TAPi18n } from '/imports/i18n'; import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; -import { DatePicker } from '/client/lib/datepicker'; +import { + setupDatePicker, + datePickerRendered, + datePickerHelpers, + datePickerEvents, +} from '/client/lib/datepicker'; import { formatDateTime, formatDate, @@ -31,62 +36,221 @@ import Lists from '/models/lists'; import CardComments from '/models/cardComments'; import { ALLOWED_COLORS } from '/config/const'; import { UserAvatar } from '../users/userAvatar'; -import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList'; -import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard'; +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; -BlazeComponent.extendComponent({ - mixins() { - return [Mixins.InfiniteScrolling]; - }, +function getCardId() { + return getCurrentCardIdFromContext(); +} - calculateNextPeak() { +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.callFirstWith(this, 'setNextPeak', altitude); + this.infiniteScrolling.setNextPeak(altitude); } - }, + }; - reachNextPeak() { - const activitiesComponent = this.childComponents('activities')[0]; - activitiesComponent.loadNextPage(); - }, - - onCreated() { - this.currentBoard = Utils.getCurrentBoard(); - this.isLoaded = new ReactiveVar(false); - - if (this.parentComponent() && this.parentComponent().parentComponent()) { - const boardBody = this.parentComponent().parentComponent(); - //in Miniview parent is Board, not BoardBody. - if (boardBody !== null) { - // Only show overlay in mobile mode, not in desktop mode - const isMobile = Utils.getMobileMode(); - if (isMobile) { - boardBody.showOverlay.set(true); + 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(); } - boardBody.mouseHasEnterCardDetails = false; } } - this.calculateNextPeak(); + }; - Meteor.subscribe('unsaved-edits'); + Meteor.subscribe('unsaved-edits'); +}); - // this.findUsersOptions = new ReactiveVar({}); - // this.page = new ReactiveVar(1); - // this.autorun(() => { - // const limitUsers = this.page.get() * Number.MAX_SAFE_INTEGER; - // this.subscribe('people', this.findUsersOptions.get(), limitUsers, () => {}); - // }); - }, +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 = this.currentData(); + const card = Template.currentData(); if (!card || typeof card.findWatcher !== 'function') return false; return card.findWatcher(Meteor.userId()); }, @@ -95,7 +259,6 @@ BlazeComponent.extendComponent({ return ReactiveCache.getCurrentUser().hasCustomFieldsGrid(); }, - cardMaximized() { return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized(); }, @@ -118,7 +281,8 @@ BlazeComponent.extendComponent({ }, presentParentTask() { - let result = this.currentBoard.presentParentTask; + const tpl = Template.instance(); + let result = tpl.currentBoard.presentParentTask; if (result === null || result === undefined) { result = 'no-parent'; } @@ -126,7 +290,7 @@ BlazeComponent.extendComponent({ }, linkForCard() { - const card = this.currentData(); + const card = Template.currentData(); let result = '#'; if (card) { const board = ReactiveCache.getBoard(card.boardId); @@ -142,7 +306,7 @@ BlazeComponent.extendComponent({ }, showVotingButtons() { - const card = this.currentData(); + const card = Template.currentData(); return ( (currentUser.isBoardMember() || (currentUser && card.voteAllowNonBoardMembers())) && @@ -151,7 +315,7 @@ BlazeComponent.extendComponent({ }, showPlanningPokerButtons() { - const card = this.currentData(); + const card = Template.currentData(); return ( (currentUser.isBoardMember() || (currentUser && card.pokerAllowNonBoardMembers())) && @@ -164,557 +328,424 @@ BlazeComponent.extendComponent({ return user && user.isVerticalScrollbars(); }, - /** returns if the list id is the current list id - * @param listId list id to check - * @return is the list id the current list id ? - */ isCurrentListId(listId) { - const data = this.data(); + const data = Template.currentData(); if (!data || typeof data.listId === 'undefined') return false; return data.listId == listId; }, - onRendered() { - if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) { - // Send Webhook but not create Activities records --- - const card = this.currentData(); - const userId = Meteor.userId(); - const params = { - userId, - cardId: card._id, - boardId: card.boardId, - listId: card.listId, - user: ReactiveCache.getCurrentUser().username, - url: '', - }; + isLoaded() { + return Template.instance().isLoaded; + }, +}); - const integrations = ReactiveCache.getIntegrations({ - boardId: { $in: [card.boardId, Integrations.Const.GLOBAL_WEBHOOK_ID] }, - enabled: true, - activities: { $in: ['CardDetailsRendered', 'all'] }, +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' }); - - 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); - } - }); - }, - - onDestroyed() { - if (this.parentComponent() === null) return; - const parentComponent = this.parentComponent().parentComponent(); - //on mobile view parent is Board, not board body. - if (parentComponent === null) return; - parentComponent.showOverlay.set(false); - }, - - events() { - const events = { - [`${CSSEvents.transitionend} .js-card-details`]() { - this.isLoaded.set(true); - }, - [`${CSSEvents.animationend} .js-card-details`]() { - this.isLoaded.set(true); - }, }; - return [ - { - ...events, - 'click .js-card-collapse-toggle'() { - 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 onMouseUp = () => { + $(document).off('mousemove', onMouseMove); + $(document).off('mouseup', onMouseUp); + }; - 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'() { - // Get board ID from either the card data or current board in session - const card = this.currentData() || this.data(); - 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) { - event.preventDefault(); - const url = this.data().absoluteUrl(); - const promise = Utils.copyTextToClipboard(url); - - const $tooltip = this.$('.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); - }, - 'click .js-card-mobile-desktop-toggle'(event) { - event.preventDefault(); - const currentMode = Utils.getMobileMode(); - Utils.setMobileMode(!currentMode); - }, - async 'submit .js-card-description'(event) { - event.preventDefault(); - const description = this.currentComponent().getValue(); - await this.data().setDescription(description); - }, - async 'submit .js-card-details-title'(event) { - event.preventDefault(); - const title = this.currentComponent().getValue().trim(); - if (title) { - await this.data().setTitle(title); - } else { - await this.data().setTitle(''); - } - }, - 'submit .js-card-details-assigner'(event) { - event.preventDefault(); - const assigner = this.currentComponent().getValue().trim(); - if (assigner) { - this.data().setAssignedBy(assigner); - } else { - this.data().setAssignedBy(''); - } - }, - 'submit .js-card-details-requester'(event) { - event.preventDefault(); - const requester = this.currentComponent().getValue().trim(); - if (requester) { - this.data().setRequestedBy(requester); - } else { - this.data().setRequestedBy(''); - } - }, - 'keydown input.js-edit-card-sort'(evt) { - // enter = save - if (evt.keyCode === 13) { - this.find('button[type=submit]').click(); - } - }, - async 'submit .js-card-details-sort'(event) { - event.preventDefault(); - const sort = parseFloat(this.currentComponent() - .getValue() - .trim()); - if (!Number.isNaN(sort)) { - let card = this.data(); - await card.move(card.boardId, card.swimlaneId, card.listId, sort); - } - }, - async 'change .js-select-card-details-lists'(event) { - let card = this.data(); - const listSelect = this.$('.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'() { - Utils.goCardId(this.data().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'() { - if (this.parentComponent() === null) return; - const parentComponent = this.parentComponent().parentComponent(); - //on mobile view parent is Board, not BoardBody. - if (parentComponent === null) return; - parentComponent.showOverlay.set(true); - parentComponent.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'() { - await this.data().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 forIt = $(e.target).hasClass('js-vote-positive'); - let newState = null; - if ( - this.data().voteState() === null || - (this.data().voteState() === false && forIt) || - (this.data().voteState() === true && !forIt) - ) { - newState = forIt; - } - // Use secure server method; direct client updates to vote are blocked - Meteor.call('cards.vote', this.data()._id, newState); - }, - 'click .js-poker'(e) { - let newState = null; - if ($(e.target).hasClass('js-poker-vote-one')) { - newState = 'one'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-two')) { - newState = 'two'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-three')) { - newState = 'three'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-five')) { - newState = 'five'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-eight')) { - newState = 'eight'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-thirteen')) { - newState = 'thirteen'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-twenty')) { - newState = 'twenty'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-forty')) { - newState = 'forty'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-one-hundred')) { - newState = 'oneHundred'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - if ($(e.target).hasClass('js-poker-vote-unsure')) { - newState = 'unsure'; - Meteor.call('cards.pokerVote', this.data()._id, newState); - } - }, - 'click .js-poker-finish'(e) { - if ($(e.target).hasClass('js-poker-finish')) { - e.preventDefault(); - const now = new Date(); - Meteor.call('cards.setPokerEnd', this.data()._id, now); - } - }, - - 'click .js-poker-replay'(e) { - if ($(e.target).hasClass('js-poker-replay')) { - e.preventDefault(); - this.currentCard = this.currentData(); - Meteor.call('cards.replayPoker', this.currentCard._id); - Meteor.call('cards.unsetPokerEnd', this.currentCard._id); - Meteor.call('cards.unsetPokerEstimation', this.currentCard._id); - } - }, - 'click .js-poker-estimation'(event) { - event.preventDefault(); - - const ruleTitle = this.find('#pokerEstimation').value; - if (ruleTitle !== undefined && ruleTitle !== '') { - this.find('#pokerEstimation').value = ''; - - if (ruleTitle) { - Meteor.call('cards.setPokerEstimation', this.data()._id, parseInt(ruleTitle, 10)); - } else { - Meteor.call('cards.unsetPokerEstimation', this.data()._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 = this.data(); - const board = card.board(); - // Only allow drag-and-drop if user can modify card and board allows attachments - if (Utils.canModifyCard() && board && board.allowsAttachments) { - $(event.currentTarget).addClass('is-dragging-over'); - } - } - }, - 'dragleave .js-card-details'(event) { - 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 = this.data(); - 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); - } - } - }, - }, - ]; + $(document).on('mousemove', onMouseMove); + $(document).on('mouseup', onMouseUp); }, -}).register('cardDetails'); + '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() { @@ -748,18 +779,16 @@ Template.cardDetailsPopup.helpers({ }, }); -BlazeComponent.extendComponent({ - template() { - return 'exportCard'; - }, +Template.exportCardPopup.helpers({ withApi() { return Template.instance().apiEnabled.get(); }, exportUrlCardPDF() { + const card = getCurrentCardFromContext({ ignorePopupCard: true }) || this; const params = { - boardId: Session.get('currentBoard'), - listId: this.listId, - cardId: this.cardId, + boardId: card.boardId || Session.get('currentBoard'), + listId: card.listId, + cardId: card._id || card.cardId, }; const queryParams = { authToken: Accounts._storedLoginToken(), @@ -771,11 +800,13 @@ BlazeComponent.extendComponent({ ); }, exportFilenameCardPDF() { - //const boardId = Session.get('currentBoard'); - //return `export-card-pdf-${boardId}.xlsx`; - return `export-card.pdf`; + const card = getCurrentCardFromContext({ ignorePopupCard: true }) || this; + return `${String(card.title || 'export-card') + .replace(/[^a-z0-9._-]+/gi, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, '') || 'export-card'}.pdf`; }, -}).register('exportCardPopup'); +}); // only allow number input Template.editCardSortOrderForm.onRendered(function () { @@ -789,45 +820,67 @@ Template.editCardSortOrderForm.onRendered(function () { }); }); -// We extends the normal InlinedForm component to support UnsavedEdits draft -// feature. -(class extends InlinedForm { - _getUnsavedEditKey() { - return { - fieldName: 'cardDescription', - // XXX Recovering the currentCard identifier form a session variable is - // fragile because this variable may change for instance if the route - // change. We should use some component props instead. - docId: Utils.getCurrentCardId(), - }; - } +// inlinedCardDescription extends the normal inlinedForm to support UnsavedEdits +// draft feature for card descriptions. +Template.inlinedCardDescription.onCreated(function () { + this.isOpen = new ReactiveVar(false); - close(isReset = 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(); - let card = Utils.getCurrentCard(); + const draft = (this._getValue() || '').trim(); + const card = getCurrentCardFromContext(); if (card && draft !== card.getDescription()) { - UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue()); + UnsavedEdits.set(this._getUnsavedEditKey(), this._getValue()); } } - super.close(); - } + this.isOpen.set(false); + }; - reset() { + this._reset = () => { UnsavedEdits.reset(this._getUnsavedEditKey()); - this.close(true); - } + this._close(true); + }; +}); - events() { - const parentEvents = InlinedForm.prototype.events()[0]; - return [ - { - ...parentEvents, - 'click .js-close-inlined-form': this.reset, - }, - ]; - } -}.register('inlinedCardDescription')); +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() { @@ -864,62 +917,65 @@ Template.cardDetailsActionsPopup.events({ 'click .js-set-card-color': Popup.open('setCardColor'), async 'click .js-move-card-to-top'(event) { event.preventDefault(); - const minOrder = this.getMinSort(); - await this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1); + 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 maxOrder = this.getMaxSort(); - await this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1); + 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(); - await this.archive(); - Utils.goBoardId(this.boardId); + if (!card) return; + await card.archive(); + Utils.goBoardId(card.boardId); }), 'click .js-more': Popup.open('cardMore'), 'click .js-toggle-watch-card'() { - const currentCard = this; + 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 = this; + const currentCard = Cards.findOne(getCardId()); + if (!currentCard) return; const newValue = !currentCard.showListOnMinicard; Cards.update(currentCard._id, { $set: { showListOnMinicard: newValue } }); Popup.close(); }, }); -BlazeComponent.extendComponent({ - onRendered() { - autosize(this.$('textarea.js-edit-card-title')); - }, - events() { - return [ - { - 'click a.fa.fa-copy'(event) { - const $editor = this.$('textarea'); - const promise = Utils.copyTextToClipboard($editor[0].value); +Template.editCardTitleForm.onRendered(function () { + autosize(this.$('textarea.js-edit-card-title')); +}); - const $tooltip = this.$('.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(); - } - }, - } - ]; - } -}).register('editCardTitleForm'); +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(); @@ -929,7 +985,8 @@ Template.cardMembersPopup.onCreated(function () { Template.cardMembersPopup.events({ 'click .js-select-member'(event) { - const card = Utils.getCurrentCard(); + const card = getCurrentCardFromContext(); + if (!card) return; const memberId = this.userId; card.toggleMember(memberId); event.preventDefault(); @@ -942,7 +999,8 @@ Template.cardMembersPopup.events({ Template.cardMembersPopup.helpers({ isCardMember() { - const card = Template.parentData(); + const card = getCurrentCardFromContext(); + if (!card) return false; const cardMembers = card.getMembers(); return _.contains(cardMembers, this.userId); @@ -1005,179 +1063,158 @@ Template.editCardAssignerForm.events({ }, }); +/** + * 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 */ -(class extends DialogWithBoardSwimlaneListCard { - getDialogOptions() { - const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); - return ret; - } - async setDone(cardId, options) { - // Capture DOM values immediately before any async operations - const position = this.$('input[name="position"]:checked').val(); - - ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); - const card = this.data(); - let sortIndex = 0; - - if (cardId) { - const targetCard = ReactiveCache.getCard(cardId); - if (targetCard) { - if (position === 'above') { - sortIndex = targetCard.sort - 0.5; - } else { - sortIndex = targetCard.sort + 0.5; - } - } - } else { - // If no card selected, move to end - const maxSort = card.getMaxSort(options.listId, options.swimlaneId); - sortIndex = maxSort !== null ? maxSort + 1 : 0; - } - - await card.move(options.boardId, options.swimlaneId, options.listId, sortIndex); - } -}).register('moveCardPopup'); - -/** Copy Card Dialog */ -(class extends DialogWithBoardSwimlaneListCard { - getDialogOptions() { - const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); - return ret; - } - async setDone(cardId, options) { - // Capture DOM values immediately before any async operations - const textarea = this.$('#copy-card-title'); - const title = textarea.val().trim(); - const position = this.$('input[name="position"]:checked').val(); - - ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); - const card = this.data(); - - if (title) { - const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title}); - - // Position the copied card (newCard may be null for cross-board copies - // if the client hasn't received the publication update yet) - if (newCardId) { - const newCard = ReactiveCache.getCard(newCardId); - if (newCard) { - let sortIndex = 0; - - if (cardId) { - const targetCard = ReactiveCache.getCard(cardId); - if (targetCard) { - if (position === 'above') { - sortIndex = targetCard.sort - 0.5; - } else { - sortIndex = targetCard.sort + 0.5; - } - } - } else { - // If no card selected, copy to end - const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId); - sortIndex = maxSort !== null ? 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); - } - } -}).register('copyCardPopup'); - -/** Convert Checklist-Item to card dialog */ -(class extends DialogWithBoardSwimlaneListCard { - getDialogOptions() { - const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); - return ret; - } - async setDone(cardId, options) { - // Capture DOM values immediately before any async operations - const textarea = this.$('#copy-card-title'); - const title = textarea.val().trim(); - const position = this.$('input[name="position"]:checked').val(); - - ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); - const card = this.data(); - - if (title) { - const _id = Cards.insert({ - title: title, - listId: options.listId, - boardId: options.boardId, - swimlaneId: options.swimlaneId, - sort: 0, - }); - const newCard = ReactiveCache.getCard(_id); +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 = targetCard.sort - 0.5; + sortIndex = targetSort - 0.5; } else { - sortIndex = targetCard.sort + 0.5; + sortIndex = targetSort + 0.5; } } } else { - const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId); - sortIndex = maxSort !== null ? maxSort + 1 : 0; + const maxSort = card.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); + await card.move(options.boardId, options.swimlaneId, options.listId, sortIndex); + }, + }); +}); +registerCardDialogTemplate('moveCardPopup'); - Filter.addException(_id); - } - } -}).register('convertChecklistItemToCardPopup'); +/** 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(); -/** Copy many cards dialog */ -(class extends DialogWithBoardSwimlaneListCard { - getDialogOptions() { - const ret = ReactiveCache.getCurrentUser().getMoveAndCopyDialogOptions(); - return ret; - } - async setDone(cardId, options) { - // Capture DOM values immediately before any async operations - const textarea = this.$('#copy-card-title'); - const title = textarea.val().trim(); - const position = this.$('input[name="position"]:checked').val(); + ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); + const card = Template.currentData(); - ReactiveCache.getCurrentUser().setMoveAndCopyDialogOption(this.currentBoardId, options); - const card = this.data(); + if (title) { + const newCardId = await Meteor.callAsync('copyCard', card._id, options.boardId, options.swimlaneId, options.listId, true, {title: title}); - 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}); - - // Position the copied card if (newCardId) { const newCard = ReactiveCache.getCard(newCardId); - let sortIndex = 0; + if (newCard) { + let sortIndex = 0; - if (cardId) { - const targetCard = ReactiveCache.getCard(cardId); - if (targetCard) { - if (position === 'above') { - sortIndex = targetCard.sort - 0.5; - } else { - sortIndex = targetCard.sort + 0.5; + 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; } - } else { - const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId); - sortIndex = maxSort !== null ? maxSort + 1 : 0; - } - await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex); + 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 @@ -1186,107 +1223,221 @@ Template.editCardAssignerForm.events({ // See https://github.com/wekan/wekan/issues/80 Filter.addException(newCardId); } - } - } -}).register('copyManyCardsPopup'); + }, + }); +}); +registerCardDialogTemplate('copyCardPopup'); -BlazeComponent.extendComponent({ - onCreated() { - this.currentCard = this.currentData(); - this.currentColor = new ReactiveVar(this.currentCard.color); - }, +/** 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) { - if (this.currentColor.get() === null) { + const tpl = Template.instance(); + if (tpl.currentColor.get() === null) { return color === 'white'; } - return this.currentColor.get() === color; + return tpl.currentColor.get() === color; }, +}); - events() { - return [ - { - 'click .js-palette-color'() { - this.currentColor.set(this.currentData().color); - }, - async 'click .js-submit'(event) { - event.preventDefault(); - await this.currentCard.setColor(this.currentColor.get()); - Popup.back(); - }, - async 'click .js-remove-color'(event) { - event.preventDefault(); - await this.currentCard.setColor(null); - Popup.back(); - }, - }, - ]; +Template.setCardColorPopup.events({ + 'click .js-palette-color'(event, tpl) { + tpl.currentColor.set(Template.currentData().color); }, -}).register('setCardColorPopup'); - -BlazeComponent.extendComponent({ - onCreated() { - this.currentColor = new ReactiveVar(null); + 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 this.currentColor.get() === color; + return Template.instance().currentColor.get() === color; }, +}); - events() { - return [ - { - 'click .js-palette-color'(event) { - // 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; - this.currentColor.set(color); - }, - async 'click .js-submit'(event) { - event.preventDefault(); - const color = this.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) { - 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.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); }, -}).register('setSelectionColorPopup'); - -BlazeComponent.extendComponent({ - onCreated() { - this.currentCard = this.currentData(); - 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); + 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( { @@ -1302,10 +1453,11 @@ BlazeComponent.extendComponent({ }, cards() { - const currentId = Utils.getCurrentCardId(); - if (this.parentBoard.get()) { + const tpl = Template.instance(); + const currentId = getCardId(); + if (tpl.parentBoard.get()) { const ret = ReactiveCache.getCards({ - boardId: this.parentBoard.get(), + boardId: tpl.parentBoard.get(), _id: { $ne: currentId }, }); return ret; @@ -1315,564 +1467,256 @@ BlazeComponent.extendComponent({ }, isParentBoard() { - const board = this.currentData(); - if (this.parentBoard.get()) { - return board._id === this.parentBoard.get(); + const tpl = Template.instance(); + const board = Template.currentData(); + if (tpl.parentBoard.get()) { + return board._id === tpl.parentBoard.get(); } return false; }, isParentCard() { - const card = this.currentData(); - if (this.parentCard) { - return card._id === this.parentCard; + const tpl = Template.instance(); + const card = Template.currentData(); + if (tpl.parentCard) { + return card._id === tpl.parentCard; } return false; }, +}); - setParentCardId(cardId) { - if (cardId) { - this.parentCard = ReactiveCache.getCard(cardId); +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 { - this.parentCard = null; + // 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); } - this.currentCard.setParentId(cardId); + 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); }, - - events() { - return [ - { - 'click .js-copy-card-link-to-clipboard'(event) { - const promise = Utils.copyTextToClipboard(location.origin + document.getElementById('cardURL').value); - - const $tooltip = this.$('.copied-tooltip'); - Utils.showCopied(promise, $tooltip); - }, - 'click .js-delete': Popup.afterConfirm('cardDelete', function () { - Popup.close(); - // verify that there are no linked cards - if (ReactiveCache.getCards({ linkedId: this._id }).length === 0) { - Cards.remove(this._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: ${this._id - } at client/components/cards/cardDetails.js and https://github.com/wekan/wekan/issues/2785`; - alert(message); - } - Utils.goBoardId(this.boardId); - }), - 'change .js-field-parent-board'(event) { - const selection = $(event.currentTarget).val(); - const list = $('.js-field-parent-card'); - if (selection === 'none') { - this.parentBoard.set(null); - } else { - subManager.subscribe('board', $(event.currentTarget).val(), false); - this.parentBoard.set(selection); - list.prop('disabled', false); - } - this.setParentCardId(null); - }, - 'change .js-field-parent-card'(event) { - const selection = $(event.currentTarget).val(); - this.setParentCardId(selection); - }, - }, - ]; + 'change .js-field-parent-card'(event, tpl) { + const selection = $(event.currentTarget).val(); + tpl.setParentCardId(selection); }, -}).register('cardMorePopup'); +}); -BlazeComponent.extendComponent({ - onCreated() { - this.currentCard = this.currentData(); - this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion); - }, +Template.cardStartVotingPopup.onCreated(function () { + const cardId = getCardId(); + this.currentCard = Cards.findOne(cardId); + this.voteQuestion = new ReactiveVar(this.currentCard?.voteQuestion); +}); - events() { - return [ - { - 'click .js-end-date': Popup.open('editVoteEndDate'), - 'submit .edit-vote-question'(evt) { - evt.preventDefault(); - const voteQuestion = evt.target.vote.value; - const publicVote = $('#vote-public').hasClass('is-checked'); - const allowNonBoardMembers = $('#vote-allow-non-members').hasClass( - 'is-checked', - ); - const endString = this.currentCard.getVoteEnd(); - Meteor.call('cards.setVoteQuestion', this.currentCard._id, voteQuestion, publicVote, allowNonBoardMembers); - if (endString) { - Meteor.call('cards.setVoteEnd', this.currentCard._id, endString); - } - Popup.back(); - }, - 'click .js-remove-vote': Popup.afterConfirm('deleteVote', () => { - event.preventDefault(); - Meteor.call('cards.unsetVote', this.currentCard._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.cardStartVotingPopup.helpers({ + getVoteQuestion() { + const card = Cards.findOne(getCardId()); + return card && card.getVoteQuestion ? card.getVoteQuestion() : null; }, -}).register('cardStartVotingPopup'); + 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 -(class extends DatePicker { - onCreated() { - super.onCreated(formatDateTime(now())); - this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd())); - } - events() { - return [ - { - 'submit .edit-date'(evt) { - evt.preventDefault(); +Template.editVoteEndDatePopup.onCreated(function () { + const card = Cards.findOne(getCardId()); + setupDatePicker(this, { + defaultTime: formatDateTime(now()), + initialDate: card?.getVoteEnd ? (card.getVoteEnd() || undefined) : undefined, + }); +}); - // if no time was given, init with 12:00 - const time = - evt.target.time.value || - formatTime(new Date().setHours(12, 0, 0)); +Template.editVoteEndDatePopup.onRendered(function () { + datePickerRendered(this); +}); - const dateString = `${evt.target.date.value} ${time}`; +Template.editVoteEndDatePopup.helpers(datePickerHelpers()); - /* - const newDate = parseDate(dateString, ['L LT'], true); - if (newDate.isValid()) { - // if active vote - store it - if (this.currentData().getVoteQuestion()) { - this._storeDate(newDate.toDate()); - Popup.back(); - } else { - this.currentData().vote = { end: newDate.toDate() }; // set vote end temp - Popup.back(); - } - - - */ - - // Try to parse different date formats using native Date parsing - const formats = [ - 'YYYY-MM-DD HH:mm', - 'MM/DD/YYYY HH:mm', - 'DD.MM.YYYY HH:mm', - 'DD/MM/YYYY HH:mm', - 'DD-MM-YYYY HH:mm' - ]; - - let parsedDate = null; - for (const format of formats) { - parsedDate = parseDate(dateString, [format], true); - if (parsedDate) break; - } - - // Fallback to native Date parsing - if (!parsedDate) { - parsedDate = new Date(dateString); - } - - if (isValidDate(parsedDate)) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(usaDate.toDate()); - } else { - this.currentData().poker = { end: usaDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (euroAmDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(euroAmDate.toDate()); - } else { - this.currentData().poker = { end: euroAmDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (euro24hDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(euro24hDate.toDate()); - this.card.setPokerEnd(euro24hDate.toDate()); - } else { - this.currentData().poker = { end: euro24hDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (eurodotDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(eurodotDate.toDate()); - this.card.setPokerEnd(eurodotDate.toDate()); - } else { - this.currentData().poker = { end: eurodotDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (minusDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(minusDate.toDate()); - this.card.setPokerEnd(minusDate.toDate()); - } else { - this.currentData().poker = { end: minusDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (slashDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(slashDate.toDate()); - this.card.setPokerEnd(slashDate.toDate()); - } else { - this.currentData().poker = { end: slashDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (dotDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(dotDate.toDate()); - this.card.setPokerEnd(dotDate.toDate()); - } else { - this.currentData().poker = { end: dotDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (brezhonegDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(brezhonegDate.toDate()); - this.card.setPokerEnd(brezhonegDate.toDate()); - } else { - this.currentData().poker = { end: brezhonegDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (hrvatskiDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(hrvatskiDate.toDate()); - this.card.setPokerEnd(hrvatskiDate.toDate()); - } else { - this.currentData().poker = { end: hrvatskiDate.toDate() }; // set poker end temp - Popup.back(); - } - } else if (latviaDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(latviaDate.toDate()); - this.card.setPokerEnd(latviaDate.toDate()); - } else { - this.currentData().poker = { end: latviaDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (nederlandsDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(nederlandsDate.toDate()); - this.card.setPokerEnd(nederlandsDate.toDate()); - } else { - this.currentData().poker = { end: nederlandsDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (greekDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(greekDate.toDate()); - this.card.setPokerEnd(greekDate.toDate()); - } else { - this.currentData().poker = { end: greekDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (macedonianDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(macedonianDate.toDate()); - this.card.setPokerEnd(macedonianDate.toDate()); - } else { - this.currentData().poker = { end: macedonianDate.toDate() }; // set poker end temp - } - Popup.back(); - } else { - this.error.set('invalid-date'); - evt.target.date.focus(); - } - }, - 'click .js-delete-date'(evt) { - evt.preventDefault(); - this._deleteDate(); - Popup.back(); - }, - }, - ]; - } - _storeDate(newDate) { - Meteor.call('cards.setVoteEnd', this.card._id, newDate); - } - _deleteDate() { - Meteor.call('cards.unsetVoteEnd', this.card._id); - } -}.register('editVoteEndDatePopup')); - -BlazeComponent.extendComponent({ - onCreated() { - this.currentCard = this.currentData(); - this.pokerQuestion = new ReactiveVar(this.currentCard.pokerQuestion); +Template.editVoteEndDatePopup.events(datePickerEvents({ + storeDate(date) { + Meteor.call('cards.setVoteEnd', this.datePicker.card._id, date); }, - - events() { - return [ - { - 'click .js-end-date': Popup.open('editPokerEndDate'), - 'submit .edit-poker-question'(evt) { - evt.preventDefault(); - const pokerQuestion = true; - const allowNonBoardMembers = $('#poker-allow-non-members').hasClass( - 'is-checked', - ); - const endString = this.currentCard.getPokerEnd(); - - Meteor.call('cards.setPokerQuestion', this.currentCard._id, pokerQuestion, allowNonBoardMembers); - if (endString) { - Meteor.call('cards.setPokerEnd', this.currentCard._id, new Date(endString)); - } - Popup.back(); - }, - 'click .js-remove-poker': Popup.afterConfirm('deletePoker', (event) => { - Meteor.call('cards.unsetPoker', this.currentCard._id); - Popup.back(); - }), - 'click a.js-toggle-poker-allow-non-members'(event) { - event.preventDefault(); - $('#poker-allow-non-members').toggleClass('is-checked'); - }, - }, - ]; + deleteDate() { + Meteor.call('cards.unsetVoteEnd', this.datePicker.card._id); }, -}).register('cardStartPlanningPokerPopup'); +})); + +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 -(class extends DatePicker { - onCreated() { - super.onCreated(formatDateTime(now())); - this.data().getPokerEnd() && - this.date.set(new Date(this.data().getPokerEnd())); - } +Template.editPokerEndDatePopup.onCreated(function () { + const card = Cards.findOne(getCardId()); + setupDatePicker(this, { + defaultTime: formatDateTime(now()), + initialDate: card?.getPokerEnd ? (card.getPokerEnd() || undefined) : undefined, + }); +}); - /* - Tried to use dateFormat and timeFormat from client/components/lib/datepicker.js - to make detecting all date formats not necessary, - but got error "language mk does not exist". - Maybe client/components/lib/datepicker.jade could have hidden input field for - datepicker format that could be used to detect date format? +Template.editPokerEndDatePopup.onRendered(function () { + datePickerRendered(this); +}); - dateFormat() { - return moment.localeData().longDateFormat('L'); - } +Template.editPokerEndDatePopup.helpers(datePickerHelpers()); - timeFormat() { - return moment.localeData().longDateFormat('LT'); - } - - const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true); - */ - - events() { - return [ - { - 'submit .edit-date'(evt) { - evt.preventDefault(); - - // if no time was given, init with 12:00 - const time = - evt.target.time.value || - formatTime(new Date().setHours(12, 0, 0)); - - const dateString = `${evt.target.date.value} ${time}`; - - /* - Tried to use dateFormat and timeFormat from client/components/lib/datepicker.js - to make detecting all date formats not necessary, - but got error "language mk does not exist". - Maybe client/components/lib/datepicker.jade could have hidden input field for - datepicker format that could be used to detect date format? - - const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true); - - if (newDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(newDate.toDate()); - Popup.back(); - } else { - this.currentData().poker = { end: newDate.toDate() }; // set poker end temp - Popup.back(); - } - */ - - // Try to parse different date formats using native Date parsing - const formats = [ - 'YYYY-MM-DD HH:mm', - 'MM/DD/YYYY HH:mm', - 'DD.MM.YYYY HH:mm', - 'DD/MM/YYYY HH:mm', - 'DD-MM-YYYY HH:mm' - ]; - - let parsedDate = null; - for (const format of formats) { - parsedDate = parseDate(dateString, [format], true); - if (parsedDate) break; - } - - // Fallback to native Date parsing - if (!parsedDate) { - parsedDate = new Date(dateString); - } - - if (isValidDate(parsedDate)) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(usaDate.toDate()); - } else { - this.currentData().poker = { end: usaDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (euroAmDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(euroAmDate.toDate()); - } else { - this.currentData().poker = { end: euroAmDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (euro24hDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(euro24hDate.toDate()); - this.card.setPokerEnd(euro24hDate.toDate()); - } else { - this.currentData().poker = { end: euro24hDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (eurodotDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(eurodotDate.toDate()); - this.card.setPokerEnd(eurodotDate.toDate()); - } else { - this.currentData().poker = { end: eurodotDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (minusDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(minusDate.toDate()); - this.card.setPokerEnd(minusDate.toDate()); - } else { - this.currentData().poker = { end: minusDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (slashDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(slashDate.toDate()); - this.card.setPokerEnd(slashDate.toDate()); - } else { - this.currentData().poker = { end: slashDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (dotDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(dotDate.toDate()); - this.card.setPokerEnd(dotDate.toDate()); - } else { - this.currentData().poker = { end: dotDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (brezhonegDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(brezhonegDate.toDate()); - this.card.setPokerEnd(brezhonegDate.toDate()); - } else { - this.currentData().poker = { end: brezhonegDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (hrvatskiDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(hrvatskiDate.toDate()); - this.card.setPokerEnd(hrvatskiDate.toDate()); - } else { - this.currentData().poker = { end: hrvatskiDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (latviaDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(latviaDate.toDate()); - this.card.setPokerEnd(latviaDate.toDate()); - } else { - this.currentData().poker = { end: latviaDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (nederlandsDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(nederlandsDate.toDate()); - this.card.setPokerEnd(nederlandsDate.toDate()); - } else { - this.currentData().poker = { end: nederlandsDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (greekDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(greekDate.toDate()); - this.card.setPokerEnd(greekDate.toDate()); - } else { - this.currentData().poker = { end: greekDate.toDate() }; // set poker end temp - } - Popup.back(); - } else if (macedonianDate.isValid()) { - // if active poker - store it - if (this.currentData().getPokerQuestion()) { - this._storeDate(macedonianDate.toDate()); - this.card.setPokerEnd(macedonianDate.toDate()); - } else { - this.currentData().poker = { end: macedonianDate.toDate() }; // set poker end temp - } - Popup.back(); - } else { - // this.error.set('invalid-date); - this.error.set('invalid-date' + ' ' + dateString); - evt.target.date.focus(); - } - }, - 'click .js-delete-date'(evt) { - evt.preventDefault(); - this._deleteDate(); - Popup.back(); - }, - }, - ]; - } - _storeDate(newDate) { - Meteor.call('cards.setPokerEnd', this.card._id, newDate); - } - _deleteDate() { - Meteor.call('cards.unsetPokerEnd', this.card._id); - } -}.register('editPokerEndDatePopup')); +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( @@ -1882,13 +1726,17 @@ EscapeActions.register( // ask user whether changes should be applied if (ReactiveCache.getCurrentUser()) { if (ReactiveCache.getCurrentUser().profile.rescueCardDescription == true) { - currentDescription = document.getElementsByClassName("editor js-new-description-input").item(0) - if (currentDescription?.value && !(currentDescription.value === Utils.getCurrentCard().getDescription())) { + 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 Utils.getCurrentCard().setDescription(document.getElementsByClassName("editor js-new-description-input").item(0).value); + await currentCard.setDescription(currentDescription.value); // Save it! - console.log(document.getElementsByClassName("editor js-new-description-input").item(0).value); - console.log("current description", Utils.getCurrentCard().getDescription()); + console.log(currentDescription.value); + console.log("current description", currentCard.getDescription()); } else { // Do nothing! console.log('Description changes were not saved to the database.'); @@ -1922,7 +1770,8 @@ Template.cardAssigneesPopup.onCreated(function () { Template.cardAssigneesPopup.events({ 'click .js-select-assignee'(event) { - const card = Utils.getCurrentCard(); + const card = getCurrentCardFromContext(); + if (!card) return; const assigneeId = this.userId; card.toggleAssignee(assigneeId); event.preventDefault(); @@ -1935,7 +1784,8 @@ Template.cardAssigneesPopup.events({ Template.cardAssigneesPopup.helpers({ isCardAssignee() { - const card = Template.parentData(); + const card = getCurrentCardFromContext(); + if (!card) return false; const cardAssignees = card.getAssignees(); return _.contains(cardAssignees, this.userId); @@ -1971,7 +1821,8 @@ Template.cardAssigneePopup.helpers({ }, isCardAssignee() { - const card = Template.parentData(); + const card = getCurrentCardFromContext(); + if (!card) return false; const cardAssignees = card.getAssignees(); return _.contains(cardAssignees, this.userId); diff --git a/client/components/cards/cardTime.js b/client/components/cards/cardTime.js index fe6b7b03c..7cebe5431 100644 --- a/client/components/cards/cardTime.js +++ b/client/components/cards/cardTime.js @@ -1,85 +1,91 @@ import { TAPi18n } from '/imports/i18n'; +import Cards from '/models/cards'; +import { getCurrentCardIdFromContext } from '/client/lib/currentCard'; -BlazeComponent.extendComponent({ - template() { - return 'editCardSpentTime'; +function getCardId() { + return getCurrentCardIdFromContext(); +} + +Template.editCardSpentTimePopup.onCreated(function () { + this.error = new ReactiveVar(''); + this.card = Cards.findOne(getCardId()); +}); + +Template.editCardSpentTimePopup.helpers({ + error() { + return Template.instance().error; }, - onCreated() { - this.error = new ReactiveVar(''); - this.card = this.data(); + card() { + return Cards.findOne(getCardId()); }, - toggleOvertime() { - this.card.setIsOvertime(!this.card.getIsOvertime()); + getIsOvertime() { + const card = Cards.findOne(getCardId()); + return card?.getIsOvertime ? card.getIsOvertime() : false; + }, +}); + +Template.editCardSpentTimePopup.events({ + //TODO : need checking this portion + 'submit .edit-time'(evt, tpl) { + evt.preventDefault(); + const card = Cards.findOne(getCardId()); + if (!card) return; + + const spentTime = parseFloat(evt.target.time.value); + let isOvertime = false; + if ($('#overtime').attr('class').indexOf('is-checked') >= 0) { + isOvertime = true; + } + if (spentTime >= 0) { + card.setSpentTime(spentTime); + card.setIsOvertime(isOvertime); + Popup.back(); + } else { + tpl.error.set('invalid-time'); + evt.target.time.focus(); + } + }, + 'click .js-delete-time'(evt) { + evt.preventDefault(); + const card = Cards.findOne(getCardId()); + if (!card) return; + card.setSpentTime(null); + card.setIsOvertime(false); + Popup.back(); + }, + 'click a.js-toggle-overtime'(evt) { + const card = Cards.findOne(getCardId()); + if (!card) return; + card.setIsOvertime(!card.getIsOvertime()); $('#overtime .materialCheckBox').toggleClass('is-checked'); $('#overtime').toggleClass('is-checked'); }, - storeTime(spentTime, isOvertime) { - this.card.setSpentTime(spentTime); - this.card.setIsOvertime(isOvertime); - }, - deleteTime() { - this.card.setSpentTime(null); - this.card.setIsOvertime(false); - }, - events() { - return [ - { - //TODO : need checking this portion - 'submit .edit-time'(evt) { - evt.preventDefault(); +}); - const spentTime = parseFloat(evt.target.time.value); - //const isOvertime = this.card.getIsOvertime(); - let isOvertime = false; - if ($('#overtime').attr('class').indexOf('is-checked') >= 0) { - isOvertime = true; - } - if (spentTime >= 0) { - this.storeTime(spentTime, isOvertime); - Popup.back(); - } else { - this.error.set('invalid-time'); - evt.target.time.focus(); - } - }, - 'click .js-delete-time'(evt) { - evt.preventDefault(); - this.deleteTime(); - Popup.back(); - }, - 'click a.js-toggle-overtime': this.toggleOvertime, - }, - ]; - }, -}).register('editCardSpentTimePopup'); - -BlazeComponent.extendComponent({ - template() { - return 'timeBadge'; - }, - onCreated() { - const self = this; - self.time = ReactiveVar(); - }, +Template.cardSpentTime.helpers({ showTitle() { - if (this.data().getIsOvertime()) { + const card = Cards.findOne(this._id) || this; + if (card.getIsOvertime && card.getIsOvertime()) { return `${TAPi18n.__( 'overtime', - )} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`; - } else { + )} ${card.getSpentTime()} ${TAPi18n.__('hours')}`; + } else if (card.getSpentTime) { return `${TAPi18n.__( 'card-spent', - )} ${this.data().getSpentTime()} ${TAPi18n.__('hours')}`; + )} ${card.getSpentTime()} ${TAPi18n.__('hours')}`; } + return ''; }, showTime() { - return this.data().getSpentTime(); + const card = Cards.findOne(this._id) || this; + return card.getSpentTime ? card.getSpentTime() : ''; }, - events() { - return [ - { - 'click .js-edit-time': Popup.open('editCardSpentTime'), - }, - ]; + getIsOvertime() { + const card = Cards.findOne(this._id) || this; + return card.getIsOvertime ? card.getIsOvertime() : false; }, -}).register('cardSpentTime'); +}); + +Template.cardSpentTime.events({ + 'click .js-edit-time': Popup.open('editCardSpentTime'), +}); diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js index d32903699..7dd0c3343 100644 --- a/client/components/cards/checklists.js +++ b/client/components/cards/checklists.js @@ -2,7 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache'; import { TAPi18n } from '/imports/i18n'; import Cards from '/models/cards'; import Boards from '/models/boards'; -import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard'; +import { BoardSwimlaneListCardDialog } from '/client/lib/dialogWithBoardSwimlaneListCard'; const subManager = new SubsManager(); const { calculateIndexData, capitalize } = Utils; @@ -45,55 +45,63 @@ function initSorting(items) { }); } -BlazeComponent.extendComponent({ - onRendered() { - const self = this; - self.itemsDom = this.$('.js-checklist-items'); - initSorting(self.itemsDom); - self.itemsDom.mousedown(function (evt) { - evt.stopPropagation(); - }); +Template.checklistDetail.onRendered(function () { + const tpl = this; + tpl.itemsDom = this.$('.js-checklist-items'); + initSorting(tpl.itemsDom); + tpl.itemsDom.mousedown(function (evt) { + evt.stopPropagation(); + }); - function userIsMember() { - return ReactiveCache.getCurrentUser()?.isBoardMember(); - } + function userIsMember() { + return ReactiveCache.getCurrentUser()?.isBoardMember(); + } - // Disable sorting if the current user is not a board member - self.autorun(() => { - const $itemsDom = $(self.itemsDom); - if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) { - $(self.itemsDom).sortable('option', 'disabled', !userIsMember()); - if (Utils.isTouchScreenOrShowDesktopDragHandles()) { - $(self.itemsDom).sortable({ - handle: 'span.fa.checklistitem-handle', - }); - } + // Disable sorting if the current user is not a board member + tpl.autorun(() => { + const $itemsDom = $(tpl.itemsDom); + if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) { + $(tpl.itemsDom).sortable('option', 'disabled', !userIsMember()); + if (Utils.isTouchScreenOrShowDesktopDragHandles()) { + $(tpl.itemsDom).sortable({ + handle: 'span.fa.checklistitem-handle', + }); } - }); - }, + } + }); +}); +Template.checklistDetail.helpers({ /** returns the finished percent of the checklist */ finishedPercent() { - const ret = this.data().checklist.finishedPercent(); + const ret = this.checklist.finishedPercent(); return ret; }, -}).register('checklistDetail'); +}); -BlazeComponent.extendComponent({ - addChecklist(event) { +Template.checklists.helpers({ + checklists() { + const card = ReactiveCache.getCard(this.cardId); + const ret = card.checklists(); + return ret; + }, +}); + +Template.checklists.events({ + 'click .js-open-checklist-details-menu': Popup.open('checklistActions'), + 'submit .js-add-checklist'(event, tpl) { event.preventDefault(); - const textarea = this.find('textarea.js-add-checklist-item'); + const textarea = tpl.find('textarea.js-add-checklist-item'); const title = textarea.value.trim(); - let cardId = this.currentData().cardId; + let cardId = Template.currentData().cardId; const card = ReactiveCache.getCard(cardId); - //if (card.isLinked()) cardId = card.linkedId; if (card.isLinkedCard()) { cardId = card.linkedId; } let sortIndex; let checklistItemIndex; - if (this.currentData().position === 'top') { + if (Template.currentData().position === 'top') { sortIndex = Utils.calculateIndexData(null, card.firstChecklist()).base; checklistItemIndex = 0; } else { @@ -107,27 +115,34 @@ BlazeComponent.extendComponent({ title, sort: sortIndex, }); - this.closeAllInlinedForms(); + tpl.$('.js-close-inlined-form').click(); setTimeout(() => { - this.$('.add-checklist-item') + tpl.$('.add-checklist-item') .eq(checklistItemIndex) .click(); }, 100); } }, - addChecklistItem(event) { + 'submit .js-edit-checklist-title'(event, tpl) { event.preventDefault(); - const textarea = this.find('textarea.js-add-checklist-item'); - const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem'); - const newlineBecomesNewChecklistItemOriginOrder = this.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder'); + const textarea = tpl.find('textarea.js-edit-checklist-item'); const title = textarea.value.trim(); - const checklist = this.currentData().checklist; + const checklist = Template.currentData().checklist; + checklist.setTitle(title); + }, + 'submit .js-add-checklist-item'(event, tpl) { + event.preventDefault(); + const textarea = tpl.find('textarea.js-add-checklist-item'); + const newlineBecomesNewChecklistItem = tpl.find('input#toggleNewlineBecomesNewChecklistItem'); + const newlineBecomesNewChecklistItemOriginOrder = tpl.find('input#toggleNewlineBecomesNewChecklistItemOriginOrder'); + const title = textarea.value.trim(); + const checklist = Template.currentData().checklist; if (title) { let checklistItems = [title]; if (newlineBecomesNewChecklistItem.checked) { checklistItems = title.split('\n').map(_value => _value.trim()); - if (this.currentData().position === 'top') { + if (Template.currentData().position === 'top') { if (newlineBecomesNewChecklistItemOriginOrder.checked === false) { checklistItems = checklistItems.reverse(); } @@ -135,7 +150,7 @@ BlazeComponent.extendComponent({ } let addIndex; let sortIndex; - if (this.currentData().position === 'top') { + if (Template.currentData().position === 'top') { sortIndex = Utils.calculateIndexData(null, checklist.firstItem()).base; addIndex = -1; } else { @@ -156,33 +171,39 @@ BlazeComponent.extendComponent({ textarea.value = ''; textarea.focus(); }, - - async deleteItem() { - const checklist = this.currentData().checklist; - const item = this.currentData().item; + 'submit .js-edit-checklist-item'(event, tpl) { + event.preventDefault(); + const textarea = tpl.find('textarea.js-edit-checklist-item'); + const title = textarea.value.trim(); + const item = Template.currentData().item; + item.setTitle(title); + }, + 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'), + async 'click .js-delete-checklist-item'() { + const checklist = Template.currentData().checklist; + const item = Template.currentData().item; if (checklist && item && item._id) { ChecklistItems.remove(item._id); } }, - - editChecklist(event) { - event.preventDefault(); - const textarea = this.find('textarea.js-edit-checklist-item'); - const title = textarea.value.trim(); - const checklist = this.currentData().checklist; - checklist.setTitle(title); + 'focus .js-add-checklist-item'(event) { + // If a new checklist is created, pre-fill the title and select it. + const checklist = Template.currentData().checklist; + if (!checklist) { + const textarea = event.target; + textarea.value = capitalize(TAPi18n.__('r-checklist')); + textarea.select(); + } }, - - editChecklistItem(event) { - event.preventDefault(); - - const textarea = this.find('textarea.js-edit-checklist-item'); - const title = textarea.value.trim(); - const item = this.currentData().item; - item.setTitle(title); + // add and delete checklist / checklist-item + 'click .js-open-inlined-form'(event, tpl) { + tpl.$('.js-close-inlined-form').click(); }, - - pressKey(event) { + 'click #toggleHideFinishedChecklist'(event) { + event.preventDefault(); + Template.currentData().card.toggleHideFinishedChecklist(); + }, + keydown(event) { //If user press enter key inside a form, submit it //Unless the user is also holding down the 'shift' key if (event.keyCode === 13 && !event.shiftKey) { @@ -191,201 +212,201 @@ BlazeComponent.extendComponent({ $form.find('button[type=submit]').click(); } }, +}); - focusChecklistItem(event) { - // If a new checklist is created, pre-fill the title and select it. - const checklist = this.currentData().checklist; - if (!checklist) { - const textarea = event.target; - textarea.value = capitalize(TAPi18n.__('r-checklist')); - textarea.select(); - } - }, +// NOTE: boardsSwimlanesAndLists template was removed from jade but JS was left behind. +// This is dead code — the template no longer exists in any jade file. - /** closes all inlined forms (checklist and checklist-item input fields) */ - closeAllInlinedForms() { - this.$('.js-close-inlined-form').click(); - }, +Template.addChecklistItemForm.onRendered(function () { + autosize(this.$('textarea.js-add-checklist-item')); +}); - events() { - return [ - { - 'click .js-open-checklist-details-menu': Popup.open('checklistActions'), - 'submit .js-add-checklist': this.addChecklist, - 'submit .js-edit-checklist-title': this.editChecklist, - 'submit .js-add-checklist-item': this.addChecklistItem, - 'submit .js-edit-checklist-item': this.editChecklistItem, - 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'), - 'click .js-delete-checklist-item': this.deleteItem, - 'focus .js-add-checklist-item': this.focusChecklistItem, - // add and delete checklist / checklist-item - 'click .js-open-inlined-form': this.closeAllInlinedForms, - 'click #toggleHideFinishedChecklist'(event) { - event.preventDefault(); - this.data().card.toggleHideFinishedChecklist(); - }, - keydown: this.pressKey, - }, - ]; - }, -}).register('checklists'); +Template.addChecklistItemForm.events({ + 'click a.fa.fa-copy'(event, tpl) { + const $editor = tpl.$('textarea'); + const promise = Utils.copyTextToClipboard($editor[0].value); -BlazeComponent.extendComponent({ - onCreated() { - subManager.subscribe('board', Session.get('currentBoard'), false); - this.selectedBoardId = new ReactiveVar(Session.get('currentBoard')); - }, - - boards() { - const ret = ReactiveCache.getBoards( - { - archived: false, - 'members.userId': Meteor.userId(), - _id: { $ne: ReactiveCache.getCurrentUser().getTemplatesBoardId() }, - }, - { - sort: { sort: 1 /* boards default sorting */ }, - }, - ); - return ret; - }, - - swimlanes() { - const board = ReactiveCache.getBoard(this.selectedBoardId.get()); - return board.swimlanes(); - }, - - aBoardLists() { - const board = ReactiveCache.getBoard(this.selectedBoardId.get()); - return board.lists(); - }, - - events() { - return [ - { - 'change .js-select-boards'(event) { - this.selectedBoardId.set($(event.currentTarget).val()); - subManager.subscribe('board', this.selectedBoardId.get(), false); - }, - }, - ]; - }, -}).register('boardsSwimlanesAndLists'); - -Template.checklists.helpers({ - checklists() { - const card = ReactiveCache.getCard(this.cardId); - const ret = card.checklists(); - return ret; + const $tooltip = tpl.$('.copied-tooltip'); + Utils.showCopied(promise, $tooltip); }, }); -BlazeComponent.extendComponent({ - onRendered() { - autosize(this.$('textarea.js-add-checklist-item')); +Template.checklistActionsPopup.events({ + 'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () { + Popup.back(2); + const checklist = this.checklist; + if (checklist && checklist._id) { + Checklists.remove(checklist._id); + } + }), + 'click .js-move-checklist': Popup.open('moveChecklist'), + 'click .js-copy-checklist': Popup.open('copyChecklist'), + 'click .js-hide-checked-checklist-items'(event) { + event.preventDefault(); + Template.currentData().checklist.toggleHideCheckedChecklistItems(); + Popup.back(); }, - events() { - return [ - { - 'click a.fa.fa-copy'(event) { - const $editor = this.$('textarea'); - const promise = Utils.copyTextToClipboard($editor[0].value); - - const $tooltip = this.$('.copied-tooltip'); - Utils.showCopied(promise, $tooltip); - }, - } - ]; - } -}).register('addChecklistItemForm'); - -BlazeComponent.extendComponent({ - events() { - return [ - { - 'click .js-delete-checklist': Popup.afterConfirm('checklistDelete', function () { - Popup.back(2); - const checklist = this.checklist; - if (checklist && checklist._id) { - Checklists.remove(checklist._id); - } - }), - 'click .js-move-checklist': Popup.open('moveChecklist'), - 'click .js-copy-checklist': Popup.open('copyChecklist'), - 'click .js-hide-checked-checklist-items'(event) { - event.preventDefault(); - this.data().checklist.toggleHideCheckedChecklistItems(); - Popup.back(); - }, - 'click .js-hide-all-checklist-items'(event) { - event.preventDefault(); - this.data().checklist.toggleHideAllChecklistItems(); - Popup.back(); - }, - } - ] - } -}).register('checklistActionsPopup'); - -BlazeComponent.extendComponent({ - onRendered() { - autosize(this.$('textarea.js-edit-checklist-item')); + 'click .js-hide-all-checklist-items'(event) { + event.preventDefault(); + Template.currentData().checklist.toggleHideAllChecklistItems(); + Popup.back(); }, - events() { - return [ - { - 'click a.fa.fa-copy'(event) { - const $editor = this.$('textarea'); - const promise = Utils.copyTextToClipboard($editor[0].value); +}); - const $tooltip = this.$('.copied-tooltip'); - Utils.showCopied(promise, $tooltip); - }, - } - ]; - } -}).register('editChecklistItemForm'); +Template.editChecklistItemForm.onRendered(function () { + autosize(this.$('textarea.js-edit-checklist-item')); +}); + +Template.editChecklistItemForm.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); + }, +}); Template.checklistItemDetail.helpers({ }); -BlazeComponent.extendComponent({ - toggleItem() { - const checklist = this.currentData().checklist; - const item = this.currentData().item; +Template.checklistItemDetail.events({ + 'click .js-checklist-item .check-box-container'() { + const checklist = Template.currentData().checklist; + const item = Template.currentData().item; if (checklist && item && item._id) { item.toggleItem(); } }, - events() { - return [ - { - 'click .js-checklist-item .check-box-container': this.toggleItem, - }, - ]; +}); + +/** + * Helper to find the dialog instance from a parent popup template. + * copyAndMoveChecklist is included inside moveChecklistPopup / copyChecklistPopup, + * so we traverse up the view hierarchy to find the parent template's dialog. + */ +function getParentDialog(tpl) { + let view = tpl.view.parentView; + while (view) { + if (view.templateInstance && view.templateInstance() && view.templateInstance().dialog) { + return view.templateInstance().dialog; + } + view = view.parentView; + } + return null; +} + +/** Shared helpers for copyAndMoveChecklist sub-template */ +Template.copyAndMoveChecklist.helpers({ + boards() { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.boards() : []; }, -}).register('checklistItemDetail'); + swimlanes() { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.swimlanes() : []; + }, + lists() { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.lists() : []; + }, + cards() { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.cards() : []; + }, + isDialogOptionBoardId(boardId) { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.isDialogOptionBoardId(boardId) : false; + }, + isDialogOptionSwimlaneId(swimlaneId) { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.isDialogOptionSwimlaneId(swimlaneId) : false; + }, + isDialogOptionListId(listId) { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.isDialogOptionListId(listId) : false; + }, + isDialogOptionCardId(cardId) { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.isDialogOptionCardId(cardId) : false; + }, + isTitleDefault(title) { + const dialog = getParentDialog(Template.instance()); + return dialog ? dialog.isTitleDefault(title) : title; + }, +}); + +/** + * Helper: register standard card dialog events on a checklist popup template. + * Events bubble up from the copyAndMoveChecklist sub-template to the parent popup. + */ +function registerChecklistDialogEvents(templateName) { + 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 Checklist Dialog */ -(class extends DialogWithBoardSwimlaneListCard { - getDialogOptions() { - const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions(); - return ret; - } - async setDone(cardId, options) { - ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options); - await this.data().checklist.move(cardId); - } -}).register('moveChecklistPopup'); +Template.moveChecklistPopup.onCreated(function () { + this.dialog = new BoardSwimlaneListCardDialog(this, { + getDialogOptions() { + return ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions(); + }, + async setDone(cardId, options) { + ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options); + await Template.currentData().checklist.move(cardId); + }, + }); +}); +registerChecklistDialogEvents('moveChecklistPopup'); /** Copy Checklist Dialog */ -(class extends DialogWithBoardSwimlaneListCard { - getDialogOptions() { - const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions(); - return ret; - } - async setDone(cardId, options) { - ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options); - await this.data().checklist.copy(cardId); - } -}).register('copyChecklistPopup'); +Template.copyChecklistPopup.onCreated(function () { + this.dialog = new BoardSwimlaneListCardDialog(this, { + getDialogOptions() { + return ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions(); + }, + async setDone(cardId, options) { + ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options); + await Template.currentData().checklist.copy(cardId); + }, + }); +}); +registerChecklistDialogEvents('copyChecklistPopup'); diff --git a/client/components/cards/labels.js b/client/components/cards/labels.js index e09598189..2bf1cd8ce 100644 --- a/client/components/cards/labels.js +++ b/client/components/cards/labels.js @@ -5,29 +5,32 @@ Meteor.startup(() => { labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; }); -BlazeComponent.extendComponent({ - onCreated() { - this.currentColor = new ReactiveVar(this.data().color); - }, +Template.formLabel.onCreated(function () { + this.currentColor = new ReactiveVar(this.data.color); +}); +Template.formLabel.helpers({ labels() { return labelColors.map(color => ({ color, name: '' })); }, - isSelected(color) { - return this.currentColor.get() === color; + return Template.instance().currentColor.get() === color; }, +}); - events() { - return [ - { - 'click .js-palette-color'() { - this.currentColor.set(this.currentData().color); - }, - }, - ]; +Template.formLabel.events({ + 'click .js-palette-color'(event, tpl) { + tpl.currentColor.set(Template.currentData().color); + + const $this = $(event.currentTarget); + + // hide selected ll colors + $('.js-palette-select').addClass('hide'); + + // show select color + $this.find('.js-palette-select').removeClass('hide'); }, -}).register('formLabel'); +}); Template.createLabelPopup.helpers({ // This is the default color for a new label. We search the first color that @@ -41,81 +44,68 @@ Template.createLabelPopup.helpers({ }, }); -BlazeComponent.extendComponent({ - onRendered() { - const itemsSelector = 'li.js-card-label-item:not(.placeholder)'; - const $labels = this.$('.edit-labels-pop-over'); +Template.cardLabelsPopup.onRendered(function () { + const tpl = this; + const itemsSelector = 'li.js-card-label-item:not(.placeholder)'; + const $labels = tpl.$('.edit-labels-pop-over'); - $labels.sortable({ - connectWith: '.edit-labels-pop-over', - tolerance: 'pointer', - appendTo: '.edit-labels-pop-over', - helper(element, currentItem) { - let ret = currentItem.clone(); - if (currentItem.closest('.popup-container-depth-0').length == 0) - { // only set css transform at every sub-popup, not at the main popup - const content = currentItem.closest('.content')[0] - const offsetLeft = content.offsetLeft; - const offsetTop = $('.pop-over > .header').height() * -1; - ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`); - } - return ret; - }, - distance: 7, - items: itemsSelector, - placeholder: 'card-label-wrapper placeholder', - start(evt, ui) { - ui.helper.css('z-index', 1000); - ui.placeholder.height(ui.helper.height()); - EscapeActions.clickExecute(evt.target, 'inlinedForm'); - }, - stop(evt, ui) { - const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id) - const card = Blaze.getData(this); - card.board().setNewLabelOrder(newLabelOrderOnlyIds); - }, - }); - - // Disable drag-dropping if the current user is not a board member or is comment only - this.autorun(() => { - if (Utils.isTouchScreenOrShowDesktopDragHandles()) { - $labels.sortable({ - handle: '.label-handle', - }); + $labels.sortable({ + connectWith: '.edit-labels-pop-over', + tolerance: 'pointer', + appendTo: '.edit-labels-pop-over', + helper(element, currentItem) { + let ret = currentItem.clone(); + if (currentItem.closest('.popup-container-depth-0').length == 0) + { // only set css transform at every sub-popup, not at the main popup + const content = currentItem.closest('.content')[0] + const offsetLeft = content.offsetLeft; + const offsetTop = $('.pop-over > .header').height() * -1; + ret.css("transform", `translate(${offsetLeft}px, ${offsetTop}px)`); } - }); - }, - events() { - return [ - { - 'click .js-select-label'(event) { - const card = this.data(); - const labelId = this.currentData()._id; - card.toggleLabel(labelId); - event.preventDefault(); - }, - 'click .js-edit-label': Popup.open('editLabel'), - 'click .js-add-label': Popup.open('createLabel'), - } - ]; - } -}).register('cardLabelsPopup'); + return ret; + }, + distance: 7, + items: itemsSelector, + placeholder: 'card-label-wrapper placeholder', + start(evt, ui) { + ui.helper.css('z-index', 1000); + ui.placeholder.height(ui.helper.height()); + EscapeActions.clickExecute(evt.target, 'inlinedForm'); + }, + stop(evt, ui) { + const newLabelOrderOnlyIds = ui.item.parent().children().toArray().map(_element => Blaze.getData(_element)._id) + const card = Blaze.getData(this); + card.board().setNewLabelOrder(newLabelOrderOnlyIds); + }, + }); -Template.cardLabelsPopup.events({ + // Disable drag-dropping if the current user is not a board member or is comment only + tpl.autorun(() => { + if (Utils.isTouchScreenOrShowDesktopDragHandles()) { + $labels.sortable({ + handle: '.label-handle', + }); + } + }); }); -Template.formLabel.events({ - 'click .js-palette-color'(event) { - const $this = $(event.currentTarget); - - // hide selected ll colors - $('.js-palette-select').addClass('hide'); - - // show select color - $this.find('.js-palette-select').removeClass('hide'); +Template.cardLabelsPopup.helpers({ + isLabelSelected(cardId) { + return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id); }, }); +Template.cardLabelsPopup.events({ + 'click .js-select-label'(event) { + const card = Template.currentData(); + const labelId = this._id; + card.toggleLabel(labelId); + event.preventDefault(); + }, + 'click .js-edit-label': Popup.open('editLabel'), + 'click .js-add-label': Popup.open('createLabel'), +}); + Template.createLabelPopup.events({ // Create the new label 'submit .create-label'(event, templateInstance) { @@ -149,9 +139,3 @@ Template.editLabelPopup.events({ Popup.back(); }, }); - -Template.cardLabelsPopup.helpers({ - isLabelSelected(cardId) { - return _.contains(ReactiveCache.getCard(cardId).labelIds, this._id); - }, -}); diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 06069ea73..55bbf6cb9 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -8,13 +8,9 @@ import uploadProgressManager from '../../lib/uploadProgressManager'; // 'click .member': Popup.open('cardMember') // }); -BlazeComponent.extendComponent({ - template() { - return 'minicard'; - }, - +Template.minicard.helpers({ formattedCurrencyCustomFieldValue(definition) { - const customField = this.data() + const customField = this .customFieldsWD() .find(f => f._id === definition._id); const customFieldTrueValue = @@ -28,7 +24,7 @@ BlazeComponent.extendComponent({ }, formattedStringtemplateCustomFieldValue(definition) { - const customField = this.data() + const customField = this .customFieldsWD() .find(f => f._id === definition._id); @@ -41,7 +37,7 @@ BlazeComponent.extendComponent({ showCreatorOnMinicard() { // cache "board" to reduce the mini-mongodb access - const board = this.data().board(); + const board = this.board(); let ret = false; if (board) { ret = board.allowsCreatorOnMinicard ?? false; @@ -49,13 +45,12 @@ BlazeComponent.extendComponent({ return ret; }, isWatching() { - const card = this.currentData(); - return card.findWatcher(Meteor.userId()); + return this.findWatcher(Meteor.userId()); }, showMembers() { // cache "board" to reduce the mini-mongodb access - const board = this.data().board(); + const board = this.board(); let ret = false; if (board) { ret = @@ -69,7 +64,7 @@ BlazeComponent.extendComponent({ showAssignee() { // cache "board" to reduce the mini-mongodb access - const board = this.data().board(); + const board = this.board(); let ret = false; if (board) { ret = @@ -81,144 +76,6 @@ BlazeComponent.extendComponent({ return ret; }, - /** opens the card label popup only if clicked onto a label - *
  • this is necessary to have the data context of the minicard. - * if .js-card-label is used at click event, then only the data context of the label itself is available at this.currentData() - */ - cardLabelsPopup(event) { - if (this.find('.js-card-label:hover')) { - Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()}); - } - }, - - async toggleChecklistItem() { - const item = this.currentData(); - if (item && item._id) { - await item.toggleItem(); - } - }, - - events() { - return [ - { - 'click .js-linked-link'() { - if (this.data().isLinkedCard()) Utils.goCardId(this.data().linkedId); - else if (this.data().isLinkedBoard()) - Utils.goBoardId(this.data().linkedId); - }, - 'click .js-toggle-minicard-label-text'() { - if (window.localStorage.getItem('hiddenMinicardLabelText')) { - window.localStorage.removeItem('hiddenMinicardLabelText'); //true - } else { - window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true - } - }, - 'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"), - 'click .minicard-labels' : this.cardLabelsPopup, - 'click .js-open-minicard-details-menu'(event) { - event.preventDefault(); - event.stopPropagation(); - Popup.open('cardDetailsActions').call(this, event); - }, - // Drag and drop file upload handlers - 'dragover .minicard'(event) { - // Only prevent default for file drags to avoid interfering with sortable - const dataTransfer = event.originalEvent.dataTransfer; - if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { - event.preventDefault(); - event.stopPropagation(); - } - }, - 'dragenter .minicard'(event) { - const dataTransfer = event.originalEvent.dataTransfer; - if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { - event.preventDefault(); - event.stopPropagation(); - const card = this.data(); - const board = card.board(); - // Only allow drag-and-drop if user can modify card and board allows attachments - if (Utils.canModifyCard() && board && board.allowsAttachments) { - $(event.currentTarget).addClass('is-dragging-over'); - } - } - }, - 'dragleave .minicard'(event) { - const dataTransfer = event.originalEvent.dataTransfer; - if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { - event.preventDefault(); - event.stopPropagation(); - $(event.currentTarget).removeClass('is-dragging-over'); - } - }, - 'drop .minicard'(event) { - 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 = this.data(); - const board = card.board(); - - // Check permissions - if (!Utils.canModifyCard() || !board || !board.allowsAttachments) { - return; - } - - // Check if this is a file drop (not a card reorder) - if (!dataTransfer.files || dataTransfer.files.length === 0) { - return; - } - - const files = dataTransfer.files; - if (files && files.length > 0) { - handleFileUpload(card, files); - } - } - }, - } - ]; - }, -}).register('minicard'); - -BlazeComponent.extendComponent({ - template() { - return 'minicardChecklist'; - }, - - events() { - return [ - { - 'click .js-open-checklist-menu'(event) { - const data = this.currentData(); - const checklist = data.checklist || data; - const card = data.card || this.data(); - const context = { currentData: () => ({ checklist, card }) }; - Popup.open('checklistActions').call(context, event); - }, - }, - ]; - }, - - visibleItems() { - const checklist = this.currentData().checklist || this.currentData(); - const items = checklist.items(); - - return items.filter(item => { - // Hide finished items if hideCheckedChecklistItems is true - if (item.isFinished && checklist.hideCheckedChecklistItems) { - return false; - } - // Hide all items if hideAllChecklistItems is true - if (checklist.hideAllChecklistItems) { - return false; - } - return true; - }); - }, -}).register('minicardChecklist'); - -Template.minicard.helpers({ hiddenMinicardLabelText() { const currentUser = ReactiveCache.getCurrentUser(); if (currentUser) { @@ -235,9 +92,6 @@ Template.minicard.helpers({ ? Meteor.connection._lastSessionId : null; }, - isWatching() { - return this.findWatcher(Meteor.userId()); - }, // Upload progress helpers hasActiveUploads() { return uploadProgressManager.hasActiveUploads(this._id); @@ -283,30 +137,135 @@ Template.minicard.helpers({ } }); -BlazeComponent.extendComponent({ - events() { - return [ - { - 'keydown input.js-edit-card-sort-popup'(evt) { - // enter = save - if (evt.keyCode === 13) { - this.find('button[type=submit]').click(); - } - }, - 'click button.js-submit-edit-card-sort-popup'(event) { - // save button pressed - event.preventDefault(); - const sort = this.$('.js-edit-card-sort-popup')[0] - .value - .trim(); - if (!Number.isNaN(sort)) { - let card = this.data(); - card.move(card.boardId, card.swimlaneId, card.listId, sort); - Popup.back(); - } - }, +Template.minicard.events({ + 'click .js-linked-link'() { + if (this.isLinkedCard()) Utils.goCardId(this.linkedId); + else if (this.isLinkedBoard()) + Utils.goBoardId(this.linkedId); + }, + 'click .js-toggle-minicard-label-text'() { + if (window.localStorage.getItem('hiddenMinicardLabelText')) { + window.localStorage.removeItem('hiddenMinicardLabelText'); //true + } else { + window.localStorage.setItem('hiddenMinicardLabelText', 'true'); //true + } + }, + 'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"), + 'click .minicard-labels'(event, tpl) { + if (tpl.find('.js-card-label:hover')) { + Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: Template.currentData()}); + } + }, + 'click .js-open-minicard-details-menu'(event, tpl) { + event.preventDefault(); + event.stopPropagation(); + const card = Template.currentData(); + Popup.open('cardDetailsActions').call({currentData: () => card}, event); + }, + // Drag and drop file upload handlers + 'dragover .minicard'(event) { + // Only prevent default for file drags to avoid interfering with sortable + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + event.preventDefault(); + event.stopPropagation(); + } + }, + 'dragenter .minicard'(event) { + const dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.types && dataTransfer.types.includes('Files')) { + event.preventDefault(); + event.stopPropagation(); + const card = this; + 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'); } - ] - } -}).register('editCardSortOrderPopup'); + } + }, + 'dragleave .minicard'(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 .minicard'(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 = this; + const board = card.board(); + + // Check permissions + if (!Utils.canModifyCard() || !board || !board.allowsAttachments) { + return; + } + + // Check if this is a file drop (not a card reorder) + if (!dataTransfer.files || dataTransfer.files.length === 0) { + return; + } + + const files = dataTransfer.files; + if (files && files.length > 0) { + handleFileUpload(card, files); + } + } + }, +}); + +Template.minicardChecklist.helpers({ + visibleItems() { + const checklist = this.checklist || this; + const items = checklist.items(); + + return items.filter(item => { + // Hide finished items if hideCheckedChecklistItems is true + if (item.isFinished && checklist.hideCheckedChecklistItems) { + return false; + } + // Hide all items if hideAllChecklistItems is true + if (checklist.hideAllChecklistItems) { + return false; + } + return true; + }); + }, +}); + +Template.minicardChecklist.events({ + 'click .js-open-checklist-menu'(event) { + const data = Template.currentData(); + const checklist = data.checklist || data; + const card = data.card || this; + const context = { currentData: () => ({ checklist, card }) }; + Popup.open('checklistActions').call(context, event); + }, +}); + +Template.editCardSortOrderPopup.events({ + 'keydown input.js-edit-card-sort-popup'(evt, tpl) { + // enter = save + if (evt.keyCode === 13) { + tpl.find('button[type=submit]').click(); + } + }, + 'click button.js-submit-edit-card-sort-popup'(event, tpl) { + // save button pressed + event.preventDefault(); + const sort = tpl.$('.js-edit-card-sort-popup')[0] + .value + .trim(); + if (!Number.isNaN(sort)) { + let card = this; + card.move(card.boardId, card.swimlaneId, card.listId, sort); + Popup.back(); + } + }, +}); diff --git a/client/components/cards/resultCard.js b/client/components/cards/resultCard.js index 8e04f1654..78bc9acd2 100644 --- a/client/components/cards/resultCard.js +++ b/client/components/cards/resultCard.js @@ -4,32 +4,19 @@ Template.resultCard.helpers({ }, }); -BlazeComponent.extendComponent({ - clickOnMiniCard(evt) { - evt.preventDefault(); - const this_ = this; - const cardId = this.currentData()._id; - const boardId = this.currentData().boardId; +Template.resultCard.events({ + 'click .js-minicard'(event) { + event.preventDefault(); + const cardId = Template.currentData()._id; + const boardId = Template.currentData().boardId; Meteor.subscribe('popupCardData', cardId, { onReady() { Session.set('popupCardId', cardId); Session.set('popupCardBoardId', boardId); - this_.cardDetailsPopup(evt); + if (!Popup.isOpen()) { + Popup.open("cardDetails")(event); + } }, }); }, - - cardDetailsPopup(event) { - if (!Popup.isOpen()) { - Popup.open("cardDetails")(event); - } - }, - - events() { - return [ - { - 'click .js-minicard': this.clickOnMiniCard, - }, - ]; - }, -}).register('resultCard'); +}); diff --git a/client/components/cards/subtasks.js b/client/components/cards/subtasks.js index d0d19438c..9687a89bb 100644 --- a/client/components/cards/subtasks.js +++ b/client/components/cards/subtasks.js @@ -1,12 +1,13 @@ import { ReactiveCache } from '/imports/reactiveCache'; import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; -BlazeComponent.extendComponent({ - addSubtask(event) { +Template.subtasks.events({ + 'click .js-open-subtask-details-menu': Popup.open('subtaskActions'), + 'submit .js-add-subtask'(event, tpl) { event.preventDefault(); - const textarea = this.find('textarea.js-add-subtask-item'); + const textarea = tpl.find('textarea.js-add-subtask-item'); const title = textarea.value.trim(); - const cardId = this.currentData().cardId; + const cardId = Template.currentData().cardId; const card = ReactiveCache.getCard(cardId); const sortIndex = -1; const crtBoard = ReactiveCache.getBoard(card.boardId); @@ -53,7 +54,7 @@ BlazeComponent.extendComponent({ Filter.addException(_id); setTimeout(() => { - this.$('.add-subtask-item') + tpl.$('.add-subtask-item') .last() .click(); }, 100); @@ -61,27 +62,20 @@ BlazeComponent.extendComponent({ textarea.value = ''; textarea.focus(); }, - - async deleteSubtask() { - const subtask = this.currentData().subtask; + 'submit .js-edit-subtask-title'(event, tpl) { + event.preventDefault(); + const textarea = tpl.find('textarea.js-edit-subtask-item'); + const title = textarea.value.trim(); + const subtask = Template.currentData().subtask; + subtask.setTitle(title); + }, + async 'click .js-delete-subtask-item'() { + const subtask = Template.currentData().subtask; if (subtask && subtask._id) { await subtask.archive(); } }, - - isBoardAdmin() { - return ReactiveCache.getCurrentUser().isBoardAdmin(); - }, - - async editSubtask(event) { - event.preventDefault(); - const textarea = this.find('textarea.js-edit-subtask-item'); - const title = textarea.value.trim(); - const subtask = this.currentData().subtask; - await subtask.setTitle(title); - }, - - pressKey(event) { + keydown(event) { //If user press enter key inside a form, submit it //Unless the user is also holding down the 'shift' key if (event.keyCode === 13 && !event.shiftKey) { @@ -90,65 +84,56 @@ BlazeComponent.extendComponent({ $form.find('button[type=submit]').click(); } }, +}); - events() { - return [ - { - 'click .js-open-subtask-details-menu': Popup.open('subtaskActions'), - 'submit .js-add-subtask': this.addSubtask, - 'submit .js-edit-subtask-title': this.editSubtask, - 'click .js-delete-subtask-item': this.deleteSubtask, - keydown: this.pressKey, - }, - ]; +Template.subtasks.onCreated(function () { + this.toggleDeleteDialog = new ReactiveVar(false); +}); + +Template.subtasks.helpers({ + isBoardAdmin() { + return ReactiveCache.getCurrentUser().isBoardAdmin(); }, -}).register('subtasks'); + toggleDeleteDialog() { + return Template.instance().toggleDeleteDialog; + }, +}); -BlazeComponent.extendComponent({ - async toggleItem() { - const item = this.currentData().item; +Template.subtaskItemDetail.events({ + async 'click .js-subtasks-item .check-box-unicode'() { + const item = Template.currentData().item; if (item && item._id) { await item.toggleItem(); } }, - events() { - return [ - { - 'click .js-subtasks-item .check-box-unicode': this.toggleItem, - }, - ]; - }, -}).register('subtaskItemDetail'); +}); -BlazeComponent.extendComponent({ +Template.subtaskActionsPopup.helpers({ isBoardAdmin() { return ReactiveCache.getCurrentUser().isBoardAdmin(); }, - events() { - return [ - { - 'click .js-view-subtask'(event) { - if ($(event.target).hasClass('js-view-subtask')) { - const subtask = this.currentData().subtask; - const board = subtask.board(); - FlowRouter.go('card', { - boardId: board._id, - slug: board.slug, - cardId: subtask._id, - }); - } - }, - 'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () { - Popup.back(2); - const subtask = this.subtask; - if (subtask && subtask._id) { - await subtask.archive(); - } - }), - } - ] - } -}).register('subtaskActionsPopup'); +}); + +Template.subtaskActionsPopup.events({ + 'click .js-view-subtask'(event) { + if ($(event.target).hasClass('js-view-subtask')) { + const subtask = Template.currentData().subtask; + const board = subtask.board(); + FlowRouter.go('card', { + boardId: board._id, + slug: board.slug, + cardId: subtask._id, + }); + } + }, + 'click .js-delete-subtask' : Popup.afterConfirm('subtaskDelete', async function () { + Popup.back(2); + const subtask = this.subtask; + if (subtask && subtask._id) { + await subtask.archive(); + } + }), +}); Template.editSubtaskItemForm.helpers({ user() { @@ -158,5 +143,3 @@ Template.editSubtaskItemForm.helpers({ return ReactiveCache.getCurrentUser().isBoardAdmin(); }, }); - -