cards.js 60 KB

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