mirror of
https://github.com/wekan/wekan.git
synced 2026-03-03 04:10:16 +01:00
Resolve merge conflicts by accepting PR #6131 changes
Co-authored-by: xet7 <15545+xet7@users.noreply.github.com>
This commit is contained in:
parent
dc0b68ee80
commit
97dd5d2064
257 changed files with 9483 additions and 14103 deletions
|
|
@ -113,7 +113,7 @@ class BoardConverter {
|
|||
}
|
||||
|
||||
conversionStatus.set(`Converting ${listsToConvert.length} lists...`);
|
||||
|
||||
|
||||
const startTime = Date.now();
|
||||
const totalLists = listsToConvert.length;
|
||||
let convertedLists = 0;
|
||||
|
|
@ -122,20 +122,20 @@ class BoardConverter {
|
|||
const batchSize = 10;
|
||||
for (let i = 0; i < listsToConvert.length; i += batchSize) {
|
||||
const batch = listsToConvert.slice(i, i + batchSize);
|
||||
|
||||
|
||||
// Process batch
|
||||
await this.processBatch(batch, defaultSwimlane._id);
|
||||
|
||||
|
||||
convertedLists += batch.length;
|
||||
const progress = Math.round((convertedLists / totalLists) * 100);
|
||||
conversionProgress.set(progress);
|
||||
|
||||
|
||||
// Calculate estimated time remaining
|
||||
const elapsed = Date.now() - startTime;
|
||||
const rate = convertedLists / elapsed; // lists per millisecond
|
||||
const remaining = totalLists - convertedLists;
|
||||
const estimatedMs = remaining / rate;
|
||||
|
||||
|
||||
conversionStatus.set(`Converting list ${convertedLists} of ${totalLists}...`);
|
||||
conversionEstimatedTime.set(this.formatTime(estimatedMs));
|
||||
|
||||
|
|
@ -146,11 +146,11 @@ class BoardConverter {
|
|||
// Mark as converted
|
||||
this.conversionCache.set(boardId, true);
|
||||
globalConvertedBoards.add(boardId); // Mark board as converted
|
||||
|
||||
|
||||
conversionStatus.set('Board conversion completed!');
|
||||
conversionProgress.set(100);
|
||||
console.log(`Board ${boardId} conversion completed and marked as converted`);
|
||||
|
||||
|
||||
// Clear status after a delay
|
||||
setTimeout(() => {
|
||||
isConverting.set(false);
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent {
|
|||
*/
|
||||
getDefaultOption(boardId) {
|
||||
const ret = {
|
||||
'boardId' : "",
|
||||
'swimlaneId' : "",
|
||||
'listId' : "",
|
||||
'boardId' : this.data().boardId,
|
||||
'swimlaneId' : this.data().swimlaneId,
|
||||
'listId' : this.data().listId,
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -44,21 +44,20 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent {
|
|||
let 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)
|
||||
this.selectedSwimlaneId.set(this.cardOption.swimlaneId);
|
||||
this.selectedListId.set(this.cardOption.listId);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +73,7 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent {
|
|||
setFirstListId() {
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
const listId = board.lists()[0]._id;
|
||||
const listId = board.listsInSwimlane(this.selectedSwimlaneId.get())[0]._id;
|
||||
this.selectedListId.set(listId);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
|
@ -131,7 +130,7 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent {
|
|||
/** returns all available lists of the current board */
|
||||
lists() {
|
||||
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
|
||||
const ret = board.lists();
|
||||
const ret = board.listsInSwimlane(this.selectedSwimlaneId.get());
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -219,4 +218,3 @@ export class DialogWithBoardSwimlaneList extends BlazeComponent {
|
|||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList
|
|||
*/
|
||||
setOption(boardId) {
|
||||
super.setOption(boardId);
|
||||
|
||||
|
||||
// Also set cardId if available
|
||||
if (this.cardOption && this.cardOption.cardId) {
|
||||
this.selectedCardId.set(this.cardOption.cardId);
|
||||
|
|
@ -69,7 +69,7 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList
|
|||
|
||||
// reset list id
|
||||
self.setFirstListId();
|
||||
|
||||
|
||||
// reset card id
|
||||
self.selectedCardId.set('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,10 +128,24 @@ hotkeys('escape', () => {
|
|||
Sidebar.hide();
|
||||
});
|
||||
|
||||
let currentMouseDown;
|
||||
|
||||
// Avoid the common issue of dragging an element a bit fast and releasing
|
||||
// out of the element; in that case e.g. popup closes, which is not pleasant.
|
||||
// Only execute actions if mousedown and mouseup are on the same element (the
|
||||
// initial issue is that a long drag is still a click event)
|
||||
$(document).on('pointerdown', evt => {
|
||||
currentMouseDown = evt.target;
|
||||
});
|
||||
// On a left click on the document, we try to exectute one escape action (eg,
|
||||
// close the popup). We don't execute any action if the user has clicked on a
|
||||
// link or a button.
|
||||
$(document).on('click', evt => {
|
||||
$(document).on('pointerup', evt => {
|
||||
const currentMouseUp = evt.target;
|
||||
if (currentMouseDown !== currentMouseUp) {
|
||||
// console.debug(`not executing escape actions on ${currentMouseUp} because click started on ${currentMouseDown}`);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
evt.button === 0 &&
|
||||
$(evt.target).closest('a,button,.is-editable').length === 0
|
||||
|
|
|
|||
|
|
@ -77,8 +77,28 @@ InlinedForm = BlazeComponent.extendComponent({
|
|||
return [
|
||||
{
|
||||
'click .js-close-inlined-form': this.close,
|
||||
'click .js-open-inlined-form': this.open,
|
||||
|
||||
'pointerdown .js-open-inlined-form'(e) {
|
||||
if (Utils.shouldIgnorePointer(e)) {
|
||||
return;
|
||||
}
|
||||
// to measure the click duration
|
||||
$(e.target).data("clickStart", new Date());
|
||||
},
|
||||
'pointerup .js-open-inlined-form'(e) {
|
||||
if(Utils.shouldIgnorePointer(e)) {
|
||||
return;
|
||||
}
|
||||
const start = $(e.target).data("clickStart",);
|
||||
if (!start) {
|
||||
return;
|
||||
}
|
||||
const end = new Date();
|
||||
// 500ms feels reasonable for a simple click
|
||||
if (end - start < 500) {
|
||||
this.open(e);
|
||||
}
|
||||
$(e.target).data("clickStart", null);
|
||||
},
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea'(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ hotkeys(nums, (event, handler) => {
|
|||
return;
|
||||
}
|
||||
const board = ReactiveCache.getBoard(currentBoardId);
|
||||
if (!board) {return}
|
||||
const labels = board.labels;
|
||||
if (MultiSelection.isActive() && ReactiveCache.getCurrentUser().isBoardMember()) {
|
||||
const cardIds = MultiSelection.getSelectedCardIds();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
const closedValue = null;
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
|
||||
window.Modal = new (class {
|
||||
constructor() {
|
||||
this._currentModal = new ReactiveVar(closedValue);
|
||||
|
|
|
|||
|
|
@ -1,121 +1,25 @@
|
|||
import PopupComponent from '/client/components/main/popup';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
|
||||
window.Popup = new (class {
|
||||
constructor() {
|
||||
// The template we use to render popups
|
||||
this.template = Template.popup;
|
||||
|
||||
// We only want to display one popup at a time and we keep the view object
|
||||
// in this `Popup.current` variable. If there is no popup currently opened
|
||||
// the value is `null`.
|
||||
this.current = null;
|
||||
|
||||
// It's possible to open a sub-popup B from a popup A. In that case we keep
|
||||
// the data of popup A so we can return back to it. Every time we open a new
|
||||
// popup the stack grows, every time we go back the stack decrease, and if
|
||||
// we close the popup the stack is reseted to the empty stack [].
|
||||
this._stack = [];
|
||||
|
||||
// We invalidate this internal dependency every time the top of the stack
|
||||
// has changed and we want to re-render a popup with the new top-stack data.
|
||||
this._dep = new Tracker.Dependency();
|
||||
}
|
||||
|
||||
/// This function returns a callback that can be used in an event map:
|
||||
/// Template.tplName.events({
|
||||
/// 'click .elementClass': Popup.open("popupName"),
|
||||
/// });
|
||||
/// The popup inherit the data context of its parent.
|
||||
open(name) {
|
||||
open(name, args) {
|
||||
const self = this;
|
||||
const popupName = `${name}Popup`;
|
||||
function clickFromPopup(evt) {
|
||||
return $(evt.target).closest('.js-pop-over').length !== 0;
|
||||
}
|
||||
/** opens the popup
|
||||
* @param evt the current event
|
||||
* @param options options (dataContextIfCurrentDataIsUndefined use this dataContext if this.currentData() is undefined)
|
||||
*/
|
||||
return function(evt, options) {
|
||||
// If a popup is already opened, clicking again on the opener element
|
||||
// should close it -- and interrupt the current `open` function.
|
||||
if (self.isOpen()) {
|
||||
const previousOpenerElement = self._getTopStack().openerElement;
|
||||
if (previousOpenerElement === evt.currentTarget) {
|
||||
self.close();
|
||||
return;
|
||||
} else {
|
||||
$(previousOpenerElement).removeClass('is-active');
|
||||
// Clean up previous popup content to prevent mixing
|
||||
self._cleanupPreviousPopupContent();
|
||||
}
|
||||
const popupName = `${name}Popup`;
|
||||
const openerElement = evt.target;
|
||||
let classicArgs = { openerElement: openerElement, name: popupName, title: self._getTitle(popupName), miscOptions: options };
|
||||
if (typeof(args) === "object") {
|
||||
classicArgs = Object.assign(classicArgs, args);
|
||||
}
|
||||
|
||||
// We determine the `openerElement` (the DOM element that is being clicked
|
||||
// and the one we take in reference to position the popup) from the event
|
||||
// if the popup has no parent, or from the parent `openerElement` if it
|
||||
// has one. This allows us to position a sub-popup exactly at the same
|
||||
// position than its parent.
|
||||
let openerElement;
|
||||
if (clickFromPopup(evt) && self._getTopStack()) {
|
||||
openerElement = self._getTopStack().openerElement;
|
||||
} else {
|
||||
// For Member Settings sub-popups, always start fresh to avoid content mixing
|
||||
if (popupName.includes('changeLanguage') || popupName.includes('changeAvatar') ||
|
||||
popupName.includes('editProfile') || popupName.includes('changePassword') ||
|
||||
popupName.includes('invitePeople') || popupName.includes('support')) {
|
||||
self._stack = [];
|
||||
}
|
||||
openerElement = evt.currentTarget;
|
||||
}
|
||||
$(openerElement).addClass('is-active');
|
||||
PopupComponent.open(classicArgs);
|
||||
evt.preventDefault();
|
||||
|
||||
// We push our popup data to the stack. The top of the stack is always
|
||||
// used as the data source for our current popup.
|
||||
self._stack.push({
|
||||
popupName,
|
||||
openerElement,
|
||||
hasPopupParent: clickFromPopup(evt),
|
||||
title: self._getTitle(popupName),
|
||||
depth: self._stack.length,
|
||||
offset: self._getOffset(openerElement),
|
||||
dataContext: (this && this.currentData && this.currentData()) || (options && options.dataContextIfCurrentDataIsUndefined) || this,
|
||||
});
|
||||
|
||||
const $contentWrapper = $('.content-wrapper')
|
||||
if ($contentWrapper.length > 0) {
|
||||
const contentWrapper = $contentWrapper[0];
|
||||
self._getTopStack().scrollTop = contentWrapper.scrollTop;
|
||||
// scroll from e.g. delete comment to the top (where the confirm button is)
|
||||
$contentWrapper.scrollTop(0);
|
||||
}
|
||||
|
||||
// If there are no popup currently opened we use the Blaze API to render
|
||||
// one into the DOM. We use a reactive function as the data parameter that
|
||||
// return the complete along with its top element and depends on our
|
||||
// internal dependency that is being invalidated every time the top
|
||||
// element of the stack has changed and we want to update the popup.
|
||||
//
|
||||
// Otherwise if there is already a popup open we just need to invalidate
|
||||
// our internal dependency, and since we just changed the top element of
|
||||
// our internal stack, the popup will be updated with the new data.
|
||||
if (!self.isOpen()) {
|
||||
if (!Template[popupName]) {
|
||||
console.error('Template not found:', popupName);
|
||||
return;
|
||||
}
|
||||
self.current = Blaze.renderWithData(
|
||||
self.template,
|
||||
() => {
|
||||
self._dep.depend();
|
||||
return { ...self._getTopStack(), stack: self._stack };
|
||||
},
|
||||
document.body,
|
||||
);
|
||||
} else {
|
||||
self._dep.changed();
|
||||
}
|
||||
// important so that one click does not opens multiple, stacked popups
|
||||
evt.stopPropagation();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -127,149 +31,40 @@ window.Popup = new (class {
|
|||
/// });
|
||||
afterConfirm(name, action) {
|
||||
const self = this;
|
||||
|
||||
return function(evt, tpl) {
|
||||
const context = (this.currentData && this.currentData()) || this;
|
||||
context.__afterConfirmAction = action;
|
||||
self.open(name).call(context, evt, tpl);
|
||||
tpl ??= {};
|
||||
tpl.afterConfirm = action;
|
||||
// Just a wrapper of open which will call `action` on some events
|
||||
// see PopupDetachedComponent; for now this is hardcoded
|
||||
self.open(name)(evt, tpl);
|
||||
evt.preventDefault();
|
||||
};
|
||||
}
|
||||
|
||||
/// The public reactive state of the popup.
|
||||
isOpen() {
|
||||
this._dep.changed();
|
||||
return Boolean(this.current);
|
||||
}
|
||||
|
||||
/// In case the popup was opened from a parent popup we can get back to it
|
||||
/// with this `Popup.back()` function. You can go back several steps at once
|
||||
/// by providing a number to this function, e.g. `Popup.back(2)`. In this case
|
||||
/// intermediate popup won't even be rendered on the DOM. If the number of
|
||||
/// steps back is greater than the popup stack size, the popup will be closed.
|
||||
back(n = 1) {
|
||||
if (this._stack.length > n) {
|
||||
const $contentWrapper = $('.content-wrapper')
|
||||
if ($contentWrapper.length > 0) {
|
||||
const contentWrapper = $contentWrapper[0];
|
||||
const stack = this._stack[this._stack.length - n];
|
||||
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
|
||||
const scrollTopMax = contentWrapper.scrollTopMax || contentWrapper.scrollHeight - contentWrapper.clientHeight;
|
||||
if (scrollTopMax && stack.scrollTop > scrollTopMax) {
|
||||
// sometimes scrollTopMax is lower than scrollTop, so i need this dirty hack
|
||||
setTimeout(() => {
|
||||
$contentWrapper.scrollTop(stack.scrollTop);
|
||||
}, 6);
|
||||
}
|
||||
// restore the old popup scroll position
|
||||
$contentWrapper.scrollTop(stack.scrollTop);
|
||||
}
|
||||
_.times(n, () => this._stack.pop());
|
||||
this._dep.changed();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
_.times(n, () => PopupComponent.destroy());
|
||||
}
|
||||
|
||||
/// Close the current opened popup.
|
||||
close() {
|
||||
if (this.isOpen()) {
|
||||
Blaze.remove(this.current);
|
||||
this.current = null;
|
||||
|
||||
const openerElement = this._getTopStack().openerElement;
|
||||
$(openerElement).removeClass('is-active');
|
||||
|
||||
this._stack = [];
|
||||
// Clean up popup content when closing
|
||||
this._cleanupPreviousPopupContent();
|
||||
}
|
||||
this.back();
|
||||
}
|
||||
|
||||
closeAll() {
|
||||
this.back(PopupComponent.stack.length)
|
||||
}
|
||||
|
||||
|
||||
getOpenerComponent(n=4) {
|
||||
const { openerElement } = Template.parentData(n);
|
||||
return BlazeComponent.getComponentForElement(openerElement);
|
||||
}
|
||||
|
||||
// An utility function that returns the top element of the internal stack
|
||||
_getTopStack() {
|
||||
return this._stack[this._stack.length - 1];
|
||||
}
|
||||
|
||||
_cleanupPreviousPopupContent() {
|
||||
// Force a re-render to ensure proper cleanup
|
||||
if (this._dep) {
|
||||
this._dep.changed();
|
||||
}
|
||||
}
|
||||
|
||||
// We automatically calculate the popup offset from the reference element
|
||||
// position and dimensions. We also reactively use the window dimensions to
|
||||
// ensure that the popup is always visible on the screen.
|
||||
_getOffset(element) {
|
||||
const $element = $(element);
|
||||
return () => {
|
||||
Utils.windowResizeDep.depend();
|
||||
|
||||
if (Utils.isMiniScreen()) return { left: 0, top: 0 };
|
||||
|
||||
// If the opener element is missing (e.g., programmatic open), fallback to viewport origin
|
||||
if (!$element || $element.length === 0) {
|
||||
return { left: 10, top: 10, maxHeight: $(window).height() - 20 };
|
||||
}
|
||||
|
||||
const offset = $element.offset();
|
||||
// Calculate actual popup width based on CSS: min(380px, 55vw)
|
||||
const viewportWidth = $(window).width();
|
||||
const viewportHeight = $(window).height();
|
||||
const popupWidth = Math.min(380, viewportWidth * 0.55) + 15; // Add 15px for margin
|
||||
|
||||
// Check if this is an admin panel edit popup
|
||||
const isAdminEditPopup = $element.hasClass('edit-user') ||
|
||||
$element.hasClass('edit-org') ||
|
||||
$element.hasClass('edit-team');
|
||||
|
||||
if (isAdminEditPopup) {
|
||||
// Center the popup horizontally and use full height
|
||||
const centeredLeft = (viewportWidth - popupWidth) / 2;
|
||||
|
||||
return {
|
||||
left: Math.max(10, centeredLeft), // Ensure popup doesn't go off screen
|
||||
top: 10, // Start from top with small margin
|
||||
maxHeight: viewportHeight - 20, // Use full height minus small margins
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate available height for popup
|
||||
const popupTop = offset.top + $element.outerHeight();
|
||||
|
||||
// For language popup, don't use dynamic height to avoid overlapping board
|
||||
const isLanguagePopup = $element.hasClass('js-change-language');
|
||||
let availableHeight, maxPopupHeight;
|
||||
|
||||
if (isLanguagePopup) {
|
||||
// For language popup, position content area below right vertical scrollbar
|
||||
const availableHeight = viewportHeight - popupTop - 20; // 20px margin from bottom (near scrollbar)
|
||||
const calculatedHeight = Math.min(availableHeight, viewportHeight * 0.5); // Max 50% of viewport
|
||||
|
||||
return {
|
||||
left: Math.min(offset.left, viewportWidth - popupWidth),
|
||||
top: popupTop,
|
||||
maxHeight: Math.max(calculatedHeight, 200), // Minimum 200px height
|
||||
};
|
||||
} else {
|
||||
// For other popups, use the dynamic height calculation
|
||||
availableHeight = viewportHeight - popupTop - 20; // 20px margin from bottom
|
||||
maxPopupHeight = Math.min(availableHeight, viewportHeight * 0.8); // Max 80% of viewport
|
||||
|
||||
return {
|
||||
left: Math.min(offset.left, viewportWidth - popupWidth),
|
||||
top: popupTop,
|
||||
maxHeight: Math.max(maxPopupHeight, 200), // Minimum 200px height
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// We get the title from the translation files. Instead of returning the
|
||||
// result, we return a function that compute the result and since `TAPi18n.__`
|
||||
// is a reactive data source, the title will be changed reactively.
|
||||
|
|
@ -297,10 +92,11 @@ escapeActions.forEach(actionName => {
|
|||
EscapeActions.register(
|
||||
`popup-${actionName}`,
|
||||
() => Popup[actionName](),
|
||||
() => Popup.isOpen(),
|
||||
() => PopupComponent.stack.length > 0,
|
||||
{
|
||||
noClickEscapeOn: '.js-pop-over,.js-open-card-title-popup,.js-open-inlined-form,.textcomplete-dropdown',
|
||||
// will maybe need something more robust, but for now it enables multiple cards opened without closing each other when clicking on common UI elements
|
||||
noClickEscapeOn: '.js-pop-over,.js-open-card-title-popup,.js-open-inlined-form,.textcomplete-dropdown,.js-card-details,.board-sidebar,#header,.add-comment-reaction',
|
||||
enabledOnClick: actionName === 'close',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -24,7 +24,7 @@ Utils = {
|
|||
}
|
||||
return ret;
|
||||
},
|
||||
getCurrentCardId(ignorePopupCard) {
|
||||
getCurrentCardId(ignorePopupCard = false) {
|
||||
let ret = Session.get('currentCard');
|
||||
if (!ret && !ignorePopupCard) {
|
||||
ret = Utils.getPopupCardId();
|
||||
|
|
@ -47,70 +47,62 @@ Utils = {
|
|||
const ret = ReactiveCache.getBoard(boardId);
|
||||
return ret;
|
||||
},
|
||||
getCurrentCard(ignorePopupCard) {
|
||||
getCurrentCard(ignorePopupCard = false) {
|
||||
const cardId = Utils.getCurrentCardId(ignorePopupCard);
|
||||
const ret = ReactiveCache.getCard(cardId);
|
||||
return ret;
|
||||
},
|
||||
|
||||
// Zoom and mobile mode utilities
|
||||
getZoomLevel() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile && user.profile.zoomLevel !== undefined) {
|
||||
return user.profile.zoomLevel;
|
||||
}
|
||||
// For non-logged-in users, check localStorage
|
||||
const stored = localStorage.getItem('wekan-zoom-level');
|
||||
return stored ? parseFloat(stored) : 1.0;
|
||||
// in fact, what we really care is screen size
|
||||
// large mobile device like iPad or android Pad has a big screen, it should also behave like a desktop
|
||||
// in a small window (even on desktop), Wekan run in compact mode.
|
||||
// we can easily debug with a small window of desktop browser. :-)
|
||||
isMiniScreen() {
|
||||
this.windowResizeDep.depend();
|
||||
// Also depend on mobile mode changes to make this reactive
|
||||
|
||||
// innerWidth can be over screen width in some case; rely on physical pixels
|
||||
// we get what we want, i.e real width, no need for orientation
|
||||
const width = Math.min(window.innerWidth, window.screen.width);
|
||||
const isMobilePhone = /iPhone|iPad|Mobile|Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && !/iPad/i.test(navigator.userAgent);
|
||||
const isTouch = this.isTouchScreen();
|
||||
|
||||
return (isTouch || isMobilePhone || width < 800);
|
||||
},
|
||||
|
||||
setZoomLevel(level) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
// Update user profile
|
||||
user.setZoomLevel(level);
|
||||
isTouchScreen() {
|
||||
// NEW TOUCH DEVICE DETECTION:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
||||
var hasTouchScreen = false;
|
||||
if ("maxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
} else if ("msMaxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
} else {
|
||||
// Store in localStorage for non-logged-in users
|
||||
localStorage.setItem('wekan-zoom-level', level.toString());
|
||||
var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
|
||||
if (mQ && mQ.media === "(pointer:coarse)") {
|
||||
hasTouchScreen = !!mQ.matches;
|
||||
} else if ('orientation' in window) {
|
||||
hasTouchScreen = true; // deprecated, but good fallback
|
||||
} else {
|
||||
// Only as a last resort, fall back to user agent sniffing
|
||||
var UA = navigator.userAgent;
|
||||
hasTouchScreen = (
|
||||
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
|
||||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA)
|
||||
);
|
||||
}
|
||||
}
|
||||
Utils.applyZoomLevel(level);
|
||||
|
||||
// Trigger reactive updates for UI components
|
||||
Session.set('wekan-zoom-level', level);
|
||||
return hasTouchScreen;
|
||||
},
|
||||
|
||||
getMobileMode() {
|
||||
// Check localStorage first - user's explicit preference takes priority
|
||||
const stored = localStorage.getItem('wekan-mobile-mode');
|
||||
if (stored !== null) {
|
||||
return stored === 'true';
|
||||
}
|
||||
|
||||
// Then check user profile
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile && user.profile.mobileMode !== undefined) {
|
||||
return user.profile.mobileMode;
|
||||
}
|
||||
|
||||
// Default to mobile mode for iPhone/iPod
|
||||
const isIPhone = /iPhone|iPod/i.test(navigator.userAgent);
|
||||
return isIPhone;
|
||||
return this.isMiniScreen();
|
||||
},
|
||||
|
||||
setMobileMode(enabled) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
// Update user profile
|
||||
user.setMobileMode(enabled);
|
||||
}
|
||||
// Always store in localStorage for persistence across sessions
|
||||
localStorage.setItem('wekan-mobile-mode', enabled.toString());
|
||||
Utils.applyMobileMode(enabled);
|
||||
// Trigger reactive updates for UI components
|
||||
Session.set('wekan-mobile-mode', enabled);
|
||||
// Re-apply zoom level to ensure proper rendering
|
||||
const zoomLevel = Utils.getZoomLevel();
|
||||
Utils.applyZoomLevel(zoomLevel);
|
||||
Utils.applyMobileMode(enabled);
|
||||
},
|
||||
|
||||
getCardZoom() {
|
||||
|
|
@ -139,77 +131,6 @@ Utils = {
|
|||
}
|
||||
},
|
||||
|
||||
applyZoomLevel(level) {
|
||||
const boardWrapper = document.querySelector('.board-wrapper');
|
||||
const body = document.body;
|
||||
const isMobileMode = body.classList.contains('mobile-mode');
|
||||
|
||||
if (boardWrapper) {
|
||||
if (isMobileMode) {
|
||||
// On mobile mode, only apply zoom to text and icons, not the entire layout
|
||||
// Remove any existing transform from board-wrapper
|
||||
boardWrapper.style.transform = '';
|
||||
boardWrapper.style.transformOrigin = '';
|
||||
|
||||
// Apply zoom to text and icon elements instead
|
||||
const textElements = boardWrapper.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div, .minicard, .list-header-name, .board-header-btn, .fa, .icon');
|
||||
textElements.forEach(element => {
|
||||
element.style.transform = `scale(${level})`;
|
||||
element.style.transformOrigin = 'center';
|
||||
});
|
||||
|
||||
// Reset board-canvas height
|
||||
const boardCanvas = document.querySelector('.board-canvas');
|
||||
if (boardCanvas) {
|
||||
boardCanvas.style.height = '';
|
||||
}
|
||||
} else {
|
||||
// Desktop mode: apply zoom to entire board-wrapper as before
|
||||
boardWrapper.style.transform = `scale(${level})`;
|
||||
boardWrapper.style.transformOrigin = 'top left';
|
||||
|
||||
// If zoom is 50% or lower, make board wrapper full width like content
|
||||
if (level <= 0.5) {
|
||||
boardWrapper.style.width = '100%';
|
||||
boardWrapper.style.maxWidth = '100%';
|
||||
boardWrapper.style.margin = '0';
|
||||
} else {
|
||||
// Reset to normal width for higher zoom levels
|
||||
boardWrapper.style.width = '';
|
||||
boardWrapper.style.maxWidth = '';
|
||||
boardWrapper.style.margin = '';
|
||||
}
|
||||
|
||||
// Adjust container height to prevent scroll issues
|
||||
const boardCanvas = document.querySelector('.board-canvas');
|
||||
if (boardCanvas) {
|
||||
boardCanvas.style.height = `${100 / level}%`;
|
||||
|
||||
// For high zoom levels (200%+), enable both horizontal and vertical scrolling
|
||||
if (level >= 2.0) {
|
||||
boardCanvas.style.overflowX = 'auto';
|
||||
boardCanvas.style.overflowY = 'auto';
|
||||
// Ensure the content area can scroll both horizontally and vertically
|
||||
const content = document.querySelector('#content');
|
||||
if (content) {
|
||||
content.style.overflowX = 'auto';
|
||||
content.style.overflowY = 'auto';
|
||||
}
|
||||
} else {
|
||||
// Reset overflow for normal zoom levels
|
||||
boardCanvas.style.overflowX = '';
|
||||
boardCanvas.style.overflowY = '';
|
||||
const content = document.querySelector('#content');
|
||||
if (content) {
|
||||
content.style.overflowX = '';
|
||||
content.style.overflowY = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
applyMobileMode(enabled) {
|
||||
const body = document.body;
|
||||
if (enabled) {
|
||||
|
|
@ -223,9 +144,7 @@ Utils = {
|
|||
|
||||
initializeUserSettings() {
|
||||
// Apply saved settings on page load
|
||||
const zoomLevel = Utils.getZoomLevel();
|
||||
const mobileMode = Utils.getMobileMode();
|
||||
Utils.applyZoomLevel(zoomLevel);
|
||||
Utils.applyMobileMode(mobileMode);
|
||||
},
|
||||
getCurrentList() {
|
||||
|
|
@ -284,11 +203,11 @@ Utils = {
|
|||
},
|
||||
setBoardView(view) {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
|
||||
|
||||
if (currentUser) {
|
||||
// Update localStorage first
|
||||
window.localStorage.setItem('boardView', view);
|
||||
|
||||
|
||||
// Update user profile via Meteor method
|
||||
Meteor.call('setBoardView', view, (error) => {
|
||||
if (error) {
|
||||
|
|
@ -575,82 +494,6 @@ Utils = {
|
|||
},
|
||||
|
||||
windowResizeDep: new Tracker.Dependency(),
|
||||
// in fact, what we really care is screen size
|
||||
// large mobile device like iPad or android Pad has a big screen, it should also behave like a desktop
|
||||
// in a small window (even on desktop), Wekan run in compact mode.
|
||||
// we can easily debug with a small window of desktop browser. :-)
|
||||
isMiniScreen() {
|
||||
this.windowResizeDep.depend();
|
||||
// Also depend on mobile mode changes to make this reactive
|
||||
Session.get('wekan-mobile-mode');
|
||||
|
||||
// Show mobile view when:
|
||||
// 1. Screen width is 800px or less (matches CSS media queries)
|
||||
// 2. Mobile phones in portrait mode
|
||||
// 3. iPad in very small screens (≤ 600px)
|
||||
// 4. All iPhone models by default (including largest models), but respect user preference
|
||||
const isSmallScreen = window.innerWidth <= 800;
|
||||
const isVerySmallScreen = window.innerWidth <= 600;
|
||||
const isPortrait = window.innerWidth < window.innerHeight || window.matchMedia("(orientation: portrait)").matches;
|
||||
const isMobilePhone = /Mobile|Android|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) && !/iPad/i.test(navigator.userAgent);
|
||||
const isIPhone = /iPhone|iPod/i.test(navigator.userAgent);
|
||||
const isIPad = /iPad/i.test(navigator.userAgent);
|
||||
const isUbuntuTouch = /Ubuntu/i.test(navigator.userAgent);
|
||||
|
||||
// Check if user has explicitly set mobile mode preference
|
||||
const userMobileMode = this.getMobileMode();
|
||||
|
||||
// For iPhone: default to mobile view, but respect user's mobile mode toggle preference
|
||||
// This ensures all iPhone models (including iPhone 15 Pro Max, 14 Pro Max, etc.) start with mobile view
|
||||
// but users can still switch to desktop mode if they prefer
|
||||
if (isIPhone) {
|
||||
// If user has explicitly set a preference, respect it
|
||||
if (userMobileMode !== null && userMobileMode !== undefined) {
|
||||
return userMobileMode;
|
||||
}
|
||||
// Otherwise, default to mobile view for iPhones
|
||||
return true;
|
||||
} else if (isMobilePhone) {
|
||||
return isPortrait; // Other mobile phones: portrait = mobile, landscape = desktop
|
||||
} else if (isIPad) {
|
||||
return isVerySmallScreen; // iPad: only very small screens get mobile view
|
||||
} else if (isUbuntuTouch) {
|
||||
// Ubuntu Touch: smartphones (≤ 600px) behave like mobile phones, tablets (> 600px) like iPad
|
||||
if (isVerySmallScreen) {
|
||||
return isPortrait; // Ubuntu Touch smartphone: portrait = mobile, landscape = desktop
|
||||
} else {
|
||||
return isVerySmallScreen; // Ubuntu Touch tablet: only very small screens get mobile view
|
||||
}
|
||||
} else {
|
||||
return isSmallScreen; // Desktop: based on 800px screen width
|
||||
}
|
||||
},
|
||||
|
||||
isTouchScreen() {
|
||||
// NEW TOUCH DEVICE DETECTION:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
||||
var hasTouchScreen = false;
|
||||
if ("maxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
} else if ("msMaxTouchPoints" in navigator) {
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
} else {
|
||||
var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
|
||||
if (mQ && mQ.media === "(pointer:coarse)") {
|
||||
hasTouchScreen = !!mQ.matches;
|
||||
} else if ('orientation' in window) {
|
||||
hasTouchScreen = true; // deprecated, but good fallback
|
||||
} else {
|
||||
// Only as a last resort, fall back to user agent sniffing
|
||||
var UA = navigator.userAgent;
|
||||
hasTouchScreen = (
|
||||
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
|
||||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA)
|
||||
);
|
||||
}
|
||||
}
|
||||
return hasTouchScreen;
|
||||
},
|
||||
|
||||
// returns if desktop drag handles are enabled
|
||||
isShowDesktopDragHandles() {
|
||||
|
|
@ -894,17 +737,249 @@ Utils = {
|
|||
showCopied(promise, $tooltip) {
|
||||
if (promise) {
|
||||
promise.then(() => {
|
||||
$tooltip.show(100);
|
||||
setTimeout(() => $tooltip.hide(100), 1000);
|
||||
$tooltip.removeClass("copied-tooltip-hidden").addClass("copied-tooltip-visible");
|
||||
setTimeout(() => $tooltip.removeClass("copied-tooltip-visible").addClass("copied-tooltip-hidden"), 1000);
|
||||
}, (err) => {
|
||||
console.error("error: ", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
coalesceSearch(root, queries, fallbackSel) {
|
||||
// a little helper to chain jQuery lookups
|
||||
// use with arg like [{func: "closest", sels: [".whatever"...]}...]
|
||||
root = $(root);
|
||||
for ({func, sels} of queries) {
|
||||
for (sel of sels) {
|
||||
res = root[func](sel);
|
||||
if (res.length) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $(fallbackSel);
|
||||
},
|
||||
|
||||
scrollIfNeeded(event) {
|
||||
// helper used when dragging either cards or lists
|
||||
const xFactor = 5;
|
||||
const yFactor = Utils.isMiniScreen() ? 5 : 10;
|
||||
const limitX = window.innerWidth / xFactor;
|
||||
const limitY = window.innerHeight / yFactor;
|
||||
const componentScrollX = this.coalesceSearch(event.target, [{
|
||||
func: "closest",
|
||||
sels: [".swimlane-container", ".swimlane.js-lists", ".board-canvas"]
|
||||
}
|
||||
], ".board-canvas");
|
||||
let scrollX = 0;
|
||||
let scrollY = 0;
|
||||
if (event.clientX < limitX) {
|
||||
scrollX = -limitX;
|
||||
} else if (event.clientX > (xFactor - 1) * limitX) {
|
||||
scrollX = limitX;
|
||||
}
|
||||
if (event.clientY < limitY) {
|
||||
scrollY = -limitY;
|
||||
} else if (event.clientY > (yFactor - 1) * limitY) {
|
||||
scrollY = limitY;
|
||||
}
|
||||
window.scrollBy({ top: scrollY, behavior: "smooth" });
|
||||
componentScrollX[0].scrollBy({ left: scrollX, behavior: "smooth" });
|
||||
},
|
||||
|
||||
shouldIgnorePointer(event) {
|
||||
// handle jQuery and native events
|
||||
if (event.originalEvent) {
|
||||
event = event.originalEvent;
|
||||
}
|
||||
return !(event.isPrimary && (event.pointerType !== 'mouse' || event.button === 0));
|
||||
},
|
||||
allowsReceivedDate() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsReceivedDate : false;
|
||||
},
|
||||
|
||||
allowsStartDate() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsStartDate : false;
|
||||
},
|
||||
|
||||
allowsDueDate() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsDueDate : false;
|
||||
},
|
||||
|
||||
allowsEndDate() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsEndDate : false;
|
||||
},
|
||||
|
||||
allowsSubtasks() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsSubtasks : false;
|
||||
},
|
||||
|
||||
allowsCreator() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? (currentBoard.allowsCreator ?? false) : false;
|
||||
},
|
||||
|
||||
allowsCreatorOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? (currentBoard.allowsCreatorOnMinicard ?? false) : false;
|
||||
},
|
||||
|
||||
allowsMembers() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsMembers : false;
|
||||
},
|
||||
|
||||
allowsAssignee() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsAssignee : false;
|
||||
},
|
||||
|
||||
allowsAssignedBy() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsAssignedBy : false;
|
||||
},
|
||||
|
||||
allowsRequestedBy() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsRequestedBy : false;
|
||||
},
|
||||
|
||||
allowsCardSortingByNumber() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsCardSortingByNumber : false;
|
||||
},
|
||||
|
||||
allowsShowLists() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsShowLists : false;
|
||||
},
|
||||
|
||||
allowsLabels() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsLabels : false;
|
||||
},
|
||||
|
||||
allowsShowListsOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsShowListsOnMinicard : false;
|
||||
},
|
||||
|
||||
allowsChecklists() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsChecklists : false;
|
||||
},
|
||||
|
||||
allowsAttachments() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsAttachments : false;
|
||||
},
|
||||
|
||||
allowsComments() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsComments : false;
|
||||
},
|
||||
|
||||
allowsCardNumber() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsCardNumber : false;
|
||||
},
|
||||
|
||||
allowsDescriptionTitle() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsDescriptionTitle : false;
|
||||
},
|
||||
|
||||
allowsDescriptionText() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsDescriptionText : false;
|
||||
},
|
||||
|
||||
isBoardSelected() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.dateSettingsDefaultBoardID : false;
|
||||
},
|
||||
|
||||
isNullBoardSelected() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? (
|
||||
currentBoard.dateSettingsDefaultBoardId === null ||
|
||||
currentBoard.dateSettingsDefaultBoardId === undefined
|
||||
) : true;
|
||||
},
|
||||
|
||||
allowsDescriptionTextOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsDescriptionTextOnMinicard : false;
|
||||
},
|
||||
|
||||
allowsActivities() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsActivities : false;
|
||||
},
|
||||
|
||||
allowsCoverAttachmentOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsCoverAttachmentOnMinicard : false;
|
||||
},
|
||||
|
||||
allowsBadgeAttachmentOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsBadgeAttachmentOnMinicard : false;
|
||||
},
|
||||
|
||||
allowsCardSortingByNumberOnMinicard() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.allowsCardSortingByNumberOnMinicard : false;
|
||||
},
|
||||
};
|
||||
|
||||
// A simple tracker dependency that we invalidate every time the window is
|
||||
// resized. This is used to reactively re-calculate the popup position in case
|
||||
// of a window resize. This is the equivalent of a "Signal" in some other
|
||||
// programming environments (eg, elm).
|
||||
$(window).on('resize', () => Utils.windowResizeDep.changed());
|
||||
|
||||
$(window).on('resize', () => {
|
||||
// A simple tracker dependency that we invalidate every time the window is
|
||||
// resized. This is used to reactively re-calculate the popup position in case
|
||||
// of a window resize. This is the equivalent of a "Signal" in some other
|
||||
// programming environments (eg, elm).
|
||||
Utils.windowResizeDep.changed();
|
||||
// Simple, generic switch based exclusively on the new detection algorithm
|
||||
// Hope it will centralize decision and reduce edge cases
|
||||
Utils.setMobileMode(Utils.isMiniScreen());
|
||||
});
|
||||
|
||||
$(() => {
|
||||
const settingsHelpers = ["allowsReceivedDate", "allowsStartDate", "allowsDueDate", "allowsEndDate", "allowsSubtasks", "allowsCreator", "allowsCreatorOnMinicard", "allowsMembers", "allowsAssignee", "allowsAssignedBy", "allowsRequestedBy", "allowsCardSortingByNumber", "allowsShowLists", "allowsLabels", "allowsShowListsOnMinicard", "allowsChecklists", "allowsAttachments", "allowsComments", "allowsCardNumber", "allowsDescriptionTitle", "allowsDescriptionText", "allowsDescriptionTextOnMinicard", "allowsActivities", "allowsCoverAttachmentOnMinicard", "allowsBadgeAttachmentOnMinicard", "allowsCardSortingByNumberOnMinicard"]
|
||||
for (f of settingsHelpers) {
|
||||
Template.registerHelper(f, Utils[f]);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue