swimlanes.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. Swimlanes = new Mongo.Collection('swimlanes');
  2. /**
  3. * A swimlane is an line in the kaban board.
  4. */
  5. Swimlanes.attachSchema(
  6. new SimpleSchema({
  7. title: {
  8. /**
  9. * the title of the swimlane
  10. */
  11. type: String,
  12. },
  13. archived: {
  14. /**
  15. * is the swimlane archived?
  16. */
  17. type: Boolean,
  18. // eslint-disable-next-line consistent-return
  19. autoValue() {
  20. if (this.isInsert && !this.isSet) {
  21. return false;
  22. }
  23. },
  24. },
  25. boardId: {
  26. /**
  27. * the ID of the board the swimlane is attached to
  28. */
  29. type: String,
  30. },
  31. createdAt: {
  32. /**
  33. * creation date of the swimlane
  34. */
  35. type: Date,
  36. // eslint-disable-next-line consistent-return
  37. autoValue() {
  38. if (this.isInsert) {
  39. return new Date();
  40. } else {
  41. this.unset();
  42. }
  43. },
  44. },
  45. sort: {
  46. /**
  47. * the sort value of the swimlane
  48. */
  49. type: Number,
  50. decimal: true,
  51. // XXX We should probably provide a default
  52. optional: true,
  53. },
  54. color: {
  55. /**
  56. * the color of the swimlane
  57. */
  58. type: String,
  59. optional: true,
  60. // silver is the default, so it is left out
  61. allowedValues: [
  62. 'white',
  63. 'green',
  64. 'yellow',
  65. 'orange',
  66. 'red',
  67. 'purple',
  68. 'blue',
  69. 'sky',
  70. 'lime',
  71. 'pink',
  72. 'black',
  73. 'peachpuff',
  74. 'crimson',
  75. 'plum',
  76. 'darkgreen',
  77. 'slateblue',
  78. 'magenta',
  79. 'gold',
  80. 'navy',
  81. 'gray',
  82. 'saddlebrown',
  83. 'paleturquoise',
  84. 'mistyrose',
  85. 'indigo',
  86. ],
  87. },
  88. updatedAt: {
  89. /**
  90. * when was the swimlane last edited
  91. */
  92. type: Date,
  93. optional: true,
  94. // eslint-disable-next-line consistent-return
  95. autoValue() {
  96. if (this.isUpdate || this.isUpsert || this.isInsert) {
  97. return new Date();
  98. } else {
  99. this.unset();
  100. }
  101. },
  102. },
  103. modifiedAt: {
  104. type: Date,
  105. denyUpdate: false,
  106. // eslint-disable-next-line consistent-return
  107. autoValue() {
  108. if (this.isInsert || this.isUpsert || this.isUpdate) {
  109. return new Date();
  110. } else {
  111. this.unset();
  112. }
  113. },
  114. },
  115. type: {
  116. /**
  117. * The type of swimlane
  118. */
  119. type: String,
  120. defaultValue: 'swimlane',
  121. },
  122. }),
  123. );
  124. Swimlanes.allow({
  125. insert(userId, doc) {
  126. return allowIsBoardMemberCommentOnly(userId, Boards.findOne(doc.boardId));
  127. },
  128. update(userId, doc) {
  129. return allowIsBoardMemberCommentOnly(userId, Boards.findOne(doc.boardId));
  130. },
  131. remove(userId, doc) {
  132. return allowIsBoardMemberCommentOnly(userId, Boards.findOne(doc.boardId));
  133. },
  134. fetch: ['boardId'],
  135. });
  136. Swimlanes.helpers({
  137. copy(boardId) {
  138. const oldId = this._id;
  139. const oldBoardId = this.boardId;
  140. this.boardId = boardId;
  141. delete this._id;
  142. const _id = Swimlanes.insert(this);
  143. const query = {
  144. swimlaneId: { $in: [oldId, ''] },
  145. archived: false,
  146. };
  147. if (oldBoardId) {
  148. query.boardId = oldBoardId;
  149. }
  150. // Copy all lists in swimlane
  151. Lists.find(query).forEach(list => {
  152. list.type = 'list';
  153. list.swimlaneId = oldId;
  154. list.boardId = boardId;
  155. list.copy(boardId, _id);
  156. });
  157. },
  158. cards() {
  159. return Cards.find(
  160. Filter.mongoSelector({
  161. swimlaneId: this._id,
  162. archived: false,
  163. }),
  164. { sort: ['sort'] },
  165. );
  166. },
  167. lists() {
  168. return Lists.find(
  169. {
  170. boardId: this.boardId,
  171. swimlaneId: { $in: [this._id, ''] },
  172. archived: false,
  173. },
  174. { sort: ['sort'] },
  175. );
  176. },
  177. myLists() {
  178. return Lists.find({ swimlaneId: this._id });
  179. },
  180. allCards() {
  181. return Cards.find({ swimlaneId: this._id });
  182. },
  183. board() {
  184. return Boards.findOne(this.boardId);
  185. },
  186. colorClass() {
  187. if (this.color) return this.color;
  188. return '';
  189. },
  190. isTemplateSwimlane() {
  191. return this.type === 'template-swimlane';
  192. },
  193. isTemplateContainer() {
  194. return this.type === 'template-container';
  195. },
  196. isListTemplatesSwimlane() {
  197. const user = Users.findOne(Meteor.userId());
  198. return (user.profile || {}).listTemplatesSwimlaneId === this._id;
  199. },
  200. isCardTemplatesSwimlane() {
  201. const user = Users.findOne(Meteor.userId());
  202. return (user.profile || {}).cardTemplatesSwimlaneId === this._id;
  203. },
  204. isBoardTemplatesSwimlane() {
  205. const user = Users.findOne(Meteor.userId());
  206. return (user.profile || {}).boardTemplatesSwimlaneId === this._id;
  207. },
  208. remove() {
  209. Swimlanes.remove({ _id: this._id });
  210. },
  211. });
  212. Swimlanes.mutations({
  213. rename(title) {
  214. return { $set: { title } };
  215. },
  216. archive() {
  217. if (this.isTemplateSwimlane()) {
  218. this.myLists().forEach(list => {
  219. return list.archive();
  220. });
  221. }
  222. return { $set: { archived: true } };
  223. },
  224. restore() {
  225. if (this.isTemplateSwimlane()) {
  226. this.myLists().forEach(list => {
  227. return list.restore();
  228. });
  229. }
  230. return { $set: { archived: false } };
  231. },
  232. setColor(newColor) {
  233. if (newColor === 'silver') {
  234. newColor = null;
  235. }
  236. return {
  237. $set: {
  238. color: newColor,
  239. },
  240. };
  241. },
  242. });
  243. Swimlanes.hookOptions.after.update = { fetchPrevious: false };
  244. if (Meteor.isServer) {
  245. Meteor.startup(() => {
  246. Swimlanes._collection._ensureIndex({ modifiedAt: -1 });
  247. Swimlanes._collection._ensureIndex({ boardId: 1 });
  248. });
  249. Swimlanes.after.insert((userId, doc) => {
  250. Activities.insert({
  251. userId,
  252. type: 'swimlane',
  253. activityType: 'createSwimlane',
  254. boardId: doc.boardId,
  255. swimlaneId: doc._id,
  256. });
  257. });
  258. Swimlanes.before.remove(function(userId, doc) {
  259. const lists = Lists.find(
  260. {
  261. boardId: doc.boardId,
  262. swimlaneId: { $in: [doc._id, ''] },
  263. archived: false,
  264. },
  265. { sort: ['sort'] },
  266. );
  267. if (lists.count() < 2) {
  268. lists.forEach(list => {
  269. list.remove();
  270. });
  271. } else {
  272. Cards.remove({ swimlaneId: doc._id });
  273. }
  274. Activities.insert({
  275. userId,
  276. type: 'swimlane',
  277. activityType: 'removeSwimlane',
  278. boardId: doc.boardId,
  279. swimlaneId: doc._id,
  280. title: doc.title,
  281. });
  282. });
  283. Swimlanes.after.update((userId, doc) => {
  284. if (doc.archived) {
  285. Activities.insert({
  286. userId,
  287. type: 'swimlane',
  288. activityType: 'archivedSwimlane',
  289. swimlaneId: doc._id,
  290. boardId: doc.boardId,
  291. });
  292. }
  293. });
  294. }
  295. //SWIMLANE REST API
  296. if (Meteor.isServer) {
  297. /**
  298. * @operation get_all_swimlanes
  299. *
  300. * @summary Get the list of swimlanes attached to a board
  301. *
  302. * @param {string} boardId the ID of the board
  303. * @return_type [{_id: string,
  304. * title: string}]
  305. */
  306. JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) {
  307. try {
  308. const paramBoardId = req.params.boardId;
  309. Authentication.checkBoardAccess(req.userId, paramBoardId);
  310. JsonRoutes.sendResult(res, {
  311. code: 200,
  312. data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(
  313. function(doc) {
  314. return {
  315. _id: doc._id,
  316. title: doc.title,
  317. };
  318. },
  319. ),
  320. });
  321. } catch (error) {
  322. JsonRoutes.sendResult(res, {
  323. code: 200,
  324. data: error,
  325. });
  326. }
  327. });
  328. /**
  329. * @operation get_swimlane
  330. *
  331. * @summary Get a swimlane
  332. *
  333. * @param {string} boardId the ID of the board
  334. * @param {string} swimlaneId the ID of the swimlane
  335. * @return_type Swimlanes
  336. */
  337. JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function(
  338. req,
  339. res,
  340. ) {
  341. try {
  342. const paramBoardId = req.params.boardId;
  343. const paramSwimlaneId = req.params.swimlaneId;
  344. Authentication.checkBoardAccess(req.userId, paramBoardId);
  345. JsonRoutes.sendResult(res, {
  346. code: 200,
  347. data: Swimlanes.findOne({
  348. _id: paramSwimlaneId,
  349. boardId: paramBoardId,
  350. archived: false,
  351. }),
  352. });
  353. } catch (error) {
  354. JsonRoutes.sendResult(res, {
  355. code: 200,
  356. data: error,
  357. });
  358. }
  359. });
  360. /**
  361. * @operation new_swimlane
  362. *
  363. * @summary Add a swimlane to a board
  364. *
  365. * @param {string} boardId the ID of the board
  366. * @param {string} title the new title of the swimlane
  367. * @return_type {_id: string}
  368. */
  369. JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) {
  370. try {
  371. Authentication.checkUserId(req.userId);
  372. const paramBoardId = req.params.boardId;
  373. const board = Boards.findOne(paramBoardId);
  374. const id = Swimlanes.insert({
  375. title: req.body.title,
  376. boardId: paramBoardId,
  377. sort: board.swimlanes().count(),
  378. });
  379. JsonRoutes.sendResult(res, {
  380. code: 200,
  381. data: {
  382. _id: id,
  383. },
  384. });
  385. } catch (error) {
  386. JsonRoutes.sendResult(res, {
  387. code: 200,
  388. data: error,
  389. });
  390. }
  391. });
  392. /**
  393. * @operation delete_swimlane
  394. *
  395. * @summary Delete a swimlane
  396. *
  397. * @description The swimlane will be deleted, not moved to the recycle bin
  398. *
  399. * @param {string} boardId the ID of the board
  400. * @param {string} swimlaneId the ID of the swimlane
  401. * @return_type {_id: string}
  402. */
  403. JsonRoutes.add(
  404. 'DELETE',
  405. '/api/boards/:boardId/swimlanes/:swimlaneId',
  406. function(req, res) {
  407. try {
  408. Authentication.checkUserId(req.userId);
  409. const paramBoardId = req.params.boardId;
  410. const paramSwimlaneId = req.params.swimlaneId;
  411. Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
  412. JsonRoutes.sendResult(res, {
  413. code: 200,
  414. data: {
  415. _id: paramSwimlaneId,
  416. },
  417. });
  418. } catch (error) {
  419. JsonRoutes.sendResult(res, {
  420. code: 200,
  421. data: error,
  422. });
  423. }
  424. },
  425. );
  426. }
  427. export default Swimlanes;