mirror of
https://github.com/wekan/wekan.git
synced 2026-02-21 15:34:07 +01:00
Merge branch 'updates' of https://github.com/KhaoulaMaleh/wekan into updates
This commit is contained in:
commit
afd40b21f1
306 changed files with 12406 additions and 2587 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
template(name="board")
|
||||
|
||||
|
||||
if isConverting.get
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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="#")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'}}")
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -539,10 +539,10 @@ BlazeComponent.extendComponent({
|
|||
if (!board) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// Ensure default swimlane exists
|
||||
board.getDefaultSwimline();
|
||||
|
||||
|
||||
const swimlanes = ReactiveCache.getSwimlanes(
|
||||
{
|
||||
boardId: this.selectedBoardId.get()
|
||||
|
|
|
|||
|
|
@ -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"}}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}}")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
@ -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(', ');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 ✕
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
|
|||
|
|
@ -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.')) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue