checklists.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. import Cards from '/models/cards';
  2. import Boards from '/models/boards';
  3. const subManager = new SubsManager();
  4. const { calculateIndexData, capitalize } = Utils;
  5. function initSorting(items) {
  6. items.sortable({
  7. tolerance: 'pointer',
  8. helper: 'clone',
  9. items: '.js-checklist-item:not(.placeholder)',
  10. connectWith: '.js-checklist-items',
  11. appendTo: 'parent',
  12. distance: 7,
  13. placeholder: 'checklist-item placeholder',
  14. scroll: true,
  15. start(evt, ui) {
  16. ui.placeholder.height(ui.helper.height());
  17. EscapeActions.clickExecute(evt.target, 'inlinedForm');
  18. },
  19. stop(evt, ui) {
  20. const parent = ui.item.parents('.js-checklist-items');
  21. const checklistId = Blaze.getData(parent.get(0)).checklist._id;
  22. let prevItem = ui.item.prev('.js-checklist-item').get(0);
  23. if (prevItem) {
  24. prevItem = Blaze.getData(prevItem).item;
  25. }
  26. let nextItem = ui.item.next('.js-checklist-item').get(0);
  27. if (nextItem) {
  28. nextItem = Blaze.getData(nextItem).item;
  29. }
  30. const nItems = 1;
  31. const sortIndex = calculateIndexData(prevItem, nextItem, nItems);
  32. const checklistDomElement = ui.item.get(0);
  33. const checklistData = Blaze.getData(checklistDomElement);
  34. const checklistItem = checklistData.item;
  35. items.sortable('cancel');
  36. checklistItem.move(checklistId, sortIndex.base);
  37. },
  38. });
  39. }
  40. BlazeComponent.extendComponent({
  41. onRendered() {
  42. const self = this;
  43. self.itemsDom = this.$('.js-checklist-items');
  44. initSorting(self.itemsDom);
  45. self.itemsDom.mousedown(function(evt) {
  46. evt.stopPropagation();
  47. });
  48. function userIsMember() {
  49. return Meteor.user() && Meteor.user().isBoardMember();
  50. }
  51. // Disable sorting if the current user is not a board member
  52. self.autorun(() => {
  53. const $itemsDom = $(self.itemsDom);
  54. if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
  55. $(self.itemsDom).sortable('option', 'disabled', !userIsMember());
  56. if (Utils.isMiniScreenOrShowDesktopDragHandles()) {
  57. $(self.itemsDom).sortable({
  58. handle: 'span.fa.checklistitem-handle',
  59. });
  60. }
  61. }
  62. });
  63. },
  64. canModifyCard() {
  65. return (
  66. Meteor.user() &&
  67. Meteor.user().isBoardMember() &&
  68. !Meteor.user().isCommentOnly() &&
  69. !Meteor.user().isWorker()
  70. );
  71. },
  72. /** returns the finished percent of the checklist */
  73. finishedPercent() {
  74. const ret = this.data().checklist.finishedPercent();
  75. return ret;
  76. },
  77. }).register('checklistDetail');
  78. BlazeComponent.extendComponent({
  79. addChecklist(event) {
  80. event.preventDefault();
  81. const textarea = this.find('textarea.js-add-checklist-item');
  82. const title = textarea.value.trim();
  83. let cardId = this.currentData().cardId;
  84. const card = Cards.findOne(cardId);
  85. //if (card.isLinked()) cardId = card.linkedId;
  86. if (card.isLinkedCard()) cardId = card.linkedId;
  87. if (title) {
  88. Checklists.insert({
  89. cardId,
  90. title,
  91. sort: card.checklists().count(),
  92. });
  93. this.closeAllInlinedForms();
  94. setTimeout(() => {
  95. this.$('.add-checklist-item')
  96. .last()
  97. .click();
  98. }, 100);
  99. }
  100. },
  101. addChecklistItem(event) {
  102. event.preventDefault();
  103. const textarea = this.find('textarea.js-add-checklist-item');
  104. const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
  105. const title = textarea.value.trim();
  106. const checklist = this.currentData().checklist;
  107. if (title) {
  108. let checklistItems = [title];
  109. if (newlineBecomesNewChecklistItem.checked) {
  110. checklistItems = title.split('\n').map(_value => _value.trim());
  111. }
  112. for (let checklistItem of checklistItems) {
  113. ChecklistItems.insert({
  114. title: checklistItem,
  115. checklistId: checklist._id,
  116. cardId: checklist.cardId,
  117. sort: Utils.calculateIndexData(checklist.lastItem()).base,
  118. });
  119. }
  120. }
  121. // We keep the form opened, empty it.
  122. textarea.value = '';
  123. textarea.focus();
  124. },
  125. canModifyCard() {
  126. return (
  127. Meteor.user() &&
  128. Meteor.user().isBoardMember() &&
  129. !Meteor.user().isCommentOnly() &&
  130. !Meteor.user().isWorker()
  131. );
  132. },
  133. deleteItem() {
  134. const checklist = this.currentData().checklist;
  135. const item = this.currentData().item;
  136. if (checklist && item && item._id) {
  137. ChecklistItems.remove(item._id);
  138. }
  139. },
  140. editChecklist(event) {
  141. event.preventDefault();
  142. const textarea = this.find('textarea.js-edit-checklist-item');
  143. const title = textarea.value.trim();
  144. const checklist = this.currentData().checklist;
  145. checklist.setTitle(title);
  146. },
  147. editChecklistItem(event) {
  148. event.preventDefault();
  149. const textarea = this.find('textarea.js-edit-checklist-item');
  150. const title = textarea.value.trim();
  151. const item = this.currentData().item;
  152. item.setTitle(title);
  153. },
  154. pressKey(event) {
  155. //If user press enter key inside a form, submit it
  156. //Unless the user is also holding down the 'shift' key
  157. if (event.keyCode === 13 && !event.shiftKey) {
  158. event.preventDefault();
  159. const $form = $(event.currentTarget).closest('form');
  160. $form.find('button[type=submit]').click();
  161. }
  162. },
  163. focusChecklistItem(event) {
  164. // If a new checklist is created, pre-fill the title and select it.
  165. const checklist = this.currentData().checklist;
  166. if (!checklist) {
  167. const textarea = event.target;
  168. textarea.value = capitalize(TAPi18n.__('r-checklist'));
  169. textarea.select();
  170. }
  171. },
  172. /** closes all inlined forms (checklist and checklist-item input fields) */
  173. closeAllInlinedForms() {
  174. this.$('.js-close-inlined-form').click();
  175. },
  176. events() {
  177. const events = {
  178. 'click #toggleHideCheckedItemsButton'() {
  179. Meteor.call('toggleHideCheckedItems');
  180. },
  181. };
  182. return [
  183. {
  184. ...events,
  185. 'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
  186. 'submit .js-add-checklist': this.addChecklist,
  187. 'submit .js-edit-checklist-title': this.editChecklist,
  188. 'submit .js-add-checklist-item': this.addChecklistItem,
  189. 'submit .js-edit-checklist-item': this.editChecklistItem,
  190. 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
  191. 'click .js-delete-checklist-item': this.deleteItem,
  192. 'focus .js-add-checklist-item': this.focusChecklistItem,
  193. // add and delete checklist / checklist-item
  194. 'click .js-open-inlined-form': this.closeAllInlinedForms,
  195. keydown: this.pressKey,
  196. },
  197. ];
  198. },
  199. }).register('checklists');
  200. BlazeComponent.extendComponent({
  201. onCreated() {
  202. subManager.subscribe('board', Session.get('currentBoard'), false);
  203. this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
  204. },
  205. boards() {
  206. return Boards.find(
  207. {
  208. archived: false,
  209. 'members.userId': Meteor.userId(),
  210. _id: { $ne: Meteor.user().getTemplatesBoardId() },
  211. },
  212. {
  213. sort: { sort: 1 /* boards default sorting */ },
  214. },
  215. );
  216. },
  217. swimlanes() {
  218. const board = Boards.findOne(this.selectedBoardId.get());
  219. return board.swimlanes();
  220. },
  221. aBoardLists() {
  222. const board = Boards.findOne(this.selectedBoardId.get());
  223. return board.lists();
  224. },
  225. events() {
  226. return [
  227. {
  228. 'change .js-select-boards'(event) {
  229. this.selectedBoardId.set($(event.currentTarget).val());
  230. subManager.subscribe('board', this.selectedBoardId.get(), false);
  231. },
  232. },
  233. ];
  234. },
  235. }).register('boardsSwimlanesAndLists');
  236. Template.checklists.helpers({
  237. checklists() {
  238. const card = Cards.findOne(this.cardId);
  239. const ret = card.checklists();
  240. return ret;
  241. },
  242. hideCheckedItems() {
  243. const currentUser = Meteor.user();
  244. if (currentUser) return currentUser.hasHideCheckedItems();
  245. return false;
  246. },
  247. });
  248. BlazeComponent.extendComponent({
  249. onRendered() {
  250. autosize(this.$('textarea.js-add-checklist-item'));
  251. },
  252. canModifyCard() {
  253. return (
  254. Meteor.user() &&
  255. Meteor.user().isBoardMember() &&
  256. !Meteor.user().isCommentOnly() &&
  257. !Meteor.user().isWorker()
  258. );
  259. },
  260. events() {
  261. return [
  262. {
  263. 'click a.fa.fa-copy'(event) {
  264. const $editor = this.$('textarea');
  265. const promise = Utils.copyTextToClipboard($editor[0].value);
  266. const $tooltip = this.$('.copied-tooltip');
  267. Utils.showCopied(promise, $tooltip);
  268. },
  269. }
  270. ];
  271. }
  272. }).register('addChecklistItemForm');
  273. BlazeComponent.extendComponent({
  274. events() {
  275. return [
  276. {
  277. 'click .js-delete-checklist' : Popup.afterConfirm('checklistDelete', function () {
  278. Popup.back(2);
  279. const checklist = this.checklist;
  280. if (checklist && checklist._id) {
  281. Checklists.remove(checklist._id);
  282. }
  283. }),
  284. 'click .js-move-checklist' : Popup.open('moveChecklist'),
  285. 'click .js-copy-checklist' : Popup.open('copyChecklist'),
  286. }
  287. ]
  288. }
  289. }).register('checklistActionsPopup');
  290. BlazeComponent.extendComponent({
  291. onRendered() {
  292. autosize(this.$('textarea.js-edit-checklist-item'));
  293. },
  294. canModifyCard() {
  295. return (
  296. Meteor.user() &&
  297. Meteor.user().isBoardMember() &&
  298. !Meteor.user().isCommentOnly() &&
  299. !Meteor.user().isWorker()
  300. );
  301. },
  302. events() {
  303. return [
  304. {
  305. 'click a.fa.fa-copy'(event) {
  306. const $editor = this.$('textarea');
  307. const promise = Utils.copyTextToClipboard($editor[0].value);
  308. const $tooltip = this.$('.copied-tooltip');
  309. Utils.showCopied(promise, $tooltip);
  310. },
  311. }
  312. ];
  313. }
  314. }).register('editChecklistItemForm');
  315. Template.checklistItemDetail.helpers({
  316. canModifyCard() {
  317. return (
  318. Meteor.user() &&
  319. Meteor.user().isBoardMember() &&
  320. !Meteor.user().isCommentOnly() &&
  321. !Meteor.user().isWorker()
  322. );
  323. },
  324. hideCheckedItems() {
  325. const user = Meteor.user();
  326. if (user) return user.hasHideCheckedItems();
  327. return false;
  328. },
  329. });
  330. BlazeComponent.extendComponent({
  331. toggleItem() {
  332. const checklist = this.currentData().checklist;
  333. const item = this.currentData().item;
  334. if (checklist && item && item._id) {
  335. item.toggleItem();
  336. }
  337. },
  338. events() {
  339. return [
  340. {
  341. 'click .js-checklist-item .check-box-container': this.toggleItem,
  342. },
  343. ];
  344. },
  345. }).register('checklistItemDetail');
  346. class DialogWithBoardSwimlaneListAndCard extends BlazeComponent {
  347. /** returns the checklist dialog options
  348. * @return Object with properties { boardId, swimlaneId, listId, cardId }
  349. */
  350. getChecklistDialogOptions() {
  351. }
  352. /** checklist is done
  353. * @param cardId the selected card id
  354. * @param options the selected options (Object with properties { boardId, swimlaneId, listId, cardId })
  355. */
  356. setDone(cardId, options) {
  357. }
  358. onCreated() {
  359. this.currentBoardId = Utils.getCurrentBoardId();
  360. this.selectedBoardId = new ReactiveVar(this.currentBoardId);
  361. this.selectedSwimlaneId = new ReactiveVar('');
  362. this.selectedListId = new ReactiveVar('');
  363. this.setChecklistDialogOption(this.currentBoardId);
  364. }
  365. /** set the last confirmed dialog field values
  366. * @param boardId the current board id
  367. */
  368. setChecklistDialogOption(boardId) {
  369. this.checklistDialogOption = {
  370. 'boardId' : "",
  371. 'swimlaneId' : "",
  372. 'listId' : "",
  373. 'cardId': "",
  374. }
  375. let currentOptions = this.getChecklistDialogOptions();
  376. if (currentOptions && boardId && currentOptions[boardId]) {
  377. this.checklistDialogOption = currentOptions[boardId];
  378. if (this.checklistDialogOption.boardId &&
  379. this.checklistDialogOption.swimlaneId &&
  380. this.checklistDialogOption.listId
  381. )
  382. {
  383. this.selectedBoardId.set(this.checklistDialogOption.boardId)
  384. this.selectedSwimlaneId.set(this.checklistDialogOption.swimlaneId);
  385. this.selectedListId.set(this.checklistDialogOption.listId);
  386. }
  387. }
  388. this.getBoardData(this.selectedBoardId.get());
  389. if (!this.selectedSwimlaneId.get() || !Swimlanes.findOne({_id: this.selectedSwimlaneId.get(), boardId: this.selectedBoardId.get()})) {
  390. this.setFirstSwimlaneId();
  391. }
  392. if (!this.selectedListId.get() || !Lists.findOne({_id: this.selectedListId.get(), boardId: this.selectedBoardId.get()})) {
  393. this.setFirstListId();
  394. }
  395. }
  396. /** sets the first swimlane id */
  397. setFirstSwimlaneId() {
  398. try {
  399. const board = Boards.findOne(this.selectedBoardId.get());
  400. const swimlaneId = board.swimlanes().fetch()[0]._id;
  401. this.selectedSwimlaneId.set(swimlaneId);
  402. } catch (e) {}
  403. }
  404. /** sets the first list id */
  405. setFirstListId() {
  406. try {
  407. const board = Boards.findOne(this.selectedBoardId.get());
  408. const listId = board.lists().fetch()[0]._id;
  409. this.selectedListId.set(listId);
  410. } catch (e) {}
  411. }
  412. /** returns if the board id was the last confirmed one
  413. * @param boardId check this board id
  414. * @return if the board id was the last confirmed one
  415. */
  416. isChecklistDialogOptionBoardId(boardId) {
  417. let ret = this.checklistDialogOption.boardId == boardId;
  418. return ret;
  419. }
  420. /** returns if the swimlane id was the last confirmed one
  421. * @param swimlaneId check this swimlane id
  422. * @return if the swimlane id was the last confirmed one
  423. */
  424. isChecklistDialogOptionSwimlaneId(swimlaneId) {
  425. let ret = this.checklistDialogOption.swimlaneId == swimlaneId;
  426. return ret;
  427. }
  428. /** returns if the list id was the last confirmed one
  429. * @param listId check this list id
  430. * @return if the list id was the last confirmed one
  431. */
  432. isChecklistDialogOptionListId(listId) {
  433. let ret = this.checklistDialogOption.listId == listId;
  434. return ret;
  435. }
  436. /** returns if the card id was the last confirmed one
  437. * @param cardId check this card id
  438. * @return if the card id was the last confirmed one
  439. */
  440. isChecklistDialogOptionCardId(cardId) {
  441. let ret = this.checklistDialogOption.cardId == cardId;
  442. return ret;
  443. }
  444. /** returns all available board */
  445. boards() {
  446. const ret = Boards.find(
  447. {
  448. archived: false,
  449. 'members.userId': Meteor.userId(),
  450. _id: { $ne: Meteor.user().getTemplatesBoardId() },
  451. },
  452. {
  453. sort: { sort: 1 },
  454. },
  455. );
  456. return ret;
  457. }
  458. /** returns all available swimlanes of the current board */
  459. swimlanes() {
  460. const board = Boards.findOne(this.selectedBoardId.get());
  461. const ret = board.swimlanes();
  462. return ret;
  463. }
  464. /** returns all available lists of the current board */
  465. lists() {
  466. const board = Boards.findOne(this.selectedBoardId.get());
  467. const ret = board.lists();
  468. return ret;
  469. }
  470. /** returns all available cards of the current list */
  471. cards() {
  472. const list = Lists.findOne(this.selectedListId.get());
  473. const ret = list.cards(this.selectedSwimlaneId.get());
  474. return ret;
  475. }
  476. /** get the board data from the server
  477. * @param boardId get the board data of this board id
  478. */
  479. getBoardData(boardId) {
  480. const self = this;
  481. Meteor.subscribe('board', boardId, false, {
  482. onReady() {
  483. const sameBoardId = self.selectedBoardId.get() == boardId;
  484. self.selectedBoardId.set(boardId);
  485. if (!sameBoardId) {
  486. // reset swimlane id (for selection in cards())
  487. self.setFirstSwimlaneId();
  488. // reset list id (for selection in cards())
  489. self.setFirstListId();
  490. }
  491. },
  492. });
  493. }
  494. events() {
  495. return [
  496. {
  497. 'click .js-done'() {
  498. const boardSelect = this.$('.js-select-boards')[0];
  499. const boardId = boardSelect.options[boardSelect.selectedIndex].value;
  500. const listSelect = this.$('.js-select-lists')[0];
  501. const listId = listSelect.options[listSelect.selectedIndex].value;
  502. const swimlaneSelect = this.$('.js-select-swimlanes')[0];
  503. const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
  504. const cardSelect = this.$('.js-select-cards')[0];
  505. const cardId = cardSelect.options[cardSelect.selectedIndex].value;
  506. const options = {
  507. 'boardId' : boardId,
  508. 'swimlaneId' : swimlaneId,
  509. 'listId' : listId,
  510. 'cardId': cardId,
  511. }
  512. this.setDone(cardId, options);
  513. Popup.back(2);
  514. },
  515. 'change .js-select-boards'(event) {
  516. const boardId = $(event.currentTarget).val();
  517. this.getBoardData(boardId);
  518. },
  519. 'change .js-select-swimlanes'(event) {
  520. this.selectedSwimlaneId.set($(event.currentTarget).val());
  521. },
  522. 'change .js-select-lists'(event) {
  523. this.selectedListId.set($(event.currentTarget).val());
  524. },
  525. },
  526. ];
  527. }
  528. }
  529. /** Move Checklist Dialog */
  530. (class extends DialogWithBoardSwimlaneListAndCard {
  531. getChecklistDialogOptions() {
  532. const ret = Meteor.user().getMoveChecklistDialogOptions();
  533. return ret;
  534. }
  535. setDone(cardId, options) {
  536. Meteor.user().setMoveChecklistDialogOption(this.currentBoardId, options);
  537. this.data().checklist.move(cardId);
  538. }
  539. }).register('moveChecklistPopup');
  540. /** Copy Checklist Dialog */
  541. (class extends DialogWithBoardSwimlaneListAndCard {
  542. getChecklistDialogOptions() {
  543. const ret = Meteor.user().getCopyChecklistDialogOptions();
  544. return ret;
  545. }
  546. setDone(cardId, options) {
  547. Meteor.user().setCopyChecklistDialogOption(this.currentBoardId, options);
  548. this.data().checklist.copy(cardId);
  549. }
  550. }).register('copyChecklistPopup');