sidebar.js 18 KB

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