checklists.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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 (title) {
  87. Checklists.insert({
  88. cardId,
  89. title,
  90. sort: card.checklists().count(),
  91. });
  92. this.closeAllInlinedForms();
  93. setTimeout(() => {
  94. this.$('.add-checklist-item')
  95. .last()
  96. .click();
  97. }, 100);
  98. }
  99. },
  100. addChecklistItem(event) {
  101. event.preventDefault();
  102. const textarea = this.find('textarea.js-add-checklist-item');
  103. const newlineBecomesNewChecklistItem = this.find('input#toggleNewlineBecomesNewChecklistItem');
  104. const title = textarea.value.trim();
  105. const checklist = this.currentData().checklist;
  106. if (title) {
  107. let checklistItems = [title];
  108. if (newlineBecomesNewChecklistItem.checked) {
  109. checklistItems = title.split('\n').map(_value => _value.trim());
  110. }
  111. for (let checklistItem of checklistItems) {
  112. ChecklistItems.insert({
  113. title: checklistItem,
  114. checklistId: checklist._id,
  115. cardId: checklist.cardId,
  116. sort: Utils.calculateIndexData(checklist.lastItem()).base,
  117. });
  118. }
  119. }
  120. // We keep the form opened, empty it.
  121. textarea.value = '';
  122. textarea.focus();
  123. },
  124. canModifyCard() {
  125. return (
  126. Meteor.user() &&
  127. Meteor.user().isBoardMember() &&
  128. !Meteor.user().isCommentOnly() &&
  129. !Meteor.user().isWorker()
  130. );
  131. },
  132. deleteItem() {
  133. const checklist = this.currentData().checklist;
  134. const item = this.currentData().item;
  135. if (checklist && item && item._id) {
  136. ChecklistItems.remove(item._id);
  137. }
  138. },
  139. editChecklist(event) {
  140. event.preventDefault();
  141. const textarea = this.find('textarea.js-edit-checklist-item');
  142. const title = textarea.value.trim();
  143. const checklist = this.currentData().checklist;
  144. checklist.setTitle(title);
  145. },
  146. editChecklistItem(event) {
  147. event.preventDefault();
  148. const textarea = this.find('textarea.js-edit-checklist-item');
  149. const title = textarea.value.trim();
  150. const item = this.currentData().item;
  151. item.setTitle(title);
  152. },
  153. pressKey(event) {
  154. //If user press enter key inside a form, submit it
  155. //Unless the user is also holding down the 'shift' key
  156. if (event.keyCode === 13 && !event.shiftKey) {
  157. event.preventDefault();
  158. const $form = $(event.currentTarget).closest('form');
  159. $form.find('button[type=submit]').click();
  160. }
  161. },
  162. focusChecklistItem(event) {
  163. // If a new checklist is created, pre-fill the title and select it.
  164. const checklist = this.currentData().checklist;
  165. if (!checklist) {
  166. const textarea = event.target;
  167. textarea.value = capitalize(TAPi18n.__('r-checklist'));
  168. textarea.select();
  169. }
  170. },
  171. /** closes all inlined forms (checklist and checklist-item input fields) */
  172. closeAllInlinedForms() {
  173. this.$('.js-close-inlined-form').click();
  174. },
  175. events() {
  176. const events = {
  177. 'click #toggleHideCheckedItemsButton'() {
  178. Meteor.call('toggleHideCheckedItems');
  179. },
  180. };
  181. return [
  182. {
  183. ...events,
  184. 'click .js-open-checklist-details-menu': Popup.open('checklistActions'),
  185. 'submit .js-add-checklist': this.addChecklist,
  186. 'submit .js-edit-checklist-title': this.editChecklist,
  187. 'submit .js-add-checklist-item': this.addChecklistItem,
  188. 'submit .js-edit-checklist-item': this.editChecklistItem,
  189. 'click .js-convert-checklist-item-to-card': Popup.open('convertChecklistItemToCard'),
  190. 'click .js-delete-checklist-item': this.deleteItem,
  191. 'focus .js-add-checklist-item': this.focusChecklistItem,
  192. // add and delete checklist / checklist-item
  193. 'click .js-open-inlined-form': this.closeAllInlinedForms,
  194. keydown: this.pressKey,
  195. },
  196. ];
  197. },
  198. }).register('checklists');
  199. BlazeComponent.extendComponent({
  200. onCreated() {
  201. subManager.subscribe('board', Session.get('currentBoard'), false);
  202. this.selectedBoardId = new ReactiveVar(Session.get('currentBoard'));
  203. },
  204. boards() {
  205. return Boards.find(
  206. {
  207. archived: false,
  208. 'members.userId': Meteor.userId(),
  209. _id: { $ne: Meteor.user().getTemplatesBoardId() },
  210. },
  211. {
  212. sort: { sort: 1 /* boards default sorting */ },
  213. },
  214. );
  215. },
  216. swimlanes() {
  217. const board = Boards.findOne(this.selectedBoardId.get());
  218. return board.swimlanes();
  219. },
  220. aBoardLists() {
  221. const board = Boards.findOne(this.selectedBoardId.get());
  222. return board.lists();
  223. },
  224. events() {
  225. return [
  226. {
  227. 'change .js-select-boards'(event) {
  228. this.selectedBoardId.set($(event.currentTarget).val());
  229. subManager.subscribe('board', this.selectedBoardId.get(), false);
  230. },
  231. },
  232. ];
  233. },
  234. }).register('boardsSwimlanesAndLists');
  235. Template.checklists.helpers({
  236. checklists() {
  237. const card = Cards.findOne(this.cardId);
  238. const ret = card.checklists();
  239. return ret;
  240. },
  241. hideCheckedItems() {
  242. const currentUser = Meteor.user();
  243. if (currentUser) return currentUser.hasHideCheckedItems();
  244. return false;
  245. },
  246. });
  247. BlazeComponent.extendComponent({
  248. onRendered() {
  249. autosize(this.$('textarea.js-add-checklist-item'));
  250. },
  251. canModifyCard() {
  252. return (
  253. Meteor.user() &&
  254. Meteor.user().isBoardMember() &&
  255. !Meteor.user().isCommentOnly() &&
  256. !Meteor.user().isWorker()
  257. );
  258. },
  259. events() {
  260. return [
  261. {
  262. 'click a.fa.fa-copy'(event) {
  263. const $editor = this.$('textarea');
  264. const promise = Utils.copyTextToClipboard($editor[0].value);
  265. const $tooltip = this.$('.copied-tooltip');
  266. Utils.showCopied(promise, $tooltip);
  267. },
  268. }
  269. ];
  270. }
  271. }).register('addChecklistItemForm');
  272. BlazeComponent.extendComponent({
  273. events() {
  274. return [
  275. {
  276. 'click .js-delete-checklist' : Popup.afterConfirm('checklistDelete', function () {
  277. Popup.back(2);
  278. const checklist = this.checklist;
  279. if (checklist && checklist._id) {
  280. Checklists.remove(checklist._id);
  281. }
  282. }),
  283. 'click .js-move-checklist' : Popup.open('moveChecklist'),
  284. }
  285. ]
  286. }
  287. }).register('checklistActionsPopup');
  288. BlazeComponent.extendComponent({
  289. onRendered() {
  290. autosize(this.$('textarea.js-edit-checklist-item'));
  291. },
  292. canModifyCard() {
  293. return (
  294. Meteor.user() &&
  295. Meteor.user().isBoardMember() &&
  296. !Meteor.user().isCommentOnly() &&
  297. !Meteor.user().isWorker()
  298. );
  299. },
  300. events() {
  301. return [
  302. {
  303. 'click a.fa.fa-copy'(event) {
  304. const $editor = this.$('textarea');
  305. const promise = Utils.copyTextToClipboard($editor[0].value);
  306. const $tooltip = this.$('.copied-tooltip');
  307. Utils.showCopied(promise, $tooltip);
  308. },
  309. }
  310. ];
  311. }
  312. }).register('editChecklistItemForm');
  313. Template.checklistItemDetail.helpers({
  314. canModifyCard() {
  315. return (
  316. Meteor.user() &&
  317. Meteor.user().isBoardMember() &&
  318. !Meteor.user().isCommentOnly() &&
  319. !Meteor.user().isWorker()
  320. );
  321. },
  322. hideCheckedItems() {
  323. const user = Meteor.user();
  324. if (user) return user.hasHideCheckedItems();
  325. return false;
  326. },
  327. });
  328. BlazeComponent.extendComponent({
  329. toggleItem() {
  330. const checklist = this.currentData().checklist;
  331. const item = this.currentData().item;
  332. if (checklist && item && item._id) {
  333. item.toggleItem();
  334. }
  335. },
  336. events() {
  337. return [
  338. {
  339. 'click .js-checklist-item .check-box-container': this.toggleItem,
  340. },
  341. ];
  342. },
  343. }).register('checklistItemDetail');
  344. BlazeComponent.extendComponent({
  345. onCreated() {
  346. this.currentBoardId = Utils.getCurrentBoardId();
  347. this.selectedBoardId = new ReactiveVar(this.currentBoardId);
  348. this.selectedSwimlaneId = new ReactiveVar('');
  349. this.selectedListId = new ReactiveVar('');
  350. this.setMoveChecklistDialogOption(this.currentBoardId);
  351. },
  352. /** set the last confirmed dialog field values
  353. * @param boardId the current board id
  354. */
  355. setMoveChecklistDialogOption(boardId) {
  356. this.moveChecklistDialogOption = {
  357. 'boardId' : "",
  358. 'swimlaneId' : "",
  359. 'listId' : "",
  360. 'cardId': "",
  361. }
  362. let currentOptions = Meteor.user().getMoveChecklistDialogOptions();
  363. if (currentOptions && boardId && currentOptions[boardId]) {
  364. this.moveChecklistDialogOption = currentOptions[boardId];
  365. if (this.moveChecklistDialogOption.boardId &&
  366. this.moveChecklistDialogOption.swimlaneId &&
  367. this.moveChecklistDialogOption.listId
  368. )
  369. {
  370. this.selectedBoardId.set(this.moveChecklistDialogOption.boardId)
  371. this.selectedSwimlaneId.set(this.moveChecklistDialogOption.swimlaneId);
  372. this.selectedListId.set(this.moveChecklistDialogOption.listId);
  373. }
  374. }
  375. this.getBoardData(boardId);
  376. if (!this.selectedSwimlaneId.get() || !Swimlanes.findOne({_id: this.selectedSwimlaneId.get(), boardId: this.selectedBoardId.get()})) {
  377. this.setFirstSwimlaneId();
  378. }
  379. if (!this.selectedListId.get() || !Lists.findOne({_id: this.selectedListId.get(), boardId: this.selectedBoardId.get()})) {
  380. this.setFirstListId();
  381. }
  382. },
  383. /** sets the first swimlane id */
  384. setFirstSwimlaneId() {
  385. try {
  386. const board = Boards.findOne(this.selectedBoardId.get());
  387. const swimlaneId = board.swimlanes().fetch()[0]._id;
  388. this.selectedSwimlaneId.set(swimlaneId);
  389. } catch (e) {}
  390. },
  391. /** sets the first list id */
  392. setFirstListId() {
  393. try {
  394. const board = Boards.findOne(this.selectedBoardId.get());
  395. const listId = board.lists().fetch()[0]._id;
  396. this.selectedListId.set(listId);
  397. } catch (e) {}
  398. },
  399. /** returns if the board id was the last confirmed one
  400. * @param boardId check this board id
  401. * @return if the board id was the last confirmed one
  402. */
  403. isMoveChecklistDialogOptionBoardId(boardId) {
  404. let ret = this.moveChecklistDialogOption.boardId == boardId;
  405. return ret;
  406. },
  407. /** returns if the swimlane id was the last confirmed one
  408. * @param swimlaneId check this swimlane id
  409. * @return if the swimlane id was the last confirmed one
  410. */
  411. isMoveChecklistDialogOptionSwimlaneId(swimlaneId) {
  412. let ret = this.moveChecklistDialogOption.swimlaneId == swimlaneId;
  413. return ret;
  414. },
  415. /** returns if the list id was the last confirmed one
  416. * @param listId check this list id
  417. * @return if the list id was the last confirmed one
  418. */
  419. isMoveChecklistDialogOptionListId(listId) {
  420. let ret = this.moveChecklistDialogOption.listId == listId;
  421. return ret;
  422. },
  423. /** returns if the card id was the last confirmed one
  424. * @param cardId check this card id
  425. * @return if the card id was the last confirmed one
  426. */
  427. isMoveChecklistDialogOptionCardId(cardId) {
  428. let ret = this.moveChecklistDialogOption.cardId == cardId;
  429. return ret;
  430. },
  431. boards() {
  432. const ret = Boards.find(
  433. {
  434. archived: false,
  435. 'members.userId': Meteor.userId(),
  436. _id: { $ne: Meteor.user().getTemplatesBoardId() },
  437. },
  438. {
  439. sort: { sort: 1 },
  440. },
  441. );
  442. return ret;
  443. },
  444. swimlanes() {
  445. const board = Boards.findOne(this.selectedBoardId.get());
  446. const ret = board.swimlanes();
  447. return ret;
  448. },
  449. lists() {
  450. const board = Boards.findOne(this.selectedBoardId.get());
  451. const ret = board.lists();
  452. return ret;
  453. },
  454. cards() {
  455. const list = Lists.findOne(this.selectedListId.get());
  456. const ret = list.cards(this.selectedSwimlaneId.get());
  457. return ret;
  458. },
  459. /** get the board data from the server
  460. * @param boardId get the board data of this board id
  461. */
  462. getBoardData(boardId) {
  463. const self = this;
  464. Meteor.subscribe('board', boardId, false, {
  465. onReady() {
  466. self.selectedBoardId.set(boardId);
  467. // reset swimlane id (for selection in cards())
  468. self.setFirstSwimlaneId();
  469. // reset list id (for selection in cards())
  470. self.setFirstListId();
  471. },
  472. });
  473. },
  474. events() {
  475. return [
  476. {
  477. 'click .js-done'() {
  478. const boardSelect = this.$('.js-select-boards')[0];
  479. const boardId = boardSelect.options[boardSelect.selectedIndex].value;
  480. const listSelect = this.$('.js-select-lists')[0];
  481. const listId = listSelect.options[listSelect.selectedIndex].value;
  482. const swimlaneSelect = this.$('.js-select-swimlanes')[0];
  483. const swimlaneId = swimlaneSelect.options[swimlaneSelect.selectedIndex].value;
  484. const cardSelect = this.$('.js-select-cards')[0];
  485. const cardId = cardSelect.options[cardSelect.selectedIndex].value;
  486. const options = {
  487. 'boardId' : boardId,
  488. 'swimlaneId' : swimlaneId,
  489. 'listId' : listId,
  490. 'cardId': cardId,
  491. }
  492. Meteor.user().setMoveChecklistDialogOption(this.currentBoardId, options);
  493. this.data().checklist.move(cardId);
  494. Popup.back(2);
  495. },
  496. 'change .js-select-boards'(event) {
  497. const boardId = $(event.currentTarget).val();
  498. this.getBoardData(boardId);
  499. },
  500. 'change .js-select-swimlanes'(event) {
  501. this.selectedSwimlaneId.set($(event.currentTarget).val());
  502. },
  503. 'change .js-select-lists'(event) {
  504. this.selectedListId.set($(event.currentTarget).val());
  505. },
  506. },
  507. ];
  508. },
  509. }).register('moveChecklistPopup');