mirror of
https://github.com/wekan/wekan.git
synced 2026-02-18 22:18:07 +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
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,13 @@
|
|||
template(name='list')
|
||||
.list.js-list(id="js-list-{{_id}}"
|
||||
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
|
||||
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
unless collapsed
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
|
||||
template(name='miniList')
|
||||
a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
+listHeader
|
||||
if isCurrentList
|
||||
+listBody
|
||||
|
|
@ -4,6 +4,8 @@ require('/client/lib/jquery-ui.js')
|
|||
|
||||
const { calculateIndex } = Utils;
|
||||
|
||||
export const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// Proxy
|
||||
openForm(options) {
|
||||
|
|
@ -12,6 +14,7 @@ BlazeComponent.extendComponent({
|
|||
|
||||
onCreated() {
|
||||
this.newCardFormIsVisible = new ReactiveVar(true);
|
||||
this.collapse = new ReactiveVar(Utils.getListCollapseState(this.data()));
|
||||
},
|
||||
|
||||
// The jquery UI sortable library is the best solution I've found so far. I
|
||||
|
|
@ -22,183 +25,37 @@ BlazeComponent.extendComponent({
|
|||
// callback, we basically solve all issues related to reactive updates. A
|
||||
// comment below provides further details.
|
||||
onRendered() {
|
||||
const boardComponent = this.parentComponent().parentComponent();
|
||||
|
||||
// Initialize list resize functionality immediately
|
||||
this.list = this.firstNode();
|
||||
this.resizeHandle = this.find('.js-list-resize-handle');
|
||||
this.initializeListResize();
|
||||
|
||||
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
const $cards = this.$('.js-minicards');
|
||||
|
||||
$cards.sortable({
|
||||
connectWith: '.js-minicards:not(.js-list-full)',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.board-canvas',
|
||||
helper(evt, item) {
|
||||
const helper = item.clone();
|
||||
if (MultiSelection.isActive()) {
|
||||
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
|
||||
if (andNOthers > 0) {
|
||||
helper.append(
|
||||
$(
|
||||
Blaze.toHTML(
|
||||
HTML.DIV(
|
||||
{ class: 'and-n-other' },
|
||||
TAPi18n.__('and-n-other-card', { count: andNOthers }),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return helper;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'minicard-wrapper placeholder',
|
||||
scrollSpeed: 10,
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
boardComponent.setIsDragging(true);
|
||||
},
|
||||
stop(evt, ui) {
|
||||
// To attribute the new index number, we need to get the DOM element
|
||||
// of the previous and the following card -- if any.
|
||||
const prevCardDom = ui.item.prev('.js-minicard').get(0);
|
||||
const nextCardDom = ui.item.next('.js-minicard').get(0);
|
||||
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
|
||||
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
|
||||
let targetSwimlaneId = null;
|
||||
|
||||
// only set a new swimelane ID if the swimlanes view is active
|
||||
if (
|
||||
Utils.boardView() === 'board-view-swimlanes' ||
|
||||
currentBoard.isTemplatesBoard()
|
||||
)
|
||||
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
|
||||
._id;
|
||||
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
// (especially when we move the last card of a list, or when multiple
|
||||
// users move some cards at the same time). To prevent these UX glitches
|
||||
// we ask sortable to gracefully cancel the move, and to put back the
|
||||
// DOM in its initial state. The card move is then handled reactively by
|
||||
// Blaze with the below query.
|
||||
$cards.sortable('cancel');
|
||||
|
||||
if (MultiSelection.isActive()) {
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(
|
||||
currentBoard._id,
|
||||
newSwimlaneId,
|
||||
listId,
|
||||
sortIndex.base + i * sortIndex.increment,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const cardDomElement = ui.item.get(0);
|
||||
const card = Blaze.getData(cardDomElement);
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
|
||||
}
|
||||
boardComponent.setIsDragging(false);
|
||||
},
|
||||
sort(event, ui) {
|
||||
const $boardCanvas = $('.board-canvas');
|
||||
const boardCanvas = $boardCanvas[0];
|
||||
|
||||
if (event.pageX < 10) { // scroll to the left
|
||||
boardCanvas.scrollLeft -= 15;
|
||||
ui.helper[0].offsetLeft -= 15;
|
||||
}
|
||||
if (
|
||||
event.pageX > boardCanvas.offsetWidth - 10 &&
|
||||
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
|
||||
) { // scroll to the right
|
||||
boardCanvas.scrollLeft += 15;
|
||||
}
|
||||
if (
|
||||
event.pageY > boardCanvas.offsetHeight - 10 &&
|
||||
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
|
||||
) { // scroll to the bottom
|
||||
boardCanvas.scrollTop += 15;
|
||||
}
|
||||
if (event.pageY < 10) { // scroll to the top
|
||||
boardCanvas.scrollTop -= 15;
|
||||
}
|
||||
},
|
||||
activate(event, ui) {
|
||||
const $boardCanvas = $('.board-canvas');
|
||||
const boardCanvas = $boardCanvas[0];
|
||||
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
|
||||
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
|
||||
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
|
||||
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
|
||||
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
|
||||
},
|
||||
});
|
||||
|
||||
this.autorun(() => {
|
||||
if ($cards.data('uiSortable') || $cards.data('sortable')) {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$cards.sortable('option', 'handle', '.handle');
|
||||
} else {
|
||||
$cards.sortable('option', 'handle', '.minicard');
|
||||
}
|
||||
|
||||
$cards.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member
|
||||
!Utils.canModifyBoard(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !Utils.canModifyBoard(),
|
||||
);
|
||||
const ensureCollapseState = (collapsed) => {
|
||||
if (this.collapse.get() === collapsed) return;
|
||||
if (this.autoWidth() || collapsed) {
|
||||
$(this.resizeHandle).hide();
|
||||
} else {
|
||||
$(this.resizeHandle).show();
|
||||
}
|
||||
});
|
||||
this.collapse.set(collapsed);
|
||||
this.initializeListResize();
|
||||
}
|
||||
|
||||
// We want to re-run this function any time a card is added.
|
||||
// Reactively update collapse appearance and resize handle visibility when auto-width or collapse changes
|
||||
this.autorun(() => {
|
||||
const currentBoardId = Tracker.nonreactive(() => {
|
||||
return Session.get('currentBoard');
|
||||
});
|
||||
Tracker.afterFlush(() => {
|
||||
$cards.find(itemsSelector).droppable({
|
||||
hoverClass: 'draggable-hover-card',
|
||||
accept: '.js-member,.js-label',
|
||||
drop(event, ui) {
|
||||
const cardId = Blaze.getData(this)._id;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
|
||||
if (ui.draggable.hasClass('js-member')) {
|
||||
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
|
||||
card.assignMember(memberId);
|
||||
} else {
|
||||
const labelId = Blaze.getData(ui.draggable.get(0))._id;
|
||||
card.addLabel(labelId);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
ensureCollapseState(Utils.getListCollapseState(this.data()));
|
||||
});
|
||||
},
|
||||
|
||||
collapsed() {
|
||||
return this.collapse.get();
|
||||
},
|
||||
|
||||
|
||||
listWidth() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
if (!list) return 270; // Return default width if list is not available
|
||||
|
||||
|
||||
if (user) {
|
||||
// For logged-in users, get from user profile
|
||||
return user.getListWidthFromStorage(list.boardId, list._id);
|
||||
|
|
@ -222,8 +79,8 @@ BlazeComponent.extendComponent({
|
|||
listConstraint() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
if (!list) return 550; // Return default constraint if list is not available
|
||||
|
||||
if (!list) return 0;
|
||||
|
||||
if (user) {
|
||||
// For logged-in users, get from user profile
|
||||
return user.getListConstraintFromStorage(list.boardId, list._id);
|
||||
|
|
@ -240,7 +97,7 @@ BlazeComponent.extendComponent({
|
|||
} catch (e) {
|
||||
console.warn('Error reading list constraint from localStorage:', e);
|
||||
}
|
||||
return 550; // Return default constraint if not found
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -256,18 +113,14 @@ BlazeComponent.extendComponent({
|
|||
|
||||
initializeListResize() {
|
||||
// Check if we're still in a valid template context
|
||||
if (!Template.currentData()) {
|
||||
if (!this.data()) {
|
||||
console.warn('No current template data available for list resize initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = Template.currentData();
|
||||
const $list = this.$('.js-list');
|
||||
const $resizeHandle = this.$('.js-list-resize-handle');
|
||||
|
||||
|
||||
// Check if elements exist
|
||||
if (!$list.length || !$resizeHandle.length) {
|
||||
console.warn('List or resize handle not found, retrying in 100ms');
|
||||
if (!this.list || !this.resizeHandle) {
|
||||
console.info('List or resize handle not found, retrying in 100ms');
|
||||
Meteor.setTimeout(() => {
|
||||
if (!this.isDestroyed) {
|
||||
this.initializeListResize();
|
||||
|
|
@ -275,107 +128,129 @@ BlazeComponent.extendComponent({
|
|||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reactively show/hide resize handle based on collapse and auto-width state
|
||||
this.autorun(() => {
|
||||
const isAutoWidth = this.autoWidth();
|
||||
const isCollapsed = Utils.getListCollapseState(list);
|
||||
if (isCollapsed || isAutoWidth) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
}
|
||||
});
|
||||
|
||||
let isResizing = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
let minWidth = 270; // Minimum width matching system default
|
||||
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
|
||||
const component = this; // Store reference to component for use in event handlers
|
||||
let previousLimit = false;
|
||||
// seems reasonable; better let user shrink too much that too little
|
||||
const minWidth = 280;
|
||||
// stored width
|
||||
const width = this.listWidth();
|
||||
// min-width is initially min-content; a good start
|
||||
let maxWidth = this.listConstraint() || parseInt(this.list.style.getProperty('--list-min-width', `${(minWidth)}px`), 10) || width + 100;
|
||||
if (!width || width > maxWidth) {
|
||||
width = (maxWidth + minWidth) / 2;
|
||||
}
|
||||
|
||||
this.list.style.setProperty('--list-min-width', `${Math.round(minWidth)}px`);
|
||||
// actual size before fitting (usually max-content equivalent)
|
||||
this.list.style.setProperty('--list-max-width', `${Math.round(maxWidth)}px`);
|
||||
// avoid jump effect and ensure width stays consistent
|
||||
this.list.style.setProperty('--list-width', `${Math.round(width)}px`);
|
||||
|
||||
const component = this;
|
||||
|
||||
// wait for click to add other events
|
||||
const startResize = (e) => {
|
||||
isResizing = true;
|
||||
startX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
startWidth = $list.outerWidth();
|
||||
|
||||
|
||||
// Add visual feedback
|
||||
$list.addClass('list-resizing');
|
||||
$('body').addClass('list-resizing-active');
|
||||
|
||||
|
||||
// Prevent text selection during resize
|
||||
$('body').css('user-select', 'none');
|
||||
|
||||
// gain access to modern attributes e.g. isPrimary
|
||||
e = e.originalEvent;
|
||||
|
||||
if (isResizing || Utils.shouldIgnorePointer(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(document).on('pointermove', doResize);
|
||||
// e.g. debugger can cancel event without pointerup being fired
|
||||
$(document).on('pointercancel', stopResize);
|
||||
$(document).on('pointerup', stopResize);
|
||||
|
||||
// --list-width can be either a stored size or "auto"; get actual computed size
|
||||
component.currentWidth = component.list.offsetWidth;
|
||||
component.list.classList.add('list-resizing');
|
||||
document.body.classList.add('list-resizing-active');
|
||||
|
||||
isResizing = true;
|
||||
};
|
||||
|
||||
const doResize = (e) => {
|
||||
if (!isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const newWidth = Math.max(minWidth, startWidth + deltaX);
|
||||
|
||||
// Apply the new width immediately for real-time feedback
|
||||
$list[0].style.setProperty('--list-width', `${newWidth}px`);
|
||||
$list[0].style.setProperty('width', `${newWidth}px`);
|
||||
$list[0].style.setProperty('min-width', `${newWidth}px`);
|
||||
$list[0].style.setProperty('max-width', `${newWidth}px`);
|
||||
$list[0].style.setProperty('flex', 'none');
|
||||
$list[0].style.setProperty('flex-basis', 'auto');
|
||||
$list[0].style.setProperty('flex-grow', '0');
|
||||
$list[0].style.setProperty('flex-shrink', '0');
|
||||
|
||||
|
||||
e = e.originalEvent;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isResizing || !e.isPrimary) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!previousLimit && component.collapsed()) {
|
||||
previousLimit = true;
|
||||
component.list.classList.add('cannot-resize');
|
||||
return;
|
||||
}
|
||||
|
||||
// relative to document, always >0 because pointer sticks to the right of list
|
||||
const deltaX = e.clientX - component.list.getBoundingClientRect().right;
|
||||
const candidateWidth = component.currentWidth + deltaX;
|
||||
component.currentWidth = Math.max(minWidth, Math.min(maxWidth, candidateWidth));
|
||||
const reachingMax = (maxWidth - component.currentWidth - 20) <= 0
|
||||
const reachingMin = (component.currentWidth - 20 - minWidth) <= 0
|
||||
// visual indicator to avoid trying too hard; try not to apply each tick
|
||||
if (!previousLimit && (reachingMax && deltaX > 0 || reachingMin && deltaX < 0)) {
|
||||
component.list.classList.add('cannot-resize');
|
||||
previousLimit = true;
|
||||
} else if (previousLimit && !reachingMax && !reachingMin) {
|
||||
component.list.classList.remove('cannot-resize');
|
||||
previousLimit = false;
|
||||
}
|
||||
// Apply the new width immediately for real-time feedback
|
||||
component.list.style.setProperty('--list-width', `${component.currentWidth}px`);
|
||||
};
|
||||
|
||||
const stopResize = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
e = e.originalEvent;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isResizing || !e.isPrimary) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hopefully be gentler on cpu
|
||||
$(document).off('pointermove', doResize);
|
||||
$(document).off('pointercancel', stopResize);
|
||||
$(document).off('pointerup', stopResize);
|
||||
isResizing = false;
|
||||
|
||||
// Calculate final width
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const finalWidth = Math.max(minWidth, startWidth + deltaX);
|
||||
|
||||
// Ensure the final width is applied
|
||||
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
|
||||
$list[0].style.setProperty('width', `${finalWidth}px`);
|
||||
$list[0].style.setProperty('min-width', `${finalWidth}px`);
|
||||
$list[0].style.setProperty('max-width', `${finalWidth}px`);
|
||||
$list[0].style.setProperty('flex', 'none');
|
||||
$list[0].style.setProperty('flex-basis', 'auto');
|
||||
$list[0].style.setProperty('flex-grow', '0');
|
||||
$list[0].style.setProperty('flex-shrink', '0');
|
||||
|
||||
// Remove visual feedback but keep the width
|
||||
$list.removeClass('list-resizing');
|
||||
$('body').removeClass('list-resizing-active');
|
||||
$('body').css('user-select', '');
|
||||
|
||||
// Keep the CSS custom property for persistent width
|
||||
// The CSS custom property will remain on the element to maintain the width
|
||||
|
||||
|
||||
if (previousLimit) {
|
||||
component.list.classList.remove('cannot-resize');
|
||||
}
|
||||
|
||||
const finalWidth = parseInt(component.list.style.getPropertyValue('--list-width'), 10);
|
||||
|
||||
// Remove visual feedback but keep the height
|
||||
component.list.classList.remove('list-resizing');
|
||||
document.body.classList.remove('list-resizing-active');
|
||||
|
||||
if (component.collapse.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the new width using the existing system
|
||||
const list = component.data();
|
||||
const boardId = list.boardId;
|
||||
const listId = list._id;
|
||||
|
||||
|
||||
// Use the new storage method that handles both logged-in and non-logged-in users
|
||||
if (process.env.DEBUG === 'true') {
|
||||
}
|
||||
|
||||
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
// For logged-in users, use server method
|
||||
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
|
||||
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, maxWidth, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error saving list width:', error);
|
||||
} else {
|
||||
|
|
@ -389,61 +264,37 @@ BlazeComponent.extendComponent({
|
|||
// Save list width
|
||||
const storedWidths = localStorage.getItem('wekan-list-widths');
|
||||
let widths = storedWidths ? JSON.parse(storedWidths) : {};
|
||||
|
||||
|
||||
if (!widths[boardId]) {
|
||||
widths[boardId] = {};
|
||||
}
|
||||
widths[boardId][listId] = finalWidth;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
|
||||
|
||||
|
||||
// Save list constraint
|
||||
const storedConstraints = localStorage.getItem('wekan-list-constraints');
|
||||
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
|
||||
|
||||
|
||||
if (!constraints[boardId]) {
|
||||
constraints[boardId] = {};
|
||||
}
|
||||
constraints[boardId][listId] = listConstraint;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error saving list width/constraint to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Mouse events
|
||||
$resizeHandle.on('mousedown', startResize);
|
||||
$(document).on('mousemove', doResize);
|
||||
$(document).on('mouseup', stopResize);
|
||||
|
||||
// Touch events for mobile
|
||||
$resizeHandle.on('touchstart', startResize, { passive: false });
|
||||
$(document).on('touchmove', doResize, { passive: false });
|
||||
$(document).on('touchend', stopResize, { passive: false });
|
||||
|
||||
|
||||
// Prevent dragscroll interference
|
||||
$resizeHandle.on('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
|
||||
// Reactively update resize handle visibility when auto-width or collapse changes
|
||||
component.autorun(() => {
|
||||
const collapsed = Utils.getListCollapseState(list);
|
||||
if (component.autoWidth() || collapsed) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
}
|
||||
});
|
||||
// handle both pointer and touch
|
||||
$(this.resizeHandle).on("pointerdown", startResize);
|
||||
|
||||
// Clean up on component destruction
|
||||
component.onDestroyed(() => {
|
||||
|
|
@ -455,12 +306,6 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('list');
|
||||
|
||||
Template.list.helpers({
|
||||
collapsed() {
|
||||
return Utils.getListCollapseState(this);
|
||||
},
|
||||
});
|
||||
|
||||
Template.miniList.events({
|
||||
'click .js-select-list'() {
|
||||
const listId = this._id;
|
||||
|
|
@ -468,15 +313,10 @@ Template.miniList.events({
|
|||
},
|
||||
});
|
||||
|
||||
// Enable drag-reorder for collapsed lists from .js-collapsed-list-drag area
|
||||
this.$('.js-collapsed-list-drag').draggable({
|
||||
axis: 'x',
|
||||
helper: 'clone',
|
||||
revert: 'invalid',
|
||||
start(evt, ui) {
|
||||
boardComponent.setIsDragging(true);
|
||||
},
|
||||
stop(evt, ui) {
|
||||
boardComponent.setIsDragging(false);
|
||||
}
|
||||
});
|
||||
Template.miniList.helpers({
|
||||
isCurrentList() {
|
||||
const currentList = Utils.getCurrentList();
|
||||
const list = Template.currentData();
|
||||
return currentList && currentList._id == list._id;
|
||||
},
|
||||
});
|
||||
|
|
@ -4,17 +4,18 @@ template(name="listBody")
|
|||
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
+viewer
|
||||
= name
|
||||
if $eq customFieldsSum.type "number"
|
||||
if customFieldSum.lenght
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
+viewer
|
||||
= value
|
||||
if $eq customFieldsSum.type "currency"
|
||||
+viewer
|
||||
= formattedCurrencyCustomFieldValue(value)
|
||||
= name
|
||||
if $eq customFieldsSum.type "number"
|
||||
+viewer
|
||||
= value
|
||||
if $eq customFieldsSum.type "currency"
|
||||
+viewer
|
||||
= formattedCurrencyCustomFieldValue(value)
|
||||
each (cardsWithLimit (idOrNull ../../_id))
|
||||
a.minicard-wrapper.js-minicard(href=originRelativeUrl
|
||||
class="{{#if cardIsSelected}}is-selected{{/if}}"
|
||||
|
|
@ -25,15 +26,15 @@ template(name="listBody")
|
|||
+minicard(this)
|
||||
if (showSpinner (idOrNull ../../_id))
|
||||
+spinnerList
|
||||
|
||||
if canSeeAddCard
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
else
|
||||
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-card'}}
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
a.minicard-wrapper.minicard-add-form
|
||||
+inlinedForm(autoclose=false position="bottom")
|
||||
+addCardForm(listId=_id position="bottom")
|
||||
else
|
||||
.add-card-wrapper
|
||||
a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="spinnerList")
|
||||
.sk-spinner.sk-spinner-list(
|
||||
|
|
@ -43,33 +44,30 @@ template(name="spinnerList")
|
|||
|
||||
template(name="addCardForm")
|
||||
.minicard.minicard-composer.js-composer
|
||||
if getLabels
|
||||
.minicard-labels
|
||||
each getLabels
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
textarea.minicard-composer-textarea.js-card-title(autofocus dir="auto")
|
||||
if members.get
|
||||
.minicard-members.js-minicard-composer-members
|
||||
each members.get
|
||||
+userAvatar(userId=this)
|
||||
.minicard-bottom
|
||||
.minicard-composer-icons
|
||||
if getLabels
|
||||
each getLabels
|
||||
.minicard-label(class="card-label-{{color}}" title="{{name}}")
|
||||
if members.get
|
||||
each members.get
|
||||
+userAvatar(userId=this)
|
||||
.add-controls.clearfix
|
||||
a.js-close-inlined-form
|
||||
i.fa.fa-times-thin
|
||||
|
||||
.add-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
a.js-close-inlined-form
|
||||
i.fa.fa-times-thin
|
||||
.add-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
|
||||
.links-controls.clearfix
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-link {{_ 'link'}}
|
||||
span.quiet
|
||||
|
|
||||
| /
|
||||
a.js-search {{_ 'search'}}
|
||||
span.quiet
|
||||
|
|
||||
| /
|
||||
a.js-card-template {{_ 'template'}}
|
||||
|
||||
template(name="autocompleteLabelLine")
|
||||
|
|
@ -77,70 +75,73 @@ template(name="autocompleteLabelLine")
|
|||
span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
|
||||
|
||||
template(name="linkCardPopup")
|
||||
label {{_ 'boards'}}:
|
||||
.link-board-wrapper
|
||||
select.js-select-boards
|
||||
option(value="")
|
||||
each boards
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}")
|
||||
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each swimlanes
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each lists
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each cards
|
||||
option(value="{{getRealId}}") {{getTitle}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
|
||||
|
||||
template(name="searchElementPopup")
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required dir="auto")
|
||||
unless isTemplateSearch
|
||||
label {{_ 'boards'}}:
|
||||
.link-board-wrapper
|
||||
.link-board-dropdown
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards
|
||||
option(value="")
|
||||
each boards
|
||||
option(value="{{_id}}") {{title}}
|
||||
form.js-search-term-form
|
||||
label
|
||||
| {{_ 'template'}}
|
||||
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
|
||||
.list-body.search-card-results
|
||||
.minicards.clearfix.js-minicards
|
||||
if isBoardTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+miniboard(this)
|
||||
if isListTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minilist(this)
|
||||
if isSwimlaneTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+miniswimlane(this)
|
||||
if isCardTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
unless isTemplateSearch
|
||||
each results
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}")
|
||||
|
||||
.link-board-dropdown
|
||||
label {{_ 'swimlanes'}}:
|
||||
select.js-select-swimlanes
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each swimlanes
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
.link-board-dropdown
|
||||
label {{_ 'lists'}}:
|
||||
select.js-select-lists
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each lists
|
||||
option(value="{{_id}}") {{isTitleDefault title}}
|
||||
|
||||
.link-board-dropdown
|
||||
label {{_ 'cards'}}:
|
||||
select.js-select-cards
|
||||
option(value="") {{_ 'custom-field-dropdown-none'}}
|
||||
each cards
|
||||
option(value="{{getRealId}}") {{getTitle}}
|
||||
|
||||
.edit-controls.clearfix
|
||||
input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
|
||||
|
||||
template(name="searchElementPopup")
|
||||
.link-board-wrapper
|
||||
form
|
||||
label
|
||||
| {{_ 'title'}}
|
||||
input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required dir="auto")
|
||||
unless isTemplateSearch
|
||||
label {{_ 'boards'}}:
|
||||
select.js-select-boards
|
||||
option(value="")
|
||||
each (boards)
|
||||
option(value="{{_id}}") {{title}}
|
||||
form.js-search-term-form
|
||||
label
|
||||
| {{_ 'template'}}
|
||||
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
|
||||
.list-body.search-card-results
|
||||
.minicards.clearfix.js-minicards
|
||||
if isBoardTemplateSearch
|
||||
each (results)
|
||||
a.minicard-wrapper.js-minicard
|
||||
+miniboard(this)
|
||||
if isListTemplateSearch
|
||||
each (results)
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minilist(this)
|
||||
if isSwimlaneTemplateSearch
|
||||
each (results)
|
||||
a.minicard-wrapper.js-minicard
|
||||
+miniswimlane(this)
|
||||
if isCardTemplateSearch
|
||||
each (results)
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
unless isTemplateSearch
|
||||
each (results)
|
||||
a.minicard-wrapper.js-minicard
|
||||
+minicard(this)
|
||||
|
|
|
|||
|
|
@ -3,16 +3,168 @@ import { TAPi18n } from '/imports/i18n';
|
|||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import { Spinner } from '/client/lib/spinner';
|
||||
import getSlug from 'limax';
|
||||
import { itemsSelector } from './list';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const InfiniteScrollIter = 10;
|
||||
|
||||
|
||||
function sortableCards(boardComponent, $cards) {
|
||||
return {
|
||||
connectWith: '.js-minicards:not(.js-list-full)',
|
||||
tolerance: 'pointer',
|
||||
appendTo: '.board-canvas',
|
||||
helper(evt, item) {
|
||||
const helper = item.clone();
|
||||
const cardHeight = item.height();
|
||||
const cardWidth = item.width();
|
||||
helper[0].setAttribute('style', `height: ${cardHeight}px !important; width: ${cardWidth}px !important;`);
|
||||
|
||||
if (MultiSelection.isActive()) {
|
||||
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
|
||||
if (andNOthers > 0) {
|
||||
helper.append(
|
||||
$(
|
||||
Blaze.toHTML(
|
||||
HTML.DIV(
|
||||
{ class: 'and-n-other' },
|
||||
TAPi18n.__('and-n-other-card', { count: andNOthers }),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return helper;
|
||||
},
|
||||
distance: 7,
|
||||
items: itemsSelector,
|
||||
placeholder: 'minicard-wrapper placeholder',
|
||||
/* cursor must be tied to smaller objects, position approximately from the button
|
||||
(can be computed if visually confusing) */
|
||||
cursorAt: { right: 20, top: 30 },
|
||||
start(evt, ui) {
|
||||
const cardHeight = ui.helper.height();
|
||||
ui.placeholder[0].setAttribute('style', `height: ${cardHeight}px !important;`);
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
boardComponent.setIsDragging(true);
|
||||
},
|
||||
stop(evt, ui) {
|
||||
// To attribute the new index number, we need to get the DOM element
|
||||
// of the previous and the following card -- if any.
|
||||
const prevCardDom = ui.item.prev('.js-minicard').get(0);
|
||||
const nextCardDom = ui.item.next('.js-minicard').get(0);
|
||||
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
|
||||
const sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards);
|
||||
const listId = Blaze.getData(ui.item.parents('.list-body').get(0))._id;
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
|
||||
let targetSwimlaneId = null;
|
||||
|
||||
// only set a new swimelane ID if the swimlanes view is active
|
||||
if (
|
||||
Utils.boardView() === 'board-view-swimlanes' ||
|
||||
currentBoard.isTemplatesBoard()
|
||||
)
|
||||
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
|
||||
._id;
|
||||
|
||||
// Normally the jquery-ui sortable library moves the dragged DOM element
|
||||
// to its new position, which disrupts Blaze reactive updates mechanism
|
||||
// (especially when we move the last card of a list, or when multiple
|
||||
// users move some cards at the same time). To prevent these UX glitches
|
||||
// we ask sortable to gracefully cancel the move, and to put back the
|
||||
// DOM in its initial state. The card move is then handled reactively by
|
||||
// Blaze with the below query.
|
||||
$cards.sortable('cancel');
|
||||
|
||||
if (MultiSelection.isActive()) {
|
||||
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(
|
||||
currentBoard._id,
|
||||
newSwimlaneId,
|
||||
listId,
|
||||
sortIndex.base + i * sortIndex.increment,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const cardDomElement = ui.item.get(0);
|
||||
const card = Blaze.getData(cardDomElement);
|
||||
const newSwimlaneId = targetSwimlaneId
|
||||
? targetSwimlaneId
|
||||
: card.swimlaneId || defaultSwimlaneId;
|
||||
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
|
||||
}
|
||||
boardComponent.setIsDragging(false);
|
||||
},
|
||||
sort(event, ui) {
|
||||
Utils.scrollIfNeeded(event);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
// for infinite scrolling
|
||||
this.cardlimit = new ReactiveVar(InfiniteScrollIter);
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
// Prefer handling drag/sort in listBody rather than list as
|
||||
// it is shared between mobile and desktop view
|
||||
const boardComponent = BlazeComponent.getComponentForElement(document.getElementsByClassName('board-canvas')[0]);
|
||||
const $cards = this.$('.js-minicards');
|
||||
$cards.sortable(sortableCards(boardComponent, $cards));
|
||||
|
||||
this.autorun(() => {
|
||||
if ($cards.data('uiSortable') || $cards.data('sortable')) {
|
||||
// Use handle button on mobile, classic move otherwise
|
||||
if (Utils.isMiniScreen()) {
|
||||
$cards.sortable('option', 'handle', '.handle');
|
||||
} else {
|
||||
$cards.sortable('option', 'handle', '.minicard');
|
||||
}
|
||||
|
||||
$cards.sortable(
|
||||
'option',
|
||||
'disabled',
|
||||
// Disable drag-dropping when user is not member
|
||||
!Utils.canModifyBoard(),
|
||||
// Not disable drag-dropping while in multi-selection mode
|
||||
// MultiSelection.isActive() || !Utils.canModifyBoard(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// We want to re-run this function any time a card is added.
|
||||
this.autorun(() => {
|
||||
const currentBoardId = Tracker.nonreactive(() => {
|
||||
return Session.get('currentBoard');
|
||||
});
|
||||
Tracker.afterFlush(() => {
|
||||
$cards.find(itemsSelector).droppable({
|
||||
hoverClass: 'draggable-hover-card',
|
||||
accept: '.js-member,.js-label',
|
||||
drop(event, ui) {
|
||||
const cardId = Blaze.getData(this)._id;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
|
||||
if (ui.draggable.hasClass('js-member')) {
|
||||
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
|
||||
card.assignMember(memberId);
|
||||
} else {
|
||||
const labelId = Blaze.getData(ui.draggable.get(0))._id;
|
||||
card.addLabel(labelId);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
mixins() {
|
||||
return [];
|
||||
},
|
||||
|
|
@ -82,9 +234,10 @@ BlazeComponent.extendComponent({
|
|||
evt.preventDefault();
|
||||
const firstCardDom = this.find('.js-minicard:first');
|
||||
const lastCardDom = this.find('.js-minicard:last');
|
||||
const textarea = $(evt.currentTarget).find('textarea');
|
||||
// more robust to start from the form
|
||||
const textarea = $(evt.currentTarget).closest('.inlined-form').find('textarea');
|
||||
const position = this.currentData().position;
|
||||
const title = textarea.val().trim();
|
||||
const title = $(textarea).val().trim();
|
||||
|
||||
let sortIndex;
|
||||
if (position === 'top') {
|
||||
|
|
@ -168,7 +321,6 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// We keep the form opened, empty it, and scroll to it.
|
||||
textarea.val('').focus();
|
||||
autosize.update(textarea);
|
||||
if (position === 'bottom') {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
|
@ -194,21 +346,19 @@ BlazeComponent.extendComponent({
|
|||
|
||||
clickOnMiniCard(evt) {
|
||||
if (MultiSelection.isActive() || evt.shiftKey) {
|
||||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
|
||||
MultiSelection[methodName](this.currentData()._id);
|
||||
|
||||
// If the card is already selected, we want to de-select it.
|
||||
// XXX We should probably modify the minicard href attribute instead of
|
||||
// overwriting the event in case the card is already selected.
|
||||
} else if (Utils.isMiniScreen()) {
|
||||
evt.preventDefault();
|
||||
Session.set('popupCardId', this.currentData()._id);
|
||||
this.cardDetailsPopup(evt);
|
||||
} else if (Session.equals('currentCard', this.currentData()._id)) {
|
||||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
// We need to wait a little because router gets called first,
|
||||
// we probably need a level of indirection
|
||||
// #FIXME remove if it works with commits we rebased on,
|
||||
// which change the route declaration order
|
||||
Meteor.setTimeout(() => {
|
||||
Session.set('currentCard', null)
|
||||
}, 50);
|
||||
Utils.goBoardId(Session.get('currentBoard'));
|
||||
} else {
|
||||
// Allow normal href navigation, but if it's the same card URL,
|
||||
|
|
@ -283,12 +433,6 @@ BlazeComponent.extendComponent({
|
|||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
cardDetailsPopup(event) {
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -296,6 +440,8 @@ BlazeComponent.extendComponent({
|
|||
'click .js-toggle-multi-selection': this.toggleMultiSelection,
|
||||
'click .open-minicard-composer': this.scrollToBottom,
|
||||
submit: this.addCard,
|
||||
// #FIXME remove in final MR if it works
|
||||
'click .confirm': this.addCard
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
@ -401,6 +547,17 @@ BlazeComponent.extendComponent({
|
|||
'click .js-link': Popup.open('linkCard'),
|
||||
'click .js-search': Popup.open('searchElement'),
|
||||
'click .js-card-template': Popup.open('searchElement'),
|
||||
submit: this.addCard,
|
||||
'click .minicard-label': (event) => {
|
||||
const clickedData = BlazeComponent.getComponentForElement(event.target).currentData?.()
|
||||
this.labels.set(this.labels.get().filter(e => e !== clickedData?._id));
|
||||
},
|
||||
'click .member': (event) => {
|
||||
const clickedData = BlazeComponent.getComponentForElement(event.target).currentData?.()
|
||||
this.members.set(this.members.get().filter(e => e !== clickedData?.userId));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
@ -409,8 +566,6 @@ BlazeComponent.extendComponent({
|
|||
const editor = this;
|
||||
const $textarea = this.$('textarea');
|
||||
|
||||
autosize($textarea);
|
||||
|
||||
$textarea.escapeableTextComplete(
|
||||
[
|
||||
// User mentions
|
||||
|
|
@ -421,7 +576,9 @@ BlazeComponent.extendComponent({
|
|||
callback(
|
||||
$.map(currentBoard.activeMembers(), member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
return user.username.indexOf(term) === 0 ? user : null;
|
||||
return user.username.indexOf(term) === 0 &&
|
||||
// don't show already selected members
|
||||
!editor.members.get().find((e) => e === member.userId) ? user : null;
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
@ -445,8 +602,12 @@ BlazeComponent.extendComponent({
|
|||
const currentBoard = Utils.getCurrentBoard();
|
||||
callback(
|
||||
$.map(currentBoard.labels, label => {
|
||||
if (label.name == undefined) {
|
||||
label.name = "";
|
||||
if (
|
||||
label.name == undefined ||
|
||||
// don't show already selected labels
|
||||
editor.getLabels().find((e) => e._id === label._id)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
label.name.indexOf(term) > -1 ||
|
||||
|
|
@ -503,10 +664,10 @@ BlazeComponent.extendComponent({
|
|||
subManager.subscribe('board', this.boardId, false);
|
||||
this.board = ReactiveCache.getBoard(this.boardId);
|
||||
// List where to insert card
|
||||
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
|
||||
this.list = $(PopupComponent.stack[0].openerElement).closest('.js-list');
|
||||
this.listId = Blaze.getData(this.list[0])._id;
|
||||
// Swimlane where to insert card
|
||||
const swimlane = $(Popup._getTopStack().openerElement).closest(
|
||||
const swimlane = $(PopupComponent.stack[0].openerElement).closest(
|
||||
'.js-swimlane',
|
||||
);
|
||||
this.swimlaneId = '';
|
||||
|
|
@ -539,10 +700,10 @@ BlazeComponent.extendComponent({
|
|||
if (!board) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// Ensure default swimlane exists
|
||||
board.getDefaultSwimline();
|
||||
|
||||
|
||||
const swimlanes = ReactiveCache.getSwimlanes(
|
||||
{
|
||||
boardId: this.selectedBoardId.get()
|
||||
|
|
@ -559,7 +720,8 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
const lists = ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.selectedBoardId.get()
|
||||
boardId: this.selectedBoardId.get(),
|
||||
swimlaneId: this.selectedSwimlaneId?.get?.()
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 },
|
||||
|
|
@ -703,16 +865,16 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
onCreated() {
|
||||
this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
|
||||
this.isCardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
|
||||
'js-card-template',
|
||||
);
|
||||
this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
|
||||
this.isListTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
|
||||
'js-list-template',
|
||||
);
|
||||
this.isSwimlaneTemplateSearch = $(
|
||||
Popup._getTopStack().openerElement,
|
||||
PopupComponent.stack[0].openerElement,
|
||||
).hasClass('js-open-add-swimlane-menu');
|
||||
this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
|
||||
this.isBoardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
|
||||
'js-add-board',
|
||||
);
|
||||
this.isTemplateSearch =
|
||||
|
|
@ -731,20 +893,16 @@ BlazeComponent.extendComponent({
|
|||
} else {
|
||||
this.board = Utils.getCurrentBoard();
|
||||
}
|
||||
if (!this.board) {
|
||||
Popup.back();
|
||||
return;
|
||||
}
|
||||
this.boardId = this.board._id;
|
||||
this.boardId = this.board?._id;
|
||||
// Subscribe to this board
|
||||
subManager.subscribe('board', this.boardId, false);
|
||||
this.selectedBoardId = new ReactiveVar(this.boardId);
|
||||
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
|
||||
|
||||
if (!this.isBoardTemplateSearch) {
|
||||
this.list = $(PopupComponent.stack[0].openerElement).closest('.js-list');
|
||||
this.swimlaneId = '';
|
||||
// Swimlane where to insert card
|
||||
const swimlane = $(Popup._getTopStack().openerElement).parents(
|
||||
const swimlane = $(PopupComponent.stack[0].openerElement).parents(
|
||||
'.js-swimlane',
|
||||
);
|
||||
if (Utils.boardView() === 'board-view-swimlanes')
|
||||
|
|
@ -783,11 +941,7 @@ BlazeComponent.extendComponent({
|
|||
} else if (this.isSwimlaneTemplateSearch) {
|
||||
return board.searchSwimlanes(this.term.get());
|
||||
} else if (this.isBoardTemplateSearch) {
|
||||
const boards = board.searchBoards(this.term.get());
|
||||
boards.forEach(board => {
|
||||
subManager.subscribe('board', board.linkedId, false);
|
||||
});
|
||||
return boards;
|
||||
return board.searchBoards(this.term.get());
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,66 +9,68 @@ template(name="listHeader")
|
|||
if currentList
|
||||
a.list-header-left-icon.js-unselect-list
|
||||
i.fa.fa-caret-left
|
||||
else
|
||||
if collapsed
|
||||
if showCardsCountForList cards.length
|
||||
br
|
||||
span.cardCount {{cardsCount}}
|
||||
if isMiniScreen
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
else
|
||||
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
|
||||
if collapsed
|
||||
else
|
||||
//- start by this on mobile to have cohesion with other views
|
||||
a.list-header-menu-icon.js-select-list
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}")
|
||||
.list-header-name-container
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
else
|
||||
div.list-header-name-container
|
||||
unless isMiniScreen
|
||||
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
|
||||
if collapsed
|
||||
i.fa.fa-caret-right
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}").list-header-wrap
|
||||
h2.list-header-name(
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
unless collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
if isMiniScreen
|
||||
if currentList
|
||||
if isWatching
|
||||
i.list-header-watch-icon i.fa.fa-eye
|
||||
i.list-header-watch-icon.i.fa.fa-eye
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
else
|
||||
a.list-header-menu-icon.js-select-list
|
||||
i.fa.fa-caret-right
|
||||
unless currentUser.isWorker
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
if isMiniScreen
|
||||
a.list-header-handle.handle.js-list-handle
|
||||
i.fa.fa-arrows
|
||||
else if currentUser.isBoardMember
|
||||
|
|
@ -77,24 +79,13 @@ template(name="listHeader")
|
|||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
if isMiniScreen
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
i.fa.fa-arrows
|
||||
unless collapsed
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
//if isBoardAdmin
|
||||
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
i.fa.fa-arrows
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
|
||||
i.fa.fa-bars
|
||||
unless isMiniScreen
|
||||
if collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}}
|
||||
|
||||
template(name="editListTitleForm")
|
||||
.list-composer
|
||||
|
|
@ -224,14 +215,14 @@ template(name="wipLimitErrorPopup")
|
|||
.wip-limit-invalid
|
||||
p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
|
||||
p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
|
||||
button.full.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
button.negate.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
|
||||
template(name="setListWidthPopup")
|
||||
#js-list-width-edit
|
||||
label {{_ 'set-list-width-value'}}
|
||||
p
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="270")
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="100")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="100")
|
||||
input.list-width-apply(type="submit" value="{{_ 'apply'}}")
|
||||
input.list-width-error
|
||||
br
|
||||
|
|
@ -242,8 +233,8 @@ template(name="setListWidthPopup")
|
|||
|
||||
template(name="listWidthErrorPopup")
|
||||
.list-width-invalid
|
||||
p {{_ 'list-width-error-message'}} '>=270'
|
||||
button.full.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
p {{_ 'list-width-error-message'}} '>=100'
|
||||
button.negate.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
|
||||
template(name="setListColorPopup")
|
||||
form.edit-label
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@ Meteor.startup(() => {
|
|||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
/* #FIXME I have no idea why this exact same
|
||||
event won't fire when in event maps */
|
||||
$(this.find('.js-collapse')).on('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.collapsed(!this.collapsed());
|
||||
});
|
||||
},
|
||||
|
||||
canSeeAddCard() {
|
||||
const list = Template.currentData();
|
||||
return (
|
||||
|
|
@ -34,7 +43,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
collapsed(check = undefined) {
|
||||
const list = Template.currentData();
|
||||
const list = this.data();
|
||||
const status = Utils.getListCollapseState(list);
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
|
|
@ -110,7 +119,11 @@ BlazeComponent.extendComponent({
|
|||
return TAPi18n.__('cards-count');
|
||||
}
|
||||
},
|
||||
|
||||
currentList() {
|
||||
const currentList = Utils.getCurrentList();
|
||||
const list = Template.currentData();
|
||||
return currentList && currentList._id == list._id;
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -118,10 +131,6 @@ BlazeComponent.extendComponent({
|
|||
event.preventDefault();
|
||||
this.starred(!this.starred());
|
||||
},
|
||||
'click .js-collapse'(event) {
|
||||
event.preventDefault();
|
||||
this.collapsed(!this.collapsed());
|
||||
},
|
||||
'click .js-open-list-menu': Popup.open('listAction'),
|
||||
'click .js-add-card.list-header-plus-top'(event) {
|
||||
const listDom = $(event.target).parents(
|
||||
|
|
@ -459,10 +468,10 @@ BlazeComponent.extendComponent({
|
|||
this.currentBoard = Utils.getCurrentBoard();
|
||||
this.currentSwimlaneId = new ReactiveVar(null);
|
||||
this.currentListId = new ReactiveVar(null);
|
||||
|
||||
|
||||
// Get the swimlane context from opener
|
||||
const openerData = Popup.getOpenerComponent()?.data();
|
||||
|
||||
|
||||
// If opened from swimlane menu, openerData is the swimlane
|
||||
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
|
||||
this.currentSwimlane = openerData;
|
||||
|
|
@ -554,4 +563,3 @@ BlazeComponent.extendComponent({
|
|||
];
|
||||
},
|
||||
}).register('addListPopup');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue