From 5fa0821e078ff03647d23909517ddf6984f8baf5 Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Thu, 24 Jan 2019 16:41:23 +0100 Subject: [PATCH 01/11] card colors: remove unused variables --- client/components/cards/cardDetails.js | 1 - client/components/cards/minicard.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 046200843..79a686a71 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -27,7 +27,6 @@ BlazeComponent.extendComponent({ onCreated() { this.currentBoard = Boards.findOne(Session.get('currentBoard')); this.isLoaded = new ReactiveVar(false); - this.currentColor = new ReactiveVar(this.data().color); const boardBody = this.parentComponent().parentComponent(); //in Miniview parent is Board, not BoardBody. if (boardBody !== null) { diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index e468ec567..da7f9e01b 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -3,10 +3,6 @@ // }); BlazeComponent.extendComponent({ - onCreated() { - this.currentColor = new ReactiveVar(this.data().color); - }, - template() { return 'minicard'; }, From dd88eb4cc191a06f7eb84213b026dfb93546f245 Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Thu, 24 Jan 2019 12:09:23 +0100 Subject: [PATCH 02/11] swimlane-view: have the swimlane header horizontal This allows to use the header as a separator between swimlanes. This will be most useful when we can set the background color of these headers. --- client/components/boards/boardBody.jade | 2 ++ client/components/lists/list.styl | 1 - client/components/swimlanes/swimlanes.jade | 36 ++++++++------------- client/components/swimlanes/swimlanes.js | 37 +++++++--------------- client/components/swimlanes/swimlanes.styl | 18 ++++------- 5 files changed, 33 insertions(+), 61 deletions(-) diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 9e4b9c61b..382c04f36 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -23,6 +23,8 @@ template(name="boardBody") if isViewSwimlanes each currentBoard.swimlanes +swimlane(this) + if currentUser.isBoardMember + +addSwimlaneForm if isViewLists +listsGroup if isViewCalendar diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl index 72cb19f42..ec8359612 100644 --- a/client/components/lists/list.styl +++ b/client/components/lists/list.styl @@ -10,7 +10,6 @@ // transparent, because that won't work during a list drag. background: darken(white, 13%) border-left: 1px solid darken(white, 20%) - border-bottom: 1px solid #CCC padding: 0 float: left diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 76f54c660..4380de2b3 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -1,21 +1,22 @@ template(name="swimlane") .swimlane.js-lists.js-swimlane +swimlaneHeader - if isMiniScreen - if currentList - +list(currentList) + .swimlane.list-group.js-lists + if isMiniScreen + if currentList + +list(currentList) + else + each currentBoard.lists + +miniList(this) + if currentUser.isBoardMember + +addListForm else each currentBoard.lists - +miniList(this) + +list(this) + if currentCardIsInThisList _id ../_id + +cardDetails(currentCard) if currentUser.isBoardMember +addListForm - else - each currentBoard.lists - +list(this) - if currentCardIsInThisList _id ../_id - +cardDetails(currentCard) - if currentUser.isBoardMember - +addListAndSwimlaneForm template(name="listsGroup") .swimlane.list-group.js-lists @@ -35,19 +36,8 @@ template(name="listsGroup") if currentUser.isBoardMember +addListForm -template(name="addListAndSwimlaneForm") +template(name="addSwimlaneForm") .list.list-composer.js-list-composer - .list-header - +inlinedForm(autoclose=false) - input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}" - autocomplete="off" autofocus) - .edit-controls.clearfix - button.primary.confirm(type="submit") {{_ 'save'}} - a.fa.fa-times-thin.js-close-inlined-form - else - a.open-list-composer.js-open-inlined-form - i.fa.fa-plus - | {{_ 'add-list'}} .list-header +inlinedForm(autoclose=false) input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}" diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index 865895a9c..a7743ec7f 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -185,37 +185,22 @@ BlazeComponent.extendComponent({ return [{ submit(evt) { evt.preventDefault(); - let titleInput = this.find('.list-name-input'); - if (titleInput) { - const title = titleInput.value.trim(); - if (title) { - Lists.insert({ - title, - boardId: Session.get('currentBoard'), - sort: $('.list').length, - }); + let titleInput = this.find('.swimlane-name-input'); + const title = titleInput.value.trim(); + if (title) { + Swimlanes.insert({ + title, + boardId: Session.get('currentBoard'), + sort: $('.swimlane').length, + }); - titleInput.value = ''; - titleInput.focus(); - } - } else { - titleInput = this.find('.swimlane-name-input'); - const title = titleInput.value.trim(); - if (title) { - Swimlanes.insert({ - title, - boardId: Session.get('currentBoard'), - sort: $('.swimlane').length, - }); - - titleInput.value = ''; - titleInput.focus(); - } + titleInput.value = ''; + titleInput.focus(); } }, }]; }, -}).register('addListAndSwimlaneForm'); +}).register('addSwimlaneForm'); Template.swimlane.helpers({ canSeeAddList() { diff --git a/client/components/swimlanes/swimlanes.styl b/client/components/swimlanes/swimlanes.styl index abcc90d4e..fe7f5e539 100644 --- a/client/components/swimlanes/swimlanes.styl +++ b/client/components/swimlanes/swimlanes.styl @@ -5,7 +5,7 @@ // transparent, because that won't work during a swimlane drag. background: darken(white, 13%) display: flex - flex-direction: row + flex-direction: column overflow: 0; max-height: 100% @@ -27,20 +27,15 @@ .swimlane-header-wrap display: flex; flex-direction: row; - flex: 0 0 50px; - padding-bottom: 30px; - border-bottom: 1px solid #CCC + flex: 0 0 24px; + background-color: #ccc; .swimlane-header - -ms-writing-mode: tb-rl; - writing-mode: vertical-rl; - transform: rotate(180deg); font-size: 14px; - line-height: 50px; - margin-top: 50px; + padding: 5px 5px font-weight: bold; min-height: 9px; - width: 50px; + width: 100%; overflow: hidden; -o-text-overflow: ellipsis; text-overflow: ellipsis; @@ -49,7 +44,8 @@ .swimlane-header-menu position: absolute - padding: 20px 20px + padding: 5px 5px .list-group + flex-direction: row height: 100% From 416b17062e57f215206e93a85b02ef9eb1ab4902 Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Thu, 24 Jan 2019 15:16:13 +0100 Subject: [PATCH 03/11] Remove the 'Add Swimlane' entry and replace it by a plus sign Still need to create the swimlane right after the one that has been created --- client/components/boards/boardBody.jade | 2 -- .../components/swimlanes/swimlaneHeader.jade | 9 ++++++ client/components/swimlanes/swimlaneHeader.js | 28 +++++++++++++++++++ client/components/swimlanes/swimlanes.jade | 14 ---------- client/components/swimlanes/swimlanes.js | 27 ------------------ client/components/swimlanes/swimlanes.styl | 4 +++ i18n/en.i18n.json | 1 + 7 files changed, 42 insertions(+), 43 deletions(-) diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 382c04f36..9e4b9c61b 100644 --- a/client/components/boards/boardBody.jade +++ b/client/components/boards/boardBody.jade @@ -23,8 +23,6 @@ template(name="boardBody") if isViewSwimlanes each currentBoard.swimlanes +swimlane(this) - if currentUser.isBoardMember - +addSwimlaneForm if isViewLists +listsGroup if isViewCalendar diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade index 483de06f4..3e20e2d00 100644 --- a/client/components/swimlanes/swimlaneHeader.jade +++ b/client/components/swimlanes/swimlaneHeader.jade @@ -8,6 +8,7 @@ template(name="swimlaneHeader") = title .swimlane-header-menu unless currentUser.isCommentOnly + a.fa.fa-plus.js-open-add-swimlane-menu.swimlane-header-plus-icon a.fa.fa-navicon.js-open-swimlane-menu template(name="editSwimlaneTitleForm") @@ -21,3 +22,11 @@ template(name="swimlaneActionPopup") unless currentUser.isCommentOnly ul.pop-over-list li: a.js-close-swimlane {{_ 'archive-swimlane'}} + +template(name="swimlaneAddPopup") + unless currentUser.isCommentOnly + form + input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}" + autocomplete="off" autofocus) + .edit-controls.clearfix + button.primary.confirm(type="submit") {{_ 'add'}} diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index 50635f861..72437ba42 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -11,6 +11,7 @@ BlazeComponent.extendComponent({ events() { return [{ 'click .js-open-swimlane-menu': Popup.open('swimlaneAction'), + 'click .js-open-add-swimlane-menu': Popup.open('swimlaneAdd'), submit: this.editTitle, }]; }, @@ -23,3 +24,30 @@ Template.swimlaneActionPopup.events({ Popup.close(); }, }); + +BlazeComponent.extendComponent({ + events() { + return [{ + submit(evt) { + evt.preventDefault(); + const titleInput = this.find('.swimlane-name-input'); + const title = titleInput.value.trim(); + if (title) { + Swimlanes.insert({ + title, + boardId: Session.get('currentBoard'), + // XXX we should insert the swimlane right after the caller + sort: $('.swimlane').length, + }); + + titleInput.value = ''; + titleInput.focus(); + } + // XXX ideally, we should move the popup to the newly + // created swimlane so a user can add more than one swimlane + // with a minimum of interactions + Popup.close(); + }, + }]; + }, +}).register('swimlaneAddPopup'); diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 4380de2b3..cd864a7c9 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -36,20 +36,6 @@ template(name="listsGroup") if currentUser.isBoardMember +addListForm -template(name="addSwimlaneForm") - .list.list-composer.js-list-composer - .list-header - +inlinedForm(autoclose=false) - input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}" - autocomplete="off" autofocus) - .edit-controls.clearfix - button.primary.confirm(type="submit") {{_ 'save'}} - a.fa.fa-times-thin.js-close-inlined-form - else - a.open-list-composer.js-open-inlined-form - i.fa.fa-plus - | {{_ 'add-swimlane'}} - template(name="addListForm") .list.list-composer.js-list-composer .list-header diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index a7743ec7f..71317714e 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -175,33 +175,6 @@ BlazeComponent.extendComponent({ }, }).register('addListForm'); -BlazeComponent.extendComponent({ - // Proxy - open() { - this.childComponents('inlinedForm')[0].open(); - }, - - events() { - return [{ - submit(evt) { - evt.preventDefault(); - let titleInput = this.find('.swimlane-name-input'); - const title = titleInput.value.trim(); - if (title) { - Swimlanes.insert({ - title, - boardId: Session.get('currentBoard'), - sort: $('.swimlane').length, - }); - - titleInput.value = ''; - titleInput.focus(); - } - }, - }]; - }, -}).register('addSwimlaneForm'); - Template.swimlane.helpers({ canSeeAddList() { return Meteor.user() && Meteor.user().isBoardMember() && !Meteor.user().isCommentOnly(); diff --git a/client/components/swimlanes/swimlanes.styl b/client/components/swimlanes/swimlanes.styl index fe7f5e539..71089bb4f 100644 --- a/client/components/swimlanes/swimlanes.styl +++ b/client/components/swimlanes/swimlanes.styl @@ -46,6 +46,10 @@ position: absolute padding: 5px 5px + .swimlane-header-plus-icon + margin-left: 5px + margin-right: 10px + .list-group flex-direction: row height: 100% diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 6c5f22a5e..1890f4887 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -337,6 +337,7 @@ "list-select-cards": "Select all cards in this list", "listActionPopup-title": "List Actions", "swimlaneActionPopup-title": "Swimlane Actions", + "swimlaneAddPopup-title": "Add a Swimlane below", "listImportCardPopup-title": "Import a Trello card", "listMorePopup-title": "More", "link-list": "Link to this list", From c075187088e69d30db31489d75b22f991e1972ff Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Thu, 24 Jan 2019 20:45:52 +0100 Subject: [PATCH 04/11] swimlane: insert the new swimlane after the one we clicked on --- client/components/swimlanes/swimlaneHeader.js | 13 +++++++++++-- models/boards.js | 11 +++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index 72437ba42..632a0f508 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -1,3 +1,5 @@ +const { calculateIndexData } = Utils; + BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); @@ -26,18 +28,25 @@ Template.swimlaneActionPopup.events({ }); BlazeComponent.extendComponent({ + onCreated() { + this.currentSwimlane = this.currentData(); + }, + events() { return [{ submit(evt) { evt.preventDefault(); + const currentBoard = Boards.findOne(Session.get('currentBoard')); + const nextSwimlane = currentBoard.nextSwimlane(this.currentSwimlane); const titleInput = this.find('.swimlane-name-input'); const title = titleInput.value.trim(); + const sortValue = calculateIndexData(this.currentSwimlane, nextSwimlane, 1); + if (title) { Swimlanes.insert({ title, boardId: Session.get('currentBoard'), - // XXX we should insert the swimlane right after the caller - sort: $('.swimlane').length, + sort: sortValue.base, }); titleInput.value = ''; diff --git a/models/boards.js b/models/boards.js index 99480ca70..d92bec47a 100644 --- a/models/boards.js +++ b/models/boards.js @@ -351,6 +351,17 @@ Boards.helpers({ return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); }, + nextSwimlane(swimlane) { + return Swimlanes.findOne({ + boardId: this._id, + archived: false, + sort: { $gte: swimlane.sort }, + _id: { $ne: swimlane._id }, + }, { + sort: { sort: 1 }, + }); + }, + hasOvertimeCards(){ const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} ); return card !== undefined; From 03efeaeb1abae0c8c39ad5644d44bad36f415d99 Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Thu, 24 Jan 2019 16:47:09 +0100 Subject: [PATCH 05/11] Add colors to swimlanes fixes #1688 --- .../components/swimlanes/swimlaneHeader.jade | 14 +++- client/components/swimlanes/swimlaneHeader.js | 37 +++++++++ client/components/swimlanes/swimlanes.styl | 81 +++++++++++++++++++ i18n/en.i18n.json | 1 + models/swimlanes.js | 32 ++++++++ 5 files changed, 164 insertions(+), 1 deletion(-) diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade index 3e20e2d00..33eb5731f 100644 --- a/client/components/swimlanes/swimlaneHeader.jade +++ b/client/components/swimlanes/swimlaneHeader.jade @@ -1,5 +1,5 @@ template(name="swimlaneHeader") - .swimlane-header-wrap.js-swimlane-header + .swimlane-header-wrap.js-swimlane-header(class='{{#if colorClass}}swimlane-{{colorClass}}{{/if}}') +inlinedForm +editSwimlaneTitleForm else @@ -20,6 +20,9 @@ template(name="editSwimlaneTitleForm") template(name="swimlaneActionPopup") unless currentUser.isCommentOnly + ul.pop-over-list + li: a.js-set-swimlane-color {{_ 'select-color'}} + hr ul.pop-over-list li: a.js-close-swimlane {{_ 'archive-swimlane'}} @@ -30,3 +33,12 @@ template(name="swimlaneAddPopup") autocomplete="off" autofocus) .edit-controls.clearfix button.primary.confirm(type="submit") {{_ 'add'}} + +template(name="setSwimlaneColorPopup") + form.edit-label + .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'}} + button.js-remove-color.negate.wide.right {{_ 'unset-color'}} diff --git a/client/components/swimlanes/swimlaneHeader.js b/client/components/swimlanes/swimlaneHeader.js index 632a0f508..1004cb254 100644 --- a/client/components/swimlanes/swimlaneHeader.js +++ b/client/components/swimlanes/swimlaneHeader.js @@ -1,5 +1,10 @@ const { calculateIndexData } = Utils; +let swimlaneColors; +Meteor.startup(() => { + swimlaneColors = Swimlanes.simpleSchema()._schema.color.allowedValues; +}); + BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); @@ -20,6 +25,7 @@ BlazeComponent.extendComponent({ }).register('swimlaneHeader'); Template.swimlaneActionPopup.events({ + 'click .js-set-swimlane-color': Popup.open('setSwimlaneColor'), 'click .js-close-swimlane' (evt) { evt.preventDefault(); this.archive(); @@ -60,3 +66,34 @@ BlazeComponent.extendComponent({ }]; }, }).register('swimlaneAddPopup'); + +BlazeComponent.extendComponent({ + onCreated() { + this.currentSwimlane = this.currentData(); + this.currentColor = new ReactiveVar(this.currentSwimlane.color); + }, + + colors() { + return swimlaneColors.map((color) => ({ color, name: '' })); + }, + + isSelected(color) { + return this.currentColor.get() === color; + }, + + events() { + return [{ + 'click .js-palette-color'() { + this.currentColor.set(this.currentData().color); + }, + 'click .js-submit' () { + this.currentSwimlane.setColor(this.currentColor.get()); + Popup.close(); + }, + 'click .js-remove-color'() { + this.currentSwimlane.setColor(null); + Popup.close(); + }, + }]; + }, +}).register('setSwimlaneColorPopup'); diff --git a/client/components/swimlanes/swimlanes.styl b/client/components/swimlanes/swimlanes.styl index 71089bb4f..e4e2cd3b9 100644 --- a/client/components/swimlanes/swimlanes.styl +++ b/client/components/swimlanes/swimlanes.styl @@ -53,3 +53,84 @@ .list-group flex-direction: row height: 100% + +swimlane-color(background, color...) + background: background !important + if color + color: color !important //overwrite text for better visibility + +.swimlane-white + swimlane-color(#ffffff, #4d4d4d) //Black text for better visibility + border: 1px solid #eee + +.swimlane-green + swimlane-color(#3cb500, #ffffff) //White text for better visibility + +.swimlane-yellow + swimlane-color(#fad900, #4d4d4d) //Black text for better visibility + +.swimlane-orange + swimlane-color(#ff9f19, #4d4d4d) //Black text for better visibility + +.swimlane-red + swimlane-color(#eb4646, #ffffff) //White text for better visibility + +.swimlane-purple + swimlane-color(#a632db, #ffffff) //White text for better visibility + +.swimlane-blue + swimlane-color(#0079bf, #ffffff) //White text for better visibility + +.swimlane-pink + swimlane-color(#ff78cb, #4d4d4d) //Black text for better visibility + +.swimlane-sky + swimlane-color(#00c2e0, #ffffff) //White text for better visibility + +.swimlane-black + swimlane-color(#4d4d4d, #ffffff) //White text for better visibility + +.swimlane-lime + swimlane-color(#51e898, #4d4d4d) //Black text for better visibility + +.swimlane-silver + swimlane-color(unset, #4d4d4d) //Black text for better visibility + +.swimlane-peachpuff + swimlane-color(#ffdab9, #4d4d4d) //Black text for better visibility + +.swimlane-crimson + swimlane-color(#dc143c, #ffffff) //White text for better visibility + +.swimlane-plum + swimlane-color(#dda0dd, #4d4d4d) //Black text for better visibility + +.swimlane-darkgreen + swimlane-color(#006400, #ffffff) //White text for better visibility + +.swimlane-slateblue + swimlane-color(#6a5acd, #ffffff) //White text for better visibility + +.swimlane-magenta + swimlane-color(#ff00ff, #ffffff) //White text for better visibility + +.swimlane-gold + swimlane-color(#ffd700, #4d4d4d) //Black text for better visibility + +.swimlane-navy + swimlane-color(#000080, #ffffff) //White text for better visibility + +.swimlane-gray + swimlane-color(#808080, #ffffff) //White text for better visibility + +.swimlane-saddlebrown + swimlane-color(#8b4513, #ffffff) //White text for better visibility + +.swimlane-paleturquoise + swimlane-color(#afeeee, #4d4d4d) //Black text for better visibility + +.swimlane-mistyrose + swimlane-color(#ffe4e1, #4d4d4d) //Black text for better visibility + +.swimlane-indigo + swimlane-color(#4b0082, #ffffff) //White text for better visibility diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 1890f4887..409946bbb 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -520,6 +520,7 @@ "editCardEndDatePopup-title": "Change end date", "setCardColorPopup-title": "Set color", "setCardActionsColorPopup-title": "Choose a color", + "setSwimlaneColorPopup-title": "Choose a color", "assigned-by": "Assigned By", "requested-by": "Requested By", "board-delete-notice": "Deleting is permanent. You will lose all lists, cards and actions associated with this board.", diff --git a/models/swimlanes.js b/models/swimlanes.js index fa5245da0..93057362e 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -49,6 +49,21 @@ Swimlanes.attachSchema(new SimpleSchema({ // XXX We should probably provide a default optional: true, }, + color: { + /** + * the color of the swimlane + */ + type: String, + optional: true, + // silver is the default, so it is left out + allowedValues: [ + 'white', 'green', 'yellow', 'orange', 'red', 'purple', + 'blue', 'sky', 'lime', 'pink', 'black', + 'peachpuff', 'crimson', 'plum', 'darkgreen', + 'slateblue', 'magenta', 'gold', 'navy', 'gray', + 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', + ], + }, updatedAt: { /** * when was the swimlane last edited @@ -93,6 +108,12 @@ Swimlanes.helpers({ board() { return Boards.findOne(this.boardId); }, + + colorClass() { + if (this.color) + return this.color; + return ''; + }, }); Swimlanes.mutations({ @@ -107,6 +128,17 @@ Swimlanes.mutations({ restore() { return { $set: { archived: false } }; }, + + setColor(newColor) { + if (newColor === 'silver') { + newColor = null; + } + return { + $set: { + color: newColor, + }, + }; + }, }); Swimlanes.hookOptions.after.update = { fetchPrevious: false }; From 5c6a725712a443b4d03b4f86262033ddfb66bc3d Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Fri, 25 Jan 2019 10:47:36 +0100 Subject: [PATCH 06/11] Make sure Swimlanes and Lists have a populated sort field When moving around the swimlanes or the lists, if one element has a sort with a null value, the computation of the new sort value is aborted, meaning that there are glitches in the UI. This happens on the first swimlane created with the new board, or when a swimlane or a list gets added through the API. --- client/components/boards/boardBody.js | 31 +++++++++++++++++++++++++++ models/boards.js | 16 ++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index ccbd0f23b..ae5b67fd4 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -35,6 +35,37 @@ BlazeComponent.extendComponent({ this._isDragging = false; // Used to set the overlay this.mouseHasEnterCardDetails = false; + + // fix swimlanes sort field if there are null values + const currentBoardData = Boards.findOne(Session.get('currentBoard')); + const nullSortSwimlanes = currentBoardData.nullSortSwimlanes(); + if (nullSortSwimlanes.count() > 0) { + const swimlanes = currentBoardData.swimlanes(); + let count = 0; + swimlanes.forEach((s) => { + Swimlanes.update(s._id, { + $set: { + sort: count, + }, + }); + count += 1; + }); + } + + // fix lists sort field if there are null values + const nullSortLists = currentBoardData.nullSortLists(); + if (nullSortLists.count() > 0) { + const lists = currentBoardData.lists(); + let count = 0; + lists.forEach((l) => { + Lists.update(l._id, { + $set: { + sort: count, + }, + }); + count += 1; + }); + } }, onRendered() { const boardComponent = this; diff --git a/models/boards.js b/models/boards.js index d92bec47a..b0f5cecba 100644 --- a/models/boards.js +++ b/models/boards.js @@ -347,6 +347,14 @@ Boards.helpers({ return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); }, + nullSortLists() { + return Lists.find({ + boardId: this._id, + archived: false, + sort: { $eq: null }, + }); + }, + swimlanes() { return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); }, @@ -362,6 +370,14 @@ Boards.helpers({ }); }, + nullSortSwimlanes() { + return Swimlanes.find({ + boardId: this._id, + archived: false, + sort: { $eq: null }, + }); + }, + hasOvertimeCards(){ const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} ); return card !== undefined; From b5411841cf6aa33b2c0d29d85cbc795e3faa7f4f Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Fri, 25 Jan 2019 10:57:46 +0100 Subject: [PATCH 07/11] api: fix the sort field when inserting a swimlane or a list This has the side effect of always inserting the element at the end. --- models/lists.js | 2 ++ models/swimlanes.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/models/lists.js b/models/lists.js index 0e1ba8013..39ff130a1 100644 --- a/models/lists.js +++ b/models/lists.js @@ -314,9 +314,11 @@ if (Meteor.isServer) { try { Authentication.checkUserId( req.userId); const paramBoardId = req.params.boardId; + const board = Boards.findOne(paramBoardId); const id = Lists.insert({ title: req.body.title, boardId: paramBoardId, + sort: board.lists().count(), }); JsonRoutes.sendResult(res, { code: 200, diff --git a/models/swimlanes.js b/models/swimlanes.js index 93057362e..e2c3925c4 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -256,9 +256,11 @@ if (Meteor.isServer) { try { Authentication.checkUserId( req.userId); const paramBoardId = req.params.boardId; + const board = Boards.findOne(paramBoardId); const id = Swimlanes.insert({ title: req.body.title, boardId: paramBoardId, + sort: board.swimlanes().count(), }); JsonRoutes.sendResult(res, { code: 200, From 6c3dbc3c6f52a42ddbeeaec9bbfcc82c1c839f7d Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Fri, 25 Jan 2019 12:44:27 +0100 Subject: [PATCH 08/11] api: new_card: add the card at the end of the list If we keep the `0` value, the card might be inserted in the middle of the list, making it hard to find it later on. Always append the card at the end of the list by setting a sort value based on the number of cards in the list. --- models/cards.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/models/cards.js b/models/cards.js index 9b32e89a1..ff19a9a0e 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1526,6 +1526,10 @@ if (Meteor.isServer) { Authentication.checkUserId(req.userId); const paramBoardId = req.params.boardId; const paramListId = req.params.listId; + const currentCards = Cards.find({ + listId: paramListId, + archived: false, + }, { sort: ['sort'] }); const check = Users.findOne({ _id: req.body.authorId, }); @@ -1538,7 +1542,7 @@ if (Meteor.isServer) { description: req.body.description, userId: req.body.authorId, swimlaneId: req.body.swimlaneId, - sort: 0, + sort: currentCards.count(), members, }); JsonRoutes.sendResult(res, { From 78c779faafad2010842bfccca9ef5c483530c892 Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Thu, 24 Jan 2019 09:13:49 +0100 Subject: [PATCH 09/11] client: lists headers: use padding instead of margin No visual changes but allows to set a background color to the list header. --- client/components/lists/list.styl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl index ec8359612..c2bfa3db8 100644 --- a/client/components/lists/list.styl +++ b/client/components/lists/list.styl @@ -45,7 +45,7 @@ .list-header flex: 0 0 auto - margin: 20px 12px 4px + padding: 20px 12px 4px position: relative min-height: 20px @@ -73,10 +73,10 @@ .list-header-menu position: absolute - padding: 7px + padding: 27px 19px margin-top: 1px - top: -@padding - right: -@padding + top: -7px + right: -7px .list-header-plus-icon color: #a6a6a6 From d0a9d8c581f9356f5e72ccb698fc3963c59e96cd Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Fri, 25 Jan 2019 15:56:40 +0100 Subject: [PATCH 10/11] colors: add per list color Hamburger menu only. Note that I am definitively not responsible for the resulting Christmas tree. fixes #328 --- client/components/lists/list.styl | 81 +++++++++++++++++++++++++ client/components/lists/listHeader.jade | 15 ++++- client/components/lists/listHeader.js | 37 +++++++++++ i18n/en.i18n.json | 2 + models/lists.js | 32 ++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl index c2bfa3db8..91823bdbe 100644 --- a/client/components/lists/list.styl +++ b/client/components/lists/list.styl @@ -197,3 +197,84 @@ .search-card-results max-height: 250px overflow: hidden + +list-header-color(background, color...) + background: background !important + if color + color: color !important //overwrite text for better visibility + +.list-header-white + list-header-color(#ffffff, #4d4d4d) //Black text for better visibility + border: 1px solid #eee + +.list-header-green + list-header-color(#3cb500, #ffffff) //White text for better visibility + +.list-header-yellow + list-header-color(#fad900, #4d4d4d) //Black text for better visibility + +.list-header-orange + list-header-color(#ff9f19, #4d4d4d) //Black text for better visibility + +.list-header-red + list-header-color(#eb4646, #ffffff) //White text for better visibility + +.list-header-purple + list-header-color(#a632db, #ffffff) //White text for better visibility + +.list-header-blue + list-header-color(#0079bf, #ffffff) //White text for better visibility + +.list-header-pink + list-header-color(#ff78cb, #4d4d4d) //Black text for better visibility + +.list-header-sky + list-header-color(#00c2e0, #ffffff) //White text for better visibility + +.list-header-black + list-header-color(#4d4d4d, #ffffff) //White text for better visibility + +.list-header-lime + list-header-color(#51e898, #4d4d4d) //Black text for better visibility + +.list-header-silver + list-header-color(unset, #4d4d4d) //Black text for better visibility + +.list-header-peachpuff + list-header-color(#ffdab9, #4d4d4d) //Black text for better visibility + +.list-header-crimson + list-header-color(#dc143c, #ffffff) //White text for better visibility + +.list-header-plum + list-header-color(#dda0dd, #4d4d4d) //Black text for better visibility + +.list-header-darkgreen + list-header-color(#006400, #ffffff) //White text for better visibility + +.list-header-slateblue + list-header-color(#6a5acd, #ffffff) //White text for better visibility + +.list-header-magenta + list-header-color(#ff00ff, #ffffff) //White text for better visibility + +.list-header-gold + list-header-color(#ffd700, #4d4d4d) //Black text for better visibility + +.list-header-navy + list-header-color(#000080, #ffffff) //White text for better visibility + +.list-header-gray + list-header-color(#808080, #ffffff) //White text for better visibility + +.list-header-saddlebrown + list-header-color(#8b4513, #ffffff) //White text for better visibility + +.list-header-paleturquoise + list-header-color(#afeeee, #4d4d4d) //Black text for better visibility + +.list-header-mistyrose + list-header-color(#ffe4e1, #4d4d4d) //Black text for better visibility + +.list-header-indigo + list-header-color(#4b0082, #ffffff) //White text for better visibility diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 25ab8c20b..48005eaf1 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -1,5 +1,6 @@ template(name="listHeader") - .list-header.js-list-header + .list-header.js-list-header( + class="{{#if colorClass}}list-header-{{colorClass}}{{/if}}") +inlinedForm +editListTitleForm else @@ -49,6 +50,9 @@ template(name="listActionPopup") li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}} unless currentUser.isCommentOnly hr + ul.pop-over-list + li: a.js-set-color-list {{_ 'set-color-list'}} + hr ul.pop-over-list if cards.count li: a.js-select-cards {{_ 'list-select-cards'}} @@ -111,3 +115,12 @@ template(name="wipLimitErrorPopup") p {{_ 'wipLimitErrorPopup-dialog-pt1'}} p {{_ 'wipLimitErrorPopup-dialog-pt2'}} button.full.js-back-view(type="submit") {{_ 'cancel'}} + +template(name="setListColorPopup") + form.edit-label + .palette-colors: each colors + span.card-label.palette-color.js-palette-color(class="list-header-{{color}}") + if(isSelected color) + i.fa.fa-check + button.primary.confirm.js-submit {{_ 'save'}} + button.js-remove-color.negate.wide.right {{_ 'unset-color'}} diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index abcc46397..25e6cb691 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -1,3 +1,8 @@ +let listsColors; +Meteor.startup(() => { + listsColors = Lists.simpleSchema()._schema.color.allowedValues; +}); + BlazeComponent.extendComponent({ canSeeAddCard() { const list = Template.currentData(); @@ -72,6 +77,7 @@ Template.listActionPopup.helpers({ Template.listActionPopup.events({ 'click .js-list-subscribe' () {}, + 'click .js-set-color-list': Popup.open('setListColor'), 'click .js-select-cards' () { const cardIds = this.allCards().map((card) => card._id); MultiSelection.add(cardIds); @@ -154,3 +160,34 @@ Template.listMorePopup.events({ Utils.goBoardId(this.boardId); }), }); + +BlazeComponent.extendComponent({ + onCreated() { + this.currentList = this.currentData(); + this.currentColor = new ReactiveVar(this.currentList.color); + }, + + colors() { + return listsColors.map((color) => ({ color, name: '' })); + }, + + isSelected(color) { + return this.currentColor.get() === color; + }, + + events() { + return [{ + 'click .js-palette-color'() { + this.currentColor.set(this.currentData().color); + }, + 'click .js-submit' () { + this.currentList.setColor(this.currentColor.get()); + Popup.close(); + }, + 'click .js-remove-color'() { + this.currentList.setColor(null); + Popup.close(); + }, + }]; + }, +}).register('setListColorPopup'); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 409946bbb..2c2d41da6 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -335,6 +335,7 @@ "list-archive-cards-pop": "This will remove all the cards in this list from the board. To view cards in Archive and bring them back to the board, click “Menu” > “Archive”.", "list-move-cards": "Move all cards in this list", "list-select-cards": "Select all cards in this list", + "set-color-list": "Set Color", "listActionPopup-title": "List Actions", "swimlaneActionPopup-title": "Swimlane Actions", "swimlaneAddPopup-title": "Add a Swimlane below", @@ -521,6 +522,7 @@ "setCardColorPopup-title": "Set color", "setCardActionsColorPopup-title": "Choose a color", "setSwimlaneColorPopup-title": "Choose a color", + "setListColorPopup-title": "Choose a color", "assigned-by": "Assigned By", "requested-by": "Requested By", "board-delete-notice": "Deleting is permanent. You will lose all lists, cards and actions associated with this board.", diff --git a/models/lists.js b/models/lists.js index 39ff130a1..54e7d0373 100644 --- a/models/lists.js +++ b/models/lists.js @@ -92,6 +92,21 @@ Lists.attachSchema(new SimpleSchema({ type: Boolean, defaultValue: false, }, + color: { + /** + * the color of the list + */ + type: String, + optional: true, + // silver is the default, so it is left out + allowedValues: [ + 'white', 'green', 'yellow', 'orange', 'red', 'purple', + 'blue', 'sky', 'lime', 'pink', 'black', + 'peachpuff', 'crimson', 'plum', 'darkgreen', + 'slateblue', 'magenta', 'gold', 'navy', 'gray', + 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', + ], + }, })); Lists.allow({ @@ -148,6 +163,12 @@ Lists.helpers({ return list.wipLimit[option] ? list.wipLimit[option] : 0; // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set } }, + + colorClass() { + if (this.color) + return this.color; + return ''; + }, }); Lists.mutations({ @@ -174,6 +195,17 @@ Lists.mutations({ setWipLimit(limit) { return { $set: { 'wipLimit.value': limit } }; }, + + setColor(newColor) { + if (newColor === 'silver') { + newColor = null; + } + return { + $set: { + color: newColor, + }, + }; + }, }); Meteor.methods({ From 97d95b4bcbcab86629e368ea41bb9f00450b21f6 Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Fri, 25 Jan 2019 15:58:52 +0100 Subject: [PATCH 11/11] ui: lists: make sure all lists boxes are the same height When `Show card count` is enabled, the lists with the card counts have two lines of text while the lists without have only one. This results in the box around the list headers are not of the same size and this is visible when setting a color to the list. --- client/components/lists/list.styl | 5 +++++ client/components/lists/listHeader.jade | 1 + 2 files changed, 6 insertions(+) diff --git a/client/components/lists/list.styl b/client/components/lists/list.styl index 91823bdbe..c12a2c73e 100644 --- a/client/components/lists/list.styl +++ b/client/components/lists/list.styl @@ -43,12 +43,16 @@ background: white margin: -3px 0 8px +.list-header-card-count + height: 35px + .list-header flex: 0 0 auto padding: 20px 12px 4px position: relative min-height: 20px + &.ui-sortable-handle cursor: grab @@ -67,6 +71,7 @@ text-overflow: ellipsis word-wrap: break-word + .list-header-watch-icon padding-left: 10px color: #a6a6a6 diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 48005eaf1..eafcc510a 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -1,5 +1,6 @@ template(name="listHeader") .list-header.js-list-header( + class="{{#if limitToShowCardsCount}}list-header-card-count{{/if}}" class="{{#if colorClass}}list-header-{{colorClass}}{{/if}}") +inlinedForm +editListTitleForm