Opened card Checklist menu: Hide finished tasks. Show Checklist at Minicard.

Thanks to C0rn3j and xet7 !

Fixes #6019,
fixes #5567,
fixes #2984
This commit is contained in:
Lauri Ojansivu 2025-12-29 21:42:19 +02:00
parent cf62807ad5
commit fbfde81bc8
13 changed files with 312 additions and 161 deletions

View file

@ -211,25 +211,6 @@ body.desktop-mode .card-details.card-details-collapsed {
color: #000;
}
/* Bring to front / Send to back buttons */
.card-details .card-details-header .card-bring-to-front,
.card-details .card-details-header .card-send-to-back {
float: right;
font-size: 18px;
padding: 7px 8px;
margin-right: 5px;
cursor: pointer;
user-select: none;
color: #333;
}
.card-details .card-details-header .card-bring-to-front:hover,
.card-details .card-details-header .card-send-to-back:hover {
color: #000;
background: rgba(0,0,0,0.05);
border-radius: 3px;
}
/* Drag handle */
.card-details .card-details-header .card-drag-handle {
font-size: 20px;

View file

@ -20,10 +20,6 @@ template(name="cardDetails")
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
a.card-bring-to-front.js-card-bring-to-front(title="Bring to front")
| ⏫
a.card-send-to-back.js-card-send-to-back(title="Send to back")
| ⏬
if cardMaximized
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽

View file

@ -324,30 +324,6 @@ BlazeComponent.extendComponent({
Users.setPublicCardCollapsed(!currentState);
}
},
'click .js-card-bring-to-front'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
// Find the highest z-index among all cards
let maxZ = 100;
$('.card-details').each(function() {
const z = parseInt($(this).css('z-index')) || 100;
if (z > maxZ) maxZ = z;
});
// Set this card's z-index to be higher
$card.css('z-index', maxZ + 1);
},
'click .js-card-send-to-back'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
// Find the lowest z-index among all cards
let minZ = 100;
$('.card-details').each(function() {
const z = parseInt($(this).css('z-index')) || 100;
if (z < minZ) minZ = z;
});
// Set this card's z-index to be lower
$card.css('z-index', minZ - 1);
},
'mousedown .js-card-drag-handle'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');

View file

@ -9,19 +9,10 @@ template(name="checklists")
else
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
if currentUser.isBoardMember
.material-toggle-switch(title="{{_ 'hide-finished-checklist'}}")
//span.toggle-switch-title
if card.hideFinishedChecklistIfItemsAreHidden
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleHideFinishedChecklist")
label.toggle-label(for="toggleHideFinishedChecklist")
.card-checklist-items
each checklist in checklists
if checklist.showChecklist card.hideFinishedChecklistIfItemsAreHidden
+checklistDetail(checklist = checklist card = card)
+checklistDetail(checklist = checklist card = card)
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId)
@ -38,7 +29,7 @@ template(name="checklistDetail")
.checklist-title
span
if canModifyCard
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
a.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
if canModifyCard
h4.title.js-open-inlined-form.is-editable
@ -173,34 +164,62 @@ template(name="checklistActionsPopup")
else
input.toggle-switch(type="checkbox" id="toggleHideAllChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideAllChecklistItems_{{checklist._id}}")
a.js-toggle-show-checklist-at-minicard
| 📋
| {{_ "showChecklistAtMinicard"}} ...
.material-toggle-switch(title="{{_ 'showChecklistAtMinicard'}}")
if checklist.showChecklistAtMinicard
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}" checked="checked")
else
input.toggle-switch(type="checkbox" id="toggleShowChecklistAtMinicard_{{checklist._id}}")
label.toggle-label(for="toggleShowChecklistAtMinicard_{{checklist._id}}")
template(name="copyChecklistPopup")
+copyAndMoveChecklist
template(name="moveChecklistPopup")
+copyAndMoveChecklist
template(name="copyAndMoveChecklist")
unless currentUser.isWorker
label {{_ 'boards'}}:
select.js-select-boards(autofocus)
each boards
option(value="{{_id}}" selected="{{#if isDialogOptionBoardId _id}}selected{{/if}}") {{title}}
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}}") {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionSwimlaneId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'cards'}}:
label {{_ 'card'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{title}}
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}
template(name="moveChecklistPopup")
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}}. {{title}}
label {{_ 'lists'}}:
select.js-select-lists
each lists
option(value="{{_id}}" selected="{{#if isDialogOptionListId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
label {{_ 'card'}}:
select.js-select-cards
each cards
option(value="{{_id}}" selected="{{#if isDialogOptionCardId _id}}selected{{/if}}") {{add @index 1}}. {{title}}
.edit-controls.clearfix
button.primary.confirm.js-done {{_ 'done'}}

View file

@ -230,10 +230,6 @@ BlazeComponent.extendComponent({
'focus .js-add-checklist-item': this.focusChecklistItem,
// add and delete checklist / checklist-item
'click .js-open-inlined-form': this.closeAllInlinedForms,
'click #toggleHideFinishedChecklist'(event) {
event.preventDefault();
this.data().card.toggleHideFinishedChecklist();
},
keydown: this.pressKey,
},
];
@ -335,6 +331,12 @@ BlazeComponent.extendComponent({
this.data().checklist.toggleHideAllChecklistItems();
Popup.back();
},
'click .js-toggle-show-checklist-at-minicard'(event) {
event.preventDefault();
const checklist = this.data().checklist;
checklist.toggleShowChecklistAtMinicard();
Popup.back();
},
}
]
}
@ -388,7 +390,12 @@ BlazeComponent.extendComponent({
}
setDone(cardId, options) {
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
this.data().checklist.move(cardId);
const checklist = this.data().checklist;
Meteor.call('moveChecklist', checklist._id, cardId, (error) => {
if (error) {
console.error('Error moving checklist:', error);
}
});
}
}).register('moveChecklistPopup');

View file

@ -730,3 +730,81 @@
align-items: center;
gap: 0.3vw;
}
/* Checklist display on minicard */
.minicard-checklist {
width: 100%;
margin-top: 0.5vh;
margin-bottom: 0.5vh;
padding: 0.3vh 0.5vw;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0.3vw;
border: 1px solid #e0e0e0;
}
.minicard-checklist .checklist-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.3vh;
}
.minicard-checklist .checklist-title {
font-size: 0.8em;
font-weight: bold;
color: #4d4d4d;
flex: 1;
}
.minicard-checklist .checklist-menu {
font-size: 1.2em;
color: #666;
cursor: pointer;
padding: 0 0.3vw;
border-radius: 0.2vw;
transition: background-color 0.2s;
}
.minicard-checklist .checklist-menu:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.minicard-checklist .checklist-item {
font-size: 0.75em;
color: #666;
margin-bottom: 0.2vh;
display: flex;
align-items: flex-start;
gap: 0.3vw;
line-height: 1.2;
cursor: pointer;
padding: 0.2vh 0;
border-radius: 0.2vw;
transition: background-color 0.2s;
}
.minicard-checklist .checklist-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.minicard-checklist .checklist-item.is-checked {
text-decoration: line-through;
color: #999;
}
.minicard-checklist .checklist-item .check-box-unicode {
flex-shrink: 0;
font-size: 0.8em;
margin-top: 0.1vh;
transition: transform 0.2s;
}
.minicard-checklist .checklist-item:hover .check-box-unicode {
transform: scale(1.1);
}
.minicard-checklist .checklist-item .item-title {
flex: 1;
word-wrap: break-word;
overflow-wrap: break-word;
}

View file

@ -167,10 +167,6 @@ template(name="minicard")
.badge
span.badge-icon 📎
span.badge-text= attachments.length
if checklists.length
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
span.badge-icon ☑️
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count
.badge
span.badge-icon 🌐
@ -181,6 +177,9 @@ template(name="minicard")
.badge
span.badge-icon 🔢
span.badge-text.check-list-sort {{ sort }}
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if currentBoard.allowsDescriptionTextOnMinicard
if getDescription
.minicard-description
@ -202,55 +201,12 @@ template(name="editCardSortOrderPopup")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
template(name="minicardDetailsActionsPopup")
ul.pop-over-list
if canModifyCard
li
a.js-move-card
| ➡️
| {{_ 'moveCardPopup-title'}}
li
a.js-copy-card
| 📋
| {{_ 'copyCardPopup-title'}}
hr
li
a.js-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
li
a.js-move-card-to-top
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
li
a.js-add-labels
| 🏷️
| {{_ 'card-edit-labels'}}
li
a.js-due-date
| 📥
| {{_ 'editCardDueDatePopup-title'}}
li
a.js-set-card-color
| 🎨
| {{_ 'setCardColorPopup-title'}}
li
a.js-link
| 🔗
| {{_ 'link-card'}}
li
a.js-toggle-watch-card
if isWatching
| 👁️
| {{_ 'unwatch'}}
else
| 👁️
| {{_ 'watch'}}
template(name="minicardChecklist")
.minicard-checklist
.checklist-header
.checklist-title= checklist.title
if canModifyCard
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}") ☰
each visibleItems
+checklistItemDetail(item = . checklist = checklist card = card)

View file

@ -91,6 +91,13 @@ BlazeComponent.extendComponent({
}
},
toggleChecklistItem() {
const item = this.currentData();
if (item && item._id) {
item.toggleItem();
}
},
events() {
return [
{
@ -108,7 +115,7 @@ 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('minicardDetailsActions'),
'click .js-open-minicard-details-menu': Popup.open('cardDetailsActions'),
// Drag and drop file upload handlers
'dragover .minicard'(event) {
// Only prevent default for file drags to avoid interfering with sortable
@ -170,6 +177,43 @@ BlazeComponent.extendComponent({
},
}).register('minicard');
BlazeComponent.extendComponent({
template() {
return 'minicardChecklist';
},
events() {
return [
{
'click .js-open-checklist-menu'(event) {
const data = this.currentData();
const checklist = data.checklist || data;
const card = data.card || this.data();
const context = { currentData: () => ({ checklist, card }) };
Popup.open('checklistActions').call(context, event);
},
},
];
},
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) {
return false;
}
// Hide all items if hideAllChecklistItems is true
if (checklist.hideAllChecklistItems) {
return false;
}
return true;
});
},
}).register('minicardChecklist');
Template.minicard.helpers({
hiddenMinicardLabelText() {
const currentUser = ReactiveCache.getCurrentUser();
@ -209,9 +253,29 @@ Template.minicard.helpers({
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
const currentBoard = this.currentBoard;
const currentBoard = this.board();
if (!currentBoard) return false;
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
},
shouldShowChecklistAtMinicard() {
// Return checklists that should be shown on minicard
const currentBoard = this.board();
if (!currentBoard) return [];
const checklists = this.checklists();
const visibleChecklists = [];
checklists.forEach(checklist => {
// Show checklist if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific checklist has the setting enabled
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
visibleChecklists.push(checklist);
}
});
return visibleChecklists;
}
});
@ -242,7 +306,7 @@ BlazeComponent.extendComponent({
}
}).register('editCardSortOrderPopup');
Template.minicardDetailsActionsPopup.events({
Template.cardDetailsActionsPopup.events({
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-move-card': Popup.open('moveCard'),
'click .js-copy-card': Popup.open('copyCard'),

View file

@ -29,6 +29,16 @@ export class DialogWithBoardSwimlaneListCard extends DialogWithBoardSwimlaneList
}
}
/** returns all available cards of the current list */
cards() {
const list = ReactiveCache.getList({_id: this.selectedListId.get(), boardId: this.selectedBoardId.get()});
if (list) {
return list.cards();
} else {
return [];
}
}
/** returns if the card id was the last confirmed one
* @param cardId check this card id
* @return if the card id was the last confirmed one

View file

@ -1584,6 +1584,7 @@
"schedule": "Schedule",
"search-boards-or-operations": "Search boards or operations...",
"show-list-on-minicard": "Show List on Minicard",
"showChecklistAtMinicard": "Show Checklist at Minicard",
"showing": "Showing",
"start-test-operation": "Start Test Operation",
"start-time": "Start Time",

View file

@ -570,6 +570,14 @@ Boards.attachSchema(
defaultValue: false,
},
allowsChecklistAtMinicard: {
/**
* Does the board allow showing checklists on all minicards?
*/
type: Boolean,
defaultValue: false,
},
allowsReceivedDate: {
/**
* Does the board allows received date?
@ -1578,6 +1586,10 @@ Boards.mutations({
return { $set: { allowsShowListsOnMinicard } };
},
setAllowsChecklistAtMinicard(allowsChecklistAtMinicard) {
return { $set: { allowsChecklistAtMinicard } };
},
setAllowsRequestedBy(allowsRequestedBy) {
return { $set: { allowsRequestedBy } };
},

View file

@ -497,13 +497,6 @@ Cards.attachSchema(
type: Boolean,
defaultValue: false,
},
hideFinishedChecklistIfItemsAreHidden: {
/**
* hide completed checklist?
*/
type: Boolean,
optional: true,
},
showListOnMinicard: {
/**
* show list name on minicard?
@ -512,6 +505,14 @@ Cards.attachSchema(
optional: true,
defaultValue: false,
},
showChecklistAtMinicard: {
/**
* show checklist on minicard?
*/
type: Boolean,
optional: true,
defaultValue: false,
},
}),
);
@ -2297,10 +2298,10 @@ Cards.mutations({
};
},
toggleHideFinishedChecklist() {
toggleShowChecklistAtMinicard() {
return {
$set: {
hideFinishedChecklistIfItemsAreHidden: !this.hideFinishedChecklistIfItemsAreHidden,
showChecklistAtMinicard: !this.showChecklistAtMinicard,
}
};
},

View file

@ -77,6 +77,13 @@ Checklists.attachSchema(
type: Boolean,
optional: true,
},
showChecklistAtMinicard: {
/**
* show this checklist on minicard?
*/
type: Boolean,
defaultValue: false,
},
}),
);
@ -189,26 +196,9 @@ Checklists.mutations({
* @param newCardId move the checklist to this cardId
*/
move(newCardId) {
// update every activity
ReactiveCache.getActivities(
{checklistId: this._id}
).forEach(activity => {
Activities.update(activity._id, {
$set: {
cardId: newCardId,
},
});
});
// update every checklist-item
ReactiveCache.getChecklistItems(
{checklistId: this._id}
).forEach(checklistItem => {
ChecklistItems.update(checklistItem._id, {
$set: {
cardId: newCardId,
},
});
});
// Note: Activities and ChecklistItems updates are now handled server-side
// in the moveChecklist Meteor method to avoid client-side permission issues
// update the checklist itself
return {
$set: {
@ -230,9 +220,69 @@ Checklists.mutations({
}
};
},
toggleShowChecklistAtMinicard() {
return {
$set: {
showChecklistAtMinicard: !this.showChecklistAtMinicard,
}
};
},
});
if (Meteor.isServer) {
Meteor.methods({
moveChecklist(checklistId, newCardId) {
check(checklistId, String);
check(newCardId, String);
const checklist = ReactiveCache.getChecklist(checklistId);
if (!checklist) {
throw new Meteor.Error('checklist-not-found', 'Checklist not found');
}
const newCard = ReactiveCache.getCard(newCardId);
if (!newCard) {
throw new Meteor.Error('card-not-found', 'Target card not found');
}
// Check permissions on both source and target cards
const sourceCard = ReactiveCache.getCard(checklist.cardId);
if (!allowIsBoardMemberByCard(this.userId, sourceCard)) {
throw new Meteor.Error('not-authorized', 'Not authorized to move checklist from source card');
}
if (!allowIsBoardMemberByCard(this.userId, newCard)) {
throw new Meteor.Error('not-authorized', 'Not authorized to move checklist to target card');
}
// Update activities
ReactiveCache.getActivities({ checklistId }).forEach(activity => {
Activities.update(activity._id, {
$set: {
cardId: newCardId,
},
});
});
// Update checklist items
ReactiveCache.getChecklistItems({ checklistId }).forEach(checklistItem => {
ChecklistItems.update(checklistItem._id, {
$set: {
cardId: newCardId,
},
});
});
// Update the checklist itself
Checklists.update(checklistId, {
$set: {
cardId: newCardId,
},
});
return checklistId;
},
});
Meteor.startup(() => {
Checklists._collection.createIndex({ modifiedAt: -1 });
Checklists._collection.createIndex({ cardId: 1, createdAt: 1 });