cards.js 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003
  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. /**
  8. * the title of the card
  9. */
  10. type: String,
  11. optional: true,
  12. defaultValue: '',
  13. },
  14. archived: {
  15. /**
  16. * is the card archived
  17. */
  18. type: Boolean,
  19. autoValue() { // eslint-disable-line consistent-return
  20. if (this.isInsert && !this.isSet) {
  21. return false;
  22. }
  23. },
  24. },
  25. parentId: {
  26. /**
  27. * ID of the parent card
  28. */
  29. type: String,
  30. optional: true,
  31. defaultValue: '',
  32. },
  33. listId: {
  34. /**
  35. * List ID where the card is
  36. */
  37. type: String,
  38. optional: true,
  39. defaultValue: '',
  40. },
  41. swimlaneId: {
  42. /**
  43. * Swimlane ID where the card is
  44. */
  45. type: String,
  46. },
  47. // The system could work without this `boardId` information (we could deduce
  48. // the board identifier from the card), but it would make the system more
  49. // difficult to manage and less efficient.
  50. boardId: {
  51. /**
  52. * Board ID of the card
  53. */
  54. type: String,
  55. optional: true,
  56. defaultValue: '',
  57. },
  58. coverId: {
  59. /**
  60. * Cover ID of the card
  61. */
  62. type: String,
  63. optional: true,
  64. defaultValue: '',
  65. },
  66. color: {
  67. type: String,
  68. optional: true,
  69. allowedValues: [
  70. 'white', 'green', 'yellow', 'orange', 'red', 'purple',
  71. 'blue', 'sky', 'lime', 'pink', 'black',
  72. 'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen',
  73. 'slateblue', 'magenta', 'gold', 'navy', 'gray',
  74. 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
  75. ],
  76. },
  77. createdAt: {
  78. /**
  79. * creation date
  80. */
  81. type: Date,
  82. // eslint-disable-next-line consistent-return
  83. autoValue() {
  84. if (this.isInsert) {
  85. return new Date();
  86. } else {
  87. this.unset();
  88. }
  89. },
  90. },
  91. modifiedAt: {
  92. type: Date,
  93. denyUpdate: false,
  94. // eslint-disable-next-line consistent-return
  95. autoValue() {
  96. if (this.isInsert || this.isUpsert || this.isUpdate) {
  97. return new Date();
  98. } else {
  99. this.unset();
  100. }
  101. },
  102. },
  103. customFields: {
  104. /**
  105. * list of custom fields
  106. */
  107. type: [Object],
  108. optional: true,
  109. defaultValue: [],
  110. },
  111. 'customFields.$': {
  112. type: new SimpleSchema({
  113. _id: {
  114. /**
  115. * the ID of the related custom field
  116. */
  117. type: String,
  118. optional: true,
  119. defaultValue: '',
  120. },
  121. value: {
  122. /**
  123. * value attached to the custom field
  124. */
  125. type: Match.OneOf(String, Number, Boolean, Date),
  126. optional: true,
  127. defaultValue: '',
  128. },
  129. }),
  130. },
  131. dateLastActivity: {
  132. /**
  133. * Date of last activity
  134. */
  135. type: Date,
  136. autoValue() {
  137. return new Date();
  138. },
  139. },
  140. description: {
  141. /**
  142. * description of the card
  143. */
  144. type: String,
  145. optional: true,
  146. defaultValue: '',
  147. },
  148. requestedBy: {
  149. /**
  150. * who requested the card (ID of the user)
  151. */
  152. type: String,
  153. optional: true,
  154. defaultValue: '',
  155. },
  156. assignedBy: {
  157. /**
  158. * who assigned the card (ID of the user)
  159. */
  160. type: String,
  161. optional: true,
  162. defaultValue: '',
  163. },
  164. labelIds: {
  165. /**
  166. * list of labels ID the card has
  167. */
  168. type: [String],
  169. optional: true,
  170. defaultValue: [],
  171. },
  172. members: {
  173. /**
  174. * list of members (user IDs)
  175. */
  176. type: [String],
  177. optional: true,
  178. defaultValue: [],
  179. },
  180. receivedAt: {
  181. /**
  182. * Date the card was received
  183. */
  184. type: Date,
  185. optional: true,
  186. },
  187. startAt: {
  188. /**
  189. * Date the card was started to be worked on
  190. */
  191. type: Date,
  192. optional: true,
  193. },
  194. dueAt: {
  195. /**
  196. * Date the card is due
  197. */
  198. type: Date,
  199. optional: true,
  200. },
  201. endAt: {
  202. /**
  203. * Date the card ended
  204. */
  205. type: Date,
  206. optional: true,
  207. },
  208. spentTime: {
  209. /**
  210. * How much time has been spent on this
  211. */
  212. type: Number,
  213. decimal: true,
  214. optional: true,
  215. defaultValue: 0,
  216. },
  217. isOvertime: {
  218. /**
  219. * is the card over time?
  220. */
  221. type: Boolean,
  222. defaultValue: false,
  223. optional: true,
  224. },
  225. // XXX Should probably be called `authorId`. Is it even needed since we have
  226. // the `members` field?
  227. userId: {
  228. /**
  229. * user ID of the author of the card
  230. */
  231. type: String,
  232. autoValue() { // eslint-disable-line consistent-return
  233. if (this.isInsert && !this.isSet) {
  234. return this.userId;
  235. }
  236. },
  237. },
  238. sort: {
  239. /**
  240. * Sort value
  241. */
  242. type: Number,
  243. decimal: true,
  244. defaultValue: '',
  245. },
  246. subtaskSort: {
  247. /**
  248. * subtask sort value
  249. */
  250. type: Number,
  251. decimal: true,
  252. defaultValue: -1,
  253. optional: true,
  254. },
  255. type: {
  256. /**
  257. * type of the card
  258. */
  259. type: String,
  260. defaultValue: 'cardType-card',
  261. },
  262. linkedId: {
  263. /**
  264. * ID of the linked card
  265. */
  266. type: String,
  267. optional: true,
  268. defaultValue: '',
  269. },
  270. }));
  271. Cards.allow({
  272. insert(userId, doc) {
  273. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  274. },
  275. update(userId, doc) {
  276. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  277. },
  278. remove(userId, doc) {
  279. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  280. },
  281. fetch: ['boardId'],
  282. });
  283. Cards.helpers({
  284. copy(boardId, swimlaneId, listId) {
  285. const oldBoard = Boards.findOne(this.boardId);
  286. const oldBoardLabels = oldBoard.labels;
  287. // Get old label names
  288. const oldCardLabels = _.pluck(_.filter(oldBoardLabels, (label) => {
  289. return _.contains(this.labelIds, label._id);
  290. }), 'name');
  291. const newBoard = Boards.findOne(boardId);
  292. const newBoardLabels = newBoard.labels;
  293. const newCardLabels = _.pluck(_.filter(newBoardLabels, (label) => {
  294. return _.contains(oldCardLabels, label.name);
  295. }), '_id');
  296. const oldId = this._id;
  297. const oldCard = Cards.findOne(oldId);
  298. // Copy Custom Fields
  299. if (oldBoard._id !== boardId) {
  300. CustomFields.find({
  301. _id: {$in: oldCard.customFields.map((cf) => { return cf._id; })},
  302. }).forEach((cf) => {
  303. if (!_.contains(cf.boardIds, boardId))
  304. cf.addBoard(boardId);
  305. });
  306. }
  307. delete this._id;
  308. delete this.labelIds;
  309. this.labelIds = newCardLabels;
  310. this.boardId = boardId;
  311. this.swimlaneId = swimlaneId;
  312. this.listId = listId;
  313. const _id = Cards.insert(this);
  314. // Copy attachments
  315. oldCard.attachments().forEach((att) => {
  316. att.cardId = _id;
  317. delete att._id;
  318. return Attachments.insert(att);
  319. });
  320. // copy checklists
  321. Checklists.find({cardId: oldId}).forEach((ch) => {
  322. ch.copy(_id);
  323. });
  324. // copy subtasks
  325. Cards.find({parentId: oldId}).forEach((subtask) => {
  326. subtask.parentId = _id;
  327. subtask._id = null;
  328. Cards.insert(subtask);
  329. });
  330. // copy card comments
  331. CardComments.find({cardId: oldId}).forEach((cmt) => {
  332. cmt.copy(_id);
  333. });
  334. return _id;
  335. },
  336. list() {
  337. return Lists.findOne(this.listId);
  338. },
  339. board() {
  340. return Boards.findOne(this.boardId);
  341. },
  342. labels() {
  343. const boardLabels = this.board().labels;
  344. const cardLabels = _.filter(boardLabels, (label) => {
  345. return _.contains(this.labelIds, label._id);
  346. });
  347. return cardLabels;
  348. },
  349. hasLabel(labelId) {
  350. return _.contains(this.labelIds, labelId);
  351. },
  352. user() {
  353. return Users.findOne(this.userId);
  354. },
  355. isAssigned(memberId) {
  356. return _.contains(this.getMembers(), memberId);
  357. },
  358. activities() {
  359. if (this.isLinkedCard()) {
  360. return Activities.find({cardId: this.linkedId}, {sort: {createdAt: -1}});
  361. } else if (this.isLinkedBoard()) {
  362. return Activities.find({boardId: this.linkedId}, {sort: {createdAt: -1}});
  363. } else {
  364. return Activities.find({cardId: this._id}, {sort: {createdAt: -1}});
  365. }
  366. },
  367. comments() {
  368. if (this.isLinkedCard()) {
  369. return CardComments.find({cardId: this.linkedId}, {sort: {createdAt: -1}});
  370. } else {
  371. return CardComments.find({cardId: this._id}, {sort: {createdAt: -1}});
  372. }
  373. },
  374. attachments() {
  375. if (this.isLinkedCard()) {
  376. return Attachments.find({cardId: this.linkedId}, {sort: {uploadedAt: -1}});
  377. } else {
  378. return Attachments.find({cardId: this._id}, {sort: {uploadedAt: -1}});
  379. }
  380. },
  381. cover() {
  382. const cover = Attachments.findOne(this.coverId);
  383. // if we return a cover before it is fully stored, we will get errors when we try to display it
  384. // todo XXX we could return a default "upload pending" image in the meantime?
  385. return cover && cover.url() && cover;
  386. },
  387. checklists() {
  388. if (this.isLinkedCard()) {
  389. return Checklists.find({cardId: this.linkedId}, {sort: { sort: 1 } });
  390. } else {
  391. return Checklists.find({cardId: this._id}, {sort: { sort: 1 } });
  392. }
  393. },
  394. checklistItemCount() {
  395. const checklists = this.checklists().fetch();
  396. return checklists.map((checklist) => {
  397. return checklist.itemCount();
  398. }).reduce((prev, next) => {
  399. return prev + next;
  400. }, 0);
  401. },
  402. checklistFinishedCount() {
  403. const checklists = this.checklists().fetch();
  404. return checklists.map((checklist) => {
  405. return checklist.finishedCount();
  406. }).reduce((prev, next) => {
  407. return prev + next;
  408. }, 0);
  409. },
  410. checklistFinished() {
  411. return this.hasChecklist() && this.checklistItemCount() === this.checklistFinishedCount();
  412. },
  413. hasChecklist() {
  414. return this.checklistItemCount() !== 0;
  415. },
  416. subtasks() {
  417. return Cards.find({
  418. parentId: this._id,
  419. archived: false,
  420. }, {
  421. sort: {
  422. sort: 1,
  423. },
  424. });
  425. },
  426. allSubtasks() {
  427. return Cards.find({
  428. parentId: this._id,
  429. archived: false,
  430. }, {
  431. sort: {
  432. sort: 1,
  433. },
  434. });
  435. },
  436. subtasksCount() {
  437. return Cards.find({
  438. parentId: this._id,
  439. archived: false,
  440. }).count();
  441. },
  442. subtasksFinishedCount() {
  443. return Cards.find({
  444. parentId: this._id,
  445. archived: true,
  446. }).count();
  447. },
  448. subtasksFinished() {
  449. const finishCount = this.subtasksFinishedCount();
  450. return finishCount > 0 && this.subtasksCount() === finishCount;
  451. },
  452. allowsSubtasks() {
  453. return this.subtasksCount() !== 0;
  454. },
  455. customFieldIndex(customFieldId) {
  456. return _.pluck(this.customFields, '_id').indexOf(customFieldId);
  457. },
  458. // customFields with definitions
  459. customFieldsWD() {
  460. // get all definitions
  461. const definitions = CustomFields.find({
  462. boardIds: {$in: [this.boardId]},
  463. }).fetch();
  464. // match right definition to each field
  465. if (!this.customFields) return [];
  466. return this.customFields.map((customField) => {
  467. const definition = definitions.find((definition) => {
  468. return definition._id === customField._id;
  469. });
  470. if (!definition) {
  471. return {};
  472. }
  473. //search for "True Value" which is for DropDowns other then the Value (which is the id)
  474. let trueValue = customField.value;
  475. if (definition.settings.dropdownItems && definition.settings.dropdownItems.length > 0) {
  476. for (let i = 0; i < definition.settings.dropdownItems.length; i++) {
  477. if (definition.settings.dropdownItems[i]._id === customField.value) {
  478. trueValue = definition.settings.dropdownItems[i].name;
  479. }
  480. }
  481. }
  482. return {
  483. _id: customField._id,
  484. value: customField.value,
  485. trueValue,
  486. definition,
  487. };
  488. });
  489. },
  490. colorClass() {
  491. if (this.color)
  492. return this.color;
  493. return '';
  494. },
  495. absoluteUrl() {
  496. const board = this.board();
  497. return FlowRouter.url('card', {
  498. boardId: board._id,
  499. slug: board.slug,
  500. cardId: this._id,
  501. });
  502. },
  503. canBeRestored() {
  504. const list = Lists.findOne({
  505. _id: this.listId,
  506. });
  507. if (!list.getWipLimit('soft') && list.getWipLimit('enabled') && list.getWipLimit('value') === list.cards().count()) {
  508. return false;
  509. }
  510. return true;
  511. },
  512. parentCard() {
  513. if (this.parentId === '') {
  514. return null;
  515. }
  516. return Cards.findOne(this.parentId);
  517. },
  518. parentCardName() {
  519. let result = '';
  520. if (this.parentId !== '') {
  521. const card = Cards.findOne(this.parentId);
  522. if (card) {
  523. result = card.title;
  524. }
  525. }
  526. return result;
  527. },
  528. parentListId() {
  529. const result = [];
  530. let crtParentId = this.parentId;
  531. while (crtParentId !== '') {
  532. const crt = Cards.findOne(crtParentId);
  533. if ((crt === null) || (crt === undefined)) {
  534. // maybe it has been deleted
  535. break;
  536. }
  537. if (crtParentId in result) {
  538. // circular reference
  539. break;
  540. }
  541. result.unshift(crtParentId);
  542. crtParentId = crt.parentId;
  543. }
  544. return result;
  545. },
  546. parentList() {
  547. const resultId = [];
  548. const result = [];
  549. let crtParentId = this.parentId;
  550. while (crtParentId !== '') {
  551. const crt = Cards.findOne(crtParentId);
  552. if ((crt === null) || (crt === undefined)) {
  553. // maybe it has been deleted
  554. break;
  555. }
  556. if (crtParentId in resultId) {
  557. // circular reference
  558. break;
  559. }
  560. resultId.unshift(crtParentId);
  561. result.unshift(crt);
  562. crtParentId = crt.parentId;
  563. }
  564. return result;
  565. },
  566. parentString(sep) {
  567. return this.parentList().map(function(elem) {
  568. return elem.title;
  569. }).join(sep);
  570. },
  571. isTopLevel() {
  572. return this.parentId === '';
  573. },
  574. isLinkedCard() {
  575. return this.type === 'cardType-linkedCard';
  576. },
  577. isLinkedBoard() {
  578. return this.type === 'cardType-linkedBoard';
  579. },
  580. isLinked() {
  581. return this.isLinkedCard() || this.isLinkedBoard();
  582. },
  583. setDescription(description) {
  584. if (this.isLinkedCard()) {
  585. return Cards.update({_id: this.linkedId}, {$set: {description}});
  586. } else if (this.isLinkedBoard()) {
  587. return Boards.update({_id: this.linkedId}, {$set: {description}});
  588. } else {
  589. return Cards.update(
  590. {_id: this._id},
  591. {$set: {description}}
  592. );
  593. }
  594. },
  595. getDescription() {
  596. if (this.isLinkedCard()) {
  597. const card = Cards.findOne({_id: this.linkedId});
  598. if (card && card.description)
  599. return card.description;
  600. else
  601. return null;
  602. } else if (this.isLinkedBoard()) {
  603. const board = Boards.findOne({_id: this.linkedId});
  604. if (board && board.description)
  605. return board.description;
  606. else
  607. return null;
  608. } else if (this.description) {
  609. return this.description;
  610. } else {
  611. return null;
  612. }
  613. },
  614. getMembers() {
  615. if (this.isLinkedCard()) {
  616. const card = Cards.findOne({_id: this.linkedId});
  617. return card.members;
  618. } else if (this.isLinkedBoard()) {
  619. const board = Boards.findOne({_id: this.linkedId});
  620. return board.activeMembers().map((member) => {
  621. return member.userId;
  622. });
  623. } else {
  624. return this.members;
  625. }
  626. },
  627. assignMember(memberId) {
  628. if (this.isLinkedCard()) {
  629. return Cards.update(
  630. { _id: this.linkedId },
  631. { $addToSet: { members: memberId }}
  632. );
  633. } else if (this.isLinkedBoard()) {
  634. const board = Boards.findOne({_id: this.linkedId});
  635. return board.addMember(memberId);
  636. } else {
  637. return Cards.update(
  638. { _id: this._id },
  639. { $addToSet: { members: memberId}}
  640. );
  641. }
  642. },
  643. unassignMember(memberId) {
  644. if (this.isLinkedCard()) {
  645. return Cards.update(
  646. { _id: this.linkedId },
  647. { $pull: { members: memberId }}
  648. );
  649. } else if (this.isLinkedBoard()) {
  650. const board = Boards.findOne({_id: this.linkedId});
  651. return board.removeMember(memberId);
  652. } else {
  653. return Cards.update(
  654. { _id: this._id },
  655. { $pull: { members: memberId}}
  656. );
  657. }
  658. },
  659. toggleMember(memberId) {
  660. if (this.getMembers() && this.getMembers().indexOf(memberId) > -1) {
  661. return this.unassignMember(memberId);
  662. } else {
  663. return this.assignMember(memberId);
  664. }
  665. },
  666. getReceived() {
  667. if (this.isLinkedCard()) {
  668. const card = Cards.findOne({_id: this.linkedId});
  669. return card.receivedAt;
  670. } else {
  671. return this.receivedAt;
  672. }
  673. },
  674. setReceived(receivedAt) {
  675. if (this.isLinkedCard()) {
  676. return Cards.update(
  677. {_id: this.linkedId},
  678. {$set: {receivedAt}}
  679. );
  680. } else {
  681. return Cards.update(
  682. {_id: this._id},
  683. {$set: {receivedAt}}
  684. );
  685. }
  686. },
  687. getStart() {
  688. if (this.isLinkedCard()) {
  689. const card = Cards.findOne({_id: this.linkedId});
  690. return card.startAt;
  691. } else if (this.isLinkedBoard()) {
  692. const board = Boards.findOne({_id: this.linkedId});
  693. return board.startAt;
  694. } else {
  695. return this.startAt;
  696. }
  697. },
  698. setStart(startAt) {
  699. if (this.isLinkedCard()) {
  700. return Cards.update(
  701. { _id: this.linkedId },
  702. {$set: {startAt}}
  703. );
  704. } else if (this.isLinkedBoard()) {
  705. return Boards.update(
  706. {_id: this.linkedId},
  707. {$set: {startAt}}
  708. );
  709. } else {
  710. return Cards.update(
  711. {_id: this._id},
  712. {$set: {startAt}}
  713. );
  714. }
  715. },
  716. getDue() {
  717. if (this.isLinkedCard()) {
  718. const card = Cards.findOne({_id: this.linkedId});
  719. return card.dueAt;
  720. } else if (this.isLinkedBoard()) {
  721. const board = Boards.findOne({_id: this.linkedId});
  722. return board.dueAt;
  723. } else {
  724. return this.dueAt;
  725. }
  726. },
  727. setDue(dueAt) {
  728. if (this.isLinkedCard()) {
  729. return Cards.update(
  730. { _id: this.linkedId },
  731. {$set: {dueAt}}
  732. );
  733. } else if (this.isLinkedBoard()) {
  734. return Boards.update(
  735. {_id: this.linkedId},
  736. {$set: {dueAt}}
  737. );
  738. } else {
  739. return Cards.update(
  740. {_id: this._id},
  741. {$set: {dueAt}}
  742. );
  743. }
  744. },
  745. getEnd() {
  746. if (this.isLinkedCard()) {
  747. const card = Cards.findOne({_id: this.linkedId});
  748. return card.endAt;
  749. } else if (this.isLinkedBoard()) {
  750. const board = Boards.findOne({_id: this.linkedId});
  751. return board.endAt;
  752. } else {
  753. return this.endAt;
  754. }
  755. },
  756. setEnd(endAt) {
  757. if (this.isLinkedCard()) {
  758. return Cards.update(
  759. { _id: this.linkedId },
  760. {$set: {endAt}}
  761. );
  762. } else if (this.isLinkedBoard()) {
  763. return Boards.update(
  764. {_id: this.linkedId},
  765. {$set: {endAt}}
  766. );
  767. } else {
  768. return Cards.update(
  769. {_id: this._id},
  770. {$set: {endAt}}
  771. );
  772. }
  773. },
  774. getIsOvertime() {
  775. if (this.isLinkedCard()) {
  776. const card = Cards.findOne({ _id: this.linkedId });
  777. return card.isOvertime;
  778. } else if (this.isLinkedBoard()) {
  779. const board = Boards.findOne({ _id: this.linkedId});
  780. return board.isOvertime;
  781. } else {
  782. return this.isOvertime;
  783. }
  784. },
  785. setIsOvertime(isOvertime) {
  786. if (this.isLinkedCard()) {
  787. return Cards.update(
  788. { _id: this.linkedId },
  789. {$set: {isOvertime}}
  790. );
  791. } else if (this.isLinkedBoard()) {
  792. return Boards.update(
  793. {_id: this.linkedId},
  794. {$set: {isOvertime}}
  795. );
  796. } else {
  797. return Cards.update(
  798. {_id: this._id},
  799. {$set: {isOvertime}}
  800. );
  801. }
  802. },
  803. getSpentTime() {
  804. if (this.isLinkedCard()) {
  805. const card = Cards.findOne({ _id: this.linkedId });
  806. return card.spentTime;
  807. } else if (this.isLinkedBoard()) {
  808. const board = Boards.findOne({ _id: this.linkedId});
  809. return board.spentTime;
  810. } else {
  811. return this.spentTime;
  812. }
  813. },
  814. setSpentTime(spentTime) {
  815. if (this.isLinkedCard()) {
  816. return Cards.update(
  817. { _id: this.linkedId },
  818. {$set: {spentTime}}
  819. );
  820. } else if (this.isLinkedBoard()) {
  821. return Boards.update(
  822. {_id: this.linkedId},
  823. {$set: {spentTime}}
  824. );
  825. } else {
  826. return Cards.update(
  827. {_id: this._id},
  828. {$set: {spentTime}}
  829. );
  830. }
  831. },
  832. getId() {
  833. if (this.isLinked()) {
  834. return this.linkedId;
  835. } else {
  836. return this._id;
  837. }
  838. },
  839. getTitle() {
  840. if (this.isLinkedCard()) {
  841. const card = Cards.findOne({ _id: this.linkedId });
  842. return card.title;
  843. } else if (this.isLinkedBoard()) {
  844. const board = Boards.findOne({ _id: this.linkedId});
  845. return board.title;
  846. } else {
  847. return this.title;
  848. }
  849. },
  850. getBoardTitle() {
  851. if (this.isLinkedCard()) {
  852. const card = Cards.findOne({ _id: this.linkedId });
  853. const board = Boards.findOne({ _id: card.boardId });
  854. return board.title;
  855. } else if (this.isLinkedBoard()) {
  856. const board = Boards.findOne({ _id: this.linkedId});
  857. return board.title;
  858. } else {
  859. const board = Boards.findOne({ _id: this.boardId });
  860. return board.title;
  861. }
  862. },
  863. setTitle(title) {
  864. if (this.isLinkedCard()) {
  865. return Cards.update(
  866. { _id: this.linkedId },
  867. {$set: {title}}
  868. );
  869. } else if (this.isLinkedBoard()) {
  870. return Boards.update(
  871. {_id: this.linkedId},
  872. {$set: {title}}
  873. );
  874. } else {
  875. return Cards.update(
  876. {_id: this._id},
  877. {$set: {title}}
  878. );
  879. }
  880. },
  881. getArchived() {
  882. if (this.isLinkedCard()) {
  883. const card = Cards.findOne({ _id: this.linkedId });
  884. return card.archived;
  885. } else if (this.isLinkedBoard()) {
  886. const board = Boards.findOne({ _id: this.linkedId});
  887. return board.archived;
  888. } else {
  889. return this.archived;
  890. }
  891. },
  892. setRequestedBy(requestedBy) {
  893. if (this.isLinkedCard()) {
  894. return Cards.update(
  895. { _id: this.linkedId },
  896. {$set: {requestedBy}}
  897. );
  898. } else {
  899. return Cards.update(
  900. {_id: this._id},
  901. {$set: {requestedBy}}
  902. );
  903. }
  904. },
  905. getRequestedBy() {
  906. if (this.isLinkedCard()) {
  907. const card = Cards.findOne({ _id: this.linkedId });
  908. return card.requestedBy;
  909. } else {
  910. return this.requestedBy;
  911. }
  912. },
  913. setAssignedBy(assignedBy) {
  914. if (this.isLinkedCard()) {
  915. return Cards.update(
  916. { _id: this.linkedId },
  917. {$set: {assignedBy}}
  918. );
  919. } else {
  920. return Cards.update(
  921. {_id: this._id},
  922. {$set: {assignedBy}}
  923. );
  924. }
  925. },
  926. getAssignedBy() {
  927. if (this.isLinkedCard()) {
  928. const card = Cards.findOne({ _id: this.linkedId });
  929. return card.assignedBy;
  930. } else {
  931. return this.assignedBy;
  932. }
  933. },
  934. isTemplateCard() {
  935. return this.type === 'template-card';
  936. },
  937. });
  938. Cards.mutations({
  939. applyToChildren(funct) {
  940. Cards.find({
  941. parentId: this._id,
  942. }).forEach((card) => {
  943. funct(card);
  944. });
  945. },
  946. archive() {
  947. this.applyToChildren((card) => {
  948. return card.archive();
  949. });
  950. return {
  951. $set: {
  952. archived: true,
  953. },
  954. };
  955. },
  956. restore() {
  957. this.applyToChildren((card) => {
  958. return card.restore();
  959. });
  960. return {
  961. $set: {
  962. archived: false,
  963. },
  964. };
  965. },
  966. move(boardId, swimlaneId, listId, sort) {
  967. // Copy Custom Fields
  968. if (this.boardId !== boardId) {
  969. CustomFields.find({
  970. _id: {$in: this.customFields.map((cf) => { return cf._id; })},
  971. }).forEach((cf) => {
  972. if (!_.contains(cf.boardIds, boardId))
  973. cf.addBoard(boardId);
  974. });
  975. }
  976. // Get label names
  977. const oldBoard = Boards.findOne(this.boardId);
  978. const oldBoardLabels = oldBoard.labels;
  979. const oldCardLabels = _.pluck(_.filter(oldBoardLabels, (label) => {
  980. return _.contains(this.labelIds, label._id);
  981. }), 'name');
  982. const newBoard = Boards.findOne(boardId);
  983. const newBoardLabels = newBoard.labels;
  984. const newCardLabelIds = _.pluck(_.filter(newBoardLabels, (label) => {
  985. return label.name && _.contains(oldCardLabels, label.name);
  986. }), '_id');
  987. const mutatedFields = {
  988. boardId,
  989. swimlaneId,
  990. listId,
  991. sort,
  992. labelIds: newCardLabelIds,
  993. };
  994. Cards.update(this._id, {
  995. $set: mutatedFields,
  996. });
  997. },
  998. addLabel(labelId) {
  999. return {
  1000. $addToSet: {
  1001. labelIds: labelId,
  1002. },
  1003. };
  1004. },
  1005. removeLabel(labelId) {
  1006. return {
  1007. $pull: {
  1008. labelIds: labelId,
  1009. },
  1010. };
  1011. },
  1012. toggleLabel(labelId) {
  1013. if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
  1014. return this.removeLabel(labelId);
  1015. } else {
  1016. return this.addLabel(labelId);
  1017. }
  1018. },
  1019. setColor(newColor) {
  1020. if (newColor === 'white') {
  1021. newColor = null;
  1022. }
  1023. return {
  1024. $set: {
  1025. color: newColor,
  1026. },
  1027. };
  1028. },
  1029. assignMember(memberId) {
  1030. return {
  1031. $addToSet: {
  1032. members: memberId,
  1033. },
  1034. };
  1035. },
  1036. unassignMember(memberId) {
  1037. return {
  1038. $pull: {
  1039. members: memberId,
  1040. },
  1041. };
  1042. },
  1043. toggleMember(memberId) {
  1044. if (this.members && this.members.indexOf(memberId) > -1) {
  1045. return this.unassignMember(memberId);
  1046. } else {
  1047. return this.assignMember(memberId);
  1048. }
  1049. },
  1050. assignCustomField(customFieldId) {
  1051. return {
  1052. $addToSet: {
  1053. customFields: {
  1054. _id: customFieldId,
  1055. value: null,
  1056. },
  1057. },
  1058. };
  1059. },
  1060. unassignCustomField(customFieldId) {
  1061. return {
  1062. $pull: {
  1063. customFields: {
  1064. _id: customFieldId,
  1065. },
  1066. },
  1067. };
  1068. },
  1069. toggleCustomField(customFieldId) {
  1070. if (this.customFields && this.customFieldIndex(customFieldId) > -1) {
  1071. return this.unassignCustomField(customFieldId);
  1072. } else {
  1073. return this.assignCustomField(customFieldId);
  1074. }
  1075. },
  1076. setCustomField(customFieldId, value) {
  1077. // todo
  1078. const index = this.customFieldIndex(customFieldId);
  1079. if (index > -1) {
  1080. const update = {
  1081. $set: {},
  1082. };
  1083. update.$set[`customFields.${index}.value`] = value;
  1084. return update;
  1085. }
  1086. // TODO
  1087. // Ignatz 18.05.2018: Return null to silence ESLint. No Idea if that is correct
  1088. return null;
  1089. },
  1090. setCover(coverId) {
  1091. return {
  1092. $set: {
  1093. coverId,
  1094. },
  1095. };
  1096. },
  1097. unsetCover() {
  1098. return {
  1099. $unset: {
  1100. coverId: '',
  1101. },
  1102. };
  1103. },
  1104. setReceived(receivedAt) {
  1105. return {
  1106. $set: {
  1107. receivedAt,
  1108. },
  1109. };
  1110. },
  1111. unsetReceived() {
  1112. return {
  1113. $unset: {
  1114. receivedAt: '',
  1115. },
  1116. };
  1117. },
  1118. setStart(startAt) {
  1119. return {
  1120. $set: {
  1121. startAt,
  1122. },
  1123. };
  1124. },
  1125. unsetStart() {
  1126. return {
  1127. $unset: {
  1128. startAt: '',
  1129. },
  1130. };
  1131. },
  1132. setDue(dueAt) {
  1133. return {
  1134. $set: {
  1135. dueAt,
  1136. },
  1137. };
  1138. },
  1139. unsetDue() {
  1140. return {
  1141. $unset: {
  1142. dueAt: '',
  1143. },
  1144. };
  1145. },
  1146. setEnd(endAt) {
  1147. return {
  1148. $set: {
  1149. endAt,
  1150. },
  1151. };
  1152. },
  1153. unsetEnd() {
  1154. return {
  1155. $unset: {
  1156. endAt: '',
  1157. },
  1158. };
  1159. },
  1160. setOvertime(isOvertime) {
  1161. return {
  1162. $set: {
  1163. isOvertime,
  1164. },
  1165. };
  1166. },
  1167. setSpentTime(spentTime) {
  1168. return {
  1169. $set: {
  1170. spentTime,
  1171. },
  1172. };
  1173. },
  1174. unsetSpentTime() {
  1175. return {
  1176. $unset: {
  1177. spentTime: '',
  1178. isOvertime: false,
  1179. },
  1180. };
  1181. },
  1182. setParentId(parentId) {
  1183. return {
  1184. $set: {
  1185. parentId,
  1186. },
  1187. };
  1188. },
  1189. });
  1190. //FUNCTIONS FOR creation of Activities
  1191. function updateActivities(doc, fieldNames, modifier) {
  1192. if (_.contains(fieldNames, 'labelIds') && _.contains(fieldNames, 'boardId')) {
  1193. Activities.find({
  1194. activityType: 'addedLabel',
  1195. cardId: doc._id,
  1196. }).forEach((a) => {
  1197. const lidx = doc.labelIds.indexOf(a.labelId);
  1198. if (lidx !== -1 && modifier.$set.labelIds.length > lidx) {
  1199. Activities.update(a._id, {
  1200. $set: {
  1201. labelId: modifier.$set.labelIds[doc.labelIds.indexOf(a.labelId)],
  1202. boardId: modifier.$set.boardId,
  1203. },
  1204. });
  1205. } else {
  1206. Activities.remove(a._id);
  1207. }
  1208. });
  1209. } else if (_.contains(fieldNames, 'boardId')) {
  1210. Activities.remove({
  1211. activityType: 'addedLabel',
  1212. cardId: doc._id,
  1213. });
  1214. }
  1215. }
  1216. function cardMove(userId, doc, fieldNames, oldListId, oldSwimlaneId, oldBoardId) {
  1217. if (_.contains(fieldNames, 'boardId') && (doc.boardId !== oldBoardId)) {
  1218. Activities.insert({
  1219. userId,
  1220. activityType: 'moveCardBoard',
  1221. boardName: Boards.findOne(doc.boardId).title,
  1222. boardId: doc.boardId,
  1223. oldBoardId,
  1224. oldBoardName: Boards.findOne(oldBoardId).title,
  1225. cardId: doc._id,
  1226. swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
  1227. swimlaneId: doc.swimlaneId,
  1228. oldSwimlaneId,
  1229. });
  1230. } else if ((_.contains(fieldNames, 'listId') && doc.listId !== oldListId) ||
  1231. (_.contains(fieldNames, 'swimlaneId') && doc.swimlaneId !== oldSwimlaneId)){
  1232. Activities.insert({
  1233. userId,
  1234. oldListId,
  1235. activityType: 'moveCard',
  1236. listName: Lists.findOne(doc.listId).title,
  1237. listId: doc.listId,
  1238. boardId: doc.boardId,
  1239. cardId: doc._id,
  1240. cardTitle:doc.title,
  1241. swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
  1242. swimlaneId: doc.swimlaneId,
  1243. oldSwimlaneId,
  1244. });
  1245. }
  1246. }
  1247. function cardState(userId, doc, fieldNames) {
  1248. if (_.contains(fieldNames, 'archived')) {
  1249. if (doc.archived) {
  1250. Activities.insert({
  1251. userId,
  1252. activityType: 'archivedCard',
  1253. listName: Lists.findOne(doc.listId).title,
  1254. boardId: doc.boardId,
  1255. listId: doc.listId,
  1256. cardId: doc._id,
  1257. swimlaneId: doc.swimlaneId,
  1258. });
  1259. } else {
  1260. Activities.insert({
  1261. userId,
  1262. activityType: 'restoredCard',
  1263. boardId: doc.boardId,
  1264. listName: Lists.findOne(doc.listId).title,
  1265. listId: doc.listId,
  1266. cardId: doc._id,
  1267. swimlaneId: doc.swimlaneId,
  1268. });
  1269. }
  1270. }
  1271. }
  1272. function cardMembers(userId, doc, fieldNames, modifier) {
  1273. if (!_.contains(fieldNames, 'members'))
  1274. return;
  1275. let memberId;
  1276. // Say hello to the new member
  1277. if (modifier.$addToSet && modifier.$addToSet.members) {
  1278. memberId = modifier.$addToSet.members;
  1279. const username = Users.findOne(memberId).username;
  1280. if (!_.contains(doc.members, memberId)) {
  1281. Activities.insert({
  1282. userId,
  1283. username,
  1284. activityType: 'joinMember',
  1285. boardId: doc.boardId,
  1286. cardId: doc._id,
  1287. memberId,
  1288. listId: doc.listId,
  1289. swimlaneId: doc.swimlaneId,
  1290. });
  1291. }
  1292. }
  1293. // Say goodbye to the former member
  1294. if (modifier.$pull && modifier.$pull.members) {
  1295. memberId = modifier.$pull.members;
  1296. const username = Users.findOne(memberId).username;
  1297. // Check that the former member is member of the card
  1298. if (_.contains(doc.members, memberId)) {
  1299. Activities.insert({
  1300. userId,
  1301. username,
  1302. activityType: 'unjoinMember',
  1303. boardId: doc.boardId,
  1304. cardId: doc._id,
  1305. memberId,
  1306. listId: doc.listId,
  1307. swimlaneId: doc.swimlaneId,
  1308. });
  1309. }
  1310. }
  1311. }
  1312. function cardLabels(userId, doc, fieldNames, modifier) {
  1313. if (!_.contains(fieldNames, 'labelIds'))
  1314. return;
  1315. let labelId;
  1316. // Say hello to the new label
  1317. if (modifier.$addToSet && modifier.$addToSet.labelIds) {
  1318. labelId = modifier.$addToSet.labelIds;
  1319. if (!_.contains(doc.labelIds, labelId)) {
  1320. const act = {
  1321. userId,
  1322. labelId,
  1323. activityType: 'addedLabel',
  1324. boardId: doc.boardId,
  1325. cardId: doc._id,
  1326. listId: doc.listId,
  1327. swimlaneId: doc.swimlaneId,
  1328. };
  1329. Activities.insert(act);
  1330. }
  1331. }
  1332. // Say goodbye to the label
  1333. if (modifier.$pull && modifier.$pull.labelIds) {
  1334. labelId = modifier.$pull.labelIds;
  1335. // Check that the former member is member of the card
  1336. if (_.contains(doc.labelIds, labelId)) {
  1337. Activities.insert({
  1338. userId,
  1339. labelId,
  1340. activityType: 'removedLabel',
  1341. boardId: doc.boardId,
  1342. cardId: doc._id,
  1343. listId: doc.listId,
  1344. swimlaneId: doc.swimlaneId,
  1345. });
  1346. }
  1347. }
  1348. }
  1349. function cardCustomFields(userId, doc, fieldNames, modifier) {
  1350. if (!_.contains(fieldNames, 'customFields'))
  1351. return;
  1352. // Say hello to the new customField value
  1353. if (modifier.$set) {
  1354. _.each(modifier.$set, (value, key) => {
  1355. if (key.startsWith('customFields')) {
  1356. const dotNotation = key.split('.');
  1357. // only individual changes are registered
  1358. if (dotNotation.length > 1) {
  1359. const customFieldId = doc.customFields[dotNotation[1]]._id;
  1360. const act = {
  1361. userId,
  1362. customFieldId,
  1363. value,
  1364. activityType: 'setCustomField',
  1365. boardId: doc.boardId,
  1366. cardId: doc._id,
  1367. };
  1368. Activities.insert(act);
  1369. }
  1370. }
  1371. });
  1372. }
  1373. // Say goodbye to the former customField value
  1374. if (modifier.$unset) {
  1375. _.each(modifier.$unset, (value, key) => {
  1376. if (key.startsWith('customFields')) {
  1377. const dotNotation = key.split('.');
  1378. // only individual changes are registered
  1379. if (dotNotation.length > 1) {
  1380. const customFieldId = doc.customFields[dotNotation[1]]._id;
  1381. const act = {
  1382. userId,
  1383. customFieldId,
  1384. activityType: 'unsetCustomField',
  1385. boardId: doc.boardId,
  1386. cardId: doc._id,
  1387. };
  1388. Activities.insert(act);
  1389. }
  1390. }
  1391. });
  1392. }
  1393. }
  1394. function cardCreation(userId, doc) {
  1395. Activities.insert({
  1396. userId,
  1397. activityType: 'createCard',
  1398. boardId: doc.boardId,
  1399. listName: Lists.findOne(doc.listId).title,
  1400. listId: doc.listId,
  1401. cardId: doc._id,
  1402. cardTitle:doc.title,
  1403. swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
  1404. swimlaneId: doc.swimlaneId,
  1405. });
  1406. }
  1407. function cardRemover(userId, doc) {
  1408. ChecklistItems.remove({
  1409. cardId: doc._id,
  1410. });
  1411. Checklists.remove({
  1412. cardId: doc._id,
  1413. });
  1414. Cards.remove({
  1415. parentId: doc._id,
  1416. });
  1417. CardComments.remove({
  1418. cardId: doc._id,
  1419. });
  1420. Attachments.remove({
  1421. cardId: doc._id,
  1422. });
  1423. }
  1424. if (Meteor.isServer) {
  1425. // Cards are often fetched within a board, so we create an index to make these
  1426. // queries more efficient.
  1427. Meteor.startup(() => {
  1428. Cards._collection._ensureIndex({ modifiedAt: -1 });
  1429. Cards._collection._ensureIndex({ boardId: 1, createdAt: -1 });
  1430. // https://github.com/wekan/wekan/issues/1863
  1431. // Swimlane added a new field in the cards collection of mongodb named parentId.
  1432. // When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection).
  1433. // With a huge database, this result in a very slow app and high CPU on the mongodb side.
  1434. // To correct it, add Index to parentId:
  1435. Cards._collection._ensureIndex({parentId: 1});
  1436. });
  1437. Cards.after.insert((userId, doc) => {
  1438. cardCreation(userId, doc);
  1439. });
  1440. // New activity for card (un)archivage
  1441. Cards.after.update((userId, doc, fieldNames) => {
  1442. cardState(userId, doc, fieldNames);
  1443. });
  1444. //New activity for card moves
  1445. Cards.after.update(function(userId, doc, fieldNames) {
  1446. const oldListId = this.previous.listId;
  1447. const oldSwimlaneId = this.previous.swimlaneId;
  1448. const oldBoardId = this.previous.boardId;
  1449. cardMove(userId, doc, fieldNames, oldListId, oldSwimlaneId, oldBoardId);
  1450. });
  1451. // Add a new activity if we add or remove a member to the card
  1452. Cards.before.update((userId, doc, fieldNames, modifier) => {
  1453. cardMembers(userId, doc, fieldNames, modifier);
  1454. updateActivities(doc, fieldNames, modifier);
  1455. });
  1456. // Add a new activity if we add or remove a label to the card
  1457. Cards.before.update((userId, doc, fieldNames, modifier) => {
  1458. cardLabels(userId, doc, fieldNames, modifier);
  1459. });
  1460. // Add a new activity if we edit a custom field
  1461. Cards.before.update((userId, doc, fieldNames, modifier) => {
  1462. cardCustomFields(userId, doc, fieldNames, modifier);
  1463. });
  1464. Cards.before.update((userId, doc, fieldNames, modifier, options) => {
  1465. modifier.$set = modifier.$set || {};
  1466. modifier.$set.modifiedAt = Date.now();
  1467. });
  1468. // Remove all activities associated with a card if we remove the card
  1469. // Remove also card_comments / checklists / attachments
  1470. Cards.before.remove((userId, doc) => {
  1471. cardRemover(userId, doc);
  1472. });
  1473. }
  1474. //SWIMLANES REST API
  1475. if (Meteor.isServer) {
  1476. /**
  1477. * @operation get_swimlane_cards
  1478. * @summary get all cards attached to a swimlane
  1479. *
  1480. * @param {string} boardId the board ID
  1481. * @param {string} swimlaneId the swimlane ID
  1482. * @return_type [{_id: string,
  1483. * title: string,
  1484. * description: string,
  1485. * listId: string}]
  1486. */
  1487. JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId/cards', function(req, res) {
  1488. const paramBoardId = req.params.boardId;
  1489. const paramSwimlaneId = req.params.swimlaneId;
  1490. Authentication.checkBoardAccess(req.userId, paramBoardId);
  1491. JsonRoutes.sendResult(res, {
  1492. code: 200,
  1493. data: Cards.find({
  1494. boardId: paramBoardId,
  1495. swimlaneId: paramSwimlaneId,
  1496. archived: false,
  1497. }).map(function(doc) {
  1498. return {
  1499. _id: doc._id,
  1500. title: doc.title,
  1501. description: doc.description,
  1502. listId: doc.listId,
  1503. };
  1504. }),
  1505. });
  1506. });
  1507. }
  1508. //LISTS REST API
  1509. if (Meteor.isServer) {
  1510. /**
  1511. * @operation get_all_cards
  1512. * @summary Get all Cards attached to a List
  1513. *
  1514. * @param {string} boardId the board ID
  1515. * @param {string} listId the list ID
  1516. * @return_type [{_id: string,
  1517. * title: string,
  1518. * description: string}]
  1519. */
  1520. JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function(req, res) {
  1521. const paramBoardId = req.params.boardId;
  1522. const paramListId = req.params.listId;
  1523. Authentication.checkBoardAccess(req.userId, paramBoardId);
  1524. JsonRoutes.sendResult(res, {
  1525. code: 200,
  1526. data: Cards.find({
  1527. boardId: paramBoardId,
  1528. listId: paramListId,
  1529. archived: false,
  1530. }).map(function(doc) {
  1531. return {
  1532. _id: doc._id,
  1533. title: doc.title,
  1534. description: doc.description,
  1535. };
  1536. }),
  1537. });
  1538. });
  1539. /**
  1540. * @operation get_card
  1541. * @summary Get a Card
  1542. *
  1543. * @param {string} boardId the board ID
  1544. * @param {string} listId the list ID of the card
  1545. * @param {string} cardId the card ID
  1546. * @return_type Cards
  1547. */
  1548. JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards/:cardId', function(req, res) {
  1549. const paramBoardId = req.params.boardId;
  1550. const paramListId = req.params.listId;
  1551. const paramCardId = req.params.cardId;
  1552. Authentication.checkBoardAccess(req.userId, paramBoardId);
  1553. JsonRoutes.sendResult(res, {
  1554. code: 200,
  1555. data: Cards.findOne({
  1556. _id: paramCardId,
  1557. listId: paramListId,
  1558. boardId: paramBoardId,
  1559. archived: false,
  1560. }),
  1561. });
  1562. });
  1563. /**
  1564. * @operation new_card
  1565. * @summary Create a new Card
  1566. *
  1567. * @param {string} boardId the board ID of the new card
  1568. * @param {string} listId the list ID of the new card
  1569. * @param {string} authorID the user ID of the person owning the card
  1570. * @param {string} parentId the parent ID of the new card
  1571. * @param {string} title the title of the new card
  1572. * @param {string} description the description of the new card
  1573. * @param {string} swimlaneId the swimlane ID of the new card
  1574. * @param {string} [members] the member IDs list of the new card
  1575. * @return_type {_id: string}
  1576. */
  1577. JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function(req, res) {
  1578. Authentication.checkUserId(req.userId);
  1579. const paramBoardId = req.params.boardId;
  1580. const paramListId = req.params.listId;
  1581. const paramParentId = req.params.parentId;
  1582. const currentCards = Cards.find({
  1583. listId: paramListId,
  1584. archived: false,
  1585. }, { sort: ['sort'] });
  1586. const check = Users.findOne({
  1587. _id: req.body.authorId,
  1588. });
  1589. const members = req.body.members || [req.body.authorId];
  1590. if (typeof check !== 'undefined') {
  1591. const id = Cards.direct.insert({
  1592. title: req.body.title,
  1593. boardId: paramBoardId,
  1594. listId: paramListId,
  1595. parentId: paramParentId,
  1596. description: req.body.description,
  1597. userId: req.body.authorId,
  1598. swimlaneId: req.body.swimlaneId,
  1599. sort: currentCards.count(),
  1600. members,
  1601. });
  1602. JsonRoutes.sendResult(res, {
  1603. code: 200,
  1604. data: {
  1605. _id: id,
  1606. },
  1607. });
  1608. const card = Cards.findOne({
  1609. _id: id,
  1610. });
  1611. cardCreation(req.body.authorId, card);
  1612. } else {
  1613. JsonRoutes.sendResult(res, {
  1614. code: 401,
  1615. });
  1616. }
  1617. });
  1618. /*
  1619. * Note for the JSDoc:
  1620. * 'list' will be interpreted as the path parameter
  1621. * 'listID' will be interpreted as the body parameter
  1622. */
  1623. /**
  1624. * @operation edit_card
  1625. * @summary Edit Fields in a Card
  1626. *
  1627. * @description Edit a card
  1628. *
  1629. * The color has to be chosen between `white`, `green`, `yellow`, `orange`,
  1630. * `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`,
  1631. * `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`,
  1632. * `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`,
  1633. * `indigo`:
  1634. *
  1635. * <img src="/card-colors.png" width="40%" alt="Wekan card colors" />
  1636. *
  1637. * Note: setting the color to white has the same effect than removing it.
  1638. *
  1639. * @param {string} boardId the board ID of the card
  1640. * @param {string} list the list ID of the card
  1641. * @param {string} cardId the ID of the card
  1642. * @param {string} [title] the new title of the card
  1643. * @param {string} [listId] the new list ID of the card (move operation)
  1644. * @param {string} [description] the new description of the card
  1645. * @param {string} [authorId] change the owner of the card
  1646. * @param {string} [parentId] change the parent of the card
  1647. * @param {string} [labelIds] the new list of label IDs attached to the card
  1648. * @param {string} [swimlaneId] the new swimlane ID of the card
  1649. * @param {string} [members] the new list of member IDs attached to the card
  1650. * @param {string} [requestedBy] the new requestedBy field of the card
  1651. * @param {string} [assignedBy] the new assignedBy field of the card
  1652. * @param {string} [receivedAt] the new receivedAt field of the card
  1653. * @param {string} [assignBy] the new assignBy field of the card
  1654. * @param {string} [startAt] the new startAt field of the card
  1655. * @param {string} [dueAt] the new dueAt field of the card
  1656. * @param {string} [endAt] the new endAt field of the card
  1657. * @param {string} [spentTime] the new spentTime field of the card
  1658. * @param {boolean} [isOverTime] the new isOverTime field of the card
  1659. * @param {string} [customFields] the new customFields value of the card
  1660. * @param {string} [color] the new color of the card
  1661. * @return_type {_id: string}
  1662. */
  1663. JsonRoutes.add('PUT', '/api/boards/:boardId/lists/:listId/cards/:cardId', function(req, res) {
  1664. Authentication.checkUserId(req.userId);
  1665. const paramBoardId = req.params.boardId;
  1666. const paramCardId = req.params.cardId;
  1667. const paramListId = req.params.listId;
  1668. if (req.body.hasOwnProperty('title')) {
  1669. const newTitle = req.body.title;
  1670. Cards.direct.update({
  1671. _id: paramCardId,
  1672. listId: paramListId,
  1673. boardId: paramBoardId,
  1674. archived: false,
  1675. }, {
  1676. $set: {
  1677. title: newTitle,
  1678. },
  1679. });
  1680. }
  1681. if (req.body.hasOwnProperty('listId')) {
  1682. const newParamListId = req.body.listId;
  1683. Cards.direct.update({
  1684. _id: paramCardId,
  1685. listId: paramListId,
  1686. boardId: paramBoardId,
  1687. archived: false,
  1688. }, {
  1689. $set: {
  1690. listId: newParamListId,
  1691. },
  1692. });
  1693. const card = Cards.findOne({
  1694. _id: paramCardId,
  1695. });
  1696. cardMove(req.body.authorId, card, {
  1697. fieldName: 'listId',
  1698. }, paramListId);
  1699. }
  1700. if (req.body.hasOwnProperty('parentId')) {
  1701. const newParentId = req.body.parentId;
  1702. Cards.direct.update({
  1703. _id: paramCardId,
  1704. listId: paramListId,
  1705. boardId: paramBoardId,
  1706. archived: false,
  1707. }, {
  1708. $set: {
  1709. parentId: newParentId,
  1710. },
  1711. });
  1712. }
  1713. if (req.body.hasOwnProperty('description')) {
  1714. const newDescription = req.body.description;
  1715. Cards.direct.update({
  1716. _id: paramCardId,
  1717. listId: paramListId,
  1718. boardId: paramBoardId,
  1719. archived: false,
  1720. }, {
  1721. $set: {
  1722. description: newDescription,
  1723. },
  1724. });
  1725. }
  1726. if (req.body.hasOwnProperty('color')) {
  1727. const newColor = req.body.color;
  1728. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1729. {$set: {color: newColor}});
  1730. }
  1731. if (req.body.hasOwnProperty('labelIds')) {
  1732. let newlabelIds = req.body.labelIds;
  1733. if (_.isString(newlabelIds)) {
  1734. if (newlabelIds === '') {
  1735. newlabelIds = null;
  1736. }
  1737. else {
  1738. newlabelIds = [newlabelIds];
  1739. }
  1740. }
  1741. Cards.direct.update({
  1742. _id: paramCardId,
  1743. listId: paramListId,
  1744. boardId: paramBoardId,
  1745. archived: false,
  1746. }, {
  1747. $set: {
  1748. labelIds: newlabelIds,
  1749. },
  1750. });
  1751. }
  1752. if (req.body.hasOwnProperty('requestedBy')) {
  1753. const newrequestedBy = req.body.requestedBy;
  1754. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1755. {$set: {requestedBy: newrequestedBy}});
  1756. }
  1757. if (req.body.hasOwnProperty('assignedBy')) {
  1758. const newassignedBy = req.body.assignedBy;
  1759. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1760. {$set: {assignedBy: newassignedBy}});
  1761. }
  1762. if (req.body.hasOwnProperty('receivedAt')) {
  1763. const newreceivedAt = req.body.receivedAt;
  1764. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1765. {$set: {receivedAt: newreceivedAt}});
  1766. }
  1767. if (req.body.hasOwnProperty('startAt')) {
  1768. const newstartAt = req.body.startAt;
  1769. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1770. {$set: {startAt: newstartAt}});
  1771. }
  1772. if (req.body.hasOwnProperty('dueAt')) {
  1773. const newdueAt = req.body.dueAt;
  1774. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1775. {$set: {dueAt: newdueAt}});
  1776. }
  1777. if (req.body.hasOwnProperty('endAt')) {
  1778. const newendAt = req.body.endAt;
  1779. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1780. {$set: {endAt: newendAt}});
  1781. }
  1782. if (req.body.hasOwnProperty('spentTime')) {
  1783. const newspentTime = req.body.spentTime;
  1784. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1785. {$set: {spentTime: newspentTime}});
  1786. }
  1787. if (req.body.hasOwnProperty('isOverTime')) {
  1788. const newisOverTime = req.body.isOverTime;
  1789. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1790. {$set: {isOverTime: newisOverTime}});
  1791. }
  1792. if (req.body.hasOwnProperty('customFields')) {
  1793. const newcustomFields = req.body.customFields;
  1794. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1795. {$set: {customFields: newcustomFields}});
  1796. }
  1797. if (req.body.hasOwnProperty('members')) {
  1798. let newmembers = req.body.members;
  1799. if (_.isString(newmembers)) {
  1800. if (newmembers === '') {
  1801. newmembers = null;
  1802. }
  1803. else {
  1804. newmembers = [newmembers];
  1805. }
  1806. }
  1807. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1808. {$set: {members: newmembers}});
  1809. }
  1810. if (req.body.hasOwnProperty('swimlaneId')) {
  1811. const newParamSwimlaneId = req.body.swimlaneId;
  1812. Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
  1813. {$set: {swimlaneId: newParamSwimlaneId}});
  1814. }
  1815. JsonRoutes.sendResult(res, {
  1816. code: 200,
  1817. data: {
  1818. _id: paramCardId,
  1819. },
  1820. });
  1821. });
  1822. /**
  1823. * @operation delete_card
  1824. * @summary Delete a card from a board
  1825. *
  1826. * @description This operation **deletes** a card, and therefore the card
  1827. * is not put in the recycle bin.
  1828. *
  1829. * @param {string} boardId the board ID of the card
  1830. * @param {string} list the list ID of the card
  1831. * @param {string} cardId the ID of the card
  1832. * @return_type {_id: string}
  1833. */
  1834. JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId/cards/:cardId', function(req, res) {
  1835. Authentication.checkUserId(req.userId);
  1836. const paramBoardId = req.params.boardId;
  1837. const paramListId = req.params.listId;
  1838. const paramCardId = req.params.cardId;
  1839. Cards.direct.remove({
  1840. _id: paramCardId,
  1841. listId: paramListId,
  1842. boardId: paramBoardId,
  1843. });
  1844. const card = Cards.find({
  1845. _id: paramCardId,
  1846. });
  1847. cardRemover(req.body.authorId, card);
  1848. JsonRoutes.sendResult(res, {
  1849. code: 200,
  1850. data: {
  1851. _id: paramCardId,
  1852. },
  1853. });
  1854. });
  1855. }
  1856. export default Cards;