cards.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. const cover = Attachments.findOne(this.coverId);
  100. // if we return a cover before it is fully stored, we will get errors when we try to display it
  101. // todo XXX we could return a default "upload pending" image in the meantime?
  102. return cover && cover.url() && cover;
  103. },
  104. absoluteUrl() {
  105. const board = this.board();
  106. return FlowRouter.path('card', {
  107. boardId: board._id,
  108. slug: board.slug,
  109. cardId: this._id,
  110. });
  111. },
  112. rootUrl() {
  113. return Meteor.absoluteUrl(this.absoluteUrl().replace('/', ''));
  114. },
  115. });
  116. Cards.mutations({
  117. archive() {
  118. return { $set: { archived: true }};
  119. },
  120. restore() {
  121. return { $set: { archived: false }};
  122. },
  123. setTitle(title) {
  124. return { $set: { title }};
  125. },
  126. setDescription(description) {
  127. return { $set: { description }};
  128. },
  129. move(listId, sortIndex) {
  130. const mutatedFields = { listId };
  131. if (sortIndex) {
  132. mutatedFields.sort = sortIndex;
  133. }
  134. return { $set: mutatedFields };
  135. },
  136. addLabel(labelId) {
  137. return { $addToSet: { labelIds: labelId }};
  138. },
  139. removeLabel(labelId) {
  140. return { $pull: { labelIds: labelId }};
  141. },
  142. toggleLabel(labelId) {
  143. if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
  144. return this.removeLabel(labelId);
  145. } else {
  146. return this.addLabel(labelId);
  147. }
  148. },
  149. assignMember(memberId) {
  150. return { $addToSet: { members: memberId }};
  151. },
  152. unassignMember(memberId) {
  153. return { $pull: { members: memberId }};
  154. },
  155. toggleMember(memberId) {
  156. if (this.members && this.members.indexOf(memberId) > -1) {
  157. return this.unassignMember(memberId);
  158. } else {
  159. return this.assignMember(memberId);
  160. }
  161. },
  162. setCover(coverId) {
  163. return { $set: { coverId }};
  164. },
  165. unsetCover() {
  166. return { $unset: { coverId: '' }};
  167. },
  168. });
  169. Cards.before.insert((userId, doc) => {
  170. doc.createdAt = new Date();
  171. doc.dateLastActivity = new Date();
  172. if(!doc.hasOwnProperty('archived')){
  173. doc.archived = false;
  174. }
  175. if (!doc.userId) {
  176. doc.userId = userId;
  177. }
  178. });
  179. if (Meteor.isServer) {
  180. Cards.after.insert((userId, doc) => {
  181. Activities.insert({
  182. userId,
  183. activityType: 'createCard',
  184. boardId: doc.boardId,
  185. listId: doc.listId,
  186. cardId: doc._id,
  187. });
  188. });
  189. // New activity for card (un)archivage
  190. Cards.after.update((userId, doc, fieldNames) => {
  191. if (_.contains(fieldNames, 'archived')) {
  192. if (doc.archived) {
  193. Activities.insert({
  194. userId,
  195. activityType: 'archivedCard',
  196. boardId: doc.boardId,
  197. listId: doc.listId,
  198. cardId: doc._id,
  199. });
  200. } else {
  201. Activities.insert({
  202. userId,
  203. activityType: 'restoredCard',
  204. boardId: doc.boardId,
  205. listId: doc.listId,
  206. cardId: doc._id,
  207. });
  208. }
  209. }
  210. });
  211. // New activity for card moves
  212. Cards.after.update(function(userId, doc, fieldNames) {
  213. const oldListId = this.previous.listId;
  214. if (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) {
  215. Activities.insert({
  216. userId,
  217. oldListId,
  218. activityType: 'moveCard',
  219. listId: doc.listId,
  220. boardId: doc.boardId,
  221. cardId: doc._id,
  222. });
  223. }
  224. });
  225. // Add a new activity if we add or remove a member to the card
  226. Cards.before.update((userId, doc, fieldNames, modifier) => {
  227. if (!_.contains(fieldNames, 'members'))
  228. return;
  229. let memberId;
  230. // Say hello to the new member
  231. if (modifier.$addToSet && modifier.$addToSet.members) {
  232. memberId = modifier.$addToSet.members;
  233. if (!_.contains(doc.members, memberId)) {
  234. Activities.insert({
  235. userId,
  236. memberId,
  237. activityType: 'joinMember',
  238. boardId: doc.boardId,
  239. cardId: doc._id,
  240. });
  241. }
  242. }
  243. // Say goodbye to the former member
  244. if (modifier.$pull && modifier.$pull.members) {
  245. memberId = modifier.$pull.members;
  246. Activities.insert({
  247. userId,
  248. memberId,
  249. activityType: 'unjoinMember',
  250. boardId: doc.boardId,
  251. cardId: doc._id,
  252. });
  253. }
  254. });
  255. // Remove all activities associated with a card if we remove the card
  256. Cards.after.remove((userId, doc) => {
  257. Activities.remove({
  258. cardId: doc._id,
  259. });
  260. });
  261. }