Merge branch 'main' into updates

This commit is contained in:
Khaoula Maleh 2026-02-11 15:13:00 -11:00 committed by GitHub
commit 1870c256b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
306 changed files with 12406 additions and 2587 deletions

View file

@ -170,14 +170,14 @@
width: 95%;
margin: 20px;
}
.board-conversion-header,
.board-conversion-content,
.board-conversion-footer {
padding-left: 16px;
padding-right: 16px;
}
.board-conversion-header h3 {
font-size: 18px;
}

View file

@ -6,21 +6,21 @@ template(name="boardConversionProgress")
i.fa.fa-cog
| {{_ 'converting-board'}}
p {{_ 'converting-board-description'}}
.board-conversion-content
.conversion-progress
.progress-bar
.progress-fill(style="width: {{conversionProgress}}%")
.progress-text {{conversionProgress}}%
.conversion-status
i.fa.fa-cog
| {{conversionStatus}}
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
i.fa.fa-clock-o
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
.board-conversion-footer
.conversion-info
i.fa.fa-info-circle

View file

@ -1,6 +1,6 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import {
import {
boardConverter,
isConverting,
conversionProgress,
@ -12,15 +12,15 @@ Template.boardConversionProgress.helpers({
isConverting() {
return isConverting.get();
},
conversionProgress() {
return conversionProgress.get();
},
conversionStatus() {
return conversionStatus.get();
},
conversionEstimatedTime() {
return conversionEstimatedTime.get();
}

View file

@ -293,7 +293,7 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
.board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
/* In desktop mode on small screens, still keep overlay behind card */
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;

View file

@ -1,5 +1,5 @@
template(name="board")
if isConverting.get
+boardConversionProgress
else if isBoardReady.get

View file

@ -242,6 +242,16 @@ BlazeComponent.extendComponent({
setTimeout(function () {
focusFirstInteractive(node);
}, 10);
const popupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 &&
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
!node.closest('.js-swimlanes') &&
!node.closest('.swimlane') &&
!node.closest('.list') &&
!node.closest('.minicard')) {
setTimeout(function() { focusFirstInteractive(node); }, 10);
}
});
});

View file

@ -93,9 +93,12 @@ template(name="boardHeaderBar")
i.fa.fa-archive
//if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
// i.fa(class="{{directionClass}}")
// span {{_ 'sort'}}{{_ listSortShortDesc}}
//
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
//
i.fa(class="{{directionClass}}")
//
span {{_ 'sort'}}{{_ listSortShortDesc}}
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
@ -314,18 +317,30 @@ template(name="createTemplateContainerPopup")
a.js-board-template {{_ 'template'}}
//template(name="listsortPopup")
// h2
// | {{_ 'list-sort-by'}}
// hr
// ul.pop-over-list
// each value in allowedSortValues
// li
// a.js-sort-by(name="{{value.name}}")
// if $eq sortby value.name
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
// | {{_ value.label }}{{_ value.shortLabel}}
// if $eq sortby value.name
// i.fa.fa-check
//
h2
//
| {{_ 'list-sort-by'}}
//
hr
//
ul.pop-over-list
//
each value in allowedSortValues
//
li
//
a.js-sort-by(name="{{value.name}}")
//
if $eq sortby value.name
//
| {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
//
| {{_ value.label }}{{_ value.shortLabel}}
//
if $eq sortby value.name
//
i.fa.fa-check
template(name="boardChangeTitlePopup")
form
label

View file

@ -182,7 +182,7 @@ Template.boardHeaderBar.helpers({
if (!sortBy) {
return '🃏'; // Card icon when nothing is selected
}
// Determine which sort option is active based on sortBy object
if (sortBy.dueAt) {
return '📅'; // Due date icon
@ -191,7 +191,7 @@ Template.boardHeaderBar.helpers({
} else if (sortBy.createdAt) {
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
}
return '🃏'; // Default card icon
},
});

View file

@ -133,7 +133,7 @@ template(name="boardList")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon
i.fa.fa-arrows
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'template-container'}}")
@ -161,7 +161,7 @@ template(name="boardList")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon
i.fa.fa-arrows
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
@ -197,12 +197,18 @@ template(name="boardList")
template(name="boardListHeaderBar")
h1 {{_ title }}
//.board-header-btns.right
// a.board-header-btn.js-open-archived-board
// i.fa.fa-archive
// span {{_ 'archives'}}
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
// i.fa.fa-clone
// span {{_ 'templates'}}
//
a.board-header-btn.js-open-archived-board
//
i.fa.fa-archive
//
span {{_ 'archives'}}
//
a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
//
i.fa.fa-clone
//
span {{_ 'templates'}}
// Recursive template for workspaces tree
template(name="workspaceTree")
@ -214,7 +220,7 @@ template(name="workspaceTree")
span.workspace-drag-handle
span.emoji-icon
i.fa.fa-arrows
a.js-select-workspace(data-id="{{id}}")
span.workspace-icon
if icon

View file

@ -164,32 +164,32 @@
margin: 5px 0;
padding: 10px;
}
.original-positions-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.original-positions-header .btn {
justify-content: center;
}
.original-positions-filters .btn-group {
justify-content: center;
}
.original-position-item-header {
flex-wrap: wrap;
gap: 6px;
}
.entity-name {
flex: 1;
min-width: 0;
word-break: break-word;
}
.original-position-item-details {
margin-left: 0;
margin-top: 8px;
@ -203,60 +203,60 @@
border-color: #4a5568;
color: #e2e8f0;
}
.original-positions-content {
background-color: #1a202c;
border-color: #4a5568;
}
.original-position-item {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-item:hover {
background-color: #4a5568;
border-color: #718096;
}
.original-position-item-header {
color: #e2e8f0;
}
.original-position-item-header i {
color: #a0aec0;
}
.entity-name {
color: #e2e8f0;
}
.entity-id {
color: #a0aec0;
}
.original-position-description {
color: #e2e8f0;
}
.original-title {
background-color: #4a5568;
color: #a0aec0;
}
.original-title strong {
color: #e2e8f0;
}
.original-position-date {
color: #a0aec0;
}
.no-original-positions {
color: #a0aec0;
}
.no-original-positions i {
color: #718096;
}

View file

@ -5,7 +5,7 @@
<i class="fa fa-history"></i>
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
</button>
{{#if isShowingOriginalPositions}}
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
<i class="fa fa-refresh"></i> Refresh
@ -22,22 +22,22 @@
{{else}}
<div class="original-positions-filters">
<div class="btn-group btn-group-sm" role="group">
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'all'}}">
All
</button>
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'swimlane'}}">
<i class="fa fa-bars"></i> Swimlanes
</button>
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'list'}}">
<i class="fa fa-columns"></i> Lists
</button>
<button type="button"
<button type="button"
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
onclick="{{setFilterType 'card'}}">
<i class="fa fa-sticky-note"></i> Cards

View file

@ -26,7 +26,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
if (!boardId) return;
this.isLoading.set(true);
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
this.isLoading.set(false);
if (error) {
@ -57,11 +57,11 @@ class OriginalPositionsViewComponent extends BlazeComponent {
getFilteredHistory() {
const history = this.getBoardHistory();
const filterType = this.filterType.get();
if (filterType === 'all') {
return history;
}
return history.filter(item => item.entityType === filterType);
}
@ -93,7 +93,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
getEntityOriginalPositionDescription(entity) {
const position = entity.originalPosition || {};
let description = `Position: ${position.sort || 0}`;
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
description += ` in swimlane ${entity.originalSwimlaneId}`;
} else if (entity.entityType === 'card') {
@ -104,7 +104,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
description += ` in list ${entity.originalListId}`;
}
}
return description;
}

View file

@ -1,26 +1,26 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import {
formatDateTime,
formatDate,
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import { CustomFieldStringTemplate } from '/client/lib/customFields'

View file

@ -1,24 +1,24 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar,
diff
} from '/imports/lib/dateUtils';
@ -143,7 +143,7 @@ class CardReceivedDate extends CardDate {
const startAt = this.data().getStart();
const theDate = this.date.get();
const now = this.now.get();
// Received date logic: if received date is after start, due, or end dates, it's overdue
if (
(startAt && isAfter(theDate, startAt)) ||
@ -187,7 +187,7 @@ class CardStartDate extends CardDate {
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// Start date logic: if start date is after due or end dates, it's overdue
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
@ -230,7 +230,7 @@ class CardDueDate extends CardDate {
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// If there's an end date and it's before the due date, task is completed early
if (endAt && isBefore(endAt, theDate)) {
classes += 'completed-early';
@ -242,7 +242,7 @@ class CardDueDate extends CardDate {
// Due date logic based on current time
else {
const daysDiff = diff(theDate, now, 'days');
if (daysDiff < 0) {
// Due date is in the past - overdue
classes += 'overdue';
@ -254,7 +254,7 @@ class CardDueDate extends CardDate {
classes += 'not-due';
}
}
return classes;
}
@ -286,7 +286,7 @@ class CardEndDate extends CardDate {
let classes = 'end-date ';
const dueAt = this.data().getDue();
const theDate = this.date.get();
if (!dueAt) {
// No due date set - just show as completed
classes += 'completed';
@ -371,7 +371,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardReceivedDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -383,7 +383,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardStartDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -395,7 +395,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardDueDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -407,7 +407,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardEndDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -419,7 +419,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardCustomFieldDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';

View file

@ -428,7 +428,7 @@ body.desktop-mode .card-details.card-details-collapsed {
position: fixed;
resize: both;
}
/* Override for mobile mode even on larger screens */
body.mobile-mode .card-details {
width: 100vw !important;
@ -440,7 +440,7 @@ body.desktop-mode .card-details.card-details-collapsed {
max-height: 100vh !important;
resize: none !important;
}
.card-details-maximized {
padding: 0;
flex-shrink: 0;
@ -510,7 +510,7 @@ input[type="submit"].attachment-add-link-submit {
border-radius: 0 !important;
box-shadow: none !important;
}
/* Ensure card details are above everything on mobile */
body.mobile-mode .card-details {
z-index: 100 !important;

View file

@ -351,7 +351,8 @@ template(name="cardDetails")
.card-label.card-label-green {{ voteCountPositive }}
.card-label.card-label-red {{ voteCountNegative }}
unless ($and currentBoard.isPublic voteAllowNonBoardMembers )
.card-label.card-label-gray {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
.card-label.card-label-gray
| {{ voteCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
+viewer
= getVoteQuestion
if showVotingButtons
@ -377,7 +378,8 @@ template(name="cardDetails")
.poker-result
if expiredPoker
unless ($and currentBoard.isPublic pokerAllowNonBoardMembers )
.card-label.card-label-gray {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
.card-label.card-label-gray
| {{ pokerCount }} {{_ 'r-of' }} {{ currentBoard.activeMembers.length }}
if showPlanningPokerButtons
.poker-result
.poker-deck
@ -847,27 +849,111 @@ template(name="exportCardPopup")
| {{_ 'export-card-pdf'}}
template(name="moveCardPopup")
+copyAndMoveCard
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="copyCardPopup")
label(for='copy-card-title') {{_ 'title'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
= getTitle
+copyAndMoveCard
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="copyManyCardsPopup")
label(for='copy-checklist-cards-title') {{_ 'copyManyCardsPopup-instructions'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
| {{_ 'copyManyCardsPopup-format'}}
+copyAndMoveCard
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
each swimlanes
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
div
input(type="radio" name="position" value="above" checked id="position-above" style="display: inline")
label(for="position-above") {{_ 'above-selected-card'}}
div
input(type="radio" name="position" value="below" id="position-below" style="display: inline")
label(for="position-below") {{_ 'below-selected-card'}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="convertChecklistItemToCardPopup")
label(for='convert-checklist-item-to-card-title') {{_ 'title'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)
= item.title
+copyAndMoveCard
template(name="copyAndMoveCard")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
@ -925,7 +1011,8 @@ template(name="cardAssigneesPopup")
if userData.username
| (#{userData.username})
if isCardAssignee
i.fa.fa-check if currentUser.isWorker
i.fa.fa-check
if currentUser.isWorker
ul.pop-over-list.js-card-assignee-list
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
a.name.js-select-assignee(href="#")

View file

@ -2,25 +2,25 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
} from '/imports/lib/dateUtils';
import Cards from '/models/cards';
import Boards from '/models/boards';
@ -337,7 +337,7 @@ BlazeComponent.extendComponent({
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
@ -346,12 +346,12 @@ BlazeComponent.extendComponent({
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
},
@ -361,14 +361,14 @@ BlazeComponent.extendComponent({
if (event.target.tagName === 'A' || $(event.target).closest('a').length > 0) {
return; // Don't drag if clicking on links
}
event.preventDefault();
const $card = $(event.target).closest('.card-details');
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
@ -377,12 +377,12 @@ BlazeComponent.extendComponent({
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
},
@ -1030,7 +1030,8 @@ Template.editCardAssignerForm.events({
}
} else {
// If no card selected, move to end
sortIndex = card.getMaxSort(options.listId, options.swimlaneId) + 1;
const maxSort = card.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await card.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
@ -1073,7 +1074,8 @@ Template.editCardAssignerForm.events({
}
} else {
// If no card selected, copy to end
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
@ -1125,7 +1127,8 @@ Template.editCardAssignerForm.events({
}
}
} else {
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
@ -1170,7 +1173,8 @@ Template.editCardAssignerForm.events({
}
}
} else {
sortIndex = newCard.getMaxSort(options.listId, options.swimlaneId) + 1;
const maxSort = newCard.getMaxSort(options.listId, options.swimlaneId);
sortIndex = maxSort !== null ? maxSort + 1 : 0;
}
await newCard.move(options.boardId, options.swimlaneId, options.listId, sortIndex);
@ -1469,13 +1473,13 @@ BlazeComponent.extendComponent({
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
@ -1721,13 +1725,13 @@ BlazeComponent.extendComponent({
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);

View file

@ -4,7 +4,8 @@ template(name="checklists")
i.fa.fa-check
| {{_ 'checklists'}}
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId
position="top")
+addChecklistItemForm
else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")

View file

@ -157,7 +157,7 @@ BlazeComponent.extendComponent({
textarea.focus();
},
deleteItem() {
async deleteItem() {
const checklist = this.currentData().checklist;
const item = this.currentData().item;
if (checklist && item && item._id) {
@ -372,9 +372,9 @@ BlazeComponent.extendComponent({
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
return ret;
}
setDone(cardId, options) {
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
this.data().checklist.move(cardId);
await this.data().checklist.move(cardId);
}
}).register('moveChecklistPopup');
@ -384,8 +384,8 @@ BlazeComponent.extendComponent({
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
return ret;
}
setDone(cardId, options) {
async setDone(cardId, options) {
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
this.data().checklist.copy(cardId);
await this.data().checklist.copy(cardId);
}
}).register('copyChecklistPopup');

View file

@ -44,9 +44,8 @@
}
}
.minicard-details-menu-with-handle {
position: absolute;
right: 0.7vw;
top: 0.7vh;
float: right;
padding-left: 0.7vw;
font-size: clamp(14px, 3vw, 18px);
padding: 0;
z-index: 1;
@ -137,8 +136,8 @@
width: clamp(20px, 2.5vw, 28px);
height: clamp(20px, 2.5vw, 28px);
position: absolute;
right: 3vw;
top: 0.7vh;
right: 0vw;
top: 4vh;
display: none;
z-index: 1;
}
@ -155,7 +154,7 @@
text-align: center;
}
.minicard .minicard-title {
margin-right: 6vw;
margin-right: 1.5vw;
}
.minicard .minicard-title .card-number {
color: #b3b3b3;

View file

@ -46,7 +46,8 @@ template(name="minicard")
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check span {{_ 'upload-completed'}}
i.fa.fa-check
span {{_ 'upload-completed'}}
.minicard-title
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
@ -151,7 +152,8 @@ template(name="minicard")
= ' '
= comments.length
//span.badge-comment.badge-text
//| {{_ 'comment'}}
//|
{{_ 'comment'}}
if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription)

View file

@ -115,7 +115,11 @@ BlazeComponent.extendComponent({
},
'click span.badge-icon.fa.fa-sort, click span.badge-text.check-list-sort' : Popup.open("editCardSortOrder"),
'click .minicard-labels' : this.cardLabelsPopup,
'click .js-open-minicard-details-menu': Popup.open('cardDetailsActions'),
'click .js-open-minicard-details-menu'(event) {
event.preventDefault();
event.stopPropagation();
Popup.open('cardDetailsActions').call(this, event);
},
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// Only prevent default for file drags to avoid interfering with sortable
@ -199,7 +203,7 @@ BlazeComponent.extendComponent({
visibleItems() {
const checklist = this.currentData().checklist || this.currentData();
const items = checklist.items();
return items.filter(item => {
// Hide finished items if hideCheckedChecklistItems is true
if (item.isFinished && checklist.hideCheckedChecklistItems) {
@ -306,35 +310,3 @@ BlazeComponent.extendComponent({
}
}).register('editCardSortOrderPopup');
Template.cardDetailsActionsPopup.events({
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-move-card': Popup.open('moveCard'),
'click .js-copy-card': Popup.open('copyCard'),
'click .js-set-card-color': Popup.open('setCardColor'),
'click .js-add-labels': Popup.open('cardLabels'),
'click .js-link': Popup.open('linkCard'),
'click .js-move-card-to-top'(event) {
event.preventDefault();
const minOrder = this.getMinSort();
this.move(this.boardId, this.swimlaneId, this.listId, minOrder - 1);
Popup.back();
},
async 'click .js-move-card-to-bottom'(event) {
event.preventDefault();
const maxOrder = this.getMaxSort();
await this.move(this.boardId, this.swimlaneId, this.listId, maxOrder + 1);
Popup.back();
},
'click .js-archive': Popup.afterConfirm('cardArchive', async function () {
Popup.close();
await this.archive();
Utils.goBoardId(this.boardId);
}),
'click .js-toggle-watch-card'() {
const currentCard = this;
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
if (!err && ret) Popup.back();
});
},
});

View file

@ -81,11 +81,11 @@
font-size: 11px;
padding: 6px;
}
.original-position-details {
padding: 4px 6px;
}
.original-position-moved,
.original-position-unchanged {
padding: 3px 5px;
@ -99,24 +99,24 @@
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-moved {
background-color: #744210;
border-color: #b7791f;
color: #fbd38d;
}
.original-position-unchanged {
background-color: #22543d;
border-color: #38a169;
color: #9ae6b4;
}
.original-title {
color: #a0aec0;
border-color: #4a5568;
}
.original-title strong {
color: #e2e8f0;
}

View file

@ -15,7 +15,7 @@
<span class="original-position-text">✅ In original position</span>
</div>
{{/if}}
{{#if getOriginalTitle}}
<div class="original-title">
<strong>Original title:</strong> {{getOriginalTitle}}

View file

@ -13,7 +13,7 @@ class OriginalPositionComponent extends BlazeComponent {
this.originalPosition = new ReactiveVar(null);
this.isLoading = new ReactiveVar(false);
this.hasMoved = new ReactiveVar(false);
this.autorun(() => {
const data = this.data();
if (data && data.entityId && data.entityType) {
@ -24,9 +24,9 @@ class OriginalPositionComponent extends BlazeComponent {
loadOriginalPosition(entityId, entityType) {
this.isLoading.set(true);
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
Meteor.call(methodName, entityId, (error, result) => {
this.isLoading.set(false);
if (error) {
@ -34,7 +34,7 @@ class OriginalPositionComponent extends BlazeComponent {
this.originalPosition.set(null);
} else {
this.originalPosition.set(result);
// Check if the entity has moved
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
@ -61,11 +61,11 @@ class OriginalPositionComponent extends BlazeComponent {
getOriginalPositionDescription() {
const position = this.getOriginalPosition();
if (!position) return 'No original position data';
if (position.originalPosition) {
const entityType = this.data().entityType;
let description = `Original position: ${position.originalPosition.sort || 0}`;
if (entityType === 'list' && position.originalSwimlaneId) {
description += ` in swimlane ${position.originalSwimlaneId}`;
} else if (entityType === 'card') {
@ -76,10 +76,10 @@ class OriginalPositionComponent extends BlazeComponent {
description += ` in list ${position.originalListId}`;
}
}
return description;
}
return 'No original position data';
}

View file

@ -347,7 +347,7 @@ BlazeComponent.extendComponent({
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
this.searchResults.set(results);
this.searching.set(false);
if (results.length === 0) {
this.noResults.set(true);
}
@ -358,11 +358,11 @@ BlazeComponent.extendComponent({
{
'keyup .js-search-member-input'(event) {
const query = event.target.value.trim();
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, 300);

View file

@ -198,7 +198,7 @@ body.list-resizing-active * {
.list-header .list-header-plus-top {
position: absolute !important;
top: 5px !important;
right: 10px !important;
right: 30px !important;
z-index: 15 !important;
display: inline-block !important;
padding: 4px !important;
@ -207,7 +207,7 @@ body.list-resizing-active * {
.list-header .list-header-handle-desktop {
position: absolute !important;
top: 5px !important;
right: 40px !important;
right: 80px !important;
z-index: 15 !important;
display: inline-block !important;
cursor: move !important;

View file

@ -198,7 +198,7 @@ BlazeComponent.extendComponent({
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);
@ -223,7 +223,7 @@ BlazeComponent.extendComponent({
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!list) return 550; // Return default constraint if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListConstraintFromStorage(list.boardId, list._id);
@ -260,11 +260,11 @@ BlazeComponent.extendComponent({
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');
@ -275,7 +275,7 @@ BlazeComponent.extendComponent({
}, 100);
return;
}
// Reactively show/hide resize handle based on collapse and auto-width state
this.autorun(() => {
const isAutoWidth = this.autoWidth();
@ -298,16 +298,16 @@ BlazeComponent.extendComponent({
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');
e.preventDefault();
e.stopPropagation();
};
@ -316,11 +316,11 @@ BlazeComponent.extendComponent({
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`);
@ -330,22 +330,22 @@ BlazeComponent.extendComponent({
$list[0].style.setProperty('flex-basis', 'auto');
$list[0].style.setProperty('flex-grow', '0');
$list[0].style.setProperty('flex-shrink', '0');
e.preventDefault();
e.stopPropagation();
};
const stopResize = (e) => {
if (!isResizing) return;
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`);
@ -355,23 +355,23 @@ BlazeComponent.extendComponent({
$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
// Save the new width using the existing system
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
@ -389,32 +389,32 @@ 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();
};
@ -422,19 +422,19 @@ BlazeComponent.extendComponent({
$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);

View file

@ -25,6 +25,13 @@ 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")

View file

@ -539,10 +539,10 @@ BlazeComponent.extendComponent({
if (!board) {
return [];
}
// Ensure default swimlane exists
board.getDefaultSwimline();
const swimlanes = ReactiveCache.getSwimlanes(
{
boardId: this.selectedBoardId.get()

View file

@ -59,6 +59,9 @@ template(name="listHeader")
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
@ -83,7 +86,14 @@ template(name="listHeader")
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
//
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
@ -185,8 +195,10 @@ template(name="listMorePopup")
| {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
//unless currentUser.isWorker
// if currentUser.isBoardAdmin
// a.js-delete {{_ 'delete'}}
//
if currentUser.isBoardAdmin
//
a.js-delete {{_ 'delete'}}
template(name="listDeletePopup")
p {{_ "list-delete-pop"}}

View file

@ -123,6 +123,15 @@ BlazeComponent.extendComponent({
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(
`#js-list-${this.currentData()._id}`,
)[0];
const listComponent = BlazeComponent.getComponentForElement(listDom);
listComponent.openForm({
position: 'top',
});
},
'click .js-unselect-list'() {
Session.set('currentList', null);
},
@ -450,10 +459,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;
@ -497,7 +506,7 @@ BlazeComponent.extendComponent({
let sortIndex = 0;
const boardId = Utils.getCurrentBoardId();
const swimlaneId = this.currentSwimlane?._id;
let swimlaneId = this.currentSwimlane?._id;
const positionInput = this.find('.list-position-input');
@ -507,6 +516,9 @@ BlazeComponent.extendComponent({
if (selectedList) {
sortIndex = selectedList.sort + 1;
// Use the swimlane ID from the selected list to ensure the new list
// is added to the same swimlane as the selected list
swimlaneId = selectedList.swimlaneId;
} else {
// No specific position, add at end of swimlane
if (swimlaneId) {

View file

@ -52,7 +52,8 @@ template(name="dueCardsViewChangePopup")
i.fa.fa-user
| {{_ 'dueCardsViewChange-choice-me'}}
if $eq Utils.dueCardsView "me"
i.fa.fa-check hr
i.fa.fa-check
hr
li
with "dueCardsViewChange-choice-all"
a.js-due-cards-view-all
@ -62,4 +63,4 @@ template(name="dueCardsViewChangePopup")
+viewer
| {{_ 'dueCardsViewChange-choice-all-description' }}
if $eq Utils.dueCardsView "all"
i.fa.fa-check
i.fa.fa-check

View file

@ -92,14 +92,14 @@ BlazeComponent.extendComponent({
class DueCardsComponent extends BlazeComponent {
onCreated() {
super.onCreated();
this._cachedCards = null;
this._cachedTimestamp = null;
this.subscriptionHandle = null;
this.isLoading = new ReactiveVar(true);
this.hasResults = new ReactiveVar(false);
this.searching = new ReactiveVar(false);
// Subscribe to the optimized due cards publication
this.autorun(() => {
const allUsers = this.dueCardsView() === 'all';
@ -107,7 +107,7 @@ class DueCardsComponent extends BlazeComponent {
this.subscriptionHandle.stop();
}
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
// Update loading state based on subscription
this.autorun(() => {
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
@ -162,7 +162,7 @@ class DueCardsComponent extends BlazeComponent {
// Get the translated text and manually replace %s with the count
const baseText = TAPi18n.__('n-cards-found');
const result = baseText.replace('%s', count);
if (process.env.DEBUG === 'true') {
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
}
@ -196,10 +196,10 @@ class DueCardsComponent extends BlazeComponent {
if (process.env.DEBUG === 'true') {
console.log('dueCards client: found', cards.length, 'cards with due dates');
console.log('dueCards client: cards details:', cards.map(c => ({
id: c._id,
title: c.title,
dueAt: c.dueAt,
console.log('dueCards client: cards details:', cards.map(c => ({
id: c._id,
title: c.title,
dueAt: c.dueAt,
boardId: c.boardId,
members: c.members,
assignees: c.assignees,
@ -223,11 +223,11 @@ class DueCardsComponent extends BlazeComponent {
const isAssignee = card.assignees && card.assignees.includes(currentUser._id);
const isAuthor = card.userId === currentUser._id;
const matches = isMember || isAssignee || isAuthor;
if (process.env.DEBUG === 'true' && matches) {
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
}
return matches;
});
}

View file

@ -387,13 +387,15 @@ Blaze.Template.registerHelper(
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard)
return HTML.Raw(sanitizeHTML(content));
const knowedUsers = _.union(currentBoard.members.map(member => {
const u = ReactiveCache.getUser(member.userId);
if (u) {
member.username = u.username;
}
return member;
}), [...specialHandles]);
const knowedUsers = _.union(currentBoard.members
.filter(member => member.isActive)
.map(member => {
const u = ReactiveCache.getUser(member.userId);
if (u) {
member.username = u.username;
}
return member;
}), [...specialHandles]);
const mentionRegex = /\B@([\w.-]*)/gi;
let currentMention;
@ -410,14 +412,14 @@ Blaze.Template.registerHelper(
if (knowedUser.userId === Meteor.userId()) {
linkClass += ' me';
}
// For special group mentions, display translated text
let displayText = knowedUser.username;
if (specialHandleNames.includes(knowedUser.username)) {
displayText = TAPi18n.__(knowedUser.username);
linkClass = 'atMention'; // Remove js-open-member for special handles
}
// This @user mention link generation did open same Wekan
// window in new tab, so now A is changed to U so it's
// underlined and there is no link popup. This way also

View file

@ -56,9 +56,12 @@ template(name="header")
else
ul.header-quick-access-list
//li
// a(href="{{pathFor 'public'}}")
// span.fa.fa-globe
// | {{_ 'public'}}
//
a(href="{{pathFor 'public'}}")
//
span.fa.fa-globe
//
| {{_ 'public'}}
each currentUser.starredBoards
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
a(href="{{pathFor 'board' id=_id slug=slug}}")
@ -71,10 +74,13 @@ template(name="header")
// Next line is used only for spacing at header,
// there is no visible clickable icon.
#header-new-board-icon
// Hide duplicate create board button,
// because it did not show board templates correctly.
//
Hide duplicate create board button,
//
because it did not show board templates correctly.
//a#header-new-board-icon.js-create-board
// i.fa.fa-plus(title="Create a new board")
//
i.fa.fa-plus(title="Create a new board")
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")

View file

@ -515,7 +515,7 @@ a:not(.disabled).is-active i.fa {
max-width: 95vw;
margin: 0 auto;
}
/* Improve touch targets */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 44px;
@ -524,7 +524,7 @@ a:not(.disabled).is-active i.fa {
font-size: 16px; /* Prevent zoom on iOS */
touch-action: manipulation;
}
/* Form elements */
input, select, textarea {
font-size: 16px; /* Prevent zoom on iOS */
@ -532,7 +532,7 @@ a:not(.disabled).is-active i.fa {
min-height: 44px;
touch-action: manipulation;
}
/* Cards and lists */
.minicard {
min-height: 48px;
@ -540,19 +540,19 @@ a:not(.disabled).is-active i.fa {
margin-bottom: 8px;
touch-action: manipulation;
}
.list {
margin: 0 8px;
min-width: 280px;
}
/* Board canvas */
.board-canvas {
padding: 0 8px 8px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Header mobile layout */
#header {
padding: 8px;
@ -561,7 +561,7 @@ a:not(.disabled).is-active i.fa {
align-items: center;
gap: 8px;
}
#header-quick-access {
/* Keep quick-access items in one row */
display: flex;
@ -585,43 +585,43 @@ a:not(.disabled).is-active i.fa {
overflow: hidden;
white-space: nowrap;
}
/* Hide text in home icon on mobile, show only icon */
#header-quick-access .home-icon a span:not(.fa) {
display: none !important;
}
/* Ensure proper spacing for mobile header elements */
#header-quick-access .zoom-controls {
margin-left: auto;
margin-right: 8px;
}
.mobile-mode-toggle {
margin-right: 8px;
}
#header-user-bar {
margin-left: auto;
}
/* Ensure header elements don't wrap on very small screens */
#header-quick-access {
min-width: 0; /* Allow flexbox to shrink */
}
/* Make sure logo doesn't take too much space on mobile */
#header-quick-access img {
max-height: 24px;
max-width: 120px;
}
/* Ensure zoom controls are compact on mobile */
.zoom-controls .zoom-level {
padding: 4px 8px;
font-size: 12px;
}
/* Modal mobile optimization */
#modal .modal-content,
#modal .modal-content-wide {
@ -632,7 +632,7 @@ a:not(.disabled).is-active i.fa {
max-height: 90vh;
overflow-y: auto;
}
/* Table mobile optimization */
table {
font-size: 14px;
@ -642,19 +642,19 @@ a:not(.disabled).is-active i.fa {
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
/* Admin panel mobile optimization */
.setting-content .content-body {
flex-direction: column;
gap: 16px;
padding: 8px;
}
.setting-content .content-body .side-menu {
width: 100%;
order: 2;
}
.setting-content .content-body .main-body {
order: 1;
min-height: 60vh;
@ -668,59 +668,59 @@ a:not(.disabled).is-active i.fa {
#content > .wrapper {
padding: 12px;
}
.wrapper {
padding: 12px;
}
.panel-default {
width: 90vw;
max-width: 90vw;
}
/* Touch-friendly but more compact */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 48px;
min-width: 48px;
padding: 10px 14px;
}
.minicard {
min-height: 40px;
padding: 10px;
}
.list {
margin: 0 12px;
min-width: 300px;
}
.board-canvas {
padding: 0 12px 12px 0;
}
#header {
padding: 12px 16px;
}
#modal .modal-content {
width: 80vw;
max-width: 600px;
}
#modal .modal-content-wide {
width: 90vw;
max-width: 800px;
}
.setting-content .content-body {
gap: 20px;
}
.setting-content .content-body .side-menu {
width: 250px;
}
/* Responsive handling for quick-access description on tablets */
#header-quick-access ul.header-quick-access-list li.current.empty {
max-width: 300px;
@ -732,49 +732,49 @@ a:not(.disabled).is-active i.fa {
body {
font-size: 18px;
}
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 56px;
min-width: 56px;
padding: 16px 20px;
font-size: 18px;
}
.minicard {
min-height: 56px;
padding: 16px;
font-size: 18px;
}
.list {
margin: 0 8px;
min-width: 360px;
}
.board-canvas {
padding: 0;
}
#header {
padding: 0 8px;
}
#content > .wrapper {
padding: 0;
}
#modal .modal-content {
width: 600px;
}
#modal .modal-content-wide {
width: 1000px;
}
.setting-content .content-body {
gap: 32px;
}
.setting-content .content-body .side-menu {
width: 320px;
}
@ -935,24 +935,24 @@ a:not(.disabled).is-active i.fa {
width: 100%;
height: 100vh;
}
/* Fix z-index stacking for mobile Safari */
body.mobile-mode .board-wrapper {
z-index: 1;
}
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
body.mobile-mode .card-details {
z-index: 100 !important;
}
body.mobile-mode .pop-over {
z-index: 999;
}
/* Ensure smooth scrolling on iOS */
body.mobile-mode .card-details,
body.mobile-mode .pop-over .content-wrapper {

View file

@ -85,7 +85,7 @@ Template.userFormsLayout.onRendered(() => {
validator,
);
EscapeActions.executeAll();
// Set up MutationObserver for OIDC button instead of deprecated DOMSubtreeModified
const oidcButton = document.getElementById('at-oidc');
if (oidcButton) {
@ -115,7 +115,7 @@ Template.userFormsLayout.onRendered(() => {
});
observer.observe(oidcButton, { childList: true, subtree: true });
}
// Set up MutationObserver for .at-form instead of deprecated DOMSubtreeModified
const atForm = document.querySelector('.at-form');
if (atForm) {
@ -312,9 +312,9 @@ function getAuthenticationMethod(
if (!settings) {
return getUserAuthenticationMethod(undefined, match);
}
const { displayAuthenticationMethod, defaultAuthenticationMethod } = settings;
if (displayAuthenticationMethod) {
return $('.select-authentication').val();
}

View file

@ -2,7 +2,8 @@ template(name="myCardsHeaderBar")
if currentUser
h1
//a.back-btn(href="{{pathFor 'home'}}")
// i.fa.fa-chevron-left
//
i.fa.fa-chevron-left
i.fa.fa-list
| {{_ 'my-cards'}}
@ -72,7 +73,8 @@ template(name="myCards")
.my-cards-card-title-table
| {{card.title}}
//a.minicard-wrapper(href=card.originRelativeUrl)
// | {{card.title}}
//
| {{card.title}}
td
| {{list.title}}
td

View file

@ -5,7 +5,7 @@ Template.notification.events({
const update = {};
const newReadValue = this.read ? null : Date.now();
update[`profile.notifications.${this.index}.read`] = newReadValue;
Users.update(Meteor.userId(), { $set: update }, (error, result) => {
if (error) {
console.error('Error updating notification:', error);
@ -34,13 +34,13 @@ Template.notification.helpers({
activityDate() {
const activity = this.activityData;
if (!activity || !activity.createdAt) return '';
const user = ReactiveCache.getCurrentUser();
if (!user) return '';
const dateFormat = user.getDateFormat ? user.getDateFormat() : 'L';
const timeFormat = user.getTimeFormat ? user.getTimeFormat() : 'LT';
return moment(activity.createdAt).format(`${dateFormat} ${timeFormat}`);
},
});

View file

@ -33,7 +33,7 @@ template(name='notificationIcon')
else if($in activityType 'createList' 'removeList' 'archivedList')
+listNotificationIcon
else if($in activityType 'importList')
else if($in activityType 'importList')
+listNotificationIcon
//- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
//- DRY and consistant

View file

@ -36,7 +36,7 @@ Template.notificationsDrawer.events({
},
'click .notification-menu .menu-item'(event) {
const target = event.currentTarget;
if (target.classList.contains('mark-all-read')) {
const notifications = ReactiveCache.getCurrentUser().profile.notifications;
for (const index in notifications) {

View file

@ -84,4 +84,5 @@ template(name="setCardActionsColorPopup")
.palette-colors: each colors
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check button.primary.confirm.js-submit {{_ 'save'}}
i.fa.fa-check
button.primary.confirm.js-submit {{_ 'save'}}

View file

@ -5,10 +5,10 @@ template(name="checklistActions")
select(id="check-action")
option(value="add") {{_'r-add'}}
option(value="remove") {{_'r-remove'}}
div.trigger-text
div.trigger-text
| {{_'r-checklist'}}
div.trigger-dropdown
input(id="checklist-name",type=text,placeholder="{{_'r-name'}}")
input(id="checklist-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-checklist-action.js-goto-rules
i.fa.fa-plus
@ -18,10 +18,10 @@ template(name="checklistActions")
select(id="checkall-action")
option(value="check") {{_'r-check-all'}}
option(value="uncheck") {{_'r-uncheck-all'}}
div.trigger-text
div.trigger-text
| {{_'r-items-check'}}
div.trigger-dropdown
input(id="checklist-name2",type=text,placeholder="{{_'r-name'}}")
input(id="checklist-name2",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-checkall-action.js-goto-rules
i.fa.fa-plus
@ -32,32 +32,32 @@ template(name="checklistActions")
select(id="check-item-action")
option(value="check") {{_'r-check'}}
option(value="uncheck") {{_'r-uncheck'}}
div.trigger-text
div.trigger-text
| {{_'r-item'}}
div.trigger-dropdown
input(id="checkitem-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
div.trigger-text
| {{_'r-of-checklist'}}
div.trigger-dropdown
input(id="checklist-name3",type=text,placeholder="{{_'r-name'}}")
input(id="checklist-name3",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-check-item-action.js-goto-rules
i.fa.fa-plus
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-add-checklist'}}
div.trigger-dropdown
input(id="checklist-name-3",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
div.trigger-text
| {{_'r-with-items'}}
div.trigger-dropdown
input(id="checklist-items",type=text,placeholder="{{_'r-items-list'}}")
input(id="checklist-items",type=text,placeholder="{{_'r-items-list'}}")
div.trigger-button.js-add-checklist-items-action.js-goto-rules
i.fa.fa-plus
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-checklist-note'}}

View file

@ -6,6 +6,6 @@ template(name="mailActions")
div.trigger-dropdown-mail
input(id="email-to",type=text,placeholder="{{_'r-to'}}")
input(id="email-subject",type=text,placeholder="{{_'r-subject'}}")
textarea(id="email-msg")
textarea(id="email-msg")
div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules
i.fa.fa-plus

View file

@ -10,14 +10,14 @@ template(name="ruleDetails")
| {{_ 'r-trigger'}}
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
= trigger
h4
h4
| {{_ 'r-action'}}
div.trigger-item
div.trigger-content
div.trigger-text
= action
div.trigger-text
= action
div.rules-back
button.js-goback
i.fa.fa-arrow-left

View file

@ -11,7 +11,8 @@ template(name="rulesActions")
li.js-set-card-actions
i.fa.fa-file-text-o
li.js-set-checklist-actions
i.fa.fa-check li.js-set-mail-actions
i.fa.fa-check
li.js-set-mail-actions
| @
.triggers-main-body
if $eq currentActions.get 'board'

View file

@ -7,7 +7,7 @@ template(name="rulesList")
ul.rules-list
each rules
li.rules-lists-item
p
p
= title
div.rules-btns-group
button.js-goto-details

View file

@ -1,22 +1,22 @@
template(name="boardTriggers")
div.trigger-item#trigger-two
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-search
div.trigger-text
div.trigger-text
| {{_'r-is'}}
div.trigger-text
div.trigger-text
| {{_'r-added-to'}}
div.trigger-text
div.trigger-text
| {{_'r-list'}}
div.trigger-dropdown
input(id="create-list-name",type=text,placeholder="{{_'r-list-name'}}")
div.trigger-text
div.trigger-text
| {{_'r-in-swimlane'}}
div.trigger-dropdown
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
div.user-details.hide-element
@ -29,11 +29,11 @@ template(name="boardTriggers")
div.trigger-item#trigger-three
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-search
div.trigger-text
div.trigger-text
| {{_'r-is-moved'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
@ -47,24 +47,24 @@ template(name="boardTriggers")
div.trigger-item#trigger-four
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-search
div.trigger-text
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
select(id="move-action")
option(value="moved-to") {{_'r-moved-to'}}
option(value="moved-from") {{_'r-moved-from'}}
div.trigger-text
div.trigger-text
| {{_'r-list'}}
div.trigger-dropdown
input(id="move-list-name",type=text,placeholder="{{_'r-list-name'}}")
div.trigger-text
div.trigger-text
| {{_'r-in-swimlane'}}
div.trigger-dropdown
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
div.user-details.hide-element
@ -77,11 +77,11 @@ template(name="boardTriggers")
div.trigger-item#trigger-five
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-a-card'}}
div.trigger-inline-button.js-open-card-title-popup
div.trigger-inline-button.js-open-card-title-popup
i.fa.fa-search
div.trigger-text
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
select(id="arch-action")
@ -99,7 +99,7 @@ template(name="boardTriggers")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-board-note'}}
template(name="boardCardTitlePopup")

View file

@ -1,13 +1,13 @@
template(name="checklistTriggers")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-a-checklist'}}
div.trigger-dropdown
select(id="gen-check-action")
option(value="created") {{_'r-added-to'}}
option(value="removed") {{_'r-removed-from'}}
div.trigger-text
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
@ -22,17 +22,17 @@ template(name="checklistTriggers")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-the-checklist'}}
div.trigger-dropdown
input(id="check-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
input(id="check-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
select(id="spec-check-action")
option(value="created") {{_'r-added-to'}}
option(value="removed") {{_'r-removed-from'}}
div.trigger-text
div.trigger-text
| {{_'r-a-card'}}
div.trigger-button.trigger-button-person.js-show-user-field
i.fa.fa-user
@ -46,7 +46,7 @@ template(name="checklistTriggers")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-a-checklist'}}
div.trigger-dropdown
select(id="gen-comp-check-action")
@ -64,11 +64,11 @@ template(name="checklistTriggers")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-the-checklist'}}
div.trigger-dropdown
input(id="spec-comp-check-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
input(id="spec-comp-check-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
select(id="spec-comp-check-action")
@ -86,7 +86,7 @@ template(name="checklistTriggers")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-a-item'}}
div.trigger-dropdown
select(id="check-item-gen-action")
@ -104,11 +104,11 @@ template(name="checklistTriggers")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-when-the-item'}}
div.trigger-dropdown
input(id="check-item-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
input(id="check-item-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
| {{_'r-is'}}
div.trigger-dropdown
select(id="check-item-spec-action")

View file

@ -6,12 +6,12 @@ template(name="attachmentSettings")
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
@ -30,42 +30,42 @@ template(name="attachmentSettings")
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
@ -73,19 +73,19 @@ template(name="attachmentSettings")
template(name="storageSettings")
.storage-settings
h3 {{_ 'attachment-storage-configuration'}}
.storage-config-section
h4 {{_ 'filesystem-storage'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
@ -104,37 +104,37 @@ template(name="storageSettings")
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
@ -147,18 +147,18 @@ template(name="storageSettings")
template(name="attachmentMigration")
.attachment-migration
h3 {{_ 'attachment-migration'}}
.migration-controls
.form-group
label {{_ 'migration-batch-size'}}
input.wekan-form-control#migration-batch-size(type="number" value="{{migrationBatchSize}}" min="1" max="100")
small.form-text.text-muted {{_ 'migration-batch-size-description'}}
.form-group
label {{_ 'migration-delay-ms'}}
input.wekan-form-control#migration-delay-ms(type="number" value="{{migrationDelayMs}}" min="100" max="10000")
small.form-text.text-muted {{_ 'migration-delay-ms-description'}}
.form-group
label {{_ 'migration-cpu-threshold'}}
input.wekan-form-control#migration-cpu-threshold(type="number" value="{{migrationCpuThreshold}}" min="10" max="90")
@ -169,7 +169,7 @@ template(name="attachmentMigration")
button.js-migrate-all-to-filesystem.btn.btn-primary {{_ 'migrate-all-to-filesystem'}}
button.js-migrate-all-to-gridfs.btn.btn-primary {{_ 'migrate-all-to-gridfs'}}
button.js-migrate-all-to-s3.btn.btn-primary {{_ 'migrate-all-to-s3'}}
.migration-controls
button.js-pause-migration.btn.btn-warning {{_ 'pause-migration'}}
button.js-resume-migration.btn.btn-success {{_ 'resume-migration'}}
@ -180,7 +180,7 @@ template(name="attachmentMigration")
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.migration-stats
.stat-item
span.label {{_ 'total-attachments'}}:
@ -203,7 +203,7 @@ template(name="attachmentMigration")
template(name="attachmentMonitoring")
.attachment-monitoring
h3 {{_ 'attachment-monitoring'}}
.monitoring-stats
.stats-grid
.stat-card

View file

@ -838,26 +838,26 @@
align-items: flex-start;
gap: 15px;
}
.migration-controls,
.jobs-controls {
width: 100%;
justify-content: center;
}
.table {
font-size: 12px;
}
.table th,
.table td {
padding: 8px 12px;
}
.btn-group {
flex-direction: column;
}
.add-job-form {
max-width: 100%;
}

View file

@ -8,7 +8,7 @@ template(name="cronSettings")
option(value="0") 0 - {{_ 'all-migrations'}}
each migrationStepsWithIndex
option(value="{{index}}") {{index}} - {{name}}
.form-group
label {{_ 'migration-status'}}
.status-indicator
@ -18,16 +18,16 @@ template(name="cronSettings")
.step-counter
| Step {{migrationCurrentStepNum}}/{{migrationTotalSteps}}
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.progress-text
| {{migrationProgress}}% {{_ 'complete'}}
.form-group
button.js-start-migration.btn.btn-primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start'}}
button.js-pause-migration.btn.btn-warning(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause'}}
button.js-stop-migration.btn.btn-danger(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop'}}
.form-group.migration-errors-section
h4 {{_ 'cron-migration-errors'}}
if hasErrors
@ -49,7 +49,7 @@ template(name="cronSettings")
else
.no-errors
| {{_ 'cron-no-errors'}}
li
h3 {{_ 'board-operations'}}
.form-group
@ -57,7 +57,7 @@ template(name="cronSettings")
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
li
h3 {{_ 'cron-jobs'}}
.form-group
@ -90,22 +90,22 @@ template(name="cronMigrations")
button.btn.btn-danger.js-stop-all-migrations
i.fa.fa-stop
| {{_ 'stop-all-migrations'}}
.migration-progress
.progress-overview
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-progress'}}
.current-step
i.fa.fa-cog
| {{migrationCurrentStep}}
.migration-status
i.fa.fa-info-circle
| {{migrationStatus}}
.migration-steps
h3 {{_ 'migration-steps'}}
.steps-list
@ -149,7 +149,7 @@ template(name="cronBoardOperations")
button.btn.btn-info.js-force-board-scan
i.fa.fa-search
| {{_ 'force-board-scan'}}
.board-operations-stats
.stats-grid
.stat-item
@ -176,7 +176,7 @@ template(name="cronBoardOperations")
.stat-item
.stat-value {{boardMigrationStats.isScanning}}
.stat-label {{_ 'scanning-status'}}
.system-resources
.resource-item
.resource-label {{_ 'cpu-usage'}}
@ -191,18 +191,18 @@ template(name="cronBoardOperations")
.resource-item
.resource-label {{_ 'cpu-cores'}}
.resource-value {{systemResources.cpuCores}}
.board-operations-search
.search-box
input.form-control.js-search-board-operations(type="text" placeholder="{{_ 'search-boards-or-operations'}}")
i.fa.fa-search.search-icon
.board-operations-list
.operations-header
h3 {{_ 'board-operations'}} ({{pagination.total}})
.pagination-info
| {{_ 'showing'}} {{pagination.start}} - {{pagination.end}} {{_ 'of'}} {{pagination.total}}
.operations-table
table.table.table-striped
thead
@ -242,7 +242,7 @@ template(name="cronBoardOperations")
i.fa.fa-stop
button.btn.btn-sm.btn-info.js-view-details(data-operation="{{id}}")
i.fa.fa-info-circle
.pagination
if pagination.hasPrev
button.btn.btn-sm.btn-default.js-prev-page
@ -265,7 +265,7 @@ template(name="cronJobs")
button.btn.btn-success.js-refresh-jobs
i.fa.fa-refresh
| {{_ 'refresh'}}
.jobs-list
table.table.table-striped
thead
@ -304,17 +304,17 @@ template(name="cronAddJob")
h2
i.fa.fa-plus
| {{_ 'add-cron-job'}}
.add-job-form
form.js-add-cron-job-form
.form-group
label(for="job-name") {{_ 'job-name'}}
input.form-control#job-name(type="text" name="name" required)
.form-group
label(for="job-description") {{_ 'job-description'}}
textarea.form-control#job-description(name="description" rows="3")
.form-group
label(for="job-schedule") {{_ 'schedule'}}
select.form-control#job-schedule(name="schedule")
@ -326,11 +326,11 @@ template(name="cronAddJob")
option(value="every 6 hours") {{_ 'every-6-hours'}}
option(value="every 1 day") {{_ 'every-1-day'}}
option(value="once") {{_ 'run-once'}}
.form-group
label(for="job-weight") {{_ 'weight'}}
input.form-control#job-weight(type="number" name="weight" value="1" min="1" max="10")
.form-actions
button.btn.btn-primary(type="submit")
i.fa.fa-plus

View file

@ -209,15 +209,15 @@
width: 95%;
margin: 20px;
}
.migration-progress-content {
padding: 20px;
}
.migration-progress-header {
padding: 15px;
}
.migration-progress-title {
font-size: 16px;
}
@ -229,40 +229,40 @@
background: #2d3748;
color: #e2e8f0;
}
.migration-progress-overall-label,
.migration-progress-step-label,
.migration-progress-status-label {
color: #e2e8f0;
}
.migration-progress-status {
background: #4a5568;
border-left-color: #63b3ed;
}
.migration-progress-status-text {
color: #cbd5e0;
}
.migration-progress-details {
background: #2b6cb0;
border-left-color: #4299e1;
}
.migration-progress-details-label {
color: #bee3f8;
}
.migration-progress-details-text {
color: #90cdf4;
}
.migration-progress-footer {
background: #4a5568;
border-top-color: #718096;
}
.migration-progress-note {
color: #a0aec0;
}

View file

@ -8,7 +8,7 @@ template(name="migrationProgress")
| {{_ 'migration-progress-title'}}
.migration-progress-close.js-close-migration-progress
i.fa.fa-times-thin
.migration-progress-content
.migration-progress-overall
.migration-progress-overall-label
@ -17,7 +17,7 @@ template(name="migrationProgress")
.migration-progress-overall-fill(style="{{progressBarStyle}}")
.migration-progress-overall-percentage
| {{overallProgress}}%
.migration-progress-current-step
.migration-progress-step-label
| {{_ 'migration-progress-current-step'}}: {{stepNameFormatted}}
@ -25,20 +25,20 @@ template(name="migrationProgress")
.migration-progress-step-fill(style="{{stepProgressBarStyle}}")
.migration-progress-step-percentage
| {{stepProgress}}%
.migration-progress-status
.migration-progress-status-label
| {{_ 'migration-progress-status'}}:
.migration-progress-status-text
| {{stepStatus}}
if stepDetailsFormatted
.migration-progress-details
.migration-progress-details-label
| {{_ 'migration-progress-details'}}:
.migration-progress-details-text
| {{stepDetailsFormatted}}
.migration-progress-footer
.migration-progress-note
| {{_ 'migration-progress-note'}}

View file

@ -79,7 +79,7 @@ class MigrationProgressManager {
isMigrating.set(false);
migrationProgress.set(100);
migrationStatus.set('Migration completed successfully!');
// Clear step details after a delay
setTimeout(() => {
migrationStepName.set('');
@ -178,7 +178,7 @@ Template.migrationProgress.helpers({
stepNameFormatted() {
const stepName = migrationStepName.get();
if (!stepName) return '';
// Convert snake_case to Title Case
return stepName
.split('_')
@ -189,7 +189,7 @@ Template.migrationProgress.helpers({
stepDetailsFormatted() {
const details = migrationStepDetails.get();
if (!details) return '';
const formatted = [];
for (const [key, value] of Object.entries(details)) {
const formattedKey = key
@ -199,7 +199,7 @@ Template.migrationProgress.helpers({
.replace(/^\w/, c => c.toUpperCase());
formatted.push(`${formattedKey}: ${value}`);
}
return formatted.join(', ');
}
});

View file

@ -512,7 +512,8 @@ template(name="newUserPopup")
span.error.hide.username-taken
| {{_ 'error-username-taken'}}
//if isLdap
// input.js-profile-username(type="text" value=user.username readonly)
//
input.js-profile-username(type="text" value=user.username readonly)
//else
input.js-profile-username(type="text" value="" required)
label
@ -523,7 +524,8 @@ template(name="newUserPopup")
span.error.hide.email-taken
| {{_ 'error-email-taken'}}
//if isLdap
// input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
//
input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
//else
input.js-profile-email(type="email" value="" required)
label
@ -596,10 +598,14 @@ template(name="settingsOrgPopup")
// It's not yet possible to impersonate organization. Only impersonate user,
// because that changes current user ID. What would it mean in practice
// to impersonate organization?
// li
// a.impersonate-org
// i.fa.fa-user
// | {{_ 'impersonate-org'}}
//
li
//
a.impersonate-org
//
i.fa.fa-user
//
| {{_ 'impersonate-org'}}
//
//
@ -640,8 +646,10 @@ template(name="settingsUserPopup")
// - wekan/client/components/settings/peopleBody.jade deleteButton
// - wekan/client/components/settings/peopleBody.js deleteButton
// - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
// that does now remove member from board, card members and assignees correctly,
// but that should be used to remove user from all boards similarly
//
that does now remove member from board, card members and assignees correctly,
//
but that should be used to remove user from all boards similarly
// - wekan/models/users.js Delete is not enabled
template(name="lockedUsersGeneral")

View file

@ -117,6 +117,24 @@
padding-bottom: 50px;
}
/* Admin panel buttons should use theme darker color */
.setting-content .content-body .main-body .setting-detail button.btn {
background: #005377;
color: #fff;
border: none;
}
.setting-content .content-body .main-body .setting-detail button.btn:hover,
.setting-content .content-body .main-body .setting-detail button.btn:focus {
background: #004766;
color: #fff;
}
.setting-content .content-body .main-body .setting-detail button.btn:active {
background: #01628c;
color: #fff;
}
/* Force horizontal scrollbar to always be visible at bottom */
.setting-content .content-body .main-body {
position: relative;

View file

@ -107,7 +107,7 @@ template(name="setting")
a.js-setting-menu(data-id="cron-settings")
span.emoji-icon
i.fa.fa-clock
| {{_ 'cron'}}
| {{_ 'migrations'}}
.main-body
if isLoading
+spinner
@ -119,12 +119,12 @@ template(name="setting")
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
@ -143,49 +143,55 @@ template(name="setting")
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
else if isCronSettings
ul#cron-setting.setting-detail
li
h3 {{_ 'cron-migrations'}}
h3 {{_ 'migrations'}}
.form-group
label {{_ 'select-migration'}}
select.js-migration-select.wekan-form-control
option(value="0") 0 - {{_ 'all-migrations'}}
each migrationStepsWithIndex
option(value="{{index}}") {{index}} - {{name}}
.form-group
label {{_ 'migration-status'}}
.status-indicator
@ -193,43 +199,45 @@ template(name="setting")
span.status-value
if isMigrating
i.fa.fa-spinner.fa-spin(style="margin-right: 8px;")
| {{#if isMigrating}}{{migrationStatus}}{{else}}{{_ 'idle'}}{{/if}}
else if isUpdatingMigrationDropdown
i.fa.fa-spinner.fa-spin(style="margin-right: 8px;")
| {{#if isMigrating}}{{migrationStatusLine}}{{else}}{{migrationStatus}}{{/if}}
if isMigrating
.progress-section
if migrationCurrentAction
.step-counter
| {{migrationCurrentAction}}
else if migrationJobTotalSteps
.step-counter
| Step {{migrationJobStepNum}}/{{migrationJobTotalSteps}}
else if migrationTotalSteps
.step-counter
| Migration {{migrationCurrentStepNum}}/{{migrationTotalSteps}}
else
.step-counter
i.fa.fa-spinner.fa-spin(style="margin-right: 8px;")
| Calculating migration scope...
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.progress-bar(role="progressbar" style="width: {{migrationJobProgress}}%" aria-valuenow="{{migrationJobProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationJobProgress}}%
.progress-text
| {{migrationProgress}}% {{_ 'complete'}}
| {{migrationJobProgress}}% {{_ 'complete'}}
.migration-details
if migrationJobTotalSteps
if migrationJobTotalSteps gt 1
.detail-line
| Job step: {{migrationJobStepNum}}/{{migrationJobTotalSteps}}
if migrationEtaSeconds
.detail-line
| ETA: {{formatDurationSeconds migrationEtaSeconds}}
if migrationElapsedSeconds
.detail-line
| Elapsed: {{formatDurationSeconds migrationElapsedSeconds}}
.form-group
button.js-start-all-migrations.btn.btn-primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.btn.btn-warning(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.btn.btn-danger(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop-all-migrations'}}
li
h3 {{_ 'board-operations'}}
.form-group
label {{_ 'scheduled-board-operations'}}
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
li
h3 {{_ 'cron-jobs'}}
.form-group
label {{_ 'active-cron-jobs'}}
each cronJobs
.job-item
.job-info
.job-name {{name}}
.job-schedule {{schedule}}
.job-status {{status}}
.job-actions
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
.add-job-section
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
button.js-start-migration.primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.primary(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.primary(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop-all-migrations'}}
else if isGeneralSetting
+general
else if isEmailSetting
@ -285,39 +293,69 @@ template(name="general")
template(name='email')
ul#email-setting.setting-detail
//if isSandstorm
// li.smtp-form
// .title {{_ 'smtp-host'}}
// .description {{_ 'smtp-host-description'}}
// .form-group
// input.wekan-form-control#mail-server-host(type="text", placeholder="smtp.domain.com" value="{{currentSetting.mailServer.host}}")
// li.smtp-form
// .title {{_ 'smtp-port'}}
// .description {{_ 'smtp-port-description'}}
// .form-group
// input.wekan-form-control#mail-server-port(type="text", placeholder="25" value="{{currentSetting.mailServer.port}}")
// li.smtp-form
// .title {{_ 'smtp-username'}}
// .form-group
// input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
// li.smtp-form
// .title {{_ 'smtp-password'}}
// .form-group
// input.wekan-form-control#mail-server-password(type="password", placeholder="{{_ 'password'}}" value="")
// li.smtp-form
// .title {{_ 'smtp-tls'}}
// .form-group
// a.flex.js-toggle-tls
// .materialCheckBox#mail-server-tls(class="{{#if currentSetting.mailServer.enableTLS}}is-checked{{/if}}")
//
// span {{_ 'smtp-tls-description'}}
li.smtp-form
//
// li.smtp-form
// .title {{_ 'send-from'}}
// .form-group
// input.wekan-form-control#mail-server-from(type="email", placeholder="no-reply@domain.com" value="{{currentSetting.mailServer.from}}")
.title {{_ 'smtp-host'}}
//
// li
// button.js-save.primary {{_ 'save'}}
.description {{_ 'smtp-host-description'}}
//
.form-group
//
input.wekan-form-control#mail-server-host(type="text", placeholder="smtp.domain.com" value="{{currentSetting.mailServer.host}}")
//
li.smtp-form
//
.title {{_ 'smtp-port'}}
//
.description {{_ 'smtp-port-description'}}
//
.form-group
//
input.wekan-form-control#mail-server-port(type="text", placeholder="25" value="{{currentSetting.mailServer.port}}")
//
li.smtp-form
//
.title {{_ 'smtp-username'}}
//
.form-group
//
input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
//
li.smtp-form
//
.title {{_ 'smtp-password'}}
//
.form-group
//
input.wekan-form-control#mail-server-password(type="password", placeholder="{{_ 'password'}}" value="")
//
li.smtp-form
//
.title {{_ 'smtp-tls'}}
//
.form-group
//
a.flex.js-toggle-tls
//
.materialCheckBox#mail-server-tls(class="{{#if currentSetting.mailServer.enableTLS}}is-checked{{/if}}")
//
//
span {{_ 'smtp-tls-description'}}
//
//
li.smtp-form
//
.title {{_ 'send-from'}}
//
.form-group
//
input.wekan-form-control#mail-server-from(type="email", placeholder="no-reply@domain.com" value="{{currentSetting.mailServer.from}}")
//
//
li
//
button.js-save.primary {{_ 'save'}}
li
button.js-send-smtp-test-email.primary {{_ 'send-smtp-test'}}

View file

@ -2,15 +2,23 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { ALLOWED_WAIT_SPINNERS } from '/config/const';
import LockoutSettings from '/models/lockoutSettings';
import {
cronMigrationProgress,
cronMigrationStatus,
cronMigrationCurrentStep,
cronMigrationSteps,
cronIsMigrating,
import {
cronMigrationProgress,
cronMigrationStatus,
cronMigrationCurrentStep,
cronMigrationSteps,
cronIsMigrating,
cronJobs,
cronMigrationCurrentStepNum,
cronMigrationTotalSteps
cronMigrationTotalSteps,
cronMigrationCurrentAction,
cronMigrationJobProgress,
cronMigrationJobStepNum,
cronMigrationJobTotalSteps,
cronMigrationEtaSeconds,
cronMigrationElapsedSeconds,
cronMigrationCurrentNumber,
cronMigrationCurrentName
} from '/imports/cronMigrationClient';
@ -39,7 +47,7 @@ BlazeComponent.extendComponent({
Meteor.subscribe('accessibilitySettings');
Meteor.subscribe('globalwebhooks');
Meteor.subscribe('lockoutSettings');
// Poll for migration errors
this.errorPollInterval = Meteor.setInterval(() => {
if (this.cronSettings.get()) {
@ -62,7 +70,7 @@ BlazeComponent.extendComponent({
setError(error) {
this.error.set(error);
},
// Template helpers moved to BlazeComponent - using different names to avoid conflicts
isGeneralSetting() {
return this.generalSetting && this.generalSetting.get();
@ -102,41 +110,41 @@ BlazeComponent.extendComponent({
filesystemPath() {
return process.env.WRITABLE_PATH || '/data';
},
attachmentsPath() {
const writablePath = process.env.WRITABLE_PATH || '/data';
return `${writablePath}/attachments`;
},
avatarsPath() {
const writablePath = process.env.WRITABLE_PATH || '/data';
return `${writablePath}/avatars`;
},
gridfsEnabled() {
return process.env.GRIDFS_ENABLED === 'true';
},
s3Enabled() {
return process.env.S3_ENABLED === 'true';
},
s3Endpoint() {
return process.env.S3_ENDPOINT || '';
},
s3Bucket() {
return process.env.S3_BUCKET || '';
},
s3Region() {
return process.env.S3_REGION || '';
},
s3SslEnabled() {
return process.env.S3_SSL_ENABLED === 'true';
},
s3Port() {
return process.env.S3_PORT || 443;
},
@ -145,23 +153,23 @@ BlazeComponent.extendComponent({
migrationStatus() {
return cronMigrationStatus.get() || TAPi18n.__('idle');
},
migrationProgress() {
return cronMigrationProgress.get() || 0;
},
migrationCurrentStep() {
return cronMigrationCurrentStep.get() || '';
},
isMigrating() {
return cronIsMigrating.get() || false;
},
migrationSteps() {
return cronMigrationSteps.get() || [];
},
migrationStepsWithIndex() {
const steps = cronMigrationSteps.get() || [];
return steps.map((step, idx) => ({
@ -169,11 +177,15 @@ BlazeComponent.extendComponent({
index: idx + 1
}));
},
cronJobs() {
return cronJobs.get() || [];
},
isCronJobPaused(status) {
return status === 'paused';
},
migrationCurrentStepNum() {
return cronMigrationCurrentStepNum.get() || 0;
},
@ -182,6 +194,52 @@ BlazeComponent.extendComponent({
return cronMigrationTotalSteps.get() || 0;
},
migrationCurrentAction() {
return cronMigrationCurrentAction.get() || '';
},
migrationJobProgress() {
return cronMigrationJobProgress.get() || 0;
},
migrationJobStepNum() {
return cronMigrationJobStepNum.get() || 0;
},
migrationJobTotalSteps() {
return cronMigrationJobTotalSteps.get() || 0;
},
migrationEtaSeconds() {
return cronMigrationEtaSeconds.get();
},
migrationElapsedSeconds() {
return cronMigrationElapsedSeconds.get();
},
migrationNumber() {
return cronMigrationCurrentNumber.get();
},
migrationName() {
return cronMigrationCurrentName.get() || '';
},
migrationStatusLine() {
const number = cronMigrationCurrentNumber.get();
const name = cronMigrationCurrentName.get();
if (number && name) {
return `${number} - ${name}`;
}
return this.migrationStatus();
},
isUpdatingMigrationDropdown() {
const status = this.migrationStatus();
return status && status.startsWith('Updating Select Migration dropdown menu');
},
migrationErrors() {
return this.migrationErrorsList ? this.migrationErrorsList.get() : [];
},
@ -196,6 +254,19 @@ BlazeComponent.extendComponent({
return moment(date).format('YYYY-MM-DD HH:mm:ss');
},
formatDurationSeconds(seconds) {
if (seconds === null || seconds === undefined) return '';
const total = Math.max(0, Math.floor(seconds));
const hrs = Math.floor(total / 3600);
const mins = Math.floor((total % 3600) / 60);
const secs = total % 60;
const parts = [];
if (hrs > 0) parts.push(String(hrs).padStart(2, '0'));
parts.push(String(mins).padStart(2, '0'));
parts.push(String(secs).padStart(2, '0'));
return parts.join(':');
},
setLoading(w) {
this.loading.set(w);
},
@ -240,8 +311,14 @@ BlazeComponent.extendComponent({
'click button.js-start-migration'(event) {
event.preventDefault();
this.setLoading(true);
cronIsMigrating.set(true);
cronMigrationStatus.set(TAPi18n.__('migration-starting'));
cronMigrationCurrentAction.set('');
cronMigrationJobProgress.set(0);
cronMigrationJobStepNum.set(0);
cronMigrationJobTotalSteps.set(0);
const selectedIndex = parseInt($('.js-migration-select').val() || '0', 10);
if (selectedIndex === 0) {
// Run all migrations
Meteor.call('cron.startAllMigrations', (error, result) => {
@ -258,6 +335,10 @@ BlazeComponent.extendComponent({
this.setLoading(false);
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else if (result && result.skipped) {
cronIsMigrating.set(false);
cronMigrationStatus.set(TAPi18n.__('migration-not-needed'));
alert(TAPi18n.__('migration-not-needed'));
} else {
alert(TAPi18n.__('migration-started'));
}
@ -265,9 +346,52 @@ BlazeComponent.extendComponent({
}
},
'click button.js-start-all-migrations'(event) {
event.preventDefault();
this.setLoading(true);
Meteor.call('cron.startAllMigrations', (error) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-started'));
}
});
},
'click button.js-pause-all-migrations'(event) {
event.preventDefault();
this.setLoading(true);
Meteor.call('cron.pauseAllMigrations', (error) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-paused'));
}
});
},
'click button.js-stop-all-migrations'(event) {
event.preventDefault();
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
this.setLoading(true);
Meteor.call('cron.stopAllMigrations', (error) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-stopped'));
}
});
}
},
'click button.js-pause-migration'(event) {
event.preventDefault();
this.setLoading(true);
cronIsMigrating.set(false);
cronMigrationStatus.set(TAPi18n.__('migration-pausing'));
Meteor.call('cron.pauseAllMigrations', (error, result) => {
this.setLoading(false);
if (error) {
@ -282,6 +406,12 @@ BlazeComponent.extendComponent({
event.preventDefault();
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
this.setLoading(true);
cronIsMigrating.set(false);
cronMigrationStatus.set(TAPi18n.__('migration-stopping'));
cronMigrationCurrentAction.set('');
cronMigrationJobProgress.set(0);
cronMigrationJobStepNum.set(0);
cronMigrationJobTotalSteps.set(0);
Meteor.call('cron.stopAllMigrations', (error, result) => {
this.setLoading(false);
if (error) {
@ -293,29 +423,25 @@ BlazeComponent.extendComponent({
}
},
'click button.js-schedule-board-cleanup'(event) {
'click button.js-start-job'(event) {
event.preventDefault();
// Placeholder - board cleanup scheduling
alert(TAPi18n.__('board-cleanup-scheduled'));
},
'click button.js-schedule-board-archive'(event) {
event.preventDefault();
// Placeholder - board archive scheduling
alert(TAPi18n.__('board-archive-scheduled'));
},
'click button.js-schedule-board-backup'(event) {
event.preventDefault();
// Placeholder - board backup scheduling
alert(TAPi18n.__('board-backup-scheduled'));
const jobName = $(event.target).data('job-name');
this.setLoading(true);
Meteor.call('cron.startJob', jobName, (error) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('cron-job-start-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('cron-job-started'));
}
});
},
'click button.js-pause-job'(event) {
event.preventDefault();
const jobId = $(event.target).data('job-id');
const jobName = $(event.target).data('job-name');
this.setLoading(true);
Meteor.call('cron.pauseJob', jobId, (error, result) => {
Meteor.call('cron.pauseJob', jobName, (error) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
@ -325,12 +451,26 @@ BlazeComponent.extendComponent({
});
},
'click button.js-resume-job'(event) {
event.preventDefault();
const jobName = $(event.target).data('job-name');
this.setLoading(true);
Meteor.call('cron.resumeJob', jobName, (error) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('cron-job-resume-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('cron-job-resumed'));
}
});
},
'click button.js-delete-job'(event) {
event.preventDefault();
const jobId = $(event.target).data('job-id');
const jobName = $(event.target).data('job-name');
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
this.setLoading(true);
Meteor.call('cron.removeJob', jobId, (error, result) => {
Meteor.call('cron.removeJob', jobName, (error) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
@ -429,7 +569,7 @@ BlazeComponent.extendComponent({
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
const targetID = target.data('id');
// Reset all settings to false
this.forgotPasswordSetting.set(false);
this.generalSetting.set(false);
@ -442,7 +582,7 @@ BlazeComponent.extendComponent({
this.webhookSetting.set(false);
this.attachmentSettings.set(false);
this.cronSettings.set(false);
// Set the selected setting to true
if (targetID === 'registration-setting') {
this.generalSetting.set(true);
@ -847,7 +987,7 @@ BlazeComponent.extendComponent({
const content = $('#admin-accessibility-content')
.val()
.trim();
try {
AccessibilitySettings.update(AccessibilitySettings.findOne()._id, {
$set: {

View file

@ -1,9 +1,12 @@
template(name="sidebar")
.board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}} {{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}")
//a.sidebar-tongue.js-toggle-sidebar(
// class="{{#if isTongueHidden}}is-hidden{{/if}}",
// title="{{showTongueTitle}}")
// i.fa.fa-navicon
//
class="{{#if isTongueHidden}}is-hidden{{/if}}",
//
title="{{showTongueTitle}}")
//
i.fa.fa-navicon
.sidebar-actions
.sidebar-shortcuts
a.sidebar-btn.js-shortcuts(title="{{_ 'keyboard-shortcuts' }}")
@ -19,7 +22,8 @@ template(name="sidebar")
a.sidebar-xmark.js-close-sidebar &#10005;
.sidebar-content.js-board-sidebar-content
//a.hide-btn.js-hide-sidebar
// i.fa.fa-navicon
//
i.fa.fa-navicon
unless isDefaultView
h2
a.fa.fa-arrow-left.js-back-home
@ -460,17 +464,27 @@ template(name="boardCardSettingsPopup")
i.fa.fa-picture-o
| {{_ 'cover-attachment-on-minicard'}}
//div.check-div
// a.flex.js-field-has-comments(class="{{#if allowsComments}}is-checked{{/if}}")
// .materialCheckBox(class="{{#if allowsComments}}is-checked{{/if}}")
// span
// i.fa.fa-comment-o
// | {{_ 'comment'}}
//
a.flex.js-field-has-comments(class="{{#if allowsComments}}is-checked{{/if}}")
//
.materialCheckBox(class="{{#if allowsComments}}is-checked{{/if}}")
//
span
//
i.fa.fa-comment-o
//
| {{_ 'comment'}}
//div.check-div
// a.flex.js-field-has-activities(class="{{#if allowsActivities}}is-checked{{/if}}")
// .materialCheckBox(class="{{#if allowsActivities}}is-checked{{/if}}")
// span
// i.fa.fa-history
// | {{_ 'activities'}}
//
a.flex.js-field-has-activities(class="{{#if allowsActivities}}is-checked{{/if}}")
//
.materialCheckBox(class="{{#if allowsActivities}}is-checked{{/if}}")
//
span
//
i.fa.fa-history
//
| {{_ 'activities'}}
template(name="boardSubtaskSettingsPopup")
form.board-subtask-settings
@ -597,12 +611,18 @@ template(name="boardMenuPopup")
| {{_ 'board-change-background-image'}}
//Bug Board icons random dance https://github.com/wekan/wekan/issues/4214
//if currentUser.isBoardAdmin
// unless currentSetting.hideBoardMemberList
// unless currentSetting.hideCardCounterList
// li
// a.js-board-info-on-my-boards(title="{{_ 'board-info-on-my-boards'}}")
// i.fa.fa-id-card-o
// | {{_ 'board-info-on-my-boards'}}
//
unless currentSetting.hideBoardMemberList
//
unless currentSetting.hideCardCounterList
//
li
//
a.js-board-info-on-my-boards(title="{{_ 'board-info-on-my-boards'}}")
//
i.fa.fa-id-card-o
//
| {{_ 'board-info-on-my-boards'}}
hr
ul.pop-over-list
if withApi
@ -628,9 +648,12 @@ template(name="boardMenuPopup")
hr
ul.pop-over-list
// li
// a.js-delete-duplicate-lists
// | 🗑️
// | {{_ 'delete-duplicate-lists'}}
//
a.js-delete-duplicate-lists
//
| 🗑️
//
| {{_ 'delete-duplicate-lists'}}
li
a.js-archive-board
i.fa.fa-archive

View file

@ -291,10 +291,10 @@ Template.boardMenuPopup.events({
'click .js-delete-duplicate-lists': Popup.afterConfirm('deleteDuplicateLists', function() {
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard) return;
// Get all lists in the current board
const allLists = ReactiveCache.getLists({ boardId: currentBoard._id, archived: false });
// Group lists by title to find duplicates
const listsByTitle = {};
allLists.forEach(list => {
@ -303,7 +303,7 @@ Template.boardMenuPopup.events({
}
listsByTitle[list.title].push(list);
});
// Find and delete duplicate lists that have no cards
let deletedCount = 0;
Object.keys(listsByTitle).forEach(title => {
@ -313,7 +313,7 @@ Template.boardMenuPopup.events({
for (let i = 1; i < listsWithSameTitle.length; i++) {
const list = listsWithSameTitle[i];
const cardsInList = ReactiveCache.getCards({ listId: list._id, archived: false });
if (cardsInList.length === 0) {
Lists.remove(list._id);
deletedCount++;
@ -321,7 +321,7 @@ Template.boardMenuPopup.events({
}
}
});
// Show notification
if (deletedCount > 0) {
// You could add a toast notification here if available
@ -402,7 +402,7 @@ Template.memberPopup.events({
FlowRouter.go('home');
});
}),
});
Template.removeMemberPopup.helpers({
@ -934,7 +934,7 @@ BlazeComponent.extendComponent({
// Get the current board reactively using board ID from Session
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
let result = currentBoard ? currentBoard.presentParentTask : null;
if (result === null || result === undefined) {
result = 'no-parent';
@ -970,7 +970,7 @@ BlazeComponent.extendComponent({
// Get the ID from the anchor element, not the span
const anchorElement = $(evt.target).closest('.js-field-show-parent-in-minicard')[0];
const value = anchorElement ? anchorElement.id : null;
if (value) {
Boards.update(this.currentBoard._id, { $set: { presentParentTask: value } });
}
@ -1541,12 +1541,12 @@ BlazeComponent.extendComponent({
'keyup .js-search-member-input'(event) {
Session.set('addMemberPopup.error', '');
const query = event.target.value.trim();
// Clear previous timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// Debounce search
this.searchTimeout = setTimeout(() => {
this.performSearch(query);

View file

@ -20,8 +20,7 @@ template(name="archivesSidebar")
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
| | {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-card {{_ 'restore'}}
if currentUser.isBoardAdmin
@ -52,8 +51,7 @@ template(name="archivesSidebar")
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
| | {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-list {{_ 'restore'}}
if currentUser.isBoardAdmin
@ -82,8 +80,7 @@ template(name="archivesSidebar")
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
| | {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-swimlane {{_ 'restore'}}
if currentUser.isBoardAdmin

View file

@ -57,7 +57,8 @@ template(name="filterSidebar")
= profile.fullname
| (<span class="username">{{ username }}</span>)
if Filter.members.isSelected _id
i.fa.fa-check hr
i.fa.fa-check
hr
h3
i.fa.fa-user
| {{_ 'filter-assignee-label'}}

View file

@ -1,4 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
const subManager = new SubsManager();
@ -116,6 +117,70 @@ async function mutateSelectedCards(mutationNameOrCallback, ...args) {
}
}
function getSelectedCardsSorted() {
return ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] });
}
function getListsForBoardSwimlane(boardId, swimlaneId) {
if (!boardId) return [];
const board = ReactiveCache.getBoard(boardId);
if (!board) return [];
const selector = {
boardId,
archived: false,
};
if (swimlaneId) {
const defaultSwimlane = board.getDefaultSwimline && board.getDefaultSwimline();
if (defaultSwimlane && defaultSwimlane._id === swimlaneId) {
selector.swimlaneId = { $in: [swimlaneId, null, ''] };
} else {
selector.swimlaneId = swimlaneId;
}
}
return ReactiveCache.getLists(selector, { sort: { sort: 1 } });
}
function getMaxSortForList(listId, swimlaneId) {
if (!listId || !swimlaneId) return null;
const card = ReactiveCache.getCard(
{ listId, swimlaneId, archived: false },
{ sort: { sort: -1 } },
true,
);
return card ? card.sort : null;
}
function buildInsertionSortIndexes(cardsCount, targetCard, position, listId, swimlaneId) {
const indexes = [];
if (cardsCount <= 0) return indexes;
if (targetCard) {
const step = 0.5;
if (position === 'above') {
const start = targetCard.sort - step * cardsCount;
for (let i = 0; i < cardsCount; i += 1) {
indexes.push(start + step * i);
}
} else {
const start = targetCard.sort + step;
for (let i = 0; i < cardsCount; i += 1) {
indexes.push(start + step * i);
}
}
return indexes;
}
const maxSort = getMaxSortForList(listId, swimlaneId);
const start = maxSort === null ? 0 : maxSort + 1;
for (let i = 0; i < cardsCount; i += 1) {
indexes.push(start + i);
}
return indexes;
}
BlazeComponent.extendComponent({
mapSelection(kind, _id) {
return ReactiveCache.getCards(MultiSelection.getMongoSelector(), {sort: ['sort']}).map(card => {
@ -241,9 +306,12 @@ Template.moveSelectionPopup.onCreated(function() {
this.setFirstListId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const listId = board.lists()[0]._id;
const boardId = this.selectedBoardId.get();
const swimlaneId = this.selectedSwimlaneId.get();
const lists = getListsForBoardSwimlane(boardId, swimlaneId);
const listId = lists[0] ? lists[0]._id : '';
this.selectedListId.set(listId);
this.selectedCardId.set('');
} catch (e) {}
};
@ -270,8 +338,11 @@ Template.moveSelectionPopup.helpers({
return board ? board.swimlanes() : [];
},
lists() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.lists() : [];
const instance = Template.instance();
return getListsForBoardSwimlane(
instance.selectedBoardId.get(),
instance.selectedSwimlaneId.get(),
);
},
cards() {
const instance = Template.instance();
@ -288,6 +359,25 @@ Template.moveSelectionPopup.helpers({
isDialogOptionListId(listId) {
return Template.instance().selectedListId.get() === listId;
},
isTitleDefault(title) {
if (
title.startsWith("key 'default") &&
title.endsWith('returned an object instead of string.')
) {
const translated = `${TAPi18n.__('defaultdefault')}`;
if (
translated.startsWith("key 'default") &&
translated.endsWith('returned an object instead of string.')
) {
return 'Default';
}
return translated;
}
if (title === 'Default') {
return `${TAPi18n.__('defaultdefault')}`;
}
return title;
},
});
Template.moveSelectionPopup.events({
@ -296,10 +386,14 @@ Template.moveSelectionPopup.events({
Template.instance().getBoardData(boardId);
},
'change .js-select-swimlanes'(event) {
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
const instance = Template.instance();
instance.selectedSwimlaneId.set($(event.currentTarget).val());
instance.setFirstListId();
},
'change .js-select-lists'(event) {
Template.instance().selectedListId.set($(event.currentTarget).val());
const instance = Template.instance();
instance.selectedListId.set($(event.currentTarget).val());
instance.selectedCardId.set('');
},
'change .js-select-cards'(event) {
Template.instance().selectedCardId.set($(event.currentTarget).val());
@ -307,7 +401,7 @@ Template.moveSelectionPopup.events({
'change input[name="position"]'(event) {
Template.instance().position.set($(event.currentTarget).val());
},
'click .js-done'() {
async 'click .js-done'() {
const instance = Template.instance();
const boardId = instance.selectedBoardId.get();
const swimlaneId = instance.selectedSwimlaneId.get();
@ -315,27 +409,19 @@ Template.moveSelectionPopup.events({
const cardId = instance.selectedCardId.get();
const position = instance.position.get();
// Calculate sortIndex
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// If no card selected, move to end
const board = ReactiveCache.getBoard(boardId);
const cards = board.cards({ swimlaneId, listId }).sort('sort');
if (cards.length > 0) {
sortIndex = cards[cards.length - 1].sort + 1;
}
}
const selectedCards = getSelectedCardsSorted();
const targetCard = cardId ? ReactiveCache.getCard(cardId) : null;
const sortIndexes = buildInsertionSortIndexes(
selectedCards.length,
targetCard,
position,
listId,
swimlaneId,
);
mutateSelectedCards('move', boardId, swimlaneId, listId, sortIndex);
for (let i = 0; i < selectedCards.length; i += 1) {
await selectedCards[i].move(boardId, swimlaneId, listId, sortIndexes[i]);
}
EscapeActions.executeUpTo('multiselection');
},
});
@ -372,9 +458,12 @@ Template.copySelectionPopup.onCreated(function() {
this.setFirstListId = function() {
try {
const board = ReactiveCache.getBoard(this.selectedBoardId.get());
const listId = board.lists()[0]._id;
const boardId = this.selectedBoardId.get();
const swimlaneId = this.selectedSwimlaneId.get();
const lists = getListsForBoardSwimlane(boardId, swimlaneId);
const listId = lists[0] ? lists[0]._id : '';
this.selectedListId.set(listId);
this.selectedCardId.set('');
} catch (e) {}
};
@ -401,8 +490,11 @@ Template.copySelectionPopup.helpers({
return board ? board.swimlanes() : [];
},
lists() {
const board = ReactiveCache.getBoard(Template.instance().selectedBoardId.get());
return board ? board.lists() : [];
const instance = Template.instance();
return getListsForBoardSwimlane(
instance.selectedBoardId.get(),
instance.selectedSwimlaneId.get(),
);
},
cards() {
const instance = Template.instance();
@ -419,6 +511,25 @@ Template.copySelectionPopup.helpers({
isDialogOptionListId(listId) {
return Template.instance().selectedListId.get() === listId;
},
isTitleDefault(title) {
if (
title.startsWith("key 'default") &&
title.endsWith('returned an object instead of string.')
) {
const translated = `${TAPi18n.__('defaultdefault')}`;
if (
translated.startsWith("key 'default") &&
translated.endsWith('returned an object instead of string.')
) {
return 'Default';
}
return translated;
}
if (title === 'Default') {
return `${TAPi18n.__('defaultdefault')}`;
}
return title;
},
});
Template.copySelectionPopup.events({
@ -427,10 +538,14 @@ Template.copySelectionPopup.events({
Template.instance().getBoardData(boardId);
},
'change .js-select-swimlanes'(event) {
Template.instance().selectedSwimlaneId.set($(event.currentTarget).val());
const instance = Template.instance();
instance.selectedSwimlaneId.set($(event.currentTarget).val());
instance.setFirstListId();
},
'change .js-select-lists'(event) {
Template.instance().selectedListId.set($(event.currentTarget).val());
const instance = Template.instance();
instance.selectedListId.set($(event.currentTarget).val());
instance.selectedCardId.set('');
},
'change .js-select-cards'(event) {
Template.instance().selectedCardId.set($(event.currentTarget).val());
@ -438,7 +553,7 @@ Template.copySelectionPopup.events({
'change input[name="position"]'(event) {
Template.instance().position.set($(event.currentTarget).val());
},
'click .js-done'() {
async 'click .js-done'() {
const instance = Template.instance();
const boardId = instance.selectedBoardId.get();
const swimlaneId = instance.selectedSwimlaneId.get();
@ -446,33 +561,34 @@ Template.copySelectionPopup.events({
const cardId = instance.selectedCardId.get();
const position = instance.position.get();
mutateSelectedCards(async (card) => {
const newCardId = await card.copy(boardId, swimlaneId, listId);
if (newCardId) {
const newCard = ReactiveCache.getCard(newCardId);
if (newCard) {
let sortIndex = 0;
if (cardId) {
const targetCard = ReactiveCache.getCard(cardId);
if (targetCard) {
if (position === 'above') {
sortIndex = targetCard.sort - 0.5;
} else {
sortIndex = targetCard.sort + 0.5;
}
}
} else {
// To end
const board = ReactiveCache.getBoard(boardId);
const cards = board.cards({ swimlaneId, listId }).sort('sort');
if (cards.length > 0) {
sortIndex = cards[cards.length - 1].sort + 1;
}
}
newCard.setSort(sortIndex);
}
}
});
const selectedCards = getSelectedCardsSorted();
const targetCard = cardId ? ReactiveCache.getCard(cardId) : null;
const sortIndexes = buildInsertionSortIndexes(
selectedCards.length,
targetCard,
position,
listId,
swimlaneId,
);
for (let i = 0; i < selectedCards.length; i += 1) {
const card = selectedCards[i];
const newCardId = await Meteor.callAsync(
'copyCard',
card._id,
boardId,
swimlaneId,
listId,
true,
{ title: card.title },
);
if (!newCardId) continue;
const newCard = ReactiveCache.getCard(newCardId);
if (!newCard) continue;
await newCard.move(boardId, swimlaneId, listId, sortIndexes[i]);
}
EscapeActions.executeUpTo('multiselection');
},
});

View file

@ -35,6 +35,8 @@ template(name="swimlaneFixedHeader")
i.fa.fa-caret-down
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
i.fa.fa-bars
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
i.fa.fa-plus
if isTouchScreenOrShowDesktopDragHandles
unless isTouchScreen
a.swimlane-header-handle.handle.js-swimlane-header-handle
@ -59,12 +61,14 @@ template(name="swimlaneActionPopup")
ul.pop-over-list
li: a.js-add-swimlane
i.fa.fa-plus
span {{_ 'add-swimlane'}}
span
| {{_ 'add-swimlane'}}
hr
ul.pop-over-list
li: a.js-add-list-from-swimlane
i.fa.fa-plus
span {{_ 'add-list'}}
span
| {{_ 'add-list'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
@ -114,7 +118,8 @@ template(name="setSwimlaneColorPopup")
each colors
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check // Buttons aligned left too
i.fa.fa-check
// Buttons aligned left too
.flush-left
button.primary.confirm.js-submit(style="margin-left:0") {{_ 'save'}}
button.js-remove-color.negate.wide.right(style="margin-left:8px") {{_ 'unset-color'}}

View file

@ -39,6 +39,7 @@ BlazeComponent.extendComponent({
this.collapsed(!this.collapsed());
},
'click .js-open-swimlane-menu': Popup.open('swimlaneAction'),
'click .js-open-add-swimlane-menu': Popup.open('swimlaneAdd'),
submit: this.editTitle,
},
];
@ -85,7 +86,7 @@ Template.editSwimlaneTitleForm.helpers({
// When that happens, try use translation "defaultdefault" that has same content of default, or return text "Default".
// This can happen, if swimlane does not have name.
// Yes, this is fixing the symptom (Swimlane title does not have title)
// instead of fixing the problem (Add Swimlane title when creating swimlane)
// instead of fixing the problem (Add Swimlane title when creating swimlane)
// because there could be thousands of swimlanes, adding name Default to all of them
// would be very slow.
if (title.startsWith("key 'default") && title.endsWith('returned an object instead of string.')) {

View file

@ -109,12 +109,13 @@
.swimlane .swimlane-header-wrap .swimlane-header-plus-icon {
top: calc(50% + 6px);
padding: 5px;
margin-left: 20px;
font-size: 22px;
color: #a6a6a6;
}
.swimlane .swimlane-header-wrap .swimlane-header-menu-icon {
top: calc(50% + 6px);
padding: 5px;
padding-left: 5px;
font-size: 22px;
}
.swimlane .swimlane-header-wrap .swimlane-header-handle {

View file

@ -9,9 +9,15 @@ template(name="swimlane")
if currentListIsInThisSwimlane _id
+list(currentList)
unless currentList
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
each lists
+miniList(this)
else
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
each lists
if visible this
+list(this)

View file

@ -78,13 +78,7 @@ function saveSorting(ui) {
}
// Allow reordering within the same swimlane by not canceling the sortable
try {
Lists.update(list._id, {
$set: updateData,
});
} catch (error) {
return;
}
// Do not update the restricted collection on the client; rely on the server method below.
// Save to localStorage for non-logged-in users (backup)
if (!Meteor.userId()) {
@ -366,11 +360,9 @@ BlazeComponent.extendComponent({
const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles()
? '.js-list-handle'
: '.js-list-header';
const $lists = this.$('.js-list');
const $parent = this.$('.js-lists');
const $parent = $lists.parent();
if ($lists.length > 0) {
if ($parent.length > 0) {
// Check for drag handles
const $handles = $parent.find('.js-list-handle');
@ -391,6 +383,7 @@ BlazeComponent.extendComponent({
distance: 7,
handle: handleSelector,
disabled: !Utils.canModifyBoard(),
dropOnEmpty: true,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
@ -412,7 +405,6 @@ BlazeComponent.extendComponent({
try { $parent.sortable('option', 'handle', newHandle); } catch (e) {}
}
});
} else {
}
}, 100);
},
@ -730,6 +722,7 @@ BlazeComponent.extendComponent({
let sortIndex = 0;
const lastList = this.currentBoard.getLastList();
const boardId = Utils.getCurrentBoardId();
let swimlaneId = this.currentSwimlane._id;
const positionInput = this.find('.list-position-input');
@ -739,6 +732,9 @@ BlazeComponent.extendComponent({
if (selectedList) {
sortIndex = selectedList.sort + 1;
// Use the swimlane ID from the selected list to ensure the new list
// is added to the same swimlane as the selected list
swimlaneId = selectedList.swimlaneId;
} else {
sortIndex = Utils.calculateIndexData(lastList, null).base;
}
@ -751,7 +747,7 @@ BlazeComponent.extendComponent({
boardId: Session.get('currentBoard'),
sort: sortIndex,
type: this.isListTemplatesSwimlane ? 'template-list' : 'list',
swimlaneId: this.currentSwimlane._id, // Always set swimlaneId for per-swimlane list titles
swimlaneId: swimlaneId, // Always set swimlaneId for per-swimlane list titles
});
titleInput.value = '';
@ -805,6 +801,7 @@ setTimeout(() => {
distance: 7,
handle: computeHandle(),
disabled: !Utils.canModifyBoard(),
dropOnEmpty: true,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
@ -896,13 +893,7 @@ setTimeout(() => {
}
// Allow reordering within the same swimlane by not canceling the sortable
try {
Lists.update(list._id, {
$set: updateData,
});
} catch (error) {
return;
}
// Do not update the restricted collection on the client; rely on the server method below.
// Save to localStorage for non-logged-in users (backup)
if (!Meteor.userId()) {
@ -1022,7 +1013,8 @@ BlazeComponent.extendComponent({
const $parent = $lists.parent();
if ($lists.length > 0) {
// Initialize sortable even if there are no lists (to allow dropping into empty swimlanes)
if ($parent.hasClass('js-lists')) {
// Check for drag handles
const $handles = $parent.find('.js-list-handle');
@ -1043,6 +1035,7 @@ BlazeComponent.extendComponent({
distance: 7,
handle: handleSelector,
disabled: !Utils.canModifyBoard(),
dropOnEmpty: true,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
@ -1063,7 +1056,6 @@ BlazeComponent.extendComponent({
try { $parent.sortable('option', 'handle', newHandle); } catch (e) {}
}
});
} else {
}
}, 100);
},

View file

@ -4,17 +4,17 @@ Template.passwordInput.onRendered(function() {
const template = this;
const input = template.find('input.password-field');
const label = template.find('label');
// Set the dynamic id and name based on the field _id
if (template.data && template.data._id) {
const fieldId = `at-field-${template.data._id}`;
input.id = fieldId;
input.name = fieldId;
label.setAttribute('for', fieldId);
// Ensure the input starts as password type for password fields
input.type = 'password';
// Initially show eye icon (password is hidden) and hide eye-slash icon
const eyeIcon = template.find('.eye-icon');
const eyeSlashIcon = template.find('.eye-slash-icon');
@ -33,7 +33,7 @@ Template.passwordInput.events({
const input = template.find('input.password-field');
const eyeIcon = template.find('.eye-icon');
const eyeSlashIcon = template.find('.eye-slash-icon');
if (input.type === 'password') {
input.type = 'text';
// Show eye-slash icon when password is visible

View file

@ -92,7 +92,8 @@ template(name="changeAvatarPopup")
.member
img.avatar.avatar-image(src="{{link}}")
if isSelected
i.fa.fa-check p.sub-name
i.fa.fa-check
p.sub-name
a.js-delete-avatar {{_ 'delete'}}
| -
= name
@ -101,7 +102,8 @@ template(name="changeAvatarPopup")
+userAvatarInitials(userId=currentUser._id)
| {{_ 'initials' }}
if noAvatarUrl
i.fa.fa-check p.sub-name {{_ 'default-avatar'}}
i.fa.fa-check
p.sub-name {{_ 'default-avatar'}}
input.hide.js-upload-avatar-input(accept="image/*;capture=camera" type="file")
if Meteor.settings.public.avatarsUploadMaxSize
| {{_ 'max-avatar-filesize'}} {{Meteor.settings.public.avatarsUploadMaxSize}}

View file

@ -34,10 +34,10 @@ Template.userAvatar.helpers({
memberType() {
const user = ReactiveCache.getUser(this.userId);
if (!user) return '';
const board = Utils.getCurrentBoard();
if (!board) return '';
// Return role in priority order: Admin, Normal, NormalAssignedOnly, NoComments, CommentOnly, CommentAssignedOnly, Worker, ReadOnly, ReadAssignedOnly
if (user.isBoardAdmin()) return 'admin';
if (board.hasReadAssignedOnly(user._id)) return 'read-assigned-only';

View file

@ -188,7 +188,8 @@ template(name="changeSettingsPopup")
i.fa.fa-arrows
| {{_ 'show-desktop-drag-handles'}}
if isShowDesktopDragHandles
i.fa.fa-check unless currentUser.isWorker
i.fa.fa-check
unless currentUser.isWorker
li
label.bold.clear
i.fa.fa-sort-numeric-asc

View file

@ -168,33 +168,22 @@ Template.invitePeoplePopup.events({
},
});
Template.editProfilePopup.onCreated(function() {
this.subscribe('accountSettings');
});
Template.editProfilePopup.helpers({
allowEmailChange() {
Meteor.call('AccountSettings.allowEmailChange', (_, result) => {
if (result) {
return true;
} else {
return false;
}
});
const setting = AccountSettings.findOne('accounts-allowEmailChange');
return setting && setting.booleanValue;
},
allowUserNameChange() {
Meteor.call('AccountSettings.allowUserNameChange', (_, result) => {
if (result) {
return true;
} else {
return false;
}
});
const setting = AccountSettings.findOne('accounts-allowUserNameChange');
return setting && setting.booleanValue;
},
allowUserDelete() {
Meteor.call('AccountSettings.allowUserDelete', (_, result) => {
if (result) {
return true;
} else {
return false;
}
});
const setting = AccountSettings.findOne('accounts-allowUserDelete');
return setting && setting.booleanValue;
},
});