diff options
author | Maxime Quandalle <maxime@quandalle.com> | 2015-09-08 20:19:42 +0200 |
---|---|---|
committer | Maxime Quandalle <maxime@quandalle.com> | 2015-09-08 20:19:42 +0200 |
commit | 45b662a1ddb46a0f17fab7b2383c82aa1e1620ef (patch) | |
tree | cc7be215c7e7ebffd2597df70cf271b3dd435e1a | |
parent | c04341f1ea5efe082bf7318cf9eb0e99b9b8374a (diff) | |
download | wekan-45b662a1ddb46a0f17fab7b2383c82aa1e1620ef.tar.gz wekan-45b662a1ddb46a0f17fab7b2383c82aa1e1620ef.tar.bz2 wekan-45b662a1ddb46a0f17fab7b2383c82aa1e1620ef.zip |
Centralize all mutations at the model level
This commit uses a new package that I need to document. It tries to
solve the long-standing debate in the Meteor community about
allow/deny rules versus methods (RPC).
This approach gives us both the centralized security rules of
allow/deny and the white-list of allowed mutations similarly to Meteor
methods. The idea to have static mutation descriptions is also
inspired by Facebook's Relay/GraphQL.
This will allow the development of a REST API using the high-level
methods instead of the MongoDB queries to do the mapping between the
HTTP requests and our collections.
26 files changed, 395 insertions, 377 deletions
@@ -117,6 +117,7 @@ globals: presences: true Ps: true ReactiveTabs: false + Restivus: false SimpleSchema: false SubsManager: false T9n: false diff --git a/.meteor/packages b/.meteor/packages index 0aa0fa68..765932d4 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -18,7 +18,6 @@ mquandalle:stylus es5-shim # Collections -mongo aldeed:collection2 cfs:gridfs cfs:standard-packages @@ -26,6 +25,8 @@ dburles:collection-helpers idmontie:migrations matb33:collection-hooks matteodem:easy-search +mongo +mquandalle:collection-mutations reywood:publish-composite # Account system diff --git a/.meteor/versions b/.meteor/versions index 6c410d0b..1f19267e 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -86,6 +86,7 @@ mongo-id@1.0.1-rc.0 mongo-livedata@1.0.9-rc.0 mousetrap:mousetrap@1.4.6_1 mquandalle:autofocus@1.0.0 +mquandalle:collection-mutations@0.1.0 mquandalle:jade@0.4.3_1 mquandalle:jade-compiler@0.4.3 mquandalle:jquery-textcomplete@0.3.9_1 diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js index 9d7ca7f2..35f795f3 100644 --- a/client/components/boards/boardArchive.js +++ b/client/components/boards/boardArchive.js @@ -22,13 +22,9 @@ BlazeComponent.extendComponent({ events() { return [{ 'click .js-restore-board'() { - const boardId = this.currentData()._id; - Boards.update(boardId, { - $set: { - archived: false, - }, - }); - Utils.goBoardId(boardId); + const board = this.currentData(); + board.restore(); + Utils.goBoardId(board._id); }, }]; }, diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index f259b2a6..19b463ed 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -7,8 +7,8 @@ Template.boardMenuPopup.events({ 'click .js-change-board-color': Popup.open('boardChangeColor'), 'click .js-change-language': Popup.open('changeLanguage'), 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', () => { - const boardId = Session.get('currentBoard'); - Boards.update(boardId, { $set: { archived: true }}); + const currentBoard = Boards.findOne(Session.get('currentBoard')); + currentBoard.archive(); // XXX We should have some kind of notification on top of the page to // confirm that the board was successfully archived. FlowRouter.go('home'); @@ -17,13 +17,9 @@ Template.boardMenuPopup.events({ Template.boardChangeTitlePopup.events({ submit(evt, tpl) { - const title = tpl.$('.js-board-name').val().trim(); - if (title) { - Boards.update(this._id, { - $set: { - title, - }, - }); + const newTitle = tpl.$('.js-board-name').val().trim(); + if (newTitle) { + this.rename(newTitle); Popup.close(); } evt.preventDefault(); @@ -95,12 +91,9 @@ BlazeComponent.extendComponent({ events() { return [{ 'click .js-select-background'(evt) { - const currentBoardId = Session.get('currentBoard'); - Boards.update(currentBoardId, { - $set: { - color: this.currentData().toString(), - }, - }); + const currentBoard = Boards.findOne(Session.get('currentBoard')); + const newColor = this.currentData().toString(); + currentBoard.setColor(newColor); evt.preventDefault(); }, }]; @@ -168,11 +161,9 @@ BlazeComponent.extendComponent({ }, selectBoardVisibility() { - Boards.update(Session.get('currentBoard'), { - $set: { - permission: this.currentData(), - }, - }); + const currentBoard = Boards.findOne(Session.get('currentBoard')); + const visibility = this.currentData(); + currentBoard.setVisibility(visibility); Popup.close(); }, diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index ba56aa1a..5b81f115 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -15,10 +15,10 @@ Template.attachmentsGalery.events({ // XXX Not implemented! }, 'click .js-add-cover'() { - Cards.update(this.cardId, { $set: { coverId: this._id } }); + Cards.findOne(this.cardId).setCover(this._id); }, 'click .js-remove-cover'() { - Cards.update(this.cardId, { $unset: { coverId: '' } }); + Cards.findOne(this.cardId).unsetCover(); }, }); diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 69e0cfdd..a493d938 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -55,12 +55,6 @@ BlazeComponent.extendComponent({ this.componentParent().showOverlay.set(false); }, - updateCard(modifier) { - Cards.update(this.data()._id, { - $set: modifier, - }); - }, - events() { const events = { [`${CSSEvents.animationend} .js-card-details`]() { @@ -76,13 +70,13 @@ BlazeComponent.extendComponent({ 'submit .js-card-description'(evt) { evt.preventDefault(); const description = this.currentComponent().getValue(); - this.updateCard({ description }); + this.data().setDescription(description); }, 'submit .js-card-details-title'(evt) { evt.preventDefault(); const title = this.currentComponent().getValue(); if ($.trim(title)) { - this.updateCard({ title }); + this.data().setTitle(title); } }, 'click .js-member': Popup.open('cardMember'), @@ -135,14 +129,9 @@ Template.cardDetailsActionsPopup.events({ 'click .js-labels': Popup.open('cardLabels'), 'click .js-attachments': Popup.open('cardAttachments'), 'click .js-move-card': Popup.open('moveCard'), - // 'click .js-copy': Popup.open(), 'click .js-archive'(evt) { evt.preventDefault(); - Cards.update(this._id, { - $set: { - archived: true, - }, - }); + this.archive(); Popup.close(); }, 'click .js-more': Popup.open('cardMore'), @@ -152,13 +141,9 @@ Template.moveCardPopup.events({ 'click .js-select-list'() { // XXX We should *not* get the currentCard from the global state, but // instead from a “component” state. - const cardId = Session.get('currentCard'); + const card = Cards.findOne(Session.get('currentCard')); const newListId = this._id; - Cards.update(cardId, { - $set: { - listId: newListId, - }, - }); + card.move(newListId); Popup.close(); }, }); diff --git a/client/components/cards/labels.js b/client/components/cards/labels.js index 2da3b80b..d2ee0140 100644 --- a/client/components/cards/labels.js +++ b/client/components/cards/labels.js @@ -45,19 +45,9 @@ Template.createLabelPopup.helpers({ Template.cardLabelsPopup.events({ 'click .js-select-label'(evt) { - const cardId = Template.parentData(2).data._id; + const card = Cards.findOne(Session.get('currentCard')); const labelId = this._id; - let operation; - if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0) - operation = '$addToSet'; - else - operation = '$pull'; - - Cards.update(cardId, { - [operation]: { - labelIds: labelId, - }, - }); + card.toggleLabel(labelId); evt.preventDefault(); }, 'click .js-edit-label': Popup.open('editLabel'), @@ -79,20 +69,10 @@ Template.formLabel.events({ Template.createLabelPopup.events({ // Create the new label 'submit .create-label'(evt, tpl) { + const board = Boards.findOne(Session.get('currentBoard')); const name = tpl.$('#labelName').val().trim(); - const boardId = Session.get('currentBoard'); const color = Blaze.getData(tpl.find('.fa-check')).color; - - Boards.update(boardId, { - $push: { - labels: { - name, - color, - _id: Random.id(6), - }, - }, - }); - + board.addLabel(name, color); Popup.back(); evt.preventDefault(); }, @@ -100,31 +80,16 @@ Template.createLabelPopup.events({ Template.editLabelPopup.events({ 'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() { - const boardId = Session.get('currentBoard'); - Boards.update(boardId, { - $pull: { - labels: { - _id: this._id, - }, - }, - }); - + const board = Boards.findOne(Session.get('currentBoard')); + board.removeLabel(this._id); Popup.back(2); }), 'submit .edit-label'(evt, tpl) { evt.preventDefault(); + const board = Boards.findOne(Session.get('currentBoard')); const name = tpl.$('#labelName').val().trim(); - const boardId = Session.get('currentBoard'); - const getLabel = Utils.getLabelIndex(boardId, this._id); const color = Blaze.getData(tpl.find('.fa-check')).color; - - Boards.update(boardId, { - $set: { - [getLabel.key('name')]: name, - [getLabel.key('color')]: color, - }, - }); - + board.editLabel(this._id, name, color); Popup.back(); }, }); diff --git a/client/components/lists/list.js b/client/components/lists/list.js index cdf30fc2..af9bef98 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -73,23 +73,13 @@ BlazeComponent.extendComponent({ $cards.sortable('cancel'); if (MultiSelection.isActive()) { - Cards.find(MultiSelection.getMongoSelector()).forEach((c, i) => { - Cards.update(c._id, { - $set: { - listId, - sort: sortIndex.base + i * sortIndex.increment, - }, - }); + Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => { + card.move(listId, sortIndex.base + i * sortIndex.increment); }); } else { const cardDomElement = ui.item.get(0); - const cardId = Blaze.getData(cardDomElement)._id; - Cards.update(cardId, { - $set: { - listId, - sort: sortIndex.base, - }, - }); + const card = Blaze.getData(cardDomElement); + card.move(listId, sortIndex.base); } boardComponent.setIsDragging(false); }, @@ -107,16 +97,15 @@ BlazeComponent.extendComponent({ accept: '.js-member,.js-label', drop(event, ui) { const cardId = Blaze.getData(this)._id; - let addToSet; + const card = Cards.findOne(cardId); if (ui.draggable.hasClass('js-member')) { const memberId = Blaze.getData(ui.draggable.get(0)).userId; - addToSet = { members: memberId }; + card.assignMember(memberId); } else { const labelId = Blaze.getData(ui.draggable.get(0))._id; - addToSet = { labelIds: labelId }; + card.addLabel(labelId); } - Cards.update(cardId, { $addToSet: addToSet }); }, }); }); diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 9431b461..d4891fec 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -5,14 +5,10 @@ BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); - const form = this.componentChildren('inlinedForm')[0]; - const newTitle = form.getValue(); + const newTitle = this.componentChildren('inlinedForm')[0].getValue(); + const list = this.currentData(); if ($.trim(newTitle)) { - Lists.update(this.currentData()._id, { - $set: { - title: newTitle, - }, - }); + list.rename(newTitle); } }, @@ -33,45 +29,30 @@ Template.listActionPopup.events({ }, 'click .js-list-subscribe'() {}, 'click .js-select-cards'() { - const cardIds = Cards.find( - {listId: this._id}, - {fields: { _id: 1 }} - ).map((card) => card._id); + const cardIds = this.allCards().map((card) => card._id); MultiSelection.add(cardIds); Popup.close(); }, 'click .js-move-cards': Popup.open('listMoveCards'), 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', () => { - Cards.find({listId: this._id}).forEach((card) => { - Cards.update(card._id, { - $set: { - archived: true, - }, - }); + this.allCards().forEach((card) => { + card.archive(); }); Popup.close(); }), 'click .js-close-list'(evt) { evt.preventDefault(); - Lists.update(this._id, { - $set: { - archived: true, - }, - }); + this.archive(); Popup.close(); }, }); Template.listMoveCardsPopup.events({ 'click .js-select-list'() { - const fromList = Template.parentData(2).data._id; + const fromList = Template.parentData(2).data; const toList = this._id; - Cards.find({ listId: fromList }).forEach((card) => { - Cards.update(card._id, { - $set: { - listId: toList, - }, - }); + fromList.allCards().forEach((card) => { + card.move(toList); }); Popup.close(); }, diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index eff0ef52..8c58c37e 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -109,14 +109,6 @@ EscapeActions.register('sidebarView', () => { return Sidebar && Sidebar.getView() !== defaultView; } ); -function getMemberIndex(board, searchId) { - for (let i = 0; i < board.members.length; i++) { - if (board.members[i].userId === searchId) - return i; - } - throw new Meteor.Error('Member not found'); -} - Template.memberPopup.helpers({ user() { return Users.findOne(this.userId); @@ -135,13 +127,8 @@ Template.memberPopup.events({ 'click .js-change-role': Popup.open('changePermissions'), 'click .js-remove-member': Popup.afterConfirm('removeMember', function() { const currentBoard = Boards.findOne(Session.get('currentBoard')); - const memberIndex = getMemberIndex(currentBoard, this.userId); - - Boards.update(currentBoard._id, { - $set: { - [`members.${memberIndex}.isActive`]: false, - }, - }); + const memberId = this.userId; + currentBoard.removeMember(memberId); Popup.close(); }), 'click .js-leave-member'() { @@ -209,26 +196,7 @@ Template.addMemberPopup.events({ 'click .js-select-member'() { const userId = this._id; const currentBoard = Boards.findOne(Session.get('currentBoard')); - const currentMembersIds = _.pluck(currentBoard.members, 'userId'); - if (currentMembersIds.indexOf(userId) === -1) { - Boards.update(currentBoard._id, { - $push: { - members: { - userId, - isAdmin: false, - isActive: true, - }, - }, - }); - } else { - const memberIndex = getMemberIndex(currentBoard, userId); - - Boards.update(currentBoard._id, { - $set: { - [`members.${memberIndex}.isActive`]: true, - }, - }); - } + currentBoard.addMember(userId); Popup.close(); }, }); @@ -240,14 +208,9 @@ Template.addMemberPopup.onRendered(function() { Template.changePermissionsPopup.events({ 'click .js-set-admin, click .js-set-normal'(event) { const currentBoard = Boards.findOne(Session.get('currentBoard')); - const memberIndex = getMemberIndex(currentBoard, this.userId); + const memberId = this.userId; const isAdmin = $(event.currentTarget).hasClass('js-set-admin'); - - Boards.update(currentBoard._id, { - $set: { - [`members.${memberIndex}.isAdmin`]: isAdmin, - }, - }); + currentBoard.setMemberPermission(memberId, isAdmin); Popup.back(1); }, }); diff --git a/client/components/sidebar/sidebarArchives.js b/client/components/sidebar/sidebarArchives.js index f2597c3c..52d42edb 100644 --- a/client/components/sidebar/sidebarArchives.js +++ b/client/components/sidebar/sidebarArchives.js @@ -29,8 +29,8 @@ BlazeComponent.extendComponent({ events() { return [{ 'click .js-restore-card'() { - const cardId = this.currentData()._id; - Cards.update(cardId, {$set: {archived: false}}); + const card = this.currentData(); + card.restore(); }, 'click .js-delete-card': Popup.afterConfirm('cardDelete', function() { const cardId = this._id; @@ -38,8 +38,8 @@ BlazeComponent.extendComponent({ Popup.close(); }), 'click .js-restore-list'() { - const listId = this.currentData()._id; - Lists.update(listId, {$set: {archived: false}}); + const list = this.currentData(); + list.restore(); }, }]; }, diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 335cc7d6..babd2f1e 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -30,9 +30,9 @@ BlazeComponent.extendComponent({ }, }).register('filterSidebar'); -function updateSelectedCards(query) { +function mutateSelectedCards(mutationName, ...args) { Cards.find(MultiSelection.getMongoSelector()).forEach((card) => { - Cards.update(card._id, query); + card[mutationName](...args); }); } @@ -67,47 +67,34 @@ BlazeComponent.extendComponent({ 'click .js-toggle-label-multiselection'(evt) { const labelId = this.currentData()._id; const mappedSelection = this.mapSelection('label', labelId); - let operation; - if (_.every(mappedSelection)) - operation = '$pull'; - else if (_.every(mappedSelection, (bool) => !bool)) - operation = '$addToSet'; - else { + + if (_.every(mappedSelection)) { + mutateSelectedCards('addLabel', labelId); + } else if (_.every(mappedSelection, (bool) => !bool)) { + mutateSelectedCards('removeLabel', labelId); + } else { const popup = Popup.open('disambiguateMultiLabel'); // XXX We need to have a better integration between the popup and the // UI components systems. return popup.call(this.currentData(), evt); } - - updateSelectedCards({ - [operation]: { - labelIds: labelId, - }, - }); }, 'click .js-toggle-member-multiselection'(evt) { const memberId = this.currentData()._id; const mappedSelection = this.mapSelection('member', memberId); - let operation; - if (_.every(mappedSelection)) - operation = '$pull'; - else if (_.every(mappedSelection, (bool) => !bool)) - operation = '$addToSet'; - else { + if (_.every(mappedSelection)) { + mutateSelectedCards('assignMember', memberId); + } else if (_.every(mappedSelection, (bool) => !bool)) { + mutateSelectedCards('unassignMember', memberId); + } else { const popup = Popup.open('disambiguateMultiMember'); // XXX We need to have a better integration between the popup and the // UI components systems. return popup.call(this.currentData(), evt); } - - updateSelectedCards({ - [operation]: { - members: memberId, - }, - }); }, 'click .js-archive-selection'() { - updateSelectedCards({$set: {archived: true}}); + mutateSelectedCards('archive'); }, }]; }, @@ -115,22 +102,22 @@ BlazeComponent.extendComponent({ Template.disambiguateMultiLabelPopup.events({ 'click .js-remove-label'() { - updateSelectedCards({$pull: {labelIds: this._id}}); + mutateSelectedCards('removeLabel', this._id); Popup.close(); }, 'click .js-add-label'() { - updateSelectedCards({$addToSet: {labelIds: this._id}}); + mutateSelectedCards('addLabel', this._id); Popup.close(); }, }); Template.disambiguateMultiMemberPopup.events({ 'click .js-unassign-member'() { - updateSelectedCards({$pull: {members: this._id}}); + mutateSelectedCards('assignMember', this._id); Popup.close(); }, 'click .js-assign-member'() { - updateSelectedCards({$addToSet: {members: this._id}}); + mutateSelectedCards('unassignMember', this._id); Popup.close(); }, }); diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js index 04add0a6..1f1da251 100644 --- a/client/components/users/userAvatar.js +++ b/client/components/users/userAvatar.js @@ -82,11 +82,7 @@ BlazeComponent.extendComponent({ }, setAvatar(avatarUrl) { - Meteor.users.update(Meteor.userId(), { - $set: { - 'profile.avatarUrl': avatarUrl, - }, - }); + Meteor.user().setAvatarUrl(avatarUrl); }, setError(error) { @@ -151,19 +147,9 @@ Template.cardMembersPopup.helpers({ Template.cardMembersPopup.events({ 'click .js-select-member'(evt) { - const cardId = Template.parentData(2).data._id; + const card = Cards.findOne(Session.get('currentCard')); const memberId = this.userId; - let operation; - if (Cards.find({ _id: cardId, members: memberId}).count() === 0) - operation = '$addToSet'; - else - operation = '$pull'; - - Cards.update(cardId, { - [operation]: { - members: memberId, - }, - }); + card.toggleMember(memberId); evt.preventDefault(); }, }); @@ -176,7 +162,7 @@ Template.cardMemberPopup.helpers({ Template.cardMemberPopup.events({ 'click .js-remove-member'() { - Cards.update(this.cardId, {$pull: {members: this.userId}}); + Cards.findOne(this.cardId).unassignMember(this.userId); Popup.close(); }, 'click .js-edit-profile': Popup.open('editProfile'), diff --git a/client/lib/unsavedEdits.js b/client/lib/unsavedEdits.js index dc267bfb..17bb29b5 100644 --- a/client/lib/unsavedEdits.js +++ b/client/lib/unsavedEdits.js @@ -65,7 +65,7 @@ UnsavedEdits = { }; Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => { - // Workaround some blaze feature that ass a list of keywords arguments as the + // Workaround some blaze feature that pass a list of keywords arguments as the // last parameter (even if the caller didn't specify any). if (!_.isString(defaultTo)) { defaultTo = ''; diff --git a/client/lib/utils.js b/client/lib/utils.js index 0cd93419..6bdd5822 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -22,20 +22,6 @@ Utils = { return string.charAt(0).toUpperCase() + string.slice(1); }, - getLabelIndex(boardId, labelId) { - const board = Boards.findOne(boardId); - const labels = {}; - _.each(board.labels, (a, b) => { - labels[a._id] = b; - }); - return { - index: labels[labelId], - key(key) { - return `labels.${labels[labelId]}.${key}`; - }, - }; - }, - // Determine the new sort index calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) { let base, increment; diff --git a/collections/activities.js b/models/activities.js index 5de07ee5..5de07ee5 100644 --- a/collections/activities.js +++ b/models/activities.js diff --git a/collections/attachments.js b/models/attachments.js index 8ef0fef0..8ef0fef0 100644 --- a/collections/attachments.js +++ b/models/attachments.js diff --git a/collections/avatars.js b/models/avatars.js index 53924ffb..53924ffb 100644 --- a/collections/avatars.js +++ b/models/avatars.js diff --git a/collections/boards.js b/models/boards.js index fcd04153..4baec280 100644 --- a/collections/boards.js +++ b/models/boards.js @@ -73,6 +73,125 @@ Boards.attachSchema(new SimpleSchema({ }, })); + +Boards.helpers({ + isPublic() { + return this.permission === 'public'; + }, + + lists() { + return Lists.find({ boardId: this._id, archived: false }, + { sort: { sort: 1 }}); + }, + + activities() { + return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }}); + }, + + activeMembers() { + return _.where(this.members, {isActive: true}); + }, + + labelIndex(labelId) { + return _.indexOf(_.pluck(this.labels, '_id'), labelId); + }, + + memberIndex(memberId) { + return _.indexOf(_.pluck(this.members, 'userId'), memberId); + }, + + absoluteUrl() { + return FlowRouter.path('board', { id: this._id, slug: this.slug }); + }, + + colorClass() { + return `board-color-${this.color}`; + }, +}); + +Boards.mutations({ + archive() { + return { $set: { archived: true }}; + }, + + restore() { + return { $set: { archived: false }}; + }, + + rename(title) { + return { $set: { title }}; + }, + + setColor(color) { + return { $set: { color }}; + }, + + setVisibility(visibility) { + return { $set: { permission: visibility }}; + }, + + addLabel(name, color) { + const _id = Random.id(6); + return { $push: {labels: { _id, name, color }}}; + }, + + editLabel(labelId, name, color) { + const labelIndex = this.labelIndex(labelId); + return { + $set: { + [`labels.${labelIndex}.name`]: name, + [`labels.${labelIndex}.color`]: color, + }, + }; + }, + + removeLabel(labelId) { + return { $pull: { labels: { _id: labelId }}}; + }, + + addMember(memberId) { + const memberIndex = this.memberIndex(memberId); + if (memberIndex === -1) { + return { + $push: { + members: { + userId: memberId, + isAdmin: false, + isActive: true, + }, + }, + }; + } else { + return { + $set: { + [`members.${memberIndex}.isActive`]: true, + [`members.${memberIndex}.isAdmin`]: false, + }, + }; + } + }, + + removeMember(memberId) { + const memberIndex = this.memberIndex(memberId); + + return { + $set: { + [`members.${memberIndex}.isActive`]: false, + }, + }; + }, + + setMemberPermission(memberId, isAdmin) { + const memberIndex = this.memberIndex(memberId); + + return { + $set: { + [`members.${memberIndex}.isAdmin`]: isAdmin, + }, + }; + }, +}); + if (Meteor.isServer) { Boards.allow({ insert: Meteor.userId, @@ -119,33 +238,6 @@ if (Meteor.isServer) { }); } -Boards.helpers({ - isPublic() { - return this.permission === 'public'; - }, - - lists() { - return Lists.find({ boardId: this._id, archived: false }, - { sort: { sort: 1 }}); - }, - - activities() { - return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }}); - }, - - activeMembers() { - return _.where(this.members, {isActive: true}); - }, - - absoluteUrl() { - return FlowRouter.path('board', { id: this._id, slug: this.slug }); - }, - - colorClass() { - return `board-color-${this.color}`; - }, -}); - Boards.before.insert((userId, doc) => { // XXX We need to improve slug management. Only the id should be necessary // to identify a board in the code. diff --git a/models/cardComments.js b/models/cardComments.js new file mode 100644 index 00000000..224deb03 --- /dev/null +++ b/models/cardComments.js @@ -0,0 +1,69 @@ +CardComments = new Mongo.Collection('card_comments'); + +CardComments.attachSchema(new SimpleSchema({ + boardId: { + type: String, + }, + cardId: { + type: String, + }, + // XXX Rename in `content`? `text` is a bit vague... + text: { + type: String, + }, + // XXX We probably don't need this information here, since we already have it + // in the associated comment creation activity + createdAt: { + type: Date, + denyUpdate: false, + }, + // XXX Should probably be called `authorId` + userId: { + type: String, + }, +})); + +CardComments.allow({ + insert(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); + }, + update(userId, doc) { + return userId === doc.userId; + }, + remove(userId, doc) { + return userId === doc.userId; + }, + fetch: ['userId', 'boardId'], +}); + +CardComments.helpers({ + user() { + return Users.findOne(this.userId); + }, +}); + +CardComments.hookOptions.after.update = { fetchPrevious: false }; + +CardComments.before.insert((userId, doc) => { + doc.createdAt = new Date(); + doc.userId = userId; +}); + +if (Meteor.isServer) { + CardComments.after.insert((userId, doc) => { + Activities.insert({ + userId, + activityType: 'addComment', + boardId: doc.boardId, + cardId: doc.cardId, + commentId: doc._id, + }); + }); + + CardComments.after.remove((userId, doc) => { + const activity = Activities.findOne({ commentId: doc._id }); + if (activity) { + Activities.remove(activity._id); + } + }); +} diff --git a/collections/cards.js b/models/cards.js index 97ba4e3c..95943ae2 100644 --- a/collections/cards.js +++ b/models/cards.js @@ -1,5 +1,4 @@ Cards = new Mongo.Collection('cards'); -CardComments = new Mongo.Collection('card_comments'); // XXX To improve pub/sub performances a card document should include a // de-normalized number of comments so we don't have to publish the whole list @@ -54,64 +53,28 @@ Cards.attachSchema(new SimpleSchema({ }, })); -CardComments.attachSchema(new SimpleSchema({ - boardId: { - type: String, +Cards.allow({ + insert(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); }, - cardId: { - type: String, - }, - // XXX Rename in `content`? `text` is a bit vague... - text: { - type: String, + update(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); }, - // XXX We probably don't need this information here, since we already have it - // in the associated comment creation activity - createdAt: { - type: Date, - denyUpdate: false, - }, - // XXX Should probably be called `authorId` - userId: { - type: String, + remove(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); }, -})); - -if (Meteor.isServer) { - Cards.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - remove(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - fetch: ['boardId'], - }); - - CardComments.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return userId === doc.userId; - }, - remove(userId, doc) { - return userId === doc.userId; - }, - fetch: ['userId', 'boardId'], - }); -} + fetch: ['boardId'], +}); Cards.helpers({ list() { return Lists.findOne(this.listId); }, + board() { return Boards.findOne(this.boardId); }, + labels() { const boardLabels = this.board().labels; const cardLabels = _.filter(boardLabels, (label) => { @@ -119,27 +82,35 @@ Cards.helpers({ }); return cardLabels; }, + hasLabel(labelId) { return _.contains(this.labelIds, labelId); }, + user() { return Users.findOne(this.userId); }, + isAssigned(memberId) { return _.contains(this.members, memberId); }, + activities() { return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }}); }, + comments() { return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }}); }, + attachments() { return Attachments.find({ cardId: this._id }, { sort: { uploadedAt: -1 }}); }, + cover() { return Attachments.findOne(this.coverId); }, + absoluteUrl() { const board = this.board(); return FlowRouter.path('card', { @@ -148,33 +119,86 @@ Cards.helpers({ cardId: this._id, }); }, + rootUrl() { return Meteor.absoluteUrl(this.absoluteUrl().replace('/', '')); }, }); -CardComments.helpers({ - user() { - return Users.findOne(this.userId); +Cards.mutations({ + archive() { + return { $set: { archived: true }}; + }, + + restore() { + return { $set: { archived: false }}; + }, + + setTitle(title) { + return { $set: { title }}; + }, + + setDescription(description) { + return { $set: { description }}; + }, + + move(listId, sortIndex) { + const mutatedFields = { listId }; + if (sortIndex) { + mutatedFields.sort = sortIndex; + } + return { $set: mutatedFields }; + }, + + addLabel(labelId) { + return { $addToSet: { labelIds: labelId }}; + }, + + removeLabel(labelId) { + return { $pull: { labelIds: labelId }}; + }, + + toggleLabel(labelId) { + if (this.labelIds && this.labelIds.indexOf(labelId) > -1) { + return this.removeLabel(labelId); + } else { + return this.addLabel(labelId); + } + }, + + assignMember(memberId) { + return { $addToSet: { members: memberId }}; + }, + + unassignMember(memberId) { + return { $pull: { members: memberId }}; + }, + + toggleMember(memberId) { + if (this.members && this.members.indexOf(memberId) > -1) { + return this.unassignMember(memberId); + } else { + return this.assignMember(memberId); + } + }, + + setCover(coverId) { + return { $set: { coverId }}; + }, + + unsetCover() { + return { $unset: { coverId: '' }}; }, }); -CardComments.hookOptions.after.update = { fetchPrevious: false }; Cards.before.insert((userId, doc) => { doc.createdAt = new Date(); doc.dateLastActivity = new Date(); - - // defaults doc.archived = false; - // userId native set. - if (!doc.userId) + if (!doc.userId) { doc.userId = userId; -}); - -CardComments.before.insert((userId, doc) => { - doc.createdAt = new Date(); - doc.userId = userId; + } }); if (Meteor.isServer) { @@ -264,21 +288,4 @@ if (Meteor.isServer) { cardId: doc._id, }); }); - - CardComments.after.insert((userId, doc) => { - Activities.insert({ - userId, - activityType: 'addComment', - boardId: doc.boardId, - cardId: doc.cardId, - commentId: doc._id, - }); - }); - - CardComments.after.remove((userId, doc) => { - const activity = Activities.findOne({ commentId: doc._id }); - if (activity) { - Activities.remove(activity._id); - } - }); } diff --git a/collections/lists.js b/models/lists.js index 0c6ba407..4e4a1134 100644 --- a/collections/lists.js +++ b/models/lists.js @@ -27,20 +27,18 @@ Lists.attachSchema(new SimpleSchema({ }, })); -if (Meteor.isServer) { - Lists.allow({ - insert(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - update(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - remove(userId, doc) { - return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); - }, - fetch: ['boardId'], - }); -} +Lists.allow({ + insert(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); + }, + update(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); + }, + remove(userId, doc) { + return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); + }, + fetch: ['boardId'], +}); Lists.helpers({ cards() { @@ -49,12 +47,30 @@ Lists.helpers({ archived: false, }), { sort: ['sort'] }); }, + + allCards() { + return Cards.find({ listId: this._id }); + }, + board() { return Boards.findOne(this.boardId); }, }); -// HOOKS +Lists.mutations({ + rename(title) { + return { $set: { title }}; + }, + + archive() { + return { $set: { archived: true }}; + }, + + restore() { + return { $set: { archived: false }}; + }, +}); + Lists.hookOptions.after.update = { fetchPrevious: false }; Lists.before.insert((userId, doc) => { diff --git a/collections/unsavedEdits.js b/models/unsavedEdits.js index 87a70e22..87a70e22 100644 --- a/collections/unsavedEdits.js +++ b/models/unsavedEdits.js diff --git a/collections/users.js b/models/users.js index fa910c4a..4260dc56 100644 --- a/collections/users.js +++ b/models/users.js @@ -49,14 +49,20 @@ Users.helpers({ return this.username[0].toUpperCase(); } }, +}); +Users.mutations({ toggleBoardStar(boardId) { const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet'; - Meteor.users.update(this._id, { + return { [queryKind]: { 'profile.starredBoards': boardId, }, - }); + }; + }, + + setAvatarUrl(avatarUrl) { + return { $set: { 'profile.avatarUrl': avatarUrl }}; }, }); diff --git a/sandstorm.js b/sandstorm.js index c430c3a8..97d42bdf 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -25,11 +25,7 @@ if (isSandstorm && Meteor.isServer) { // apparently meteor-core misses an API to handle that cleanly, cf. // https://github.com/meteor/meteor/blob/ff783e9a12ffa04af6fd163843a563c9f4bbe8c1/packages/accounts-base/accounts_server.js#L1143 function updateUserAvatar(userId, avatarUrl) { - Users.update(userId, { - $set: { - 'profile.avatarUrl': avatarUrl, - }, - }); + Users.findOne(userId).setAvatarUrl(avatarUrl); } function updateUserPermissions(userId, permissions) { |