mirror of
https://github.com/wekan/wekan.git
synced 2026-03-14 01:16:13 +01:00
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:
parent
012947c076
commit
f1625ad1f5
8 changed files with 352 additions and 449 deletions
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(/-/, '');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue