diff --git a/client/components/cards/cardDetails.css b/client/components/cards/cardDetails.css index f89019cf8..c387bf3aa 100644 --- a/client/components/cards/cardDetails.css +++ b/client/components/cards/cardDetails.css @@ -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; diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index ee9bc82e5..0b8f23c7a 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -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'}}") | 🔽 diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 760e96e50..532fc641b 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -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'); diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade index 7dbadf5e6..75d9fbd9c 100644 --- a/client/components/cards/checklists.jade +++ b/client/components/cards/checklists.jade @@ -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'}} diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js index 1447a0886..15a9b9fa1 100644 --- a/client/components/cards/checklists.js +++ b/client/components/cards/checklists.js @@ -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'); diff --git a/client/components/cards/minicard.css b/client/components/cards/minicard.css index bb1430060..2ef301d86 100644 --- a/client/components/cards/minicard.css +++ b/client/components/cards/minicard.css @@ -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; +} diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index ffb900db7..ccb546476 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -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) diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 91ebddc8c..194f37ee9 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -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'), diff --git a/client/lib/dialogWithBoardSwimlaneListCard.js b/client/lib/dialogWithBoardSwimlaneListCard.js index 4513eafb8..75d6cb219 100644 --- a/client/lib/dialogWithBoardSwimlaneListCard.js +++ b/client/lib/dialogWithBoardSwimlaneListCard.js @@ -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 diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index 550c2fcb4..94bfb3925 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -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", diff --git a/models/boards.js b/models/boards.js index d36574f81..8fe525843 100644 --- a/models/boards.js +++ b/models/boards.js @@ -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 } }; }, diff --git a/models/cards.js b/models/cards.js index 023c918ee..d5bbb8bec 100644 --- a/models/cards.js +++ b/models/cards.js @@ -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, } }; }, diff --git a/models/checklists.js b/models/checklists.js index 1a906ccaa..40a348ba5 100644 --- a/models/checklists.js +++ b/models/checklists.js @@ -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 });