boards.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986
  1. Boards = new Mongo.Collection('boards');
  2. Boards.attachSchema(new SimpleSchema({
  3. title: {
  4. type: String,
  5. },
  6. slug: {
  7. type: String,
  8. autoValue() { // eslint-disable-line consistent-return
  9. // XXX We need to improve slug management. Only the id should be necessary
  10. // to identify a board in the code.
  11. // XXX If the board title is updated, the slug should also be updated.
  12. // In some cases (Chinese and Japanese for instance) the `getSlug` function
  13. // return an empty string. This is causes bugs in our application so we set
  14. // a default slug in this case.
  15. if (this.isInsert && !this.isSet) {
  16. let slug = 'board';
  17. const title = this.field('title');
  18. if (title.isSet) {
  19. slug = getSlug(title.value) || slug;
  20. }
  21. return slug;
  22. }
  23. },
  24. },
  25. archived: {
  26. type: Boolean,
  27. autoValue() { // eslint-disable-line consistent-return
  28. if (this.isInsert && !this.isSet) {
  29. return false;
  30. }
  31. },
  32. },
  33. createdAt: {
  34. type: Date,
  35. autoValue() { // eslint-disable-line consistent-return
  36. if (this.isInsert) {
  37. return new Date();
  38. } else {
  39. this.unset();
  40. }
  41. },
  42. },
  43. // XXX Inconsistent field naming
  44. modifiedAt: {
  45. type: Date,
  46. optional: true,
  47. autoValue() { // eslint-disable-line consistent-return
  48. if (this.isUpdate) {
  49. return new Date();
  50. } else {
  51. this.unset();
  52. }
  53. },
  54. },
  55. // De-normalized number of users that have starred this board
  56. stars: {
  57. type: Number,
  58. autoValue() { // eslint-disable-line consistent-return
  59. if (this.isInsert) {
  60. return 0;
  61. }
  62. },
  63. },
  64. // De-normalized label system
  65. 'labels': {
  66. type: [Object],
  67. autoValue() { // eslint-disable-line consistent-return
  68. if (this.isInsert && !this.isSet) {
  69. const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
  70. const defaultLabelsColors = _.clone(colors).splice(0, 6);
  71. return defaultLabelsColors.map((color) => ({
  72. color,
  73. _id: Random.id(6),
  74. name: '',
  75. }));
  76. }
  77. },
  78. },
  79. 'labels.$._id': {
  80. // We don't specify that this field must be unique in the board because that
  81. // will cause performance penalties and is not necessary since this field is
  82. // always set on the server.
  83. // XXX Actually if we create a new label, the `_id` is set on the client
  84. // without being overwritten by the server, could it be a problem?
  85. type: String,
  86. },
  87. 'labels.$.name': {
  88. type: String,
  89. optional: true,
  90. },
  91. 'labels.$.color': {
  92. type: String,
  93. allowedValues: [
  94. 'green', 'yellow', 'orange', 'red', 'purple',
  95. 'blue', 'sky', 'lime', 'pink', 'black',
  96. 'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen',
  97. 'slateblue', 'magenta', 'gold', 'navy', 'gray',
  98. 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
  99. ],
  100. },
  101. // XXX We might want to maintain more informations under the member sub-
  102. // documents like de-normalized meta-data (the date the member joined the
  103. // board, the number of contributions, etc.).
  104. 'members': {
  105. type: [Object],
  106. autoValue() { // eslint-disable-line consistent-return
  107. if (this.isInsert && !this.isSet) {
  108. return [{
  109. userId: this.userId,
  110. isAdmin: true,
  111. isActive: true,
  112. isNoComments: false,
  113. isCommentOnly: false,
  114. }];
  115. }
  116. },
  117. },
  118. 'members.$.userId': {
  119. type: String,
  120. },
  121. 'members.$.isAdmin': {
  122. type: Boolean,
  123. },
  124. 'members.$.isActive': {
  125. type: Boolean,
  126. },
  127. 'members.$.isNoComments': {
  128. type: Boolean,
  129. },
  130. 'members.$.isCommentOnly': {
  131. type: Boolean,
  132. },
  133. permission: {
  134. type: String,
  135. allowedValues: ['public', 'private'],
  136. },
  137. color: {
  138. type: String,
  139. allowedValues: [
  140. 'belize',
  141. 'nephritis',
  142. 'pomegranate',
  143. 'pumpkin',
  144. 'wisteria',
  145. 'midnight',
  146. ],
  147. autoValue() { // eslint-disable-line consistent-return
  148. if (this.isInsert && !this.isSet) {
  149. return Boards.simpleSchema()._schema.color.allowedValues[0];
  150. }
  151. },
  152. },
  153. description: {
  154. type: String,
  155. optional: true,
  156. },
  157. subtasksDefaultBoardId: {
  158. type: String,
  159. optional: true,
  160. defaultValue: null,
  161. },
  162. subtasksDefaultListId: {
  163. type: String,
  164. optional: true,
  165. defaultValue: null,
  166. },
  167. allowsSubtasks: {
  168. type: Boolean,
  169. defaultValue: true,
  170. },
  171. presentParentTask: {
  172. type: String,
  173. allowedValues: [
  174. 'prefix-with-full-path',
  175. 'prefix-with-parent',
  176. 'subtext-with-full-path',
  177. 'subtext-with-parent',
  178. 'no-parent',
  179. ],
  180. optional: true,
  181. defaultValue: 'no-parent',
  182. },
  183. startAt: {
  184. type: Date,
  185. optional: true,
  186. },
  187. dueAt: {
  188. type: Date,
  189. optional: true,
  190. },
  191. endAt: {
  192. type: Date,
  193. optional: true,
  194. },
  195. spentTime: {
  196. type: Number,
  197. decimal: true,
  198. optional: true,
  199. },
  200. isOvertime: {
  201. type: Boolean,
  202. defaultValue: false,
  203. optional: true,
  204. },
  205. }));
  206. Boards.helpers({
  207. /**
  208. * Is supplied user authorized to view this board?
  209. */
  210. isVisibleBy(user) {
  211. if (this.isPublic()) {
  212. // public boards are visible to everyone
  213. return true;
  214. } else {
  215. // otherwise you have to be logged-in and active member
  216. return user && this.isActiveMember(user._id);
  217. }
  218. },
  219. /**
  220. * Is the user one of the active members of the board?
  221. *
  222. * @param userId
  223. * @returns {boolean} the member that matches, or undefined/false
  224. */
  225. isActiveMember(userId) {
  226. if (userId) {
  227. return this.members.find((member) => (member.userId === userId && member.isActive));
  228. } else {
  229. return false;
  230. }
  231. },
  232. isPublic() {
  233. return this.permission === 'public';
  234. },
  235. cards() {
  236. return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } });
  237. },
  238. lists() {
  239. return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
  240. },
  241. swimlanes() {
  242. return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
  243. },
  244. hasOvertimeCards(){
  245. const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
  246. return card !== undefined;
  247. },
  248. hasSpentTimeCards(){
  249. const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} );
  250. return card !== undefined;
  251. },
  252. activities() {
  253. return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 } });
  254. },
  255. activeMembers() {
  256. return _.where(this.members, { isActive: true });
  257. },
  258. activeAdmins() {
  259. return _.where(this.members, { isActive: true, isAdmin: true });
  260. },
  261. memberUsers() {
  262. return Users.find({ _id: { $in: _.pluck(this.members, 'userId') } });
  263. },
  264. getMember(id) {
  265. return _.findWhere(this.members, { userId: id });
  266. },
  267. getLabel(name, color) {
  268. return _.findWhere(this.labels, { name, color });
  269. },
  270. getLabelById(labelId){
  271. return _.findWhere(this.labels, { _id: labelId });
  272. },
  273. labelIndex(labelId) {
  274. return _.pluck(this.labels, '_id').indexOf(labelId);
  275. },
  276. memberIndex(memberId) {
  277. return _.pluck(this.members, 'userId').indexOf(memberId);
  278. },
  279. hasMember(memberId) {
  280. return !!_.findWhere(this.members, { userId: memberId, isActive: true });
  281. },
  282. hasAdmin(memberId) {
  283. return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true });
  284. },
  285. hasNoComments(memberId) {
  286. return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isNoComments: true });
  287. },
  288. hasCommentOnly(memberId) {
  289. return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isCommentOnly: true });
  290. },
  291. absoluteUrl() {
  292. return FlowRouter.url('board', { id: this._id, slug: this.slug });
  293. },
  294. colorClass() {
  295. return `board-color-${this.color}`;
  296. },
  297. customFields() {
  298. return CustomFields.find({ boardId: this._id }, { sort: { name: 1 } });
  299. },
  300. // XXX currently mutations return no value so we have an issue when using addLabel in import
  301. // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
  302. pushLabel(name, color) {
  303. const _id = Random.id(6);
  304. Boards.direct.update(this._id, { $push: { labels: { _id, name, color } } });
  305. return _id;
  306. },
  307. searchCards(term, excludeLinked) {
  308. check(term, Match.OneOf(String, null, undefined));
  309. const query = { boardId: this._id };
  310. if (excludeLinked) {
  311. query.linkedId = null;
  312. }
  313. const projection = { limit: 10, sort: { createdAt: -1 } };
  314. if (term) {
  315. const regex = new RegExp(term, 'i');
  316. query.$or = [
  317. { title: regex },
  318. { description: regex },
  319. ];
  320. }
  321. return Cards.find(query, projection);
  322. },
  323. // A board alwasy has another board where it deposits subtasks of thasks
  324. // that belong to itself.
  325. getDefaultSubtasksBoardId() {
  326. if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) {
  327. this.subtasksDefaultBoardId = Boards.insert({
  328. title: `^${this.title}^`,
  329. permission: this.permission,
  330. members: this.members,
  331. color: this.color,
  332. description: TAPi18n.__('default-subtasks-board', {board: this.title}),
  333. });
  334. Swimlanes.insert({
  335. title: TAPi18n.__('default'),
  336. boardId: this.subtasksDefaultBoardId,
  337. });
  338. Boards.update(this._id, {$set: {
  339. subtasksDefaultBoardId: this.subtasksDefaultBoardId,
  340. }});
  341. }
  342. return this.subtasksDefaultBoardId;
  343. },
  344. getDefaultSubtasksBoard() {
  345. return Boards.findOne(this.getDefaultSubtasksBoardId());
  346. },
  347. getDefaultSubtasksListId() {
  348. if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) {
  349. this.subtasksDefaultListId = Lists.insert({
  350. title: TAPi18n.__('queue'),
  351. boardId: this._id,
  352. });
  353. Boards.update(this._id, {$set: {
  354. subtasksDefaultListId: this.subtasksDefaultListId,
  355. }});
  356. }
  357. return this.subtasksDefaultListId;
  358. },
  359. getDefaultSubtasksList() {
  360. return Lists.findOne(this.getDefaultSubtasksListId());
  361. },
  362. getDefaultSwimline() {
  363. let result = Swimlanes.findOne({boardId: this._id});
  364. if (result === undefined) {
  365. Swimlanes.insert({
  366. title: TAPi18n.__('default'),
  367. boardId: this._id,
  368. });
  369. result = Swimlanes.findOne({boardId: this._id});
  370. }
  371. return result;
  372. },
  373. cardsInInterval(start, end) {
  374. return Cards.find({
  375. boardId: this._id,
  376. $or: [
  377. {
  378. startAt: {
  379. $lte: start,
  380. }, endAt: {
  381. $gte: start,
  382. },
  383. }, {
  384. startAt: {
  385. $lte: end,
  386. }, endAt: {
  387. $gte: end,
  388. },
  389. }, {
  390. startAt: {
  391. $gte: start,
  392. }, endAt: {
  393. $lte: end,
  394. },
  395. },
  396. ],
  397. });
  398. },
  399. });
  400. Boards.mutations({
  401. archive() {
  402. return { $set: { archived: true } };
  403. },
  404. restore() {
  405. return { $set: { archived: false } };
  406. },
  407. rename(title) {
  408. return { $set: { title } };
  409. },
  410. setDescription(description) {
  411. return { $set: { description } };
  412. },
  413. setColor(color) {
  414. return { $set: { color } };
  415. },
  416. setVisibility(visibility) {
  417. return { $set: { permission: visibility } };
  418. },
  419. addLabel(name, color) {
  420. // If label with the same name and color already exists we don't want to
  421. // create another one because they would be indistinguishable in the UI
  422. // (they would still have different `_id` but that is not exposed to the
  423. // user).
  424. if (!this.getLabel(name, color)) {
  425. const _id = Random.id(6);
  426. return { $push: { labels: { _id, name, color } } };
  427. }
  428. return {};
  429. },
  430. editLabel(labelId, name, color) {
  431. if (!this.getLabel(name, color)) {
  432. const labelIndex = this.labelIndex(labelId);
  433. return {
  434. $set: {
  435. [`labels.${labelIndex}.name`]: name,
  436. [`labels.${labelIndex}.color`]: color,
  437. },
  438. };
  439. }
  440. return {};
  441. },
  442. removeLabel(labelId) {
  443. return { $pull: { labels: { _id: labelId } } };
  444. },
  445. changeOwnership(fromId, toId) {
  446. const memberIndex = this.memberIndex(fromId);
  447. return {
  448. $set: {
  449. [`members.${memberIndex}.userId`]: toId,
  450. },
  451. };
  452. },
  453. addMember(memberId) {
  454. const memberIndex = this.memberIndex(memberId);
  455. if (memberIndex >= 0) {
  456. return {
  457. $set: {
  458. [`members.${memberIndex}.isActive`]: true,
  459. },
  460. };
  461. }
  462. return {
  463. $push: {
  464. members: {
  465. userId: memberId,
  466. isAdmin: false,
  467. isActive: true,
  468. isNoComments: false,
  469. isCommentOnly: false,
  470. },
  471. },
  472. };
  473. },
  474. removeMember(memberId) {
  475. const memberIndex = this.memberIndex(memberId);
  476. // we do not allow the only one admin to be removed
  477. const allowRemove = (!this.members[memberIndex].isAdmin) || (this.activeAdmins().length > 1);
  478. if (!allowRemove) {
  479. return {
  480. $set: {
  481. [`members.${memberIndex}.isActive`]: true,
  482. },
  483. };
  484. }
  485. return {
  486. $set: {
  487. [`members.${memberIndex}.isActive`]: false,
  488. [`members.${memberIndex}.isAdmin`]: false,
  489. },
  490. };
  491. },
  492. setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly, currentUserId = Meteor.userId()) {
  493. const memberIndex = this.memberIndex(memberId);
  494. // do not allow change permission of self
  495. if (memberId === currentUserId) {
  496. isAdmin = this.members[memberIndex].isAdmin;
  497. }
  498. return {
  499. $set: {
  500. [`members.${memberIndex}.isAdmin`]: isAdmin,
  501. [`members.${memberIndex}.isNoComments`]: isNoComments,
  502. [`members.${memberIndex}.isCommentOnly`]: isCommentOnly,
  503. },
  504. };
  505. },
  506. setAllowsSubtasks(allowsSubtasks) {
  507. return { $set: { allowsSubtasks } };
  508. },
  509. setSubtasksDefaultBoardId(subtasksDefaultBoardId) {
  510. return { $set: { subtasksDefaultBoardId } };
  511. },
  512. setSubtasksDefaultListId(subtasksDefaultListId) {
  513. return { $set: { subtasksDefaultListId } };
  514. },
  515. setPresentParentTask(presentParentTask) {
  516. return { $set: { presentParentTask } };
  517. },
  518. });
  519. if (Meteor.isServer) {
  520. Boards.allow({
  521. insert: Meteor.userId,
  522. update: allowIsBoardAdmin,
  523. remove: allowIsBoardAdmin,
  524. fetch: ['members'],
  525. });
  526. // The number of users that have starred this board is managed by trusted code
  527. // and the user is not allowed to update it
  528. Boards.deny({
  529. update(userId, board, fieldNames) {
  530. return _.contains(fieldNames, 'stars');
  531. },
  532. fetch: [],
  533. });
  534. // We can't remove a member if it is the last administrator
  535. Boards.deny({
  536. update(userId, doc, fieldNames, modifier) {
  537. if (!_.contains(fieldNames, 'members'))
  538. return false;
  539. // We only care in case of a $pull operation, ie remove a member
  540. if (!_.isObject(modifier.$pull && modifier.$pull.members))
  541. return false;
  542. // If there is more than one admin, it's ok to remove anyone
  543. const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true }).length;
  544. if (nbAdmins > 1)
  545. return false;
  546. // If all the previous conditions were verified, we can't remove
  547. // a user if it's an admin
  548. const removedMemberId = modifier.$pull.members.userId;
  549. return Boolean(_.findWhere(doc.members, {
  550. userId: removedMemberId,
  551. isAdmin: true,
  552. }));
  553. },
  554. fetch: ['members'],
  555. });
  556. Meteor.methods({
  557. quitBoard(boardId) {
  558. check(boardId, String);
  559. const board = Boards.findOne(boardId);
  560. if (board) {
  561. const userId = Meteor.userId();
  562. const index = board.memberIndex(userId);
  563. if (index >= 0) {
  564. board.removeMember(userId);
  565. return true;
  566. } else throw new Meteor.Error('error-board-notAMember');
  567. } else throw new Meteor.Error('error-board-doesNotExist');
  568. },
  569. });
  570. }
  571. if (Meteor.isServer) {
  572. // Let MongoDB ensure that a member is not included twice in the same board
  573. Meteor.startup(() => {
  574. Boards._collection._ensureIndex({
  575. _id: 1,
  576. 'members.userId': 1,
  577. }, { unique: true });
  578. Boards._collection._ensureIndex({ 'members.userId': 1 });
  579. });
  580. // Genesis: the first activity of the newly created board
  581. Boards.after.insert((userId, doc) => {
  582. Activities.insert({
  583. userId,
  584. type: 'board',
  585. activityTypeId: doc._id,
  586. activityType: 'createBoard',
  587. boardId: doc._id,
  588. });
  589. });
  590. // If the user remove one label from a board, we cant to remove reference of
  591. // this label in any card of this board.
  592. Boards.after.update((userId, doc, fieldNames, modifier) => {
  593. if (!_.contains(fieldNames, 'labels') ||
  594. !modifier.$pull ||
  595. !modifier.$pull.labels ||
  596. !modifier.$pull.labels._id) {
  597. return;
  598. }
  599. const removedLabelId = modifier.$pull.labels._id;
  600. Cards.update(
  601. { boardId: doc._id },
  602. {
  603. $pull: {
  604. labelIds: removedLabelId,
  605. },
  606. },
  607. { multi: true }
  608. );
  609. });
  610. const foreachRemovedMember = (doc, modifier, callback) => {
  611. Object.keys(modifier).forEach((set) => {
  612. if (modifier[set] !== false) {
  613. return;
  614. }
  615. const parts = set.split('.');
  616. if (parts.length === 3 && parts[0] === 'members' && parts[2] === 'isActive') {
  617. callback(doc.members[parts[1]].userId);
  618. }
  619. });
  620. };
  621. // Remove a member from all objects of the board before leaving the board
  622. Boards.before.update((userId, doc, fieldNames, modifier) => {
  623. if (!_.contains(fieldNames, 'members')) {
  624. return;
  625. }
  626. if (modifier.$set) {
  627. const boardId = doc._id;
  628. foreachRemovedMember(doc, modifier.$set, (memberId) => {
  629. Cards.update(
  630. { boardId },
  631. {
  632. $pull: {
  633. members: memberId,
  634. watchers: memberId,
  635. },
  636. },
  637. { multi: true }
  638. );
  639. Lists.update(
  640. { boardId },
  641. {
  642. $pull: {
  643. watchers: memberId,
  644. },
  645. },
  646. { multi: true }
  647. );
  648. const board = Boards._transform(doc);
  649. board.setWatcher(memberId, false);
  650. // Remove board from users starred list
  651. if (!board.isPublic()) {
  652. Users.update(
  653. memberId,
  654. {
  655. $pull: {
  656. 'profile.starredBoards': boardId,
  657. },
  658. }
  659. );
  660. }
  661. });
  662. }
  663. });
  664. // Add a new activity if we add or remove a member to the board
  665. Boards.after.update((userId, doc, fieldNames, modifier) => {
  666. if (!_.contains(fieldNames, 'members')) {
  667. return;
  668. }
  669. // Say hello to the new member
  670. if (modifier.$push && modifier.$push.members) {
  671. const memberId = modifier.$push.members.userId;
  672. Activities.insert({
  673. userId,
  674. memberId,
  675. type: 'member',
  676. activityType: 'addBoardMember',
  677. boardId: doc._id,
  678. });
  679. }
  680. // Say goodbye to the former member
  681. if (modifier.$set) {
  682. foreachRemovedMember(doc, modifier.$set, (memberId) => {
  683. Activities.insert({
  684. userId,
  685. memberId,
  686. type: 'member',
  687. activityType: 'removeBoardMember',
  688. boardId: doc._id,
  689. });
  690. });
  691. }
  692. });
  693. }
  694. //BOARDS REST API
  695. if (Meteor.isServer) {
  696. JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) {
  697. try {
  698. Authentication.checkLoggedIn(req.userId);
  699. const paramUserId = req.params.userId;
  700. // A normal user should be able to see their own boards,
  701. // admins can access boards of any user
  702. Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId);
  703. const data = Boards.find({
  704. archived: false,
  705. 'members.userId': paramUserId,
  706. }, {
  707. sort: ['title'],
  708. }).map(function(board) {
  709. return {
  710. _id: board._id,
  711. title: board.title,
  712. };
  713. });
  714. JsonRoutes.sendResult(res, {code: 200, data});
  715. }
  716. catch (error) {
  717. JsonRoutes.sendResult(res, {
  718. code: 200,
  719. data: error,
  720. });
  721. }
  722. });
  723. JsonRoutes.add('GET', '/api/boards', function (req, res) {
  724. try {
  725. Authentication.checkUserId(req.userId);
  726. JsonRoutes.sendResult(res, {
  727. code: 200,
  728. data: Boards.find({ permission: 'public' }).map(function (doc) {
  729. return {
  730. _id: doc._id,
  731. title: doc.title,
  732. };
  733. }),
  734. });
  735. }
  736. catch (error) {
  737. JsonRoutes.sendResult(res, {
  738. code: 200,
  739. data: error,
  740. });
  741. }
  742. });
  743. JsonRoutes.add('GET', '/api/boards/:boardId', function (req, res) {
  744. try {
  745. const id = req.params.boardId;
  746. Authentication.checkBoardAccess(req.userId, id);
  747. JsonRoutes.sendResult(res, {
  748. code: 200,
  749. data: Boards.findOne({ _id: id }),
  750. });
  751. }
  752. catch (error) {
  753. JsonRoutes.sendResult(res, {
  754. code: 200,
  755. data: error,
  756. });
  757. }
  758. });
  759. JsonRoutes.add('PUT', '/api/boards/:boardId/members', function (req, res) {
  760. Authentication.checkUserId(req.userId);
  761. try {
  762. const boardId = req.params.boardId;
  763. const board = Boards.findOne({ _id: boardId });
  764. const userId = req.body.userId;
  765. const user = Users.findOne({ _id: userId });
  766. if (!board.getMember(userId)) {
  767. user.addInvite(boardId);
  768. board.addMember(userId);
  769. JsonRoutes.sendResult(res, {
  770. code: 200,
  771. data: id,
  772. });
  773. } else {
  774. JsonRoutes.sendResult(res, {
  775. code: 200,
  776. });
  777. }
  778. }
  779. catch (error) {
  780. JsonRoutes.sendResult(res, {
  781. data: error,
  782. });
  783. }
  784. });
  785. JsonRoutes.add('POST', '/api/boards', function (req, res) {
  786. try {
  787. Authentication.checkUserId(req.userId);
  788. const id = Boards.insert({
  789. title: req.body.title,
  790. members: [
  791. {
  792. userId: req.body.owner,
  793. isAdmin: req.body.isAdmin || true,
  794. isActive: req.body.isActive || true,
  795. isNoComments: req.body.isNoComments || false,
  796. isCommentOnly: req.body.isCommentOnly || false,
  797. },
  798. ],
  799. permission: req.body.permission || 'private',
  800. color: req.body.color || 'belize',
  801. });
  802. const swimlaneId = Swimlanes.insert({
  803. title: TAPi18n.__('default'),
  804. boardId: id,
  805. });
  806. JsonRoutes.sendResult(res, {
  807. code: 200,
  808. data: {
  809. _id: id,
  810. defaultSwimlaneId: swimlaneId,
  811. },
  812. });
  813. }
  814. catch (error) {
  815. JsonRoutes.sendResult(res, {
  816. code: 200,
  817. data: error,
  818. });
  819. }
  820. });
  821. JsonRoutes.add('DELETE', '/api/boards/:boardId', function (req, res) {
  822. try {
  823. Authentication.checkUserId(req.userId);
  824. const id = req.params.boardId;
  825. Boards.remove({ _id: id });
  826. JsonRoutes.sendResult(res, {
  827. code: 200,
  828. data:{
  829. _id: id,
  830. },
  831. });
  832. }
  833. catch (error) {
  834. JsonRoutes.sendResult(res, {
  835. code: 200,
  836. data: error,
  837. });
  838. }
  839. });
  840. JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function (req, res) {
  841. Authentication.checkUserId(req.userId);
  842. const id = req.params.boardId;
  843. try {
  844. if (req.body.hasOwnProperty('label')) {
  845. const board = Boards.findOne({ _id: id });
  846. const color = req.body.label.color;
  847. const name = req.body.label.name;
  848. const labelId = Random.id(6);
  849. if (!board.getLabel(name, color)) {
  850. Boards.direct.update({ _id: id }, { $push: { labels: { _id: labelId, name, color } } });
  851. JsonRoutes.sendResult(res, {
  852. code: 200,
  853. data: labelId,
  854. });
  855. } else {
  856. JsonRoutes.sendResult(res, {
  857. code: 200,
  858. });
  859. }
  860. }
  861. }
  862. catch (error) {
  863. JsonRoutes.sendResult(res, {
  864. data: error,
  865. });
  866. }
  867. });
  868. JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function (req, res) {
  869. try {
  870. const boardId = req.params.boardId;
  871. const memberId = req.params.memberId;
  872. const {isAdmin, isNoComments, isCommentOnly} = req.body;
  873. Authentication.checkBoardAccess(req.userId, boardId);
  874. const board = Boards.findOne({ _id: boardId });
  875. function isTrue(data){
  876. return data.toLowerCase() === 'true';
  877. }
  878. board.setMemberPermission(memberId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), req.userId);
  879. JsonRoutes.sendResult(res, {
  880. code: 200,
  881. data: query,
  882. });
  883. }
  884. catch (error) {
  885. JsonRoutes.sendResult(res, {
  886. code: 200,
  887. data: error,
  888. });
  889. }
  890. });
  891. }