From f1625ad1f50e2a3e4d1e762bae4f54a94ed94a31 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Sun, 8 Mar 2026 10:57:20 +0200 Subject: [PATCH] Migrate client library code from BlazeComponent to Template pattern Convert popup, inlinedform, multiSelection, spinner, cardSearch, datepicker, and dialog helper libraries to use native Meteor Template.onCreated/helpers/events instead of BlazeComponent. Update reactiveCache to remove BlazeComponent dependency. --- client/lib/cardSearch.js | 50 ++-- client/lib/datepicker.js | 251 ++++++++++-------- client/lib/dialogWithBoardSwimlaneList.js | 203 ++++++-------- client/lib/dialogWithBoardSwimlaneListCard.js | 108 ++------ client/lib/inlinedform.js | 142 ++++------ client/lib/multiSelection.js | 10 +- client/lib/popup.js | 9 +- client/lib/spinner.js | 28 +- 8 files changed, 352 insertions(+), 449 deletions(-) diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index a143965cf..c2e9df3a0 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -5,8 +5,11 @@ import SessionData from '../../models/usersessiondata'; import {QueryDebug} from "../../config/query-classes"; import {OPERATOR_DEBUG} from "../../config/search-const"; -export class CardSearchPagedComponent extends BlazeComponent { - onCreated() { +// Plain helper class for search pages with pagination. +// Not a BlazeComponent; instantiated in each template's onCreated. +export class CardSearchPaged { + constructor(templateInstance) { + this.tpl = templateInstance; this.searching = new ReactiveVar(false); this.hasResults = new ReactiveVar(false); this.hasQueryErrors = new ReactiveVar(false); @@ -33,24 +36,24 @@ export class CardSearchPagedComponent extends BlazeComponent { console.log('Subscription ready, getting results...'); console.log('Subscription ready - sessionId:', that.sessionId); } - + // Wait for session data to be available (with timeout) let waitCount = 0; const maxWaitCount = 50; // 10 seconds max wait - + const waitForSessionData = () => { waitCount++; const sessionData = that.getSessionData(); if (process.env.DEBUG === 'true') { console.log('waitForSessionData - attempt', waitCount, 'session data:', sessionData); } - + if (sessionData) { const results = that.getResults(); if (process.env.DEBUG === 'true') { console.log('Search results count:', results ? results.length : 0); } - + // If no results and this is a due cards search, try to retry if ((!results || results.length === 0) && that.searchRetryCount !== undefined && that.searchRetryCount < that.maxRetries) { if (process.env.DEBUG === 'true') { @@ -64,7 +67,7 @@ export class CardSearchPagedComponent extends BlazeComponent { }, 500); return; } - + that.searching.set(false); that.hasResults.set(true); that.serverError.set(false); @@ -83,7 +86,7 @@ export class CardSearchPagedComponent extends BlazeComponent { if (process.env.DEBUG === 'true') { console.log('Fallback search results count:', results ? results.length : 0); } - + if (results && results.length > 0) { that.searching.set(false); that.hasResults.set(true); @@ -95,7 +98,7 @@ export class CardSearchPagedComponent extends BlazeComponent { } } }; - + // Start waiting for session data Meteor.setTimeout(waitForSessionData, 100); }, @@ -131,7 +134,7 @@ export class CardSearchPagedComponent extends BlazeComponent { if (process.env.DEBUG === 'true') { console.log('getSessionData - looking for sessionId:', sessionIdToUse); } - + // Try using the raw SessionData collection instead of ReactiveCache const sessionData = SessionData.findOne({ sessionId: sessionIdToUse, @@ -139,7 +142,7 @@ export class CardSearchPagedComponent extends BlazeComponent { if (process.env.DEBUG === 'true') { console.log('getSessionData - found session data (raw):', sessionData); } - + // Also try ReactiveCache for comparison const reactiveSessionData = ReactiveCache.getSessionData({ sessionId: sessionIdToUse, @@ -147,7 +150,7 @@ export class CardSearchPagedComponent extends BlazeComponent { if (process.env.DEBUG === 'true') { console.log('getSessionData - found session data (reactive):', reactiveSessionData); } - + return sessionData || reactiveSessionData; } @@ -161,7 +164,7 @@ export class CardSearchPagedComponent extends BlazeComponent { console.log('getResults - session data:', this.sessionData); } const cards = []; - + if (this.sessionData && this.sessionData.cards) { if (process.env.DEBUG === 'true') { console.log('getResults - cards array length:', this.sessionData.cards.length); @@ -192,7 +195,7 @@ export class CardSearchPagedComponent extends BlazeComponent { if (process.env.DEBUG === 'true') { console.log('getResults - direct card search found:', allCards ? allCards.length : 0, 'cards'); } - + if (allCards && allCards.length > 0) { allCards.forEach(card => { if (card && card._id) { @@ -200,7 +203,7 @@ export class CardSearchPagedComponent extends BlazeComponent { } }); } - + this.queryErrors = []; } if (this.queryErrors.length) { @@ -267,7 +270,7 @@ export class CardSearchPagedComponent extends BlazeComponent { queryParams.text, this.subscriptionCallbacks, ); - + const sessionDataHandle = Meteor.subscribe('sessionData', this.sessionId); if (process.env.DEBUG === 'true') { console.log('Subscribed to sessionData with sessionId:', this.sessionId); @@ -337,19 +340,4 @@ export class CardSearchPagedComponent extends BlazeComponent { const baseUrl = window.location.href.replace(/([?#].*$|\s*$)/, ''); return `${baseUrl}?q=${encodeURIComponent(this.query.get())}`; } - - events() { - return [ - { - 'click .js-next-page'(evt) { - evt.preventDefault(); - this.nextPage(); - }, - 'click .js-previous-page'(evt) { - evt.preventDefault(); - this.previousPage(); - }, - }, - ]; - } } diff --git a/client/lib/datepicker.js b/client/lib/datepicker.js index fa2ff8129..7b4d031f9 100644 --- a/client/lib/datepicker.js +++ b/client/lib/datepicker.js @@ -1,5 +1,5 @@ import { ReactiveCache } from '/imports/reactiveCache'; -import { TAPi18n } from '/imports/i18n'; +import { getCurrentCardFromContext } from '/client/lib/currentCard'; // Helper to check if a date is valid function isValidDate(date) { @@ -23,119 +23,148 @@ function formatTime(date) { return `${hours}:${minutes}`; } -export class DatePicker extends BlazeComponent { - template() { - return 'datepicker'; - } +/** + * Sets up datepicker state on a template instance. + * Call from onCreated. Stores state on tpl.datePicker. + * + * @param {TemplateInstance} tpl - The Blaze template instance + * @param {Object} options + * @param {string} [options.defaultTime='1970-01-01 08:00:00'] - Default time string + * @param {Date} [options.initialDate] - Initial date to set (if valid) + */ +export function setupDatePicker(tpl, { defaultTime = '1970-01-01 08:00:00', initialDate } = {}) { + const card = getCurrentCardFromContext() || Template.currentData(); + tpl.datePicker = { + error: new ReactiveVar(''), + card, + date: new ReactiveVar(initialDate && isValidDate(new Date(initialDate)) ? new Date(initialDate) : new Date('invalid')), + defaultTime, + }; +} - onCreated(defaultTime = '1970-01-01 08:00:00') { - this.error = new ReactiveVar(''); - this.card = this.data(); - this.date = new ReactiveVar(new Date('invalid')); - this.defaultTime = defaultTime; - } +/** + * onRendered logic for datepicker templates. + * Sets initial input values from the datePicker state. + * + * @param {TemplateInstance} tpl - The Blaze template instance + */ +export function datePickerRendered(tpl) { + const dp = tpl.datePicker; + if (isValidDate(dp.date.get())) { + const dateInput = tpl.find('#date'); + const timeInput = tpl.find('#time'); - startDayOfWeek() { - const currentUser = ReactiveCache.getCurrentUser(); - if (currentUser) { - return currentUser.getStartDayOfWeek(); - } else { - return 1; + if (dateInput) { + dateInput.value = formatDate(dp.date.get()); } - } - - onRendered() { - // Set initial values for native HTML inputs - if (isValidDate(this.date.get())) { - const dateInput = this.find('#date'); - const timeInput = this.find('#time'); - - if (dateInput) { - dateInput.value = formatDate(this.date.get()); - } - if (timeInput && !timeInput.value && this.defaultTime) { - const defaultDate = new Date(this.defaultTime); - timeInput.value = formatTime(defaultDate); - } else if (timeInput && isValidDate(this.date.get())) { - timeInput.value = formatTime(this.date.get()); - } + if (timeInput && !timeInput.value && dp.defaultTime) { + const defaultDate = new Date(dp.defaultTime); + timeInput.value = formatTime(defaultDate); + } else if (timeInput && isValidDate(dp.date.get())) { + timeInput.value = formatTime(dp.date.get()); } } - - showDate() { - if (isValidDate(this.date.get())) return formatDate(this.date.get()); - return ''; - } - showTime() { - if (isValidDate(this.date.get())) return formatTime(this.date.get()); - return ''; - } - dateFormat() { - return 'YYYY-MM-DD'; - } - timeFormat() { - return 'HH:mm'; - } - - events() { - return [ - { - 'change .js-date-field'() { - // Native HTML date input validation - const dateValue = this.find('#date').value; - if (dateValue) { - // HTML date input format is always YYYY-MM-DD - const dateObj = new Date(dateValue + 'T12:00:00'); - if (isValidDate(dateObj)) { - this.error.set(''); - } else { - this.error.set('invalid-date'); - } - } - }, - 'change .js-time-field'() { - // Native HTML time input validation - const timeValue = this.find('#time').value; - if (timeValue) { - // HTML time input format is always HH:mm - const timeObj = new Date(`1970-01-01T${timeValue}:00`); - if (isValidDate(timeObj)) { - this.error.set(''); - } else { - this.error.set('invalid-time'); - } - } - }, - 'submit .edit-date'(evt) { - evt.preventDefault(); - - const dateValue = evt.target.date.value; - const timeValue = evt.target.time.value || '12:00'; // Default to 12:00 if no time given - - if (!dateValue) { - this.error.set('invalid-date'); - evt.target.date.focus(); - return; - } - - // Combine date and time: HTML date input is YYYY-MM-DD, time input is HH:mm - const dateTimeString = `${dateValue}T${timeValue}:00`; - const newCompleteDate = new Date(dateTimeString); - - if (!isValidDate(newCompleteDate)) { - this.error.set('invalid'); - return; - } - - this._storeDate(newCompleteDate); - Popup.back(); - }, - 'click .js-delete-date'(evt) { - evt.preventDefault(); - this._deleteDate(); - Popup.back(); - }, - }, - ]; - } +} + +/** + * Returns helpers object for datepicker templates. + * All helpers read from Template.instance().datePicker. + */ +export function datePickerHelpers() { + return { + error() { + return Template.instance().datePicker.error; + }, + showDate() { + const dp = Template.instance().datePicker; + if (isValidDate(dp.date.get())) return formatDate(dp.date.get()); + return ''; + }, + showTime() { + const dp = Template.instance().datePicker; + if (isValidDate(dp.date.get())) return formatTime(dp.date.get()); + return ''; + }, + dateFormat() { + return 'YYYY-MM-DD'; + }, + timeFormat() { + return 'HH:mm'; + }, + startDayOfWeek() { + const currentUser = ReactiveCache.getCurrentUser(); + if (currentUser) { + return currentUser.getStartDayOfWeek(); + } else { + return 1; + } + }, + }; +} + +/** + * Returns events object for datepicker templates. + * + * @param {Object} callbacks + * @param {Function} callbacks.storeDate - Called with (date) when form is submitted + * @param {Function} callbacks.deleteDate - Called when delete button is clicked + */ +export function datePickerEvents({ storeDate, deleteDate }) { + return { + 'change .js-date-field'(evt, tpl) { + // Native HTML date input validation + const dateValue = tpl.find('#date').value; + if (dateValue) { + // HTML date input format is always YYYY-MM-DD + const dateObj = new Date(dateValue + 'T12:00:00'); + if (isValidDate(dateObj)) { + tpl.datePicker.error.set(''); + } else { + tpl.datePicker.error.set('invalid-date'); + } + } + }, + 'change .js-time-field'(evt, tpl) { + // Native HTML time input validation + const timeValue = tpl.find('#time').value; + if (timeValue) { + // HTML time input format is always HH:mm + const timeObj = new Date(`1970-01-01T${timeValue}:00`); + if (isValidDate(timeObj)) { + tpl.datePicker.error.set(''); + } else { + tpl.datePicker.error.set('invalid-time'); + } + } + }, + 'submit .edit-date'(evt, tpl) { + evt.preventDefault(); + + const dateValue = evt.target.date.value; + const timeValue = evt.target.time.value || '12:00'; // Default to 12:00 if no time given + + if (!dateValue) { + tpl.datePicker.error.set('invalid-date'); + evt.target.date.focus(); + return; + } + + // Combine date and time: HTML date input is YYYY-MM-DD, time input is HH:mm + const dateTimeString = `${dateValue}T${timeValue}:00`; + const newCompleteDate = new Date(dateTimeString); + + if (!isValidDate(newCompleteDate)) { + tpl.datePicker.error.set('invalid'); + return; + } + + storeDate.call(tpl, newCompleteDate); + Popup.back(); + }, + 'click .js-delete-date'(evt, tpl) { + evt.preventDefault(); + deleteDate.call(tpl); + Popup.back(); + }, + }; } diff --git a/client/lib/dialogWithBoardSwimlaneList.js b/client/lib/dialogWithBoardSwimlaneList.js index f1a780069..4ed7dc7de 100644 --- a/client/lib/dialogWithBoardSwimlaneList.js +++ b/client/lib/dialogWithBoardSwimlaneList.js @@ -1,33 +1,26 @@ import { ReactiveCache } from '/imports/reactiveCache'; import { TAPi18n } from '/imports/i18n'; -export class DialogWithBoardSwimlaneList extends BlazeComponent { - /** returns the card dialog options - * @return Object with properties { boardId, swimlaneId, listId } +/** + * Helper class for popup dialogs that let users select a board, swimlane, and list. + * Not a BlazeComponent — instantiated by each Template's onCreated callback. + */ +export class BoardSwimlaneListDialog { + /** + * @param {Blaze.TemplateInstance} tpl - the template instance + * @param {Object} callbacks + * @param {Function} callbacks.getDialogOptions - returns saved options from card/user + * @param {Function} callbacks.setDone - performs the action (boardId, swimlaneId, listId, options) + * @param {Function} [callbacks.getDefaultOption] - override default option shape */ - getDialogOptions() { - } - - /** list is done - * @param listId the selected list id - * @param options the selected options (Object with properties { boardId, swimlaneId, listId }) - */ - setDone(listId, options) { - } - - /** get the default options - * @return the options - */ - getDefaultOption(boardId) { - const ret = { - 'boardId' : "", - 'swimlaneId' : "", - 'listId' : "", + constructor(tpl, callbacks = {}) { + this.tpl = tpl; + this._getDialogOptions = callbacks.getDialogOptions || (() => undefined); + this._setDone = callbacks.setDone || (() => {}); + if (callbacks.getDefaultOption) { + this.getDefaultOption = callbacks.getDefaultOption; } - return ret; - } - onCreated() { this.currentBoardId = Utils.getCurrentBoardId(); this.selectedBoardId = new ReactiveVar(this.currentBoardId); this.selectedSwimlaneId = new ReactiveVar(''); @@ -35,33 +28,67 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { this.setOption(this.currentBoardId); } + /** get the default options + * @return the options + */ + getDefaultOption() { + return { + boardId: '', + swimlaneId: '', + listId: '', + }; + } + + /** returns the card dialog options (delegates to callback) */ + getDialogOptions() { + return this._getDialogOptions(); + } + + /** performs the done action (delegates to callback) */ + async setDone(...args) { + return this._setDone(...args); + } + /** set the last confirmed dialog field values * @param boardId the current board id */ setOption(boardId) { this.cardOption = this.getDefaultOption(); - let currentOptions = this.getDialogOptions(); + const currentOptions = this.getDialogOptions(); if (currentOptions && boardId && currentOptions[boardId]) { this.cardOption = currentOptions[boardId]; - if (this.cardOption.boardId && - this.cardOption.swimlaneId && - this.cardOption.listId - ) - { - this.selectedBoardId.set(this.cardOption.boardId) + if ( + this.cardOption.boardId && + this.cardOption.swimlaneId && + this.cardOption.listId + ) { + this.selectedBoardId.set(this.cardOption.boardId); this.selectedSwimlaneId.set(this.cardOption.swimlaneId); this.selectedListId.set(this.cardOption.listId); } } this.getBoardData(this.selectedBoardId.get()); - if (!this.selectedSwimlaneId.get() || !ReactiveCache.getSwimlane({_id: this.selectedSwimlaneId.get(), boardId: this.selectedBoardId.get()})) { + if ( + !this.selectedSwimlaneId.get() || + !ReactiveCache.getSwimlane({ + _id: this.selectedSwimlaneId.get(), + boardId: this.selectedBoardId.get(), + }) + ) { this.setFirstSwimlaneId(); } - if (!this.selectedListId.get() || !ReactiveCache.getList({_id: this.selectedListId.get(), boardId: this.selectedBoardId.get()})) { + if ( + !this.selectedListId.get() || + !ReactiveCache.getList({ + _id: this.selectedListId.get(), + boardId: this.selectedBoardId.get(), + }) + ) { this.setFirstListId(); } } + /** sets the first swimlane id */ setFirstSwimlaneId() { try { @@ -70,6 +97,7 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { this.selectedSwimlaneId.set(swimlaneId); } catch (e) {} } + /** sets the first list id */ setFirstListId() { try { @@ -93,7 +121,8 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { }; if (swimlaneId) { - const defaultSwimlane = board.getDefaultSwimline && board.getDefaultSwimline(); + const defaultSwimlane = + board.getDefaultSwimline && board.getDefaultSwimline(); if (defaultSwimlane && defaultSwimlane._id === swimlaneId) { selector.swimlaneId = { $in: [swimlaneId, null, ''] }; } else { @@ -104,36 +133,24 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { return ReactiveCache.getLists(selector, { sort: { sort: 1 } }); } - /** returns if the board id was the last confirmed one - * @param boardId check this board id - * @return if the board id was the last confirmed one - */ + /** returns if the board id was the last confirmed one */ isDialogOptionBoardId(boardId) { - let ret = this.cardOption.boardId == boardId; - return ret; + return this.cardOption.boardId == boardId; } - /** returns if the swimlane id was the last confirmed one - * @param swimlaneId check this swimlane id - * @return if the swimlane id was the last confirmed one - */ + /** returns if the swimlane id was the last confirmed one */ isDialogOptionSwimlaneId(swimlaneId) { - let ret = this.cardOption.swimlaneId == swimlaneId; - return ret; + return this.cardOption.swimlaneId == swimlaneId; } - /** returns if the list id was the last confirmed one - * @param listId check this list id - * @return if the list id was the last confirmed one - */ + /** returns if the list id was the last confirmed one */ isDialogOptionListId(listId) { - let ret = this.cardOption.listId == listId; - return ret; + return this.cardOption.listId == listId; } - /** returns all available board */ + /** returns all available boards */ boards() { - const ret = ReactiveCache.getBoards( + return ReactiveCache.getBoards( { archived: false, 'members.userId': Meteor.userId(), @@ -143,14 +160,12 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { sort: { sort: 1 }, }, ); - return ret; } /** returns all available swimlanes of the current board */ swimlanes() { const board = ReactiveCache.getBoard(this.selectedBoardId.get()); - const ret = board.swimlanes(); - return ret; + return board.swimlanes(); } /** returns all available lists of the current board */ @@ -161,36 +176,30 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { ); } - /** Fix swimlane title translation issue for "Default" swimlane - * @param title the swimlane title - * @return the properly translated title - */ + /** Fix swimlane title translation issue for "Default" swimlane */ isTitleDefault(title) { - // https://github.com/wekan/wekan/issues/4763 - // https://github.com/wekan/wekan/issues/4742 - // Translation text for "default" does not work, it returns an object. - // When that happens, try use translation "defaultdefault" that has same content of default, or return text "Default". - // This can happen, if swimlane does not have name. - // Yes, this is fixing the symptom (Swimlane title does not have title) - // instead of fixing the problem (Add Swimlane title when creating swimlane) - // because there could be thousands of swimlanes, adding name Default to all of them - // would be very slow. - if (title.startsWith("key 'default") && title.endsWith('returned an object instead of string.')) { - if (`${TAPi18n.__('defaultdefault')}`.startsWith("key 'default") && `${TAPi18n.__('defaultdefault')}`.endsWith('returned an object instead of string.')) { + if ( + title.startsWith("key 'default") && + title.endsWith('returned an object instead of string.') + ) { + if ( + `${TAPi18n.__('defaultdefault')}`.startsWith("key 'default") && + `${TAPi18n.__('defaultdefault')}`.endsWith( + 'returned an object instead of string.', + ) + ) { return 'Default'; - } else { + } else { return `${TAPi18n.__('defaultdefault')}`; } } else if (title === 'Default') { return `${TAPi18n.__('defaultdefault')}`; - } else { + } else { return title; } } - /** get the board data from the server - * @param boardId get the board data of this board id - */ + /** get the board data from the server */ getBoardData(boardId) { const self = this; Meteor.subscribe('board', boardId, false, { @@ -199,51 +208,11 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent { self.selectedBoardId.set(boardId); if (!sameBoardId) { - // reset swimlane id (for selection in cards()) self.setFirstSwimlaneId(); - - // reset list id (for selection in cards()) self.setFirstListId(); } }, }); } - events() { - return [ - { - async 'click .js-done'() { - const boardSelect = this.$('.js-select-boards')[0]; - const boardId = boardSelect.options[boardSelect.selectedIndex].value; - - const listSelect = this.$('.js-select-lists')[0]; - const listId = listSelect.options[listSelect.selectedIndex].value; - - const swimlaneSelect = this.$('.js-select-swimlanes')[0]; - const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value; - - const options = { - 'boardId' : boardId, - 'swimlaneId' : swimlaneId, - 'listId' : listId, - } - try { - await this.setDone(boardId, swimlaneId, listId, options); - } catch (e) { - console.error('Error in list dialog operation:', e); - } - Popup.back(2); - }, - 'change .js-select-boards'(event) { - const boardId = $(event.currentTarget).val(); - this.getBoardData(boardId); - }, - 'change .js-select-swimlanes'(event) { - this.selectedSwimlaneId.set($(event.currentTarget).val()); - this.setFirstListId(); - }, - }, - ]; - } } - diff --git a/client/lib/dialogWithBoardSwimlaneListCard.js b/client/lib/dialogWithBoardSwimlaneListCard.js index bf86cfea1..1239ce147 100644 --- a/client/lib/dialogWithBoardSwimlaneListCard.js +++ b/client/lib/dialogWithBoardSwimlaneListCard.js @@ -1,34 +1,28 @@ import { ReactiveCache } from '/imports/reactiveCache'; -import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlaneList'; +import { BoardSwimlaneListDialog } from '/client/lib/dialogWithBoardSwimlaneList'; -export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList { - constructor() { - super(); +/** + * Extension of BoardSwimlaneListDialog that adds card selection. + * Used by popup templates that need board + swimlane + list + card selectors. + */ +export class BoardSwimlaneListCardDialog extends BoardSwimlaneListDialog { + constructor(tpl, callbacks = {}) { + super(tpl, callbacks); this.selectedCardId = new ReactiveVar(''); } - getDefaultOption(boardId) { - const ret = { - 'boardId' : "", - 'swimlaneId' : "", - 'listId' : "", - 'cardId': "", - } - return ret; + getDefaultOption() { + return { + boardId: '', + swimlaneId: '', + listId: '', + cardId: '', + }; } - onCreated() { - super.onCreated(); - this.selectedCardId = new ReactiveVar(''); - } - - /** set the last confirmed dialog field values - * @param boardId the current board id - */ + /** Override to also set cardId if available */ setOption(boardId) { super.setOption(boardId); - - // Also set cardId if available if (this.cardOption && this.cardOption.cardId) { this.selectedCardId.set(this.cardOption.cardId); } @@ -36,7 +30,10 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList /** returns all available cards of the current list */ cards() { - const list = ReactiveCache.getList({_id: this.selectedListId.get(), boardId: this.selectedBoardId.get()}); + const list = ReactiveCache.getList({ + _id: this.selectedListId.get(), + boardId: this.selectedBoardId.get(), + }); const swimlaneId = this.selectedSwimlaneId.get(); if (list && swimlaneId) { return list.cards(swimlaneId).sort((a, b) => a.sort - b.sort); @@ -45,18 +42,12 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList } } - /** returns if the card id was the last confirmed one - * @param cardId check this card id - * @return if the card id was the last confirmed one - */ + /** returns if the card id was the last confirmed one */ isDialogOptionCardId(cardId) { - let ret = this.cardOption.cardId == cardId; - return ret; + return this.cardOption.cardId == cardId; } - /** get the board data from the server - * @param boardId get the board data of this board id - */ + /** Override to also reset card id on board change */ getBoardData(boardId) { const self = this; Meteor.subscribe('board', boardId, false, { @@ -65,65 +56,12 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList self.selectedBoardId.set(boardId); if (!sameBoardId) { - // reset swimlane id self.setFirstSwimlaneId(); - - // reset list id self.setFirstListId(); - - // reset card id self.selectedCardId.set(''); } }, }); } - events() { - return [ - { - async 'click .js-done'() { - const boardSelect = this.$('.js-select-boards')[0]; - const boardId = boardSelect.options[boardSelect.selectedIndex].value; - - const listSelect = this.$('.js-select-lists')[0]; - const listId = listSelect.options[listSelect.selectedIndex].value; - - const swimlaneSelect = this.$('.js-select-swimlanes')[0]; - const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value; - - const cardSelect = this.$('.js-select-cards')[0]; - const cardId = cardSelect.options.length > 0 ? cardSelect.options[cardSelect.selectedIndex].value : null; - - const options = { - 'boardId' : boardId, - 'swimlaneId' : swimlaneId, - 'listId' : listId, - 'cardId': cardId, - } - try { - await this.setDone(cardId, options); - } catch (e) { - console.error('Error in card dialog operation:', e); - } - Popup.back(2); - }, - 'change .js-select-boards'(event) { - const boardId = $(event.currentTarget).val(); - this.getBoardData(boardId); - }, - 'change .js-select-swimlanes'(event) { - this.selectedSwimlaneId.set($(event.currentTarget).val()); - this.setFirstListId(); - }, - 'change .js-select-lists'(event) { - this.selectedListId.set($(event.currentTarget).val()); - // Reset card selection when list changes - this.selectedCardId.set(''); - }, - 'change .js-select-cards'(event) { - this.selectedCardId.set($(event.currentTarget).val()); - }, - }, - ]; - } } diff --git a/client/lib/inlinedform.js b/client/lib/inlinedform.js index 62da01993..d4da6c647 100644 --- a/client/lib/inlinedform.js +++ b/client/lib/inlinedform.js @@ -15,95 +15,77 @@ // We can only have one inlined form element opened at a time const currentlyOpenedForm = new ReactiveVar(null); -InlinedForm = BlazeComponent.extendComponent({ - template() { - return 'inlinedForm'; - }, +Template.inlinedForm.onCreated(function () { + this.isOpen = new ReactiveVar(false); +}); - onCreated() { - this.isOpen = new ReactiveVar(false); - }, - - onRendered() { - // Autofocus when form becomes open - this.autorun(() => { - if (this.isOpen.get()) { - Tracker.afterFlush(() => { - const input = this.find('textarea,input[type=text]'); - if (input && typeof input.focus === 'function') { - setTimeout(() => { - input.focus(); - // Select content if it exists (useful for editing) - if (input.value && input.select) { - input.select(); - } - }, 50); - } - }); - } - }); - }, - - onDestroyed() { - currentlyOpenedForm.set(null); - }, - - open(evt) { - if (evt) { - evt.preventDefault(); - // Close currently opened form, if any - EscapeActions.clickExecute(evt.target, 'inlinedForm'); - } else { - // Close currently opened form, if any - EscapeActions.executeUpTo('inlinedForm'); +Template.inlinedForm.onRendered(function () { + const tpl = this; + tpl.autorun(() => { + if (tpl.isOpen.get()) { + Tracker.afterFlush(() => { + const input = tpl.find('textarea,input[type=text]'); + if (input && typeof input.focus === 'function') { + setTimeout(() => { + input.focus(); + if (input.value && input.select) { + input.select(); + } + }, 50); + } + }); } + }); +}); - this.isOpen.set(true); - currentlyOpenedForm.set(this); +Template.inlinedForm.onDestroyed(function () { + currentlyOpenedForm.set(null); +}); + +Template.inlinedForm.helpers({ + isOpen() { + return Template.instance().isOpen; }, +}); - close() { - this.isOpen.set(false); +Template.inlinedForm.events({ + 'click .js-close-inlined-form'(evt, tpl) { + tpl.isOpen.set(false); currentlyOpenedForm.set(null); }, - - getValue() { - const input = this.find('textarea,input[type=text]'); - // \s without \n + unicode (https://developer.mozilla.org/de/docs/Web/JavaScript/Guide/Regular_Expressions#special-white-space) - return this.isOpen.get() && input && input.value.replaceAll(/[ \f\r\t\v]+$/gm, ''); + 'click .js-open-inlined-form'(evt, tpl) { + evt.preventDefault(); + EscapeActions.clickExecute(evt.target, 'inlinedForm'); + tpl.isOpen.set(true); + currentlyOpenedForm.set(tpl); }, - - events() { - return [ - { - 'click .js-close-inlined-form': this.close, - 'click .js-open-inlined-form': this.open, - - // Pressing Ctrl+Enter should submit the form - 'keydown form textarea'(evt) { - if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { - this.find('button[type=submit]').click(); - } - }, - - // Close the inlined form when after its submission - submit() { - if (this.currentData().autoclose !== false) { - Tracker.afterFlush(() => { - this.close(); - }); - } - }, - }, - ]; + 'keydown form textarea'(evt, tpl) { + if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { + tpl.find('button[type=submit]').click(); + } }, -}).register('inlinedForm'); + submit(evt, tpl) { + const data = Template.currentData(); + if (data.autoclose !== false) { + Tracker.afterFlush(() => { + tpl.isOpen.set(false); + currentlyOpenedForm.set(null); + }); + } + }, +}); // Press escape to close the currently opened inlinedForm EscapeActions.register( 'inlinedForm', () => { - currentlyOpenedForm.get().close(); + const form = currentlyOpenedForm.get(); + if (form) { + if (form.isOpen) { + form.isOpen.set(false); + currentlyOpenedForm.set(null); + } + } }, () => { return currentlyOpenedForm.get() !== null; @@ -112,13 +94,3 @@ EscapeActions.register( enabledOnClick: false, }, ); - -// submit on click outside -//document.addEventListener('click', function(evt) { -// const openedForm = currentlyOpenedForm.get(); -// const isClickOutside = $(evt.target).closest('.js-inlined-form').length === 0; -// if (openedForm && isClickOutside) { -// $('.js-inlined-form button[type=submit]').click(); -// openedForm.close(); -// } -//}, true); diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js index 853c7114d..3597d2a98 100644 --- a/client/lib/multiSelection.js +++ b/client/lib/multiSelection.js @@ -91,14 +91,16 @@ MultiSelection = { activate() { if (!this.isActive()) { - this._sidebarWasOpen = Sidebar.isOpen(); + this._sidebarWasOpen = Sidebar && Sidebar.isOpen(); EscapeActions.executeUpTo('detailsPane'); this._isActive.set(true); Tracker.flush(); } - Sidebar.setView(this.sidebarView); - if(Utils.isMiniScreen()) { - Sidebar.hide(); + if (Sidebar) { + Sidebar.setView(this.sidebarView); + if(Utils.isMiniScreen()) { + Sidebar.hide(); + } } }, diff --git a/client/lib/popup.js b/client/lib/popup.js index 9b9acaadc..dad029883 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -187,7 +187,14 @@ window.Popup = new (class { getOpenerComponent(n=4) { const { openerElement } = Template.parentData(n); - return BlazeComponent.getComponentForElement(openerElement); + if (!openerElement) return null; + const view = Blaze.getView(openerElement); + let current = view; + while (current) { + if (current.templateInstance) return current.templateInstance(); + current = current.parentView; + } + return null; } // An utility function that returns the top element of the internal stack diff --git a/client/lib/spinner.js b/client/lib/spinner.js index f13d8478d..ac6ea0c30 100644 --- a/client/lib/spinner.js +++ b/client/lib/spinner.js @@ -4,22 +4,20 @@ Meteor.subscribe('setting'); import { ALLOWED_WAIT_SPINNERS } from '/config/const'; -export class Spinner extends BlazeComponent { - getSpinnerName() { - let ret = 'Bounce'; - let defaultWaitSpinner = Meteor.settings.public.WAIT_SPINNER; - if (defaultWaitSpinner && ALLOWED_WAIT_SPINNERS.includes(defaultWaitSpinner)) { - ret = defaultWaitSpinner; - } - let settings = ReactiveCache.getCurrentSetting(); - - if (settings && settings.spinnerName) { - ret = settings.spinnerName; - } - return ret; +export function getSpinnerName() { + let ret = 'Bounce'; + let defaultWaitSpinner = Meteor.settings.public.WAIT_SPINNER; + if (defaultWaitSpinner && ALLOWED_WAIT_SPINNERS.includes(defaultWaitSpinner)) { + ret = defaultWaitSpinner; } + let settings = ReactiveCache.getCurrentSetting(); - getSpinnerTemplate() { - return 'spinner' + this.getSpinnerName().replace(/-/, ''); + if (settings && settings.spinnerName) { + ret = settings.spinnerName; } + return ret; +} + +export function getSpinnerTemplate() { + return 'spinner' + getSpinnerName().replace(/-/, ''); }