sidebar.js 19 KB

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