sidebar.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. Sidebar = null;
  2. const defaultView = 'home';
  3. const viewTitles = {
  4. filter: 'filter-cards',
  5. search: 'search-cards',
  6. multiselection: 'multi-selection',
  7. customFields: 'custom-fields',
  8. archives: 'archives',
  9. };
  10. BlazeComponent.extendComponent({
  11. mixins() {
  12. return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
  13. },
  14. onCreated() {
  15. this._isOpen = new ReactiveVar(false);
  16. this._view = new ReactiveVar(defaultView);
  17. Sidebar = this;
  18. },
  19. onDestroyed() {
  20. Sidebar = null;
  21. },
  22. isOpen() {
  23. return this._isOpen.get();
  24. },
  25. open() {
  26. if (!this._isOpen.get()) {
  27. this._isOpen.set(true);
  28. EscapeActions.executeUpTo('detailsPane');
  29. }
  30. },
  31. hide() {
  32. if (this._isOpen.get()) {
  33. this._isOpen.set(false);
  34. }
  35. },
  36. toggle() {
  37. this._isOpen.set(!this._isOpen.get());
  38. },
  39. calculateNextPeak() {
  40. const sidebarElement = this.find('.js-board-sidebar-content');
  41. if (sidebarElement) {
  42. const altitude = sidebarElement.scrollHeight;
  43. this.callFirstWith(this, 'setNextPeak', altitude);
  44. }
  45. },
  46. reachNextPeak() {
  47. const activitiesComponent = this.childComponents('activities')[0];
  48. activitiesComponent.loadNextPage();
  49. },
  50. isTongueHidden() {
  51. return this.isOpen() && this.getView() !== defaultView;
  52. },
  53. scrollTop() {
  54. this.$('.js-board-sidebar-content').scrollTop(0);
  55. },
  56. getView() {
  57. return this._view.get();
  58. },
  59. setView(view) {
  60. view = _.isString(view) ? view : defaultView;
  61. if (this._view.get() !== view) {
  62. this._view.set(view);
  63. this.scrollTop();
  64. EscapeActions.executeUpTo('detailsPane');
  65. }
  66. this.open();
  67. },
  68. isDefaultView() {
  69. return this.getView() === defaultView;
  70. },
  71. getViewTemplate() {
  72. return `${this.getView()}Sidebar`;
  73. },
  74. getViewTitle() {
  75. return TAPi18n.__(viewTitles[this.getView()]);
  76. },
  77. showTongueTitle() {
  78. if (this.isOpen()) return `${TAPi18n.__('sidebar-close')}`;
  79. else return `${TAPi18n.__('sidebar-open')}`;
  80. },
  81. events() {
  82. return [
  83. {
  84. 'click .js-hide-sidebar': this.hide,
  85. 'click .js-toggle-sidebar': this.toggle,
  86. 'click .js-back-home': this.setView,
  87. 'click .js-toggle-minicard-label-text'() {
  88. Meteor.call('toggleMinicardLabelText');
  89. },
  90. 'click .js-shortcuts'() {
  91. FlowRouter.go('shortcuts');
  92. },
  93. },
  94. ];
  95. },
  96. }).register('sidebar');
  97. Blaze.registerHelper('Sidebar', () => Sidebar);
  98. Template.homeSidebar.helpers({
  99. hiddenMinicardLabelText() {
  100. return Meteor.user().hasHiddenMinicardLabelText();
  101. },
  102. });
  103. EscapeActions.register(
  104. 'sidebarView',
  105. () => {
  106. Sidebar.setView(defaultView);
  107. },
  108. () => {
  109. return Sidebar && Sidebar.getView() !== defaultView;
  110. },
  111. );
  112. Template.memberPopup.helpers({
  113. user() {
  114. return Users.findOne(this.userId);
  115. },
  116. memberType() {
  117. const type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal';
  118. if (type === 'normal') {
  119. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  120. const commentOnly = currentBoard.hasCommentOnly(this.userId);
  121. const noComments = currentBoard.hasNoComments(this.userId);
  122. if (commentOnly) {
  123. return TAPi18n.__('comment-only').toLowerCase();
  124. } else if (noComments) {
  125. return TAPi18n.__('no-comments').toLowerCase();
  126. } else {
  127. return TAPi18n.__(type).toLowerCase();
  128. }
  129. } else {
  130. return TAPi18n.__(type).toLowerCase();
  131. }
  132. },
  133. isInvited() {
  134. return Users.findOne(this.userId).isInvitedTo(Session.get('currentBoard'));
  135. },
  136. });
  137. Template.boardMenuPopup.events({
  138. 'click .js-rename-board': Popup.open('boardChangeTitle'),
  139. 'click .js-custom-fields'() {
  140. Sidebar.setView('customFields');
  141. Popup.close();
  142. },
  143. 'click .js-open-archives'() {
  144. Sidebar.setView('archives');
  145. Popup.close();
  146. },
  147. 'click .js-change-board-color': Popup.open('boardChangeColor'),
  148. 'click .js-change-language': Popup.open('changeLanguage'),
  149. 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
  150. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  151. currentBoard.archive();
  152. // XXX We should have some kind of notification on top of the page to
  153. // confirm that the board was successfully archived.
  154. FlowRouter.go('home');
  155. }),
  156. 'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
  157. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  158. Popup.close();
  159. Boards.remove(currentBoard._id);
  160. FlowRouter.go('home');
  161. }),
  162. 'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
  163. 'click .js-import-board': Popup.open('chooseBoardSource'),
  164. 'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
  165. });
  166. Template.boardMenuPopup.helpers({
  167. exportUrl() {
  168. const params = {
  169. boardId: Session.get('currentBoard'),
  170. };
  171. const queryParams = {
  172. authToken: Accounts._storedLoginToken(),
  173. };
  174. return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
  175. },
  176. exportFilename() {
  177. const boardId = Session.get('currentBoard');
  178. return `wekan-export-board-${boardId}.json`;
  179. },
  180. });
  181. Template.memberPopup.events({
  182. 'click .js-filter-member'() {
  183. Filter.members.toggle(this.userId);
  184. Popup.close();
  185. },
  186. 'click .js-change-role': Popup.open('changePermissions'),
  187. 'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
  188. const boardId = Session.get('currentBoard');
  189. const memberId = this.userId;
  190. Cards.find({ boardId, members: memberId }).forEach(card => {
  191. card.unassignMember(memberId);
  192. });
  193. Boards.findOne(boardId).removeMember(memberId);
  194. Popup.close();
  195. }),
  196. 'click .js-leave-member': Popup.afterConfirm('leaveBoard', () => {
  197. const boardId = Session.get('currentBoard');
  198. Meteor.call('quitBoard', boardId, () => {
  199. Popup.close();
  200. FlowRouter.go('home');
  201. });
  202. }),
  203. });
  204. Template.removeMemberPopup.helpers({
  205. user() {
  206. return Users.findOne(this.userId);
  207. },
  208. board() {
  209. return Boards.findOne(Session.get('currentBoard'));
  210. },
  211. });
  212. Template.leaveBoardPopup.helpers({
  213. board() {
  214. return Boards.findOne(Session.get('currentBoard'));
  215. },
  216. });
  217. Template.membersWidget.helpers({
  218. isInvited() {
  219. const user = Meteor.user();
  220. return user && user.isInvitedTo(Session.get('currentBoard'));
  221. },
  222. });
  223. Template.membersWidget.events({
  224. 'click .js-member': Popup.open('member'),
  225. 'click .js-open-board-menu': Popup.open('boardMenu'),
  226. 'click .js-manage-board-members': Popup.open('addMember'),
  227. 'click .js-import': Popup.open('boardImportBoard'),
  228. submit: this.onSubmit,
  229. 'click .js-import-board': Popup.open('chooseBoardSource'),
  230. 'click .js-open-archived-board'() {
  231. Modal.open('archivedBoards');
  232. },
  233. 'click .sandstorm-powerbox-request-identity'() {
  234. window.sandstormRequestIdentity();
  235. },
  236. 'click .js-member-invite-accept'() {
  237. const boardId = Session.get('currentBoard');
  238. Meteor.user().removeInvite(boardId);
  239. },
  240. 'click .js-member-invite-decline'() {
  241. const boardId = Session.get('currentBoard');
  242. Meteor.call('quitBoard', boardId, (err, ret) => {
  243. if (!err && ret) {
  244. Meteor.user().removeInvite(boardId);
  245. FlowRouter.go('home');
  246. }
  247. });
  248. },
  249. });
  250. BlazeComponent.extendComponent({
  251. integrations() {
  252. const boardId = Session.get('currentBoard');
  253. return Integrations.find({ boardId: `${boardId}` }).fetch();
  254. },
  255. integration(id) {
  256. const boardId = Session.get('currentBoard');
  257. return Integrations.findOne({ _id: id, boardId: `${boardId}` });
  258. },
  259. events() {
  260. return [
  261. {
  262. submit(evt) {
  263. evt.preventDefault();
  264. const url = evt.target.url.value;
  265. const boardId = Session.get('currentBoard');
  266. let id = null;
  267. let integration = null;
  268. if (evt.target.id) {
  269. id = evt.target.id.value;
  270. integration = this.integration(id);
  271. if (url) {
  272. Integrations.update(integration._id, {
  273. $set: {
  274. url: `${url}`,
  275. },
  276. });
  277. } else {
  278. Integrations.remove(integration._id);
  279. }
  280. } else if (url) {
  281. Integrations.insert({
  282. userId: Meteor.userId(),
  283. enabled: true,
  284. type: 'outgoing-webhooks',
  285. url: `${url}`,
  286. boardId: `${boardId}`,
  287. activities: ['all'],
  288. });
  289. }
  290. Popup.close();
  291. },
  292. },
  293. ];
  294. },
  295. }).register('outgoingWebhooksPopup');
  296. BlazeComponent.extendComponent({
  297. template() {
  298. return 'chooseBoardSource';
  299. },
  300. }).register('chooseBoardSourcePopup');
  301. Template.labelsWidget.events({
  302. 'click .js-label': Popup.open('editLabel'),
  303. 'click .js-add-label': Popup.open('createLabel'),
  304. });
  305. // Board members can assign people or labels by drag-dropping elements from the
  306. // sidebar to the cards on the board. In order to re-initialize the jquery-ui
  307. // plugin any time a draggable member or label is modified or removed we use a
  308. // autorun function and register a dependency on the both members and labels
  309. // fields of the current board document.
  310. function draggableMembersLabelsWidgets() {
  311. this.autorun(() => {
  312. const currentBoardId = Tracker.nonreactive(() => {
  313. return Session.get('currentBoard');
  314. });
  315. Boards.findOne(currentBoardId, {
  316. fields: {
  317. members: 1,
  318. labels: 1,
  319. },
  320. });
  321. Tracker.afterFlush(() => {
  322. const $draggables = this.$('.js-member,.js-label');
  323. $draggables.draggable({
  324. appendTo: 'body',
  325. helper: 'clone',
  326. revert: 'invalid',
  327. revertDuration: 150,
  328. snap: false,
  329. snapMode: 'both',
  330. start() {
  331. EscapeActions.executeUpTo('popup-back');
  332. },
  333. });
  334. function userIsMember() {
  335. return Meteor.user() && Meteor.user().isBoardMember();
  336. }
  337. this.autorun(() => {
  338. $draggables.draggable('option', 'disabled', !userIsMember());
  339. });
  340. });
  341. });
  342. }
  343. Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
  344. Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
  345. BlazeComponent.extendComponent({
  346. backgroundColors() {
  347. return Boards.simpleSchema()._schema.color.allowedValues;
  348. },
  349. isSelected() {
  350. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  351. return currentBoard.color === this.currentData().toString();
  352. },
  353. events() {
  354. return [
  355. {
  356. 'click .js-select-background'(evt) {
  357. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  358. const newColor = this.currentData().toString();
  359. currentBoard.setColor(newColor);
  360. evt.preventDefault();
  361. },
  362. },
  363. ];
  364. },
  365. }).register('boardChangeColorPopup');
  366. BlazeComponent.extendComponent({
  367. onCreated() {
  368. this.currentBoard = Boards.findOne(Session.get('currentBoard'));
  369. },
  370. allowsSubtasks() {
  371. return this.currentBoard.allowsSubtasks;
  372. },
  373. isBoardSelected() {
  374. return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
  375. },
  376. isNullBoardSelected() {
  377. return (
  378. this.currentBoard.subtasksDefaultBoardId === null ||
  379. this.currentBoard.subtasksDefaultBoardId === undefined
  380. );
  381. },
  382. boards() {
  383. return Boards.find(
  384. {
  385. archived: false,
  386. 'members.userId': Meteor.userId(),
  387. },
  388. {
  389. sort: ['title'],
  390. },
  391. );
  392. },
  393. lists() {
  394. return Lists.find(
  395. {
  396. boardId: this.currentBoard._id,
  397. archived: false,
  398. },
  399. {
  400. sort: ['title'],
  401. },
  402. );
  403. },
  404. hasLists() {
  405. return this.lists().count() > 0;
  406. },
  407. isListSelected() {
  408. return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
  409. },
  410. presentParentTask() {
  411. let result = this.currentBoard.presentParentTask;
  412. if (result === null || result === undefined) {
  413. result = 'no-parent';
  414. }
  415. return result;
  416. },
  417. events() {
  418. return [
  419. {
  420. 'click .js-field-has-subtasks'(evt) {
  421. evt.preventDefault();
  422. this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
  423. this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
  424. $('.js-field-has-subtasks .materialCheckBox').toggleClass(
  425. 'is-checked',
  426. this.currentBoard.allowsSubtasks,
  427. );
  428. $('.js-field-has-subtasks').toggleClass(
  429. 'is-checked',
  430. this.currentBoard.allowsSubtasks,
  431. );
  432. $('.js-field-deposit-board').prop(
  433. 'disabled',
  434. !this.currentBoard.allowsSubtasks,
  435. );
  436. },
  437. 'change .js-field-deposit-board'(evt) {
  438. let value = evt.target.value;
  439. if (value === 'null') {
  440. value = null;
  441. }
  442. this.currentBoard.setSubtasksDefaultBoardId(value);
  443. evt.preventDefault();
  444. },
  445. 'change .js-field-deposit-list'(evt) {
  446. this.currentBoard.setSubtasksDefaultListId(evt.target.value);
  447. evt.preventDefault();
  448. },
  449. 'click .js-field-show-parent-in-minicard'(evt) {
  450. const value =
  451. evt.target.id ||
  452. $(evt.target).parent()[0].id ||
  453. $(evt.target)
  454. .parent()[0]
  455. .parent()[0].id;
  456. const options = [
  457. 'prefix-with-full-path',
  458. 'prefix-with-parent',
  459. 'subtext-with-full-path',
  460. 'subtext-with-parent',
  461. 'no-parent',
  462. ];
  463. options.forEach(function(element) {
  464. if (element !== value) {
  465. $(`#${element} .materialCheckBox`).toggleClass(
  466. 'is-checked',
  467. false,
  468. );
  469. $(`#${element}`).toggleClass('is-checked', false);
  470. }
  471. });
  472. $(`#${value} .materialCheckBox`).toggleClass('is-checked', true);
  473. $(`#${value}`).toggleClass('is-checked', true);
  474. this.currentBoard.setPresentParentTask(value);
  475. evt.preventDefault();
  476. },
  477. },
  478. ];
  479. },
  480. }).register('boardSubtaskSettingsPopup');
  481. BlazeComponent.extendComponent({
  482. onCreated() {
  483. this.error = new ReactiveVar('');
  484. this.loading = new ReactiveVar(false);
  485. },
  486. onRendered() {
  487. this.find('.js-search-member input').focus();
  488. this.setLoading(false);
  489. },
  490. isBoardMember() {
  491. const userId = this.currentData()._id;
  492. const user = Users.findOne(userId);
  493. return user && user.isBoardMember();
  494. },
  495. isValidEmail(email) {
  496. return SimpleSchema.RegEx.Email.test(email);
  497. },
  498. setError(error) {
  499. this.error.set(error);
  500. },
  501. setLoading(w) {
  502. this.loading.set(w);
  503. },
  504. isLoading() {
  505. return this.loading.get();
  506. },
  507. inviteUser(idNameEmail) {
  508. const boardId = Session.get('currentBoard');
  509. this.setLoading(true);
  510. const self = this;
  511. Meteor.call('inviteUserToBoard', idNameEmail, boardId, (err, ret) => {
  512. self.setLoading(false);
  513. if (err) self.setError(err.error);
  514. else if (ret.email) self.setError('email-sent');
  515. else Popup.close();
  516. });
  517. },
  518. events() {
  519. return [
  520. {
  521. 'keyup input'() {
  522. this.setError('');
  523. },
  524. 'click .js-select-member'() {
  525. const userId = this.currentData()._id;
  526. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  527. if (!currentBoard.hasMember(userId)) {
  528. this.inviteUser(userId);
  529. }
  530. },
  531. 'click .js-email-invite'() {
  532. const idNameEmail = $('.js-search-member input').val();
  533. if (idNameEmail.indexOf('@') < 0 || this.isValidEmail(idNameEmail)) {
  534. this.inviteUser(idNameEmail);
  535. } else this.setError('email-invalid');
  536. },
  537. },
  538. ];
  539. },
  540. }).register('addMemberPopup');
  541. Template.changePermissionsPopup.events({
  542. 'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only'(
  543. event,
  544. ) {
  545. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  546. const memberId = this.userId;
  547. const isAdmin = $(event.currentTarget).hasClass('js-set-admin');
  548. const isCommentOnly = $(event.currentTarget).hasClass(
  549. 'js-set-comment-only',
  550. );
  551. const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
  552. currentBoard.setMemberPermission(
  553. memberId,
  554. isAdmin,
  555. isNoComments,
  556. isCommentOnly,
  557. );
  558. Popup.back(1);
  559. },
  560. });
  561. Template.changePermissionsPopup.helpers({
  562. isAdmin() {
  563. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  564. return currentBoard.hasAdmin(this.userId);
  565. },
  566. isNormal() {
  567. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  568. return (
  569. !currentBoard.hasAdmin(this.userId) &&
  570. !currentBoard.hasNoComments(this.userId) &&
  571. !currentBoard.hasCommentOnly(this.userId)
  572. );
  573. },
  574. isNoComments() {
  575. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  576. return (
  577. !currentBoard.hasAdmin(this.userId) &&
  578. currentBoard.hasNoComments(this.userId)
  579. );
  580. },
  581. isCommentOnly() {
  582. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  583. return (
  584. !currentBoard.hasAdmin(this.userId) &&
  585. currentBoard.hasCommentOnly(this.userId)
  586. );
  587. },
  588. isLastAdmin() {
  589. const currentBoard = Boards.findOne(Session.get('currentBoard'));
  590. return (
  591. currentBoard.hasAdmin(this.userId) && currentBoard.activeAdmins() === 1
  592. );
  593. },
  594. });