checklists.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  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.isTouchScreenOrShowDesktopDragHandles()) {
  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. 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');