checklists.js 17 KB

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