diff --git a/client/components/main/dueCards.jade b/client/components/main/dueCards.jade new file mode 100644 index 000000000..c3e58dc1f --- /dev/null +++ b/client/components/main/dueCards.jade @@ -0,0 +1,64 @@ +template(name="dueCardsHeaderBar") + h1 + //a.back-btn(href="{{pathFor 'home'}}") + // i.fa.fa-chevron-left + | {{_ 'dueCards-title'}} + + .board-header-btns.left + a.board-header-btn.js-due-cards-view-change(title="{{_ 'dueCardsViewChange-title'}}") + i.fa.fa-caret-down + if $eq dueCardsView 'me' + i.fa.fa-user + | {{_ 'dueCardsViewChange-choice-me'}} + if $eq dueCardsView 'all' + i.fa.fa-users + | {{_ 'dueCardsViewChange-choice-all'}} + +template(name="dueCardsModalTitle") + h2 + i.fa.fa-keyboard-o + | {{_ 'dueCards-title'}} + +template(name="dueCards") + .wrapper + .due-cards-dueat-list-wrapper + each card in dueCardsList + .due-cards-card-wrapper + a.minicard-wrapper.card-title(href=card.absoluteUrl) + +minicard(card) + ul.due-cards-context-list + li.due-cards-context(title="{{_ 'board'}}") + +viewer + = card.getBoard.title + li.due-cards-context.due-cards-context-separator + = ' ' + | {{_ 'context-separator'}} + = ' ' + li.due-cards-context(title="{{_ 'swimlane'}}") + +viewer + = card.getSwimlane.title + li.due-cards-context + = ' ' + | {{_ 'context-separator'}} + = ' ' + li.due-cards-context(title="{{_ 'list'}}") + +viewer + = card.getList.title + + +template(name="dueCardsViewChangePopup") + ul.pop-over-list + li + with "dueCardsViewChange-choice-me" + a.js-due-cards-view-me + i.fa.fa-user.colorful + | {{_ 'dueCardsViewChange-choice-me'}} + if $eq Utils.dueCardsView "me" + i.fa.fa-check + li + with "dueCardsViewChange-choice-all" + a.js-due-cards-view-all + i.fa.fa-users.colorful + | {{_ 'dueCardsViewChange-choice-all'}} + if $eq Utils.dueCardsView "all" + i.fa.fa-check diff --git a/client/components/main/dueCards.js b/client/components/main/dueCards.js new file mode 100644 index 000000000..984e74cf5 --- /dev/null +++ b/client/components/main/dueCards.js @@ -0,0 +1,140 @@ +BlazeComponent.extendComponent({ + dueCardsView() { + // eslint-disable-next-line no-console + // console.log('sort:', Utils.dueCardsView()); + return Utils.dueCardsView(); + }, + + events() { + return [ + { + 'click .js-due-cards-view-change': Popup.open('dueCardsViewChange'), + }, + ]; + }, +}).register('dueCardsHeaderBar'); + +Template.dueCards.helpers({ + userId() { + return Meteor.userId(); + }, +}); + +BlazeComponent.extendComponent({ + events() { + return [ + { + 'click .js-due-cards-view-me'() { + Utils.setDueCardsView('me'); + Popup.close(); + }, + + 'click .js-due-cards-view-all'() { + Utils.setDueCardsView('all'); + Popup.close(); + }, + }, + ]; + }, +}).register('dueCardsViewChangePopup'); + +BlazeComponent.extendComponent({ + onCreated() { + Meteor.subscribe('setting'); + Meteor.subscribe('dueCards', Utils.dueCardsView() === 'all'); + }, + + dueCardsView() { + // eslint-disable-next-line no-console + console.log('sort:', Utils.dueCardsView()); + return Utils.dueCardsView(); + }, + + sortByBoard() { + return this.dueCardsView() === 'board'; + }, + + dueCardsList() { + const allUsers = Utils.dueCardsView() === 'all'; + + const user = Meteor.user(); + + const archivedBoards = []; + Boards.find({ archived: true }).forEach(board => { + archivedBoards.push(board._id); + }); + + const permiitedBoards = []; + let selector = { + archived: false, + }; + // if user is not an admin allow her to see cards only from public boards + // or those where she is a member + if (!user.isAdmin) { + selector.$or = [ + { permission: 'public' }, + { members: { $elemMatch: { userId: user._id, isActive: true } } }, + ]; + } + Boards.find(selector).forEach(board => { + permiitedBoards.push(board._id); + }); + + const archivedSwimlanes = []; + Swimlanes.find({ archived: true }).forEach(swimlane => { + archivedSwimlanes.push(swimlane._id); + }); + + const archivedLists = []; + Lists.find({ archived: true }).forEach(list => { + archivedLists.push(list._id); + }); + + selector = { + archived: false, + boardId: { + $nin: archivedBoards, + $in: permiitedBoards, + }, + swimlaneId: { $nin: archivedSwimlanes }, + listId: { $nin: archivedLists }, + dueAt: { $ne: null }, + endAt: null, + }; + + if (!allUsers) { + selector.$or = [{ members: user._id }, { assignees: user._id }]; + } + + const cards = []; + + // eslint-disable-next-line no-console + // console.log('cards selector:', selector); + Cards.find(selector).forEach(card => { + cards.push(card); + // eslint-disable-next-line no-console + // console.log( + // 'board:', + // card.board(), + // 'swimlane:', + // card.swimlane(), + // 'list:', + // card.list(), + // ); + }); + + cards.sort((a, b) => { + const x = a.dueAt === null ? Date('2100-12-31') : a.dueAt; + const y = b.dueAt === null ? Date('2100-12-31') : b.dueAt; + + if (x > y) return 1; + else if (x < y) return -1; + + return 0; + }); + + // eslint-disable-next-line no-console + // console.log('cards:', cards); + return cards; + }, +}).register('dueCards'); diff --git a/client/components/main/dueCards.styl b/client/components/main/dueCards.styl new file mode 100644 index 000000000..f49d684be --- /dev/null +++ b/client/components/main/dueCards.styl @@ -0,0 +1,69 @@ +.due-cards-board-wrapper + border-radius: 8px + //padding: 0.5rem + min-width: 400px + border-width: 8px + border-color: grey + border-style: solid + margin-bottom: 2rem + margin-right: auto + margin-left: auto + +.due-cards-board-title + font-size: 1.4rem + font-weight: bold + padding: 0.5rem + background-color: grey + color: white + +.due-cards-swimlane-title + font-size: 1.1rem + font-weight: bold + padding: 0.5rem + padding-bottom: 0.4rem + margin-top: 0 + margin-bottom: 0.5rem + //border-top: black 1px solid + //border-bottom: black 1px solid + text-align: center + +.swimlane-default-color + background-color: lightgrey + +.due-cards-list-title + font-weight: bold + font-size: 1.1rem + //padding-bottom: 0 + //margin-bottom: 0 + text-align: center + margin-bottom: 0.7rem + +.due-cards-list-wrapper + margin: 1rem + border-radius: 5px + padding: 1.5rem + padding-top: 0.75rem + display: inline-block + min-width: 250px + max-width: 350px + +.due-cards-card-wrapper + margin-top: 0 + margin-bottom: 10px + +.due-cards-dueat-list-wrapper + max-width: 500px + margin-right: auto + margin-left: auto + +.due-cards-field-name + font-weight: bold + +.due-cards-context + display: inline-block + +.due-cards-context-separator + font-weight: bold + +.due-cards-context-list + margin-bottom: 0.7rem diff --git a/client/components/main/myCards.jade b/client/components/main/myCards.jade index e16fb8e98..edc8a8857 100644 --- a/client/components/main/myCards.jade +++ b/client/components/main/myCards.jade @@ -5,15 +5,14 @@ template(name="myCardsHeaderBar") | {{_ 'my-cards'}} .board-header-btns.left - a.board-header-btn.js-toggle-my-cards-choose-sort(title="{{_ 'my-cards-sort'}}") - //i.fa.fa-caret-down - i.fa.fa-sort + a.board-header-btn.js-toggle-my-cards-choose-sort(title="{{_ 'myCardsSortChange-title'}}") + i.fa.fa-caret-down if $eq myCardsSort 'board' i.fa.fa-th-large - | {{_ 'my-cards-sort-board'}} + | {{_ 'myCardsSortChange-choice-board'}} if $eq myCardsSort 'dueAt' i.fa.fa-calendar - | {{_ 'my-cards-sort-dueat'}} + | {{_ 'myCardsSortChange-choice-dueat'}} template(name="myCardsModalTitle") h2 @@ -50,21 +49,21 @@ template(name="myCards") ul.my-cards-context-list li.my-cards-context(title="{{_ 'board'}}") +viewer - = card.board.title + = card.getBoard.title li.my-cards-context.my-cards-context-separator = ' ' | {{_ 'context-separator'}} = ' ' li.my-cards-context(title="{{_ 'swimlane'}}") +viewer - = card.swimlane.title + = card.getSwimlane.title li.my-cards-context = ' ' | {{_ 'context-separator'}} = ' ' li.my-cards-context(title="{{_ 'list'}}") +viewer - = card.list.title + = card.getList.title template(name="myCardsSortChangePopup") @@ -73,24 +72,13 @@ template(name="myCardsSortChangePopup") with "my-cards-sort-board" a.js-my-cards-sort-board i.fa.fa-th-large.colorful - | {{_ 'my-cards-sort-board'}} + | {{_ 'myCardsSortChange-choice-board'}} if $eq Utils.myCardsSort "board" i.fa.fa-check li with "my-cards-sort-dueat" a.js-my-cards-sort-dueat i.fa.fa-calendar.colorful - | {{_ 'my-cards-sort-dueat'}} + | {{_ 'myCardsSortChange-choice-dueat'}} if $eq Utils.myCardsSort "dueAt" i.fa.fa-check - -//template(name="myCardsSortChangePopup") -// ul.pop-over-list -// li -// a.js-my-cards-sort-board -// i.fa.fa-th-large.colorful -// | {{_ 'my-cards-sort-board'}} -// li -// a.js-my-cards-sort-dueat -// i.fa.fa-calendar.colorful -// | {{_ 'my-cards-sort-dueat'}} diff --git a/client/components/main/myCards.js b/client/components/main/myCards.js index 7b562c2e5..76a3ab1ee 100644 --- a/client/components/main/myCards.js +++ b/client/components/main/myCards.js @@ -8,12 +8,9 @@ BlazeComponent.extendComponent({ events() { return [ { - 'click .js-toggle-my-cards-choose-sort'() { - // eslint-disable-next-line no-console - // console.log('open sort'); - // Popup.open('myCardsSortChange'); - Utils.myCardsSortToggle(); - }, + 'click .js-toggle-my-cards-choose-sort': Popup.open( + 'myCardsSortChange', + ), }, ]; }, @@ -93,7 +90,7 @@ BlazeComponent.extendComponent({ if (list === null || card.listId !== list._id) { // eslint-disable-next-line no-console // console.log('new list'); - list = card.list(); + list = card.getList(); if (list.archived) { list = null; return; @@ -104,7 +101,7 @@ BlazeComponent.extendComponent({ if (swimlane === null || card.swimlaneId !== swimlane._id) { // eslint-disable-next-line no-console // console.log('new swimlane'); - swimlane = card.swimlane(); + swimlane = card.getSwimlane(); if (swimlane.archived) { swimlane = null; return; @@ -115,7 +112,7 @@ BlazeComponent.extendComponent({ if (board === null || card.boardId !== board._id) { // eslint-disable-next-line no-console // console.log('new board'); - board = card.board(); + board = card.getBoard(); if (board.archived) { board = null; return; @@ -200,7 +197,13 @@ BlazeComponent.extendComponent({ const cards = []; cursor.forEach(card => { - cards.push(card); + if ( + !card.getBoard().archived && + !card.getSwimlane().archived && + !card.getList().archived + ) { + cards.push(card); + } }); cards.sort((a, b) => { diff --git a/client/lib/utils.js b/client/lib/utils.js index 94d7ed017..156b37e4b 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -69,6 +69,31 @@ Utils = { location.reload(); }, + archivedBoardIds() { + const archivedBoards = []; + Boards.find({ archived: false }).forEach(board => { + archivedBoards.push(board._id); + }); + return archivedBoards; + }, + + dueCardsView() { + let view = window.localStorage.getItem('dueCardsView'); + + if (!view || !['me', 'all'].includes(view)) { + window.localStorage.setItem('dueCardsView', 'me'); + location.reload(); + view = 'me'; + } + + return view; + }, + + setDueCardsView(view) { + window.localStorage.setItem('dueCardsView', view); + location.reload(); + }, + // XXX We should remove these two methods goBoardId(_id) { const board = Boards.findOne(_id); diff --git a/config/router.js b/config/router.js index 473fcbbff..a87817055 100644 --- a/config/router.js +++ b/config/router.js @@ -139,6 +139,32 @@ FlowRouter.route('/my-cards', { }, }); +FlowRouter.route('/due-cards', { + name: 'due-cards', + action() { + const dueCardsTemplate = 'dueCards'; + + Filter.reset(); + // EscapeActions.executeAll(); + EscapeActions.executeUpTo('popup-close'); + + Utils.manageCustomUI(); + Utils.manageMatomo(); + + // if (previousPath) { + // Modal.open(dueCardsTemplate, { + // header: 'dueCardsModalTitle', + // onCloseGoTo: previousPath, + // }); + // } else { + BlazeLayout.render('defaultLayout', { + headerBar: 'dueCardsHeaderBar', + content: dueCardsTemplate, + }); + // } + }, +}); + FlowRouter.route('/import/:source', { name: 'import', triggersEnter: [AccountsTemplates.ensureSignedIn], diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index ec62e6a75..6d96a6649 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -854,7 +854,11 @@ "list": "List", "board": "Board", "context-separator": "/", - "my-cards-sort": "My Cards Sort", - "my-cards-sort-board": "By Board", - "my-cards-sort-dueat": "By Due Date" + "myCardsSortChange-title": "My Cards Sort", + "myCardsSortChange-choice-board": "By Board", + "myCardsSortChange-choice-dueat": "By Due Date", + "dueCards-title": "Due Cards", + "dueCardsViewChange-title": "Due Cards View", + "dueCardsViewChange-choice-me": "Me", + "dueCardsViewChange-choice-all": "All Users" } diff --git a/models/cards.js b/models/cards.js index d8d1513dd..92e80931a 100644 --- a/models/cards.js +++ b/models/cards.js @@ -469,6 +469,45 @@ Cards.helpers({ return Boards.findOne(this.boardId); }, + getList() { + const list = this.list(); + if (!list) { + return { + _id: this.listId, + title: 'Undefined List', + archived: false, + colorClass: '', + }; + } + return list; + }, + + getSwimlane() { + const swimlane = this.swimlane(); + if (!swimlane) { + return { + _id: this.swimlaneId, + title: 'Undefined Swimlane', + archived: false, + colorClass: '', + }; + } + return swimlane; + }, + + getBoard() { + const board = this.board(); + if (!board) { + return { + _id: this.boardId, + title: 'Undefined Board', + archived: false, + colorClass: '', + }; + } + return board; + }, + labels() { const boardLabels = this.board().labels; const cardLabels = _.filter(boardLabels, label => { diff --git a/server/publications/cards.js b/server/publications/cards.js index e9209f01e..54efe5684 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -32,3 +32,90 @@ Meteor.publish('myCards', function() { }, ); }); + +Meteor.publish('dueCards', function(allUsers = false) { + check(allUsers, Boolean); + + // eslint-disable-next-line no-console + // console.log('all users:', allUsers); + + const user = Users.findOne(this.userId); + + const archivedBoards = []; + Boards.find({ archived: true }).forEach(board => { + archivedBoards.push(board._id); + }); + + const permiitedBoards = []; + let selector = { + archived: false, + }; + // if user is not an admin allow her to see cards only from boards where + // she is a member + if (!user.isAdmin) { + selector.$or = [ + { permission: 'public' }, + { members: { $elemMatch: { userId: user._id, isActive: true } } }, + ]; + } + Boards.find(selector).forEach(board => { + permiitedBoards.push(board._id); + }); + + const archivedSwimlanes = []; + Swimlanes.find({ archived: true }).forEach(swimlane => { + archivedSwimlanes.push(swimlane._id); + }); + + const archivedLists = []; + Lists.find({ archived: true }).forEach(list => { + archivedLists.push(list._id); + }); + + selector = { + archived: false, + boardId: { $nin: archivedBoards, $in: permiitedBoards }, + swimlaneId: { $nin: archivedSwimlanes }, + listId: { $nin: archivedLists }, + dueAt: { $ne: null }, + endAt: null, + }; + + if (!allUsers) { + selector.$or = [{ members: user._id }, { assignees: user._id }]; + } + + const cards = Cards.find(selector, { + fields: { + _id: 1, + archived: 1, + boardId: 1, + swimlaneId: 1, + listId: 1, + title: 1, + type: 1, + sort: 1, + members: 1, + assignees: 1, + colors: 1, + dueAt: 1, + }, + }); + + const boards = []; + const swimlanes = []; + const lists = []; + + cards.forEach(card => { + if (card.boardId) boards.push(card.boardId); + if (card.swimlaneId) swimlanes.push(card.swimlaneId); + if (card.listId) lists.push(card.listId); + }); + + return [ + cards, + Boards.find({ _id: { $in: boards } }), + Swimlanes.find({ _id: { $in: swimlanes } }), + Lists.find({ _id: { $in: lists } }), + ]; +});