cards.js 82 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319
  1. const escapeForRegex = require('escape-string-regexp');
  2. Cards = new Mongo.Collection('cards');
  3. // XXX To improve pub/sub performances a card document should include a
  4. // de-normalized number of comments so we don't have to publish the whole list
  5. // of comments just to display the number of them in the board view.
  6. Cards.attachSchema(
  7. new SimpleSchema({
  8. title: {
  9. /**
  10. * the title of the card
  11. */
  12. type: String,
  13. optional: true,
  14. defaultValue: '',
  15. },
  16. archived: {
  17. /**
  18. * is the card archived
  19. */
  20. type: Boolean,
  21. // eslint-disable-next-line consistent-return
  22. autoValue() {
  23. // eslint-disable-line consistent-return
  24. if (this.isInsert && !this.isSet) {
  25. return false;
  26. }
  27. },
  28. },
  29. parentId: {
  30. /**
  31. * ID of the parent card
  32. */
  33. type: String,
  34. optional: true,
  35. defaultValue: '',
  36. },
  37. listId: {
  38. /**
  39. * List ID where the card is
  40. */
  41. type: String,
  42. optional: true,
  43. defaultValue: '',
  44. },
  45. swimlaneId: {
  46. /**
  47. * Swimlane ID where the card is
  48. */
  49. type: String,
  50. },
  51. // The system could work without this `boardId` information (we could deduce
  52. // the board identifier from the card), but it would make the system more
  53. // difficult to manage and less efficient.
  54. boardId: {
  55. /**
  56. * Board ID of the card
  57. */
  58. type: String,
  59. optional: true,
  60. defaultValue: '',
  61. },
  62. coverId: {
  63. /**
  64. * Cover ID of the card
  65. */
  66. type: String,
  67. optional: true,
  68. defaultValue: '',
  69. },
  70. color: {
  71. type: String,
  72. optional: true,
  73. allowedValues: [
  74. 'white',
  75. 'green',
  76. 'yellow',
  77. 'orange',
  78. 'red',
  79. 'purple',
  80. 'blue',
  81. 'sky',
  82. 'lime',
  83. 'pink',
  84. 'black',
  85. 'silver',
  86. 'peachpuff',
  87. 'crimson',
  88. 'plum',
  89. 'darkgreen',
  90. 'slateblue',
  91. 'magenta',
  92. 'gold',
  93. 'navy',
  94. 'gray',
  95. 'saddlebrown',
  96. 'paleturquoise',
  97. 'mistyrose',
  98. 'indigo',
  99. ],
  100. },
  101. createdAt: {
  102. /**
  103. * creation date
  104. */
  105. type: Date,
  106. // eslint-disable-next-line consistent-return
  107. autoValue() {
  108. if (this.isInsert) {
  109. return new Date();
  110. } else if (this.isUpsert) {
  111. return { $setOnInsert: new Date() };
  112. } else {
  113. this.unset();
  114. }
  115. },
  116. },
  117. modifiedAt: {
  118. type: Date,
  119. denyUpdate: false,
  120. // eslint-disable-next-line consistent-return
  121. autoValue() {
  122. if (this.isInsert || this.isUpsert || this.isUpdate) {
  123. return new Date();
  124. } else {
  125. this.unset();
  126. }
  127. },
  128. },
  129. customFields: {
  130. /**
  131. * list of custom fields
  132. */
  133. type: [Object],
  134. optional: true,
  135. defaultValue: [],
  136. },
  137. 'customFields.$': {
  138. type: new SimpleSchema({
  139. _id: {
  140. /**
  141. * the ID of the related custom field
  142. */
  143. type: String,
  144. optional: true,
  145. defaultValue: '',
  146. },
  147. value: {
  148. /**
  149. * value attached to the custom field
  150. */
  151. type: Match.OneOf(String, Number, Boolean, Date),
  152. optional: true,
  153. defaultValue: '',
  154. },
  155. }),
  156. },
  157. dateLastActivity: {
  158. /**
  159. * Date of last activity
  160. */
  161. type: Date,
  162. autoValue() {
  163. return new Date();
  164. },
  165. },
  166. description: {
  167. /**
  168. * description of the card
  169. */
  170. type: String,
  171. optional: true,
  172. defaultValue: '',
  173. },
  174. requestedBy: {
  175. /**
  176. * who requested the card (ID of the user)
  177. */
  178. type: String,
  179. optional: true,
  180. defaultValue: '',
  181. },
  182. assignedBy: {
  183. /**
  184. * who assigned the card (ID of the user)
  185. */
  186. type: String,
  187. optional: true,
  188. defaultValue: '',
  189. },
  190. labelIds: {
  191. /**
  192. * list of labels ID the card has
  193. */
  194. type: [String],
  195. optional: true,
  196. defaultValue: [],
  197. },
  198. members: {
  199. /**
  200. * list of members (user IDs)
  201. */
  202. type: [String],
  203. optional: true,
  204. defaultValue: [],
  205. },
  206. assignees: {
  207. /**
  208. * who is assignee of the card (user ID),
  209. * maximum one ID of assignee in array.
  210. */
  211. type: [String],
  212. optional: true,
  213. defaultValue: [],
  214. },
  215. receivedAt: {
  216. /**
  217. * Date the card was received
  218. */
  219. type: Date,
  220. optional: true,
  221. },
  222. startAt: {
  223. /**
  224. * Date the card was started to be worked on
  225. */
  226. type: Date,
  227. optional: true,
  228. },
  229. dueAt: {
  230. /**
  231. * Date the card is due
  232. */
  233. type: Date,
  234. optional: true,
  235. },
  236. endAt: {
  237. /**
  238. * Date the card ended
  239. */
  240. type: Date,
  241. optional: true,
  242. },
  243. spentTime: {
  244. /**
  245. * How much time has been spent on this
  246. */
  247. type: Number,
  248. decimal: true,
  249. optional: true,
  250. defaultValue: 0,
  251. },
  252. isOvertime: {
  253. /**
  254. * is the card over time?
  255. */
  256. type: Boolean,
  257. defaultValue: false,
  258. optional: true,
  259. },
  260. // XXX Should probably be called `authorId`. Is it even needed since we have
  261. // the `members` field?
  262. userId: {
  263. /**
  264. * user ID of the author of the card
  265. */
  266. type: String,
  267. // eslint-disable-next-line consistent-return
  268. autoValue() {
  269. // eslint-disable-line consistent-return
  270. if (this.isInsert && !this.isSet) {
  271. return this.userId;
  272. }
  273. },
  274. },
  275. sort: {
  276. /**
  277. * Sort value
  278. */
  279. type: Number,
  280. decimal: true,
  281. defaultValue: 0,
  282. },
  283. subtaskSort: {
  284. /**
  285. * subtask sort value
  286. */
  287. type: Number,
  288. decimal: true,
  289. defaultValue: -1,
  290. optional: true,
  291. },
  292. type: {
  293. /**
  294. * type of the card
  295. */
  296. type: String,
  297. defaultValue: 'cardType-card',
  298. },
  299. linkedId: {
  300. /**
  301. * ID of the linked card
  302. */
  303. type: String,
  304. optional: true,
  305. defaultValue: '',
  306. },
  307. vote: {
  308. /**
  309. * vote object, see below
  310. */
  311. type: Object,
  312. optional: true,
  313. },
  314. 'vote.question': {
  315. type: String,
  316. defaultValue: '',
  317. },
  318. 'vote.positive': {
  319. /**
  320. * list of members (user IDs)
  321. */
  322. type: [String],
  323. optional: true,
  324. defaultValue: [],
  325. },
  326. 'vote.negative': {
  327. /**
  328. * list of members (user IDs)
  329. */
  330. type: [String],
  331. optional: true,
  332. defaultValue: [],
  333. },
  334. 'vote.end': {
  335. type: Date,
  336. optional: true,
  337. defaultValue: null,
  338. },
  339. 'vote.public': {
  340. type: Boolean,
  341. defaultValue: false,
  342. },
  343. 'vote.allowNonBoardMembers': {
  344. type: Boolean,
  345. defaultValue: false,
  346. },
  347. }),
  348. );
  349. Cards.allow({
  350. insert(userId, doc) {
  351. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  352. },
  353. update(userId, doc, fields) {
  354. // Allow board members or logged in users if only vote get's changed
  355. return (
  356. allowIsBoardMember(userId, Boards.findOne(doc.boardId)) ||
  357. (_.isEqual(fields, ['vote', 'modifiedAt', 'dateLastActivity']) &&
  358. !!userId)
  359. );
  360. },
  361. remove(userId, doc) {
  362. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  363. },
  364. fetch: ['boardId'],
  365. });
  366. Cards.helpers({
  367. mapCustomFieldsToBoard(boardId) {
  368. // Map custom fields to new board
  369. return this.customFields.map(cf => {
  370. const oldCf = CustomFields.findOne(cf._id);
  371. const newCf = CustomFields.findOne({
  372. boardIds: boardId,
  373. name: oldCf.name,
  374. type: oldCf.type,
  375. });
  376. if (newCf) {
  377. cf._id = newCf._id;
  378. } else if (!_.contains(oldCf.boardIds, boardId)) {
  379. oldCf.addBoard(boardId);
  380. }
  381. return cf;
  382. });
  383. },
  384. copy(boardId, swimlaneId, listId) {
  385. const oldId = this._id;
  386. const oldCard = Cards.findOne(oldId);
  387. // we must only copy the labels and custom fields if the target board
  388. // differs from the source board
  389. if (this.boardId !== boardId) {
  390. const oldBoard = Boards.findOne(this.boardId);
  391. const oldBoardLabels = oldBoard.labels;
  392. // Get old label names
  393. const oldCardLabels = _.pluck(
  394. _.filter(oldBoardLabels, label => {
  395. return _.contains(this.labelIds, label._id);
  396. }),
  397. 'name',
  398. );
  399. const newBoard = Boards.findOne(boardId);
  400. const newBoardLabels = newBoard.labels;
  401. const newCardLabels = _.pluck(
  402. _.filter(newBoardLabels, label => {
  403. return _.contains(oldCardLabels, label.name);
  404. }),
  405. '_id',
  406. );
  407. // now set the new label ids
  408. delete this.labelIds;
  409. this.labelIds = newCardLabels;
  410. this.customFields = this.mapCustomFieldsToBoard(newBoard._id);
  411. }
  412. delete this._id;
  413. this.boardId = boardId;
  414. this.swimlaneId = swimlaneId;
  415. this.listId = listId;
  416. const _id = Cards.insert(this);
  417. // Copy attachments
  418. oldCard.attachments().forEach(att => {
  419. att.cardId = _id;
  420. delete att._id;
  421. return Attachments.insert(att);
  422. });
  423. // copy checklists
  424. Checklists.find({ cardId: oldId }).forEach(ch => {
  425. ch.copy(_id);
  426. });
  427. // copy subtasks
  428. Cards.find({ parentId: oldId }).forEach(subtask => {
  429. subtask.parentId = _id;
  430. subtask._id = null;
  431. Cards.insert(subtask);
  432. });
  433. // copy card comments
  434. CardComments.find({ cardId: oldId }).forEach(cmt => {
  435. cmt.copy(_id);
  436. });
  437. return _id;
  438. },
  439. link(boardId, swimlaneId, listId) {
  440. // TODO is there a better method to create a deepcopy?
  441. linkCard = JSON.parse(JSON.stringify(this));
  442. // TODO is this how it is meant to be?
  443. linkCard.linkedId = linkCard.linkedId || linkCard._id;
  444. linkCard.boardId = boardId;
  445. linkCard.swimlaneId = swimlaneId;
  446. linkCard.listId = listId;
  447. linkCard.type = 'cardType-linkedCard';
  448. delete linkCard._id;
  449. // TODO shall we copy the labels for a linked card?!
  450. delete linkCard.labelIds;
  451. return Cards.insert(linkCard);
  452. },
  453. list() {
  454. return Lists.findOne(this.listId);
  455. },
  456. swimlane() {
  457. return Swimlanes.findOne(this.swimlaneId);
  458. },
  459. board() {
  460. return Boards.findOne(this.boardId);
  461. },
  462. getList() {
  463. const list = this.list();
  464. if (!list) {
  465. return {
  466. _id: this.listId,
  467. title: 'Undefined List',
  468. archived: false,
  469. colorClass: '',
  470. };
  471. }
  472. return list;
  473. },
  474. getSwimlane() {
  475. const swimlane = this.swimlane();
  476. if (!swimlane) {
  477. return {
  478. _id: this.swimlaneId,
  479. title: 'Undefined Swimlane',
  480. archived: false,
  481. colorClass: '',
  482. };
  483. }
  484. return swimlane;
  485. },
  486. getBoard() {
  487. const board = this.board();
  488. if (!board) {
  489. return {
  490. _id: this.boardId,
  491. title: 'Undefined Board',
  492. archived: false,
  493. colorClass: '',
  494. };
  495. }
  496. return board;
  497. },
  498. labels() {
  499. const boardLabels = this.board().labels;
  500. const cardLabels = _.filter(boardLabels, label => {
  501. return _.contains(this.labelIds, label._id);
  502. });
  503. return cardLabels;
  504. },
  505. hasLabel(labelId) {
  506. return _.contains(this.labelIds, labelId);
  507. },
  508. user() {
  509. return Users.findOne(this.userId);
  510. },
  511. isAssigned(memberId) {
  512. return _.contains(this.getMembers(), memberId);
  513. },
  514. isAssignee(assigneeId) {
  515. return _.contains(this.getAssignees(), assigneeId);
  516. },
  517. activities() {
  518. if (this.isLinkedCard()) {
  519. return Activities.find(
  520. { cardId: this.linkedId },
  521. { sort: { createdAt: -1 } },
  522. );
  523. } else if (this.isLinkedBoard()) {
  524. return Activities.find(
  525. { boardId: this.linkedId },
  526. { sort: { createdAt: -1 } },
  527. );
  528. } else {
  529. return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 } });
  530. }
  531. },
  532. comments() {
  533. if (this.isLinkedCard()) {
  534. return CardComments.find(
  535. { cardId: this.linkedId },
  536. { sort: { createdAt: -1 } },
  537. );
  538. } else {
  539. return CardComments.find(
  540. { cardId: this._id },
  541. { sort: { createdAt: -1 } },
  542. );
  543. }
  544. },
  545. attachments() {
  546. if (this.isLinkedCard()) {
  547. return Attachments.find(
  548. { cardId: this.linkedId },
  549. { sort: { uploadedAt: -1 } },
  550. );
  551. } else {
  552. return Attachments.find(
  553. { cardId: this._id },
  554. { sort: { uploadedAt: -1 } },
  555. );
  556. }
  557. },
  558. cover() {
  559. if (!this.coverId) return false;
  560. const cover = Attachments.findOne(this.coverId);
  561. // if we return a cover before it is fully stored, we will get errors when we try to display it
  562. // todo XXX we could return a default "upload pending" image in the meantime?
  563. return cover && cover.url() && cover;
  564. },
  565. checklists() {
  566. if (this.isLinkedCard()) {
  567. return Checklists.find({ cardId: this.linkedId }, { sort: { sort: 1 } });
  568. } else {
  569. return Checklists.find({ cardId: this._id }, { sort: { sort: 1 } });
  570. }
  571. },
  572. checklistItemCount() {
  573. const checklists = this.checklists().fetch();
  574. return checklists
  575. .map(checklist => {
  576. return checklist.itemCount();
  577. })
  578. .reduce((prev, next) => {
  579. return prev + next;
  580. }, 0);
  581. },
  582. checklistFinishedCount() {
  583. const checklists = this.checklists().fetch();
  584. return checklists
  585. .map(checklist => {
  586. return checklist.finishedCount();
  587. })
  588. .reduce((prev, next) => {
  589. return prev + next;
  590. }, 0);
  591. },
  592. checklistFinished() {
  593. return (
  594. this.hasChecklist() &&
  595. this.checklistItemCount() === this.checklistFinishedCount()
  596. );
  597. },
  598. hasChecklist() {
  599. return this.checklistItemCount() !== 0;
  600. },
  601. subtasks() {
  602. return Cards.find(
  603. {
  604. parentId: this._id,
  605. archived: false,
  606. },
  607. {
  608. sort: {
  609. sort: 1,
  610. },
  611. },
  612. );
  613. },
  614. allSubtasks() {
  615. return Cards.find(
  616. {
  617. parentId: this._id,
  618. archived: false,
  619. },
  620. {
  621. sort: {
  622. sort: 1,
  623. },
  624. },
  625. );
  626. },
  627. subtasksCount() {
  628. return Cards.find({
  629. parentId: this._id,
  630. archived: false,
  631. }).count();
  632. },
  633. subtasksFinishedCount() {
  634. return Cards.find({
  635. parentId: this._id,
  636. archived: true,
  637. }).count();
  638. },
  639. subtasksFinished() {
  640. const finishCount = this.subtasksFinishedCount();
  641. return finishCount > 0 && this.subtasksCount() === finishCount;
  642. },
  643. allowsSubtasks() {
  644. return this.subtasksCount() !== 0;
  645. },
  646. customFieldIndex(customFieldId) {
  647. return _.pluck(this.customFields, '_id').indexOf(customFieldId);
  648. },
  649. // customFields with definitions
  650. customFieldsWD() {
  651. // get all definitions
  652. const definitions = CustomFields.find({
  653. boardIds: { $in: [this.boardId] },
  654. }).fetch();
  655. // match right definition to each field
  656. if (!this.customFields) return [];
  657. const ret = this.customFields.map(customField => {
  658. const definition = definitions.find(definition => {
  659. return definition._id === customField._id;
  660. });
  661. if (!definition) {
  662. return {};
  663. }
  664. //search for "True Value" which is for DropDowns other then the Value (which is the id)
  665. let trueValue = customField.value;
  666. if (
  667. definition.settings.dropdownItems &&
  668. definition.settings.dropdownItems.length > 0
  669. ) {
  670. for (let i = 0; i < definition.settings.dropdownItems.length; i++) {
  671. if (definition.settings.dropdownItems[i]._id === customField.value) {
  672. trueValue = definition.settings.dropdownItems[i].name;
  673. }
  674. }
  675. }
  676. return {
  677. _id: customField._id,
  678. value: customField.value,
  679. trueValue,
  680. definition,
  681. };
  682. });
  683. if (ret.definition !== undefined) {
  684. ret.sort((a, b) => a.definition.name.localeCompare(b.definition.name));
  685. }
  686. return ret;
  687. },
  688. colorClass() {
  689. if (this.color) return this.color;
  690. return '';
  691. },
  692. absoluteUrl() {
  693. const board = this.board();
  694. return FlowRouter.url('card', {
  695. boardId: board._id,
  696. slug: board.slug,
  697. cardId: this._id,
  698. });
  699. },
  700. canBeRestored() {
  701. const list = Lists.findOne({
  702. _id: this.listId,
  703. });
  704. if (
  705. !list.getWipLimit('soft') &&
  706. list.getWipLimit('enabled') &&
  707. list.getWipLimit('value') === list.cards().count()
  708. ) {
  709. return false;
  710. }
  711. return true;
  712. },
  713. parentCard() {
  714. if (this.parentId === '') {
  715. return null;
  716. }
  717. return Cards.findOne(this.parentId);
  718. },
  719. parentCardName() {
  720. let result = '';
  721. if (this.parentId !== '') {
  722. const card = Cards.findOne(this.parentId);
  723. if (card) {
  724. result = card.title;
  725. }
  726. }
  727. return result;
  728. },
  729. parentListId() {
  730. const result = [];
  731. let crtParentId = this.parentId;
  732. while (crtParentId !== '') {
  733. const crt = Cards.findOne(crtParentId);
  734. if (crt === null || crt === undefined) {
  735. // maybe it has been deleted
  736. break;
  737. }
  738. if (crtParentId in result) {
  739. // circular reference
  740. break;
  741. }
  742. result.unshift(crtParentId);
  743. crtParentId = crt.parentId;
  744. }
  745. return result;
  746. },
  747. parentList() {
  748. const resultId = [];
  749. const result = [];
  750. let crtParentId = this.parentId;
  751. while (crtParentId !== '') {
  752. const crt = Cards.findOne(crtParentId);
  753. if (crt === null || crt === undefined) {
  754. // maybe it has been deleted
  755. break;
  756. }
  757. if (crtParentId in resultId) {
  758. // circular reference
  759. break;
  760. }
  761. resultId.unshift(crtParentId);
  762. result.unshift(crt);
  763. crtParentId = crt.parentId;
  764. }
  765. return result;
  766. },
  767. parentString(sep) {
  768. return this.parentList()
  769. .map(function(elem) {
  770. return elem.title;
  771. })
  772. .join(sep);
  773. },
  774. isTopLevel() {
  775. return this.parentId === '';
  776. },
  777. isLinkedCard() {
  778. return this.type === 'cardType-linkedCard';
  779. },
  780. isLinkedBoard() {
  781. return this.type === 'cardType-linkedBoard';
  782. },
  783. isLinked() {
  784. return this.isLinkedCard() || this.isLinkedBoard();
  785. },
  786. setDescription(description) {
  787. if (this.isLinkedCard()) {
  788. return Cards.update({ _id: this.linkedId }, { $set: { description } });
  789. } else if (this.isLinkedBoard()) {
  790. return Boards.update({ _id: this.linkedId }, { $set: { description } });
  791. } else {
  792. return Cards.update({ _id: this._id }, { $set: { description } });
  793. }
  794. },
  795. getDescription() {
  796. if (this.isLinkedCard()) {
  797. const card = Cards.findOne({ _id: this.linkedId });
  798. if (card && card.description) return card.description;
  799. else return null;
  800. } else if (this.isLinkedBoard()) {
  801. const board = Boards.findOne({ _id: this.linkedId });
  802. if (board && board.description) return board.description;
  803. else return null;
  804. } else if (this.description) {
  805. return this.description;
  806. } else {
  807. return null;
  808. }
  809. },
  810. getMembers() {
  811. if (this.isLinkedCard()) {
  812. const card = Cards.findOne({ _id: this.linkedId });
  813. if (card === undefined) {
  814. return null;
  815. } else {
  816. return card.members;
  817. }
  818. } else if (this.isLinkedBoard()) {
  819. const board = Boards.findOne({ _id: this.linkedId });
  820. if (board === undefined) {
  821. return null;
  822. } else {
  823. return board.activeMembers().map(member => {
  824. return member.userId;
  825. });
  826. }
  827. } else {
  828. return this.members;
  829. }
  830. },
  831. getAssignees() {
  832. if (this.isLinkedCard()) {
  833. const card = Cards.findOne({ _id: this.linkedId });
  834. if (card === undefined) {
  835. return null;
  836. } else {
  837. return card.assignees;
  838. }
  839. } else if (this.isLinkedBoard()) {
  840. const board = Boards.findOne({ _id: this.linkedId });
  841. if (board === undefined) {
  842. return null;
  843. } else {
  844. return board.activeMembers().map(assignee => {
  845. return assignee.userId;
  846. });
  847. }
  848. } else {
  849. return this.assignees;
  850. }
  851. },
  852. assignMember(memberId) {
  853. if (this.isLinkedCard()) {
  854. return Cards.update(
  855. { _id: this.linkedId },
  856. { $addToSet: { members: memberId } },
  857. );
  858. } else if (this.isLinkedBoard()) {
  859. const board = Boards.findOne({ _id: this.linkedId });
  860. return board.addMember(memberId);
  861. } else {
  862. return Cards.update(
  863. { _id: this._id },
  864. { $addToSet: { members: memberId } },
  865. );
  866. }
  867. },
  868. assignAssignee(assigneeId) {
  869. if (this.isLinkedCard()) {
  870. return Cards.update(
  871. { _id: this.linkedId },
  872. { $addToSet: { assignees: assigneeId } },
  873. );
  874. } else if (this.isLinkedBoard()) {
  875. const board = Boards.findOne({ _id: this.linkedId });
  876. return board.addAssignee(assigneeId);
  877. } else {
  878. return Cards.update(
  879. { _id: this._id },
  880. { $addToSet: { assignees: assigneeId } },
  881. );
  882. }
  883. },
  884. unassignMember(memberId) {
  885. if (this.isLinkedCard()) {
  886. return Cards.update(
  887. { _id: this.linkedId },
  888. { $pull: { members: memberId } },
  889. );
  890. } else if (this.isLinkedBoard()) {
  891. const board = Boards.findOne({ _id: this.linkedId });
  892. return board.removeMember(memberId);
  893. } else {
  894. return Cards.update({ _id: this._id }, { $pull: { members: memberId } });
  895. }
  896. },
  897. unassignAssignee(assigneeId) {
  898. if (this.isLinkedCard()) {
  899. return Cards.update(
  900. { _id: this.linkedId },
  901. { $pull: { assignees: assigneeId } },
  902. );
  903. } else if (this.isLinkedBoard()) {
  904. const board = Boards.findOne({ _id: this.linkedId });
  905. return board.removeAssignee(assigneeId);
  906. } else {
  907. return Cards.update(
  908. { _id: this._id },
  909. { $pull: { assignees: assigneeId } },
  910. );
  911. }
  912. },
  913. toggleMember(memberId) {
  914. if (this.getMembers() && this.getMembers().indexOf(memberId) > -1) {
  915. return this.unassignMember(memberId);
  916. } else {
  917. return this.assignMember(memberId);
  918. }
  919. },
  920. toggleAssignee(assigneeId) {
  921. if (this.getAssignees() && this.getAssignees().indexOf(assigneeId) > -1) {
  922. return this.unassignAssignee(assigneeId);
  923. } else {
  924. return this.assignAssignee(assigneeId);
  925. }
  926. },
  927. getReceived() {
  928. if (this.isLinkedCard()) {
  929. const card = Cards.findOne({ _id: this.linkedId });
  930. if (card === undefined) {
  931. return null;
  932. } else {
  933. return card.receivedAt;
  934. }
  935. } else {
  936. return this.receivedAt;
  937. }
  938. },
  939. setReceived(receivedAt) {
  940. if (this.isLinkedCard()) {
  941. return Cards.update({ _id: this.linkedId }, { $set: { receivedAt } });
  942. } else {
  943. return Cards.update({ _id: this._id }, { $set: { receivedAt } });
  944. }
  945. },
  946. getStart() {
  947. if (this.isLinkedCard()) {
  948. const card = Cards.findOne({ _id: this.linkedId });
  949. if (card === undefined) {
  950. return null;
  951. } else {
  952. return card.startAt;
  953. }
  954. } else if (this.isLinkedBoard()) {
  955. const board = Boards.findOne({ _id: this.linkedId });
  956. if (board === undefined) {
  957. return null;
  958. } else {
  959. return board.startAt;
  960. }
  961. } else {
  962. return this.startAt;
  963. }
  964. },
  965. setStart(startAt) {
  966. if (this.isLinkedCard()) {
  967. return Cards.update({ _id: this.linkedId }, { $set: { startAt } });
  968. } else if (this.isLinkedBoard()) {
  969. return Boards.update({ _id: this.linkedId }, { $set: { startAt } });
  970. } else {
  971. return Cards.update({ _id: this._id }, { $set: { startAt } });
  972. }
  973. },
  974. getDue() {
  975. if (this.isLinkedCard()) {
  976. const card = Cards.findOne({ _id: this.linkedId });
  977. if (card === undefined) {
  978. return null;
  979. } else {
  980. return card.dueAt;
  981. }
  982. } else if (this.isLinkedBoard()) {
  983. const board = Boards.findOne({ _id: this.linkedId });
  984. if (board === undefined) {
  985. return null;
  986. } else {
  987. return board.dueAt;
  988. }
  989. } else {
  990. return this.dueAt;
  991. }
  992. },
  993. setDue(dueAt) {
  994. if (this.isLinkedCard()) {
  995. return Cards.update({ _id: this.linkedId }, { $set: { dueAt } });
  996. } else if (this.isLinkedBoard()) {
  997. return Boards.update({ _id: this.linkedId }, { $set: { dueAt } });
  998. } else {
  999. return Cards.update({ _id: this._id }, { $set: { dueAt } });
  1000. }
  1001. },
  1002. getEnd() {
  1003. if (this.isLinkedCard()) {
  1004. const card = Cards.findOne({ _id: this.linkedId });
  1005. if (card === undefined) {
  1006. return null;
  1007. } else {
  1008. return card.endAt;
  1009. }
  1010. } else if (this.isLinkedBoard()) {
  1011. const board = Boards.findOne({ _id: this.linkedId });
  1012. if (board === undefined) {
  1013. return null;
  1014. } else {
  1015. return board.endAt;
  1016. }
  1017. } else {
  1018. return this.endAt;
  1019. }
  1020. },
  1021. setEnd(endAt) {
  1022. if (this.isLinkedCard()) {
  1023. return Cards.update({ _id: this.linkedId }, { $set: { endAt } });
  1024. } else if (this.isLinkedBoard()) {
  1025. return Boards.update({ _id: this.linkedId }, { $set: { endAt } });
  1026. } else {
  1027. return Cards.update({ _id: this._id }, { $set: { endAt } });
  1028. }
  1029. },
  1030. getIsOvertime() {
  1031. if (this.isLinkedCard()) {
  1032. const card = Cards.findOne({ _id: this.linkedId });
  1033. if (card === undefined) {
  1034. return null;
  1035. } else {
  1036. return card.isOvertime;
  1037. }
  1038. } else if (this.isLinkedBoard()) {
  1039. const board = Boards.findOne({ _id: this.linkedId });
  1040. if (board === undefined) {
  1041. return null;
  1042. } else {
  1043. return board.isOvertime;
  1044. }
  1045. } else {
  1046. return this.isOvertime;
  1047. }
  1048. },
  1049. setIsOvertime(isOvertime) {
  1050. if (this.isLinkedCard()) {
  1051. return Cards.update({ _id: this.linkedId }, { $set: { isOvertime } });
  1052. } else if (this.isLinkedBoard()) {
  1053. return Boards.update({ _id: this.linkedId }, { $set: { isOvertime } });
  1054. } else {
  1055. return Cards.update({ _id: this._id }, { $set: { isOvertime } });
  1056. }
  1057. },
  1058. getSpentTime() {
  1059. if (this.isLinkedCard()) {
  1060. const card = Cards.findOne({ _id: this.linkedId });
  1061. if (card === undefined) {
  1062. return null;
  1063. } else {
  1064. return card.spentTime;
  1065. }
  1066. } else if (this.isLinkedBoard()) {
  1067. const board = Boards.findOne({ _id: this.linkedId });
  1068. if (board === undefined) {
  1069. return null;
  1070. } else {
  1071. return board.spentTime;
  1072. }
  1073. } else {
  1074. return this.spentTime;
  1075. }
  1076. },
  1077. setSpentTime(spentTime) {
  1078. if (this.isLinkedCard()) {
  1079. return Cards.update({ _id: this.linkedId }, { $set: { spentTime } });
  1080. } else if (this.isLinkedBoard()) {
  1081. return Boards.update({ _id: this.linkedId }, { $set: { spentTime } });
  1082. } else {
  1083. return Cards.update({ _id: this._id }, { $set: { spentTime } });
  1084. }
  1085. },
  1086. getVoteQuestion() {
  1087. if (this.isLinkedCard()) {
  1088. const card = Cards.findOne({ _id: this.linkedId });
  1089. if (card === undefined) {
  1090. return null;
  1091. } else if (card && card.vote) {
  1092. return card.vote.question;
  1093. } else {
  1094. return null;
  1095. }
  1096. } else if (this.isLinkedBoard()) {
  1097. const board = Boards.findOne({ _id: this.linkedId });
  1098. if (board === undefined) {
  1099. return null;
  1100. } else if (board && board.vote) {
  1101. return board.vote.question;
  1102. } else {
  1103. return null;
  1104. }
  1105. } else if (this.vote) {
  1106. return this.vote.question;
  1107. } else {
  1108. return null;
  1109. }
  1110. },
  1111. getVotePublic() {
  1112. if (this.isLinkedCard()) {
  1113. const card = Cards.findOne({ _id: this.linkedId });
  1114. if (card === undefined) {
  1115. return null;
  1116. } else if (card && card.vote) {
  1117. return card.vote.public;
  1118. } else {
  1119. return null;
  1120. }
  1121. } else if (this.isLinkedBoard()) {
  1122. const board = Boards.findOne({ _id: this.linkedId });
  1123. if (board === undefined) {
  1124. return null;
  1125. } else if (board && board.vote) {
  1126. return board.vote.public;
  1127. } else {
  1128. return null;
  1129. }
  1130. } else if (this.vote) {
  1131. return this.vote.public;
  1132. } else {
  1133. return null;
  1134. }
  1135. },
  1136. getVoteEnd() {
  1137. if (this.isLinkedCard()) {
  1138. const card = Cards.findOne({ _id: this.linkedId });
  1139. if (card === undefined) {
  1140. return null;
  1141. } else if (card && card.vote) {
  1142. return card.vote.end;
  1143. } else {
  1144. return null;
  1145. }
  1146. } else if (this.isLinkedBoard()) {
  1147. const board = Boards.findOne({ _id: this.linkedId });
  1148. if (board === undefined) {
  1149. return null;
  1150. } else if (board && board.vote) {
  1151. return board.vote.end;
  1152. } else {
  1153. return null;
  1154. }
  1155. } else if (this.vote) {
  1156. return this.vote.end;
  1157. } else {
  1158. return null;
  1159. }
  1160. },
  1161. expiredVote() {
  1162. let end = this.getVoteEnd();
  1163. if (end) {
  1164. end = moment(end);
  1165. return end.isBefore(new Date());
  1166. }
  1167. return false;
  1168. },
  1169. voteMemberPositive() {
  1170. if (this.vote && this.vote.positive)
  1171. return Users.find({ _id: { $in: this.vote.positive } });
  1172. return [];
  1173. },
  1174. voteMemberNegative() {
  1175. if (this.vote && this.vote.negative)
  1176. return Users.find({ _id: { $in: this.vote.negative } });
  1177. return [];
  1178. },
  1179. voteState() {
  1180. const userId = Meteor.userId();
  1181. let state;
  1182. if (this.vote) {
  1183. if (this.vote.positive) {
  1184. state = _.contains(this.vote.positive, userId);
  1185. if (state === true) return true;
  1186. }
  1187. if (this.vote.negative) {
  1188. state = _.contains(this.vote.negative, userId);
  1189. if (state === true) return false;
  1190. }
  1191. }
  1192. return null;
  1193. },
  1194. getId() {
  1195. if (this.isLinked()) {
  1196. return this.linkedId;
  1197. } else {
  1198. return this._id;
  1199. }
  1200. },
  1201. getTitle() {
  1202. if (this.isLinkedCard()) {
  1203. const card = Cards.findOne({ _id: this.linkedId });
  1204. if (card === undefined) {
  1205. return null;
  1206. } else {
  1207. return card.title;
  1208. }
  1209. } else if (this.isLinkedBoard()) {
  1210. const board = Boards.findOne({ _id: this.linkedId });
  1211. if (board === undefined) {
  1212. return null;
  1213. } else {
  1214. return board.title;
  1215. }
  1216. } else if (this.title === undefined) {
  1217. return null;
  1218. } else {
  1219. return this.title;
  1220. }
  1221. },
  1222. getBoardTitle() {
  1223. if (this.isLinkedCard()) {
  1224. const card = Cards.findOne({ _id: this.linkedId });
  1225. if (card === undefined) {
  1226. return null;
  1227. }
  1228. const board = Boards.findOne({ _id: card.boardId });
  1229. if (board === undefined) {
  1230. return null;
  1231. } else {
  1232. return board.title;
  1233. }
  1234. } else if (this.isLinkedBoard()) {
  1235. const board = Boards.findOne({ _id: this.linkedId });
  1236. if (board === undefined) {
  1237. return null;
  1238. } else {
  1239. return board.title;
  1240. }
  1241. } else {
  1242. const board = Boards.findOne({ _id: this.boardId });
  1243. if (board === undefined) {
  1244. return null;
  1245. } else {
  1246. return board.title;
  1247. }
  1248. }
  1249. },
  1250. setTitle(title) {
  1251. if (this.isLinkedCard()) {
  1252. return Cards.update({ _id: this.linkedId }, { $set: { title } });
  1253. } else if (this.isLinkedBoard()) {
  1254. return Boards.update({ _id: this.linkedId }, { $set: { title } });
  1255. } else {
  1256. return Cards.update({ _id: this._id }, { $set: { title } });
  1257. }
  1258. },
  1259. getArchived() {
  1260. if (this.isLinkedCard()) {
  1261. const card = Cards.findOne({ _id: this.linkedId });
  1262. if (card === undefined) {
  1263. return null;
  1264. } else {
  1265. return card.archived;
  1266. }
  1267. } else if (this.isLinkedBoard()) {
  1268. const board = Boards.findOne({ _id: this.linkedId });
  1269. if (board === undefined) {
  1270. return null;
  1271. } else {
  1272. return board.archived;
  1273. }
  1274. } else {
  1275. return this.archived;
  1276. }
  1277. },
  1278. setRequestedBy(requestedBy) {
  1279. if (this.isLinkedCard()) {
  1280. return Cards.update({ _id: this.linkedId }, { $set: { requestedBy } });
  1281. } else {
  1282. return Cards.update({ _id: this._id }, { $set: { requestedBy } });
  1283. }
  1284. },
  1285. getRequestedBy() {
  1286. if (this.isLinkedCard()) {
  1287. const card = Cards.findOne({ _id: this.linkedId });
  1288. if (card === undefined) {
  1289. return null;
  1290. } else {
  1291. return card.requestedBy;
  1292. }
  1293. } else {
  1294. return this.requestedBy;
  1295. }
  1296. },
  1297. setAssignedBy(assignedBy) {
  1298. if (this.isLinkedCard()) {
  1299. return Cards.update({ _id: this.linkedId }, { $set: { assignedBy } });
  1300. } else {
  1301. return Cards.update({ _id: this._id }, { $set: { assignedBy } });
  1302. }
  1303. },
  1304. getAssignedBy() {
  1305. if (this.isLinkedCard()) {
  1306. const card = Cards.findOne({ _id: this.linkedId });
  1307. if (card === undefined) {
  1308. return null;
  1309. } else {
  1310. return card.assignedBy;
  1311. }
  1312. } else {
  1313. return this.assignedBy;
  1314. }
  1315. },
  1316. isTemplateCard() {
  1317. return this.type === 'template-card';
  1318. },
  1319. votePublic() {
  1320. if (this.vote) return this.vote.public;
  1321. return null;
  1322. },
  1323. voteAllowNonBoardMembers() {
  1324. if (this.vote) return this.vote.allowNonBoardMembers;
  1325. return null;
  1326. },
  1327. voteCountNegative() {
  1328. if (this.vote && this.vote.negative) return this.vote.negative.length;
  1329. return null;
  1330. },
  1331. voteCountPositive() {
  1332. if (this.vote && this.vote.positive) return this.vote.positive.length;
  1333. return null;
  1334. },
  1335. voteCount() {
  1336. return this.voteCountPositive() + this.voteCountNegative();
  1337. },
  1338. });
  1339. Cards.mutations({
  1340. applyToChildren(funct) {
  1341. Cards.find({
  1342. parentId: this._id,
  1343. }).forEach(card => {
  1344. funct(card);
  1345. });
  1346. },
  1347. archive() {
  1348. this.applyToChildren(card => {
  1349. return card.archive();
  1350. });
  1351. return {
  1352. $set: {
  1353. archived: true,
  1354. },
  1355. };
  1356. },
  1357. restore() {
  1358. this.applyToChildren(card => {
  1359. return card.restore();
  1360. });
  1361. return {
  1362. $set: {
  1363. archived: false,
  1364. },
  1365. };
  1366. },
  1367. moveToEndOfList({ listId } = {}) {
  1368. let swimlaneId = this.swimlaneId;
  1369. const boardId = this.boardId;
  1370. let sortIndex = 0;
  1371. // This should never happen, but there was a bug that was fixed in commit
  1372. // ea0239538a68e225c867411a4f3e0d27c158383.
  1373. if (!swimlaneId) {
  1374. const board = Boards.findOne(boardId);
  1375. swimlaneId = board.getDefaultSwimline()._id;
  1376. }
  1377. // Move the minicard to the end of the target list
  1378. let parentElementDom = $(`#swimlane-${this.swimlaneId}`).get(0);
  1379. if (!parentElementDom) parentElementDom = $(':root');
  1380. const lastCardDom = $(parentElementDom)
  1381. .find(`#js-list-${listId} .js-minicard:last`)
  1382. .get(0);
  1383. if (lastCardDom) sortIndex = Utils.calculateIndex(lastCardDom, null).base;
  1384. return this.moveOptionalArgs({
  1385. boardId,
  1386. swimlaneId,
  1387. listId,
  1388. sort: sortIndex,
  1389. });
  1390. },
  1391. moveOptionalArgs({ boardId, swimlaneId, listId, sort } = {}) {
  1392. boardId = boardId || this.boardId;
  1393. swimlaneId = swimlaneId || this.swimlaneId;
  1394. // This should never happen, but there was a bug that was fixed in commit
  1395. // ea0239538a68e225c867411a4f3e0d27c158383.
  1396. if (!swimlaneId) {
  1397. const board = Boards.findOne(boardId);
  1398. swimlaneId = board.getDefaultSwimline()._id;
  1399. }
  1400. listId = listId || this.listId;
  1401. if (sort === undefined || sort === null) sort = this.sort;
  1402. return this.move(boardId, swimlaneId, listId, sort);
  1403. },
  1404. move(boardId, swimlaneId, listId, sort) {
  1405. const mutatedFields = {
  1406. boardId,
  1407. swimlaneId,
  1408. listId,
  1409. sort,
  1410. };
  1411. // we must only copy the labels and custom fields if the target board
  1412. // differs from the source board
  1413. if (this.boardId !== boardId) {
  1414. // Get label names
  1415. const oldBoard = Boards.findOne(this.boardId);
  1416. const oldBoardLabels = oldBoard.labels;
  1417. const oldCardLabels = _.pluck(
  1418. _.filter(oldBoardLabels, label => {
  1419. return _.contains(this.labelIds, label._id);
  1420. }),
  1421. 'name',
  1422. );
  1423. const newBoard = Boards.findOne(boardId);
  1424. const newBoardLabels = newBoard.labels;
  1425. const newCardLabelIds = _.pluck(
  1426. _.filter(newBoardLabels, label => {
  1427. return label.name && _.contains(oldCardLabels, label.name);
  1428. }),
  1429. '_id',
  1430. );
  1431. Object.assign(mutatedFields, {
  1432. labelIds: newCardLabelIds,
  1433. });
  1434. mutatedFields.customFields = this.mapCustomFieldsToBoard(newBoard._id);
  1435. }
  1436. Cards.update(this._id, {
  1437. $set: mutatedFields,
  1438. });
  1439. },
  1440. addLabel(labelId) {
  1441. return {
  1442. $addToSet: {
  1443. labelIds: labelId,
  1444. },
  1445. };
  1446. },
  1447. removeLabel(labelId) {
  1448. return {
  1449. $pull: {
  1450. labelIds: labelId,
  1451. },
  1452. };
  1453. },
  1454. toggleLabel(labelId) {
  1455. if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
  1456. return this.removeLabel(labelId);
  1457. } else {
  1458. return this.addLabel(labelId);
  1459. }
  1460. },
  1461. setColor(newColor) {
  1462. if (newColor === 'white') {
  1463. newColor = null;
  1464. }
  1465. return {
  1466. $set: {
  1467. color: newColor,
  1468. },
  1469. };
  1470. },
  1471. assignMember(memberId) {
  1472. return {
  1473. $addToSet: {
  1474. members: memberId,
  1475. },
  1476. };
  1477. },
  1478. assignAssignee(assigneeId) {
  1479. // If there is not any assignee, allow one assignee, not more.
  1480. /*
  1481. if (this.getAssignees().length === 0) {
  1482. return {
  1483. $addToSet: {
  1484. assignees: assigneeId,
  1485. },
  1486. };
  1487. */
  1488. // Allow more that one assignee:
  1489. // https://github.com/wekan/wekan/issues/3302
  1490. return {
  1491. $addToSet: {
  1492. assignees: assigneeId,
  1493. },
  1494. };
  1495. //} else {
  1496. // return false,
  1497. //}
  1498. },
  1499. unassignMember(memberId) {
  1500. return {
  1501. $pull: {
  1502. members: memberId,
  1503. },
  1504. };
  1505. },
  1506. unassignAssignee(assigneeId) {
  1507. return {
  1508. $pull: {
  1509. assignees: assigneeId,
  1510. },
  1511. };
  1512. },
  1513. toggleMember(memberId) {
  1514. if (this.members && this.members.indexOf(memberId) > -1) {
  1515. return this.unassignMember(memberId);
  1516. } else {
  1517. return this.assignMember(memberId);
  1518. }
  1519. },
  1520. toggleAssignee(assigneeId) {
  1521. if (this.assignees && this.assignees.indexOf(assigneeId) > -1) {
  1522. return this.unassignAssignee(assigneeId);
  1523. } else {
  1524. return this.assignAssignee(assigneeId);
  1525. }
  1526. },
  1527. assignCustomField(customFieldId) {
  1528. return {
  1529. $addToSet: {
  1530. customFields: {
  1531. _id: customFieldId,
  1532. value: null,
  1533. },
  1534. },
  1535. };
  1536. },
  1537. unassignCustomField(customFieldId) {
  1538. return {
  1539. $pull: {
  1540. customFields: {
  1541. _id: customFieldId,
  1542. },
  1543. },
  1544. };
  1545. },
  1546. toggleCustomField(customFieldId) {
  1547. if (this.customFields && this.customFieldIndex(customFieldId) > -1) {
  1548. return this.unassignCustomField(customFieldId);
  1549. } else {
  1550. return this.assignCustomField(customFieldId);
  1551. }
  1552. },
  1553. setCustomField(customFieldId, value) {
  1554. // todo
  1555. const index = this.customFieldIndex(customFieldId);
  1556. if (index > -1) {
  1557. const update = {
  1558. $set: {},
  1559. };
  1560. update.$set[`customFields.${index}.value`] = value;
  1561. return update;
  1562. }
  1563. // TODO
  1564. // Ignatz 18.05.2018: Return null to silence ESLint. No Idea if that is correct
  1565. return null;
  1566. },
  1567. setCover(coverId) {
  1568. return {
  1569. $set: {
  1570. coverId,
  1571. },
  1572. };
  1573. },
  1574. unsetCover() {
  1575. return {
  1576. $unset: {
  1577. coverId: '',
  1578. },
  1579. };
  1580. },
  1581. setReceived(receivedAt) {
  1582. return {
  1583. $set: {
  1584. receivedAt,
  1585. },
  1586. };
  1587. },
  1588. unsetReceived() {
  1589. return {
  1590. $unset: {
  1591. receivedAt: '',
  1592. },
  1593. };
  1594. },
  1595. setStart(startAt) {
  1596. return {
  1597. $set: {
  1598. startAt,
  1599. },
  1600. };
  1601. },
  1602. unsetStart() {
  1603. return {
  1604. $unset: {
  1605. startAt: '',
  1606. },
  1607. };
  1608. },
  1609. setDue(dueAt) {
  1610. return {
  1611. $set: {
  1612. dueAt,
  1613. },
  1614. };
  1615. },
  1616. unsetDue() {
  1617. return {
  1618. $unset: {
  1619. dueAt: '',
  1620. },
  1621. };
  1622. },
  1623. setEnd(endAt) {
  1624. return {
  1625. $set: {
  1626. endAt,
  1627. },
  1628. };
  1629. },
  1630. unsetEnd() {
  1631. return {
  1632. $unset: {
  1633. endAt: '',
  1634. },
  1635. };
  1636. },
  1637. setOvertime(isOvertime) {
  1638. return {
  1639. $set: {
  1640. isOvertime,
  1641. },
  1642. };
  1643. },
  1644. setSpentTime(spentTime) {
  1645. return {
  1646. $set: {
  1647. spentTime,
  1648. },
  1649. };
  1650. },
  1651. unsetSpentTime() {
  1652. return {
  1653. $unset: {
  1654. spentTime: '',
  1655. isOvertime: false,
  1656. },
  1657. };
  1658. },
  1659. setParentId(parentId) {
  1660. return {
  1661. $set: {
  1662. parentId,
  1663. },
  1664. };
  1665. },
  1666. setVoteQuestion(question, publicVote, allowNonBoardMembers) {
  1667. return {
  1668. $set: {
  1669. vote: {
  1670. question,
  1671. public: publicVote,
  1672. allowNonBoardMembers,
  1673. positive: [],
  1674. negative: [],
  1675. },
  1676. },
  1677. };
  1678. },
  1679. unsetVote() {
  1680. return {
  1681. $unset: {
  1682. vote: '',
  1683. },
  1684. };
  1685. },
  1686. setVoteEnd(end) {
  1687. return {
  1688. $set: { 'vote.end': end },
  1689. };
  1690. },
  1691. unsetVoteEnd() {
  1692. return {
  1693. $unset: { 'vote.end': '' },
  1694. };
  1695. },
  1696. setVote(userId, forIt) {
  1697. switch (forIt) {
  1698. case true:
  1699. // vote for it
  1700. return {
  1701. $pull: {
  1702. 'vote.negative': userId,
  1703. },
  1704. $addToSet: {
  1705. 'vote.positive': userId,
  1706. },
  1707. };
  1708. case false:
  1709. // vote against
  1710. return {
  1711. $pull: {
  1712. 'vote.positive': userId,
  1713. },
  1714. $addToSet: {
  1715. 'vote.negative': userId,
  1716. },
  1717. };
  1718. default:
  1719. // Remove votes
  1720. return {
  1721. $pull: {
  1722. 'vote.positive': userId,
  1723. 'vote.negative': userId,
  1724. },
  1725. };
  1726. }
  1727. },
  1728. });
  1729. Cards.globalSearch = queryParams => {
  1730. const userId = Meteor.userId();
  1731. // eslint-disable-next-line no-console
  1732. // console.log('userId:', userId);
  1733. const errors = new (class {
  1734. constructor() {
  1735. this.notFound = {
  1736. boards: [],
  1737. swimlanes: [],
  1738. lists: [],
  1739. labels: [],
  1740. users: [],
  1741. members: [],
  1742. assignees: [],
  1743. is: [],
  1744. };
  1745. this.colorMap = {};
  1746. for (const color of Boards.simpleSchema()._schema['labels.$.color']
  1747. .allowedValues) {
  1748. this.colorMap[TAPi18n.__(`color-${color}`)] = color;
  1749. }
  1750. }
  1751. hasErrors() {
  1752. for (const prop in this.notFound) {
  1753. if (this.notFound[prop].length) {
  1754. return true;
  1755. }
  1756. }
  1757. return false;
  1758. }
  1759. errorMessages() {
  1760. const messages = [];
  1761. this.notFound.boards.forEach(board => {
  1762. messages.push(TAPi18n.__('board-title-not-found', board));
  1763. });
  1764. this.notFound.swimlanes.forEach(swim => {
  1765. messages.push(TAPi18n.__('swimlane-title-not-found', swim));
  1766. });
  1767. this.notFound.lists.forEach(list => {
  1768. messages.push(TAPi18n.__('list-title-not-found', list));
  1769. });
  1770. this.notFound.labels.forEach(label => {
  1771. const color = Object.entries(this.colorMap)
  1772. .filter(value => value[1] === label)
  1773. .map(value => value[0]);
  1774. if (color.length) {
  1775. messages.push(TAPi18n.__('label-color-not-found', color[0]));
  1776. } else {
  1777. messages.push(TAPi18n.__('label-not-found', label));
  1778. }
  1779. });
  1780. this.notFound.users.forEach(user => {
  1781. messages.push(TAPi18n.__('user-username-not-found', user));
  1782. });
  1783. this.notFound.members.forEach(user => {
  1784. messages.push(TAPi18n.__('user-username-not-found', user));
  1785. });
  1786. this.notFound.assignees.forEach(user => {
  1787. messages.push(TAPi18n.__('user-username-not-found', user));
  1788. });
  1789. return messages;
  1790. }
  1791. })();
  1792. const selector = {
  1793. archived: false,
  1794. type: 'cardType-card',
  1795. boardId: { $in: Boards.userBoardIds(userId) },
  1796. swimlaneId: { $nin: Swimlanes.archivedSwimlaneIds() },
  1797. listId: { $nin: Lists.archivedListIds() },
  1798. };
  1799. if (queryParams.boards.length) {
  1800. const queryBoards = [];
  1801. queryParams.boards.forEach(query => {
  1802. const boards = Boards.userSearch(userId, {
  1803. title: new RegExp(escapeForRegex(query), 'i'),
  1804. });
  1805. if (boards.count()) {
  1806. boards.forEach(board => {
  1807. queryBoards.push(board._id);
  1808. });
  1809. } else {
  1810. errors.notFound.boards.push(query);
  1811. }
  1812. });
  1813. selector.boardId.$in = queryBoards;
  1814. }
  1815. if (queryParams.swimlanes.length) {
  1816. const querySwimlanes = [];
  1817. queryParams.swimlanes.forEach(query => {
  1818. const swimlanes = Swimlanes.find({
  1819. title: new RegExp(escapeForRegex(query), 'i'),
  1820. });
  1821. if (swimlanes.count()) {
  1822. swimlanes.forEach(swim => {
  1823. querySwimlanes.push(swim._id);
  1824. });
  1825. } else {
  1826. errors.notFound.swimlanes.push(query);
  1827. }
  1828. });
  1829. selector.swimlaneId.$in = querySwimlanes;
  1830. }
  1831. if (queryParams.lists.length) {
  1832. const queryLists = [];
  1833. queryParams.lists.forEach(query => {
  1834. const lists = Lists.find({
  1835. title: new RegExp(escapeForRegex(query), 'i'),
  1836. });
  1837. if (lists.count()) {
  1838. lists.forEach(list => {
  1839. queryLists.push(list._id);
  1840. });
  1841. } else {
  1842. errors.notFound.lists.push(query);
  1843. }
  1844. });
  1845. selector.listId.$in = queryLists;
  1846. }
  1847. if (queryParams.comments.length) {
  1848. selector._id = {
  1849. $in: CardComments.textSearch(userId, queryParams.comments).map(com => {
  1850. return com.cardId;
  1851. }),
  1852. };
  1853. }
  1854. if (queryParams.dueAt !== null) {
  1855. selector.dueAt = { $lte: new Date(queryParams.dueAt) };
  1856. }
  1857. if (queryParams.createdAt !== null) {
  1858. selector.createdAt = { $gte: new Date(queryParams.createdAt) };
  1859. }
  1860. if (queryParams.modifiedAt !== null) {
  1861. selector.modifiedAt = { $gte: new Date(queryParams.modifiedAt) };
  1862. }
  1863. const queryMembers = [];
  1864. const queryAssignees = [];
  1865. if (queryParams.users.length) {
  1866. queryParams.users.forEach(query => {
  1867. const users = Users.find({
  1868. username: query,
  1869. });
  1870. if (users.count()) {
  1871. users.forEach(user => {
  1872. queryMembers.push(user._id);
  1873. queryAssignees.push(user._id);
  1874. });
  1875. } else {
  1876. errors.notFound.users.push(query);
  1877. }
  1878. });
  1879. }
  1880. if (queryParams.members.length) {
  1881. queryParams.members.forEach(query => {
  1882. const users = Users.find({
  1883. username: query,
  1884. });
  1885. if (users.count()) {
  1886. users.forEach(user => {
  1887. queryMembers.push(user._id);
  1888. });
  1889. } else {
  1890. errors.notFound.members.push(query);
  1891. }
  1892. });
  1893. }
  1894. if (queryParams.assignees.length) {
  1895. queryParams.assignees.forEach(query => {
  1896. const users = Users.find({
  1897. username: query,
  1898. });
  1899. if (users.count()) {
  1900. users.forEach(user => {
  1901. queryAssignees.push(user._id);
  1902. });
  1903. } else {
  1904. errors.notFound.assignees.push(query);
  1905. }
  1906. });
  1907. }
  1908. if (queryMembers.length && queryAssignees.length) {
  1909. selector.$or = [
  1910. { members: { $in: queryMembers } },
  1911. { assignees: { $in: queryAssignees } },
  1912. ];
  1913. } else if (queryMembers.length) {
  1914. selector.members = { $in: queryMembers };
  1915. } else if (queryAssignees.length) {
  1916. selector.assignees = { $in: queryAssignees };
  1917. }
  1918. if (queryParams.labels.length) {
  1919. queryParams.labels.forEach(label => {
  1920. const queryLabels = [];
  1921. let boards = Boards.userSearch(userId, {
  1922. labels: { $elemMatch: { color: label.toLowerCase() } },
  1923. });
  1924. if (boards.count()) {
  1925. boards.forEach(board => {
  1926. // eslint-disable-next-line no-console
  1927. // console.log('board:', board);
  1928. // eslint-disable-next-line no-console
  1929. // console.log('board.labels:', board.labels);
  1930. board.labels
  1931. .filter(boardLabel => {
  1932. return boardLabel.color === label.toLowerCase();
  1933. })
  1934. .forEach(boardLabel => {
  1935. queryLabels.push(boardLabel._id);
  1936. });
  1937. });
  1938. } else {
  1939. // eslint-disable-next-line no-console
  1940. // console.log('label:', label);
  1941. const reLabel = new RegExp(escapeForRegex(label), 'i');
  1942. // eslint-disable-next-line no-console
  1943. // console.log('reLabel:', reLabel);
  1944. boards = Boards.userSearch(userId, {
  1945. labels: { $elemMatch: { name: reLabel } },
  1946. });
  1947. if (boards.count()) {
  1948. boards.forEach(board => {
  1949. board.labels
  1950. .filter(boardLabel => {
  1951. return boardLabel.name.match(reLabel);
  1952. })
  1953. .forEach(boardLabel => {
  1954. queryLabels.push(boardLabel._id);
  1955. });
  1956. });
  1957. } else {
  1958. errors.notFound.labels.push(label);
  1959. }
  1960. }
  1961. selector.labelIds = { $in: queryLabels };
  1962. });
  1963. }
  1964. if (errors.hasErrors()) {
  1965. return { cards: null, errors };
  1966. }
  1967. if (queryParams.text) {
  1968. const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
  1969. selector.$or = [
  1970. { title: regex },
  1971. { description: regex },
  1972. { customFields: { $elemMatch: { value: regex } } },
  1973. {
  1974. _id: {
  1975. $in: CardComments.textSearch(userId, [queryParams.text]).map(
  1976. com => com.cardId,
  1977. ),
  1978. },
  1979. },
  1980. ];
  1981. }
  1982. // eslint-disable-next-line no-console
  1983. console.log('selector:', selector);
  1984. const projection = {
  1985. fields: {
  1986. _id: 1,
  1987. archived: 1,
  1988. boardId: 1,
  1989. swimlaneId: 1,
  1990. listId: 1,
  1991. title: 1,
  1992. type: 1,
  1993. sort: 1,
  1994. members: 1,
  1995. assignees: 1,
  1996. colors: 1,
  1997. dueAt: 1,
  1998. createdAt: 1,
  1999. modifiedAt: 1,
  2000. labelIds: 1,
  2001. },
  2002. limit: 50,
  2003. };
  2004. if (queryParams.sort === 'due') {
  2005. projection.sort = {
  2006. dueAt: 1,
  2007. boardId: 1,
  2008. swimlaneId: 1,
  2009. listId: 1,
  2010. sort: 1,
  2011. };
  2012. } else if (queryParams.sort === 'modified') {
  2013. projection.sort = {
  2014. modifiedAt: -1,
  2015. boardId: 1,
  2016. swimlaneId: 1,
  2017. listId: 1,
  2018. sort: 1,
  2019. };
  2020. } else if (queryParams.sort === 'created') {
  2021. projection.sort = {
  2022. createdAt: -1,
  2023. boardId: 1,
  2024. swimlaneId: 1,
  2025. listId: 1,
  2026. sort: 1,
  2027. };
  2028. } else if (queryParams.sort === 'system') {
  2029. projection.sort = {
  2030. boardId: 1,
  2031. swimlaneId: 1,
  2032. listId: 1,
  2033. modifiedAt: 1,
  2034. sort: 1,
  2035. };
  2036. }
  2037. const cards = Cards.find(selector, projection);
  2038. // eslint-disable-next-line no-console
  2039. console.log('count:', cards.count());
  2040. return { cards, errors };
  2041. };
  2042. //FUNCTIONS FOR creation of Activities
  2043. function updateActivities(doc, fieldNames, modifier) {
  2044. if (_.contains(fieldNames, 'labelIds') && _.contains(fieldNames, 'boardId')) {
  2045. Activities.find({
  2046. activityType: 'addedLabel',
  2047. cardId: doc._id,
  2048. }).forEach(a => {
  2049. const lidx = doc.labelIds.indexOf(a.labelId);
  2050. if (lidx !== -1 && modifier.$set.labelIds.length > lidx) {
  2051. Activities.update(a._id, {
  2052. $set: {
  2053. labelId: modifier.$set.labelIds[doc.labelIds.indexOf(a.labelId)],
  2054. boardId: modifier.$set.boardId,
  2055. },
  2056. });
  2057. } else {
  2058. Activities.remove(a._id);
  2059. }
  2060. });
  2061. } else if (_.contains(fieldNames, 'boardId')) {
  2062. Activities.remove({
  2063. activityType: 'addedLabel',
  2064. cardId: doc._id,
  2065. });
  2066. }
  2067. }
  2068. function cardMove(
  2069. userId,
  2070. doc,
  2071. fieldNames,
  2072. oldListId,
  2073. oldSwimlaneId,
  2074. oldBoardId,
  2075. ) {
  2076. if (_.contains(fieldNames, 'boardId') && doc.boardId !== oldBoardId) {
  2077. Activities.insert({
  2078. userId,
  2079. activityType: 'moveCardBoard',
  2080. boardName: Boards.findOne(doc.boardId).title,
  2081. boardId: doc.boardId,
  2082. oldBoardId,
  2083. oldBoardName: Boards.findOne(oldBoardId).title,
  2084. cardId: doc._id,
  2085. swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
  2086. swimlaneId: doc.swimlaneId,
  2087. oldSwimlaneId,
  2088. });
  2089. } else if (
  2090. (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) ||
  2091. (_.contains(fieldNames, 'swimlaneId') && doc.swimlaneId !== oldSwimlaneId)
  2092. ) {
  2093. Activities.insert({
  2094. userId,
  2095. oldListId,
  2096. activityType: 'moveCard',
  2097. listName: Lists.findOne(doc.listId).title,
  2098. listId: doc.listId,
  2099. boardId: doc.boardId,
  2100. cardId: doc._id,
  2101. cardTitle: doc.title,
  2102. swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
  2103. swimlaneId: doc.swimlaneId,
  2104. oldSwimlaneId,
  2105. });
  2106. }
  2107. }
  2108. function cardState(userId, doc, fieldNames) {
  2109. if (_.contains(fieldNames, 'archived')) {
  2110. if (doc.archived) {
  2111. Activities.insert({
  2112. userId,
  2113. activityType: 'archivedCard',
  2114. listName: Lists.findOne(doc.listId).title,
  2115. boardId: doc.boardId,
  2116. listId: doc.listId,
  2117. cardId: doc._id,
  2118. swimlaneId: doc.swimlaneId,
  2119. });
  2120. } else {
  2121. Activities.insert({
  2122. userId,
  2123. activityType: 'restoredCard',
  2124. boardId: doc.boardId,
  2125. listName: Lists.findOne(doc.listId).title,
  2126. listId: doc.listId,
  2127. cardId: doc._id,
  2128. swimlaneId: doc.swimlaneId,
  2129. });
  2130. }
  2131. }
  2132. }
  2133. function cardMembers(userId, doc, fieldNames, modifier) {
  2134. if (!_.contains(fieldNames, 'members')) return;
  2135. let memberId;
  2136. // Say hello to the new member
  2137. if (modifier.$addToSet && modifier.$addToSet.members) {
  2138. memberId = modifier.$addToSet.members;
  2139. const username = Users.findOne(memberId).username;
  2140. if (!_.contains(doc.members, memberId)) {
  2141. Activities.insert({
  2142. userId,
  2143. username,
  2144. activityType: 'joinMember',
  2145. boardId: doc.boardId,
  2146. cardId: doc._id,
  2147. memberId,
  2148. listId: doc.listId,
  2149. swimlaneId: doc.swimlaneId,
  2150. });
  2151. }
  2152. }
  2153. // Say goodbye to the former member
  2154. if (modifier.$pull && modifier.$pull.members) {
  2155. memberId = modifier.$pull.members;
  2156. const username = Users.findOne(memberId).username;
  2157. // Check that the former member is member of the card
  2158. if (_.contains(doc.members, memberId)) {
  2159. Activities.insert({
  2160. userId,
  2161. username,
  2162. activityType: 'unjoinMember',
  2163. boardId: doc.boardId,
  2164. cardId: doc._id,
  2165. memberId,
  2166. listId: doc.listId,
  2167. swimlaneId: doc.swimlaneId,
  2168. });
  2169. }
  2170. }
  2171. }
  2172. function cardAssignees(userId, doc, fieldNames, modifier) {
  2173. if (!_.contains(fieldNames, 'assignees')) return;
  2174. let assigneeId;
  2175. // Say hello to the new assignee
  2176. if (modifier.$addToSet && modifier.$addToSet.assignees) {
  2177. assigneeId = modifier.$addToSet.assignees;
  2178. const username = Users.findOne(assigneeId).username;
  2179. if (!_.contains(doc.assignees, assigneeId)) {
  2180. Activities.insert({
  2181. userId,
  2182. username,
  2183. activityType: 'joinAssignee',
  2184. boardId: doc.boardId,
  2185. cardId: doc._id,
  2186. assigneeId,
  2187. listId: doc.listId,
  2188. swimlaneId: doc.swimlaneId,
  2189. });
  2190. }
  2191. }
  2192. // Say goodbye to the former assignee
  2193. if (modifier.$pull && modifier.$pull.assignees) {
  2194. assigneeId = modifier.$pull.assignees;
  2195. const username = Users.findOne(assigneeId).username;
  2196. // Check that the former assignee is assignee of the card
  2197. if (_.contains(doc.assignees, assigneeId)) {
  2198. Activities.insert({
  2199. userId,
  2200. username,
  2201. activityType: 'unjoinAssignee',
  2202. boardId: doc.boardId,
  2203. cardId: doc._id,
  2204. assigneeId,
  2205. listId: doc.listId,
  2206. swimlaneId: doc.swimlaneId,
  2207. });
  2208. }
  2209. }
  2210. }
  2211. function cardLabels(userId, doc, fieldNames, modifier) {
  2212. if (!_.contains(fieldNames, 'labelIds')) return;
  2213. let labelId;
  2214. // Say hello to the new label
  2215. if (modifier.$addToSet && modifier.$addToSet.labelIds) {
  2216. labelId = modifier.$addToSet.labelIds;
  2217. if (!_.contains(doc.labelIds, labelId)) {
  2218. const act = {
  2219. userId,
  2220. labelId,
  2221. activityType: 'addedLabel',
  2222. boardId: doc.boardId,
  2223. cardId: doc._id,
  2224. listId: doc.listId,
  2225. swimlaneId: doc.swimlaneId,
  2226. };
  2227. Activities.insert(act);
  2228. }
  2229. }
  2230. // Say goodbye to the label
  2231. if (modifier.$pull && modifier.$pull.labelIds) {
  2232. labelId = modifier.$pull.labelIds;
  2233. // Check that the former member is member of the card
  2234. if (_.contains(doc.labelIds, labelId)) {
  2235. Activities.insert({
  2236. userId,
  2237. labelId,
  2238. activityType: 'removedLabel',
  2239. boardId: doc.boardId,
  2240. cardId: doc._id,
  2241. listId: doc.listId,
  2242. swimlaneId: doc.swimlaneId,
  2243. });
  2244. }
  2245. }
  2246. }
  2247. function cardCustomFields(userId, doc, fieldNames, modifier) {
  2248. if (!_.contains(fieldNames, 'customFields')) return;
  2249. // Say hello to the new customField value
  2250. if (modifier.$set) {
  2251. _.each(modifier.$set, (value, key) => {
  2252. if (key.startsWith('customFields')) {
  2253. const dotNotation = key.split('.');
  2254. // only individual changes are registered
  2255. if (dotNotation.length > 1) {
  2256. const customFieldId = doc.customFields[dotNotation[1]]._id;
  2257. const act = {
  2258. userId,
  2259. customFieldId,
  2260. value,
  2261. activityType: 'setCustomField',
  2262. boardId: doc.boardId,
  2263. cardId: doc._id,
  2264. };
  2265. Activities.insert(act);
  2266. }
  2267. }
  2268. });
  2269. }
  2270. // Say goodbye to the former customField value
  2271. if (modifier.$unset) {
  2272. _.each(modifier.$unset, (value, key) => {
  2273. if (key.startsWith('customFields')) {
  2274. const dotNotation = key.split('.');
  2275. // only individual changes are registered
  2276. if (dotNotation.length > 1) {
  2277. const customFieldId = doc.customFields[dotNotation[1]]._id;
  2278. const act = {
  2279. userId,
  2280. customFieldId,
  2281. activityType: 'unsetCustomField',
  2282. boardId: doc.boardId,
  2283. cardId: doc._id,
  2284. };
  2285. Activities.insert(act);
  2286. }
  2287. }
  2288. });
  2289. }
  2290. }
  2291. function cardCreation(userId, doc) {
  2292. Activities.insert({
  2293. userId,
  2294. activityType: 'createCard',
  2295. boardId: doc.boardId,
  2296. listName: Lists.findOne(doc.listId).title,
  2297. listId: doc.listId,
  2298. cardId: doc._id,
  2299. cardTitle: doc.title,
  2300. swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
  2301. swimlaneId: doc.swimlaneId,
  2302. });
  2303. }
  2304. function cardRemover(userId, doc) {
  2305. ChecklistItems.remove({
  2306. cardId: doc._id,
  2307. });
  2308. Checklists.remove({
  2309. cardId: doc._id,
  2310. });
  2311. Cards.remove({
  2312. parentId: doc._id,
  2313. });
  2314. CardComments.remove({
  2315. cardId: doc._id,
  2316. });
  2317. Attachments.remove({
  2318. cardId: doc._id,
  2319. });
  2320. }
  2321. const findDueCards = days => {
  2322. const seekDue = ($from, $to, activityType) => {
  2323. Cards.find({
  2324. archived: false,
  2325. dueAt: { $gte: $from, $lt: $to },
  2326. }).forEach(card => {
  2327. const username = Users.findOne(card.userId).username;
  2328. const activity = {
  2329. userId: card.userId,
  2330. username,
  2331. activityType,
  2332. boardId: card.boardId,
  2333. cardId: card._id,
  2334. cardTitle: card.title,
  2335. listId: card.listId,
  2336. timeValue: card.dueAt,
  2337. swimlaneId: card.swimlaneId,
  2338. };
  2339. Activities.insert(activity);
  2340. });
  2341. };
  2342. const now = new Date(),
  2343. aday = 3600 * 24 * 1e3,
  2344. then = day => new Date(now.setHours(0, 0, 0, 0) + day * aday);
  2345. if (!days) return;
  2346. if (!days.map) days = [days];
  2347. days.map(day => {
  2348. let args = [];
  2349. if (day === 0) {
  2350. args = [then(0), then(1), 'duenow'];
  2351. } else if (day > 0) {
  2352. args = [then(1), then(day), 'almostdue'];
  2353. } else {
  2354. args = [then(day), now, 'pastdue'];
  2355. }
  2356. seekDue(...args);
  2357. });
  2358. };
  2359. const addCronJob = _.debounce(
  2360. Meteor.bindEnvironment(function findDueCardsDebounced() {
  2361. const envValue = process.env.NOTIFY_DUE_DAYS_BEFORE_AND_AFTER;
  2362. if (!envValue) {
  2363. return;
  2364. }
  2365. const notifydays = envValue
  2366. .split(',')
  2367. .map(value => {
  2368. const iValue = parseInt(value, 10);
  2369. if (!(iValue > 0 && iValue < 15)) {
  2370. // notifying due is disabled
  2371. return false;
  2372. } else {
  2373. return iValue;
  2374. }
  2375. })
  2376. .filter(Boolean);
  2377. 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
  2378. const defaultitvl = 8; // default every morning at 8am, if the passed env variable has parsing error use default
  2379. const itvl = parseInt(notifyitvl, 10) || defaultitvl;
  2380. const scheduler = (job => () => {
  2381. const now = new Date();
  2382. const hour = 3600 * 1e3;
  2383. if (now.getHours() === itvl) {
  2384. if (typeof job === 'function') {
  2385. job();
  2386. }
  2387. }
  2388. Meteor.setTimeout(scheduler, hour);
  2389. })(() => {
  2390. findDueCards(notifydays);
  2391. });
  2392. scheduler();
  2393. }),
  2394. 500,
  2395. );
  2396. if (Meteor.isServer) {
  2397. // Cards are often fetched within a board, so we create an index to make these
  2398. // queries more efficient.
  2399. Meteor.startup(() => {
  2400. Cards._collection._ensureIndex({ modifiedAt: -1 });
  2401. Cards._collection._ensureIndex({ boardId: 1, createdAt: -1 });
  2402. // https://github.com/wekan/wekan/issues/1863
  2403. // Swimlane added a new field in the cards collection of mongodb named parentId.
  2404. // When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection).
  2405. // With a huge database, this result in a very slow app and high CPU on the mongodb side.
  2406. // To correct it, add Index to parentId:
  2407. Cards._collection._ensureIndex({ parentId: 1 });
  2408. // let notifydays = parseInt(process.env.NOTIFY_DUE_DAYS_BEFORE_AND_AFTER) || 2; // default as 2 days b4 and after
  2409. // let notifyitvl = parseInt(process.env.NOTIFY_DUE_AT_HOUR_OF_DAY) || 3600 * 24 * 1e3; // default interval as one day
  2410. // Meteor.call("findDueCards",notifydays,notifyitvl);
  2411. Meteor.defer(() => {
  2412. addCronJob();
  2413. });
  2414. });
  2415. Cards.after.insert((userId, doc) => {
  2416. cardCreation(userId, doc);
  2417. });
  2418. // New activity for card (un)archivage
  2419. Cards.after.update((userId, doc, fieldNames) => {
  2420. cardState(userId, doc, fieldNames);
  2421. });
  2422. //New activity for card moves
  2423. Cards.after.update(function(userId, doc, fieldNames) {
  2424. const oldListId = this.previous.listId;
  2425. const oldSwimlaneId = this.previous.swimlaneId;
  2426. const oldBoardId = this.previous.boardId;
  2427. cardMove(userId, doc, fieldNames, oldListId, oldSwimlaneId, oldBoardId);
  2428. });
  2429. // Add a new activity if we add or remove a member to the card
  2430. Cards.before.update((userId, doc, fieldNames, modifier) => {
  2431. cardMembers(userId, doc, fieldNames, modifier);
  2432. updateActivities(doc, fieldNames, modifier);
  2433. });
  2434. // Add a new activity if we add or remove a assignee to the card
  2435. Cards.before.update((userId, doc, fieldNames, modifier) => {
  2436. cardAssignees(userId, doc, fieldNames, modifier);
  2437. updateActivities(doc, fieldNames, modifier);
  2438. });
  2439. // Add a new activity if we add or remove a label to the card
  2440. Cards.before.update((userId, doc, fieldNames, modifier) => {
  2441. cardLabels(userId, doc, fieldNames, modifier);
  2442. });
  2443. // Add a new activity if we edit a custom field
  2444. Cards.before.update((userId, doc, fieldNames, modifier) => {
  2445. cardCustomFields(userId, doc, fieldNames, modifier);
  2446. });
  2447. // Add a new activity if modify time related field like dueAt startAt etc
  2448. Cards.before.update((userId, doc, fieldNames, modifier) => {
  2449. const dla = 'dateLastActivity';
  2450. const fields = fieldNames.filter(name => name !== dla);
  2451. const timingaction = ['receivedAt', 'dueAt', 'startAt', 'endAt'];
  2452. const action = fields[0];
  2453. if (fields.length > 0 && _.contains(timingaction, action)) {
  2454. // add activities for user change these attributes
  2455. const value = modifier.$set[action];
  2456. const oldvalue = doc[action] || '';
  2457. const activityType = `a-${action}`;
  2458. const card = Cards.findOne(doc._id);
  2459. const list = card.list();
  2460. if (list) {
  2461. // 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
  2462. const modifiedAt = new Date(
  2463. new Date(value).getTime() -
  2464. (action === 'endAt' ? 365 * 24 * 3600 * 1e3 : 0),
  2465. ); // set it as 1 year before
  2466. const boardId = list.boardId;
  2467. Lists.direct.update(
  2468. {
  2469. _id: list._id,
  2470. },
  2471. {
  2472. $set: {
  2473. modifiedAt,
  2474. boardId,
  2475. },
  2476. },
  2477. );
  2478. }
  2479. const username = Users.findOne(userId).username;
  2480. const activity = {
  2481. userId,
  2482. username,
  2483. activityType,
  2484. boardId: doc.boardId,
  2485. cardId: doc._id,
  2486. cardTitle: doc.title,
  2487. timeKey: action,
  2488. timeValue: value,
  2489. timeOldValue: oldvalue,
  2490. listId: card.listId,
  2491. swimlaneId: card.swimlaneId,
  2492. };
  2493. Activities.insert(activity);
  2494. }
  2495. });
  2496. // Remove all activities associated with a card if we remove the card
  2497. // Remove also card_comments / checklists / attachments
  2498. Cards.before.remove((userId, doc) => {
  2499. cardRemover(userId, doc);
  2500. });
  2501. }
  2502. //SWIMLANES REST API
  2503. if (Meteor.isServer) {
  2504. /**
  2505. * @operation get_swimlane_cards
  2506. * @summary get all cards attached to a swimlane
  2507. *
  2508. * @param {string} boardId the board ID
  2509. * @param {string} swimlaneId the swimlane ID
  2510. * @return_type [{_id: string,
  2511. * title: string,
  2512. * description: string,
  2513. * listId: string}]
  2514. */
  2515. JsonRoutes.add(
  2516. 'GET',
  2517. '/api/boards/:boardId/swimlanes/:swimlaneId/cards',
  2518. function(req, res) {
  2519. const paramBoardId = req.params.boardId;
  2520. const paramSwimlaneId = req.params.swimlaneId;
  2521. Authentication.checkBoardAccess(req.userId, paramBoardId);
  2522. JsonRoutes.sendResult(res, {
  2523. code: 200,
  2524. data: Cards.find({
  2525. boardId: paramBoardId,
  2526. swimlaneId: paramSwimlaneId,
  2527. archived: false,
  2528. }).map(function(doc) {
  2529. return {
  2530. _id: doc._id,
  2531. title: doc.title,
  2532. description: doc.description,
  2533. listId: doc.listId,
  2534. receivedAt: doc.receivedAt,
  2535. startAt: doc.startAt,
  2536. dueAt: doc.dueAt,
  2537. endAt: doc.endAt,
  2538. assignees: doc.assignees,
  2539. };
  2540. }),
  2541. });
  2542. },
  2543. );
  2544. }
  2545. //LISTS REST API
  2546. if (Meteor.isServer) {
  2547. /**
  2548. * @operation get_all_cards
  2549. * @summary Get all Cards attached to a List
  2550. *
  2551. * @param {string} boardId the board ID
  2552. * @param {string} listId the list ID
  2553. * @return_type [{_id: string,
  2554. * title: string,
  2555. * description: string}]
  2556. */
  2557. JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function(
  2558. req,
  2559. res,
  2560. ) {
  2561. const paramBoardId = req.params.boardId;
  2562. const paramListId = req.params.listId;
  2563. Authentication.checkBoardAccess(req.userId, paramBoardId);
  2564. JsonRoutes.sendResult(res, {
  2565. code: 200,
  2566. data: Cards.find({
  2567. boardId: paramBoardId,
  2568. listId: paramListId,
  2569. archived: false,
  2570. }).map(function(doc) {
  2571. return {
  2572. _id: doc._id,
  2573. title: doc.title,
  2574. description: doc.description,
  2575. receivedAt: doc.receivedAt,
  2576. startAt: doc.startAt,
  2577. dueAt: doc.dueAt,
  2578. endAt: doc.endAt,
  2579. assignees: doc.assignees,
  2580. };
  2581. }),
  2582. });
  2583. });
  2584. /**
  2585. * @operation get_card
  2586. * @summary Get a Card
  2587. *
  2588. * @param {string} boardId the board ID
  2589. * @param {string} listId the list ID of the card
  2590. * @param {string} cardId the card ID
  2591. * @return_type Cards
  2592. */
  2593. JsonRoutes.add(
  2594. 'GET',
  2595. '/api/boards/:boardId/lists/:listId/cards/:cardId',
  2596. function(req, res) {
  2597. const paramBoardId = req.params.boardId;
  2598. const paramListId = req.params.listId;
  2599. const paramCardId = req.params.cardId;
  2600. Authentication.checkBoardAccess(req.userId, paramBoardId);
  2601. JsonRoutes.sendResult(res, {
  2602. code: 200,
  2603. data: Cards.findOne({
  2604. _id: paramCardId,
  2605. listId: paramListId,
  2606. boardId: paramBoardId,
  2607. archived: false,
  2608. }),
  2609. });
  2610. },
  2611. );
  2612. /**
  2613. * @operation new_card
  2614. * @summary Create a new Card
  2615. *
  2616. * @param {string} boardId the board ID of the new card
  2617. * @param {string} listId the list ID of the new card
  2618. * @param {string} authorID the user ID of the person owning the card
  2619. * @param {string} parentId the parent ID of the new card
  2620. * @param {string} title the title of the new card
  2621. * @param {string} description the description of the new card
  2622. * @param {string} swimlaneId the swimlane ID of the new card
  2623. * @param {string} [members] the member IDs list of the new card
  2624. * @param {string} [assignees] the array of maximum one ID of assignee of the new card
  2625. * @return_type {_id: string}
  2626. */
  2627. JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function(
  2628. req,
  2629. res,
  2630. ) {
  2631. // Check user is logged in
  2632. Authentication.checkLoggedIn(req.userId);
  2633. const paramBoardId = req.params.boardId;
  2634. // Check user has permission to add card to the board
  2635. const board = Boards.findOne({
  2636. _id: paramBoardId,
  2637. });
  2638. const addPermission = allowIsBoardMemberCommentOnly(req.userId, board);
  2639. Authentication.checkAdminOrCondition(req.userId, addPermission);
  2640. const paramListId = req.params.listId;
  2641. const paramParentId = req.params.parentId;
  2642. const currentCards = Cards.find(
  2643. {
  2644. listId: paramListId,
  2645. archived: false,
  2646. },
  2647. { sort: ['sort'] },
  2648. );
  2649. const check = Users.findOne({
  2650. _id: req.body.authorId,
  2651. });
  2652. const members = req.body.members;
  2653. const assignees = req.body.assignees;
  2654. if (typeof check !== 'undefined') {
  2655. const id = Cards.direct.insert({
  2656. title: req.body.title,
  2657. boardId: paramBoardId,
  2658. listId: paramListId,
  2659. parentId: paramParentId,
  2660. description: req.body.description,
  2661. userId: req.body.authorId,
  2662. swimlaneId: req.body.swimlaneId,
  2663. sort: currentCards.count(),
  2664. members,
  2665. assignees,
  2666. });
  2667. JsonRoutes.sendResult(res, {
  2668. code: 200,
  2669. data: {
  2670. _id: id,
  2671. },
  2672. });
  2673. const card = Cards.findOne({
  2674. _id: id,
  2675. });
  2676. cardCreation(req.body.authorId, card);
  2677. } else {
  2678. JsonRoutes.sendResult(res, {
  2679. code: 401,
  2680. });
  2681. }
  2682. });
  2683. /*
  2684. * Note for the JSDoc:
  2685. * 'list' will be interpreted as the path parameter
  2686. * 'listID' will be interpreted as the body parameter
  2687. */
  2688. /**
  2689. * @operation edit_card
  2690. * @summary Edit Fields in a Card
  2691. *
  2692. * @description Edit a card
  2693. *
  2694. * The color has to be chosen between `white`, `green`, `yellow`, `orange`,
  2695. * `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`,
  2696. * `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`,
  2697. * `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`,
  2698. * `indigo`:
  2699. *
  2700. * <img src="/card-colors.png" width="40%" alt="Wekan card colors" />
  2701. *
  2702. * Note: setting the color to white has the same effect than removing it.
  2703. *
  2704. * @param {string} boardId the board ID of the card
  2705. * @param {string} list the list ID of the card
  2706. * @param {string} cardId the ID of the card
  2707. * @param {string} [title] the new title of the card
  2708. * @param {string} [listId] the new list ID of the card (move operation)
  2709. * @param {string} [description] the new description of the card
  2710. * @param {string} [authorId] change the owner of the card
  2711. * @param {string} [parentId] change the parent of the card
  2712. * @param {string} [labelIds] the new list of label IDs attached to the card
  2713. * @param {string} [swimlaneId] the new swimlane ID of the card
  2714. * @param {string} [members] the new list of member IDs attached to the card
  2715. * @param {string} [assignees] the array of maximum one ID of assignee attached to the card
  2716. * @param {string} [requestedBy] the new requestedBy field of the card
  2717. * @param {string} [assignedBy] the new assignedBy field of the card
  2718. * @param {string} [receivedAt] the new receivedAt field of the card
  2719. * @param {string} [assignBy] the new assignBy field of the card
  2720. * @param {string} [startAt] the new startAt field of the card
  2721. * @param {string} [dueAt] the new dueAt field of the card
  2722. * @param {string} [endAt] the new endAt field of the card
  2723. * @param {string} [spentTime] the new spentTime field of the card
  2724. * @param {boolean} [isOverTime] the new isOverTime field of the card
  2725. * @param {string} [customFields] the new customFields value of the card
  2726. * @param {string} [color] the new color of the card
  2727. * @param {Object} [vote] the vote object
  2728. * @param {string} vote.question the vote question
  2729. * @param {boolean} vote.public show who voted what
  2730. * @param {boolean} vote.allowNonBoardMembers allow all logged in users to vote?
  2731. * @return_type {_id: string}
  2732. */
  2733. JsonRoutes.add(
  2734. 'PUT',
  2735. '/api/boards/:boardId/lists/:listId/cards/:cardId',
  2736. function(req, res) {
  2737. Authentication.checkUserId(req.userId);
  2738. const paramBoardId = req.params.boardId;
  2739. const paramCardId = req.params.cardId;
  2740. const paramListId = req.params.listId;
  2741. if (req.body.hasOwnProperty('title')) {
  2742. const newTitle = req.body.title;
  2743. Cards.direct.update(
  2744. {
  2745. _id: paramCardId,
  2746. listId: paramListId,
  2747. boardId: paramBoardId,
  2748. archived: false,
  2749. },
  2750. {
  2751. $set: {
  2752. title: newTitle,
  2753. },
  2754. },
  2755. );
  2756. }
  2757. if (req.body.hasOwnProperty('parentId')) {
  2758. const newParentId = req.body.parentId;
  2759. Cards.direct.update(
  2760. {
  2761. _id: paramCardId,
  2762. listId: paramListId,
  2763. boardId: paramBoardId,
  2764. archived: false,
  2765. },
  2766. {
  2767. $set: {
  2768. parentId: newParentId,
  2769. },
  2770. },
  2771. );
  2772. }
  2773. if (req.body.hasOwnProperty('description')) {
  2774. const newDescription = req.body.description;
  2775. Cards.direct.update(
  2776. {
  2777. _id: paramCardId,
  2778. listId: paramListId,
  2779. boardId: paramBoardId,
  2780. archived: false,
  2781. },
  2782. {
  2783. $set: {
  2784. description: newDescription,
  2785. },
  2786. },
  2787. );
  2788. }
  2789. if (req.body.hasOwnProperty('color')) {
  2790. const newColor = req.body.color;
  2791. Cards.direct.update(
  2792. {
  2793. _id: paramCardId,
  2794. listId: paramListId,
  2795. boardId: paramBoardId,
  2796. archived: false,
  2797. },
  2798. { $set: { color: newColor } },
  2799. );
  2800. }
  2801. if (req.body.hasOwnProperty('vote')) {
  2802. const newVote = req.body.vote;
  2803. newVote.positive = [];
  2804. newVote.negative = [];
  2805. if (!newVote.hasOwnProperty('public')) newVote.public = false;
  2806. if (!newVote.hasOwnProperty('allowNonBoardMembers'))
  2807. newVote.allowNonBoardMembers = false;
  2808. Cards.direct.update(
  2809. {
  2810. _id: paramCardId,
  2811. listId: paramListId,
  2812. boardId: paramBoardId,
  2813. archived: false,
  2814. },
  2815. { $set: { vote: newVote } },
  2816. );
  2817. }
  2818. if (req.body.hasOwnProperty('labelIds')) {
  2819. let newlabelIds = req.body.labelIds;
  2820. if (_.isString(newlabelIds)) {
  2821. if (newlabelIds === '') {
  2822. newlabelIds = null;
  2823. } else {
  2824. newlabelIds = [newlabelIds];
  2825. }
  2826. }
  2827. Cards.direct.update(
  2828. {
  2829. _id: paramCardId,
  2830. listId: paramListId,
  2831. boardId: paramBoardId,
  2832. archived: false,
  2833. },
  2834. {
  2835. $set: {
  2836. labelIds: newlabelIds,
  2837. },
  2838. },
  2839. );
  2840. }
  2841. if (req.body.hasOwnProperty('requestedBy')) {
  2842. const newrequestedBy = req.body.requestedBy;
  2843. Cards.direct.update(
  2844. {
  2845. _id: paramCardId,
  2846. listId: paramListId,
  2847. boardId: paramBoardId,
  2848. archived: false,
  2849. },
  2850. { $set: { requestedBy: newrequestedBy } },
  2851. );
  2852. }
  2853. if (req.body.hasOwnProperty('assignedBy')) {
  2854. const newassignedBy = req.body.assignedBy;
  2855. Cards.direct.update(
  2856. {
  2857. _id: paramCardId,
  2858. listId: paramListId,
  2859. boardId: paramBoardId,
  2860. archived: false,
  2861. },
  2862. { $set: { assignedBy: newassignedBy } },
  2863. );
  2864. }
  2865. if (req.body.hasOwnProperty('receivedAt')) {
  2866. const newreceivedAt = req.body.receivedAt;
  2867. Cards.direct.update(
  2868. {
  2869. _id: paramCardId,
  2870. listId: paramListId,
  2871. boardId: paramBoardId,
  2872. archived: false,
  2873. },
  2874. { $set: { receivedAt: newreceivedAt } },
  2875. );
  2876. }
  2877. if (req.body.hasOwnProperty('startAt')) {
  2878. const newstartAt = req.body.startAt;
  2879. Cards.direct.update(
  2880. {
  2881. _id: paramCardId,
  2882. listId: paramListId,
  2883. boardId: paramBoardId,
  2884. archived: false,
  2885. },
  2886. { $set: { startAt: newstartAt } },
  2887. );
  2888. }
  2889. if (req.body.hasOwnProperty('dueAt')) {
  2890. const newdueAt = req.body.dueAt;
  2891. Cards.direct.update(
  2892. {
  2893. _id: paramCardId,
  2894. listId: paramListId,
  2895. boardId: paramBoardId,
  2896. archived: false,
  2897. },
  2898. { $set: { dueAt: newdueAt } },
  2899. );
  2900. }
  2901. if (req.body.hasOwnProperty('endAt')) {
  2902. const newendAt = req.body.endAt;
  2903. Cards.direct.update(
  2904. {
  2905. _id: paramCardId,
  2906. listId: paramListId,
  2907. boardId: paramBoardId,
  2908. archived: false,
  2909. },
  2910. { $set: { endAt: newendAt } },
  2911. );
  2912. }
  2913. if (req.body.hasOwnProperty('spentTime')) {
  2914. const newspentTime = req.body.spentTime;
  2915. Cards.direct.update(
  2916. {
  2917. _id: paramCardId,
  2918. listId: paramListId,
  2919. boardId: paramBoardId,
  2920. archived: false,
  2921. },
  2922. { $set: { spentTime: newspentTime } },
  2923. );
  2924. }
  2925. if (req.body.hasOwnProperty('isOverTime')) {
  2926. const newisOverTime = req.body.isOverTime;
  2927. Cards.direct.update(
  2928. {
  2929. _id: paramCardId,
  2930. listId: paramListId,
  2931. boardId: paramBoardId,
  2932. archived: false,
  2933. },
  2934. { $set: { isOverTime: newisOverTime } },
  2935. );
  2936. }
  2937. if (req.body.hasOwnProperty('customFields')) {
  2938. const newcustomFields = req.body.customFields;
  2939. Cards.direct.update(
  2940. {
  2941. _id: paramCardId,
  2942. listId: paramListId,
  2943. boardId: paramBoardId,
  2944. archived: false,
  2945. },
  2946. { $set: { customFields: newcustomFields } },
  2947. );
  2948. }
  2949. if (req.body.hasOwnProperty('members')) {
  2950. let newmembers = req.body.members;
  2951. if (_.isString(newmembers)) {
  2952. if (newmembers === '') {
  2953. newmembers = null;
  2954. } else {
  2955. newmembers = [newmembers];
  2956. }
  2957. }
  2958. Cards.direct.update(
  2959. {
  2960. _id: paramCardId,
  2961. listId: paramListId,
  2962. boardId: paramBoardId,
  2963. archived: false,
  2964. },
  2965. { $set: { members: newmembers } },
  2966. );
  2967. }
  2968. if (req.body.hasOwnProperty('assignees')) {
  2969. let newassignees = req.body.assignees;
  2970. if (_.isString(newassignees)) {
  2971. if (newassignees === '') {
  2972. newassignees = null;
  2973. } else {
  2974. newassignees = [newassignees];
  2975. }
  2976. }
  2977. Cards.direct.update(
  2978. {
  2979. _id: paramCardId,
  2980. listId: paramListId,
  2981. boardId: paramBoardId,
  2982. archived: false,
  2983. },
  2984. { $set: { assignees: newassignees } },
  2985. );
  2986. }
  2987. if (req.body.hasOwnProperty('swimlaneId')) {
  2988. const newParamSwimlaneId = req.body.swimlaneId;
  2989. Cards.direct.update(
  2990. {
  2991. _id: paramCardId,
  2992. listId: paramListId,
  2993. boardId: paramBoardId,
  2994. archived: false,
  2995. },
  2996. { $set: { swimlaneId: newParamSwimlaneId } },
  2997. );
  2998. }
  2999. if (req.body.hasOwnProperty('listId')) {
  3000. const newParamListId = req.body.listId;
  3001. Cards.direct.update(
  3002. {
  3003. _id: paramCardId,
  3004. listId: paramListId,
  3005. boardId: paramBoardId,
  3006. archived: false,
  3007. },
  3008. {
  3009. $set: {
  3010. listId: newParamListId,
  3011. },
  3012. },
  3013. );
  3014. const card = Cards.findOne({
  3015. _id: paramCardId,
  3016. });
  3017. cardMove(
  3018. req.body.authorId,
  3019. card,
  3020. {
  3021. fieldName: 'listId',
  3022. },
  3023. paramListId,
  3024. );
  3025. }
  3026. JsonRoutes.sendResult(res, {
  3027. code: 200,
  3028. data: {
  3029. _id: paramCardId,
  3030. },
  3031. });
  3032. },
  3033. );
  3034. /**
  3035. * @operation delete_card
  3036. * @summary Delete a card from a board
  3037. *
  3038. * @description This operation **deletes** a card, and therefore the card
  3039. * is not put in the recycle bin.
  3040. *
  3041. * @param {string} boardId the board ID of the card
  3042. * @param {string} list the list ID of the card
  3043. * @param {string} cardId the ID of the card
  3044. * @return_type {_id: string}
  3045. */
  3046. JsonRoutes.add(
  3047. 'DELETE',
  3048. '/api/boards/:boardId/lists/:listId/cards/:cardId',
  3049. function(req, res) {
  3050. Authentication.checkUserId(req.userId);
  3051. const paramBoardId = req.params.boardId;
  3052. const paramListId = req.params.listId;
  3053. const paramCardId = req.params.cardId;
  3054. const card = Cards.findOne({
  3055. _id: paramCardId,
  3056. });
  3057. Cards.direct.remove({
  3058. _id: paramCardId,
  3059. listId: paramListId,
  3060. boardId: paramBoardId,
  3061. });
  3062. cardRemover(req.body.authorId, card);
  3063. JsonRoutes.sendResult(res, {
  3064. code: 200,
  3065. data: {
  3066. _id: paramCardId,
  3067. },
  3068. });
  3069. },
  3070. );
  3071. /**
  3072. * @operation get_cards_by_custom_field
  3073. * @summary Get all Cards that matchs a value of a specific custom field
  3074. *
  3075. * @param {string} boardId the board ID
  3076. * @param {string} customFieldId the list ID
  3077. * @param {string} customFieldValue the value to look for
  3078. * @return_type [{_id: string,
  3079. * title: string,
  3080. * description: string,
  3081. * listId: string,
  3082. * swinlaneId: string}]
  3083. */
  3084. JsonRoutes.add(
  3085. 'GET',
  3086. '/api/boards/:boardId/cardsByCustomField/:customFieldId/:customFieldValue',
  3087. function(req, res) {
  3088. const paramBoardId = req.params.boardId;
  3089. const paramCustomFieldId = req.params.customFieldId;
  3090. const paramCustomFieldValue = req.params.customFieldValue;
  3091. Authentication.checkBoardAccess(req.userId, paramBoardId);
  3092. JsonRoutes.sendResult(res, {
  3093. code: 200,
  3094. data: Cards.find({
  3095. boardId: paramBoardId,
  3096. customFields: {
  3097. $elemMatch: {
  3098. _id: paramCustomFieldId,
  3099. value: paramCustomFieldValue,
  3100. },
  3101. },
  3102. archived: false,
  3103. }).fetch(),
  3104. });
  3105. },
  3106. );
  3107. }
  3108. export default Cards;