From 5cc6a2f7d01c33f93d426f0b563d8344e2de8fbd Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Thu, 1 Apr 2021 23:40:07 +0200 Subject: [PATCH 01/11] Add an avatar for the card creator * Add avatar to minicard * Add field to card details * Add show option in Board / Card Settings --- client/components/activities/comments.jade | 2 +- client/components/cards/cardDetails.jade | 11 ++++++++++- client/components/cards/cardDetails.js | 2 +- client/components/cards/cardDetails.styl | 1 + client/components/cards/minicard.jade | 4 ++++ client/components/cards/minicard.js | 12 ++++++++++++ client/components/cards/minicard.styl | 8 ++++++-- client/components/sidebar/sidebar.jade | 8 ++++++++ client/components/sidebar/sidebar.js | 21 +++++++++++++++++++++ client/components/users/userAvatar.jade | 7 ++++--- client/components/users/userAvatar.js | 1 + i18n/en.i18n.json | 3 ++- models/boards.js | 12 ++++++++++++ server/publications/cards.js | 4 ++++ 14 files changed, 87 insertions(+), 9 deletions(-) diff --git a/client/components/activities/comments.jade b/client/components/activities/comments.jade index 405778de0..4cddf4c23 100644 --- a/client/components/activities/comments.jade +++ b/client/components/activities/comments.jade @@ -1,7 +1,7 @@ template(name="commentForm") .new-comment.js-new-comment( class="{{#if commentFormIsOpen}}is-open{{/if}}") - +userAvatar(userId=currentUser._id) + +userAvatar(userId=currentUser._id noRemove=true) form.js-new-comment-form +editor(class="js-new-comment-input") | {{getUnsavedValue 'cardComment' currentCard._id}} diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 245ab773d..42426e92f 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -110,9 +110,18 @@ template(name="cardDetails") a.card-label.add-label.js-end-date i.fa.fa-plus + hr + if currentBoard.allowsCreator + .card-details-item.card-details-item-creator + h3.card-details-item-title + i.fa.fa-user + | {{_ 'creator'}} + + +userAvatar(userId=userId noRemove=true) + | {{! XXX Hack to hide syntaxic coloration /// }} + //.card-details-items if currentBoard.allowsMembers - hr .card-details-item.card-details-item-members h3.card-details-item-title i.fa.fa-users diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index c745f0730..4278bdd07 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -356,7 +356,7 @@ BlazeComponent.extendComponent({ 'click .js-go-to-linked-card'() { Utils.goCardId(this.data().linkedId); }, - 'click .js-member': Popup.open('cardMember'), + // 'click .js-member': Popup.open('cardMember'), 'click .js-add-members': Popup.open('cardMembers'), 'click .js-assignee': Popup.open('cardAssignee'), 'click .js-add-assignees': Popup.open('cardAssignees'), diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl index a2216390f..5e57d49bc 100644 --- a/client/components/cards/cardDetails.styl +++ b/client/components/cards/cardDetails.styl @@ -211,6 +211,7 @@ avatar-radius = 50% word-wrap: break-word max-width: 36% flex-grow: 1 + &.card-details-item-creator, &.card-details-item-received, &.card-details-item-start, &.card-details-item-due, diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 3931bffbc..764eb6481 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -96,6 +96,10 @@ template(name="minicard") each getMembers +userAvatar(userId=this) + if showCreator + .minicard-creator + +userAvatar(userId=this.userId noRemove=true) + .badges unless currentUser.isNoComments if comments.count diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 88348fc48..94cff243e 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -21,6 +21,18 @@ BlazeComponent.extendComponent({ }).format(customFieldTrueValue); }, + showCreator() { + if (this.data().board()) { + return ( + this.data().board.allowsCreator === null || + this.data().board().allowsCreator === undefined || + this.data().board().allowsCreator + ); + // return this.data().board().allowsCreator; + } + return false; + }, + events() { return [ { diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index 003c677b5..4e7bf53b7 100644 --- a/client/components/cards/minicard.styl +++ b/client/components/cards/minicard.styl @@ -89,7 +89,7 @@ border-radius: 2px margin-right: 3px margin-bottom: 3px - + .minicard-custom-fields display:block; .minicard-custom-field @@ -163,7 +163,8 @@ line-height: 12px .minicard-members, - .minicard-assignees + .minicard-assignees, + .minicard-creator float: right margin-left: 5px margin-bottom: 4px @@ -187,6 +188,9 @@ .minicard-assignees border-bottom: 1px solid red + .minicard-creator + border-bottom: 1px solid green + .minicard-members:empty, .minicard-assignees:empty display: none diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 09af600bb..85394f027 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -105,6 +105,14 @@ template(name="boardCardSettingsPopup") span i.fa.fa-users | {{_ 'members'}} + + div.check-div + a.flex.js-field-has-creator(class="{{#if allowsCreator}}is-checked{{/if}}") + .materialCheckBox(class="{{#if allowsCreator}}is-checked{{/if}}") + span + i.fa.fa-user + | {{_ 'creator'}} + div.check-div a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}") .materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}") diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index f4f7f45cb..5c1026c12 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -730,6 +730,14 @@ BlazeComponent.extendComponent({ return this.currentBoard.allowsSubtasks; }, + allowsCreator() { + return ( + this.currentBoard.allowsCreator === null || + this.currentBoard.allowsCreator === undefined || + this.currentBoard.allowsCreator + ); + }, + allowsMembers() { return this.currentBoard.allowsMembers; }, @@ -889,6 +897,19 @@ BlazeComponent.extendComponent({ this.currentBoard.allowsSubtasks, ); }, + 'click .js-field-has-creator'(evt) { + evt.preventDefault(); + this.currentBoard.allowsCreator = !this.currentBoard.allowsCreator; + this.currentBoard.setAllowsCreator(this.currentBoard.allowsCreator); + $(`.js-field-has-creator ${MCB}`).toggleClass( + CKCLS, + this.currentBoard.allowsCreator, + ); + $('.js-field-has-creator').toggleClass( + CKCLS, + this.currentBoard.allowsCreator, + ); + }, 'click .js-field-has-members'(evt) { evt.preventDefault(); this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers; diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade index 988a2626d..87bbb8c6a 100644 --- a/client/components/users/userAvatar.jade +++ b/client/components/users/userAvatar.jade @@ -72,9 +72,10 @@ template(name="cardMemberPopup") h3= user.profile.fullname p.quiet @{{ user.username }} ul.pop-over-list - if currentUser.isNotCommentOnly - if currentUser.isNotWorker - li: a.js-remove-member {{_ 'remove-member-from-card'}} + unless noRemove + if currentUser.isNotCommentOnly + if currentUser.isNotWorker + li: a.js-remove-member {{_ 'remove-member-from-card'}} if $eq currentUser._id user._id with currentUser diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js index 262a63aff..7b8a32145 100644 --- a/client/components/users/userAvatar.js +++ b/client/components/users/userAvatar.js @@ -32,6 +32,7 @@ Template.userAvatar.helpers({ Template.userAvatar.events({ 'click .js-change-avatar': Popup.open('changeAvatar'), + 'click .js-member': Popup.open('cardMember'), }); Template.userAvatarInitials.helpers({ diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index e20645c51..916351156 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -988,5 +988,6 @@ "hide-system-messages-of-all-users": "Hide system messages of all users", "now-system-messages-of-all-users-are-hidden": "Now system messages of all users are hidden", "move-swimlane": "Move Swimlane", - "moveSwimlanePopup-title": "Move Swimlane" + "moveSwimlanePopup-title": "Move Swimlane", + "creator": "Creator" } diff --git a/models/boards.js b/models/boards.js index b78d43a8e..598ae53c3 100644 --- a/models/boards.js +++ b/models/boards.js @@ -372,6 +372,14 @@ Boards.attachSchema( defaultValue: true, }, + allowsCreator: { + /** + * Does the board allow creator? + */ + type: Boolean, + defaultValue: true, + }, + allowsAssignee: { /** * Does the board allows assignee? @@ -1187,6 +1195,10 @@ Boards.mutations({ return { $set: { allowsSubtasks } }; }, + setAllowsCreator(allowsCreator) { + return { $set: { allowsCreator } }; + }, + setAllowsMembers(allowsMembers) { return { $set: { allowsMembers } }; }, diff --git a/server/publications/cards.js b/server/publications/cards.js index 33314ef30..871af13e6 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -488,6 +488,7 @@ function buildProjection(query) { modifiedAt: 1, labelIds: 1, customFields: 1, + userId: 1, }, sort: { boardId: 1, @@ -658,6 +659,9 @@ function findCards(sessionId, query) { if (card.boardId) boards.push(card.boardId); if (card.swimlaneId) swimlanes.push(card.swimlaneId); if (card.listId) lists.push(card.listId); + if (card.userId) { + users.push(card.userId); + } if (card.members) { card.members.forEach(userId => { users.push(userId); From edd07befe21b4ac941423eae5fe593098e69fdc5 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Thu, 1 Apr 2021 23:41:32 +0200 Subject: [PATCH 02/11] Add a new search operator `creator` --- client/components/main/globalSearch.js | 2 + config/query-classes.js | 4 +- config/search-const.js | 5 +- i18n/en.i18n.json | 2 + server/publications/cards.js | 67 ++++++++++++-------------- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 50bc9ae7e..294ac378f 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -123,6 +123,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { operator_member_abbrev: TAPi18n.__('operator-member-abbrev'), operator_assignee: TAPi18n.__('operator-assignee'), operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'), + operator_creator: TAPi18n.__('operator-creator'), operator_due: TAPi18n.__('operator-due'), operator_created: TAPi18n.__('operator-created'), operator_modified: TAPi18n.__('operator-modified'), @@ -167,6 +168,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { ['\n* ', 'globalSearch-instructions-operator-at'], ['\n* ', 'globalSearch-instructions-operator-member'], ['\n* ', 'globalSearch-instructions-operator-assignee'], + ['\n* ', 'globalSearch-instructions-operator-creator'], ['\n* ', 'globalSearch-instructions-operator-due'], ['\n* ', 'globalSearch-instructions-operator-created'], ['\n* ', 'globalSearch-instructions-operator-modified'], diff --git a/config/query-classes.js b/config/query-classes.js index 2c8a49ee0..3955bcd75 100644 --- a/config/query-classes.js +++ b/config/query-classes.js @@ -2,7 +2,7 @@ import { OPERATOR_ASSIGNEE, OPERATOR_BOARD, OPERATOR_COMMENT, - OPERATOR_CREATED_AT, + OPERATOR_CREATED_AT, OPERATOR_CREATOR, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, @@ -107,6 +107,7 @@ export class QueryErrors { [OPERATOR_USER, 'user-username-not-found'], [OPERATOR_ASSIGNEE, 'user-username-not-found'], [OPERATOR_MEMBER, 'user-username-not-found'], + [OPERATOR_CREATOR, 'user-username-not-found'], ]; constructor() { @@ -238,6 +239,7 @@ export class Query { 'operator-member': OPERATOR_MEMBER, 'operator-member-abbrev': OPERATOR_MEMBER, 'operator-assignee': OPERATOR_ASSIGNEE, + 'operator-creator': OPERATOR_CREATOR, 'operator-assignee-abbrev': OPERATOR_ASSIGNEE, 'operator-status': OPERATOR_STATUS, 'operator-due': OPERATOR_DUE, diff --git a/config/search-const.js b/config/search-const.js index 26f8ad00b..5a7f54b6b 100644 --- a/config/search-const.js +++ b/config/search-const.js @@ -1,14 +1,15 @@ export const DEFAULT_LIMIT = 25; -export const OPERATOR_ASSIGNEE = 'assignee'; +export const OPERATOR_ASSIGNEE = 'assignees'; export const OPERATOR_COMMENT = 'comment'; export const OPERATOR_CREATED_AT = 'createdAt'; +export const OPERATOR_CREATOR = 'userId'; export const OPERATOR_DUE = 'dueAt'; export const OPERATOR_BOARD = 'board'; export const OPERATOR_HAS = 'has'; export const OPERATOR_LABEL = 'label'; export const OPERATOR_LIMIT = 'limit'; export const OPERATOR_LIST = 'list'; -export const OPERATOR_MEMBER = 'member'; +export const OPERATOR_MEMBER = 'members'; export const OPERATOR_MODIFIED_AT = 'modifiedAt'; export const OPERATOR_SORT = 'sort'; export const OPERATOR_STATUS = 'status'; diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 916351156..73141e8b1 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -901,6 +901,7 @@ "operator-member-abbrev": "m", "operator-assignee": "assignee", "operator-assignee-abbrev": "a", + "operator-creator": "creator", "operator-status": "status", "operator-due": "due", "operator-created": "created", @@ -952,6 +953,7 @@ "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:`", "globalSearch-instructions-operator-member": "`__operator_member__:` - cards where ** is a *member*", "globalSearch-instructions-operator-assignee": "`__operator_assignee__:` - cards where ** is an *assignee*", + "globalSearch-instructions-operator-creator": "`__operator_creator__:` - cards where ** is the card's creator", "globalSearch-instructions-operator-due": "`__operator_due__:` - cards which are due up to ** days from now. `__operator_due__:__predicate_overdue__ lists all cards past their due date.", "globalSearch-instructions-operator-created": "`__operator_created__:` - cards which were created ** days ago or less", "globalSearch-instructions-operator-modified": "`__operator_modified__:` - cards which were modified ** days ago or less", diff --git a/server/publications/cards.js b/server/publications/cards.js index 871af13e6..1af29bdb0 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -15,12 +15,15 @@ import { OPERATOR_ASSIGNEE, OPERATOR_BOARD, OPERATOR_COMMENT, + OPERATOR_CREATED_AT, + OPERATOR_CREATOR, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, OPERATOR_LIMIT, OPERATOR_LIST, OPERATOR_MEMBER, + OPERATOR_MODIFIED_AT, OPERATOR_SORT, OPERATOR_STATUS, OPERATOR_SWIMLANE, @@ -163,7 +166,7 @@ function buildSelector(queryParams) { if (queryParams.hasOperator(OPERATOR_BOARD)) { const queryBoards = []; - queryParams.hasOperator(OPERATOR_BOARD).forEach(query => { + queryParams.getPredicates(OPERATOR_BOARD).forEach(query => { const boards = Boards.userSearch(userId, { title: new RegExp(escapeForRegex(query), 'i'), }); @@ -240,7 +243,7 @@ function buildSelector(queryParams) { } } - [OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => { + [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].forEach(field => { if (queryParams.hasOperator(field)) { selector[field] = {}; const predicate = queryParams.getPredicate(field); @@ -251,52 +254,42 @@ function buildSelector(queryParams) { const queryUsers = {}; queryUsers[OPERATOR_ASSIGNEE] = []; queryUsers[OPERATOR_MEMBER] = []; + queryUsers[OPERATOR_CREATOR] = []; if (queryParams.hasOperator(OPERATOR_USER)) { + const users = []; queryParams.getPredicates(OPERATOR_USER).forEach(username => { const user = Users.findOne({ username }); if (user) { - queryUsers[OPERATOR_MEMBER].push(user._id); - queryUsers[OPERATOR_ASSIGNEE].push(user._id); + users.push(user._id); } else { errors.addNotFound(OPERATOR_USER, username); } }); - } - - [OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => { - if (queryParams.hasOperator(key)) { - queryParams.getPredicates(key).forEach(query => { - const users = Users.find({ - username: query, - }); - if (users.count()) { - users.forEach(user => { - queryUsers[key].push(user._id); - }); - } else { - errors.addNotFound(key, query); - } + if (users.length) { + selector.$and.push({ + $or: [{ members: { $in: users } }, { assignees: { $in: users } }], }); } - }); - - if ( - queryUsers[OPERATOR_MEMBER].length && - queryUsers[OPERATOR_ASSIGNEE].length - ) { - selector.$and.push({ - $or: [ - { members: { $in: queryUsers[OPERATOR_MEMBER] } }, - { assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } }, - ], - }); - } else if (queryUsers[OPERATOR_MEMBER].length) { - selector.members = { $in: queryUsers[OPERATOR_MEMBER] }; - } else if (queryUsers[OPERATOR_ASSIGNEE].length) { - selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] }; } + [OPERATOR_MEMBER, OPERATOR_ASSIGNEE, OPERATOR_CREATOR].forEach(key => { + if (queryParams.hasOperator(key)) { + const users = []; + queryParams.getPredicates(key).forEach(username => { + const user = Users.findOne({ username }); + if (user) { + users.push(user._id); + } else { + errors.addNotFound(key, username); + } + }); + if (users.length) { + selector[key] = { $in: users }; + } + } + }); + if (queryParams.hasOperator(OPERATOR_LABEL)) { queryParams.getPredicates(OPERATOR_LABEL).forEach(label => { const queryLabels = []; @@ -443,9 +436,9 @@ function buildSelector(queryParams) { } // eslint-disable-next-line no-console - // console.log('selector:', selector); + console.log('selector:', selector); // eslint-disable-next-line no-console - // console.log('selector.$and:', selector.$and); + console.log('selector.$and:', selector.$and); const query = new Query(); query.selector = selector; From e43002d5adf79f2ef85a88e2bd1b8d6a7c2f4a21 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Fri, 2 Apr 2021 02:15:12 +0200 Subject: [PATCH 03/11] Update `Boards.userBoards()` to only return type 'board' by default --- models/boards.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/models/boards.js b/models/boards.js index 598ae53c3..42b356daa 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1330,8 +1330,11 @@ Boards.userBoards = (userId, archived = false, selector = {}) => { if (typeof archived === 'boolean') { selector.archived = archived; } - selector.$or = [{ permission: 'public' }]; + if (!selector.type) { + selector.type = 'board'; + } + selector.$or = [{ permission: 'public' }]; if (userId) { selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } }); } @@ -1435,17 +1438,15 @@ if (Meteor.isServer) { }, myLabelNames() { let names = []; - Boards.userBoards(Meteor.userId(), false, { type: 'board' }).forEach( - board => { - names = names.concat( - board.labels - .filter(label => !!label.name) - .map(label => { - return label.name; - }), - ); - }, - ); + Boards.userBoards(Meteor.userId()).forEach(board => { + names = names.concat( + board.labels + .filter(label => !!label.name) + .map(label => { + return label.name; + }), + ); + }); return _.uniq(names).sort(); }, myBoardNames() { From 69dc8f304cc0b8b35ed044de60e95730ab19f52b Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Fri, 2 Apr 2021 21:40:52 +0200 Subject: [PATCH 04/11] Only search label names in boards of type 'board' --- models/boards.js | 1 + models/cards.js | 1 + server/publications/cards.js | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/models/boards.js b/models/boards.js index 42b356daa..40435bfca 100644 --- a/models/boards.js +++ b/models/boards.js @@ -505,6 +505,7 @@ Boards.attachSchema( type: { /** * The type of board + * possible values: board, template-board, template-container */ type: String, defaultValue: 'board', diff --git a/models/cards.js b/models/cards.js index a0aed5c69..71f0e343d 100644 --- a/models/cards.js +++ b/models/cards.js @@ -299,6 +299,7 @@ Cards.attachSchema( type: { /** * type of the card + * possible values: cardType-card, cardType-linkedCard, cardType-linkedBoard */ type: String, defaultValue: 'cardType-card', diff --git a/server/publications/cards.js b/server/publications/cards.js index 1af29bdb0..c8c2497f7 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -294,7 +294,7 @@ function buildSelector(queryParams) { queryParams.getPredicates(OPERATOR_LABEL).forEach(label => { const queryLabels = []; - let boards = Boards.userSearch(userId, { + let boards = Boards.userBoards(userId, null, { labels: { $elemMatch: { color: label.toLowerCase() } }, }); @@ -318,7 +318,7 @@ function buildSelector(queryParams) { const reLabel = new RegExp(escapeForRegex(label), 'i'); // eslint-disable-next-line no-console // console.log('reLabel:', reLabel); - boards = Boards.userSearch(userId, { + boards = Boards.userBoards(userId, null, { labels: { $elemMatch: { name: reLabel } }, }); From 302ba75729102e6fbb848ebefe898a5a7403fb05 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 3 Apr 2021 01:19:02 +0200 Subject: [PATCH 05/11] * Fix bug with multiple label predicates * Add new constants --- client/components/main/globalSearch.jade | 3 +- client/lib/cardSearch.js | 2 +- config/const.js | 51 ++++++++++++++++++++ config/query-classes.js | 3 +- models/boards.js | 59 ++++++------------------ models/cards.js | 39 ++++------------ server/publications/cards.js | 18 ++++---- 7 files changed, 88 insertions(+), 87 deletions(-) create mode 100644 config/const.js diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index bd2493124..9d480c3c0 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -50,8 +50,7 @@ template(name="globalSearch") each msg in errorMessages li.global-search-error-messages = msg - else - +resultsPaged(this) + +resultsPaged(this) else if serverError.get .global-search-page .global-search-help diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index 9d89f2447..4ba9ee017 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -72,7 +72,7 @@ export class CardSearchPagedComponent extends BlazeComponent { if (this.queryErrors.length) { // console.log('queryErrors:', this.queryErrorMessages()); this.hasQueryErrors.set(true); - return null; + // return null; } if (cards) { diff --git a/config/const.js b/config/const.js new file mode 100644 index 000000000..0c9c71c7c --- /dev/null +++ b/config/const.js @@ -0,0 +1,51 @@ +export const ALLOWED_BOARD_COLORS = [ + 'belize', + 'nephritis', + 'pomegranate', + 'pumpkin', + 'wisteria', + 'moderatepink', + 'strongcyan', + 'limegreen', + 'midnight', + 'dark', + 'relax', + 'corteza', + 'clearblue', + 'natural', + 'modern', + 'moderndark', +]; +export const ALLOWED_COLORS = [ + 'white', + 'green', + 'yellow', + 'orange', + 'red', + 'purple', + 'blue', + 'sky', + 'lime', + 'pink', + 'black', + 'silver', + 'peachpuff', + 'crimson', + 'plum', + 'darkgreen', + 'slateblue', + 'magenta', + 'gold', + 'navy', + 'gray', + 'saddlebrown', + 'paleturquoise', + 'mistyrose', + 'indigo', +]; +export const TYPE_BOARD = 'board'; +export const TYPE_CARD = 'cardType-card'; +export const TYPE_LINKED_BOARD = 'cardType-linkedBoard'; +export const TYPE_LINKED_CARD = 'cardType-linkedCard'; +export const TYPE_TEMPLATE_BOARD = 'template-board'; +export const TYPE_TEMPLATE_CONTAINER = 'template-container'; diff --git a/config/query-classes.js b/config/query-classes.js index 3955bcd75..48c596617 100644 --- a/config/query-classes.js +++ b/config/query-classes.js @@ -2,7 +2,8 @@ import { OPERATOR_ASSIGNEE, OPERATOR_BOARD, OPERATOR_COMMENT, - OPERATOR_CREATED_AT, OPERATOR_CREATOR, + OPERATOR_CREATED_AT, + OPERATOR_CREATOR, OPERATOR_DUE, OPERATOR_HAS, OPERATOR_LABEL, diff --git a/models/boards.js b/models/boards.js index 40435bfca..b54957fb9 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1,3 +1,11 @@ +import { + ALLOWED_BOARD_COLORS, + ALLOWED_COLORS, + TYPE_BOARD, + TYPE_TEMPLATE_BOARD, + TYPE_TEMPLATE_CONTAINER, +} from '../config/const'; + const escapeForRegex = require('escape-string-regexp'); Boards = new Mongo.Collection('boards'); @@ -144,32 +152,7 @@ Boards.attachSchema( * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo` */ type: String, - allowedValues: [ - 'green', - 'yellow', - 'orange', - 'red', - 'purple', - 'blue', - 'sky', - 'lime', - 'pink', - 'black', - 'silver', - 'peachpuff', - 'crimson', - 'plum', - 'darkgreen', - 'slateblue', - 'magenta', - 'gold', - 'navy', - 'gray', - 'saddlebrown', - 'paleturquoise', - 'mistyrose', - 'indigo', - ], + allowedValues: ALLOWED_COLORS, }, // XXX We might want to maintain more informations under the member sub- // documents like de-normalized meta-data (the date the member joined the @@ -246,28 +229,11 @@ Boards.attachSchema( * The color of the board. */ type: String, - allowedValues: [ - 'belize', - 'nephritis', - 'pomegranate', - 'pumpkin', - 'wisteria', - 'moderatepink', - 'strongcyan', - 'limegreen', - 'midnight', - 'dark', - 'relax', - 'corteza', - 'clearblue', - 'natural', - 'modern', - 'moderndark', - ], + allowedValues: ALLOWED_BOARD_COLORS, // eslint-disable-next-line consistent-return autoValue() { if (this.isInsert && !this.isSet) { - return Boards.simpleSchema()._schema.color.allowedValues[0]; + return ALLOWED_BOARD_COLORS[0]; } }, }, @@ -508,7 +474,8 @@ Boards.attachSchema( * possible values: board, template-board, template-container */ type: String, - defaultValue: 'board', + defaultValue: TYPE_BOARD, + allowedValues: [TYPE_BOARD, TYPE_TEMPLATE_BOARD, TYPE_TEMPLATE_CONTAINER], }, sort: { /** diff --git a/models/cards.js b/models/cards.js index 71f0e343d..192fa2e3a 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1,3 +1,10 @@ +import { + ALLOWED_COLORS, + TYPE_CARD, + TYPE_LINKED_BOARD, + TYPE_LINKED_CARD, +} from '../config/const'; + Cards = new Mongo.Collection('cards'); // XXX To improve pub/sub performances a card document should include a @@ -77,33 +84,7 @@ Cards.attachSchema( color: { type: String, optional: true, - allowedValues: [ - 'white', - 'green', - 'yellow', - 'orange', - 'red', - 'purple', - 'blue', - 'sky', - 'lime', - 'pink', - 'black', - 'silver', - 'peachpuff', - 'crimson', - 'plum', - 'darkgreen', - 'slateblue', - 'magenta', - 'gold', - 'navy', - 'gray', - 'saddlebrown', - 'paleturquoise', - 'mistyrose', - 'indigo', - ], + allowedValues: ALLOWED_COLORS, }, createdAt: { /** @@ -299,10 +280,10 @@ Cards.attachSchema( type: { /** * type of the card - * possible values: cardType-card, cardType-linkedCard, cardType-linkedBoard */ type: String, - defaultValue: 'cardType-card', + defaultValue: TYPE_CARD, + allowedValues: [TYPE_CARD, TYPE_LINKED_CARD, TYPE_LINKED_BOARD], }, linkedId: { /** diff --git a/server/publications/cards.js b/server/publications/cards.js index c8c2497f7..38395851c 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -102,7 +102,7 @@ function buildSelector(queryParams) { let selector = {}; // eslint-disable-next-line no-console - // console.log('queryParams:', queryParams); + console.log('queryParams:', queryParams); if (queryParams.selector) { selector = queryParams.selector; @@ -291,9 +291,8 @@ function buildSelector(queryParams) { }); if (queryParams.hasOperator(OPERATOR_LABEL)) { + const queryLabels = []; queryParams.getPredicates(OPERATOR_LABEL).forEach(label => { - const queryLabels = []; - let boards = Boards.userBoards(userId, null, { labels: { $elemMatch: { color: label.toLowerCase() } }, }); @@ -339,9 +338,12 @@ function buildSelector(queryParams) { errors.addNotFound(OPERATOR_LABEL, label); } } - - selector.labelIds = { $in: _.uniq(queryLabels) }; }); + if (queryLabels.length) { + // eslint-disable-next-line no-console + // console.log('queryLabels:', queryLabels); + selector.labelIds = { $in: _.uniq(queryLabels) }; + } } if (queryParams.hasOperator(OPERATOR_HAS)) { @@ -597,9 +599,9 @@ function findCards(sessionId, query) { // eslint-disable-next-line no-console // console.log('projection:', projection); let cards; - if (!query.hasErrors()) { - cards = Cards.find(query.selector, query.projection); - } + // if (!query.hasErrors()) { + cards = Cards.find(query.selector, query.projection); + // } // eslint-disable-next-line no-console // console.log('count:', cards.count()); From ed6c6e5a2576ffd3e6137849e9e0214b18d4b514 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Sat, 3 Apr 2021 18:52:47 +0200 Subject: [PATCH 06/11] Use new colors constants --- models/boards.js | 4 ++-- models/lists.js | 29 +++-------------------------- models/swimlanes.js | 29 +++-------------------------- 3 files changed, 8 insertions(+), 54 deletions(-) diff --git a/models/boards.js b/models/boards.js index b54957fb9..5ed06f4d1 100644 --- a/models/boards.js +++ b/models/boards.js @@ -4,7 +4,7 @@ import { TYPE_BOARD, TYPE_TEMPLATE_BOARD, TYPE_TEMPLATE_CONTAINER, -} from '../config/const'; +} from '/config/const'; const escapeForRegex = require('escape-string-regexp'); Boards = new Mongo.Collection('boards'); @@ -1324,7 +1324,7 @@ Boards.colorMap = () => { }; Boards.labelColors = () => { - return _.clone(Boards.simpleSchema()._schema['labels.$.color'].allowedValues); + return ALLOWED_COLORS; }; if (Meteor.isServer) { diff --git a/models/lists.js b/models/lists.js index 49cda140b..29f71eb01 100644 --- a/models/lists.js +++ b/models/lists.js @@ -1,3 +1,5 @@ +import { ALLOWED_COLORS } from '/config/const'; + Lists = new Mongo.Collection('lists'); /** @@ -144,32 +146,7 @@ Lists.attachSchema( 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', - ], + allowedValues: ALLOWED_COLORS, }, type: { /** diff --git a/models/swimlanes.js b/models/swimlanes.js index 8cb0c8e9c..1c803cdc9 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -1,3 +1,5 @@ +import { ALLOWED_COLORS } from '/config/const'; + Swimlanes = new Mongo.Collection('swimlanes'); /** @@ -68,32 +70,7 @@ Swimlanes.attachSchema( 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', - ], + allowedValues: ALLOWED_COLORS, }, updatedAt: { /** From 0e593c7d810b3f30da64ce312bdc1798aac273e2 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Tue, 6 Apr 2021 12:24:08 +0200 Subject: [PATCH 07/11] Add admin reports to admin panel * Broken cards * Attachments * Orphaned Attachments --- client/components/main/globalSearch.jade | 7 +- client/components/main/globalSearch.js | 5 +- client/components/settings/adminReports.jade | 85 ++++++++++++++ client/components/settings/adminReports.js | 108 ++++++++++++++++++ client/components/settings/settingHeader.jade | 4 + client/lib/cardSearch.js | 3 + config/router.js | 22 ++++ i18n/en.i18n.json | 5 +- models/attachments.js | 5 + 9 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 client/components/settings/adminReports.jade create mode 100644 client/components/settings/adminReports.js diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 9d480c3c0..31e9584e4 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -11,9 +11,10 @@ template(name="globalSearchModalTitle") | {{_ 'globalSearch-title'}} template(name="resultsPaged") - h1 - = resultsHeading.get - a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") + if resultsHeading.get + h1 + = resultsHeading.get + a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") each card in results.get +resultCard(card) table.global-search-footer diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 294ac378f..f7e3a6a18 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -204,9 +204,8 @@ class GlobalSearchComponent extends CardSearchPagedComponent { } events() { - return [ + return super.events().concat([ { - ...super.events()[0], 'submit .js-search-query-form'(evt) { evt.preventDefault(); this.searchAllBoards(evt.target.searchQuery.value); @@ -259,7 +258,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent { this.hasResults.set(false); }, }, - ]; + ]); } } diff --git a/client/components/settings/adminReports.jade b/client/components/settings/adminReports.jade new file mode 100644 index 000000000..c4b6b93d8 --- /dev/null +++ b/client/components/settings/adminReports.jade @@ -0,0 +1,85 @@ +template(name="adminReports") + .setting-content + unless currentUser.isAdmin + | {{_ 'error-notAuthorized'}} + else + .content-body + .side-menu + ul + li + a.js-report-broken(data-id="report-broken") + i.fa.fa-chain-broken + | {{_ 'broken-cards'}} + + li + a.js-report-files(data-id="report-orphaned-files") + i.fa.fa-paperclip + | {{_ 'orphanedFilesReportTitle'}} + + li + a.js-report-files(data-id="report-files") + i.fa.fa-paperclip + | {{_ 'filesReportTitle'}} + + .main-body + if loading.get + +spinner + else if showBrokenCardsReport.get + +brokenCardsReport + else if showFilesReport.get + +filesReport + else if showOrphanedFilesReport.get + +orphanedFilesReport + + +template(name="brokenCardsReport") + .global-search-results-list-wrapper + h1 {{_ 'broken-cards'}} + if resultsCount + +resultsPaged(this) + else + div {{_ 'no-results' }} + +template(name="filesReport") + h1 {{_ 'filesReportTitle'}} + if resultsCount + table.table + tr + th Filename + th.right Size (kB) + th MIME Type + th.center Usage + th MD5 Sum + th ID + + each att in attachmentFiles + tr + td {{ att.filename }} + td.right {{fileSize att.length }} + td {{ att.contentType }} + td.center {{usageCount att._id.toHexString }} + td {{ att.md5 }} + td {{ att._id.toHexString }} + else + div {{_ 'no-results' }} + +template(name="orphanedFilesReport") + h1 {{_ 'orphanedFilesReportTitle'}} + if resultsCount + table.table + tr + th Filename + th.right Size (kB) + th MIME Type + th MD5 Sum + th ID + + each att in attachmentFiles + tr + td {{ att.filename }} + td.right {{fileSize att.length }} + td {{ att.contentType }} + td {{ att.md5 }} + td {{ att._id.toHexString }} + else + div {{_ 'no-results' }} diff --git a/client/components/settings/adminReports.js b/client/components/settings/adminReports.js new file mode 100644 index 000000000..ad999af14 --- /dev/null +++ b/client/components/settings/adminReports.js @@ -0,0 +1,108 @@ +import { AttachmentStorage } from '/models/attachments'; +import { CardSearchPagedComponent } from '/client/lib/cardSearch'; +import SessionData from '/models/usersessiondata'; + +BlazeComponent.extendComponent({ + subscription: null, + showFilesReport: new ReactiveVar(false), + showBrokenCardsReport: new ReactiveVar(false), + showOrphanedFilesReport: new ReactiveVar(false), + + onCreated() { + this.error = new ReactiveVar(''); + this.loading = new ReactiveVar(false); + }, + + events() { + return [ + { + 'click a.js-report-broken': this.switchMenu, + 'click a.js-report-files': this.switchMenu, + 'click a.js-report-orphaned-files': this.switchMenu, + }, + ]; + }, + + switchMenu(event) { + const target = $(event.target); + if (!target.hasClass('active')) { + this.loading.set(true); + this.showFilesReport.set(false); + this.showBrokenCardsReport.set(false); + this.showOrphanedFilesReport.set(false); + if (this.subscription) { + this.subscription.stop(); + } + + $('.side-menu li.active').removeClass('active'); + target.parent().addClass('active'); + const targetID = target.data('id'); + + if ('report-broken' === targetID) { + this.showBrokenCardsReport.set(true); + this.subscription = Meteor.subscribe( + 'brokenCards', + SessionData.getSessionId(), + () => { + this.loading.set(false); + }, + ); + } else if ('report-files' === targetID) { + this.showFilesReport.set(true); + this.subscription = Meteor.subscribe('attachmentsList', () => { + this.loading.set(false); + }); + } else if ('report-orphaned-files' === targetID) { + this.showOrphanedFilesReport.set(true); + this.subscription = Meteor.subscribe('orphanedAttachments', () => { + this.loading.set(false); + }); + } + } + }, +}).register('adminReports'); + +Template.filesReport.helpers({ + attachmentFiles() { + // eslint-disable-next-line no-console + // console.log('attachments:', AttachmentStorage.find()); + // console.log('attachments.count:', AttachmentStorage.find().count()); + return AttachmentStorage.find(); + }, + + resultsCount() { + return AttachmentStorage.find().count(); + }, + + fileSize(size) { + return Math.round(size / 1024); + }, + + usageCount(key) { + return Attachments.find({ 'copies.attachments.key': key }).count(); + }, +}); + +Template.orphanedFilesReport.helpers({ + attachmentFiles() { + // eslint-disable-next-line no-console + // console.log('attachments:', AttachmentStorage.find()); + // console.log('attachments.count:', AttachmentStorage.find().count()); + return AttachmentStorage.find(); + }, + + resultsCount() { + return AttachmentStorage.find().count(); + }, + + fileSize(size) { + return Math.round(size / 1024); + }, +}); + +class BrokenCardsComponent extends CardSearchPagedComponent { + onCreated() { + super.onCreated(); + } +} +BrokenCardsComponent.register('brokenCardsReport'); diff --git a/client/components/settings/settingHeader.jade b/client/components/settings/settingHeader.jade index d7ab8ca23..14de3ab1f 100644 --- a/client/components/settings/settingHeader.jade +++ b/client/components/settings/settingHeader.jade @@ -12,6 +12,10 @@ template(name="settingHeaderBar") i.fa(class="fa-users") span {{_ 'people'}} + a.setting-header-btn.informations(href="{{pathFor 'admin-reports'}}") + i.fa(class="fa-list") + span {{_ 'reports'}} + a.setting-header-btn.informations(href="{{pathFor 'information'}}") i.fa(class="fa-info-circle") span {{_ 'info'}} diff --git a/client/lib/cardSearch.js b/client/lib/cardSearch.js index 4ba9ee017..e6c506686 100644 --- a/client/lib/cardSearch.js +++ b/client/lib/cardSearch.js @@ -32,8 +32,11 @@ export class CardSearchPagedComponent extends BlazeComponent { that.searching.set(false); that.hasResults.set(false); that.serverError.set(true); + // eslint-disable-next-line no-console console.log('Error.reason:', error.reason); + // eslint-disable-next-line no-console console.log('Error.message:', error.message); + // eslint-disable-next-line no-console console.log('Error.stack:', error.stack); }, }; diff --git a/config/router.js b/config/router.js index de5654925..372f17c7e 100644 --- a/config/router.js +++ b/config/router.js @@ -291,6 +291,28 @@ FlowRouter.route('/people', { }, }); +FlowRouter.route('/admin-reports', { + name: 'admin-reports', + triggersEnter: [ + AccountsTemplates.ensureSignedIn, + () => { + Session.set('currentBoard', null); + Session.set('currentList', null); + Session.set('currentCard', null); + + Filter.reset(); + Session.set('sortBy', ''); + EscapeActions.executeAll(); + }, + ], + action() { + BlazeLayout.render('defaultLayout', { + headerBar: 'settingHeaderBar', + content: 'adminReports', + }); + }, +}); + FlowRouter.notFound = { action() { BlazeLayout.render('defaultLayout', { content: 'notFound' }); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 73141e8b1..a1fc2c473 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -991,5 +991,8 @@ "now-system-messages-of-all-users-are-hidden": "Now system messages of all users are hidden", "move-swimlane": "Move Swimlane", "moveSwimlanePopup-title": "Move Swimlane", - "creator": "Creator" + "creator": "Creator", + "filesReportTitle": "Attachments Report", + "orphanedFilesReportTitle": "Orphaned Files Report", + "reports": "Reports" } diff --git a/models/attachments.js b/models/attachments.js index 3fe1d745e..f0d6ba8e4 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,3 +1,8 @@ +export const AttachmentStorage = new Mongo.Collection( + 'cfs_gridfs.attachments.files', +); +export const AvatarStorage = new Mongo.Collection('cfs_gridfs.avatars.files'); + const localFSStore = process.env.ATTACHMENTS_STORE_PATH; const storeName = 'attachments'; const defaultStoreOptions = { From 5a6f84ce34f04404f06a386e7649da88090f3f7d Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Tue, 6 Apr 2021 12:42:15 +0200 Subject: [PATCH 08/11] * Add publications for admin reports * remove broken cards from user menu * Adjust report titles --- client/components/users/userHeader.jade | 4 -- i18n/en.i18n.json | 2 +- server/publications/attachments.js | 60 +++++++++++++++++++++++++ server/publications/cards.js | 10 ++--- 4 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 server/publications/attachments.js diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index e095e5d3b..75a80dd64 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -25,10 +25,6 @@ template(name="memberMenuPopup") a.js-global-search(href="{{pathFor 'global-search'}}") i.fa.fa-search | {{_ 'globalSearch-title'}} - li - a.js-broken-cards(href="{{pathFor 'broken-cards'}}") - i.fa.fa-chain-broken - | {{_ 'broken-cards'}} li a(href="{{pathFor 'home'}}") span.fa.fa-home diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index a1fc2c473..df81a24c2 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -992,7 +992,7 @@ "move-swimlane": "Move Swimlane", "moveSwimlanePopup-title": "Move Swimlane", "creator": "Creator", - "filesReportTitle": "Attachments Report", + "filesReportTitle": "Files Report", "orphanedFilesReportTitle": "Orphaned Files Report", "reports": "Reports" } diff --git a/server/publications/attachments.js b/server/publications/attachments.js new file mode 100644 index 000000000..80ef954cc --- /dev/null +++ b/server/publications/attachments.js @@ -0,0 +1,60 @@ +import Attachments, { AttachmentStorage } from '/models/attachments'; +import { ObjectID } from 'bson'; + +Meteor.publish('attachmentsList', function() { + // eslint-disable-next-line no-console + // console.log('attachments:', AttachmentStorage.find()); + const files = AttachmentStorage.find( + {}, + { + fields: { + _id: 1, + filename: 1, + md5: 1, + length: 1, + contentType: 1, + metadata: 1, + }, + sort: { + filename: 1, + }, + limit: 250, + }, + ); + const attIds = []; + files.forEach(file => { + attIds.push(file._id._str); + }); + + return [ + files, + Attachments.find({ 'copies.attachments.key': { $in: attIds } }), + ]; +}); + +Meteor.publish('orphanedAttachments', function() { + let keys = []; + Attachments.find({}, { fields: { copies: 1 } }).forEach(att => { + keys.push(new ObjectID(att.copies.attachments.key)); + }); + keys.sort(); + keys = _.uniq(keys, true); + + return AttachmentStorage.find( + { _id: { $nin: keys } }, + { + fields: { + _id: 1, + filename: 1, + md5: 1, + length: 1, + contentType: 1, + metadata: 1, + }, + sort: { + filename: 1, + }, + limit: 250, + }, + ); +}); diff --git a/server/publications/cards.js b/server/publications/cards.js index 38395851c..5f0d42dba 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -45,8 +45,8 @@ import { PREDICATE_PUBLIC, PREDICATE_START_AT, PREDICATE_SYSTEM, -} from '../../config/search-const'; -import { QueryErrors, QueryParams, Query } from '../../config/query-classes'; +} from '/config/search-const'; +import { QueryErrors, QueryParams, Query } from '/config/query-classes'; const escapeForRegex = require('escape-string-regexp'); @@ -598,10 +598,8 @@ function findCards(sessionId, query) { // console.log('selector.$and:', query.selector.$and); // eslint-disable-next-line no-console // console.log('projection:', projection); - let cards; - // if (!query.hasErrors()) { - cards = Cards.find(query.selector, query.projection); - // } + + const cards = Cards.find(query.selector, query.projection); // eslint-disable-next-line no-console // console.log('count:', cards.count()); From 03d29dd674323ae9467f26877fa35070249ae903 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Tue, 6 Apr 2021 17:50:03 +0200 Subject: [PATCH 09/11] Updates to userAvatar and cardDetails template * enable userAvatar to work with member and assignee * remove userAvatarAssignee template --- client/components/cards/cardDetails.jade | 29 +---- client/components/cards/cardDetails.js | 153 +++-------------------- client/components/users/userAvatar.jade | 2 +- client/components/users/userAvatar.js | 9 +- 4 files changed, 29 insertions(+), 164 deletions(-) diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index 42426e92f..341d85eea 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -126,8 +126,8 @@ template(name="cardDetails") h3.card-details-item-title i.fa.fa-users | {{_ 'members'}} - each getMembers - +userAvatar(userId=this cardId=../_id) + each userId in getMembers + +userAvatar(userId=userId cardId=_id) | {{! XXX Hack to hide syntaxic coloration /// }} if canModifyCard unless currentUser.isWorker @@ -140,8 +140,8 @@ template(name="cardDetails") h3.card-details-item-title i.fa.fa-user | {{_ 'assignee'}} - each getAssignees - +userAvatarAssignee(userId=this cardId=../_id) + each userId in getAssignees + +userAvatar(userId=userId cardId=_id assignee=true) | {{! XXX Hack to hide syntaxic coloration /// }} if canModifyCard a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}") @@ -497,23 +497,6 @@ template(name="cardAssigneesPopup") if currentUser.isCardAssignee i.fa.fa-check -template(name="userAvatarAssignee") - a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})") - if userData.profile.avatarUrl - img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}") - else - +userAvatarAssigneeInitials(userId=userData._id) - - if showStatus - span.assignee-presence-status(class=presenceStatusClassName) - span.member-type(class=memberType) - - unless isSandstorm - if showEdit - if $eq currentUser._id userData._id - a.edit-avatar.js-change-avatar - i.fa.fa-pencil - template(name="cardAssigneePopup") .board-assignee-menu .mini-profile-info @@ -531,10 +514,6 @@ template(name="cardAssigneePopup") with currentUser li: a.js-edit-profile {{_ 'edit-profile'}} -template(name="userAvatarAssigneeInitials") - svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15") - text(x="50%" y="13" text-anchor="middle")= initials - template(name="cardMorePopup") p.quiet span.clearfix diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 4278bdd07..6abd8b2dc 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -1,11 +1,18 @@ +import { DatePicker } from '/client/lib/datepicker'; +import Cards from '/models/cards'; +import Boards from '/models/boards'; +import Checklists from '/models/checklists'; +import Integrations from '/models/integrations'; +import Users from '/models/users'; +import Lists from '/models/lists'; +import CardComments from '/models/cardComments'; +import { ALLOWED_COLORS } from '/config/const'; +import moment from 'moment'; +import { UserAvatar } from '../users/userAvatar'; + const subManager = new SubsManager(); const { calculateIndexData } = Utils; -let cardColors; -Meteor.startup(() => { - cardColors = Cards.simpleSchema()._schema.color.allowedValues; -}); - BlazeComponent.extendComponent({ mixins() { return [Mixins.InfiniteScrolling]; @@ -160,9 +167,7 @@ BlazeComponent.extendComponent({ integration, 'CardSelected', params, - () => { - return; - }, + () => {}, ); }); } @@ -290,7 +295,7 @@ BlazeComponent.extendComponent({ Utils.goBoardId(this.data().boardId); }, 'click .js-copy-link'() { - StringToCopyElement = document.getElementById('cardURL_copy'); + const StringToCopyElement = document.getElementById('cardURL_copy'); StringToCopyElement.value = window.location.origin + window.location.pathname; StringToCopyElement.select(); @@ -356,7 +361,7 @@ BlazeComponent.extendComponent({ 'click .js-go-to-linked-card'() { Utils.goCardId(this.data().linkedId); }, - // 'click .js-member': Popup.open('cardMember'), + 'click .js-member': Popup.open('cardMember'), 'click .js-add-members': Popup.open('cardMembers'), 'click .js-assignee': Popup.open('cardAssignee'), 'click .js-add-assignees': Popup.open('cardAssignees'), @@ -407,122 +412,6 @@ BlazeComponent.extendComponent({ }, }).register('cardDetails'); -Template.cardDetails.helpers({ - userData() { - // We need to handle a special case for the search results provided by the - // `matteodem:easy-search` package. Since these results gets published in a - // separate collection, and not in the standard Meteor.Users collection as - // expected, we use a component parameter ("property") to distinguish the - // two cases. - const userCollection = this.esSearch ? ESSearchResults : Users; - return userCollection.findOne(this.userId, { - fields: { - profile: 1, - username: 1, - }, - }); - }, - - receivedSelected() { - if (this.getReceived().length === 0) { - return false; - } else { - return true; - } - }, - - startSelected() { - if (this.getStart().length === 0) { - return false; - } else { - return true; - } - }, - - endSelected() { - if (this.getEnd().length === 0) { - return false; - } else { - return true; - } - }, - - dueSelected() { - if (this.getDue().length === 0) { - return false; - } else { - return true; - } - }, - - memberSelected() { - if (this.getMembers().length === 0) { - return false; - } else { - return true; - } - }, - - labelSelected() { - if (this.getLabels().length === 0) { - return false; - } else { - return true; - } - }, - - assigneeSelected() { - if (this.getAssignees().length === 0) { - return false; - } else { - return true; - } - }, - - requestBySelected() { - if (this.getRequestBy().length === 0) { - return false; - } else { - return true; - } - }, - - assigneeBySelected() { - if (this.getAssigneeBy().length === 0) { - return false; - } else { - return true; - } - }, - - memberType() { - const user = Users.findOne(this.userId); - return user && user.isBoardAdmin() ? 'admin' : 'normal'; - }, - - presenceStatusClassName() { - const user = Users.findOne(this.userId); - const userPresence = presences.findOne({ userId: this.userId }); - if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending'; - else if (!userPresence) return 'disconnected'; - else if (Session.equals('currentBoard', userPresence.state.currentBoardId)) - return 'active'; - else return 'idle'; - }, -}); - -Template.userAvatarAssigneeInitials.helpers({ - initials() { - const user = Users.findOne(this.userId); - return user && user.getInitials(); - }, - - viewPortWidth() { - const user = Users.findOne(this.userId); - return ((user && user.getInitials().length) || 1) * 12; - }, -}); - // We extends the normal InlinedForm component to support UnsavedEdits draft // feature. (class extends InlinedForm { @@ -697,7 +586,7 @@ BlazeComponent.extendComponent({ }, boards() { - const boards = Boards.find( + return Boards.find( { archived: false, 'members.userId': Meteor.userId(), @@ -707,7 +596,6 @@ BlazeComponent.extendComponent({ sort: { sort: 1 /* boards default sorting */ }, }, ); - return boards; }, swimlanes() { @@ -736,7 +624,7 @@ Template.copyCardPopup.events({ 'click .js-done'() { const card = Cards.findOne(Session.get('currentCard')); const lSelect = $('.js-select-lists')[0]; - listId = lSelect.options[lSelect.selectedIndex].value; + const listId = lSelect.options[lSelect.selectedIndex].value; const slSelect = $('.js-select-swimlanes')[0]; const swimlaneId = slSelect.options[slSelect.selectedIndex].value; const bSelect = $('.js-select-boards')[0]; @@ -801,7 +689,7 @@ Template.copyChecklistToManyCardsPopup.events({ }); // copy subtasks - cursor = Cards.find({ parentId: oldId }); + const cursor = Cards.find({ parentId: oldId }); cursor.forEach(function() { 'use strict'; const subtask = arguments[0]; @@ -827,7 +715,7 @@ BlazeComponent.extendComponent({ }, colors() { - return cardColors.map(color => ({ color, name: '' })); + return ALLOWED_COLORS.map(color => ({ color, name: '' })); }, isSelected(color) { @@ -871,7 +759,7 @@ BlazeComponent.extendComponent({ }, boards() { - const boards = Boards.find( + return Boards.find( { archived: false, 'members.userId': Meteor.userId(), @@ -883,7 +771,6 @@ BlazeComponent.extendComponent({ sort: { sort: 1 /* boards default sorting */ }, }, ); - return boards; }, cards() { diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade index 87bbb8c6a..bad253e7c 100644 --- a/client/components/users/userAvatar.jade +++ b/client/components/users/userAvatar.jade @@ -1,5 +1,5 @@ template(name="userAvatar") - a.member.js-member(title="{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}") + a.member(class="js-{{#if assignee}}assignee{{else}}member{{/if}}" title="{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}") if userData.profile.avatarUrl img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}") else diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js index 7b8a32145..97c28e2d8 100644 --- a/client/components/users/userAvatar.js +++ b/client/components/users/userAvatar.js @@ -1,3 +1,7 @@ +import Cards from '/models/cards'; +import Avatars from '/models/avatars'; +import Users from '/models/users'; + Template.userAvatar.helpers({ userData() { // We need to handle a special case for the search results provided by the @@ -30,11 +34,6 @@ Template.userAvatar.helpers({ }, }); -Template.userAvatar.events({ - 'click .js-change-avatar': Popup.open('changeAvatar'), - 'click .js-member': Popup.open('cardMember'), -}); - Template.userAvatarInitials.helpers({ initials() { const user = Users.findOne(this.userId); From 4aee129cdc3cc9896e8dc0fdae248b3f83ab3977 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Tue, 6 Apr 2021 18:00:03 +0200 Subject: [PATCH 10/11] Remove redundant and unused code --- client/components/cards/cardCustomFields.js | 4 + client/components/cards/cardDate.js | 121 +------------------- client/lib/datepicker.js | 34 +++--- 3 files changed, 23 insertions(+), 136 deletions(-) diff --git a/client/components/cards/cardCustomFields.js b/client/components/cards/cardCustomFields.js index 4469e221e..49e9b3c51 100644 --- a/client/components/cards/cardCustomFields.js +++ b/client/components/cards/cardCustomFields.js @@ -1,3 +1,7 @@ +import { DatePicker } from '/client/lib/datepicker'; +import moment from 'moment'; +import Cards from '/models/cards'; + Template.cardCustomFieldsPopup.helpers({ hasCustomField() { const card = Cards.findOne(Session.get('currentCard')); diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js index 250ad0aa7..cb4a2724d 100644 --- a/client/components/cards/cardDate.js +++ b/client/components/cards/cardDate.js @@ -1,122 +1,5 @@ -// Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours -function adjustedTimeFormat() { - return moment - .localeData() - .longDateFormat('LT') - .replace(/HH/i, 'H'); -} - -// Edit received, start, due & end dates -BlazeComponent.extendComponent({ - template() { - return 'editCardDate'; - }, - - onCreated() { - this.error = new ReactiveVar(''); - this.card = this.data(); - this.date = new ReactiveVar(moment.invalid()); - }, - - onRendered() { - const $picker = this.$('.js-datepicker') - .datepicker({ - todayHighlight: true, - todayBtn: 'linked', - language: TAPi18n.getLanguage(), - }) - .on( - 'changeDate', - function(evt) { - this.find('#date').value = moment(evt.date).format('L'); - this.error.set(''); - this.find('#time').focus(); - }.bind(this), - ); - - if (this.date.get().isValid()) { - $picker.datepicker('update', this.date.get().toDate()); - } - }, - - showDate() { - if (this.date.get().isValid()) return this.date.get().format('L'); - return ''; - }, - showTime() { - if (this.date.get().isValid()) return this.date.get().format('LT'); - return ''; - }, - dateFormat() { - return moment.localeData().longDateFormat('L'); - }, - timeFormat() { - return moment.localeData().longDateFormat('LT'); - }, - - events() { - return [ - { - 'keyup .js-date-field'() { - // parse for localized date format in strict mode - const dateMoment = moment(this.find('#date').value, 'L', true); - if (dateMoment.isValid()) { - this.error.set(''); - this.$('.js-datepicker').datepicker('update', dateMoment.toDate()); - } - }, - 'keyup .js-time-field'() { - // parse for localized time format in strict mode - const dateMoment = moment( - this.find('#time').value, - adjustedTimeFormat(), - true, - ); - if (dateMoment.isValid()) { - this.error.set(''); - } - }, - 'submit .edit-date'(evt) { - evt.preventDefault(); - - // if no time was given, init with 12:00 - const time = - evt.target.time.value || - moment(new Date().setHours(12, 0, 0)).format('LT'); - const newTime = moment(time, adjustedTimeFormat(), true); - const newDate = moment(evt.target.date.value, 'L', true); - const dateString = `${evt.target.date.value} ${time}`; - const newCompleteDate = moment( - dateString, - 'L ' + adjustedTimeFormat(), - true, - ); - if (!newTime.isValid()) { - this.error.set('invalid-time'); - evt.target.time.focus(); - } - if (!newDate.isValid()) { - this.error.set('invalid-date'); - evt.target.date.focus(); - } - if (newCompleteDate.isValid()) { - this._storeDate(newCompleteDate.toDate()); - Popup.close(); - } else { - if (!this.error) { - this.error.set('invalid'); - } - } - }, - 'click .js-delete-date'(evt) { - evt.preventDefault(); - this._deleteDate(); - Popup.close(); - }, - }, - ]; - }, -}); +import moment from 'moment'; +import { DatePicker } from '/client/lib/datepicker'; Template.dateBadge.helpers({ canModifyCard() { diff --git a/client/lib/datepicker.js b/client/lib/datepicker.js index 5b62a27e5..7dc5a1be8 100644 --- a/client/lib/datepicker.js +++ b/client/lib/datepicker.js @@ -1,3 +1,5 @@ +import moment from 'moment'; + // Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours function adjustedTimeFormat() { return moment @@ -6,17 +8,17 @@ function adjustedTimeFormat() { .replace(/HH/i, 'H'); } -DatePicker = BlazeComponent.extendComponent({ +export class DatePicker extends BlazeComponent { template() { return 'datepicker'; - }, + } onCreated(defaultTime = '1970-01-01 08:00:00') { this.error = new ReactiveVar(''); this.card = this.data(); this.date = new ReactiveVar(moment.invalid()); this.defaultTime = defaultTime; - }, + } startDayOfWeek() { const currentUser = Meteor.user(); @@ -25,7 +27,7 @@ DatePicker = BlazeComponent.extendComponent({ } else { return 1; } - }, + } onRendered() { const $picker = this.$('.js-datepicker') @@ -42,7 +44,7 @@ DatePicker = BlazeComponent.extendComponent({ this.error.set(''); const timeInput = this.find('#time'); timeInput.focus(); - if (!timeInput.value) { + if (!timeInput.value && this.defaultTime) { const currentHour = evt.date.getHours(); const defaultMoment = moment( currentHour > 0 ? evt.date : this.defaultTime, @@ -55,22 +57,22 @@ DatePicker = BlazeComponent.extendComponent({ if (this.date.get().isValid()) { $picker.datepicker('update', this.date.get().toDate()); } - }, + } showDate() { if (this.date.get().isValid()) return this.date.get().format('L'); return ''; - }, + } showTime() { if (this.date.get().isValid()) return this.date.get().format('LT'); return ''; - }, + } dateFormat() { return moment.localeData().longDateFormat('L'); - }, + } timeFormat() { return moment.localeData().longDateFormat('LT'); - }, + } events() { return [ @@ -106,7 +108,7 @@ DatePicker = BlazeComponent.extendComponent({ const dateString = `${evt.target.date.value} ${time}`; const newCompleteDate = moment( dateString, - 'L ' + adjustedTimeFormat(), + `L ${adjustedTimeFormat()}`, true, ); if (!newTime.isValid()) { @@ -120,10 +122,8 @@ DatePicker = BlazeComponent.extendComponent({ if (newCompleteDate.isValid()) { this._storeDate(newCompleteDate.toDate()); Popup.close(); - } else { - if (!this.error) { - this.error.set('invalid'); - } + } else if (!this.error) { + this.error.set('invalid'); } }, 'click .js-delete-date'(evt) { @@ -133,5 +133,5 @@ DatePicker = BlazeComponent.extendComponent({ }, }, ]; - }, -}); + } +} From 1d4a65d0b4e9910b1eebfcaee56c74a7105f6d09 Mon Sep 17 00:00:00 2001 From: "John R. Supplee" Date: Mon, 12 Apr 2021 11:44:28 +0200 Subject: [PATCH 11/11] Add new rules report --- client/components/settings/adminReports.jade | 26 +++++++++++ client/components/settings/adminReports.js | 48 ++++++++++++++++++++ models/rules.js | 9 ++++ server/publications/rules.js | 25 ++++++++++ 4 files changed, 108 insertions(+) diff --git a/client/components/settings/adminReports.jade b/client/components/settings/adminReports.jade index c4b6b93d8..9c0a2123c 100644 --- a/client/components/settings/adminReports.jade +++ b/client/components/settings/adminReports.jade @@ -21,6 +21,11 @@ template(name="adminReports") i.fa.fa-paperclip | {{_ 'filesReportTitle'}} + li + a.js-report-rules(data-id="report-rules") + i.fa.fa-paperclip + | {{_ 'rulesReportTitle'}} + .main-body if loading.get +spinner @@ -30,6 +35,8 @@ template(name="adminReports") +filesReport else if showOrphanedFilesReport.get +orphanedFilesReport + else if showRulesReport.get + +rulesReport template(name="brokenCardsReport") @@ -40,6 +47,25 @@ template(name="brokenCardsReport") else div {{_ 'no-results' }} +template(name="rulesReport") + h1 {{_ 'rulesReportTitle'}} + if resultsCount + table.table + tr + th Rule Title + th Board Title + th actionType + th activityType + + each rule in rows + tr + td {{ rule.title }} + td {{ rule.boardTitle }} + td {{ rule.action.actionType }} + td {{ rule.trigger.activityType }} + else + div {{_ 'no-results' }} + template(name="filesReport") h1 {{_ 'filesReportTitle'}} if resultsCount diff --git a/client/components/settings/adminReports.js b/client/components/settings/adminReports.js index ad999af14..e8ba75fc0 100644 --- a/client/components/settings/adminReports.js +++ b/client/components/settings/adminReports.js @@ -7,6 +7,7 @@ BlazeComponent.extendComponent({ showFilesReport: new ReactiveVar(false), showBrokenCardsReport: new ReactiveVar(false), showOrphanedFilesReport: new ReactiveVar(false), + showRulesReport: new ReactiveVar(false), onCreated() { this.error = new ReactiveVar(''); @@ -19,6 +20,7 @@ BlazeComponent.extendComponent({ 'click a.js-report-broken': this.switchMenu, 'click a.js-report-files': this.switchMenu, 'click a.js-report-orphaned-files': this.switchMenu, + 'click a.js-report-rules': this.switchMenu, }, ]; }, @@ -57,6 +59,11 @@ BlazeComponent.extendComponent({ this.subscription = Meteor.subscribe('orphanedAttachments', () => { this.loading.set(false); }); + } else if ('report-rules' === targetID) { + this.subscription = Meteor.subscribe('rulesReport', () => { + this.showRulesReport.set(true); + this.loading.set(false); + }); } } }, @@ -70,6 +77,23 @@ Template.filesReport.helpers({ return AttachmentStorage.find(); }, + rulesReport() { + const rules = []; + + Rules.find().forEach(rule => { + rules.push({ + _id: rule._id, + title: rule.title, + boardId: rule.boardId, + boardTitle: rule.board().title, + action: rule.action().fetch(), + trigger: rule.trigger().fetch(), + }); + }); + + return rules; + }, + resultsCount() { return AttachmentStorage.find().count(); }, @@ -100,6 +124,30 @@ Template.orphanedFilesReport.helpers({ }, }); +Template.rulesReport.helpers({ + rows() { + const rules = []; + + Rules.find().forEach(rule => { + rules.push({ + _id: rule._id, + title: rule.title, + boardId: rule.boardId, + boardTitle: rule.board().title, + action: rule.action(), + trigger: rule.trigger(), + }); + }); + + console.log('rows:', rules); + return rules; + }, + + resultsCount() { + return Rules.find().count(); + }, +}); + class BrokenCardsComponent extends CardSearchPagedComponent { onCreated() { super.onCreated(); diff --git a/models/rules.js b/models/rules.js index 2e6729ccf..d82bf9270 100644 --- a/models/rules.js +++ b/models/rules.js @@ -62,6 +62,15 @@ Rules.helpers({ getTrigger() { return Triggers.findOne({ _id: this.triggerId }); }, + board() { + return Boards.findOne({ _id: this.boardId }); + }, + trigger() { + return Triggers.findOne({ _id: this.triggerId }); + }, + action() { + return Actions.findOne({ _id: this.actionId }); + }, }); Rules.allow({ diff --git a/server/publications/rules.js b/server/publications/rules.js index 2a593067c..1b4ce0ca0 100644 --- a/server/publications/rules.js +++ b/server/publications/rules.js @@ -1,3 +1,8 @@ +import Boards from '/models/boards'; +import Actions from '/models/actions'; +import Triggers from '/models/triggers'; +import Rules from '/models/rules'; + Meteor.publish('rules', ruleId => { check(ruleId, String); return Rules.find({ @@ -16,3 +21,23 @@ Meteor.publish('allTriggers', () => { Meteor.publish('allActions', () => { return Actions.find({}); }); + +Meteor.publish('rulesReport', () => { + const rules = Rules.find(); + const actionIds = []; + const triggerIds = []; + const boardIds = []; + + rules.forEach(rule => { + actionIds.push(rule.actionId); + triggerIds.push(rule.triggerId); + boardIds.push(rule.boardId); + }); + + return [ + rules, + Actions.find({ _id: { $in: actionIds } }), + Triggers.find({ _id: { $in: triggerIds } }), + Boards.find({ _id: { $in: boardIds } }, { fields: { title: 1 } }), + ]; +});