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.
This commit is contained in:
Harry Adel 2026-03-08 10:57:20 +02:00
parent 012947c076
commit f1625ad1f5
8 changed files with 352 additions and 449 deletions

View file

@ -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();
},
},
];
}
}

View file

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

View file

@ -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();
},
},
];
}
}

View file

@ -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());
},
},
];
}
}

View file

@ -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);

View file

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

View file

@ -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

View file

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