summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxime Quandalle <maxime@quandalle.com>2015-09-08 20:19:42 +0200
committerMaxime Quandalle <maxime@quandalle.com>2015-09-08 20:19:42 +0200
commit45b662a1ddb46a0f17fab7b2383c82aa1e1620ef (patch)
treecc7be215c7e7ebffd2597df70cf271b3dd435e1a
parentc04341f1ea5efe082bf7318cf9eb0e99b9b8374a (diff)
downloadwekan-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.
-rw-r--r--.eslintrc1
-rw-r--r--.meteor/packages3
-rw-r--r--.meteor/versions1
-rw-r--r--client/components/boards/boardArchive.js10
-rw-r--r--client/components/boards/boardHeader.js31
-rw-r--r--client/components/cards/attachments.js4
-rw-r--r--client/components/cards/cardDetails.js25
-rw-r--r--client/components/cards/labels.js51
-rw-r--r--client/components/lists/list.js25
-rw-r--r--client/components/lists/listHeader.js39
-rw-r--r--client/components/sidebar/sidebar.js47
-rw-r--r--client/components/sidebar/sidebarArchives.js8
-rw-r--r--client/components/sidebar/sidebarFilters.js49
-rw-r--r--client/components/users/userAvatar.js22
-rw-r--r--client/lib/unsavedEdits.js2
-rw-r--r--client/lib/utils.js14
-rw-r--r--models/activities.js (renamed from collections/activities.js)0
-rw-r--r--models/attachments.js (renamed from collections/attachments.js)0
-rw-r--r--models/avatars.js (renamed from collections/avatars.js)0
-rw-r--r--models/boards.js (renamed from collections/boards.js)146
-rw-r--r--models/cardComments.js69
-rw-r--r--models/cards.js (renamed from collections/cards.js)163
-rw-r--r--models/lists.js (renamed from collections/lists.js)46
-rw-r--r--models/unsavedEdits.js (renamed from collections/unsavedEdits.js)0
-rw-r--r--models/users.js (renamed from collections/users.js)10
-rw-r--r--sandstorm.js6
26 files changed, 395 insertions, 377 deletions
diff --git a/.eslintrc b/.eslintrc
index 7d596fa3..3f6639a4 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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) {