2
0

swimlanes.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { ALLOWED_COLORS } from '/config/const';
  3. Swimlanes = new Mongo.Collection('swimlanes');
  4. /**
  5. * A swimlane is an line in the kaban board.
  6. */
  7. Swimlanes.attachSchema(
  8. new SimpleSchema({
  9. title: {
  10. /**
  11. * the title of the swimlane
  12. */
  13. type: String,
  14. },
  15. archived: {
  16. /**
  17. * is the swimlane archived?
  18. */
  19. type: Boolean,
  20. // eslint-disable-next-line consistent-return
  21. autoValue() {
  22. if (this.isInsert && !this.isSet) {
  23. return false;
  24. }
  25. },
  26. },
  27. archivedAt: {
  28. /**
  29. * latest archiving date of the swimlane
  30. */
  31. type: Date,
  32. optional: true,
  33. },
  34. boardId: {
  35. /**
  36. * the ID of the board the swimlane is attached to
  37. */
  38. type: String,
  39. },
  40. createdAt: {
  41. /**
  42. * creation date of the swimlane
  43. */
  44. type: Date,
  45. // eslint-disable-next-line consistent-return
  46. autoValue() {
  47. if (this.isInsert) {
  48. return new Date();
  49. } else if (this.isUpsert) {
  50. return { $setOnInsert: new Date() };
  51. } else {
  52. this.unset();
  53. }
  54. },
  55. },
  56. sort: {
  57. /**
  58. * the sort value of the swimlane
  59. */
  60. type: Number,
  61. decimal: true,
  62. // XXX We should probably provide a default
  63. optional: true,
  64. },
  65. color: {
  66. /**
  67. * the color of the swimlane
  68. */
  69. type: String,
  70. optional: true,
  71. // silver is the default, so it is left out
  72. allowedValues: ALLOWED_COLORS,
  73. },
  74. updatedAt: {
  75. /**
  76. * when was the swimlane last edited
  77. */
  78. type: Date,
  79. optional: true,
  80. // eslint-disable-next-line consistent-return
  81. autoValue() {
  82. if (this.isUpdate || this.isUpsert || this.isInsert) {
  83. return new Date();
  84. } else {
  85. this.unset();
  86. }
  87. },
  88. },
  89. modifiedAt: {
  90. type: Date,
  91. denyUpdate: false,
  92. // eslint-disable-next-line consistent-return
  93. autoValue() {
  94. if (this.isInsert || this.isUpsert || this.isUpdate) {
  95. return new Date();
  96. } else {
  97. this.unset();
  98. }
  99. },
  100. },
  101. type: {
  102. /**
  103. * The type of swimlane
  104. */
  105. type: String,
  106. defaultValue: 'swimlane',
  107. },
  108. }),
  109. );
  110. Swimlanes.allow({
  111. insert(userId, doc) {
  112. return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
  113. },
  114. update(userId, doc) {
  115. return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
  116. },
  117. remove(userId, doc) {
  118. return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
  119. },
  120. fetch: ['boardId'],
  121. });
  122. Swimlanes.helpers({
  123. copy(boardId) {
  124. const oldId = this._id;
  125. const oldBoardId = this.boardId;
  126. this.boardId = boardId;
  127. delete this._id;
  128. const _id = Swimlanes.insert(this);
  129. const query = {
  130. swimlaneId: { $in: [oldId, ''] },
  131. archived: false,
  132. };
  133. if (oldBoardId) {
  134. query.boardId = oldBoardId;
  135. }
  136. // Copy all lists in swimlane
  137. ReactiveCache.getLists(query).forEach(list => {
  138. list.type = 'list';
  139. list.swimlaneId = oldId;
  140. list.boardId = boardId;
  141. list.copy(boardId, _id);
  142. });
  143. },
  144. move(toBoardId) {
  145. this.lists().forEach(list => {
  146. const toList = ReactiveCache.getList({
  147. boardId: toBoardId,
  148. title: list.title,
  149. archived: false,
  150. });
  151. let toListId;
  152. if (toList) {
  153. toListId = toList._id;
  154. } else {
  155. toListId = Lists.insert({
  156. title: list.title,
  157. boardId: toBoardId,
  158. type: list.type,
  159. archived: false,
  160. wipLimit: list.wipLimit,
  161. });
  162. }
  163. ReactiveCache.getCards({
  164. listId: list._id,
  165. swimlaneId: this._id,
  166. }).forEach(card => {
  167. card.move(toBoardId, this._id, toListId);
  168. });
  169. });
  170. Swimlanes.update(this._id, {
  171. $set: {
  172. boardId: toBoardId,
  173. },
  174. });
  175. // make sure there is a default swimlane
  176. this.board().getDefaultSwimline();
  177. },
  178. cards() {
  179. const ret = ReactiveCache.getCards(
  180. Filter.mongoSelector({
  181. swimlaneId: this._id,
  182. archived: false,
  183. }),
  184. { sort: ['sort'] },
  185. );
  186. return ret;
  187. },
  188. lists() {
  189. return this.draggableLists();
  190. },
  191. newestLists() {
  192. // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
  193. return ReactiveCache.getLists(
  194. {
  195. boardId: this.boardId,
  196. swimlaneId: { $in: [this._id, ''] },
  197. archived: false,
  198. },
  199. { sort: { modifiedAt: -1 } },
  200. );
  201. },
  202. draggableLists() {
  203. return ReactiveCache.getLists(
  204. {
  205. boardId: this.boardId,
  206. swimlaneId: { $in: [this._id, ''] },
  207. //archived: false,
  208. },
  209. { sort: ['sort'] },
  210. );
  211. },
  212. myLists() {
  213. return ReactiveCache.getLists({ swimlaneId: this._id });
  214. },
  215. allCards() {
  216. const ret = ReactiveCache.getCards({ swimlaneId: this._id });
  217. return ret;
  218. },
  219. board() {
  220. return ReactiveCache.getBoard(this.boardId);
  221. },
  222. colorClass() {
  223. if (this.color) return `swimlane-${this.color}`;
  224. return '';
  225. },
  226. isTemplateSwimlane() {
  227. return this.type === 'template-swimlane';
  228. },
  229. isTemplateContainer() {
  230. return this.type === 'template-container';
  231. },
  232. isListTemplatesSwimlane() {
  233. const user = ReactiveCache.getCurrentUser();
  234. return (user.profile || {}).listTemplatesSwimlaneId === this._id;
  235. },
  236. isCardTemplatesSwimlane() {
  237. const user = ReactiveCache.getCurrentUser();
  238. return (user.profile || {}).cardTemplatesSwimlaneId === this._id;
  239. },
  240. isBoardTemplatesSwimlane() {
  241. const user = ReactiveCache.getCurrentUser();
  242. return (user.profile || {}).boardTemplatesSwimlaneId === this._id;
  243. },
  244. remove() {
  245. Swimlanes.remove({ _id: this._id });
  246. },
  247. });
  248. Swimlanes.mutations({
  249. rename(title) {
  250. return { $set: { title } };
  251. },
  252. archive() {
  253. if (this.isTemplateSwimlane()) {
  254. this.myLists().forEach(list => {
  255. return list.archive();
  256. });
  257. }
  258. return { $set: { archived: true, archivedAt: new Date() } };
  259. },
  260. restore() {
  261. if (this.isTemplateSwimlane()) {
  262. this.myLists().forEach(list => {
  263. return list.restore();
  264. });
  265. }
  266. return { $set: { archived: false } };
  267. },
  268. setColor(newColor) {
  269. if (newColor === 'silver') {
  270. newColor = null;
  271. }
  272. return {
  273. $set: {
  274. color: newColor,
  275. },
  276. };
  277. },
  278. });
  279. Swimlanes.userArchivedSwimlanes = userId => {
  280. return ReactiveCache.getSwimlanes({
  281. boardId: { $in: Boards.userBoardIds(userId, null) },
  282. archived: true,
  283. })
  284. };
  285. Swimlanes.userArchivedSwimlaneIds = () => {
  286. return Swimlanes.userArchivedSwimlanes().map(swim => { return swim._id; });
  287. };
  288. Swimlanes.archivedSwimlanes = () => {
  289. return ReactiveCache.getSwimlanes({ archived: true });
  290. };
  291. Swimlanes.archivedSwimlaneIds = () => {
  292. return Swimlanes.archivedSwimlanes().map(swim => {
  293. return swim._id;
  294. });
  295. };
  296. Swimlanes.hookOptions.after.update = { fetchPrevious: false };
  297. if (Meteor.isServer) {
  298. Meteor.startup(() => {
  299. Swimlanes._collection.createIndex({ modifiedAt: -1 });
  300. Swimlanes._collection.createIndex({ boardId: 1 });
  301. });
  302. Swimlanes.after.insert((userId, doc) => {
  303. Activities.insert({
  304. userId,
  305. type: 'swimlane',
  306. activityType: 'createSwimlane',
  307. boardId: doc.boardId,
  308. swimlaneId: doc._id,
  309. });
  310. });
  311. Swimlanes.before.remove(function(userId, doc) {
  312. const lists = ReactiveCache.getLists(
  313. {
  314. boardId: doc.boardId,
  315. swimlaneId: { $in: [doc._id, ''] },
  316. archived: false,
  317. },
  318. { sort: ['sort'] },
  319. );
  320. if (lists.length < 2) {
  321. lists.forEach(list => {
  322. list.remove();
  323. });
  324. } else {
  325. Cards.remove({ swimlaneId: doc._id });
  326. }
  327. Activities.insert({
  328. userId,
  329. type: 'swimlane',
  330. activityType: 'removeSwimlane',
  331. boardId: doc.boardId,
  332. swimlaneId: doc._id,
  333. title: doc.title,
  334. });
  335. });
  336. Swimlanes.after.update((userId, doc, fieldNames) => {
  337. if (fieldNames.includes('title')) {
  338. Activities.insert({
  339. userId,
  340. type: 'swimlane',
  341. activityType: 'changedSwimlaneTitle',
  342. listId: doc._id,
  343. boardId: doc.boardId,
  344. // this preserves the name so that the activity can be useful after the
  345. // list is deleted
  346. title: doc.title,
  347. });
  348. } else if (doc.archived) {
  349. Activities.insert({
  350. userId,
  351. type: 'swimlane',
  352. activityType: 'archivedSwimlane',
  353. listId: doc._id,
  354. boardId: doc.boardId,
  355. // this preserves the name so that the activity can be useful after the
  356. // list is deleted
  357. title: doc.title,
  358. });
  359. } else if (fieldNames.includes('archived')) {
  360. Activities.insert({
  361. userId,
  362. type: 'swimlane',
  363. activityType: 'restoredSwimlane',
  364. listId: doc._id,
  365. boardId: doc.boardId,
  366. // this preserves the name so that the activity can be useful after the
  367. // list is deleted
  368. title: doc.title,
  369. });
  370. }
  371. });
  372. }
  373. //SWIMLANE REST API
  374. if (Meteor.isServer) {
  375. /**
  376. * @operation get_all_swimlanes
  377. *
  378. * @summary Get the list of swimlanes attached to a board
  379. *
  380. * @param {string} boardId the ID of the board
  381. * @return_type [{_id: string,
  382. * title: string}]
  383. */
  384. JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) {
  385. try {
  386. const paramBoardId = req.params.boardId;
  387. Authentication.checkBoardAccess(req.userId, paramBoardId);
  388. JsonRoutes.sendResult(res, {
  389. code: 200,
  390. data: ReactiveCache.getSwimlanes({ boardId: paramBoardId, archived: false }).map(
  391. function(doc) {
  392. return {
  393. _id: doc._id,
  394. title: doc.title,
  395. };
  396. },
  397. ),
  398. });
  399. } catch (error) {
  400. JsonRoutes.sendResult(res, {
  401. code: 200,
  402. data: error,
  403. });
  404. }
  405. });
  406. /**
  407. * @operation get_swimlane
  408. *
  409. * @summary Get a swimlane
  410. *
  411. * @param {string} boardId the ID of the board
  412. * @param {string} swimlaneId the ID of the swimlane
  413. * @return_type Swimlanes
  414. */
  415. JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function(
  416. req,
  417. res,
  418. ) {
  419. try {
  420. const paramBoardId = req.params.boardId;
  421. const paramSwimlaneId = req.params.swimlaneId;
  422. Authentication.checkBoardAccess(req.userId, paramBoardId);
  423. JsonRoutes.sendResult(res, {
  424. code: 200,
  425. data: ReactiveCache.getSwimlane({
  426. _id: paramSwimlaneId,
  427. boardId: paramBoardId,
  428. archived: false,
  429. }),
  430. });
  431. } catch (error) {
  432. JsonRoutes.sendResult(res, {
  433. code: 200,
  434. data: error,
  435. });
  436. }
  437. });
  438. /**
  439. * @operation new_swimlane
  440. *
  441. * @summary Add a swimlane to a board
  442. *
  443. * @param {string} boardId the ID of the board
  444. * @param {string} title the new title of the swimlane
  445. * @return_type {_id: string}
  446. */
  447. JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) {
  448. try {
  449. const paramBoardId = req.params.boardId;
  450. Authentication.checkBoardAccess(req.userId, paramBoardId);
  451. const board = ReactiveCache.getBoard(paramBoardId);
  452. const id = Swimlanes.insert({
  453. title: req.body.title,
  454. boardId: paramBoardId,
  455. sort: board.swimlanes().length,
  456. });
  457. JsonRoutes.sendResult(res, {
  458. code: 200,
  459. data: {
  460. _id: id,
  461. },
  462. });
  463. } catch (error) {
  464. JsonRoutes.sendResult(res, {
  465. code: 200,
  466. data: error,
  467. });
  468. }
  469. });
  470. /**
  471. * @operation edit_swimlane
  472. *
  473. * @summary Edit the title of a swimlane
  474. *
  475. * @param {string} boardId the ID of the board
  476. * @param {string} swimlaneId the ID of the swimlane to edit
  477. * @param {string} title the new title of the swimlane
  478. * @return_type {_id: string}
  479. */
  480. JsonRoutes.add('PUT', '/api/boards/:boardId/swimlanes/:swimlaneId', function(req, res) {
  481. try {
  482. const paramBoardId = req.params.boardId;
  483. const paramSwimlaneId = req.params.swimlaneId;
  484. Authentication.checkBoardAccess(req.userId, paramBoardId);
  485. const board = ReactiveCache.getBoard(paramBoardId);
  486. const swimlane = ReactiveCache.getSwimlane({
  487. _id: paramSwimlaneId,
  488. boardId: paramBoardId,
  489. });
  490. if (!swimlane) {
  491. throw new Meteor.Error('not-found', 'Swimlane not found');
  492. }
  493. Swimlanes.direct.update(
  494. { _id: paramSwimlaneId },
  495. { $set: { title: req.body.title } }
  496. );
  497. JsonRoutes.sendResult(res, {
  498. code: 200,
  499. data: {
  500. _id: paramSwimlaneId,
  501. },
  502. });
  503. } catch (error) {
  504. JsonRoutes.sendResult(res, {
  505. code: 200,
  506. data: error,
  507. });
  508. }
  509. });
  510. /**
  511. * @operation delete_swimlane
  512. *
  513. * @summary Delete a swimlane
  514. *
  515. * @description The swimlane will be deleted, not moved to the recycle bin
  516. *
  517. * @param {string} boardId the ID of the board
  518. * @param {string} swimlaneId the ID of the swimlane
  519. * @return_type {_id: string}
  520. */
  521. JsonRoutes.add(
  522. 'DELETE',
  523. '/api/boards/:boardId/swimlanes/:swimlaneId',
  524. function(req, res) {
  525. try {
  526. const paramBoardId = req.params.boardId;
  527. const paramSwimlaneId = req.params.swimlaneId;
  528. Authentication.checkBoardAccess(req.userId, paramBoardId);
  529. Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
  530. JsonRoutes.sendResult(res, {
  531. code: 200,
  532. data: {
  533. _id: paramSwimlaneId,
  534. },
  535. });
  536. } catch (error) {
  537. JsonRoutes.sendResult(res, {
  538. code: 200,
  539. data: error,
  540. });
  541. }
  542. },
  543. );
  544. }
  545. export default Swimlanes;