2
0

boards.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830
  1. Boards = new Mongo.Collection('boards');
  2. /**
  3. * This is a Board.
  4. */
  5. Boards.attachSchema(
  6. new SimpleSchema({
  7. title: {
  8. /**
  9. * The title of the board
  10. */
  11. type: String,
  12. },
  13. slug: {
  14. /**
  15. * The title slugified.
  16. */
  17. type: String,
  18. // eslint-disable-next-line consistent-return
  19. autoValue() {
  20. // In some cases (Chinese and Japanese for instance) the `getSlug` function
  21. // return an empty string. This is causes bugs in our application so we set
  22. // a default slug in this case.
  23. // Improvment would be to change client URL after slug is changed
  24. const title = this.field('title');
  25. if (title.isSet && !this.isSet) {
  26. let slug = 'board';
  27. slug = getSlug(title.value) || slug;
  28. return slug;
  29. }
  30. },
  31. },
  32. archived: {
  33. /**
  34. * Is the board archived?
  35. */
  36. type: Boolean,
  37. // eslint-disable-next-line consistent-return
  38. autoValue() {
  39. if (this.isInsert && !this.isSet) {
  40. return false;
  41. }
  42. },
  43. },
  44. createdAt: {
  45. /**
  46. * Creation time of the board
  47. */
  48. type: Date,
  49. // eslint-disable-next-line consistent-return
  50. autoValue() {
  51. if (this.isInsert) {
  52. return new Date();
  53. } else if (this.isUpsert) {
  54. return { $setOnInsert: new Date() };
  55. } else {
  56. this.unset();
  57. }
  58. },
  59. },
  60. // XXX Inconsistent field naming
  61. modifiedAt: {
  62. /**
  63. * Last modification time of the board
  64. */
  65. type: Date,
  66. optional: true,
  67. // eslint-disable-next-line consistent-return
  68. autoValue() {
  69. if (this.isInsert || this.isUpsert || this.isUpdate) {
  70. return new Date();
  71. } else {
  72. this.unset();
  73. }
  74. },
  75. },
  76. // De-normalized number of users that have starred this board
  77. stars: {
  78. /**
  79. * How many stars the board has
  80. */
  81. type: Number,
  82. // eslint-disable-next-line consistent-return
  83. autoValue() {
  84. if (this.isInsert) {
  85. return 0;
  86. }
  87. },
  88. },
  89. // De-normalized label system
  90. labels: {
  91. /**
  92. * List of labels attached to a board
  93. */
  94. type: [Object],
  95. // eslint-disable-next-line consistent-return
  96. autoValue() {
  97. if (this.isInsert && !this.isSet) {
  98. const colors = Boards.simpleSchema()._schema['labels.$.color']
  99. .allowedValues;
  100. const defaultLabelsColors = _.clone(colors).splice(0, 6);
  101. return defaultLabelsColors.map(color => ({
  102. color,
  103. _id: Random.id(6),
  104. name: '',
  105. }));
  106. }
  107. },
  108. },
  109. 'labels.$._id': {
  110. /**
  111. * Unique id of a label
  112. */
  113. // We don't specify that this field must be unique in the board because that
  114. // will cause performance penalties and is not necessary since this field is
  115. // always set on the server.
  116. // XXX Actually if we create a new label, the `_id` is set on the client
  117. // without being overwritten by the server, could it be a problem?
  118. type: String,
  119. },
  120. 'labels.$.name': {
  121. /**
  122. * Name of a label
  123. */
  124. type: String,
  125. optional: true,
  126. },
  127. 'labels.$.color': {
  128. /**
  129. * color of a label.
  130. *
  131. * Can be amongst `green`, `yellow`, `orange`, `red`, `purple`,
  132. * `blue`, `sky`, `lime`, `pink`, `black`,
  133. * `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`,
  134. * `slateblue`, `magenta`, `gold`, `navy`, `gray`,
  135. * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`
  136. */
  137. type: String,
  138. allowedValues: [
  139. 'green',
  140. 'yellow',
  141. 'orange',
  142. 'red',
  143. 'purple',
  144. 'blue',
  145. 'sky',
  146. 'lime',
  147. 'pink',
  148. 'black',
  149. 'silver',
  150. 'peachpuff',
  151. 'crimson',
  152. 'plum',
  153. 'darkgreen',
  154. 'slateblue',
  155. 'magenta',
  156. 'gold',
  157. 'navy',
  158. 'gray',
  159. 'saddlebrown',
  160. 'paleturquoise',
  161. 'mistyrose',
  162. 'indigo',
  163. ],
  164. },
  165. // XXX We might want to maintain more informations under the member sub-
  166. // documents like de-normalized meta-data (the date the member joined the
  167. // board, the number of contributions, etc.).
  168. members: {
  169. /**
  170. * List of members of a board
  171. */
  172. type: [Object],
  173. // eslint-disable-next-line consistent-return
  174. autoValue() {
  175. if (this.isInsert && !this.isSet) {
  176. return [
  177. {
  178. userId: this.userId,
  179. isAdmin: true,
  180. isActive: true,
  181. isNoComments: false,
  182. isCommentOnly: false,
  183. isWorker: false,
  184. },
  185. ];
  186. }
  187. },
  188. },
  189. 'members.$.userId': {
  190. /**
  191. * The uniq ID of the member
  192. */
  193. type: String,
  194. },
  195. 'members.$.isAdmin': {
  196. /**
  197. * Is the member an admin of the board?
  198. */
  199. type: Boolean,
  200. },
  201. 'members.$.isActive': {
  202. /**
  203. * Is the member active?
  204. */
  205. type: Boolean,
  206. },
  207. 'members.$.isNoComments': {
  208. /**
  209. * Is the member not allowed to make comments
  210. */
  211. type: Boolean,
  212. optional: true,
  213. },
  214. 'members.$.isCommentOnly': {
  215. /**
  216. * Is the member only allowed to comment on the board
  217. */
  218. type: Boolean,
  219. optional: true,
  220. },
  221. 'members.$.isWorker': {
  222. /**
  223. * Is the member only allowed to move card, assign himself to card and comment
  224. */
  225. type: Boolean,
  226. optional: true,
  227. },
  228. permission: {
  229. /**
  230. * visibility of the board
  231. */
  232. type: String,
  233. allowedValues: ['public', 'private'],
  234. },
  235. color: {
  236. /**
  237. * The color of the board.
  238. */
  239. type: String,
  240. allowedValues: [
  241. 'belize',
  242. 'nephritis',
  243. 'pomegranate',
  244. 'pumpkin',
  245. 'wisteria',
  246. 'moderatepink',
  247. 'strongcyan',
  248. 'limegreen',
  249. 'midnight',
  250. 'dark',
  251. 'relax',
  252. 'corteza',
  253. 'clearblue',
  254. 'natural',
  255. 'modern',
  256. 'moderndark',
  257. ],
  258. // eslint-disable-next-line consistent-return
  259. autoValue() {
  260. if (this.isInsert && !this.isSet) {
  261. return Boards.simpleSchema()._schema.color.allowedValues[0];
  262. }
  263. },
  264. },
  265. description: {
  266. /**
  267. * The description of the board
  268. */
  269. type: String,
  270. optional: true,
  271. },
  272. subtasksDefaultBoardId: {
  273. /**
  274. * The default board ID assigned to subtasks.
  275. */
  276. type: String,
  277. optional: true,
  278. defaultValue: null,
  279. },
  280. subtasksDefaultListId: {
  281. /**
  282. * The default List ID assigned to subtasks.
  283. */
  284. type: String,
  285. optional: true,
  286. defaultValue: null,
  287. },
  288. dateSettingsDefaultBoardId: {
  289. type: String,
  290. optional: true,
  291. defaultValue: null,
  292. },
  293. dateSettingsDefaultListId: {
  294. type: String,
  295. optional: true,
  296. defaultValue: null,
  297. },
  298. allowsSubtasks: {
  299. /**
  300. * Does the board allows subtasks?
  301. */
  302. type: Boolean,
  303. defaultValue: true,
  304. },
  305. allowsAttachments: {
  306. /**
  307. * Does the board allows attachments?
  308. */
  309. type: Boolean,
  310. defaultValue: true,
  311. },
  312. allowsChecklists: {
  313. /**
  314. * Does the board allows checklists?
  315. */
  316. type: Boolean,
  317. defaultValue: true,
  318. },
  319. allowsComments: {
  320. /**
  321. * Does the board allows comments?
  322. */
  323. type: Boolean,
  324. defaultValue: true,
  325. },
  326. allowsDescriptionTitle: {
  327. /**
  328. * Does the board allows description title?
  329. */
  330. type: Boolean,
  331. defaultValue: true,
  332. },
  333. allowsDescriptionText: {
  334. /**
  335. * Does the board allows description text?
  336. */
  337. type: Boolean,
  338. defaultValue: true,
  339. },
  340. allowsActivities: {
  341. /**
  342. * Does the board allows comments?
  343. */
  344. type: Boolean,
  345. defaultValue: true,
  346. },
  347. allowsLabels: {
  348. /**
  349. * Does the board allows labels?
  350. */
  351. type: Boolean,
  352. defaultValue: true,
  353. },
  354. allowsAssignee: {
  355. /**
  356. * Does the board allows assignee?
  357. */
  358. type: Boolean,
  359. defaultValue: true,
  360. },
  361. allowsMembers: {
  362. /**
  363. * Does the board allows members?
  364. */
  365. type: Boolean,
  366. defaultValue: true,
  367. },
  368. allowsRequestedBy: {
  369. /**
  370. * Does the board allows requested by?
  371. */
  372. type: Boolean,
  373. defaultValue: true,
  374. },
  375. allowsAssignedBy: {
  376. /**
  377. * Does the board allows requested by?
  378. */
  379. type: Boolean,
  380. defaultValue: true,
  381. },
  382. allowsReceivedDate: {
  383. /**
  384. * Does the board allows received date?
  385. */
  386. type: Boolean,
  387. defaultValue: true,
  388. },
  389. allowsStartDate: {
  390. /**
  391. * Does the board allows start date?
  392. */
  393. type: Boolean,
  394. defaultValue: true,
  395. },
  396. allowsEndDate: {
  397. /**
  398. * Does the board allows end date?
  399. */
  400. type: Boolean,
  401. defaultValue: true,
  402. },
  403. allowsDueDate: {
  404. /**
  405. * Does the board allows due date?
  406. */
  407. type: Boolean,
  408. defaultValue: true,
  409. },
  410. presentParentTask: {
  411. /**
  412. * Controls how to present the parent task:
  413. *
  414. * - `prefix-with-full-path`: add a prefix with the full path
  415. * - `prefix-with-parent`: add a prefisx with the parent name
  416. * - `subtext-with-full-path`: add a subtext with the full path
  417. * - `subtext-with-parent`: add a subtext with the parent name
  418. * - `no-parent`: does not show the parent at all
  419. */
  420. type: String,
  421. allowedValues: [
  422. 'prefix-with-full-path',
  423. 'prefix-with-parent',
  424. 'subtext-with-full-path',
  425. 'subtext-with-parent',
  426. 'no-parent',
  427. ],
  428. optional: true,
  429. defaultValue: 'no-parent',
  430. },
  431. startAt: {
  432. /**
  433. * Starting date of the board.
  434. */
  435. type: Date,
  436. optional: true,
  437. },
  438. dueAt: {
  439. /**
  440. * Due date of the board.
  441. */
  442. type: Date,
  443. optional: true,
  444. },
  445. endAt: {
  446. /**
  447. * End date of the board.
  448. */
  449. type: Date,
  450. optional: true,
  451. },
  452. spentTime: {
  453. /**
  454. * Time spent in the board.
  455. */
  456. type: Number,
  457. decimal: true,
  458. optional: true,
  459. },
  460. isOvertime: {
  461. /**
  462. * Is the board overtimed?
  463. */
  464. type: Boolean,
  465. defaultValue: false,
  466. optional: true,
  467. },
  468. type: {
  469. /**
  470. * The type of board
  471. */
  472. type: String,
  473. defaultValue: 'board',
  474. },
  475. sort: {
  476. /**
  477. * Sort value
  478. */
  479. type: Number,
  480. decimal: true,
  481. defaultValue: -1,
  482. },
  483. }),
  484. );
  485. Boards.helpers({
  486. copy() {
  487. const oldId = this._id;
  488. delete this._id;
  489. const _id = Boards.insert(this);
  490. // Copy all swimlanes in board
  491. Swimlanes.find({
  492. boardId: oldId,
  493. archived: false,
  494. }).forEach(swimlane => {
  495. swimlane.type = 'swimlane';
  496. swimlane.copy(_id);
  497. });
  498. },
  499. /**
  500. * Is supplied user authorized to view this board?
  501. */
  502. isVisibleBy(user) {
  503. if (this.isPublic()) {
  504. // public boards are visible to everyone
  505. return true;
  506. } else {
  507. // otherwise you have to be logged-in and active member
  508. return user && this.isActiveMember(user._id);
  509. }
  510. },
  511. /**
  512. * Is the user one of the active members of the board?
  513. *
  514. * @param userId
  515. * @returns {boolean} the member that matches, or undefined/false
  516. */
  517. isActiveMember(userId) {
  518. if (userId) {
  519. return this.members.find(
  520. member => member.userId === userId && member.isActive,
  521. );
  522. } else {
  523. return false;
  524. }
  525. },
  526. isPublic() {
  527. return this.permission === 'public';
  528. },
  529. cards() {
  530. return Cards.find(
  531. { boardId: this._id, archived: false },
  532. { sort: { title: 1 } },
  533. );
  534. },
  535. lists() {
  536. //currentUser = Meteor.user();
  537. //if (currentUser) {
  538. // enabled = Meteor.user().hasSortBy();
  539. //}
  540. //return enabled ? this.newestLists() : this.draggableLists();
  541. return this.draggableLists();
  542. },
  543. newestLists() {
  544. // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
  545. const value = Meteor.user()._getListSortBy();
  546. const sortKey = { starred: -1, [value[0]]: value[1] }; // [["starred",-1],value];
  547. return Lists.find(
  548. {
  549. boardId: this._id,
  550. archived: false,
  551. },
  552. { sort: sortKey },
  553. );
  554. },
  555. draggableLists() {
  556. return Lists.find({ boardId: this._id }, { sort: { sort: 1 } });
  557. },
  558. nullSortLists() {
  559. return Lists.find({
  560. boardId: this._id,
  561. archived: false,
  562. sort: { $eq: null },
  563. });
  564. },
  565. swimlanes() {
  566. return Swimlanes.find(
  567. { boardId: this._id, archived: false },
  568. { sort: { sort: 1 } },
  569. );
  570. },
  571. nextSwimlane(swimlane) {
  572. return Swimlanes.findOne(
  573. {
  574. boardId: this._id,
  575. archived: false,
  576. sort: { $gte: swimlane.sort },
  577. _id: { $ne: swimlane._id },
  578. },
  579. {
  580. sort: { sort: 1 },
  581. },
  582. );
  583. },
  584. nullSortSwimlanes() {
  585. return Swimlanes.find({
  586. boardId: this._id,
  587. archived: false,
  588. sort: { $eq: null },
  589. });
  590. },
  591. hasOvertimeCards() {
  592. const card = Cards.findOne({
  593. isOvertime: true,
  594. boardId: this._id,
  595. archived: false,
  596. });
  597. return card !== undefined;
  598. },
  599. hasSpentTimeCards() {
  600. const card = Cards.findOne({
  601. spentTime: { $gt: 0 },
  602. boardId: this._id,
  603. archived: false,
  604. });
  605. return card !== undefined;
  606. },
  607. activities() {
  608. return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 } });
  609. },
  610. activeMembers() {
  611. return _.where(this.members, { isActive: true });
  612. },
  613. activeAdmins() {
  614. return _.where(this.members, { isActive: true, isAdmin: true });
  615. },
  616. memberUsers() {
  617. return Users.find({ _id: { $in: _.pluck(this.members, 'userId') } });
  618. },
  619. getLabel(name, color) {
  620. return _.findWhere(this.labels, { name, color });
  621. },
  622. getLabelById(labelId) {
  623. return _.findWhere(this.labels, { _id: labelId });
  624. },
  625. labelIndex(labelId) {
  626. return _.pluck(this.labels, '_id').indexOf(labelId);
  627. },
  628. memberIndex(memberId) {
  629. return _.pluck(this.members, 'userId').indexOf(memberId);
  630. },
  631. hasMember(memberId) {
  632. return !!_.findWhere(this.members, { userId: memberId, isActive: true });
  633. },
  634. hasAdmin(memberId) {
  635. return !!_.findWhere(this.members, {
  636. userId: memberId,
  637. isActive: true,
  638. isAdmin: true,
  639. });
  640. },
  641. hasNoComments(memberId) {
  642. return !!_.findWhere(this.members, {
  643. userId: memberId,
  644. isActive: true,
  645. isAdmin: false,
  646. isNoComments: true,
  647. isWorker: false,
  648. });
  649. },
  650. hasCommentOnly(memberId) {
  651. return !!_.findWhere(this.members, {
  652. userId: memberId,
  653. isActive: true,
  654. isAdmin: false,
  655. isCommentOnly: true,
  656. isWorker: false,
  657. });
  658. },
  659. hasWorker(memberId) {
  660. return !!_.findWhere(this.members, {
  661. userId: memberId,
  662. isActive: true,
  663. isAdmin: false,
  664. isCommentOnly: false,
  665. isWorker: true,
  666. });
  667. },
  668. absoluteUrl() {
  669. return FlowRouter.url('board', { id: this._id, slug: this.slug });
  670. },
  671. colorClass() {
  672. return `board-color-${this.color}`;
  673. },
  674. customFields() {
  675. return CustomFields.find(
  676. { boardIds: { $in: [this._id] } },
  677. { sort: { name: 1 } },
  678. );
  679. },
  680. // XXX currently mutations return no value so we have an issue when using addLabel in import
  681. // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
  682. pushLabel(name, color) {
  683. const _id = Random.id(6);
  684. Boards.direct.update(this._id, { $push: { labels: { _id, name, color } } });
  685. return _id;
  686. },
  687. searchBoards(term) {
  688. check(term, Match.OneOf(String, null, undefined));
  689. const query = { boardId: this._id };
  690. query.type = 'cardType-linkedBoard';
  691. query.archived = false;
  692. const projection = { limit: 10, sort: { createdAt: -1 } };
  693. if (term) {
  694. const regex = new RegExp(term, 'i');
  695. query.$or = [{ title: regex }, { description: regex }];
  696. }
  697. return Cards.find(query, projection);
  698. },
  699. searchSwimlanes(term) {
  700. check(term, Match.OneOf(String, null, undefined));
  701. const query = { boardId: this._id };
  702. if (this.isTemplatesBoard()) {
  703. query.type = 'template-swimlane';
  704. query.archived = false;
  705. } else {
  706. query.type = { $nin: ['template-swimlane'] };
  707. }
  708. const projection = { limit: 10, sort: { createdAt: -1 } };
  709. if (term) {
  710. const regex = new RegExp(term, 'i');
  711. query.$or = [{ title: regex }, { description: regex }];
  712. }
  713. return Swimlanes.find(query, projection);
  714. },
  715. searchLists(term) {
  716. check(term, Match.OneOf(String, null, undefined));
  717. const query = { boardId: this._id };
  718. if (this.isTemplatesBoard()) {
  719. query.type = 'template-list';
  720. query.archived = false;
  721. } else {
  722. query.type = { $nin: ['template-list'] };
  723. }
  724. const projection = { limit: 10, sort: { createdAt: -1 } };
  725. if (term) {
  726. const regex = new RegExp(term, 'i');
  727. query.$or = [{ title: regex }, { description: regex }];
  728. }
  729. return Lists.find(query, projection);
  730. },
  731. searchCards(term, excludeLinked) {
  732. check(term, Match.OneOf(String, null, undefined));
  733. const query = { boardId: this._id };
  734. if (excludeLinked) {
  735. query.linkedId = null;
  736. }
  737. if (this.isTemplatesBoard()) {
  738. query.type = 'template-card';
  739. query.archived = false;
  740. } else {
  741. query.type = { $nin: ['template-card'] };
  742. }
  743. const projection = { limit: 10, sort: { createdAt: -1 } };
  744. if (term) {
  745. const regex = new RegExp(term, 'i');
  746. query.$or = [
  747. { title: regex },
  748. { description: regex },
  749. { customFields: { $elemMatch: { value: regex } } },
  750. ];
  751. }
  752. return Cards.find(query, projection);
  753. },
  754. // A board alwasy has another board where it deposits subtasks of thasks
  755. // that belong to itself.
  756. getDefaultSubtasksBoardId() {
  757. if (
  758. this.subtasksDefaultBoardId === null ||
  759. this.subtasksDefaultBoardId === undefined
  760. ) {
  761. this.subtasksDefaultBoardId = Boards.insert({
  762. title: `^${this.title}^`,
  763. permission: this.permission,
  764. members: this.members,
  765. color: this.color,
  766. description: TAPi18n.__('default-subtasks-board', {
  767. board: this.title,
  768. }),
  769. });
  770. Swimlanes.insert({
  771. title: TAPi18n.__('default'),
  772. boardId: this.subtasksDefaultBoardId,
  773. });
  774. Boards.update(this._id, {
  775. $set: {
  776. subtasksDefaultBoardId: this.subtasksDefaultBoardId,
  777. },
  778. });
  779. }
  780. return this.subtasksDefaultBoardId;
  781. },
  782. getDefaultSubtasksBoard() {
  783. return Boards.findOne(this.getDefaultSubtasksBoardId());
  784. },
  785. //Date Settings option such as received date, start date and so on.
  786. getDefaultDateSettingsBoardId() {
  787. if (
  788. this.dateSettingsDefaultBoardId === null ||
  789. this.dateSettingsDefaultBoardId === undefined
  790. ) {
  791. this.dateSettingsDefaultBoardId = Boards.insert({
  792. title: `^${this.title}^`,
  793. permission: this.permission,
  794. members: this.members,
  795. color: this.color,
  796. description: TAPi18n.__('default-dates-board', {
  797. board: this.title,
  798. }),
  799. });
  800. Swimlanes.insert({
  801. title: TAPi18n.__('default'),
  802. boardId: this.dateSettingsDefaultBoardId,
  803. });
  804. Boards.update(this._id, {
  805. $set: {
  806. dateSettingsDefaultBoardId: this.dateSettingsDefaultBoardId,
  807. },
  808. });
  809. }
  810. return this.dateSettingsDefaultBoardId;
  811. },
  812. getDefaultDateSettingsBoard() {
  813. return Boards.findOne(this.getDefaultDateSettingsBoardId());
  814. },
  815. getDefaultSubtasksListId() {
  816. if (
  817. this.subtasksDefaultListId === null ||
  818. this.subtasksDefaultListId === undefined
  819. ) {
  820. this.subtasksDefaultListId = Lists.insert({
  821. title: TAPi18n.__('queue'),
  822. boardId: this._id,
  823. });
  824. this.setSubtasksDefaultListId(this.subtasksDefaultListId);
  825. }
  826. return this.subtasksDefaultListId;
  827. },
  828. getDefaultSubtasksList() {
  829. return Lists.findOne(this.getDefaultSubtasksListId());
  830. },
  831. getDefaultDateSettingsListId() {
  832. if (
  833. this.dateSettingsDefaultListId === null ||
  834. this.dateSettingsDefaultListId === undefined
  835. ) {
  836. this.dateSettingsDefaultListId = Lists.insert({
  837. title: TAPi18n.__('queue'),
  838. boardId: this._id,
  839. });
  840. this.setDateSettingsDefaultListId(this.dateSettingsDefaultListId);
  841. }
  842. return this.dateSettingsDefaultListId;
  843. },
  844. getDefaultDateSettingsList() {
  845. return Lists.findOne(this.getDefaultDateSettingsListId());
  846. },
  847. getDefaultSwimline() {
  848. let result = Swimlanes.findOne({ boardId: this._id });
  849. if (result === undefined) {
  850. Swimlanes.insert({
  851. title: TAPi18n.__('default'),
  852. boardId: this._id,
  853. });
  854. result = Swimlanes.findOne({ boardId: this._id });
  855. }
  856. return result;
  857. },
  858. cardsDueInBetween(start, end) {
  859. return Cards.find({
  860. boardId: this._id,
  861. dueAt: { $gte: start, $lte: end },
  862. });
  863. },
  864. cardsInInterval(start, end) {
  865. return Cards.find({
  866. boardId: this._id,
  867. $or: [
  868. {
  869. startAt: {
  870. $lte: start,
  871. },
  872. endAt: {
  873. $gte: start,
  874. },
  875. },
  876. {
  877. startAt: {
  878. $lte: end,
  879. },
  880. endAt: {
  881. $gte: end,
  882. },
  883. },
  884. {
  885. startAt: {
  886. $gte: start,
  887. },
  888. endAt: {
  889. $lte: end,
  890. },
  891. },
  892. ],
  893. });
  894. },
  895. isTemplateBoard() {
  896. return this.type === 'template-board';
  897. },
  898. isTemplatesBoard() {
  899. return this.type === 'template-container';
  900. },
  901. });
  902. Boards.mutations({
  903. archive() {
  904. return { $set: { archived: true } };
  905. },
  906. restore() {
  907. return { $set: { archived: false } };
  908. },
  909. rename(title) {
  910. return { $set: { title } };
  911. },
  912. setDescription(description) {
  913. return { $set: { description } };
  914. },
  915. setColor(color) {
  916. return { $set: { color } };
  917. },
  918. setVisibility(visibility) {
  919. return { $set: { permission: visibility } };
  920. },
  921. addLabel(name, color) {
  922. // If label with the same name and color already exists we don't want to
  923. // create another one because they would be indistinguishable in the UI
  924. // (they would still have different `_id` but that is not exposed to the
  925. // user).
  926. if (!this.getLabel(name, color)) {
  927. const _id = Random.id(6);
  928. return { $push: { labels: { _id, name, color } } };
  929. }
  930. return {};
  931. },
  932. editLabel(labelId, name, color) {
  933. if (!this.getLabel(name, color)) {
  934. const labelIndex = this.labelIndex(labelId);
  935. return {
  936. $set: {
  937. [`labels.${labelIndex}.name`]: name,
  938. [`labels.${labelIndex}.color`]: color,
  939. },
  940. };
  941. }
  942. return {};
  943. },
  944. removeLabel(labelId) {
  945. return { $pull: { labels: { _id: labelId } } };
  946. },
  947. changeOwnership(fromId, toId) {
  948. const memberIndex = this.memberIndex(fromId);
  949. return {
  950. $set: {
  951. [`members.${memberIndex}.userId`]: toId,
  952. },
  953. };
  954. },
  955. addMember(memberId) {
  956. const memberIndex = this.memberIndex(memberId);
  957. if (memberIndex >= 0) {
  958. return {
  959. $set: {
  960. [`members.${memberIndex}.isActive`]: true,
  961. },
  962. };
  963. }
  964. return {
  965. $push: {
  966. members: {
  967. userId: memberId,
  968. isAdmin: false,
  969. isActive: true,
  970. isNoComments: false,
  971. isCommentOnly: false,
  972. isWorker: false,
  973. },
  974. },
  975. };
  976. },
  977. removeMember(memberId) {
  978. const memberIndex = this.memberIndex(memberId);
  979. // we do not allow the only one admin to be removed
  980. const allowRemove =
  981. !this.members[memberIndex].isAdmin || this.activeAdmins().length > 1;
  982. if (!allowRemove) {
  983. return {
  984. $set: {
  985. [`members.${memberIndex}.isActive`]: true,
  986. },
  987. };
  988. }
  989. return {
  990. $set: {
  991. [`members.${memberIndex}.isActive`]: false,
  992. [`members.${memberIndex}.isAdmin`]: false,
  993. },
  994. };
  995. },
  996. setMemberPermission(
  997. memberId,
  998. isAdmin,
  999. isNoComments,
  1000. isCommentOnly,
  1001. isWorker,
  1002. currentUserId = Meteor.userId(),
  1003. ) {
  1004. const memberIndex = this.memberIndex(memberId);
  1005. // do not allow change permission of self
  1006. if (memberId === currentUserId) {
  1007. isAdmin = this.members[memberIndex].isAdmin;
  1008. }
  1009. return {
  1010. $set: {
  1011. [`members.${memberIndex}.isAdmin`]: isAdmin,
  1012. [`members.${memberIndex}.isNoComments`]: isNoComments,
  1013. [`members.${memberIndex}.isCommentOnly`]: isCommentOnly,
  1014. [`members.${memberIndex}.isWorker`]: isWorker,
  1015. },
  1016. };
  1017. },
  1018. setAllowsSubtasks(allowsSubtasks) {
  1019. return { $set: { allowsSubtasks } };
  1020. },
  1021. setAllowsMembers(allowsMembers) {
  1022. return { $set: { allowsMembers } };
  1023. },
  1024. setAllowsChecklists(allowsChecklists) {
  1025. return { $set: { allowsChecklists } };
  1026. },
  1027. setAllowsAssignee(allowsAssignee) {
  1028. return { $set: { allowsAssignee } };
  1029. },
  1030. setAllowsAssignedBy(allowsAssignedBy) {
  1031. return { $set: { allowsAssignedBy } };
  1032. },
  1033. setAllowsRequestedBy(allowsRequestedBy) {
  1034. return { $set: { allowsRequestedBy } };
  1035. },
  1036. setAllowsAttachments(allowsAttachments) {
  1037. return { $set: { allowsAttachments } };
  1038. },
  1039. setAllowsLabels(allowsLabels) {
  1040. return { $set: { allowsLabels } };
  1041. },
  1042. setAllowsComments(allowsComments) {
  1043. return { $set: { allowsComments } };
  1044. },
  1045. setAllowsDescriptionTitle(allowsDescriptionTitle) {
  1046. return { $set: { allowsDescriptionTitle } };
  1047. },
  1048. setAllowsDescriptionText(allowsDescriptionText) {
  1049. return { $set: { allowsDescriptionText } };
  1050. },
  1051. setAllowsActivities(allowsActivities) {
  1052. return { $set: { allowsActivities } };
  1053. },
  1054. setAllowsReceivedDate(allowsReceivedDate) {
  1055. return { $set: { allowsReceivedDate } };
  1056. },
  1057. setAllowsStartDate(allowsStartDate) {
  1058. return { $set: { allowsStartDate } };
  1059. },
  1060. setAllowsEndDate(allowsEndDate) {
  1061. return { $set: { allowsEndDate } };
  1062. },
  1063. setAllowsDueDate(allowsDueDate) {
  1064. return { $set: { allowsDueDate } };
  1065. },
  1066. setSubtasksDefaultBoardId(subtasksDefaultBoardId) {
  1067. return { $set: { subtasksDefaultBoardId } };
  1068. },
  1069. setSubtasksDefaultListId(subtasksDefaultListId) {
  1070. return { $set: { subtasksDefaultListId } };
  1071. },
  1072. setPresentParentTask(presentParentTask) {
  1073. return { $set: { presentParentTask } };
  1074. },
  1075. move(sortIndex) {
  1076. return { $set: { sort: sortIndex } };
  1077. },
  1078. });
  1079. function boardRemover(userId, doc) {
  1080. [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach(
  1081. element => {
  1082. element.remove({ boardId: doc._id });
  1083. },
  1084. );
  1085. }
  1086. Boards.userSearch = (
  1087. userId,
  1088. selector = {},
  1089. projection = {},
  1090. includeArchived = false,
  1091. ) => {
  1092. if (!includeArchived) {
  1093. selector.archived = false;
  1094. }
  1095. selector.$or = [
  1096. { permission: 'public' },
  1097. { members: { $elemMatch: { userId, isActive: true } } },
  1098. ];
  1099. return Boards.find(selector, projection);
  1100. };
  1101. Boards.userBoards = (userId, includeArchived = false, selector = {}) => {
  1102. if (!includeArchived) {
  1103. selector = {
  1104. archived: false,
  1105. };
  1106. }
  1107. selector.$or = [
  1108. { permission: 'public' },
  1109. { members: { $elemMatch: { userId, isActive: true } } },
  1110. ];
  1111. return Boards.find(selector);
  1112. };
  1113. Boards.userBoardIds = (userId, includeArchived = false, selector = {}) => {
  1114. return Boards.userBoards(userId, includeArchived, selector).map(board => {
  1115. return board._id;
  1116. });
  1117. };
  1118. if (Meteor.isServer) {
  1119. Boards.allow({
  1120. insert: Meteor.userId,
  1121. update: allowIsBoardAdmin,
  1122. remove: allowIsBoardAdmin,
  1123. fetch: ['members'],
  1124. });
  1125. // All logged in users are allowed to reorder boards by dragging at All Boards page and Public Boards page.
  1126. Boards.allow({
  1127. update(userId, board, fieldNames) {
  1128. return _.contains(fieldNames, 'sort');
  1129. },
  1130. fetch: [],
  1131. });
  1132. // The number of users that have starred this board is managed by trusted code
  1133. // and the user is not allowed to update it
  1134. Boards.deny({
  1135. update(userId, board, fieldNames) {
  1136. return _.contains(fieldNames, 'stars');
  1137. },
  1138. fetch: [],
  1139. });
  1140. // We can't remove a member if it is the last administrator
  1141. Boards.deny({
  1142. update(userId, doc, fieldNames, modifier) {
  1143. if (!_.contains(fieldNames, 'members')) return false;
  1144. // We only care in case of a $pull operation, ie remove a member
  1145. if (!_.isObject(modifier.$pull && modifier.$pull.members)) return false;
  1146. // If there is more than one admin, it's ok to remove anyone
  1147. const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true })
  1148. .length;
  1149. if (nbAdmins > 1) return false;
  1150. // If all the previous conditions were verified, we can't remove
  1151. // a user if it's an admin
  1152. const removedMemberId = modifier.$pull.members.userId;
  1153. return Boolean(
  1154. _.findWhere(doc.members, {
  1155. userId: removedMemberId,
  1156. isAdmin: true,
  1157. }),
  1158. );
  1159. },
  1160. fetch: ['members'],
  1161. });
  1162. Meteor.methods({
  1163. quitBoard(boardId) {
  1164. check(boardId, String);
  1165. const board = Boards.findOne(boardId);
  1166. if (board) {
  1167. const userId = Meteor.userId();
  1168. const index = board.memberIndex(userId);
  1169. if (index >= 0) {
  1170. board.removeMember(userId);
  1171. return true;
  1172. } else throw new Meteor.Error('error-board-notAMember');
  1173. } else throw new Meteor.Error('error-board-doesNotExist');
  1174. },
  1175. acceptInvite(boardId) {
  1176. check(boardId, String);
  1177. const board = Boards.findOne(boardId);
  1178. if (!board) {
  1179. throw new Meteor.Error('error-board-doesNotExist');
  1180. }
  1181. Meteor.users.update(Meteor.userId(), {
  1182. $pull: {
  1183. 'profile.invitedBoards': boardId,
  1184. },
  1185. });
  1186. },
  1187. });
  1188. Meteor.methods({
  1189. archiveBoard(boardId) {
  1190. check(boardId, String);
  1191. const board = Boards.findOne(boardId);
  1192. if (board) {
  1193. const userId = Meteor.userId();
  1194. const index = board.memberIndex(userId);
  1195. if (index >= 0) {
  1196. board.archive();
  1197. return true;
  1198. } else throw new Meteor.Error('error-board-notAMember');
  1199. } else throw new Meteor.Error('error-board-doesNotExist');
  1200. },
  1201. });
  1202. }
  1203. // Insert new board at last position in sort order.
  1204. Boards.before.insert((userId, doc) => {
  1205. const lastBoard = Boards.findOne(
  1206. { sort: { $exists: true } },
  1207. { sort: { sort: -1 } },
  1208. );
  1209. if (lastBoard && typeof lastBoard.sort !== 'undefined') {
  1210. doc.sort = lastBoard.sort + 1;
  1211. }
  1212. });
  1213. if (Meteor.isServer) {
  1214. // Let MongoDB ensure that a member is not included twice in the same board
  1215. Meteor.startup(() => {
  1216. Boards._collection._ensureIndex({ modifiedAt: -1 });
  1217. Boards._collection._ensureIndex(
  1218. {
  1219. _id: 1,
  1220. 'members.userId': 1,
  1221. },
  1222. { unique: true },
  1223. );
  1224. Boards._collection._ensureIndex({ 'members.userId': 1 });
  1225. });
  1226. // Genesis: the first activity of the newly created board
  1227. Boards.after.insert((userId, doc) => {
  1228. Activities.insert({
  1229. userId,
  1230. type: 'board',
  1231. activityTypeId: doc._id,
  1232. activityType: 'createBoard',
  1233. boardId: doc._id,
  1234. });
  1235. });
  1236. // If the user remove one label from a board, we cant to remove reference of
  1237. // this label in any card of this board.
  1238. Boards.after.update((userId, doc, fieldNames, modifier) => {
  1239. if (
  1240. !_.contains(fieldNames, 'labels') ||
  1241. !modifier.$pull ||
  1242. !modifier.$pull.labels ||
  1243. !modifier.$pull.labels._id
  1244. ) {
  1245. return;
  1246. }
  1247. const removedLabelId = modifier.$pull.labels._id;
  1248. Cards.update(
  1249. { boardId: doc._id },
  1250. {
  1251. $pull: {
  1252. labelIds: removedLabelId,
  1253. },
  1254. },
  1255. { multi: true },
  1256. );
  1257. });
  1258. const foreachRemovedMember = (doc, modifier, callback) => {
  1259. Object.keys(modifier).forEach(set => {
  1260. if (modifier[set] !== false) {
  1261. return;
  1262. }
  1263. const parts = set.split('.');
  1264. if (
  1265. parts.length === 3 &&
  1266. parts[0] === 'members' &&
  1267. parts[2] === 'isActive'
  1268. ) {
  1269. callback(doc.members[parts[1]].userId);
  1270. }
  1271. });
  1272. };
  1273. // Remove a member from all objects of the board before leaving the board
  1274. Boards.before.update((userId, doc, fieldNames, modifier) => {
  1275. if (!_.contains(fieldNames, 'members')) {
  1276. return;
  1277. }
  1278. if (modifier.$set) {
  1279. const boardId = doc._id;
  1280. foreachRemovedMember(doc, modifier.$set, memberId => {
  1281. Cards.update(
  1282. { boardId },
  1283. {
  1284. $pull: {
  1285. members: memberId,
  1286. watchers: memberId,
  1287. },
  1288. },
  1289. { multi: true },
  1290. );
  1291. Lists.update(
  1292. { boardId },
  1293. {
  1294. $pull: {
  1295. watchers: memberId,
  1296. },
  1297. },
  1298. { multi: true },
  1299. );
  1300. const board = Boards._transform(doc);
  1301. board.setWatcher(memberId, false);
  1302. // Remove board from users starred list
  1303. if (!board.isPublic()) {
  1304. Users.update(memberId, {
  1305. $pull: {
  1306. 'profile.starredBoards': boardId,
  1307. },
  1308. });
  1309. }
  1310. });
  1311. }
  1312. });
  1313. Boards.before.remove((userId, doc) => {
  1314. boardRemover(userId, doc);
  1315. // Add removeBoard activity to keep it
  1316. Activities.insert({
  1317. userId,
  1318. type: 'board',
  1319. activityTypeId: doc._id,
  1320. activityType: 'removeBoard',
  1321. boardId: doc._id,
  1322. });
  1323. });
  1324. // Add a new activity if we add or remove a member to the board
  1325. Boards.after.update((userId, doc, fieldNames, modifier) => {
  1326. if (!_.contains(fieldNames, 'members')) {
  1327. return;
  1328. }
  1329. // Say hello to the new member
  1330. if (modifier.$push && modifier.$push.members) {
  1331. const memberId = modifier.$push.members.userId;
  1332. Activities.insert({
  1333. userId,
  1334. memberId,
  1335. type: 'member',
  1336. activityType: 'addBoardMember',
  1337. boardId: doc._id,
  1338. });
  1339. }
  1340. // Say goodbye to the former member
  1341. if (modifier.$set) {
  1342. foreachRemovedMember(doc, modifier.$set, memberId => {
  1343. Activities.insert({
  1344. userId,
  1345. memberId,
  1346. type: 'member',
  1347. activityType: 'removeBoardMember',
  1348. boardId: doc._id,
  1349. });
  1350. });
  1351. }
  1352. });
  1353. }
  1354. //BOARDS REST API
  1355. if (Meteor.isServer) {
  1356. /**
  1357. * @operation get_boards_from_user
  1358. * @summary Get all boards attached to a user
  1359. *
  1360. * @param {string} userId the ID of the user to retrieve the data
  1361. * @return_type [{_id: string,
  1362. title: string}]
  1363. */
  1364. JsonRoutes.add('GET', '/api/users/:userId/boards', function(req, res) {
  1365. try {
  1366. Authentication.checkLoggedIn(req.userId);
  1367. const paramUserId = req.params.userId;
  1368. // A normal user should be able to see their own boards,
  1369. // admins can access boards of any user
  1370. Authentication.checkAdminOrCondition(
  1371. req.userId,
  1372. req.userId === paramUserId,
  1373. );
  1374. const data = Boards.find(
  1375. {
  1376. archived: false,
  1377. 'members.userId': paramUserId,
  1378. },
  1379. {
  1380. sort: { sort: 1 /* boards default sorting */ },
  1381. },
  1382. ).map(function(board) {
  1383. return {
  1384. _id: board._id,
  1385. title: board.title,
  1386. };
  1387. });
  1388. JsonRoutes.sendResult(res, { code: 200, data });
  1389. } catch (error) {
  1390. JsonRoutes.sendResult(res, {
  1391. code: 200,
  1392. data: error,
  1393. });
  1394. }
  1395. });
  1396. /**
  1397. * @operation get_public_boards
  1398. * @summary Get all public boards
  1399. *
  1400. * @return_type [{_id: string,
  1401. title: string}]
  1402. */
  1403. JsonRoutes.add('GET', '/api/boards', function(req, res) {
  1404. try {
  1405. Authentication.checkUserId(req.userId);
  1406. JsonRoutes.sendResult(res, {
  1407. code: 200,
  1408. data: Boards.find(
  1409. { permission: 'public' },
  1410. {
  1411. sort: { sort: 1 /* boards default sorting */ },
  1412. },
  1413. ).map(function(doc) {
  1414. return {
  1415. _id: doc._id,
  1416. title: doc.title,
  1417. };
  1418. }),
  1419. });
  1420. } catch (error) {
  1421. JsonRoutes.sendResult(res, {
  1422. code: 200,
  1423. data: error,
  1424. });
  1425. }
  1426. });
  1427. /**
  1428. * @operation get_board
  1429. * @summary Get the board with that particular ID
  1430. *
  1431. * @param {string} boardId the ID of the board to retrieve the data
  1432. * @return_type Boards
  1433. */
  1434. JsonRoutes.add('GET', '/api/boards/:boardId', function(req, res) {
  1435. try {
  1436. const id = req.params.boardId;
  1437. Authentication.checkBoardAccess(req.userId, id);
  1438. JsonRoutes.sendResult(res, {
  1439. code: 200,
  1440. data: Boards.findOne({ _id: id }),
  1441. });
  1442. } catch (error) {
  1443. JsonRoutes.sendResult(res, {
  1444. code: 200,
  1445. data: error,
  1446. });
  1447. }
  1448. });
  1449. /**
  1450. * @operation new_board
  1451. * @summary Create a board
  1452. *
  1453. * @description This allows to create a board.
  1454. *
  1455. * The color has to be chosen between `belize`, `nephritis`, `pomegranate`,
  1456. * `pumpkin`, `wisteria`, `moderatepink`, `strongcyan`,
  1457. * `limegreen`, `midnight`, `dark`, `relax`, `corteza`:
  1458. *
  1459. * <img src="https://wekan.github.io/board-colors.png" width="40%" alt="Wekan logo" />
  1460. *
  1461. * @param {string} title the new title of the board
  1462. * @param {string} owner "ABCDE12345" <= User ID in Wekan.
  1463. * (Not username or email)
  1464. * @param {boolean} [isAdmin] is the owner an admin of the board (default true)
  1465. * @param {boolean} [isActive] is the board active (default true)
  1466. * @param {boolean} [isNoComments] disable comments (default false)
  1467. * @param {boolean} [isCommentOnly] only enable comments (default false)
  1468. * @param {boolean} [isWorker] only move cards, assign himself to card and comment (default false)
  1469. * @param {string} [permission] "private" board <== Set to "public" if you
  1470. * want public Wekan board
  1471. * @param {string} [color] the color of the board
  1472. *
  1473. * @return_type {_id: string,
  1474. defaultSwimlaneId: string}
  1475. */
  1476. JsonRoutes.add('POST', '/api/boards', function(req, res) {
  1477. try {
  1478. Authentication.checkUserId(req.userId);
  1479. const id = Boards.insert({
  1480. title: req.body.title,
  1481. members: [
  1482. {
  1483. userId: req.body.owner,
  1484. isAdmin: req.body.isAdmin || true,
  1485. isActive: req.body.isActive || true,
  1486. isNoComments: req.body.isNoComments || false,
  1487. isCommentOnly: req.body.isCommentOnly || false,
  1488. isWorker: req.body.isWorker || false,
  1489. },
  1490. ],
  1491. permission: req.body.permission || 'private',
  1492. color: req.body.color || 'belize',
  1493. });
  1494. const swimlaneId = Swimlanes.insert({
  1495. title: TAPi18n.__('default'),
  1496. boardId: id,
  1497. });
  1498. JsonRoutes.sendResult(res, {
  1499. code: 200,
  1500. data: {
  1501. _id: id,
  1502. defaultSwimlaneId: swimlaneId,
  1503. },
  1504. });
  1505. } catch (error) {
  1506. JsonRoutes.sendResult(res, {
  1507. code: 200,
  1508. data: error,
  1509. });
  1510. }
  1511. });
  1512. /**
  1513. * @operation delete_board
  1514. * @summary Delete a board
  1515. *
  1516. * @param {string} boardId the ID of the board
  1517. */
  1518. JsonRoutes.add('DELETE', '/api/boards/:boardId', function(req, res) {
  1519. try {
  1520. Authentication.checkUserId(req.userId);
  1521. const id = req.params.boardId;
  1522. Boards.remove({ _id: id });
  1523. JsonRoutes.sendResult(res, {
  1524. code: 200,
  1525. data: {
  1526. _id: id,
  1527. },
  1528. });
  1529. } catch (error) {
  1530. JsonRoutes.sendResult(res, {
  1531. code: 200,
  1532. data: error,
  1533. });
  1534. }
  1535. });
  1536. /**
  1537. * @operation add_board_label
  1538. * @summary Add a label to a board
  1539. *
  1540. * @description If the board doesn't have the name/color label, this function
  1541. * adds the label to the board.
  1542. *
  1543. * @param {string} boardId the board
  1544. * @param {string} color the color of the new label
  1545. * @param {string} name the name of the new label
  1546. *
  1547. * @return_type string
  1548. */
  1549. JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function(req, res) {
  1550. Authentication.checkUserId(req.userId);
  1551. const id = req.params.boardId;
  1552. try {
  1553. if (req.body.hasOwnProperty('label')) {
  1554. const board = Boards.findOne({ _id: id });
  1555. const color = req.body.label.color;
  1556. const name = req.body.label.name;
  1557. const labelId = Random.id(6);
  1558. if (!board.getLabel(name, color)) {
  1559. Boards.direct.update(
  1560. { _id: id },
  1561. { $push: { labels: { _id: labelId, name, color } } },
  1562. );
  1563. JsonRoutes.sendResult(res, {
  1564. code: 200,
  1565. data: labelId,
  1566. });
  1567. } else {
  1568. JsonRoutes.sendResult(res, {
  1569. code: 200,
  1570. });
  1571. }
  1572. }
  1573. } catch (error) {
  1574. JsonRoutes.sendResult(res, {
  1575. data: error,
  1576. });
  1577. }
  1578. });
  1579. /**
  1580. * @operation set_board_member_permission
  1581. * @tag Users
  1582. * @summary Change the permission of a member of a board
  1583. *
  1584. * @param {string} boardId the ID of the board that we are changing
  1585. * @param {string} memberId the ID of the user to change permissions
  1586. * @param {boolean} isAdmin admin capability
  1587. * @param {boolean} isNoComments NoComments capability
  1588. * @param {boolean} isCommentOnly CommentsOnly capability
  1589. * @param {boolean} isWorker Worker capability
  1590. */
  1591. JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function(
  1592. req,
  1593. res,
  1594. ) {
  1595. try {
  1596. const boardId = req.params.boardId;
  1597. const memberId = req.params.memberId;
  1598. const { isAdmin, isNoComments, isCommentOnly, isWorker } = req.body;
  1599. Authentication.checkBoardAccess(req.userId, boardId);
  1600. const board = Boards.findOne({ _id: boardId });
  1601. function isTrue(data) {
  1602. try {
  1603. return data.toLowerCase() === 'true';
  1604. } catch (error) {
  1605. return data;
  1606. }
  1607. }
  1608. const query = board.setMemberPermission(
  1609. memberId,
  1610. isTrue(isAdmin),
  1611. isTrue(isNoComments),
  1612. isTrue(isCommentOnly),
  1613. isTrue(isWorker),
  1614. req.userId,
  1615. );
  1616. JsonRoutes.sendResult(res, {
  1617. code: 200,
  1618. data: query,
  1619. });
  1620. } catch (error) {
  1621. JsonRoutes.sendResult(res, {
  1622. code: 200,
  1623. data: error,
  1624. });
  1625. }
  1626. });
  1627. //ATTACHMENTS REST API
  1628. /**
  1629. * @operation get_board_attachments
  1630. * @summary Get the list of attachments of a board
  1631. *
  1632. * @param {string} boardId the board ID
  1633. * @return_type [{attachmentId: string,
  1634. * attachmentName: string,
  1635. * attachmentType: string,
  1636. * cardId: string,
  1637. * listId: string,
  1638. * swimlaneId: string}]
  1639. */
  1640. JsonRoutes.add('GET', '/api/boards/:boardId/attachments', function(req, res) {
  1641. const paramBoardId = req.params.boardId;
  1642. Authentication.checkBoardAccess(req.userId, paramBoardId);
  1643. JsonRoutes.sendResult(res, {
  1644. code: 200,
  1645. data: Attachments.files
  1646. .find({ boardId: paramBoardId }, { fields: { boardId: 0 } })
  1647. .map(function(doc) {
  1648. return {
  1649. attachmentId: doc._id,
  1650. attachmentName: doc.original.name,
  1651. attachmentType: doc.original.type,
  1652. url: FlowRouter.url(doc.url()),
  1653. urlDownload: `${FlowRouter.url(doc.url())}?download=true&token=`,
  1654. cardId: doc.cardId,
  1655. listId: doc.listId,
  1656. swimlaneId: doc.swimlaneId,
  1657. };
  1658. }),
  1659. });
  1660. });
  1661. }
  1662. export default Boards;