cards.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. Cards = new Mongo.Collection('cards');
  2. // XXX To improve pub/sub performances a card document should include a
  3. // de-normalized number of comments so we don't have to publish the whole list
  4. // of comments just to display the number of them in the board view.
  5. Cards.attachSchema(new SimpleSchema({
  6. title: {
  7. type: String,
  8. },
  9. archived: {
  10. type: Boolean,
  11. },
  12. listId: {
  13. type: String,
  14. },
  15. // The system could work without this `boardId` information (we could deduce
  16. // the board identifier from the card), but it would make the system more
  17. // difficult to manage and less efficient.
  18. boardId: {
  19. type: String,
  20. },
  21. coverId: {
  22. type: String,
  23. optional: true,
  24. },
  25. createdAt: {
  26. type: Date,
  27. denyUpdate: true,
  28. },
  29. dateLastActivity: {
  30. type: Date,
  31. },
  32. description: {
  33. type: String,
  34. optional: true,
  35. },
  36. labelIds: {
  37. type: [String],
  38. optional: true,
  39. },
  40. members: {
  41. type: [String],
  42. optional: true,
  43. },
  44. // XXX Should probably be called `authorId`. Is it even needed since we have
  45. // the `members` field?
  46. userId: {
  47. type: String,
  48. },
  49. sort: {
  50. type: Number,
  51. decimal: true,
  52. },
  53. }));
  54. Cards.allow({
  55. insert(userId, doc) {
  56. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  57. },
  58. update(userId, doc) {
  59. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  60. },
  61. remove(userId, doc) {
  62. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  63. },
  64. fetch: ['boardId'],
  65. });
  66. Cards.helpers({
  67. list() {
  68. return Lists.findOne(this.listId);
  69. },
  70. board() {
  71. return Boards.findOne(this.boardId);
  72. },
  73. labels() {
  74. const boardLabels = this.board().labels;
  75. const cardLabels = _.filter(boardLabels, (label) => {
  76. return _.contains(this.labelIds, label._id);
  77. });
  78. return cardLabels;
  79. },
  80. hasLabel(labelId) {
  81. return _.contains(this.labelIds, labelId);
  82. },
  83. user() {
  84. return Users.findOne(this.userId);
  85. },
  86. isAssigned(memberId) {
  87. return _.contains(this.members, memberId);
  88. },
  89. activities() {
  90. return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }});
  91. },
  92. comments() {
  93. return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }});
  94. },
  95. attachments() {
  96. return Attachments.find({ cardId: this._id }, { sort: { uploadedAt: -1 }});
  97. },
  98. cover() {
  99. return Attachments.findOne(this.coverId);
  100. },
  101. absoluteUrl() {
  102. const board = this.board();
  103. return FlowRouter.path('card', {
  104. boardId: board._id,
  105. slug: board.slug,
  106. cardId: this._id,
  107. });
  108. },
  109. rootUrl() {
  110. return Meteor.absoluteUrl(this.absoluteUrl().replace('/', ''));
  111. },
  112. });
  113. Cards.mutations({
  114. archive() {
  115. return { $set: { archived: true }};
  116. },
  117. restore() {
  118. return { $set: { archived: false }};
  119. },
  120. setTitle(title) {
  121. return { $set: { title }};
  122. },
  123. setDescription(description) {
  124. return { $set: { description }};
  125. },
  126. move(listId, sortIndex) {
  127. const mutatedFields = { listId };
  128. if (sortIndex) {
  129. mutatedFields.sort = sortIndex;
  130. }
  131. return { $set: mutatedFields };
  132. },
  133. addLabel(labelId) {
  134. return { $addToSet: { labelIds: labelId }};
  135. },
  136. removeLabel(labelId) {
  137. return { $pull: { labelIds: labelId }};
  138. },
  139. toggleLabel(labelId) {
  140. if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
  141. return this.removeLabel(labelId);
  142. } else {
  143. return this.addLabel(labelId);
  144. }
  145. },
  146. assignMember(memberId) {
  147. return { $addToSet: { members: memberId }};
  148. },
  149. unassignMember(memberId) {
  150. return { $pull: { members: memberId }};
  151. },
  152. toggleMember(memberId) {
  153. if (this.members && this.members.indexOf(memberId) > -1) {
  154. return this.unassignMember(memberId);
  155. } else {
  156. return this.assignMember(memberId);
  157. }
  158. },
  159. setCover(coverId) {
  160. return { $set: { coverId }};
  161. },
  162. unsetCover() {
  163. return { $unset: { coverId: '' }};
  164. },
  165. });
  166. Cards.before.insert((userId, doc) => {
  167. doc.createdAt = new Date();
  168. doc.dateLastActivity = new Date();
  169. doc.archived = false;
  170. if (!doc.userId) {
  171. doc.userId = userId;
  172. }
  173. });
  174. if (Meteor.isServer) {
  175. Cards.after.insert((userId, doc) => {
  176. Activities.insert({
  177. userId,
  178. activityType: 'createCard',
  179. boardId: doc.boardId,
  180. listId: doc.listId,
  181. cardId: doc._id,
  182. });
  183. });
  184. // New activity for card (un)archivage
  185. Cards.after.update((userId, doc, fieldNames) => {
  186. if (_.contains(fieldNames, 'archived')) {
  187. if (doc.archived) {
  188. Activities.insert({
  189. userId,
  190. activityType: 'archivedCard',
  191. boardId: doc.boardId,
  192. listId: doc.listId,
  193. cardId: doc._id,
  194. });
  195. } else {
  196. Activities.insert({
  197. userId,
  198. activityType: 'restoredCard',
  199. boardId: doc.boardId,
  200. listId: doc.listId,
  201. cardId: doc._id,
  202. });
  203. }
  204. }
  205. });
  206. // New activity for card moves
  207. Cards.after.update(function(userId, doc, fieldNames) {
  208. const oldListId = this.previous.listId;
  209. if (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) {
  210. Activities.insert({
  211. userId,
  212. oldListId,
  213. activityType: 'moveCard',
  214. listId: doc.listId,
  215. boardId: doc.boardId,
  216. cardId: doc._id,
  217. });
  218. }
  219. });
  220. // Add a new activity if we add or remove a member to the card
  221. Cards.before.update((userId, doc, fieldNames, modifier) => {
  222. if (!_.contains(fieldNames, 'members'))
  223. return;
  224. let memberId;
  225. // Say hello to the new member
  226. if (modifier.$addToSet && modifier.$addToSet.members) {
  227. memberId = modifier.$addToSet.members;
  228. if (!_.contains(doc.members, memberId)) {
  229. Activities.insert({
  230. userId,
  231. memberId,
  232. activityType: 'joinMember',
  233. boardId: doc.boardId,
  234. cardId: doc._id,
  235. });
  236. }
  237. }
  238. // Say goodbye to the former member
  239. if (modifier.$pull && modifier.$pull.members) {
  240. memberId = modifier.$pull.members;
  241. Activities.insert({
  242. userId,
  243. memberId,
  244. activityType: 'unjoinMember',
  245. boardId: doc.boardId,
  246. cardId: doc._id,
  247. });
  248. }
  249. });
  250. // Remove all activities associated with a card if we remove the card
  251. Cards.after.remove((userId, doc) => {
  252. Activities.remove({
  253. cardId: doc._id,
  254. });
  255. });
  256. }