Просмотр исходного кода

Enforce a consistent ES6 coding style

Replace the old (and broken) jshint + jscsrc by eslint and configure
it to support some of the ES6 features.

The command `eslint` currently has one error which is a bug that was
discovered by its static analysis and should be fixed (usage of a
dead object).
Maxime Quandalle 9 лет назад
Родитель
Сommit
b3851817ec
60 измененных файлов с 1604 добавлено и 1692 удалено
  1. 157 0
      .eslintrc
  2. 0 73
      .jscsrc
  3. 0 91
      .jshintrc
  4. 4 1
      .meteor/packages
  5. 1 0
      .meteor/versions
  6. 37 38
      client/components/activities/activities.js
  7. 14 14
      client/components/activities/comments.js
  8. 16 16
      client/components/boards/boardArchive.js
  9. 46 46
      client/components/boards/boardBody.js
  10. 60 60
      client/components/boards/boardHeader.js
  11. 11 11
      client/components/boards/boardsList.js
  12. 12 12
      client/components/cards/attachments.js
  13. 51 51
      client/components/cards/cardDetails.js
  14. 57 57
      client/components/cards/labels.js
  15. 2 2
      client/components/cards/minicard.js
  16. 42 41
      client/components/lists/list.js
  17. 40 40
      client/components/lists/listBody.js
  18. 31 31
      client/components/lists/listHeader.js
  19. 29 28
      client/components/main/editor.js
  20. 5 5
      client/components/main/header.js
  21. 0 65
      client/components/main/helpers.js
  22. 8 8
      client/components/main/layouts.js
  23. 10 10
      client/components/main/popup.js
  24. 12 12
      client/components/mixins/infiniteScrolling.js
  25. 5 7
      client/components/mixins/perfectScrollbar.js
  26. 99 95
      client/components/sidebar/sidebar.js
  27. 17 17
      client/components/sidebar/sidebarArchives.js
  28. 50 50
      client/components/sidebar/sidebarFilters.js
  29. 62 62
      client/components/users/userAvatar.js
  30. 21 22
      client/components/users/userHeader.js
  31. 12 14
      client/config/accounts.js
  32. 44 4
      client/config/blazeHelpers.js
  33. 2 3
      client/config/reactiveTabs.js
  34. 19 19
      client/config/router.js
  35. 13 15
      client/lib/cssEvents.js
  36. 16 56
      client/lib/escapeActions.js
  37. 42 46
      client/lib/filter.js
  38. 4 5
      client/lib/i18n.js
  39. 16 16
      client/lib/inlinedform.js
  40. 9 9
      client/lib/keyboard.js
  41. 1 1
      client/lib/modal.js
  42. 46 47
      client/lib/multiSelection.js
  43. 60 63
      client/lib/popup.js
  44. 8 12
      client/lib/unsavedEdits.js
  45. 37 29
      client/lib/utils.js
  46. 12 12
      collections/activities.js
  47. 18 18
      collections/attachments.js
  48. 9 9
      collections/avatars.js
  49. 68 68
      collections/boards.js
  50. 72 73
      collections/cards.js
  51. 22 22
      collections/lists.js
  52. 6 6
      collections/unsavedEdits.js
  53. 49 53
      collections/users.js
  54. 28 30
      sandstorm.js
  55. 1 1
      server/lib/utils.js
  56. 43 43
      server/migrations.js
  57. 10 15
      server/publications/activities.js
  58. 36 36
      server/publications/boards.js
  59. 1 1
      server/publications/cards.js
  60. 1 1
      server/publications/unsavedEdits.js

+ 157 - 0
.eslintrc

@@ -0,0 +1,157 @@
+ecmaFeatures:
+  experimentalObjectRestSpread: true
+rules:
+  indent:
+    - 2
+    - 2
+  semi:
+    - 2
+    - always
+  comma-dangle:
+    - 2
+    - always-multiline
+  no-inner-declarations:
+    - 0
+  dot-notation:
+    - 2
+  eqeqeq:
+    - 2
+  no-eval:
+    - 2
+  radix:
+    - 2
+
+  # Stylistic Issues
+  camelcase:
+    - 2
+  comma-spacing:
+    - 2
+  comma-style:
+    - 2
+  new-parens:
+    - 2
+  no-lonely-if:
+    - 2
+  no-multiple-empty-lines:
+    - 2
+  no-nested-ternary:
+    - 2
+  linebreak-style:
+    - 2
+    - unix
+  quotes:
+    - 2
+    - single
+  semi-spacing:
+    - 2
+  spaced-comment:
+    - 2
+    - always
+    - markers:
+      - '/'
+  space-unary-ops:
+    - 2
+
+  # ECMAScript 6
+  arrow-parens:
+    - 2
+  arrow-spacing:
+    - 2
+  no-class-assign:
+    - 2
+  no-dupe-class-members:
+    - 2
+  no-var:
+    - 2
+  object-shorthand:
+    - 2
+  prefer-const:
+    - 2
+  prefer-template:
+    - 2
+  prefer-spread:
+    - 2
+globals:
+  # Meteor globals
+  Meteor: false
+  DDP: false
+  Mongo: false
+  Session: false
+  Accounts: false
+  Template: false
+  Blaze: false
+  UI: false
+  Match: false
+  check: false
+  Tracker: false
+  Deps: false
+  ReactiveVar: false
+  EJSON: false
+  HTTP: false
+  Email: false
+  Assets: false
+  Handlebars: false
+  Package: false
+  App: false
+  Npm: false
+  Tinytest: false
+  Random: false
+  HTML: false
+
+  # Exported by packages we use
+  '$': false
+  _: false
+  autosize: false
+  Avatar: true
+  Avatars: true
+  BlazeComponent: false
+  BlazeLayout: false
+  FlowRouter: false
+  FS: false
+  getSlug: false
+  Migrations: false
+  Mousetrap: false
+  Picker: false
+  Presence: true
+  Presences: true
+  Ps: true
+  ReactiveTabs: false
+  SimpleSchema: false
+  SubsManager: false
+  T9n: false
+  TAPi18n: false
+
+  # Our collections
+  AccountsTemplates: true
+  Activities: true
+  Attachments: true
+  Boards: true
+  CardComments: true
+  Cards: true
+  Lists: true
+  UnsavedEditCollection: true
+  Users: true
+
+  # Our objects
+  CSSEvents: true
+  EscapeActions: true
+  Filter: true
+  Filter: true
+  Mixins: true
+  Modal: true
+  MultiSelection: true
+  Popup: true
+  Sidebar: true
+  Utils: true
+  InlinedForm: true
+  UnsavedEdits: true
+
+  # XXX Temp, we should remove these
+  allowIsBoardAdmin: true
+  allowIsBoardMember: true
+  Emoji: true
+env:
+  es6: true
+  node: true
+  browser: true
+extends: 'eslint:recommended'

+ 0 - 73
.jscsrc

@@ -1,73 +0,0 @@
-{
-  "disallowSpacesInNamedFunctionExpression": {
-    "beforeOpeningRoundBrace": true
-  },
-  "disallowSpacesInFunctionExpression": {
-    "beforeOpeningRoundBrace": true
-  },
-  "disallowSpacesInAnonymousFunctionExpression": {
-    "beforeOpeningRoundBrace": true
-  },
-  "disallowSpacesInFunctionDeclaration": {
-    "beforeOpeningRoundBrace": true
-  },
-  "disallowEmptyBlocks": true,
-  "disallowSpacesInsideArrayBrackets": true,
-  "disallowSpacesInsideParentheses": true,
-  "disallowQuotedKeysInObjects": "allButReserved",
-  "disallowSpaceAfterObjectKeys": true,
-  "disallowSpaceAfterPrefixUnaryOperators": [
-    "++",
-    "--",
-    "+",
-    "-",
-    "~"
-  ],
-  "disallowSpaceBeforePostfixUnaryOperators": true,
-  "disallowSpaceBeforeBinaryOperators": [
-    ","
-  ],
-  "disallowMixedSpacesAndTabs": true,
-  "disallowTrailingWhitespace": true,
-  "disallowTrailingComma": true,
-  "disallowYodaConditions": true,
-  "disallowKeywords": [ "with" ],
-  "disallowMultipleLineBreaks": true,
-  "disallowMultipleVarDecl": "exceptUndefined",
-  "requireSpaceBeforeBlockStatements": true,
-  "requireParenthesesAroundIIFE": true,
-  "requireSpacesInConditionalExpression": true,
-  "requireBlocksOnNewline": 1,
-  "requireCommaBeforeLineBreak": true,
-  "requireSpaceAfterPrefixUnaryOperators": [
-    "!"
-  ],
-  "requireSpaceBeforeBinaryOperators": true,
-  "requireSpaceAfterBinaryOperators": true,
-  "requireCamelCaseOrUpperCaseIdentifiers": true,
-  "requireLineFeedAtFileEnd": true,
-  "requireCapitalizedConstructors": true,
-  "requireDotNotation": true,
-  "requireSpacesInForStatement": true,
-  "requireSpaceBetweenArguments": true,
-  "requireCurlyBraces": [
-    "do"
-  ],
-  "requireSpaceAfterKeywords": [
-    "if",
-    "else",
-    "for",
-    "while",
-    "do",
-    "switch",
-    "case",
-    "return",
-    "try",
-    "catch",
-    "typeof"
-  ],
-  "validateLineBreaks": "LF",
-  "validateQuoteMarks": "'",
-  "validateIndentation": 2,
-  "maximumLineLength": 80
-}

+ 0 - 91
.jshintrc

@@ -1,91 +0,0 @@
-{
-  // JSHint options: http://jshint.com/docs/options/
-  "maxerr": 50,
-
-  // Enforcing
-  "camelcase": true,
-  "eqeqeq": true,
-  "undef": true,
-  "unused": true,
-
-  // Environments
-  "browser": true,
-  "devel": true,
-
-  // Authorized globals
-  "globals": {
-    // Meteor globals
-    "Meteor": false,
-    "DDP": false,
-    "Mongo": false,
-    "Session": false,
-    "Accounts": false,
-    "Template": false,
-    "Blaze": false,
-    "UI": false,
-    "Match": false,
-    "check": false,
-    "Tracker": false,
-    "Deps": false,
-    "ReactiveVar": false,
-    "EJSON": false,
-    "HTTP": false,
-    "Email": false,
-    "Assets": false,
-    "Handlebars": false,
-    "Package": false,
-    "App": false,
-    "Npm": false,
-    "Tinytest": false,
-    "Random": false,
-    "HTML": false,
-
-    // Exported by packages we use
-    "_": false,
-    "$": false,
-    "autosize": false,
-    "Router": false,
-    "SimpleSchema": false,
-    "getSlug": false,
-    "Migrations": false,
-    "FS": false,
-    "BlazeComponent": false,
-    "TAPi18n": false,
-    "T9n": false,
-    "SubsManager": false,
-    "Mousetrap": false,
-    "Avatar": true,
-    "Avatars": true,
-    "Ps": true,
-    "Presence": true,
-    "Presences": true,
-
-    // Our collections
-    "Boards": true,
-    "Lists": true,
-    "Cards": true,
-    "CardComments": true,
-    "Activities": true,
-    "Attachments": true,
-    "Users": true,
-    "AccountsTemplates": true,
-
-    // Our objects
-    "CSSEvents": true,
-    "EscapeActions": true,
-    "Filter": true,
-    "Filter": true,
-    "Mixins": true,
-    "Modal": true,
-    "MultiSelection": true,
-    "Popup": true,
-    "Sidebar": true,
-    "Utils": true,
-    "InlinedForm": true,
-
-    // XXX Temp, we should remove these
-    "allowIsBoardAdmin": true,
-    "allowIsBoardMember": true,
-    "Emoji": true
-  }
-}

+ 4 - 1
.meteor/packages

@@ -9,12 +9,14 @@
 meteor-base
 
 # Build system
-es5-shim
 ecmascript
 standard-minifiers
 mquandalle:jade
 mquandalle:stylus
 
+# Polyfills
+es5-shim
+
 # Collections
 mongo
 aldeed:collection2
@@ -63,6 +65,7 @@ fortawesome:fontawesome
 mousetrap:mousetrap
 mquandalle:jquery-textcomplete
 mquandalle:jquery-ui-drag-drop-sort
+mquandalle:mousetrap-bindglobal
 mquandalle:perfect-scrollbar
 peerlibrary:blaze-components
 perak:markdown

+ 1 - 0
.meteor/versions

@@ -90,6 +90,7 @@ mquandalle:jade-compiler@0.4.3
 mquandalle:jquery-textcomplete@0.3.9_1
 mquandalle:jquery-ui-drag-drop-sort@0.1.0
 mquandalle:moment@1.0.0
+mquandalle:mousetrap-bindglobal@0.0.1
 mquandalle:perfect-scrollbar@0.6.5_2
 mquandalle:stylus@1.1.1
 npm-bcrypt@0.7.8_2

+ 37 - 38
client/components/activities/activities.js

@@ -1,99 +1,98 @@
-var activitiesPerPage = 20;
+const activitiesPerPage = 20;
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'activities';
   },
 
-  onCreated: function() {
-    var self = this;
+  onCreated() {
     // XXX Should we use ReactiveNumber?
-    self.page = new ReactiveVar(1);
-    self.loadNextPageLocked = false;
-    var sidebar = self.componentParent(); // XXX for some reason not working
+    this.page = new ReactiveVar(1);
+    this.loadNextPageLocked = false;
+    const sidebar = this.componentParent(); // XXX for some reason not working
     sidebar.callFirstWith(null, 'resetNextPeak');
-    self.autorun(function() {
-      var mode = self.data().mode;
-      var capitalizedMode = Utils.capitalize(mode);
-      var id = Session.get('current' + capitalizedMode);
-      var limit = self.page.get() * activitiesPerPage;
+    this.autorun(() => {
+      const mode = this.data().mode;
+      const capitalizedMode = Utils.capitalize(mode);
+      const id = Session.get(`current${capitalizedMode}`);
+      const limit = this.page.get() * activitiesPerPage;
       if (id === null)
         return;
 
-      self.subscribe('activities', mode, id, limit, function() {
-        self.loadNextPageLocked = false;
+      this.subscribe('activities', mode, id, limit, () => {
+        this.loadNextPageLocked = false;
 
         // If the sibear peak hasn't increased, that mean that there are no more
         // activities, and we can stop calling new subscriptions.
         // XXX This is hacky! We need to know excatly and reactively how many
         // activities there are, we probably want to denormalize this number
         // dirrectly into card and board documents.
-        var a = sidebar.callFirstWith(null, 'getNextPeak');
+        const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
         sidebar.calculateNextPeak();
-        var b = sidebar.callFirstWith(null, 'getNextPeak');
-        if (a === b) {
+        const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
+        if (nextPeakBefore === nextPeakAfter) {
           sidebar.callFirstWith(null, 'resetNextPeak');
         }
       });
     });
   },
 
-  loadNextPage: function() {
+  loadNextPage() {
     if (this.loadNextPageLocked === false) {
       this.page.set(this.page.get() + 1);
       this.loadNextPageLocked = true;
     }
   },
 
-  boardLabel: function() {
+  boardLabel() {
     return TAPi18n.__('this-board');
   },
 
-  cardLabel: function() {
+  cardLabel() {
     return TAPi18n.__('this-card');
   },
 
-  cardLink: function() {
-    var card = this.currentData().card();
+  cardLink() {
+    const card = this.currentData().card();
     return card && Blaze.toHTML(HTML.A({
       href: card.absoluteUrl(),
-      'class': 'action-card'
+      'class': 'action-card',
     }, card.title));
   },
 
-  memberLink: function() {
+  memberLink() {
     return Blaze.toHTMLWithData(Template.memberName, {
-      user: this.currentData().member()
+      user: this.currentData().member(),
     });
   },
 
-  attachmentLink: function() {
-    var attachment = this.currentData().attachment();
+  attachmentLink() {
+    const attachment = this.currentData().attachment();
     return attachment && Blaze.toHTML(HTML.A({
       href: attachment.url(),
-      'class': 'js-open-attachment-viewer'
+      'class': 'js-open-attachment-viewer',
     }, attachment.name()));
   },
 
-  events: function() {
+  events() {
     return [{
       // XXX We should use Popup.afterConfirmation here
-      'click .js-delete-comment': function() {
-        var commentId = this.currentData().commentId;
+      'click .js-delete-comment'() {
+        const commentId = this.currentData().commentId;
         CardComments.remove(commentId);
       },
-      'submit .js-edit-comment': function(evt) {
+      'submit .js-edit-comment'(evt) {
         evt.preventDefault();
-        var commentText = this.currentComponent().getValue();
-        var commentId = Template.parentData().commentId;
+        const commentText = this.currentComponent().getValue();
+        const commentId = Template.parentData().commentId;
         if ($.trim(commentText)) {
           CardComments.update(commentId, {
             $set: {
-              text: commentText
-            }
+              text: commentText,
+            },
           });
         }
-      }
+      },
     }];
-  }
+  },
 }).register('activities');

+ 14 - 14
client/components/activities/comments.js

@@ -1,4 +1,4 @@
-let commentFormIsOpen = new ReactiveVar(false);
+const commentFormIsOpen = new ReactiveVar(false);
 
 BlazeComponent.extendComponent({
   template() {
@@ -19,16 +19,16 @@ BlazeComponent.extendComponent({
 
   events() {
     return [{
-      'click .js-new-comment:not(.focus)': function() {
+      'click .js-new-comment:not(.focus)'() {
         commentFormIsOpen.set(true);
       },
-      'submit .js-new-comment-form': function(evt) {
-        let input = this.getInput();
+      'submit .js-new-comment-form'(evt) {
+        const input = this.getInput();
         if ($.trim(input.val())) {
           CardComments.insert({
             boardId: this.currentData().boardId,
             cardId: this.currentData()._id,
-            text: input.val()
+            text: input.val(),
           });
           resetCommentInput(input);
           Tracker.flush();
@@ -37,13 +37,13 @@ BlazeComponent.extendComponent({
         evt.preventDefault();
       },
       // Pressing Ctrl+Enter should submit the form
-      'keydown form textarea': function(evt) {
+      'keydown form textarea'(evt) {
         if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
           this.find('button[type=submit]').click();
         }
-      }
+      },
     }];
-  }
+  },
 }).register('commentForm');
 
 // XXX This should be a static method of the `commentForm` component
@@ -63,15 +63,15 @@ Tracker.autorun(() => {
   Tracker.afterFlush(() => {
     autosize.update($('.js-new-comment-input'));
   });
-})
+});
 
 EscapeActions.register('inlinedForm',
-  function() {
+  () => {
     const draftKey = {
       fieldName: 'cardComment',
-      docId: Session.get('currentCard')
+      docId: Session.get('currentCard'),
     };
-    let commentInput = $('.js-new-comment-input');
+    const commentInput = $('.js-new-comment-input');
     if ($.trim(commentInput.val())) {
       UnsavedEdits.set(draftKey, commentInput.val());
     } else {
@@ -79,7 +79,7 @@ EscapeActions.register('inlinedForm',
     }
     resetCommentInput(commentInput);
   },
-  function() { return commentFormIsOpen.get(); }, {
-    noClickEscapeOn: '.js-new-comment'
+  () => { return commentFormIsOpen.get(); }, {
+    noClickEscapeOn: '.js-new-comment',
   }
 );

+ 16 - 16
client/components/boards/boardArchive.js

@@ -1,8 +1,8 @@
 Template.headerTitle.events({
-  'click .js-open-archived-board': function() {
-    Modal.open('archivedBoards')
-  }
-})
+  'click .js-open-archived-board'() {
+    Modal.open('archivedBoards');
+  },
+});
 
 BlazeComponent.extendComponent({
   template() {
@@ -10,26 +10,26 @@ BlazeComponent.extendComponent({
   },
 
   onCreated() {
-    this.subscribe('archivedBoards')
+    this.subscribe('archivedBoards');
   },
 
   archivedBoards() {
     return Boards.find({ archived: true }, {
-      sort: ['title']
-    })
+      sort: ['title'],
+    });
   },
 
   events() {
     return [{
-      'click .js-restore-board': function() {
-        let boardId = this.currentData()._id
+      'click .js-restore-board'() {
+        const boardId = this.currentData()._id;
         Boards.update(boardId, {
           $set: {
-            archived: false
-          }
-        })
-        Utils.goBoardId(boardId)
-      }
-    }]
+            archived: false,
+          },
+        });
+        Utils.goBoardId(boardId);
+      },
+    }];
   },
-}).register('archivedBoards')
+}).register('archivedBoards');

+ 46 - 46
client/components/boards/boardBody.js

@@ -1,11 +1,11 @@
-var subManager = new SubsManager();
+const subManager = new SubsManager();
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'board';
   },
 
-  onCreated: function() {
+  onCreated() {
     this.draggingActive = new ReactiveVar(false);
     this.showOverlay = new ReactiveVar(false);
     this.isBoardReady = new ReactiveVar(false);
@@ -15,15 +15,15 @@ BlazeComponent.extendComponent({
     // XXX The boardId should be readed from some sort the component "props",
     // unfortunatly, Blaze doesn't have this notion.
     this.autorun(() => {
-      let currentBoardId = Session.get('currentBoard');
-      if (! currentBoardId)
+      const currentBoardId = Session.get('currentBoard');
+      if (!currentBoardId)
         return;
-      var handle = subManager.subscribe('board', currentBoardId);
+      const handle = subManager.subscribe('board', currentBoardId);
       Tracker.nonreactive(() => {
         Tracker.autorun(() => {
           this.isBoardReady.set(handle.ready());
-        })
-      })
+        });
+      });
     });
 
     this._isDragging = false;
@@ -33,52 +33,52 @@ BlazeComponent.extendComponent({
     this.mouseHasEnterCardDetails = false;
   },
 
-  openNewListForm: function() {
+  openNewListForm() {
     this.componentChildren('addListForm')[0].open();
   },
 
   // XXX Flow components allow us to avoid creating these two setter methods by
   // exposing a public API to modify the component state. We need to investigate
   // best practices here.
-  setIsDragging: function(bool) {
+  setIsDragging(bool) {
     this.draggingActive.set(bool);
   },
 
-  scrollLeft: function(position = 0) {
+  scrollLeft(position = 0) {
     this.$('.js-lists').animate({
-      scrollLeft: position
+      scrollLeft: position,
     });
   },
 
-  currentCardIsInThisList: function() {
-    var currentCard = Cards.findOne(Session.get('currentCard'));
-    var listId = this.currentData()._id;
+  currentCardIsInThisList() {
+    const currentCard = Cards.findOne(Session.get('currentCard'));
+    const listId = this.currentData()._id;
     return currentCard && currentCard.listId === listId;
   },
 
-  events: function() {
+  events() {
     return [{
       // XXX The board-overlay div should probably be moved to the parent
       // component.
-      'mouseenter .board-overlay': function() {
+      'mouseenter .board-overlay'() {
         if (this.mouseHasEnterCardDetails) {
           this.showOverlay.set(false);
         }
       },
 
       // Click-and-drag action
-      'mousedown .board-canvas': function(evt) {
+      'mousedown .board-canvas'(evt) {
         if ($(evt.target).closest('a,.js-list-header').length === 0) {
           this._isDragging = true;
           this._lastDragPositionX = evt.clientX;
         }
       },
-      'mouseup': function(evt) {
+      'mouseup'() {
         if (this._isDragging) {
           this._isDragging = false;
         }
       },
-      'mousemove': function(evt) {
+      'mousemove'(evt) {
         if (this._isDragging) {
           // Update the canvas position
           this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
@@ -91,40 +91,40 @@ BlazeComponent.extendComponent({
           EscapeActions.executeUpTo('popup-close');
           EscapeActions.preventNextClick();
         }
-      }
+      },
     }];
-  }
+  },
 }).register('board');
 
 Template.boardBody.onRendered(function() {
-  var self = BlazeComponent.getComponentForElement(this.firstNode);
+  const self = BlazeComponent.getComponentForElement(this.firstNode);
 
   self.listsDom = this.find('.js-lists');
 
-  if (! Session.get('currentCard')) {
+  if (!Session.get('currentCard')) {
     self.scrollLeft();
   }
 
   // We want to animate the card details window closing. We rely on CSS
   // transition for the actual animation.
   self.listsDom._uihooks = {
-    removeElement: function(node) {
-      var removeNode = _.once(function() {
+    removeElement(node) {
+      const removeNode = _.once(() => {
         node.parentNode.removeChild(node);
       });
       if ($(node).hasClass('js-card-details')) {
         $(node).css({
           flexBasis: 0,
-          padding: 0
+          padding: 0,
         });
         $(self.listsDom).one(CSSEvents.transitionend, removeNode);
       } else {
         removeNode();
       }
-    }
+    },
   };
 
-  if (! Meteor.user() || ! Meteor.user().isBoardMember())
+  if (!Meteor.user() || !Meteor.user().isBoardMember())
     return;
 
   self.$(self.listsDom).sortable({
@@ -134,63 +134,63 @@ Template.boardBody.onRendered(function() {
     items: '.js-list:not(.js-list-composer)',
     placeholder: 'list placeholder',
     distance: 7,
-    start: function(evt, ui) {
+    start(evt, ui) {
       ui.placeholder.height(ui.helper.height());
       Popup.close();
     },
-    stop: function() {
+    stop() {
       self.$('.js-lists').find('.js-list:not(.js-list-composer)').each(
-        function(i, list) {
-          var data = Blaze.getData(list);
+        (i, list) => {
+          const data = Blaze.getData(list);
           Lists.update(data._id, {
             $set: {
-              sort: i
-            }
+              sort: i,
+            },
           });
         }
       );
-    }
+    },
   });
 
   // Disable drag-dropping while in multi-selection mode
-  self.autorun(function() {
+  self.autorun(() => {
     self.$(self.listsDom).sortable('option', 'disabled',
       MultiSelection.isActive());
   });
 
   // If there is no data in the board (ie, no lists) we autofocus the list
   // creation form by clicking on the corresponding element.
-  var currentBoard = Boards.findOne(Session.get('currentBoard'));
+  const currentBoard = Boards.findOne(Session.get('currentBoard'));
   if (currentBoard.lists().count() === 0) {
     self.openNewListForm();
   }
 });
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'addListForm';
   },
 
   // Proxy
-  open: function() {
+  open() {
     this.componentChildren('inlinedForm')[0].open();
   },
 
-  events: function() {
+  events() {
     return [{
-      submit: function(evt) {
+      submit(evt) {
         evt.preventDefault();
-        var title = this.find('.list-name-input');
+        const title = this.find('.list-name-input');
         if ($.trim(title.value)) {
           Lists.insert({
             title: title.value,
             boardId: Session.get('currentBoard'),
-            sort: $('.list').length
+            sort: $('.list').length,
           });
 
           title.value = '';
         }
-      }
+      },
     }];
-  }
+  },
 }).register('addListForm');

+ 60 - 60
client/components/boards/boardHeader.js

@@ -1,143 +1,143 @@
 Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
-  'click .js-open-archives': function() {
+  'click .js-open-archives'() {
     Sidebar.setView('archives');
     Popup.close();
   },
   'click .js-change-board-color': Popup.open('boardChangeColor'),
   'click .js-change-language': Popup.open('changeLanguage'),
-  'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
-    var boardId = Session.get('currentBoard');
+  'click .js-archive-board ': Popup.afterConfirm('archiveBoard', () => {
+    const boardId = Session.get('currentBoard');
     Boards.update(boardId, { $set: { archived: true }});
     // XXX We should have some kind of notification on top of the page to
     // confirm that the board was successfully archived.
     FlowRouter.go('home');
-  })
+  }),
 });
 
 Template.boardChangeTitlePopup.events({
-  submit: function(evt, t) {
-    var title = t.$('.js-board-name').val().trim();
+  submit(evt, tpl) {
+    const title = tpl.$('.js-board-name').val().trim();
     if (title) {
       Boards.update(this._id, {
         $set: {
-          title: title
-        }
+          title,
+        },
       });
       Popup.close();
     }
     evt.preventDefault();
-  }
+  },
 });
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'headerBoard';
   },
 
-  isStarred: function() {
-    var boardId = Session.get('currentBoard');
-    var user = Meteor.user();
+  isStarred() {
+    const boardId = Session.get('currentBoard');
+    const user = Meteor.user();
     return user && user.hasStarred(boardId);
   },
 
   // Only show the star counter if the number of star is greater than 2
-  showStarCounter: function() {
-    var currentBoard = this.currentData();
+  showStarCounter() {
+    const currentBoard = this.currentData();
     return currentBoard && currentBoard.stars >= 2;
   },
 
-  events: function() {
+  events() {
     return [{
       'click .js-edit-board-title': Popup.open('boardChangeTitle'),
-      'click .js-star-board': function() {
+      'click .js-star-board'() {
         Meteor.user().toggleBoardStar(Session.get('currentBoard'));
       },
       'click .js-open-board-menu': Popup.open('boardMenu'),
       'click .js-change-visibility': Popup.open('boardChangeVisibility'),
-      'click .js-open-filter-view': function() {
+      'click .js-open-filter-view'() {
         Sidebar.setView('filter');
       },
-      'click .js-filter-reset': function(evt) {
+      'click .js-filter-reset'(evt) {
         evt.stopPropagation();
         Sidebar.setView();
         Filter.reset();
       },
-      'click .js-multiselection-activate': function() {
-        var currentCard = Session.get('currentCard');
+      'click .js-multiselection-activate'() {
+        const currentCard = Session.get('currentCard');
         MultiSelection.activate();
         if (currentCard) {
           MultiSelection.add(currentCard);
         }
       },
-      'click .js-multiselection-reset': function(evt) {
+      'click .js-multiselection-reset'(evt) {
         evt.stopPropagation();
         MultiSelection.disable();
-      }
+      },
     }];
-  }
+  },
 }).register('headerBoard');
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'boardChangeColorPopup';
   },
 
-  backgroundColors: function() {
+  backgroundColors() {
     return Boards.simpleSchema()._schema.color.allowedValues;
   },
 
-  isSelected: function() {
-    var currentBoard = Boards.findOne(Session.get('currentBoard'));
+  isSelected() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
     return currentBoard.color === this.currentData().toString();
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-select-background': function(evt) {
-        var currentBoardId = Session.get('currentBoard');
+      'click .js-select-background'(evt) {
+        const currentBoardId = Session.get('currentBoard');
         Boards.update(currentBoardId, {
           $set: {
-            color: this.currentData().toString()
-          }
+            color: this.currentData().toString(),
+          },
         });
         evt.preventDefault();
-      }
+      },
     }];
-  }
+  },
 }).register('boardChangeColorPopup');
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'createBoardPopup';
   },
 
-  onCreated: function() {
+  onCreated() {
     this.visibilityMenuIsOpen = new ReactiveVar(false);
     this.visibility = new ReactiveVar('private');
   },
 
-  visibilityCheck: function() {
+  visibilityCheck() {
     return this.currentData() === this.visibility.get();
   },
 
-  setVisibility: function(visibility) {
+  setVisibility(visibility) {
     this.visibility.set(visibility);
     this.visibilityMenuIsOpen.set(false);
   },
 
-  toogleVisibilityMenu: function() {
-    this.visibilityMenuIsOpen.set(! this.visibilityMenuIsOpen.get());
+  toogleVisibilityMenu() {
+    this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
   },
 
-  onSubmit: function(evt) {
+  onSubmit(evt) {
     evt.preventDefault();
-    var title = this.find('.js-new-board-title').value;
-    var visibility = this.visibility.get();
+    const title = this.find('.js-new-board-title').value;
+    const visibility = this.visibility.get();
 
-    var boardId = Boards.insert({
-      title: title,
-      permission: visibility
+    const boardId = Boards.insert({
+      title,
+      permission: visibility,
     });
 
     Utils.goBoardId(boardId);
@@ -146,39 +146,39 @@ BlazeComponent.extendComponent({
     Meteor.user().toggleBoardStar(boardId);
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-select-visibility': function() {
+      'click .js-select-visibility'() {
         this.setVisibility(this.currentData());
       },
       'click .js-change-visibility': this.toogleVisibilityMenu,
-      submit: this.onSubmit
+      submit: this.onSubmit,
     }];
-  }
+  },
 }).register('createBoardPopup');
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'boardChangeVisibilityPopup';
   },
 
-  visibilityCheck: function() {
-    var currentBoard = Boards.findOne(Session.get('currentBoard'));
+  visibilityCheck() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
     return this.currentData() === currentBoard.permission;
   },
 
-  selectBoardVisibility: function() {
+  selectBoardVisibility() {
     Boards.update(Session.get('currentBoard'), {
       $set: {
-        permission: this.currentData()
-      }
+        permission: this.currentData(),
+      },
     });
     Popup.close();
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-select-visibility': this.selectBoardVisibility
+      'click .js-select-visibility': this.selectBoardVisibility,
     }];
-  }
+  },
 }).register('boardChangeVisibilityPopup');

+ 11 - 11
client/components/boards/boardsList.js

@@ -1,30 +1,30 @@
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'boardList';
   },
 
-  boards: function() {
+  boards() {
     return Boards.find({
       archived: false,
-      'members.userId': Meteor.userId()
+      'members.userId': Meteor.userId(),
     }, {
-      sort: ['title']
+      sort: ['title'],
     });
   },
 
-  isStarred: function() {
-    var user = Meteor.user();
+  isStarred() {
+    const user = Meteor.user();
     return user && user.hasStarred(this.currentData()._id);
   },
 
-  events: function() {
+  events() {
     return [{
       'click .js-add-board': Popup.open('createBoard'),
-      'click .js-star-board': function(evt) {
-        var boardId = this.currentData()._id;
+      'click .js-star-board'(evt) {
+        const boardId = this.currentData()._id;
         Meteor.user().toggleBoardStar(boardId);
         evt.preventDefault();
-      }
+      },
     }];
-  }
+  },
 }).register('boardList');

+ 12 - 12
client/components/cards/attachments.js

@@ -1,32 +1,32 @@
 Template.attachmentsGalery.events({
   'click .js-add-attachment': Popup.open('cardAttachments'),
   'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete',
-    function() {
+    () => {
       Attachments.remove(this._id);
       Popup.close();
     }
   ),
   // If we let this event bubble, FlowRouter will handle it and empty the page
   // content, see #101.
-  'click .js-download': function(event) {
+  'click .js-download'(event) {
     event.stopPropagation();
   },
-  'click .js-open-viewer': function() {
+  'click .js-open-viewer'() {
     // XXX Not implemented!
   },
-  'click .js-add-cover': function() {
+  'click .js-add-cover'() {
     Cards.update(this.cardId, { $set: { coverId: this._id } });
   },
-  'click .js-remove-cover': function() {
+  'click .js-remove-cover'() {
     Cards.update(this.cardId, { $unset: { coverId: '' } });
-  }
+  },
 });
 
 Template.cardAttachmentsPopup.events({
-  'change .js-attach-file': function(evt) {
-    var card = this;
-    FS.Utility.eachFile(evt, function(f) {
-      var file = new FS.File(f);
+  'change .js-attach-file'(evt) {
+    const card = this;
+    FS.Utility.eachFile(evt, (f) => {
+      const file = new FS.File(f);
       file.boardId = card.boardId;
       file.cardId  = card._id;
 
@@ -34,8 +34,8 @@ Template.cardAttachmentsPopup.events({
       Popup.close();
     });
   },
-  'click .js-computer-upload': function(evt, tpl) {
+  'click .js-computer-upload'(evt, tpl) {
     tpl.find('.js-attach-file').click();
     evt.preventDefault();
-  }
+  },
 });

+ 51 - 51
client/components/cards/cardDetails.js

@@ -1,39 +1,39 @@
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'cardDetails';
   },
 
-  mixins: function() {
+  mixins() {
     return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
   },
 
-  calculateNextPeak: function() {
-    var altitude = this.find('.js-card-details').scrollHeight;
+  calculateNextPeak() {
+    const altitude = this.find('.js-card-details').scrollHeight;
     this.callFirstWith(this, 'setNextPeak', altitude);
   },
 
-  reachNextPeak: function() {
-    var activitiesComponent = this.componentChildren('activities')[0];
+  reachNextPeak() {
+    const activitiesComponent = this.componentChildren('activities')[0];
     activitiesComponent.loadNextPage();
   },
 
-  onCreated: function() {
+  onCreated() {
     this.isLoaded = new ReactiveVar(false);
     this.componentParent().showOverlay.set(true);
     this.componentParent().mouseHasEnterCardDetails = false;
   },
 
-  scrollParentContainer: function() {
+  scrollParentContainer() {
     const cardPanelWidth = 510;
-    let bodyBoardComponent = this.componentParent();
+    const bodyBoardComponent = this.componentParent();
 
-    let $cardContainer = bodyBoardComponent.$('.js-lists');
-    let $cardView = this.$(this.firstNode());
-    let cardContainerScroll = $cardContainer.scrollLeft();
-    let cardContainerWidth = $cardContainer.width();
+    const $cardContainer = bodyBoardComponent.$('.js-lists');
+    const $cardView = this.$(this.firstNode());
+    const cardContainerScroll = $cardContainer.scrollLeft();
+    const cardContainerWidth = $cardContainer.width();
 
-    let cardViewStart = $cardView.offset().left;
-    let cardViewEnd = cardViewStart + cardPanelWidth;
+    const cardViewStart = $cardView.offset().left;
+    const cardViewEnd = cardViewStart + cardPanelWidth;
 
     let offset = false;
     if (cardViewStart < 0) {
@@ -47,53 +47,53 @@ BlazeComponent.extendComponent({
     }
   },
 
-  onRendered: function() {
+  onRendered() {
     this.scrollParentContainer();
   },
 
-  onDestroyed: function() {
+  onDestroyed() {
     this.componentParent().showOverlay.set(false);
   },
 
-  updateCard: function(modifier) {
+  updateCard(modifier) {
     Cards.update(this.data()._id, {
-      $set: modifier
+      $set: modifier,
     });
   },
 
-  events: function() {
-    var events = {
-      [CSSEvents.animationend + ' .js-card-details']: function() {
+  events() {
+    const events = {
+      [`${CSSEvents.animationend} .js-card-details`]() {
         this.isLoaded.set(true);
-      }
+      },
     };
 
     return [_.extend(events, {
-      'click .js-close-card-details': function() {
+      'click .js-close-card-details'() {
         Utils.goBoardId(this.data().boardId);
       },
       'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
-      'submit .js-card-description': function(evt) {
+      'submit .js-card-description'(evt) {
         evt.preventDefault();
-        var description = this.currentComponent().getValue();
-        this.updateCard({ description: description });
+        const description = this.currentComponent().getValue();
+        this.updateCard({ description });
       },
-      'submit .js-card-details-title': function(evt) {
+      'submit .js-card-details-title'(evt) {
         evt.preventDefault();
-        var title = this.currentComponent().getValue();
+        const title = this.currentComponent().getValue();
         if ($.trim(title)) {
-          this.updateCard({ title: title });
+          this.updateCard({ title });
         }
       },
       'click .js-member': Popup.open('cardMember'),
       'click .js-add-members': Popup.open('cardMembers'),
       'click .js-add-labels': Popup.open('cardLabels'),
-      'mouseenter .js-card-details': function() {
+      'mouseenter .js-card-details'() {
         this.componentParent().showOverlay.set(true);
         this.componentParent().mouseHasEnterCardDetails = true;
-      }
+      },
     })];
-  }
+  },
 }).register('cardDetails');
 
 // We extends the normal InlinedForm component to support UnsavedEdits draft
@@ -103,12 +103,12 @@ BlazeComponent.extendComponent({
     return {
       fieldName: 'cardDescription',
       docId: Session.get('currentCard'),
-    }
+    };
   }
 
   close(isReset = false) {
-    if (this.isOpen.get() && ! isReset) {
-      let draft = $.trim(this.getValue());
+    if (this.isOpen.get() && !isReset) {
+      const draft = $.trim(this.getValue());
       if (draft !== Cards.findOne(Session.get('currentCard')).description) {
         UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
       }
@@ -136,45 +136,45 @@ Template.cardDetailsActionsPopup.events({
   'click .js-attachments': Popup.open('cardAttachments'),
   'click .js-move-card': Popup.open('moveCard'),
   // 'click .js-copy': Popup.open(),
-  'click .js-archive': function(evt) {
+  'click .js-archive'(evt) {
     evt.preventDefault();
     Cards.update(this._id, {
       $set: {
-        archived: true
-      }
+        archived: true,
+      },
     });
     Popup.close();
   },
-  'click .js-more': Popup.open('cardMore')
+  'click .js-more': Popup.open('cardMore'),
 });
 
 Template.moveCardPopup.events({
-  'click .js-select-list': function() {
+  'click .js-select-list'() {
     // XXX We should *not* get the currentCard from the global state, but
     // instead from a “component” state.
-    var cardId = Session.get('currentCard');
-    var newListId = this._id;
+    const cardId = Session.get('currentCard');
+    const newListId = this._id;
     Cards.update(cardId, {
       $set: {
-        listId: newListId
-      }
+        listId: newListId,
+      },
     });
     Popup.close();
-  }
+  },
 });
 
 Template.cardMorePopup.events({
-  'click .js-delete': Popup.afterConfirm('cardDelete', function() {
+  'click .js-delete': Popup.afterConfirm('cardDelete', () => {
     Popup.close();
     Cards.remove(this._id);
     Utils.goBoardId(this.board()._id);
-  })
+  }),
 });
 
 // Close the card details pane by pressing escape
 EscapeActions.register('detailsPane',
-  function() { Utils.goBoardId(Session.get('currentBoard')); },
-  function() { return ! Session.equals('currentCard', null); }, {
-    noClickEscapeOn: '.js-card-details,.board-sidebar,#header'
+  () => { Utils.goBoardId(Session.get('currentBoard')); },
+  () => { return !Session.equals('currentCard', null); }, {
+    noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
   }
 );

+ 57 - 57
client/components/cards/labels.js

@@ -1,136 +1,136 @@
-
-var labelColors;
-Meteor.startup(function() {
+let labelColors;
+Meteor.startup(() => {
   labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
 });
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'formLabel';
   },
 
-  onCreated: function() {
+  onCreated() {
     this.currentColor = new ReactiveVar(this.data().color);
   },
 
-  labels: function() {
-    return _.map(labelColors, function(color) {
-      return { color: color, name: '' };
+  labels() {
+    return _.map(labelColors, (color) => {
+      return { color, name: '' };
     });
   },
 
-  isSelected: function(color) {
+  isSelected(color) {
     return this.currentColor.get() === color;
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-palette-color': function() {
+      'click .js-palette-color'() {
         this.currentColor.set(this.currentData().color);
-      }
+      },
     }];
-  }
+  },
 }).register('formLabel');
 
 Template.createLabelPopup.helpers({
   // This is the default color for a new label. We search the first color that
   // is not already used in the board (although it's not a problem if two
   // labels have the same color).
-  defaultColor: function() {
-    var labels = Boards.findOne(Session.get('currentBoard')).labels;
-    var usedColors = _.pluck(labels, 'color');
-    var availableColors = _.difference(labelColors, usedColors);
+  defaultColor() {
+    const labels = Boards.findOne(Session.get('currentBoard')).labels;
+    const usedColors = _.pluck(labels, 'color');
+    const availableColors = _.difference(labelColors, usedColors);
     return availableColors.length > 1 ? availableColors[0] : labelColors[0];
-  }
+  },
 });
 
 Template.cardLabelsPopup.events({
-  'click .js-select-label': function(evt) {
-    var cardId = Template.parentData(2).data._id;
-    var labelId = this._id;
-    var operation;
+  'click .js-select-label'(evt) {
+    const cardId = Template.parentData(2).data._id;
+    const labelId = this._id;
+    let operation;
     if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0)
       operation = '$addToSet';
     else
       operation = '$pull';
 
-    var query = {};
-    query[operation] = {
-      labelIds: labelId
-    };
-    Cards.update(cardId, query);
+    Cards.update(cardId, {
+      [operation]: {
+        labelIds: labelId,
+      },
+    });
     evt.preventDefault();
   },
   'click .js-edit-label': Popup.open('editLabel'),
-  'click .js-add-label': Popup.open('createLabel')
+  'click .js-add-label': Popup.open('createLabel'),
 });
 
 Template.formLabel.events({
-  'click .js-palette-color': function(evt) {
-    var $this = $(evt.currentTarget);
+  'click .js-palette-color'(evt) {
+    const $this = $(evt.currentTarget);
 
     // hide selected ll colors
     $('.js-palette-select').addClass('hide');
 
     // show select color
     $this.find('.js-palette-select').removeClass('hide');
-  }
+  },
 });
 
 Template.createLabelPopup.events({
   // Create the new label
-  'submit .create-label': function(evt, tpl) {
-    var name = tpl.$('#labelName').val().trim();
-    var boardId = Session.get('currentBoard');
-    var color = Blaze.getData(tpl.find('.fa-check')).color;
+  'submit .create-label'(evt, tpl) {
+    const name = tpl.$('#labelName').val().trim();
+    const boardId = Session.get('currentBoard');
+    const color = Blaze.getData(tpl.find('.fa-check')).color;
 
     Boards.update(boardId, {
       $push: {
         labels: {
+          name,
+          color,
           _id: Random.id(6),
-          name: name,
-          color: color
-        }
-      }
+        },
+      },
     });
 
     Popup.back();
     evt.preventDefault();
-  }
+  },
 });
 
 Template.editLabelPopup.events({
   'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() {
-    var boardId = Session.get('currentBoard');
+    const boardId = Session.get('currentBoard');
     Boards.update(boardId, {
       $pull: {
         labels: {
-          _id: this._id
-        }
-      }
+          _id: this._id,
+        },
+      },
     });
 
     Popup.back(2);
   }),
-  'submit .edit-label': function(evt, tpl) {
+  'submit .edit-label'(evt, tpl) {
     evt.preventDefault();
-    var name = tpl.$('#labelName').val().trim();
-    var boardId = Session.get('currentBoard');
-    var getLabel = Utils.getLabelIndex(boardId, this._id);
-    var color = Blaze.getData(tpl.find('.fa-check')).color;
+    const name = tpl.$('#labelName').val().trim();
+    const boardId = Session.get('currentBoard');
+    const getLabel = Utils.getLabelIndex(boardId, this._id);
+    const color = Blaze.getData(tpl.find('.fa-check')).color;
 
-    var $set = {};
-    $set[getLabel.key('name')] = name;
-    $set[getLabel.key('color')] = color;
-
-    Boards.update(boardId, { $set: $set });
+    Boards.update(boardId, {
+      $set: {
+        [getLabel.key('name')]: name,
+        [getLabel.key('color')]: color,
+      },
+    });
 
     Popup.back();
-  }
+  },
 });
 
 Template.cardLabelsPopup.helpers({
-  isLabelSelected: function(cardId) {
+  isLabelSelected(cardId) {
     return _.contains(Cards.findOne(cardId).labelIds, this._id);
-  }
+  },
 });

+ 2 - 2
client/components/cards/minicard.js

@@ -3,7 +3,7 @@
 // });
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'minicard';
-  }
+  },
 }).register('minicard');

+ 42 - 41
client/components/lists/list.js

@@ -1,14 +1,16 @@
+const { calculateIndex } = Utils;
+
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'list';
   },
 
-  // Proxies
-  openForm: function(options) {
+  // Proxy
+  openForm(options) {
     this.componentChildren('listBody')[0].openForm(options);
   },
 
-  onCreated: function() {
+  onCreated() {
     this.newCardFormIsVisible = new ReactiveVar(true);
   },
 
@@ -19,28 +21,27 @@ BlazeComponent.extendComponent({
   // By calling asking the sortable library to cancel its move on the `stop`
   // callback, we basically solve all issues related to reactive updates. A
   // comment below provides further details.
-  onRendered: function() {
-    var self = this;
-    if (! Meteor.user() || ! Meteor.user().isBoardMember())
+  onRendered() {
+    if (!Meteor.user() || !Meteor.user().isBoardMember())
       return;
 
-    var boardComponent = self.componentParent();
-    var itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
-    var $cards = self.$('.js-minicards');
+    const boardComponent = this.componentParent();
+    const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
+    const $cards = this.$('.js-minicards');
     $cards.sortable({
       connectWith: '.js-minicards',
       tolerance: 'pointer',
       appendTo: 'body',
-      helper: function(evt, item) {
-        var helper = item.clone();
+      helper(evt, item) {
+        const helper = item.clone();
         if (MultiSelection.isActive()) {
-          var andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
+          const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
           if (andNOthers > 0) {
             helper.append($(Blaze.toHTML(HTML.DIV(
               // XXX Super bad class name
               {'class': 'and-n-other'},
               // XXX Need to translate
-              'and ' + andNOthers + ' other cards.'
+              `and ${andNOthers} other cards.`
             ))));
           }
         }
@@ -50,19 +51,19 @@ BlazeComponent.extendComponent({
       items: itemsSelector,
       scroll: false,
       placeholder: 'minicard-wrapper placeholder',
-      start: function(evt, ui) {
+      start(evt, ui) {
         ui.placeholder.height(ui.helper.height());
         EscapeActions.executeUpTo('popup');
         boardComponent.setIsDragging(true);
       },
-      stop: function(evt, ui) {
+      stop(evt, ui) {
         // To attribute the new index number, we need to get the DOM element
         // of the previous and the following card -- if any.
-        var prevCardDom = ui.item.prev('.js-minicard').get(0);
-        var nextCardDom = ui.item.next('.js-minicard').get(0);
-        var nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
-        var sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards);
-        var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
+        const prevCardDom = ui.item.prev('.js-minicard').get(0);
+        const nextCardDom = ui.item.next('.js-minicard').get(0);
+        const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
+        const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
+        const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
 
         // Normally the jquery-ui sortable library moves the dragged DOM element
         // to its new position, which disrupts Blaze reactive updates mechanism
@@ -74,53 +75,53 @@ BlazeComponent.extendComponent({
         $cards.sortable('cancel');
 
         if (MultiSelection.isActive()) {
-          Cards.find(MultiSelection.getMongoSelector()).forEach(function(c, i) {
+          Cards.find(MultiSelection.getMongoSelector()).forEach((c, i) => {
             Cards.update(c._id, {
               $set: {
-                listId: listId,
-                sort: sortIndex.base + i * sortIndex.increment
-              }
+                listId,
+                sort: sortIndex.base + i * sortIndex.increment,
+              },
             });
           });
         } else {
-          var cardDomElement = ui.item.get(0);
-          var cardId = Blaze.getData(cardDomElement)._id;
+          const cardDomElement = ui.item.get(0);
+          const cardId = Blaze.getData(cardDomElement)._id;
           Cards.update(cardId, {
             $set: {
-              listId: listId,
-              sort: sortIndex.base
-            }
+              listId,
+              sort: sortIndex.base,
+            },
           });
         }
         boardComponent.setIsDragging(false);
-      }
+      },
     });
 
     // We want to re-run this function any time a card is added.
-    self.autorun(function() {
-      var currentBoardId = Tracker.nonreactive(function() {
+    this.autorun(() => {
+      const currentBoardId = Tracker.nonreactive(() => {
         return Session.get('currentBoard');
       });
       Cards.find({ boardId: currentBoardId }).fetch();
-      Tracker.afterFlush(function() {
+      Tracker.afterFlush(() => {
         $cards.find(itemsSelector).droppable({
           hoverClass: 'draggable-hover-card',
           accept: '.js-member,.js-label',
-          drop: function(event, ui) {
-            var cardId = Blaze.getData(this)._id;
-            var addToSet;
+          drop(event, ui) {
+            const cardId = Blaze.getData(this)._id;
+            let addToSet;
 
             if (ui.draggable.hasClass('js-member')) {
-              var memberId = Blaze.getData(ui.draggable.get(0)).userId;
+              const memberId = Blaze.getData(ui.draggable.get(0)).userId;
               addToSet = { members: memberId };
             } else {
-              var labelId = Blaze.getData(ui.draggable.get(0))._id;
+              const labelId = Blaze.getData(ui.draggable.get(0))._id;
               addToSet = { labelIds: labelId };
             }
             Cards.update(cardId, { $addToSet: addToSet });
-          }
+          },
         });
       });
     });
-  }
+  },
 }).register('list');

+ 40 - 40
client/components/lists/listBody.js

@@ -1,46 +1,46 @@
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'listBody';
   },
 
-  mixins: function() {
+  mixins() {
     return [Mixins.PerfectScrollbar];
   },
 
-  openForm: function(options) {
+  openForm(options) {
     options = options || {};
     options.position = options.position || 'top';
 
-    var forms = this.componentChildren('inlinedForm');
-    var form = _.find(forms, function(component) {
+    const forms = this.componentChildren('inlinedForm');
+    let form = _.find(forms, (component) => {
       return component.data().position === options.position;
     });
-    if (! form && forms.length > 0) {
+    if (!form && forms.length > 0) {
       form = forms[0];
     }
     form.open();
   },
 
-  addCard: function(evt) {
+  addCard(evt) {
     evt.preventDefault();
-    var textarea = $(evt.currentTarget).find('textarea');
-    var title = textarea.val();
-    var position = Blaze.getData(evt.currentTarget).position;
-    var sortIndex;
-    var firstCard = this.find('.js-minicard:first');
-    var lastCard = this.find('.js-minicard:last');
+    const firstCardDom = this.find('.js-minicard:first');
+    const lastCardDom = this.find('.js-minicard:last');
+    const textarea = $(evt.currentTarget).find('textarea');
+    const title = textarea.val();
+    const position = Blaze.getData(evt.currentTarget).position;
+    let sortIndex;
     if (position === 'top') {
-      sortIndex = Utils.calculateIndex(null, firstCard).base;
+      sortIndex = Utils.calculateIndex(null, firstCardDom).base;
     } else if (position === 'bottom') {
-      sortIndex = Utils.calculateIndex(lastCard, null).base;
+      sortIndex = Utils.calculateIndex(lastCardDom, null).base;
     }
 
     if ($.trim(title)) {
-      var _id = Cards.insert({
-        title: title,
+      const _id = Cards.insert({
+        title,
         listId: this.data()._id,
         boardId: this.data().board()._id,
-        sort: sortIndex
+        sort: sortIndex,
       });
       // In case the filter is active we need to add the newly inserted card in
       // the list of exceptions -- cards that are not filtered. Otherwise the
@@ -56,18 +56,18 @@ BlazeComponent.extendComponent({
     }
   },
 
-  scrollToBottom: function() {
-    var container = this.firstNode();
+  scrollToBottom() {
+    const container = this.firstNode();
     $(container).animate({
-      scrollTop: container.scrollHeight
+      scrollTop: container.scrollHeight,
     });
   },
 
-  clickOnMiniCard: function(evt) {
+  clickOnMiniCard(evt) {
     if (MultiSelection.isActive() || evt.shiftKey) {
       evt.stopImmediatePropagation();
       evt.preventDefault();
-      var methodName = evt.shiftKey ? 'toogleRange' : 'toogle';
+      const methodName = evt.shiftKey ? 'toogleRange' : 'toogle';
       MultiSelection[methodName](this.currentData()._id);
 
     // If the card is already selected, we want to de-select it.
@@ -80,36 +80,36 @@ BlazeComponent.extendComponent({
     }
   },
 
-  cardIsSelected: function() {
+  cardIsSelected() {
     return Session.equals('currentCard', this.currentData()._id);
   },
 
-  toggleMultiSelection: function(evt) {
+  toggleMultiSelection(evt) {
     evt.stopPropagation();
     evt.preventDefault();
     MultiSelection.toogle(this.currentData()._id);
   },
 
-  events: function() {
+  events() {
     return [{
       'click .js-minicard': this.clickOnMiniCard,
       'click .js-toggle-multi-selection': this.toggleMultiSelection,
       'click .open-minicard-composer': this.scrollToBottom,
-      submit: this.addCard
+      submit: this.addCard,
     }];
-  }
+  },
 }).register('listBody');
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'addCardForm';
   },
 
-  pressKey: function(evt) {
+  pressKey(evt) {
     // Pressing Enter should submit the card
     if (evt.keyCode === 13) {
       evt.preventDefault();
-      var $form = $(evt.currentTarget).closest('form');
+      const $form = $(evt.currentTarget).closest('form');
       // XXX For some reason $form.submit() does not work (it's probably a bug
       // of blaze-component related to the fact that the submit event is non-
       // bubbling). This is why we click on the submit button instead -- which
@@ -120,24 +120,24 @@ BlazeComponent.extendComponent({
     // in the reverse order
     } else if (evt.keyCode === 9) {
       evt.preventDefault();
-      var isReverse = evt.shiftKey;
-      var list = $('#js-list-' + this.data().listId);
-      var listSelector = '.js-list:not(.js-list-composer)';
-      var nextList = list[isReverse ? 'prev' : 'next'](listSelector).get(0);
+      const isReverse = evt.shiftKey;
+      const list = $(`#js-list-${this.data().listId}`);
+      const listSelector = '.js-list:not(.js-list-composer)';
+      let nextList = list[isReverse ? 'prev' : 'next'](listSelector).get(0);
       // If there is no next list, loop back to the beginning.
-      if (! nextList) {
+      if (!nextList) {
         nextList = $(listSelector + (isReverse ? ':last' : ':first')).get(0);
       }
 
       BlazeComponent.getComponentForElement(nextList).openForm({
-        position:this.data().position
+        position:this.data().position,
       });
     }
   },
 
-  events: function() {
+  events() {
     return [{
-      keydown: this.pressKey
+      keydown: this.pressKey,
     }];
-  }
+  },
 }).register('addCardForm');

+ 31 - 31
client/components/lists/listHeader.js

@@ -1,78 +1,78 @@
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'listHeader';
   },
 
-  editTitle: function(evt) {
+  editTitle(evt) {
     evt.preventDefault();
-    var form = this.componentChildren('inlinedForm')[0];
-    var newTitle = form.getValue();
+    const form = this.componentChildren('inlinedForm')[0];
+    const newTitle = form.getValue();
     if ($.trim(newTitle)) {
       Lists.update(this.currentData()._id, {
         $set: {
-          title: newTitle
-        }
+          title: newTitle,
+        },
       });
     }
   },
 
-  events: function() {
+  events() {
     return [{
       'click .js-open-list-menu': Popup.open('listAction'),
-      submit: this.editTitle
+      submit: this.editTitle,
     }];
-  }
+  },
 }).register('listHeader');
 
 Template.listActionPopup.events({
-  'click .js-add-card': function() {
-    var listDom = document.getElementById('js-list-' + this._id);
-    var listComponent = BlazeComponent.getComponentForElement(listDom);
+  'click .js-add-card'() {
+    const listDom = document.getElementById(`js-list-${this._id}`);
+    const listComponent = BlazeComponent.getComponentForElement(listDom);
     listComponent.openForm({ position: 'top' });
     Popup.close();
   },
-  'click .js-list-subscribe': function() {},
-  'click .js-select-cards': function() {
-    var cardIds = Cards.find(
+  'click .js-list-subscribe'() {},
+  'click .js-select-cards'() {
+    const cardIds = Cards.find(
       {listId: this._id},
       {fields: { _id: 1 }}
-    ).map(function(card) { return card._id; });
+    ).map((card) => card._id);
     MultiSelection.add(cardIds);
     Popup.close();
   },
   'click .js-move-cards': Popup.open('listMoveCards'),
-  'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() {
-    Cards.find({listId: this._id}).forEach(function(card) {
+  'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', () => {
+    Cards.find({listId: this._id}).forEach((card) => {
       Cards.update(card._id, {
         $set: {
-          archived: true
-        }
+          archived: true,
+        },
       });
     });
     Popup.close();
   }),
-  'click .js-close-list': function(evt) {
+  'click .js-close-list'(evt) {
     evt.preventDefault();
     Lists.update(this._id, {
       $set: {
-        archived: true
-      }
+        archived: true,
+      },
     });
     Popup.close();
-  }
+  },
 });
 
 Template.listMoveCardsPopup.events({
-  'click .js-select-list': function() {
-    var fromList = Template.parentData(2).data._id;
-    var toList = this._id;
-    Cards.find({listId: fromList}).forEach(function(card) {
+  'click .js-select-list'() {
+    const fromList = Template.parentData(2).data._id;
+    const toList = this._id;
+    Cards.find({ listId: fromList }).forEach((card) => {
       Cards.update(card._id, {
         $set: {
-          listId: toList
-        }
+          listId: toList,
+        },
       });
     });
     Popup.close();
-  }
+  },
 });

+ 29 - 28
client/components/main/editor.js

@@ -1,7 +1,7 @@
-var dropdownMenuIsOpened = false;
+let dropdownMenuIsOpened = false;
 
-Template.editor.onRendered(function() {
-  var $textarea = this.$('textarea');
+Template.editor.onRendered(() => {
+  const $textarea = this.$('textarea');
 
   autosize($textarea);
 
@@ -9,39 +9,40 @@ Template.editor.onRendered(function() {
     // Emojies
     {
       match: /\B:([\-+\w]*)$/,
-      search: function(term, callback) {
-        callback($.map(Emoji.values, function(emoji) {
+      search(term, callback) {
+        callback($.map(Emoji.values, (emoji) => {
           return emoji.indexOf(term) === 0 ? emoji : null;
         }));
       },
-      template: function(value) {
-        var image = '<img src="' + Emoji.baseImagePath + value + '.png"></img>';
+      template(value) {
+        const imgSrc = Emoji.baseImagePath + value;
+        const image = `<img src="${imgSrc}.png" />`;
         return image + value;
       },
-      replace: function(value) {
-        return ':' + value + ':';
+      replace(value) {
+        return `:${value}:`;
       },
-      index: 1
+      index: 1,
     },
 
     // User mentions
     {
       match: /\B@(\w*)$/,
-      search: function(term, callback) {
-        var currentBoard = Boards.findOne(Session.get('currentBoard'));
-        callback($.map(currentBoard.members, function(member) {
-          var username = Users.findOne(member.userId).username;
+      search(term, callback) {
+        const currentBoard = Boards.findOne(Session.get('currentBoard'));
+        callback($.map(currentBoard.members, (member) => {
+          const username = Users.findOne(member.userId).username;
           return username.indexOf(term) === 0 ? username : null;
         }));
       },
-      template: function(value) {
+      template(value) {
         return value;
       },
-      replace: function(username) {
-        return '@' + username + ' ';
+      replace(username) {
+        return `@${username} `;
       },
-      index: 1
-    }
+      index: 1,
+    },
   ]);
 
   // Since commit d474017 jquery-textComplete automatically closes a potential
@@ -51,27 +52,27 @@ Template.editor.onRendered(function() {
   // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown
   // is opened (and rely on textComplete to execute the actual action).
   $textarea.on({
-    'textComplete:show': function() {
+    'textComplete:show'() {
       dropdownMenuIsOpened = true;
     },
-    'textComplete:hide': function() {
-      Tracker.afterFlush(function() {
+    'textComplete:hide'() {
+      Tracker.afterFlush(() => {
         dropdownMenuIsOpened = false;
       });
-    }
+    },
   });
 });
 
 EscapeActions.register('textcomplete',
-  function() {},
-  function() { return dropdownMenuIsOpened; }
+  () => {},
+  () => dropdownMenuIsOpened
 );
 
 Template.viewer.events({
   // Viewer sometimes have click-able wrapper around them (for instance to edit
   // the corresponding text). Clicking a link shouldn't fire these actions, stop
   // we stop these event at the viewer component level.
-  'click a': function(evt) {
+  'click a'(evt) {
     evt.stopPropagation();
 
     // XXX We hijack the build-in browser action because we currently don't have
@@ -79,7 +80,7 @@ Template.viewer.events({
     // handled by a third party package that we can't configure easily. Fix that
     // by using directly `_blank` attribute in the rendered HTML.
     evt.preventDefault();
-    let href = evt.currentTarget.href;
+    const href = evt.currentTarget.href;
     window.open(href, '_blank');
-  }
+  },
 });

+ 5 - 5
client/components/main/header.js

@@ -1,14 +1,14 @@
 Template.header.helpers({
   // Reactively set the color of the page from the color of the current board.
-  headerTemplate: function() {
+  headerTemplate() {
     return 'headerBoard';
   },
 
-  wrappedHeader: function() {
-    return ! Session.get('currentBoard');
-  }
+  wrappedHeader() {
+    return !Session.get('currentBoard');
+  },
 });
 
 Template.header.events({
-  'click .js-create-board': Popup.open('createBoard')
+  'click .js-create-board': Popup.open('createBoard'),
 });

+ 0 - 65
client/components/main/helpers.js

@@ -1,65 +0,0 @@
-var Helpers = {
-  error: function() {
-    return Session.get('error');
-  },
-
-  toLowerCase: function(text) {
-    return text && text.toLowerCase();
-  },
-
-  toUpperCase: function(text) {
-    return text && text.toUpperCase();
-  },
-
-  firstChar: function(text) {
-    return text && text[0].toUpperCase();
-  },
-
-  session: function(prop) {
-    return Session.get(prop);
-  },
-
-  getUser: function(userId) {
-    return Users.findOne(userId);
-  }
-};
-
-// Register all Helpers
-_.each(Helpers, function(fn, name) { Blaze.registerHelper(name, fn); });
-
-// XXX I believe we should compute a HTML rendered field on the server that
-// would handle markdown, emojies and user mentions. We can simply have two
-// fields, one source, and one compiled version (in HTML) and send only the
-// compiled version to most users -- who don't need to edit.
-// In the meantime, all the transformation are done on the client using the
-// Blaze API.
-var at = HTML.CharRef({html: '&commat;', str: '@'});
-Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
-  var view = this;
-  var content = Blaze.toHTML(view.templateContentBlock);
-  var currentBoard = Session.get('currentBoard');
-  var knowedUsers = _.map(currentBoard.members, function(member) {
-    member.username = Users.findOne(member.userId).username;
-    return member;
-  });
-
-  var mentionRegex = /\B@(\w*)/gi;
-  var currentMention, knowedUser, href, linkClass, linkValue, link;
-  while (!! (currentMention = mentionRegex.exec(content))) {
-
-    knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] });
-    if (! knowedUser)
-      continue;
-
-    linkValue = [' ', at, knowedUser.username];
-    href = Router.url('Profile', { username: knowedUser.username });
-    linkClass = 'atMention';
-    if (knowedUser.userId === Meteor.userId())
-      linkClass += ' me';
-    link = HTML.A({ href: href, 'class': linkClass }, linkValue);
-
-    content = content.replace(currentMention[0], Blaze.toHTML(link));
-  }
-
-  return HTML.Raw(content);
-}));

+ 8 - 8
client/components/main/layouts.js

@@ -1,13 +1,13 @@
-Meteor.subscribe('boards')
+Meteor.subscribe('boards');
 
-BlazeLayout.setRoot('body')
+BlazeLayout.setRoot('body');
 
-Template.userFormsLayout.onRendered(function() {
-  EscapeActions.executeAll()
-})
+Template.userFormsLayout.onRendered(() => {
+  EscapeActions.executeAll();
+});
 
 Template.defaultLayout.events({
   'click .js-close-modal': () => {
-    Modal.close()
-  }
-})
+    Modal.close();
+  },
+});

+ 10 - 10
client/components/main/popup.js

@@ -1,11 +1,11 @@
 Popup.template.events({
-  'click .js-back-view': function() {
+  'click .js-back-view'() {
     Popup.back();
   },
-  'click .js-close-pop-over': function() {
+  'click .js-close-pop-over'() {
     Popup.close();
   },
-  'click .js-confirm': function() {
+  'click .js-confirm'() {
     this.__afterConfirmAction.call(this);
   },
   // This handler intends to solve a pretty tricky bug with our popup
@@ -18,22 +18,22 @@ Popup.template.events({
   // in moving the whole popup container outside of the popup wrapper. To
   // disable this behavior we have to manually reset the scrollLeft position
   // whenever it is modified.
-  'scroll .content-wrapper': function(evt) {
+  'scroll .content-wrapper'(evt) {
     evt.currentTarget.scrollLeft = 0;
-  }
+  },
 });
 
 // When a popup content is removed (ie, when the user press the "back" button),
 // we need to wait for the container translation to end before removing the
 // actual DOM element. For that purpose we use the undocumented `_uihooks` API.
-Popup.template.onRendered(function() {
-  var container = this.find('.content-container');
+Popup.template.onRendered(() => {
+  const container = this.find('.content-container');
   container._uihooks = {
-    removeElement: function(node) {
+    removeElement(node) {
       $(node).addClass('no-height');
-      $(container).one(CSSEvents.transitionend, function() {
+      $(container).one(CSSEvents.transitionend, () => {
         node.parentNode.removeChild(node);
       });
-    }
+    },
   };
 });

+ 12 - 12
client/components/mixins/infiniteScrolling.js

@@ -1,37 +1,37 @@
-var peakAnticipation = 200;
+const peakAnticipation = 200;
 
 Mixins.InfiniteScrolling = BlazeComponent.extendComponent({
-  onCreated: function() {
+  onCreated() {
     this._nextPeak = Infinity;
   },
 
-  setNextPeak: function(v) {
+  setNextPeak(v) {
     this._nextPeak = v;
   },
 
-  getNextPeak: function() {
+  getNextPeak() {
     return this._nextPeak;
   },
 
-  resetNextPeak: function() {
+  resetNextPeak() {
     this._nextPeak = Infinity;
   },
 
   // To be overwritten by consumers of this mixin
-  reachNextPeak: function() {
+  reachNextPeak() {
 
   },
 
-  events: function() {
+  events() {
     return [{
-      scroll: function(evt) {
-        var domElement = evt.currentTarget;
-        var altitude = domElement.scrollTop + domElement.offsetHeight;
+      scroll(evt) {
+        const domElement = evt.currentTarget;
+        let altitude = domElement.scrollTop + domElement.offsetHeight;
         altitude += peakAnticipation;
         if (altitude >= this.callFirstWith(null, 'getNextPeak')) {
           this.callFirstWith(null, 'reachNextPeak');
         }
-      }
+      },
     }];
-  }
+  },
 });

+ 5 - 7
client/components/mixins/perfectScrollbar.js

@@ -1,14 +1,12 @@
 Mixins.PerfectScrollbar = BlazeComponent.extendComponent({
-  onRendered: function() {
-    var component = this.mixinParent();
-    var domElement = component.find('.js-perfect-scrollbar');
+  onRendered() {
+    const component = this.mixinParent();
+    const domElement = component.find('.js-perfect-scrollbar');
     Ps.initialize(domElement);
 
     // XXX We should create an event map to be consistent with other components
     // but since BlazeComponent doesn't merge Mixins events transparently I
     // prefered to use a jQuery event (which is what an event map ends up doing)
-    component.$(domElement).on('mouseenter', function() {
-      Ps.update(domElement);
-    });
-  }
+    component.$(domElement).on('mouseenter', () => Ps.update(domElement));
+  },
 });

+ 99 - 95
client/components/sidebar/sidebar.js

@@ -1,76 +1,76 @@
 Sidebar = null;
 
-var defaultView = 'home';
+const defaultView = 'home';
 
-var viewTitles = {
+const viewTitles = {
   filter: 'filter-cards',
   multiselection: 'multi-selection',
-  archives: 'archives'
+  archives: 'archives',
 };
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'sidebar';
   },
 
-  mixins: function() {
+  mixins() {
     return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
   },
 
-  onCreated: function() {
-    this._isOpen = new ReactiveVar(! Session.get('currentCard'));
+  onCreated() {
+    this._isOpen = new ReactiveVar(!Session.get('currentCard'));
     this._view = new ReactiveVar(defaultView);
     Sidebar = this;
   },
 
-  onDestroyed: function() {
+  onDestroyed() {
     Sidebar = null;
   },
 
-  isOpen: function() {
+  isOpen() {
     return this._isOpen.get();
   },
 
-  open: function() {
-    if (! this._isOpen.get()) {
+  open() {
+    if (!this._isOpen.get()) {
       this._isOpen.set(true);
       EscapeActions.executeUpTo('detailsPane');
     }
   },
 
-  hide: function() {
+  hide() {
     if (this._isOpen.get()) {
       this._isOpen.set(false);
     }
   },
 
-  toogle: function() {
-    this._isOpen.set(! this._isOpen.get());
+  toogle() {
+    this._isOpen.set(!this._isOpen.get());
   },
 
-  calculateNextPeak: function() {
-    var altitude = this.find('.js-board-sidebar-content').scrollHeight;
+  calculateNextPeak() {
+    const altitude = this.find('.js-board-sidebar-content').scrollHeight;
     this.callFirstWith(this, 'setNextPeak', altitude);
   },
 
-  reachNextPeak: function() {
-    var activitiesComponent = this.componentChildren('activities')[0];
+  reachNextPeak() {
+    const activitiesComponent = this.componentChildren('activities')[0];
     activitiesComponent.loadNextPage();
   },
 
-  isTongueHidden: function() {
+  isTongueHidden() {
     return this.isOpen() && this.getView() !== defaultView;
   },
 
-  scrollTop: function() {
+  scrollTop() {
     this.$('.js-board-sidebar-content').scrollTop(0);
   },
 
-  getView: function() {
+  getView() {
     return this._view.get();
   },
 
-  setView: function(view) {
+  setView(view) {
     view = _.isString(view) ? view : defaultView;
     if (this._view.get() !== view) {
       this._view.set(view);
@@ -80,83 +80,84 @@ BlazeComponent.extendComponent({
     this.open();
   },
 
-  isDefaultView: function() {
+  isDefaultView() {
     return this.getView() === defaultView;
   },
 
-  getViewTemplate: function() {
-    return this.getView() + 'Sidebar';
+  getViewTemplate() {
+    return `${this.getView()}Sidebar`;
   },
 
-  getViewTitle: function() {
+  getViewTitle() {
     return TAPi18n.__(viewTitles[this.getView()]);
   },
 
-  events: function() {
+  events() {
     // XXX Hacky, we need some kind of `super`
-    var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
+    const mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
     return mixinEvents.concat([{
       'click .js-toogle-sidebar': this.toogle,
-      'click .js-back-home': this.setView
+      'click .js-back-home': this.setView,
     }]);
-  }
+  },
 }).register('sidebar');
 
-Blaze.registerHelper('Sidebar', function() {
-  return Sidebar;
-});
+Blaze.registerHelper('Sidebar', () => Sidebar);
 
 EscapeActions.register('sidebarView',
-  function() { Sidebar.setView(defaultView); },
-  function() { return Sidebar && Sidebar.getView() !== defaultView; }
+  () => { Sidebar.setView(defaultView); },
+  () => { return Sidebar && Sidebar.getView() !== defaultView; }
 );
 
-var getMemberIndex = function(board, searchId) {
-  for (var i = 0; i < board.members.length; i++) {
+function getMemberIndex(board, searchId) {
+  for (let i = 0; i < board.members.length; i++) {
     if (board.members[i].userId === searchId)
       return i;
   }
   throw new Meteor.Error('Member not found');
-};
+}
 
 Template.memberPopup.helpers({
-  user: function() {
+  user() {
     return Users.findOne(this.userId);
   },
-  memberType: function() {
-    var type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal';
+  memberType() {
+    const type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal';
     return TAPi18n.__(type).toLowerCase();
-  }
+  },
 });
 
 Template.memberPopup.events({
-  'click .js-filter-member': function() {
+  'click .js-filter-member'() {
     Filter.members.toogle(this.userId);
     Popup.close();
   },
   'click .js-change-role': Popup.open('changePermissions'),
   'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
-    var currentBoard = Boards.findOne(Session.get('currentBoard'));
-    var memberIndex = getMemberIndex(currentBoard, this.userId);
-    var setQuery = {};
-    setQuery[['members', memberIndex, 'isActive'].join('.')] = false;
-    Boards.update(currentBoard._id, { $set: setQuery });
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    const memberIndex = getMemberIndex(currentBoard, this.userId);
+
+    Boards.update(currentBoard._id, {
+      $set: {
+        [`members.${memberIndex}.isActive`]: false,
+      },
+    });
     Popup.close();
   }),
-  'click .js-leave-member': function() {
+  'click .js-leave-member'() {
     // XXX Not implemented
     Popup.close();
-  }
+  },
 });
 
 Template.membersWidget.events({
   'click .js-member': Popup.open('member'),
-  'click .js-manage-board-members': Popup.open('addMember')
+  'click .js-manage-board-members': Popup.open('addMember'),
 });
 
 Template.labelsWidget.events({
   'click .js-label': Popup.open('editLabel'),
-  'click .js-add-label': Popup.open('createLabel')
+  'click .js-add-label': Popup.open('createLabel'),
 });
 
 // Board members can assign people or labels by drag-dropping elements from the
@@ -164,99 +165,102 @@ Template.labelsWidget.events({
 // plugin any time a draggable member or label is modified or removed we use a
 // autorun function and register a dependency on the both members and labels
 // fields of the current board document.
-var draggableMembersLabelsWidgets = function() {
-  var self = this;
-  if (! Meteor.user() || ! Meteor.user().isBoardMember())
+function draggableMembersLabelsWidgets() {
+  if (!Meteor.user() || !Meteor.user().isBoardMember())
     return;
 
-  self.autorun(function() {
-    var currentBoardId = Tracker.nonreactive(function() {
+  this.autorun(() => {
+    const currentBoardId = Tracker.nonreactive(() => {
       return Session.get('currentBoard');
     });
     Boards.findOne(currentBoardId, {
       fields: {
         members: 1,
-        labels: 1
-      }
+        labels: 1,
+      },
     });
-    Tracker.afterFlush(function() {
-      self.$('.js-member,.js-label').draggable({
+    Tracker.afterFlush(() => {
+      this.$('.js-member,.js-label').draggable({
         appendTo: 'body',
         helper: 'clone',
         revert: 'invalid',
         revertDuration: 150,
         snap: false,
         snapMode: 'both',
-        start: function() {
+        start() {
           EscapeActions.executeUpTo('popup-back');
-        }
+        },
       });
     });
   });
-};
+}
 
 Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
 Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
 
 Template.addMemberPopup.helpers({
-  isBoardMember: function() {
-    var user = Users.findOne(this._id);
+  isBoardMember() {
+    const user = Users.findOne(this._id);
     return user && user.isBoardMember();
-  }
+  },
 });
 
 Template.addMemberPopup.events({
-  'click .pop-over-member-list li:not(.disabled)': function() {
-    var userId = this._id;
-    var currentBoard = Boards.findOne(Session.get('currentBoard'));
-    var currentMembersIds = _.pluck(currentBoard.members, 'userId');
+  'click .pop-over-member-list li:not(.disabled)'() {
+    const userId = this._id;
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    const currentMembersIds = _.pluck(currentBoard.members, 'userId');
     if (currentMembersIds.indexOf(userId) === -1) {
       Boards.update(currentBoard._id, {
         $push: {
           members: {
-            userId: userId,
+            userId,
             isAdmin: false,
-            isActive: true
-          }
-        }
+            isActive: true,
+          },
+        },
       });
     } else {
-      var memberIndex = getMemberIndex(currentBoard, userId);
-      var setQuery = {};
-      setQuery[['members', memberIndex, 'isActive'].join('.')] = true;
-      Boards.update(currentBoard._id, { $set: setQuery });
+      const memberIndex = getMemberIndex(currentBoard, userId);
+
+      Boards.update(currentBoard._id, {
+        $set: {
+          [`members.${memberIndex}.isActive`]: true,
+        },
+      });
     }
     Popup.close();
-  }
+  },
 });
 
-Template.addMemberPopup.onRendered(function() {
+Template.addMemberPopup.onRendered(() => {
   this.find('.js-search-member input').focus();
 });
 
 Template.changePermissionsPopup.events({
-  'click .js-set-admin, click .js-set-normal': function(event) {
-    var currentBoard = Boards.findOne(Session.get('currentBoard'));
-    var memberIndex = getMemberIndex(currentBoard, this.user._id);
-    var isAdmin = $(event.currentTarget).hasClass('js-set-admin');
-    var setQuery = {};
-    setQuery[['members', memberIndex, 'isAdmin'].join('.')] = isAdmin;
+  'click .js-set-admin, click .js-set-normal'(event) {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    const memberIndex = getMemberIndex(currentBoard, this.user._id);
+    const isAdmin = $(event.currentTarget).hasClass('js-set-admin');
+
     Boards.update(currentBoard._id, {
-      $set: setQuery
+      $set: {
+        [`members.${memberIndex}.isAdmin`]: isAdmin,
+      },
     });
     Popup.back(1);
-  }
+  },
 });
 
 Template.changePermissionsPopup.helpers({
-  isAdmin: function() {
+  isAdmin() {
     return this.user.isBoardAdmin();
   },
-  isLastAdmin: function() {
-    if (! this.user.isBoardAdmin())
+  isLastAdmin() {
+    if (!this.user.isBoardAdmin())
       return false;
-    var currentBoard = Boards.findOne(Session.get('currentBoard'));
-    var nbAdmins = _.where(currentBoard.members, { isAdmin: true }).length;
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    const nbAdmins = _.where(currentBoard.members, { isAdmin: true }).length;
     return nbAdmins === 1;
-  }
+  },
 });

+ 17 - 17
client/components/sidebar/sidebarArchives.js

@@ -1,46 +1,46 @@
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'archivesSidebar';
   },
 
-  tabs: function() {
+  tabs() {
     return [
       { name: TAPi18n.__('cards'), slug: 'cards' },
-      { name: TAPi18n.__('lists'), slug: 'lists' }
-    ]
+      { name: TAPi18n.__('lists'), slug: 'lists' },
+    ];
   },
 
-  archivedCards: function() {
+  archivedCards() {
     return Cards.find({ archived: true });
   },
 
-  archivedLists: function() {
+  archivedLists() {
     return Lists.find({ archived: true });
   },
 
-  cardIsInArchivedList: function() {
+  cardIsInArchivedList() {
     return this.currentData().list().archived;
   },
 
-  onRendered: function() {
-    //XXX We should support dragging a card from the sidebar to the board
+  onRendered() {
+    // XXX We should support dragging a card from the sidebar to the board
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-restore-card': function() {
-        var cardId = this.currentData()._id;
+      'click .js-restore-card'() {
+        const cardId = this.currentData()._id;
         Cards.update(cardId, {$set: {archived: false}});
       },
       'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
-        var cardId = this._id;
+        const cardId = this._id;
         Cards.remove(cardId);
         Popup.close();
       }),
-      'click .js-restore-list': function() {
-        var listId = this.currentData()._id;
+      'click .js-restore-list'() {
+        const listId = this.currentData()._id;
         Lists.update(listId, {$set: {archived: false}});
-      }
+      },
     }];
-  }
+  },
 }).register('archivesSidebar');

+ 50 - 50
client/components/sidebar/sidebarFilters.js

@@ -1,136 +1,136 @@
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'filterSidebar';
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-toggle-label-filter': function(evt) {
+      'click .js-toggle-label-filter'(evt) {
         evt.preventDefault();
         Filter.labelIds.toogle(this.currentData()._id);
         Filter.resetExceptions();
       },
-      'click .js-toogle-member-filter': function(evt) {
+      'click .js-toogle-member-filter'(evt) {
         evt.preventDefault();
         Filter.members.toogle(this.currentData()._id);
         Filter.resetExceptions();
       },
-      'click .js-clear-all': function(evt) {
+      'click .js-clear-all'(evt) {
         evt.preventDefault();
         Filter.reset();
       },
-      'click .js-filter-to-selection': function(evt) {
+      'click .js-filter-to-selection'(evt) {
         evt.preventDefault();
-        var selectedCards = Cards.find(Filter.mongoSelector()).map(function(c) {
+        const selectedCards = Cards.find(Filter.mongoSelector()).map((c) => {
           return c._id;
         });
         MultiSelection.add(selectedCards);
-      }
+      },
     }];
-  }
+  },
 }).register('filterSidebar');
 
-var updateSelectedCards = function(query) {
-  Cards.find(MultiSelection.getMongoSelector()).forEach(function(card) {
+function updateSelectedCards(query) {
+  Cards.find(MultiSelection.getMongoSelector()).forEach((card) => {
     Cards.update(card._id, query);
   });
-};
+}
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'multiselectionSidebar';
   },
 
-  mapSelection: function(kind, _id) {
-    return Cards.find(MultiSelection.getMongoSelector()).map(function(card) {
-      var methodName = kind === 'label' ? 'hasLabel' : 'isAssigned';
+  mapSelection(kind, _id) {
+    return Cards.find(MultiSelection.getMongoSelector()).map((card) => {
+      const methodName = kind === 'label' ? 'hasLabel' : 'isAssigned';
       return card[methodName](_id);
     });
   },
 
-  allSelectedElementHave: function(kind, _id) {
+  allSelectedElementHave(kind, _id) {
     if (MultiSelection.isEmpty())
       return false;
     else
       return _.every(this.mapSelection(kind, _id));
   },
 
-  someSelectedElementHave: function(kind, _id) {
+  someSelectedElementHave(kind, _id) {
     if (MultiSelection.isEmpty())
       return false;
     else
       return _.some(this.mapSelection(kind, _id));
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-toggle-label-multiselection': function(evt) {
-        var labelId = this.currentData()._id;
-        var mappedSelection = this.mapSelection('label', labelId);
-        var operation;
+      'click .js-toggle-label-multiselection'(evt) {
+        const labelId = this.currentData()._id;
+        const mappedSelection = this.mapSelection('label', labelId);
+        let operation;
         if (_.every(mappedSelection))
           operation = '$pull';
-        else if (_.every(mappedSelection, function(bool) { return ! bool; }))
+        else if (_.every(mappedSelection, (bool) => !bool))
           operation = '$addToSet';
         else {
-          var popup = Popup.open('disambiguateMultiLabel');
+          const popup = Popup.open('disambiguateMultiLabel');
           // XXX We need to have a better integration between the popup and the
           // UI components systems.
           return popup.call(this.currentData(), evt);
         }
 
-        var query = {};
-        query[operation] = {
-          labelIds: labelId
-        };
-        updateSelectedCards(query);
+        updateSelectedCards({
+          [operation]: {
+            labelIds: labelId,
+          },
+        });
       },
-      'click .js-toogle-member-multiselection': function(evt) {
-        var memberId = this.currentData()._id;
-        var mappedSelection = this.mapSelection('member', memberId);
-        var operation;
+      'click .js-toogle-member-multiselection'(evt) {
+        const memberId = this.currentData()._id;
+        const mappedSelection = this.mapSelection('member', memberId);
+        let operation;
         if (_.every(mappedSelection))
           operation = '$pull';
-        else if (_.every(mappedSelection, function(bool) { return ! bool; }))
+        else if (_.every(mappedSelection, (bool) => !bool))
           operation = '$addToSet';
         else {
-          var popup = Popup.open('disambiguateMultiMember');
+          const popup = Popup.open('disambiguateMultiMember');
           // XXX We need to have a better integration between the popup and the
           // UI components systems.
           return popup.call(this.currentData(), evt);
         }
 
-        var query = {};
-        query[operation] = {
-          members: memberId
-        };
-        updateSelectedCards(query);
+        updateSelectedCards({
+          [operation]: {
+            members: memberId,
+          },
+        });
       },
-      'click .js-archive-selection': function() {
+      'click .js-archive-selection'() {
         updateSelectedCards({$set: {archived: true}});
-      }
+      },
     }];
-  }
+  },
 }).register('multiselectionSidebar');
 
 Template.disambiguateMultiLabelPopup.events({
-  'click .js-remove-label': function() {
+  'click .js-remove-label'() {
     updateSelectedCards({$pull: {labelIds: this._id}});
     Popup.close();
   },
-  'click .js-add-label': function() {
+  'click .js-add-label'() {
     updateSelectedCards({$addToSet: {labelIds: this._id}});
     Popup.close();
-  }
+  },
 });
 
 Template.disambiguateMultiMemberPopup.events({
-  'click .js-unassign-member': function() {
+  'click .js-unassign-member'() {
     updateSelectedCards({$pull: {members: this._id}});
     Popup.close();
   },
-  'click .js-assign-member': function() {
+  'click .js-assign-member'() {
     updateSelectedCards({$addToSet: {members: this._id}});
     Popup.close();
-  }
+  },
 });

+ 62 - 62
client/components/users/userAvatar.js

@@ -1,98 +1,98 @@
 Meteor.subscribe('my-avatars');
 
 Template.userAvatar.helpers({
-  userData: function() {
+  userData() {
     return Users.findOne(this.userId, {
       fields: {
         profile: 1,
-        username: 1
-      }
+        username: 1,
+      },
     });
   },
 
-  memberType: function() {
-    var user = Users.findOne(this.userId);
+  memberType() {
+    const user = Users.findOne(this.userId);
     return user && user.isBoardAdmin() ? 'admin' : 'normal';
   },
 
-  presenceStatusClassName: function() {
-    var userPresence = Presences.findOne({ userId: this.userId });
-    if (! userPresence)
+  presenceStatusClassName() {
+    const userPresence = Presences.findOne({ userId: this.userId });
+    if (!userPresence)
       return 'disconnected';
     else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
       return 'active';
     else
       return 'idle';
-  }
+  },
 });
 
 Template.userAvatar.events({
-  'click .js-change-avatar': Popup.open('changeAvatar')
+  'click .js-change-avatar': Popup.open('changeAvatar'),
 });
 
 Template.userAvatarInitials.helpers({
-  initials: function() {
-    var user = Users.findOne(this.userId);
+  initials() {
+    const user = Users.findOne(this.userId);
     return user && user.getInitials();
   },
 
-  viewPortWidth: function() {
-    var user = Users.findOne(this.userId);
+  viewPortWidth() {
+    const user = Users.findOne(this.userId);
     return (user && user.getInitials().length || 1) * 12;
-  }
+  },
 });
 
 BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'changeAvatarPopup';
   },
 
-  onCreated: function() {
+  onCreated() {
     this.error = new ReactiveVar('');
   },
 
-  avatarUrlOptions: function() {
+  avatarUrlOptions() {
     return {
       auth: false,
-      brokenIsFine: true
+      brokenIsFine: true,
     };
   },
 
-  uploadedAvatars: function() {
+  uploadedAvatars() {
     return Avatars.find({userId: Meteor.userId()});
   },
 
-  isSelected: function() {
-    var userProfile = Meteor.user().profile;
-    var avatarUrl = userProfile && userProfile.avatarUrl;
-    var currentAvatarUrl = this.currentData().url(this.avatarUrlOptions());
+  isSelected() {
+    const userProfile = Meteor.user().profile;
+    const avatarUrl = userProfile && userProfile.avatarUrl;
+    const currentAvatarUrl = this.currentData().url(this.avatarUrlOptions());
     return avatarUrl === currentAvatarUrl;
   },
 
-  noAvatarUrl: function() {
-    var userProfile = Meteor.user().profile;
-    var avatarUrl = userProfile && userProfile.avatarUrl;
-    return ! avatarUrl;
+  noAvatarUrl() {
+    const userProfile = Meteor.user().profile;
+    const avatarUrl = userProfile && userProfile.avatarUrl;
+    return !avatarUrl;
   },
 
-  setAvatar: function(avatarUrl) {
+  setAvatar(avatarUrl) {
     Meteor.users.update(Meteor.userId(), {
       $set: {
-        'profile.avatarUrl': avatarUrl
-      }
+        'profile.avatarUrl': avatarUrl,
+      },
     });
   },
 
-  setError: function(error) {
+  setError(error) {
     this.error.set(error);
   },
 
-  events: function() {
+  events() {
     return [{
-      'click .js-upload-avatar': function() {
+      'click .js-upload-avatar'() {
         this.$('.js-upload-avatar-input').click();
       },
-      'change .js-upload-avatar-input': function(evt) {
+      'change .js-upload-avatar-input'(evt) {
         let file, fileUrl;
 
         FS.Utility.eachFile(evt, (f) => {
@@ -106,71 +106,71 @@ BlazeComponent.extendComponent({
 
         if (fileUrl) {
           this.setError('');
-          let fetchAvatarInterval = window.setInterval(() => {
+          const fetchAvatarInterval = window.setInterval(() => {
             $.ajax({
               url: fileUrl,
               success: () => {
                 this.setAvatar(file.url(this.avatarUrlOptions()));
                 window.clearInterval(fetchAvatarInterval);
-              }
+              },
             });
           }, 100);
         }
       },
-      'click .js-select-avatar': function() {
-        var avatarUrl = this.currentData().url(this.avatarUrlOptions());
+      'click .js-select-avatar'() {
+        const avatarUrl = this.currentData().url(this.avatarUrlOptions());
         this.setAvatar(avatarUrl);
       },
-      'click .js-select-initials': function() {
+      'click .js-select-initials'() {
         this.setAvatar('');
       },
-      'click .js-delete-avatar': function() {
+      'click .js-delete-avatar'() {
         Avatars.remove(this.currentData()._id);
-      }
+      },
     }];
-  }
+  },
 }).register('changeAvatarPopup');
 
 Template.cardMembersPopup.helpers({
-  isCardMember: function() {
-    var cardId = Template.parentData()._id;
-    var cardMembers = Cards.findOne(cardId).members || [];
+  isCardMember() {
+    const cardId = Template.parentData()._id;
+    const cardMembers = Cards.findOne(cardId).members || [];
     return _.contains(cardMembers, this.userId);
   },
-  user: function() {
+  user() {
     return Users.findOne(this.userId);
-  }
+  },
 });
 
 Template.cardMembersPopup.events({
-  'click .js-select-member': function(evt) {
-    var cardId = Template.parentData(2).data._id;
-    var memberId = this.userId;
-    var operation;
+  'click .js-select-member'(evt) {
+    const cardId = Template.parentData(2).data._id;
+    const memberId = this.userId;
+    let operation;
     if (Cards.find({ _id: cardId, members: memberId}).count() === 0)
       operation = '$addToSet';
     else
       operation = '$pull';
 
-    var query = {};
-    query[operation] = {
-      members: memberId
-    };
-    Cards.update(cardId, query);
+    Cards.update(cardId, {
+      [operation]: {
+        members: memberId,
+      },
+    });
     evt.preventDefault();
-  }
+  },
 });
 
 Template.cardMemberPopup.helpers({
-  user: function() {
+  user() {
     return Users.findOne(this.userId);
-  }
+  },
 });
 
 Template.cardMemberPopup.events({
-  'click .js-remove-member': function() {
+  'click .js-remove-member'() {
     Cards.update(this.cardId, {$pull: {members: this.userId}});
     Popup.close();
   },
-  'click .js-edit-profile': Popup.open('editProfile')
+  'click .js-edit-profile': Popup.open('editProfile'),
 });

+ 21 - 22
client/components/users/userHeader.js

@@ -1,6 +1,6 @@
 Template.headerUserBar.events({
   'click .js-open-header-member-menu': Popup.open('memberMenu'),
-  'click .js-change-avatar': Popup.open('changeAvatar')
+  'click .js-change-avatar': Popup.open('changeAvatar'),
 });
 
 Template.memberMenuPopup.events({
@@ -8,58 +8,57 @@ Template.memberMenuPopup.events({
   'click .js-change-avatar': Popup.open('changeAvatar'),
   'click .js-change-password': Popup.open('changePassword'),
   'click .js-change-language': Popup.open('changeLanguage'),
-  'click .js-logout': function(evt) {
+  'click .js-logout'(evt) {
     evt.preventDefault();
 
     AccountsTemplates.logout();
-  }
+  },
 });
 
 Template.editProfilePopup.events({
-  submit: function(evt, tpl) {
+  submit(evt, tpl) {
     evt.preventDefault();
-    var fullname = $.trim(tpl.find('.js-profile-fullname').value);
-    var username = $.trim(tpl.find('.js-profile-username').value);
-    var initials = $.trim(tpl.find('.js-profile-initials').value);
+    const fullname = $.trim(tpl.find('.js-profile-fullname').value);
+    const username = $.trim(tpl.find('.js-profile-username').value);
+    const initials = $.trim(tpl.find('.js-profile-initials').value);
     Users.update(Meteor.userId(), {$set: {
       'profile.fullname': fullname,
-      'profile.initials': initials
+      'profile.initials': initials,
     }});
     // XXX We should report the error to the user.
     if (username !== Meteor.user().username) {
       Meteor.call('setUsername', username);
     }
     Popup.back();
-  }
+  },
 });
 
 // XXX For some reason the useraccounts autofocus isnt working in this case.
 // See https://github.com/meteor-useraccounts/core/issues/384
-Template.changePasswordPopup.onRendered(function() {
+Template.changePasswordPopup.onRendered(() => {
   this.find('#at-field-current_password').focus();
 });
 
 Template.changeLanguagePopup.helpers({
-  languages: function() {
-    return _.map(TAPi18n.getLanguages(), function(lang, tag) {
-      return {
-        tag: tag,
-        name: lang.name
-      };
+  languages() {
+    return _.map(TAPi18n.getLanguages(), (lang, tag) => {
+      const name = lang.name;
+      return { tag, name };
     });
   },
-  isCurrentLanguage: function() {
+
+  isCurrentLanguage() {
     return this.tag === TAPi18n.getLanguage();
-  }
+  },
 });
 
 Template.changeLanguagePopup.events({
-  'click .js-set-language': function(evt) {
+  'click .js-set-language'(evt) {
     Users.update(Meteor.userId(), {
       $set: {
-        'profile.language': this.tag
-      }
+        'profile.language': this.tag,
+      },
     });
     evt.preventDefault();
-  }
+  },
 });

+ 12 - 14
client/config/accounts.js

@@ -1,11 +1,11 @@
-var passwordField = AccountsTemplates.removeField('password');
-var emailField = AccountsTemplates.removeField('email');
+const passwordField = AccountsTemplates.removeField('password');
+const emailField = AccountsTemplates.removeField('email');
 AccountsTemplates.addFields([{
   _id: 'username',
   type: 'text',
   displayName: 'username',
   required: true,
-  minLength: 2
+  minLength: 2,
 }, emailField, passwordField]);
 
 AccountsTemplates.configure({
@@ -15,36 +15,34 @@ AccountsTemplates.configure({
   enablePasswordChange: true,
   sendVerificationEmail: true,
   showForgotPasswordLink: true,
-  onLogoutHook: function() {
-    var homePage = 'home';
+  onLogoutHook() {
+    const homePage = 'home';
     if (FlowRouter.getRouteName() === homePage) {
       FlowRouter.reload();
     } else {
       FlowRouter.go(homePage);
     }
-  }
+  },
 });
 
 _.each(['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'],
-  function(routeName) {
-  AccountsTemplates.configureRoute(routeName);
-});
+  (routeName) => AccountsTemplates.configureRoute(routeName));
 
 // We display the form to change the password in a popup window that already
 // have a title, so we unset the title automatically displayed by useraccounts.
 AccountsTemplates.configure({
   texts: {
     title: {
-      changePwd: ''
-    }
-  }
+      changePwd: '',
+    },
+  },
 });
 
 AccountsTemplates.configureRoute('changePwd', {
-  redirect: function() {
+  redirect() {
     // XXX We should emit a notification once we have a notification system.
     // Currently the user has no indication that his modification has been
     // applied.
     Popup.back();
-  }
+  },
 });

+ 44 - 4
client/config/blazeHelpers.js

@@ -1,13 +1,53 @@
-Blaze.registerHelper('currentBoard', function() {
-  var boardId = Session.get('currentBoard');
+Blaze.registerHelper('currentBoard', () => {
+  const boardId = Session.get('currentBoard');
   if (boardId) {
     return Boards.findOne(boardId);
   }
 });
 
-Blaze.registerHelper('currentCard', function() {
-  var cardId = Session.get('currentCard');
+Blaze.registerHelper('currentCard', () => {
+  const cardId = Session.get('currentCard');
   if (cardId) {
     return Cards.findOne(cardId);
   }
 });
+
+Blaze.registerHelper('getUser', (userId) => Users.findOne(userId));
+
+// XXX I believe we should compute a HTML rendered field on the server that
+// would handle markdown, emojies and user mentions. We can simply have two
+// fields, one source, and one compiled version (in HTML) and send only the
+// compiled version to most users -- who don't need to edit.
+// In the meantime, all the transformation are done on the client using the
+// Blaze API.
+const at = HTML.CharRef({html: '&commat;', str: '@'});
+Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
+  const view = this;
+  const currentBoard = Session.get('currentBoard');
+  const knowedUsers = _.map(currentBoard.members, (member) => {
+    member.username = Users.findOne(member.userId).username;
+    return member;
+  });
+  const mentionRegex = /\B@(\w*)/gi;
+  let content = Blaze.toHTML(view.templateContentBlock);
+
+  let currentMention, knowedUser, href, linkClass, linkValue, link;
+  while (Boolean(currentMention = mentionRegex.exec(content))) {
+
+    knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] });
+    if (!knowedUser)
+      continue;
+
+    linkValue = [' ', at, knowedUser.username];
+    // XXX We need to convert to flow router
+    href = Router.url('Profile', { username: knowedUser.username });
+    linkClass = 'atMention';
+    if (knowedUser.userId === Meteor.userId())
+      linkClass += ' me';
+    link = HTML.A({ href, 'class': linkClass }, linkValue);
+
+    content = content.replace(currentMention[0], Blaze.toHTML(link));
+  }
+
+  return HTML.Raw(content);
+}));

+ 2 - 3
client/config/reactiveTabs.js

@@ -1,5 +1,4 @@
 // XXX Since Blaze doesn't have a clean high component API, component API are
 // also tweaky to use. I guess React would be a solution.
-ReactiveTabs.createInterface({
-  template: 'basicTabs'
-});
+const template = 'basicTabs';
+ReactiveTabs.createInterface({ template });

+ 19 - 19
client/config/router.js

@@ -6,7 +6,7 @@ FlowRouter.triggers.exit([({path}) => {
 FlowRouter.route('/', {
   name: 'home',
   triggersEnter: [AccountsTemplates.ensureSignedIn],
-  action: function() {
+  action() {
     Session.set('currentBoard', null);
     Session.set('currentCard', null);
 
@@ -14,14 +14,14 @@ FlowRouter.route('/', {
     EscapeActions.executeAll();
 
     BlazeLayout.render('defaultLayout', { content: 'boardList' });
-  }
+  },
 });
 
 FlowRouter.route('/b/:id/:slug', {
   name: 'board',
-  action: function(params) {
-    let currentBoard = params.id;
-    let previousBoard = Session.get('currentBoard');
+  action(params) {
+    const currentBoard = params.id;
+    const previousBoard = Session.get('currentBoard');
     Session.set('currentBoard', currentBoard);
     Session.set('currentCard', null);
 
@@ -32,57 +32,57 @@ FlowRouter.route('/b/:id/:slug', {
     }
 
     BlazeLayout.render('defaultLayout', { content: 'board' });
-  }
+  },
 });
 
 FlowRouter.route('/b/:boardId/:slug/:cardId', {
   name: 'card',
-  action: function(params) {
+  action(params) {
     Session.set('currentBoard', params.boardId);
     Session.set('currentCard', params.cardId);
 
     EscapeActions.executeUpTo('inlinedForm');
 
     BlazeLayout.render('defaultLayout', { content: 'board' });
-  }
+  },
 });
 
 FlowRouter.route('/shortcuts', {
   name: 'shortcuts',
-  action: function(params) {
+  action() {
     const shortcutsTemplate = 'keyboardShortcuts';
 
     EscapeActions.executeUpTo('popup-close');
 
     if (previousPath) {
       Modal.open(shortcutsTemplate, {
-        onCloseGoTo: previousPath
+        onCloseGoTo: previousPath,
       });
     } else {
       // XXX There is currently no way to escape this page on Sandstorm
       BlazeLayout.render('defaultLayout', { content: shortcutsTemplate });
     }
-  }
+  },
 });
 
 FlowRouter.notFound = {
-  action: function() {
+  action() {
     BlazeLayout.render('defaultLayout', { content: 'notFound' });
-  }
-}
+  },
+};
 
 // We maintain a list of redirections to ensure that we don't break old URLs
 // when we change our routing scheme.
-var redirections = {
+const redirections = {
   '/boards': '/',
   '/boards/:id/:slug': '/b/:id/:slug',
-  '/boards/:id/:slug/:cardId': '/b/:id/:slug/:cardId'
+  '/boards/:id/:slug/:cardId': '/b/:id/:slug/:cardId',
 };
 
-_.each(redirections, function(newPath, oldPath) {
+_.each(redirections, (newPath, oldPath) => {
   FlowRouter.route(oldPath, {
-    triggersEnter: [function(context, redirect) {
+    triggersEnter: [(context, redirect) => {
       redirect(FlowRouter.path(newPath, context.params));
-    }]
+    }],
   });
 });

+ 13 - 15
client/lib/cssEvents.js

@@ -1,42 +1,40 @@
 // XXX Should we use something like Moderniz instead of our custom detector?
 
-var whichTransitionEvent = function() {
-  var t;
-  var el = document.createElement('fakeelement');
-  var transitions = {
+function whichTransitionEvent() {
+  const el = document.createElement('fakeelement');
+  const transitions = {
     transition:'transitionend',
     OTransition:'oTransitionEnd',
     MSTransition:'msTransitionEnd',
     MozTransition:'transitionend',
-    WebkitTransition:'webkitTransitionEnd'
+    WebkitTransition:'webkitTransitionEnd',
   };
 
-  for (t in transitions) {
+  for (const t in transitions) {
     if (el.style[t] !== undefined) {
       return transitions[t];
     }
   }
-};
+}
 
-var whichAnimationEvent = function() {
-  var t;
-  var el = document.createElement('fakeelement');
-  var transitions = {
+function whichAnimationEvent() {
+  const el = document.createElement('fakeelement');
+  const transitions = {
     animation:'animationend',
     OAnimation:'oAnimationEnd',
     MSTransition:'msAnimationEnd',
     MozAnimation:'animationend',
-    WebkitAnimation:'webkitAnimationEnd'
+    WebkitAnimation:'webkitAnimationEnd',
   };
 
-  for (t in transitions) {
+  for (const t in transitions) {
     if (el.style[t] !== undefined) {
       return transitions[t];
     }
   }
-};
+}
 
 CSSEvents = {
   transitionend: whichTransitionEvent(),
-  animationend: whichAnimationEvent()
+  animationend: whichAnimationEvent(),
 };

+ 16 - 56
client/lib/escapeActions.js

@@ -31,7 +31,7 @@ EscapeActions = {
       enabledOnClick = true;
     }
 
-    let noClickEscapeOn = options.noClickEscapeOn;
+    const noClickEscapeOn = options.noClickEscapeOn;
 
     this._actions = _.sortBy([...this._actions, {
       priority,
@@ -44,20 +44,20 @@ EscapeActions = {
 
   executeLowest() {
     return this._execute({
-      multipleAction: false
+      multipleAction: false,
     });
   },
 
   executeAll() {
     return this._execute({
-      multipleActions: true
+      multipleActions: true,
     });
   },
 
   executeUpTo(maxLabel) {
     return this._execute({
-      maxLabel: maxLabel,
-      multipleActions: true
+      maxLabel,
+      multipleActions: true,
     });
   },
 
@@ -66,10 +66,10 @@ EscapeActions = {
       this._nextclickPrevented = false;
     } else {
       return this._execute({
-        maxLabel: maxLabel,
+        maxLabel,
         multipleActions: false,
         isClick: true,
-        clickTarget: target
+        clickTarget: target,
       });
     }
   },
@@ -79,7 +79,7 @@ EscapeActions = {
   },
 
   _stopClick(action, clickTarget) {
-    if (! _.isString(action.noClickEscapeOn))
+    if (!_.isString(action.noClickEscapeOn))
       return false;
     else
       return $(clickTarget).closest(action.noClickEscapeOn).length > 0;
@@ -88,86 +88,46 @@ EscapeActions = {
   _execute(options) {
     const maxLabel = options.maxLabel;
     const multipleActions = options.multipleActions;
-    const isClick = !! options.isClick;
+    const isClick = Boolean(options.isClick);
     const clickTarget = options.clickTarget;
 
     let executedAtLeastOne = false;
     let maxPriority;
 
-    if (! maxLabel)
+    if (!maxLabel)
       maxPriority = Infinity;
     else
       maxPriority = this.hierarchy.indexOf(maxLabel);
 
-    for (let currentAction of this._actions) {
+    for (const currentAction of this._actions) {
       if (currentAction.priority > maxPriority)
         return executedAtLeastOne;
 
       if (isClick && this._stopClick(currentAction, clickTarget))
         return executedAtLeastOne;
 
-      let isEnabled = currentAction.enabledOnClick || ! isClick;
+      const isEnabled = currentAction.enabledOnClick || !isClick;
       if (isEnabled && currentAction.condition()) {
         currentAction.action();
         executedAtLeastOne = true;
-        if (! multipleActions)
+        if (!multipleActions)
           return executedAtLeastOne;
       }
     }
     return executedAtLeastOne;
-  }
-};
-
-// MouseTrap plugin bindGlobal plugin. Adds a bindGlobal method to Mousetrap
-// that allows you to bind specific keyboard shortcuts that will still work
-// inside a text input field.
-//
-// usage:
-// Mousetrap.bindGlobal('ctrl+s', _saveChanges);
-//
-// source:
-// https://github.com/ccampbell/mousetrap/tree/master/plugins/global-bind
-var _globalCallbacks = {};
-var _originalStopCallback = Mousetrap.stopCallback;
-
-Mousetrap.stopCallback = function(e, element, combo, sequence) {
-  var self = this;
-
-  if (self.paused) {
-    return true;
-  }
-
-  if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {
-    return false;
-  }
-
-  return _originalStopCallback.call(self, e, element, combo);
-};
-
-Mousetrap.bindGlobal = function(keys, callback, action) {
-  var self = this;
-  self.bind(keys, callback, action);
-
-  if (keys instanceof Array) {
-    for (var i = 0; i < keys.length; i++) {
-      _globalCallbacks[keys[i]] = true;
-    }
-    return;
-  }
-
-  _globalCallbacks[keys] = true;
+  },
 };
 
 // Pressing escape to execute one escape action. We use `bindGloabal` vecause
 // the shortcut sould work on textarea and inputs as well.
-Mousetrap.bindGlobal('esc', function() {
+Mousetrap.bindGlobal('esc', () => {
   EscapeActions.executeLowest();
 });
 
 // On a left click on the document, we try to exectute one escape action (eg,
 // close the popup). We don't execute any action if the user has clicked on a
 // link or a button.
-$(document).on('click', function(evt) {
+$(document).on('click', (evt) => {
   if (evt.button === 0 &&
     $(evt.target).closest('a,button,.is-editable').length === 0) {
     EscapeActions.clickExecute(evt.target, 'multiselection');

+ 42 - 46
client/lib/filter.js

@@ -4,66 +4,66 @@
 // goal is to filter complete documents by using the local filters for each
 // fields.
 
-var showFilterSidebar = function() {
+function showFilterSidebar() {
   Sidebar.setView('filter');
-};
+}
 
 // Use a "set" filter for a field that is a set of documents uniquely
 // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
-var SetFilter = function() {
-  this._dep = new Tracker.Dependency();
-  this._selectedElements = [];
-};
+class SetFilter {
+  constructor() {
+    this._dep = new Tracker.Dependency();
+    this._selectedElements = [];
+  }
 
-_.extend(SetFilter.prototype, {
-  isSelected: function(val) {
+  isSelected(val) {
     this._dep.depend();
     return this._selectedElements.indexOf(val) > -1;
-  },
+  }
 
-  add: function(val) {
+  add(val) {
     if (this._indexOfVal(val) === -1) {
       this._selectedElements.push(val);
       this._dep.changed();
       showFilterSidebar();
     }
-  },
+  }
 
-  remove: function(val) {
-    var indexOfVal = this._indexOfVal(val);
+  remove(val) {
+    const indexOfVal = this._indexOfVal(val);
     if (this._indexOfVal(val) !== -1) {
       this._selectedElements.splice(indexOfVal, 1);
       this._dep.changed();
     }
-  },
+  }
 
-  toogle: function(val) {
+  toogle(val) {
     if (this._indexOfVal(val) === -1) {
       this.add(val);
     } else {
       this.remove(val);
     }
-  },
+  }
 
-  reset: function() {
+  reset() {
     this._selectedElements = [];
     this._dep.changed();
-  },
+  }
 
-  _indexOfVal: function(val) {
+  _indexOfVal(val) {
     return this._selectedElements.indexOf(val);
-  },
+  }
 
-  _isActive: function() {
+  _isActive() {
     this._dep.depend();
     return this._selectedElements.length !== 0;
-  },
+  }
 
-  _getMongoSelector: function() {
+  _getMongoSelector() {
     this._dep.depend();
     return { $in: this._selectedElements };
   }
-});
+}
 
 // The global Filter object.
 // XXX It would be possible to re-write this object more elegantly, and removing
@@ -84,50 +84,46 @@ Filter = {
   _exceptions: [],
   _exceptionsDep: new Tracker.Dependency(),
 
-  isActive: function() {
-    var self = this;
-    return _.any(self._fields, function(fieldName) {
-      return self[fieldName]._isActive();
+  isActive() {
+    return _.any(this._fields, (fieldName) => {
+      return this[fieldName]._isActive();
     });
   },
 
-  _getMongoSelector: function() {
-    var self = this;
-
-    if (! self.isActive())
+  _getMongoSelector() {
+    if (!this.isActive())
       return {};
 
-    var filterSelector = {};
-    _.forEach(self._fields, function(fieldName) {
-      var filter = self[fieldName];
+    const filterSelector = {};
+    _.forEach(this._fields, (fieldName) => {
+      const filter = this[fieldName];
       if (filter._isActive())
         filterSelector[fieldName] = filter._getMongoSelector();
     });
 
-    var exceptionsSelector = {_id: {$in: this._exceptions}};
+    const exceptionsSelector = {_id: {$in: this._exceptions}};
     this._exceptionsDep.depend();
 
     return {$or: [filterSelector, exceptionsSelector]};
   },
 
-  mongoSelector: function(additionalSelector) {
-    var filterSelector = this._getMongoSelector();
+  mongoSelector(additionalSelector) {
+    const filterSelector = this._getMongoSelector();
     if (_.isUndefined(additionalSelector))
       return filterSelector;
     else
       return {$and: [filterSelector, additionalSelector]};
   },
 
-  reset: function() {
-    var self = this;
-    _.forEach(self._fields, function(fieldName) {
-      var filter = self[fieldName];
+  reset() {
+    _.forEach(this._fields, (fieldName) => {
+      const filter = this[fieldName];
       filter.reset();
     });
-    self.resetExceptions();
+    this.resetExceptions();
   },
 
-  addException: function(_id) {
+  addException(_id) {
     if (this.isActive()) {
       this._exceptions.push(_id);
       this._exceptionsDep.changed();
@@ -135,10 +131,10 @@ Filter = {
     }
   },
 
-  resetExceptions: function() {
+  resetExceptions() {
     this._exceptions = [];
     this._exceptionsDep.changed();
-  }
+  },
 };
 
 Blaze.registerHelper('Filter', Filter);

+ 4 - 5
client/lib/i18n.js

@@ -2,9 +2,9 @@
 // the language reactively. If the user is not connected we use the language
 // information provided by the browser, and default to english.
 
-Tracker.autorun(function() {
-  var language;
-  var currentUser = Meteor.user();
+Tracker.autorun(() => {
+  const currentUser = Meteor.user();
+  let language;
   if (currentUser) {
     language = currentUser.profile && currentUser.profile.language;
   } else {
@@ -12,11 +12,10 @@ Tracker.autorun(function() {
   }
 
   if (language) {
-
     TAPi18n.setLanguage(language);
 
     // XXX
-    var shortLanguage = language.split('-')[0];
+    const shortLanguage = language.split('-')[0];
     T9n.setLanguage(shortLanguage);
   }
 });

+ 16 - 16
client/lib/inlinedform.js

@@ -13,66 +13,66 @@
 //     // the content when the form is close (optional)
 
 // We can only have one inlined form element opened at a time
-currentlyOpenedForm = new ReactiveVar(null);
+const currentlyOpenedForm = new ReactiveVar(null);
 
 InlinedForm = BlazeComponent.extendComponent({
-  template: function() {
+  template() {
     return 'inlinedForm';
   },
 
-  onCreated: function() {
+  onCreated() {
     this.isOpen = new ReactiveVar(false);
   },
 
-  onDestroyed: function() {
+  onDestroyed() {
     currentlyOpenedForm.set(null);
   },
 
-  open: function() {
+  open() {
     // Close currently opened form, if any
     EscapeActions.executeUpTo('inlinedForm');
     this.isOpen.set(true);
     currentlyOpenedForm.set(this);
   },
 
-  close: function() {
+  close() {
     this.isOpen.set(false);
     currentlyOpenedForm.set(null);
   },
 
-  getValue: function() {
-    var input = this.find('textarea,input[type=text]');
+  getValue() {
+    const input = this.find('textarea,input[type=text]');
     return this.isOpen.get() && input && input.value;
   },
 
-  events: function() {
+  events() {
     return [{
       'click .js-close-inlined-form': this.close,
       'click .js-open-inlined-form': this.open,
 
       // Pressing Ctrl+Enter should submit the form
-      'keydown form textarea': function(evt) {
+      'keydown form textarea'(evt) {
         if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
           this.find('button[type=submit]').click();
         }
       },
 
       // Close the inlined form when after its submission
-      submit: function() {
+      submit() {
         if (this.currentData().autoclose !== false) {
           Tracker.afterFlush(() => {
             this.close();
           });
         }
-      }
+      },
     }];
-  }
+  },
 }).register('inlinedForm');
 
 // Press escape to close the currently opened inlinedForm
 EscapeActions.register('inlinedForm',
-  function() { currentlyOpenedForm.get().close(); },
-  function() { return currentlyOpenedForm.get() !== null; }, {
-    noClickEscapeOn: '.js-inlined-form'
+  () => { currentlyOpenedForm.get().close(); },
+  () => { return currentlyOpenedForm.get() !== null; }, {
+    noClickEscapeOn: '.js-inlined-form',
   }
 );

+ 9 - 9
client/lib/keyboard.js

@@ -24,7 +24,7 @@ Mousetrap.bind('x', () => {
 });
 
 Mousetrap.bind(['down', 'up'], (evt, key) => {
-  if (! Session.get('currentCard')) {
+  if (!Session.get('currentCard')) {
     return;
   }
 
@@ -39,24 +39,24 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
 Template.keyboardShortcuts.helpers({
   mapping: [{
     keys: ['W'],
-    action: 'shortcut-toogle-sidebar'
+    action: 'shortcut-toogle-sidebar',
   }, {
     keys: ['Q'],
-    action: 'shortcut-filter-my-cards'
+    action: 'shortcut-filter-my-cards',
   }, {
     keys: ['X'],
-    action: 'shortcut-clear-filters'
+    action: 'shortcut-clear-filters',
   }, {
     keys: ['?'],
-    action: 'shortcut-show-shortcuts'
+    action: 'shortcut-show-shortcuts',
   }, {
     keys: ['ESC'],
-    action: 'shortcut-close-dialog'
+    action: 'shortcut-close-dialog',
   }, {
     keys: ['@'],
-    action: 'shortcut-autocomplete-members'
+    action: 'shortcut-autocomplete-members',
   }, {
     keys: [':'],
-    action: 'shortcut-autocomplete-emojies'
-  }]
+    action: 'shortcut-autocomplete-emojies',
+  }],
 });

+ 1 - 1
client/lib/modal.js

@@ -1,4 +1,4 @@
-const closedValue = null
+const closedValue = null;
 
 window.Modal = new class {
   constructor() {

+ 46 - 47
client/lib/multiSelection.js

@@ -1,53 +1,53 @@
 
-var getCardsBetween = function(idA, idB) {
+function getCardsBetween(idA, idB) {
 
-  var pluckId = function(doc) {
+  function pluckId(doc) {
     return doc._id;
-  };
+  }
 
-  var getListsStrictlyBetween = function(id1, id2) {
+  function getListsStrictlyBetween(id1, id2) {
     return Lists.find({
       $and: [
         { sort: { $gt: Lists.findOne(id1).sort } },
-        { sort: { $lt: Lists.findOne(id2).sort } }
+        { sort: { $lt: Lists.findOne(id2).sort } },
       ],
-      archived: false
+      archived: false,
     }).map(pluckId);
-  };
+  }
 
-  var cards = _.sortBy([Cards.findOne(idA), Cards.findOne(idB)], function(c) {
+  const cards = _.sortBy([Cards.findOne(idA), Cards.findOne(idB)], (c) => {
     return c.sort;
   });
 
-  var selector;
+  let selector;
   if (cards[0].listId === cards[1].listId) {
     selector = {
       listId: cards[0].listId,
       sort: {
         $gte: cards[0].sort,
-        $lte: cards[1].sort
+        $lte: cards[1].sort,
       },
-      archived: false
+      archived: false,
     };
   } else {
     selector = {
       $or: [{
         listId: cards[0].listId,
-        sort: { $lte: cards[0].sort }
+        sort: { $lte: cards[0].sort },
       }, {
         listId: {
-          $in: getListsStrictlyBetween(cards[0].listId, cards[1].listId)
-        }
+          $in: getListsStrictlyBetween(cards[0].listId, cards[1].listId),
+        },
       }, {
         listId: cards[1].listId,
-        sort: { $gte: cards[1].sort }
+        sort: { $gte: cards[1].sort },
       }],
-      archived: false
+      archived: false,
     };
   }
 
   return Cards.find(Filter.mongoSelector(selector)).map(pluckId);
-};
+}
 
 MultiSelection = {
   sidebarView: 'multiselection',
@@ -58,30 +58,30 @@ MultiSelection = {
 
   startRangeCardId: null,
 
-  reset: function() {
+  reset() {
     this._selectedCards.set([]);
   },
 
-  getMongoSelector: function() {
+  getMongoSelector() {
     return Filter.mongoSelector({
-      _id: { $in: this._selectedCards.get() }
+      _id: { $in: this._selectedCards.get() },
     });
   },
 
-  isActive: function() {
+  isActive() {
     return this._isActive.get();
   },
 
-  count: function() {
+  count() {
     return Cards.find(this.getMongoSelector()).count();
   },
 
-  isEmpty: function() {
+  isEmpty() {
     return this.count() === 0;
   },
 
-  activate: function() {
-    if (! this.isActive()) {
+  activate() {
+    if (!this.isActive()) {
       EscapeActions.executeUpTo('detailsPane');
       this._isActive.set(true);
       Tracker.flush();
@@ -89,7 +89,7 @@ MultiSelection = {
     Sidebar.setView(this.sidebarView);
   },
 
-  disable: function() {
+  disable() {
     if (this.isActive()) {
       this._isActive.set(false);
       if (Sidebar && Sidebar.getView() === this.sidebarView) {
@@ -99,19 +99,19 @@ MultiSelection = {
     }
   },
 
-  add: function(cardIds) {
+  add(cardIds) {
     return this.toogle(cardIds, { add: true, remove: false });
   },
 
-  remove: function(cardIds) {
+  remove(cardIds) {
     return this.toogle(cardIds, { add: false, remove: true });
   },
 
-  toogleRange: function(cardId) {
-    var selectedCards = this._selectedCards.get();
-    var startRange;
+  toogleRange(cardId) {
+    const selectedCards = this._selectedCards.get();
+    let startRange;
     this.reset();
-    if (! this.isActive() || selectedCards.length === 0) {
+    if (!this.isActive() || selectedCards.length === 0) {
       this.toogle(cardId);
     } else {
       startRange = selectedCards[selectedCards.length - 1];
@@ -119,23 +119,22 @@ MultiSelection = {
     }
   },
 
-  toogle: function(cardIds, options) {
-    var self = this;
+  toogle(cardIds, options) {
     cardIds = _.isString(cardIds) ? [cardIds] : cardIds;
     options = _.extend({
       add: true,
-      remove: true
+      remove: true,
     }, options || {});
 
-    if (! self.isActive()) {
-      self.reset();
-      self.activate();
+    if (!this.isActive()) {
+      this.reset();
+      this.activate();
     }
 
-    var selectedCards = self._selectedCards.get();
+    const selectedCards = this._selectedCards.get();
 
-    _.each(cardIds, function(cardId) {
-      var indexOfCard = selectedCards.indexOf(cardId);
+    _.each(cardIds, (cardId) => {
+      const indexOfCard = selectedCards.indexOf(cardId);
 
       if (options.remove && indexOfCard > -1)
         selectedCards.splice(indexOfCard, 1);
@@ -144,19 +143,19 @@ MultiSelection = {
         selectedCards.push(cardId);
     });
 
-    self._selectedCards.set(selectedCards);
+    this._selectedCards.set(selectedCards);
   },
 
-  isSelected: function(cardId) {
+  isSelected(cardId) {
     return this._selectedCards.get().indexOf(cardId) > -1;
-  }
+  },
 };
 
 Blaze.registerHelper('MultiSelection', MultiSelection);
 
 EscapeActions.register('multiselection',
-  function() { MultiSelection.disable(); },
-  function() { return MultiSelection.isActive(); }, {
-    noClickEscapeOn: '.js-minicard,.js-board-sidebar-content'
+  () => { MultiSelection.disable(); },
+  () => { return MultiSelection.isActive(); }, {
+    noClickEscapeOn: '.js-minicard,.js-board-sidebar-content',
   }
 );

+ 60 - 63
client/lib/popup.js

@@ -1,55 +1,53 @@
 // A simple tracker dependency that we invalidate every time the window is
 // resized. This is used to reactively re-calculate the popup position in case
 // of a window resize. This is the equivalent of a "Signal" in some other
-// programming environments.
-let windowResizeDep = new Tracker.Dependency()
-$(window).on('resize', () => windowResizeDep.changed())
+// programming environments (eg, elm).
+const windowResizeDep = new Tracker.Dependency();
+$(window).on('resize', () => windowResizeDep.changed());
 
 window.Popup = new class {
   constructor() {
     // The template we use to render popups
-    this.template = Template.popup
+    this.template = Template.popup;
 
     // We only want to display one popup at a time and we keep the view object
     // in this `Popup._current` variable. If there is no popup currently opened
     // the value is `null`.
-    this._current = null
+    this._current = null;
 
     // It's possible to open a sub-popup B from a popup A. In that case we keep
     // the data of popup A so we can return back to it. Every time we open a new
     // popup the stack grows, every time we go back the stack decrease, and if
     // we close the popup the stack is reseted to the empty stack [].
-    this._stack = []
+    this._stack = [];
 
     // We invalidate this internal dependency every time the top of the stack
     // has changed and we want to re-render a popup with the new top-stack data.
-    this._dep = new Tracker.Dependency()
+    this._dep = new Tracker.Dependency();
   }
 
   /// This function returns a callback that can be used in an event map:
-  ///
   ///   Template.tplName.events({
-  ///     'click .elementClass': Popup.open("popupName")
-  ///   })
-  ///
+  ///     'click .elementClass': Popup.open("popupName"),
+  ///   });
   /// The popup inherit the data context of its parent.
   open(name) {
-    let self = this
-    const popupName = `${name}Popup`
+    const self = this;
+    const popupName = `${name}Popup`;
 
     function clickFromPopup(evt) {
-      return $(evt.target).closest('.js-pop-over').length !== 0
+      return $(evt.target).closest('.js-pop-over').length !== 0;
     }
 
     return function(evt) {
       // If a popup is already opened, clicking again on the opener element
       // should close it -- and interrupt the current `open` function.
       if (self.isOpen()) {
-        let previousOpenerElement = self._getTopStack().openerElement
+        const previousOpenerElement = self._getTopStack().openerElement;
         if (previousOpenerElement === evt.currentTarget) {
-          return self.close()
+          return self.close();
         } else {
-          $(previousOpenerElement).removeClass('is-active')
+          $(previousOpenerElement).removeClass('is-active');
         }
       }
 
@@ -58,16 +56,16 @@ window.Popup = new class {
       // if the popup has no parent, or from the parent `openerElement` if it
       // has one. This allows us to position a sub-popup exactly at the same
       // position than its parent.
-      let openerElement
+      let openerElement;
       if (clickFromPopup(evt)) {
-        openerElement = self._getTopStack().openerElement
+        openerElement = self._getTopStack().openerElement;
       } else {
-        self._stack = []
-        openerElement = evt.currentTarget
+        self._stack = [];
+        openerElement = evt.currentTarget;
       }
 
-      $(openerElement).addClass('is-active')
-      evt.preventDefault()
+      $(openerElement).addClass('is-active');
+      evt.preventDefault();
 
       // We push our popup data to the stack. The top of the stack is always
       // used as the data source for our current popup.
@@ -79,7 +77,7 @@ window.Popup = new class {
         depth: self._stack.length,
         offset: self._getOffset(openerElement),
         dataContext: this.currentData && this.currentData() || this,
-      })
+      });
 
       // If there are no popup currently opened we use the Blaze API to render
       // one into the DOM. We use a reactive function as the data parameter that
@@ -90,39 +88,38 @@ window.Popup = new class {
       // Otherwise if there is already a popup open we just need to invalidate
       // our internal dependency, and since we just changed the top element of
       // our internal stack, the popup will be updated with the new data.
-      if (! self.isOpen()) {
+      if (!self.isOpen()) {
         self.current = Blaze.renderWithData(self.template, () => {
-          self._dep.depend()
-          return _.extend(self._getTopStack(), { stack: self._stack })
-        }, document.body)
+          self._dep.depend();
+          return _.extend(self._getTopStack(), { stack: self._stack });
+        }, document.body);
 
       } else {
-        self._dep.changed()
+        self._dep.changed();
       }
-    }
+    };
   }
 
   /// This function returns a callback that can be used in an event map:
-  ///
   ///   Template.tplName.events({
   ///     'click .elementClass': Popup.afterConfirm("popupName", function() {
   ///       // What to do after the user has confirmed the action
-  ///     })
-  ///   })
+  ///     }),
+  ///   });
   afterConfirm(name, action) {
-    let self = this
+    const self = this;
 
     return function(evt, tpl) {
-      let context = this.currentData && this.currentData() || this
-      context.__afterConfirmAction = action
-      self.open(name).call(context, evt, tpl)
-    }
+      const context = this.currentData && this.currentData() || this;
+      context.__afterConfirmAction = action;
+      self.open(name).call(context, evt, tpl);
+    };
   }
 
   /// The public reactive state of the popup.
   isOpen() {
-    this._dep.changed()
-    return !! this.current
+    this._dep.changed();
+    return Boolean(this.current);
   }
 
   /// In case the popup was opened from a parent popup we can get back to it
@@ -132,45 +129,45 @@ window.Popup = new class {
   /// steps back is greater than the popup stack size, the popup will be closed.
   back(n = 1) {
     if (this._stack.length > n) {
-      _.times(n, () => this._stack.pop())
-      this._dep.changed()
+      _.times(n, () => this._stack.pop());
+      this._dep.changed();
     } else {
-      this.close()
+      this.close();
     }
   }
 
   /// Close the current opened popup.
   close() {
     if (this.isOpen()) {
-      Blaze.remove(this.current)
-      this.current = null
+      Blaze.remove(this.current);
+      this.current = null;
 
-      let openerElement = this._getTopStack().openerElement
-      $(openerElement).removeClass('is-active')
+      const openerElement = this._getTopStack().openerElement;
+      $(openerElement).removeClass('is-active');
 
-      this._stack = []
+      this._stack = [];
     }
   }
 
   // An utility fonction that returns the top element of the internal stack
   _getTopStack() {
-    return this._stack[this._stack.length - 1]
+    return this._stack[this._stack.length - 1];
   }
 
   // We automatically calculate the popup offset from the reference element
   // position and dimensions. We also reactively use the window dimensions to
   // ensure that the popup is always visible on the screen.
   _getOffset(element) {
-    let $element = $(element)
+    const $element = $(element);
     return () => {
-      windowResizeDep.depend()
-      const offset = $element.offset()
-      const popupWidth = 300 + 15
+      windowResizeDep.depend();
+      const offset = $element.offset();
+      const popupWidth = 300 + 15;
       return {
         left: Math.min(offset.left, $(window).width() - popupWidth),
         top: offset.top + $element.outerHeight(),
-      }
-    }
+      };
+    };
   }
 
   // We get the title from the translation files. Instead of returning the
@@ -178,22 +175,22 @@ window.Popup = new class {
   // is a reactive data source, the title will be changed reactively.
   _getTitle(popupName) {
     return () => {
-      const translationKey = `${popupName}-title`
+      const translationKey = `${popupName}-title`;
 
       // XXX There is no public API to check if there is an available
       // translation for a given key. So we try to translate the key and if the
       // translation output equals the key input we deduce that no translation
       // was available and returns `false`. There is a (small) risk a false
       // positives.
-      const title = TAPi18n.__(translationKey)
-      return title !== translationKey ? title : false
-    }
+      const title = TAPi18n.__(translationKey);
+      return title !== translationKey ? title : false;
+    };
   }
-}
+};
 
 // We close a potential opened popup on any left click on the document, or go
 // one step back by pressing escape.
-const escapeActions = ['back', 'close']
+const escapeActions = ['back', 'close'];
 _.each(escapeActions, (actionName) => {
   EscapeActions.register(`popup-${actionName}`,
     () => Popup[actionName](),
@@ -202,6 +199,6 @@ _.each(escapeActions, (actionName) => {
       noClickEscapeOn: '.js-pop-over',
       enabledOnClick: actionName === 'close',
     }
-  )
-})
+  );
+});
 

+ 8 - 12
client/lib/unsavedEdits.js

@@ -27,9 +27,9 @@ UnsavedEdits = {
   // _collection: UnsavedEditCollection,
 
   get({ fieldName, docId }, defaultTo = '') {
-    let unsavedValue = this._getCollectionDocument(fieldName, docId);
+    const unsavedValue = this._getCollectionDocument(fieldName, docId);
     if (unsavedValue) {
-      return unsavedValue.value
+      return unsavedValue.value;
     } else {
       return defaultTo;
     }
@@ -40,13 +40,9 @@ UnsavedEdits = {
   },
 
   set({ fieldName, docId }, value) {
-    let currentDoc = this._getCollectionDocument(fieldName, docId);
+    const currentDoc = this._getCollectionDocument(fieldName, docId);
     if (currentDoc) {
-      UnsavedEditCollection.update(currentDoc._id, {
-        $set: {
-          value: value
-        }
-      });
+      UnsavedEditCollection.update(currentDoc._id, { $set: { value }});
     } else {
       UnsavedEditCollection.insert({
         fieldName,
@@ -57,7 +53,7 @@ UnsavedEdits = {
   },
 
   reset({ fieldName, docId }) {
-    let currentDoc = this._getCollectionDocument(fieldName, docId);
+    const currentDoc = this._getCollectionDocument(fieldName, docId);
     if (currentDoc) {
       UnsavedEditCollection.remove(currentDoc._id);
     }
@@ -65,13 +61,13 @@ UnsavedEdits = {
 
   _getCollectionDocument(fieldName, docId) {
     return UnsavedEditCollection.findOne({fieldName, docId});
-  }
-}
+  },
+};
 
 Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => {
   // Workaround some blaze feature that ass a list of keywords arguments as the
   // last parameter (even if the caller didn't specify any).
-  if (! _.isString(defaultTo)) {
+  if (!_.isString(defaultTo)) {
     defaultTo = '';
   }
   return UnsavedEdits.get({ fieldName, docId }, defaultTo);

+ 37 - 29
client/lib/utils.js

@@ -1,62 +1,70 @@
 Utils = {
   // XXX We should remove these two methods
-  goBoardId: function(_id) {
-    var board = Boards.findOne(_id);
+  goBoardId(_id) {
+    const board = Boards.findOne(_id);
     return board && FlowRouter.go('board', {
       id: board._id,
-      slug: board.slug
+      slug: board.slug,
     });
   },
 
-  goCardId: function(_id) {
-    var card = Cards.findOne(_id);
-    var board = Boards.findOne(card.boardId);
+  goCardId(_id) {
+    const card = Cards.findOne(_id);
+    const board = Boards.findOne(card.boardId);
     return board && FlowRouter.go('card', {
       cardId: card._id,
       boardId: board._id,
-      slug: board.slug
+      slug: board.slug,
     });
   },
 
-  capitalize: function(string) {
+  capitalize(string) {
     return string.charAt(0).toUpperCase() + string.slice(1);
   },
 
-  getLabelIndex: function(boardId, labelId) {
-    var board = Boards.findOne(boardId);
-    var labels = {};
-    _.each(board.labels, function(a, b) {
+  getLabelIndex(boardId, labelId) {
+    const board = Boards.findOne(boardId);
+    const labels = {};
+    _.each(board.labels, (a, b) => {
       labels[a._id] = b;
     });
     return {
       index: labels[labelId],
-      key: function(key) {
-        return 'labels.' + labels[labelId] + '.' + key;
-      }
+      key(key) {
+        return `labels.${labels[labelId]}.${key}`;
+      },
     };
   },
 
   // Determine the new sort index
-  calculateIndex: function(prevCardDomElement, nextCardDomElement, nCards) {
-    nCards = nCards || 1;
-
+  calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
+    let base, increment;
     // If we drop the card to an empty column
-    if (! prevCardDomElement && ! nextCardDomElement) {
-      return {base: 0, increment: 1};
+    if (!prevCardDomElement && !nextCardDomElement) {
+      base = 0;
+      increment = 1;
     // If we drop the card in the first position
-    } else if (! prevCardDomElement) {
-      return {base: Blaze.getData(nextCardDomElement).sort - 1, increment: -1};
+    } else if (!prevCardDomElement) {
+      base = Blaze.getData(nextCardDomElement).sort - 1;
+      increment = -1;
     // If we drop the card in the last position
-    } else if (! nextCardDomElement) {
-      return {base: Blaze.getData(prevCardDomElement).sort + 1, increment: 1};
+    } else if (!nextCardDomElement) {
+      base = Blaze.getData(prevCardDomElement).sort + 1;
+      increment = 1;
     }
     // In the general case take the average of the previous and next element
     // sort indexes.
     else {
-      var prevSortIndex = Blaze.getData(prevCardDomElement).sort;
-      var nextSortIndex = Blaze.getData(nextCardDomElement).sort;
-      var increment = (nextSortIndex - prevSortIndex) / (nCards + 1);
-      return {base: prevSortIndex + increment, increment: increment};
+      const prevSortIndex = Blaze.getData(prevCardDomElement).sort;
+      const nextSortIndex = Blaze.getData(nextCardDomElement).sort;
+      increment = (nextSortIndex - prevSortIndex) / (nCards + 1);
+      base = prevSortIndex + increment;
     }
-  }
+    // XXX Return a generator that yield values instead of a base with a
+    // increment number.
+    return {
+      base,
+      increment,
+    };
+  },
 };

+ 12 - 12
collections/activities.js

@@ -11,41 +11,41 @@
 Activities = new Mongo.Collection('activities');
 
 Activities.helpers({
-  board: function() {
+  board() {
     return Boards.findOne(this.boardId);
   },
-  user: function() {
+  user() {
     return Users.findOne(this.userId);
   },
-  member: function() {
+  member() {
     return Users.findOne(this.memberId);
   },
-  list: function() {
+  list() {
     return Lists.findOne(this.listId);
   },
-  oldList: function() {
+  oldList() {
     return Lists.findOne(this.oldListId);
   },
-  card: function() {
+  card() {
     return Cards.findOne(this.cardId);
   },
-  comment: function() {
+  comment() {
     return CardComments.findOne(this.commentId);
   },
-  attachment: function() {
+  attachment() {
     return Attachments.findOne(this.attachmentId);
-  }
+  },
 });
 
-Activities.before.insert(function(userId, doc) {
+Activities.before.insert((userId, doc) => {
   doc.createdAt = new Date();
 });
 
 // For efficiency create an index on the date of creation.
 if (Meteor.isServer) {
-  Meteor.startup(function() {
+  Meteor.startup(() => {
     Activities._collection._ensureIndex({
-      createdAt: -1
+      createdAt: -1,
     });
   });
 }

+ 18 - 18
collections/attachments.js

@@ -3,19 +3,19 @@ Attachments = new FS.Collection('attachments', {
 
     // XXX Add a new store for cover thumbnails so we don't load big images in
     // the general board view
-    new FS.Store.GridFS('attachments')
-  ]
+    new FS.Store.GridFS('attachments'),
+  ],
 });
 
 if (Meteor.isServer) {
   Attachments.allow({
-    insert: function(userId, doc) {
+    insert(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    update: function(userId, doc) {
+    update(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    remove: function(userId, doc) {
+    remove(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
     // We authorize the attachment download either:
@@ -26,24 +26,24 @@ if (Meteor.isServer) {
     //
     //   https://github.com/CollectionFS/Meteor-CollectionFS/issues/449
     //
-    download: function(userId, doc) {
-      var query = {
+    download(userId, doc) {
+      const query = {
         $or: [
           { 'members.userId': userId },
-          { permission: 'public' }
-        ]
+          { permission: 'public' },
+        ],
       };
-      return !! Boards.findOne(doc.boardId, query);
+      return Boolean(Boards.findOne(doc.boardId, query));
     },
 
-    fetch: ['boardId']
+    fetch: ['boardId'],
   });
 }
 
 // XXX Enforce a schema for the Attachments CollectionFS
 
-Attachments.files.before.insert(function(userId, doc) {
-  var file = new FS.File(doc);
+Attachments.files.before.insert((userId, doc) => {
+  const file = new FS.File(doc);
   doc.userId = userId;
 
   // If the uploaded document is not an image we need to enforce browser
@@ -54,26 +54,26 @@ Attachments.files.before.insert(function(userId, doc) {
   // See https://github.com/libreboard/libreboard/issues/99
   // XXX Should we use `beforeWrite` option of CollectionFS instead of
   // collection-hooks?
-  if (! file.isImage()) {
+  if (!file.isImage()) {
     file.original.type = 'application/octet-stream';
   }
 });
 
 if (Meteor.isServer) {
-  Attachments.files.after.insert(function(userId, doc) {
+  Attachments.files.after.insert((userId, doc) => {
     Activities.insert({
+      userId,
       type: 'card',
       activityType: 'addAttachment',
       attachmentId: doc._id,
       boardId: doc.boardId,
       cardId: doc.cardId,
-      userId: userId
     });
   });
 
-  Attachments.files.after.remove(function(userId, doc) {
+  Attachments.files.after.remove((userId, doc) => {
     Activities.remove({
-      attachmentId: doc._id
+      attachmentId: doc._id,
     });
   });
 }

+ 9 - 9
collections/avatars.js

@@ -1,27 +1,27 @@
 Avatars = new FS.Collection('avatars', {
   stores: [
-    new FS.Store.GridFS('avatars')
+    new FS.Store.GridFS('avatars'),
   ],
   filter: {
     maxSize: 72000,
     allow: {
-      contentTypes: ['image/*']
-    }
-  }
+      contentTypes: ['image/*'],
+    },
+  },
 });
 
-var isOwner = function(userId, file) {
+function isOwner(userId, file) {
   return userId && userId === file.userId;
-};
+}
 
 Avatars.allow({
   insert: isOwner,
   update: isOwner,
   remove: isOwner,
-  download: function() { return true; },
-  fetch: ['userId']
+  download() { return true; },
+  fetch: ['userId'],
 });
 
-Avatars.files.before.insert(function(userId, doc) {
+Avatars.files.before.insert((userId, doc) => {
   doc.userId = userId;
 });

+ 68 - 68
collections/boards.js

@@ -2,27 +2,27 @@ Boards = new Mongo.Collection('boards');
 
 Boards.attachSchema(new SimpleSchema({
   title: {
-    type: String
+    type: String,
   },
   slug: {
-    type: String
+    type: String,
   },
   archived: {
-    type: Boolean
+    type: Boolean,
   },
   createdAt: {
     type: Date,
-    denyUpdate: true
+    denyUpdate: true,
   },
   // XXX Inconsistent field naming
   modifiedAt: {
     type: Date,
     denyInsert: true,
-    optional: true
+    optional: true,
   },
   // De-normalized number of users that have starred this board
   stars: {
-    type: Number
+    type: Number,
   },
   // De-normalized label system
   'labels.$._id': {
@@ -31,46 +31,46 @@ Boards.attachSchema(new SimpleSchema({
     // always set on the server.
     // XXX Actually if we create a new label, the `_id` is set on the client
     // without being overwritten by the server, could it be a problem?
-    type: String
+    type: String,
   },
   'labels.$.name': {
     type: String,
-    optional: true
+    optional: true,
   },
   'labels.$.color': {
     type: String,
     allowedValues: [
       'green', 'yellow', 'orange', 'red', 'purple',
-      'blue', 'sky', 'lime', 'pink', 'black'
-    ]
+      'blue', 'sky', 'lime', 'pink', 'black',
+    ],
   },
   // XXX We might want to maintain more informations under the member sub-
   // documents like de-normalized meta-data (the date the member joined the
   // board, the number of contributions, etc.).
   'members.$.userId': {
-    type: String
+    type: String,
   },
   'members.$.isAdmin': {
-    type: Boolean
+    type: Boolean,
   },
   'members.$.isActive': {
-    type: Boolean
+    type: Boolean,
   },
   permission: {
     type: String,
-    allowedValues: ['public', 'private']
+    allowedValues: ['public', 'private'],
   },
   color: {
     type: String,
     allowedValues: [
-    'belize',
-    'nephritis',
-    'pomegranate',
-    'pumpkin',
-    'wisteria',
-    'midnight',
-    ]
-  }
+      'belize',
+      'nephritis',
+      'pomegranate',
+      'pumpkin',
+      'wisteria',
+      'midnight',
+    ],
+  },
 }));
 
 if (Meteor.isServer) {
@@ -78,30 +78,30 @@ if (Meteor.isServer) {
     insert: Meteor.userId,
     update: allowIsBoardAdmin,
     remove: allowIsBoardAdmin,
-    fetch: ['members']
+    fetch: ['members'],
   });
 
   // The number of users that have starred this board is managed by trusted code
   // and the user is not allowed to update it
   Boards.deny({
-    update: function(userId, board, fieldNames) {
+    update(userId, board, fieldNames) {
       return _.contains(fieldNames, 'stars');
     },
-    fetch: []
+    fetch: [],
   });
 
   // We can't remove a member if it is the last administrator
   Boards.deny({
-    update: function(userId, doc, fieldNames, modifier) {
-      if (! _.contains(fieldNames, 'members'))
+    update(userId, doc, fieldNames, modifier) {
+      if (!_.contains(fieldNames, 'members'))
         return false;
 
       // We only care in case of a $pull operation, ie remove a member
-      if (! _.isObject(modifier.$pull && modifier.$pull.members))
+      if (!_.isObject(modifier.$pull && modifier.$pull.members))
         return false;
 
       // If there is more than one admin, it's ok to remove anyone
-      var nbAdmins = _.filter(doc.members, function(member) {
+      const nbAdmins = _.filter(doc.members, (member) => {
         return member.isAdmin;
       }).length;
       if (nbAdmins > 1)
@@ -109,36 +109,36 @@ if (Meteor.isServer) {
 
       // If all the previous conditions were verified, we can't remove
       // a user if it's an admin
-      var removedMemberId = modifier.$pull.members.userId;
-      return !! _.findWhere(doc.members, {
+      const removedMemberId = modifier.$pull.members.userId;
+      return Boolean(_.findWhere(doc.members, {
         userId: removedMemberId,
-        isAdmin: true
-      });
+        isAdmin: true,
+      }));
     },
-    fetch: ['members']
+    fetch: ['members'],
   });
 }
 
 Boards.helpers({
-  isPublic: function() {
+  isPublic() {
     return this.permission === 'public';
   },
-  lists: function() {
+  lists() {
     return Lists.find({ boardId: this._id, archived: false },
                                                           { sort: { sort: 1 }});
   },
-  activities: function() {
+  activities() {
     return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }});
   },
-  absoluteUrl: function() {
+  absoluteUrl() {
     return FlowRouter.path('board', { id: this._id, slug: this.slug });
   },
-  colorClass: function() {
-    return 'board-color-' + this.color;
-  }
+  colorClass() {
+    return `board-color-${this.color}`;
+  },
 });
 
-Boards.before.insert(function(userId, doc) {
+Boards.before.insert((userId, doc) => {
   // XXX We need to improve slug management. Only the id should be necessary
   // to identify a board in the code.
   // XXX If the board title is updated, the slug should also be updated.
@@ -149,87 +149,87 @@ Boards.before.insert(function(userId, doc) {
   doc.createdAt = new Date();
   doc.archived = false;
   doc.members = [{
-    userId: userId,
+    userId,
     isAdmin: true,
-    isActive: true
+    isActive: true,
   }];
   doc.stars = 0;
   doc.color = Boards.simpleSchema()._schema.color.allowedValues[0];
 
   // Handle labels
-  var colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
-  var defaultLabelsColors = _.clone(colors).splice(0, 6);
-  doc.labels = _.map(defaultLabelsColors, function(val) {
+  const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
+  const defaultLabelsColors = _.clone(colors).splice(0, 6);
+  doc.labels = _.map(defaultLabelsColors, (color) => {
     return {
+      color,
       _id: Random.id(6),
       name: '',
-      color: val
     };
   });
 });
 
-Boards.before.update(function(userId, doc, fieldNames, modifier) {
+Boards.before.update((userId, doc, fieldNames, modifier) => {
   modifier.$set = modifier.$set || {};
   modifier.$set.modifiedAt = new Date();
 });
 
 if (Meteor.isServer) {
   // Let MongoDB ensure that a member is not included twice in the same board
-  Meteor.startup(function() {
+  Meteor.startup(() => {
     Boards._collection._ensureIndex({
       _id: 1,
-      'members.userId': 1
+      'members.userId': 1,
     }, { unique: true });
   });
 
   // Genesis: the first activity of the newly created board
-  Boards.after.insert(function(userId, doc) {
+  Boards.after.insert((userId, doc) => {
     Activities.insert({
+      userId,
       type: 'board',
       activityTypeId: doc._id,
       activityType: 'createBoard',
       boardId: doc._id,
-      userId: userId
     });
   });
 
   // If the user remove one label from a board, we cant to remove reference of
   // this label in any card of this board.
-  Boards.after.update(function(userId, doc, fieldNames, modifier) {
-    if (! _.contains(fieldNames, 'labels') ||
-      ! modifier.$pull ||
-      ! modifier.$pull.labels ||
-      ! modifier.$pull.labels._id)
+  Boards.after.update((userId, doc, fieldNames, modifier) => {
+    if (!_.contains(fieldNames, 'labels') ||
+      !modifier.$pull ||
+      !modifier.$pull.labels ||
+      !modifier.$pull.labels._id)
       return;
 
-    var removedLabelId = modifier.$pull.labels._id;
+    const removedLabelId = modifier.$pull.labels._id;
     Cards.update(
       { boardId: doc._id },
       {
         $pull: {
-          labels: removedLabelId
-        }
+          labels: removedLabelId,
+        },
       },
       { multi: true }
     );
   });
 
   // Add a new activity if we add or remove a member to the board
-  Boards.after.update(function(userId, doc, fieldNames, modifier) {
-    if (! _.contains(fieldNames, 'members'))
+  Boards.after.update((userId, doc, fieldNames, modifier) => {
+    if (!_.contains(fieldNames, 'members'))
       return;
 
-    var memberId;
+    let memberId;
 
     // Say hello to the new member
     if (modifier.$push && modifier.$push.members) {
       memberId = modifier.$push.members.userId;
       Activities.insert({
+        userId,
+        memberId,
         type: 'member',
         activityType: 'addBoardMember',
         boardId: doc._id,
-        userId: userId,
-        memberId: memberId
       });
     }
 
@@ -237,11 +237,11 @@ if (Meteor.isServer) {
     if (modifier.$pull && modifier.$pull.members) {
       memberId = modifier.$pull.members.userId;
       Activities.insert({
+        userId,
+        memberId,
         type: 'member',
         activityType: 'removeBoardMember',
         boardId: doc._id,
-        userId: userId,
-        memberId: memberId
       });
     }
   });

+ 72 - 73
collections/cards.js

@@ -6,162 +6,161 @@ CardComments = new Mongo.Collection('card_comments');
 // of comments just to display the number of them in the board view.
 Cards.attachSchema(new SimpleSchema({
   title: {
-    type: String
+    type: String,
   },
   archived: {
-    type: Boolean
+    type: Boolean,
   },
   listId: {
-    type: String
+    type: String,
   },
   // The system could work without this `boardId` information (we could deduce
   // the board identifier from the card), but it would make the system more
   // difficult to manage and less efficient.
   boardId: {
-    type: String
+    type: String,
   },
   coverId: {
     type: String,
-    optional: true
+    optional: true,
   },
   createdAt: {
     type: Date,
-    denyUpdate: true
+    denyUpdate: true,
   },
   dateLastActivity: {
-    type: Date
+    type: Date,
   },
   description: {
     type: String,
-    optional: true
+    optional: true,
   },
   labelIds: {
     type: [String],
-    optional: true
+    optional: true,
   },
   members: {
     type: [String],
-    optional: true
+    optional: true,
   },
   // XXX Should probably be called `authorId`. Is it even needed since we have
   // the `members` field?
   userId: {
-    type: String
+    type: String,
   },
   sort: {
     type: Number,
-    decimal: true
-  }
+    decimal: true,
+  },
 }));
 
 CardComments.attachSchema(new SimpleSchema({
   boardId: {
-    type: String
+    type: String,
   },
   cardId: {
-    type: String
+    type: String,
   },
   // XXX Rename in `content`? `text` is a bit vague...
   text: {
-    type: String
+    type: String,
   },
   // XXX We probably don't need this information here, since we already have it
   // in the associated comment creation activity
   createdAt: {
     type: Date,
-    denyUpdate: false
+    denyUpdate: false,
   },
   // XXX Should probably be called `authorId`
   userId: {
-    type: String
-  }
+    type: String,
+  },
 }));
 
 if (Meteor.isServer) {
   Cards.allow({
-    insert: function(userId, doc) {
+    insert(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    update: function(userId, doc) {
+    update(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    remove: function(userId, doc) {
+    remove(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    fetch: ['boardId']
+    fetch: ['boardId'],
   });
 
   CardComments.allow({
-    insert: function(userId, doc) {
+    insert(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    update: function(userId, doc) {
+    update(userId, doc) {
       return userId === doc.userId;
     },
-    remove: function(userId, doc) {
+    remove(userId, doc) {
       return userId === doc.userId;
     },
-    fetch: ['userId', 'boardId']
+    fetch: ['userId', 'boardId'],
   });
 }
 
 Cards.helpers({
-  list: function() {
+  list() {
     return Lists.findOne(this.listId);
   },
-  board: function() {
+  board() {
     return Boards.findOne(this.boardId);
   },
-  labels: function() {
-    var self = this;
-    var boardLabels = self.board().labels;
-    var cardLabels = _.filter(boardLabels, function(label) {
-      return _.contains(self.labelIds, label._id);
+  labels() {
+    const boardLabels = this.board().labels;
+    const cardLabels = _.filter(boardLabels, (label) => {
+      return _.contains(this.labelIds, label._id);
     });
     return cardLabels;
   },
-  hasLabel: function(labelId) {
+  hasLabel(labelId) {
     return _.contains(this.labelIds, labelId);
   },
-  user: function() {
+  user() {
     return Users.findOne(this.userId);
   },
-  isAssigned: function(memberId) {
+  isAssigned(memberId) {
     return _.contains(this.members, memberId);
   },
-  activities: function() {
+  activities() {
     return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }});
   },
-  comments: function() {
+  comments() {
     return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }});
   },
-  attachments: function() {
+  attachments() {
     return Attachments.find({ cardId: this._id }, { sort: { uploadedAt: -1 }});
   },
-  cover: function() {
+  cover() {
     return Attachments.findOne(this.coverId);
   },
-  absoluteUrl: function() {
-    var board = this.board();
+  absoluteUrl() {
+    const board = this.board();
     return FlowRouter.path('card', {
       boardId: board._id,
       slug: board.slug,
-      cardId: this._id
+      cardId: this._id,
     });
   },
-  rootUrl: function() {
+  rootUrl() {
     return Meteor.absoluteUrl(this.absoluteUrl().replace('/', ''));
-  }
+  },
 });
 
 CardComments.helpers({
-  user: function() {
+  user() {
     return Users.findOne(this.userId);
-  }
+  },
 });
 
 CardComments.hookOptions.after.update = { fetchPrevious: false };
-Cards.before.insert(function(userId, doc) {
+Cards.before.insert((userId, doc) => {
   doc.createdAt = new Date();
   doc.dateLastActivity = new Date();
 
@@ -169,44 +168,44 @@ Cards.before.insert(function(userId, doc) {
   doc.archived = false;
 
   // userId native set.
-  if (! doc.userId)
+  if (!doc.userId)
     doc.userId = userId;
 });
 
-CardComments.before.insert(function(userId, doc) {
+CardComments.before.insert((userId, doc) => {
   doc.createdAt = new Date();
   doc.userId = userId;
 });
 
 if (Meteor.isServer) {
-  Cards.after.insert(function(userId, doc) {
+  Cards.after.insert((userId, doc) => {
     Activities.insert({
+      userId,
       activityType: 'createCard',
       boardId: doc.boardId,
       listId: doc.listId,
       cardId: doc._id,
-      userId: userId
     });
   });
 
   // New activity for card (un)archivage
-  Cards.after.update(function(userId, doc, fieldNames) {
+  Cards.after.update((userId, doc, fieldNames) => {
     if (_.contains(fieldNames, 'archived')) {
       if (doc.archived) {
         Activities.insert({
+          userId,
           activityType: 'archivedCard',
           boardId: doc.boardId,
           listId: doc.listId,
           cardId: doc._id,
-          userId: userId
         });
       } else {
         Activities.insert({
+          userId,
           activityType: 'restoredCard',
           boardId: doc.boardId,
           listId: doc.listId,
           cardId: doc._id,
-          userId: userId
         });
       }
     }
@@ -214,34 +213,34 @@ if (Meteor.isServer) {
 
   // New activity for card moves
   Cards.after.update(function(userId, doc, fieldNames) {
-    var oldListId = this.previous.listId;
+    const oldListId = this.previous.listId;
     if (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) {
       Activities.insert({
+        userId,
+        oldListId,
         activityType: 'moveCard',
         listId: doc.listId,
-        oldListId: oldListId,
         boardId: doc.boardId,
         cardId: doc._id,
-        userId: userId
       });
     }
   });
 
   // Add a new activity if we add or remove a member to the card
-  Cards.before.update(function(userId, doc, fieldNames, modifier) {
-    if (! _.contains(fieldNames, 'members'))
+  Cards.before.update((userId, doc, fieldNames, modifier) => {
+    if (!_.contains(fieldNames, 'members'))
       return;
-    var memberId;
+    let memberId;
     // Say hello to the new member
     if (modifier.$addToSet && modifier.$addToSet.members) {
       memberId = modifier.$addToSet.members;
-      if (! _.contains(doc.members, memberId)) {
+      if (!_.contains(doc.members, memberId)) {
         Activities.insert({
+          userId,
+          memberId,
           activityType: 'joinMember',
           boardId: doc.boardId,
           cardId: doc._id,
-          userId: userId,
-          memberId: memberId
         });
       }
     }
@@ -250,34 +249,34 @@ if (Meteor.isServer) {
     if (modifier.$pull && modifier.$pull.members) {
       memberId = modifier.$pull.members;
       Activities.insert({
+        userId,
+        memberId,
         activityType: 'unjoinMember',
         boardId: doc.boardId,
         cardId: doc._id,
-        userId: userId,
-        memberId: memberId
       });
     }
   });
 
   // Remove all activities associated with a card if we remove the card
-  Cards.after.remove(function(userId, doc) {
+  Cards.after.remove((userId, doc) => {
     Activities.remove({
-      cardId: doc._id
+      cardId: doc._id,
     });
   });
 
-  CardComments.after.insert(function(userId, doc) {
+  CardComments.after.insert((userId, doc) => {
     Activities.insert({
+      userId,
       activityType: 'addComment',
       boardId: doc.boardId,
       cardId: doc.cardId,
       commentId: doc._id,
-      userId: userId
     });
   });
 
-  CardComments.after.remove(function(userId, doc) {
-    var activity = Activities.findOne({ commentId: doc._id });
+  CardComments.after.remove((userId, doc) => {
+    const activity = Activities.findOne({ commentId: doc._id });
     if (activity) {
       Activities.remove(activity._id);
     }

+ 22 - 22
collections/lists.js

@@ -2,92 +2,92 @@ Lists = new Mongo.Collection('lists');
 
 Lists.attachSchema(new SimpleSchema({
   title: {
-    type: String
+    type: String,
   },
   archived: {
-    type: Boolean
+    type: Boolean,
   },
   boardId: {
-    type: String
+    type: String,
   },
   createdAt: {
     type: Date,
-    denyUpdate: true
+    denyUpdate: true,
   },
   sort: {
     type: Number,
     decimal: true,
     // XXX We should probably provide a default
-    optional: true
+    optional: true,
   },
   updatedAt: {
     type: Date,
     denyInsert: true,
-    optional: true
-  }
+    optional: true,
+  },
 }));
 
 if (Meteor.isServer) {
   Lists.allow({
-    insert: function(userId, doc) {
+    insert(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    update: function(userId, doc) {
+    update(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    remove: function(userId, doc) {
+    remove(userId, doc) {
       return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
     },
-    fetch: ['boardId']
+    fetch: ['boardId'],
   });
 }
 
 Lists.helpers({
-  cards: function() {
+  cards() {
     return Cards.find(Filter.mongoSelector({
       listId: this._id,
-      archived: false
+      archived: false,
     }), { sort: ['sort'] });
   },
-  board: function() {
+  board() {
     return Boards.findOne(this.boardId);
-  }
+  },
 });
 
 // HOOKS
 Lists.hookOptions.after.update = { fetchPrevious: false };
 
-Lists.before.insert(function(userId, doc) {
+Lists.before.insert((userId, doc) => {
   doc.createdAt = new Date();
   doc.archived = false;
-  if (! doc.userId)
+  if (!doc.userId)
     doc.userId = userId;
 });
 
-Lists.before.update(function(userId, doc, fieldNames, modifier) {
+Lists.before.update((userId, doc, fieldNames, modifier) => {
   modifier.$set = modifier.$set || {};
   modifier.$set.modifiedAt = new Date();
 });
 
 if (Meteor.isServer) {
-  Lists.after.insert(function(userId, doc) {
+  Lists.after.insert((userId, doc) => {
     Activities.insert({
+      userId,
       type: 'list',
       activityType: 'createList',
       boardId: doc.boardId,
       listId: doc._id,
-      userId: userId
     });
   });
 
-  Lists.after.update(function(userId, doc) {
+  Lists.after.update((userId, doc) => {
     if (doc.archived) {
       Activities.insert({
+        userId,
         type: 'list',
         activityType: 'archivedList',
         listId: doc._id,
         boardId: doc.boardId,
-        userId: userId
       });
     }
   });

+ 6 - 6
collections/unsavedEdits.js

@@ -4,16 +4,16 @@ UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
 
 UnsavedEditCollection.attachSchema(new SimpleSchema({
   fieldName: {
-    type: String
+    type: String,
   },
   docId: {
-    type: String
+    type: String,
   },
   value: {
-    type: String
+    type: String,
   },
   userId: {
-    type: String
+    type: String,
   },
 }));
 
@@ -25,10 +25,10 @@ if (Meteor.isServer) {
     insert: isAuthor,
     update: isAuthor,
     remove: isAuthor,
-    fetch: ['userId']
+    fetch: ['userId'],
   });
 }
 
-UnsavedEditCollection.before.insert(function(userId, doc) {
+UnsavedEditCollection.before.insert((userId, doc) => {
   doc.userId = userId;
 });

+ 49 - 53
collections/users.js

@@ -2,42 +2,42 @@ Users = Meteor.users;
 
 // Search a user in the complete server database by its name or username. This
 // is used for instance to add a new user to a board.
-var searchInFields = ['username', 'profile.name'];
+const searchInFields = ['username', 'profile.name'];
 Users.initEasySearch(searchInFields, {
   use: 'mongo-db',
-  returnFields: searchInFields
+  returnFields: searchInFields,
 });
 
 Users.helpers({
-  boards: function() {
+  boards() {
     return Boards.find({ userId: this._id });
   },
-  starredBoards: function() {
-    var starredBoardIds = this.profile.starredBoards || [];
+  starredBoards() {
+    const starredBoardIds = this.profile.starredBoards || [];
     return Boards.find({archived: false, _id: {$in: starredBoardIds}});
   },
-  hasStarred: function(boardId) {
-    var starredBoardIds = this.profile.starredBoards || [];
+  hasStarred(boardId) {
+    const starredBoardIds = this.profile.starredBoards || [];
     return _.contains(starredBoardIds, boardId);
   },
-  isBoardMember: function() {
-    var board = Boards.findOne(Session.get('currentBoard'));
+  isBoardMember() {
+    const board = Boards.findOne(Session.get('currentBoard'));
     return board && _.contains(_.pluck(board.members, 'userId'), this._id) &&
                          _.where(board.members, {userId: this._id})[0].isActive;
   },
-  isBoardAdmin: function() {
-    var board = Boards.findOne(Session.get('currentBoard'));
+  isBoardAdmin() {
+    const board = Boards.findOne(Session.get('currentBoard'));
     if (this.isBoardMember(board))
       return _.where(board.members, {userId: this._id})[0].isAdmin;
   },
 
-  getInitials: function() {
-    var profile = this.profile || {};
+  getInitials() {
+    const profile = this.profile || {};
     if (profile.initials)
       return profile.initials;
 
     else if (profile.fullname) {
-      return _.reduce(profile.fullname.split(/\s+/), function(memo, word) {
+      return _.reduce(profile.fullname.split(/\s+/), (memo, word) => {
         return memo + word[0];
       }, '').toUpperCase();
 
@@ -46,43 +46,41 @@ Users.helpers({
     }
   },
 
-  toggleBoardStar: function(boardId) {
-    var queryType = this.hasStarred(boardId) ? '$pull' : '$addToSet';
-    var query = {};
-    query[queryType] = {
-      'profile.starredBoards': boardId
-    };
-    Meteor.users.update(this._id, query);
-  }
+  toggleBoardStar(boardId) {
+    const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
+    Meteor.users.update(this._id, {
+      [queryKind]: {
+        'profile.starredBoards': boardId,
+      },
+    });
+  },
 });
 
 Meteor.methods({
-  setUsername: function(username) {
+  setUsername(username) {
     check(username, String);
-    var nUsersWithUsername = Users.find({username: username}).count();
+    const nUsersWithUsername = Users.find({ username }).count();
     if (nUsersWithUsername > 0) {
       throw new Meteor.Error('username-already-taken');
     } else {
-      Users.update(this.userId, {$set: {
-        username: username
-      }});
+      Users.update(this.userId, {$set: { username }});
     }
-  }
+  },
 });
 
-Users.before.insert(function(userId, doc) {
+Users.before.insert((userId, doc) => {
   doc.profile = doc.profile || {};
 
-  if (! doc.username && doc.profile.name) {
+  if (!doc.username && doc.profile.name) {
     doc.username = doc.profile.name.toLowerCase().replace(/\s/g, '');
   }
 });
 
 if (Meteor.isServer) {
   // Let mongoDB ensure username unicity
-  Meteor.startup(function() {
+  Meteor.startup(() => {
     Users._collection._ensureIndex({
-      username: 1
+      username: 1,
     }, { unique: true });
   });
 
@@ -94,44 +92,44 @@ if (Meteor.isServer) {
   Users.after.update(function(userId, user, fieldNames) {
     // The `starredBoards` list is hosted on the `profile` field. If this
     // field hasn't been modificated we don't need to run this hook.
-    if (! _.contains(fieldNames, 'profile'))
+    if (!_.contains(fieldNames, 'profile'))
       return;
 
     // To calculate a diff of board starred ids, we get both the previous
     // and the newly board ids list
-    var getStarredBoardsIds = function(doc) {
+    function getStarredBoardsIds(doc) {
       return doc.profile && doc.profile.starredBoards;
-    };
-    var oldIds = getStarredBoardsIds(this.previous);
-    var newIds = getStarredBoardsIds(user);
+    }
+    const oldIds = getStarredBoardsIds(this.previous);
+    const newIds = getStarredBoardsIds(user);
 
     // The _.difference(a, b) method returns the values from a that are not in
     // b. We use it to find deleted and newly inserted ids by using it in one
     // direction and then in the other.
-    var incrementBoards = function(boardsIds, inc) {
-      _.forEach(boardsIds, function(boardId) {
+    function incrementBoards(boardsIds, inc) {
+      _.forEach(boardsIds, (boardId) => {
         Boards.update(boardId, {$inc: {stars: inc}});
       });
-    };
+    }
     incrementBoards(_.difference(oldIds, newIds), -1);
     incrementBoards(_.difference(newIds, oldIds), +1);
   });
 
   // XXX i18n
-  Users.after.insert(function(userId, doc) {
-    var ExampleBoard = {
+  Users.after.insert((userId, doc) => {
+    const ExampleBoard = {
       title: 'Welcome Board',
       userId: doc._id,
-      permission: 'private'
+      permission: 'private',
     };
 
     // Insert the Welcome Board
-    Boards.insert(ExampleBoard, function(err, boardId) {
+    Boards.insert(ExampleBoard, (err, boardId) => {
 
-      _.forEach(['Basics', 'Advanced'], function(title) {
-        var list = {
-          title: title,
-          boardId: boardId,
+      _.forEach(['Basics', 'Advanced'], (title) => {
+        const list = {
+          title,
+          boardId,
           userId: ExampleBoard.userId,
 
           // XXX Not certain this is a bug, but we except these fields get
@@ -139,7 +137,7 @@ if (Meteor.isServer) {
           // hook is not called in this case, we have to dublicate the logic and
           // set them here.
           archived: false,
-          createdAt: new Date()
+          createdAt: new Date(),
         };
 
         Lists.insert(list);
@@ -150,9 +148,7 @@ if (Meteor.isServer) {
 
 // Presence indicator
 if (Meteor.isClient) {
-  Presence.state = function() {
-    return {
-      currentBoardId: Session.get('currentBoard')
-    };
+  Presence.state = () => {
+    return { currentBoardId: Session.get('currentBoard') };
   };
 }

+ 28 - 30
sandstorm.js

@@ -1,12 +1,12 @@
 // Sandstorm context is detected using the METEOR_SETTINGS environment variable
 // in the package definition.
-var isSandstorm = Meteor.settings && Meteor.settings.public &&
-                  Meteor.settings.public.sandstorm;
+const isSandstorm = Meteor.settings && Meteor.settings.public &&
+                    Meteor.settings.public.sandstorm;
 
 // In sandstorm we only have one board per sandstorm instance. Since we want to
 // keep most of our code unchanged, we simply hard-code a board `_id` and
 // redirect the user to this particular board.
-var sandstormBoard = {
+const sandstormBoard = {
   _id: 'sandstorm',
 
   // XXX Should be shared with the grain instance name.
@@ -16,15 +16,15 @@ var sandstormBoard = {
   // Board access security is handled by sandstorm, so in our point of view we
   // can alway assume that the board is public (unauthorized users won’t be able
   // to access it anyway).
-  permission: 'public'
+  permission: 'public',
 };
 
 // The list of permissions a user have is provided by sandstorm accounts
 // package.
-var userHasPermission = function(user, permission) {
-  var userPermissions = user.services.sandstorm.permissions;
+function userHasPermission(user, permission) {
+  const userPermissions = user.services.sandstorm.permissions;
   return userPermissions.indexOf(permission) > -1;
-};
+}
 
 if (isSandstorm && Meteor.isServer) {
   // Redirect the user to the hard-coded board. On the first launch the user
@@ -35,15 +35,15 @@ if (isSandstorm && Meteor.isServer) {
   // browser, a server-side redirection solves both of these issues.
   //
   // XXX Maybe sandstorm manifest could provide some kind of "home url"?
-  Picker.route('/', function(params, request, response) {
-    var base = request.headers['x-sandstorm-base-path'];
+  Picker.route('/', (params, request, response) => {
+    const base = request.headers['x-sandstorm-base-path'];
     // XXX If this routing scheme changes, this will break. We should generation
     // the location url using the router, but at the time of writting, the
     // router is only accessible on the client.
-    var path = '/boards/' + sandstormBoard._id + '/' + sandstormBoard.slug;
+    const path = `/boards/${sandstormBoard._id}/${sandstormBoard.slug}`;
 
     response.writeHead(301, {
-      Location: base + path
+      Location: base + path,
     });
     response.end();
   });
@@ -53,8 +53,8 @@ if (isSandstorm && Meteor.isServer) {
   // unique board document. Note that when the `Users.after.insert` hook is
   // called, the user is inserted into the database but not connected. So
   // despite the appearances `userId` is null in this block.
-  Users.after.insert(function(userId, doc) {
-    if (! Boards.findOne(sandstormBoard._id)) {
+  Users.after.insert((userId, doc) => {
+    if (!Boards.findOne(sandstormBoard._id)) {
       Boards.insert(sandstormBoard, {validate: false});
       Boards.update(sandstormBoard._id, {
         $set: {
@@ -62,14 +62,14 @@ if (isSandstorm && Meteor.isServer) {
           'members.0': {
             userId: doc._id,
             isActive: true,
-            isAdmin: true
-          }
-        }
+            isAdmin: true,
+          },
+        },
       });
       Activities.update(
-        { activityTypeId: sandstormBoard._id }, {
-        $set: { userId: doc._id }
-      });
+        { activityTypeId: sandstormBoard._id },
+        { $set: { userId: doc._id }}
+      );
     }
 
     // If the hard-coded board already exists and we are inserting a new user,
@@ -77,15 +77,15 @@ if (isSandstorm && Meteor.isServer) {
     else if (userHasPermission(doc, 'participate')) {
       Boards.update({
         _id: sandstormBoard._id,
-        permission: 'public'
+        permission: 'public',
       }, {
         $push: {
           members: {
             userId: doc._id,
             isActive: true,
-            isAdmin: userHasPermission(doc, 'configure')
-          }
-        }
+            isAdmin: userHasPermission(doc, 'configure'),
+          },
+        },
       });
     }
   });
@@ -96,10 +96,10 @@ if (isSandstorm && Meteor.isClient) {
   // session has a different URL whereas Meteor computes absoluteUrl based on
   // the ROOT_URL environment variable. So we overwrite this function on a
   // sandstorm client to return relative paths instead of absolutes.
-  var _absoluteUrl = Meteor.absoluteUrl;
-  var _defaultOptions = Meteor.absoluteUrl.defaultOptions;
-  Meteor.absoluteUrl = function(path, options) {
-    var url = _absoluteUrl(path, options);
+  const _absoluteUrl = Meteor.absoluteUrl;
+  const _defaultOptions = Meteor.absoluteUrl.defaultOptions;
+  Meteor.absoluteUrl = (path, options) => {
+    const url = _absoluteUrl(path, options);
     return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, '');
   };
   Meteor.absoluteUrl.defaultOptions = _defaultOptions;
@@ -108,6 +108,4 @@ if (isSandstorm && Meteor.isClient) {
 // We use this blaze helper in the UI to hide some templates that does not make
 // sense in the context of sandstorm, like board staring, board archiving, user
 // name edition, etc.
-Blaze.registerHelper('isSandstorm', function() {
-  return isSandstorm;
-});
+Blaze.registerHelper('isSandstorm', () => isSandstorm);

+ 1 - 1
server/lib/utils.js

@@ -1,5 +1,5 @@
 allowIsBoardAdmin = function(userId, board) {
-  var admins = _.pluck(_.where(board.members, {isAdmin: true}), 'userId');
+  const admins = _.pluck(_.where(board.members, {isAdmin: true}), 'userId');
   return _.contains(admins, userId);
 };
 

+ 43 - 43
server/migrations.js

@@ -11,35 +11,35 @@
 //
 // To prevent this bug we always have to disable the schema validation and
 // argument transformations. We generally use the shorthandlers defined below.
-var noValidate = {
+const noValidate = {
   validate: false,
   filter: false,
   autoConvert: false,
   removeEmptyStrings: false,
-  getAutoValues: false
+  getAutoValues: false,
 };
-var noValidateMulti = _.extend(noValidate, { multi: true });
+const noValidateMulti = { ...noValidate, multi: true };
 
-Migrations.add('board-background-color', function() {
-  var defaultColor = '#16A085';
+Migrations.add('board-background-color', () => {
+  const defaultColor = '#16A085';
   Boards.update({
     background: {
-      $exists: false
-    }
+      $exists: false,
+    },
   }, {
     $set: {
       background: {
         type: 'color',
-        color: defaultColor
-      }
-    }
+        color: defaultColor,
+      },
+    },
   }, noValidateMulti);
 });
 
-Migrations.add('lowercase-board-permission', function() {
-  _.forEach(['Public', 'Private'], function(permission) {
+Migrations.add('lowercase-board-permission', () => {
+  _.forEach(['Public', 'Private'], (permission) => {
     Boards.update(
-      { permission: permission },
+      { permission },
       { $set: { permission: permission.toLowerCase() } },
       noValidateMulti
     );
@@ -47,23 +47,23 @@ Migrations.add('lowercase-board-permission', function() {
 });
 
 // Security migration: see https://github.com/wekan/wekan/issues/99
-Migrations.add('change-attachments-type-for-non-images', function() {
-  var newTypeForNonImage = 'application/octet-stream';
-  Attachments.find().forEach(function(file) {
-    if (! file.isImage()) {
+Migrations.add('change-attachments-type-for-non-images', () => {
+  const newTypeForNonImage = 'application/octet-stream';
+  Attachments.find().forEach((file) => {
+    if (!file.isImage()) {
       Attachments.update(file._id, {
         $set: {
           'original.type': newTypeForNonImage,
-          'copies.attachments.type': newTypeForNonImage
-        }
+          'copies.attachments.type': newTypeForNonImage,
+        },
       }, noValidate);
     }
   });
 });
 
-Migrations.add('card-covers', function() {
-  Cards.find().forEach(function(card) {
-    var cover =  Attachments.findOne({ cardId: card._id, cover: true });
+Migrations.add('card-covers', () => {
+  Cards.find().forEach((card) => {
+    const cover =  Attachments.findOne({ cardId: card._id, cover: true });
     if (cover) {
       Cards.update(card._id, {$set: {coverId: cover._id}}, noValidate);
     }
@@ -71,54 +71,54 @@ Migrations.add('card-covers', function() {
   Attachments.update({}, {$unset: {cover: ''}}, noValidateMulti);
 });
 
-Migrations.add('use-css-class-for-boards-colors', function() {
-  var associationTable = {
+Migrations.add('use-css-class-for-boards-colors', () => {
+  const associationTable = {
     '#27AE60': 'nephritis',
     '#C0392B': 'pomegranate',
     '#2980B9': 'belize',
     '#8E44AD': 'wisteria',
     '#2C3E50': 'midnight',
-    '#E67E22': 'pumpkin'
+    '#E67E22': 'pumpkin',
   };
-  Boards.find().forEach(function(board) {
-    var oldBoardColor = board.background.color;
-    var newBoardColor = associationTable[oldBoardColor];
+  Boards.find().forEach((board) => {
+    const oldBoardColor = board.background.color;
+    const newBoardColor = associationTable[oldBoardColor];
     Boards.update(board._id, {
       $set: { color: newBoardColor },
-      $unset: { background: '' }
+      $unset: { background: '' },
     }, noValidate);
   });
 });
 
-Migrations.add('denormalize-star-number-per-board', function() {
-  Boards.find().forEach(function(board) {
-    var nStars = Users.find({'profile.starredBoards': board._id}).count();
+Migrations.add('denormalize-star-number-per-board', () => {
+  Boards.find().forEach((board) => {
+    const nStars = Users.find({'profile.starredBoards': board._id}).count();
     Boards.update(board._id, {$set: {stars: nStars}}, noValidate);
   });
 });
 
 // We want to keep a trace of former members so we can efficiently publish their
 // infos in the general board publication.
-Migrations.add('add-member-isactive-field', function() {
-  Boards.find({}, {fields: {members: 1}}).forEach(function(board) {
-    var allUsersWithSomeActivity = _.chain(
-      Activities.find({boardId: board._id}, {fields:{userId:1}}).fetch())
+Migrations.add('add-member-isactive-field', () => {
+  Boards.find({}, {fields: {members: 1}}).forEach((board) => {
+    const allUsersWithSomeActivity = _.chain(
+      Activities.find({ boardId: board._id }, { fields:{ userId:1 }}).fetch())
         .pluck('userId')
         .uniq()
         .value();
-    var currentUsers = _.pluck(board.members, 'userId');
-    var formerUsers = _.difference(allUsersWithSomeActivity, currentUsers);
+    const currentUsers = _.pluck(board.members, 'userId');
+    const formerUsers = _.difference(allUsersWithSomeActivity, currentUsers);
 
-    var newMemberSet = [];
-    _.forEach(board.members, function(member) {
+    const newMemberSet = [];
+    _.forEach(board.members, (member) => {
       member.isActive = true;
       newMemberSet.push(member);
     });
-    _.forEach(formerUsers, function(userId) {
+    _.forEach(formerUsers, (userId) => {
       newMemberSet.push({
-        userId: userId,
+        userId,
         isAdmin: false,
-        isActive: false
+        isActive: false,
       });
     });
     Boards.update(board._id, {$set: {members: newMemberSet}}, noValidate);

+ 10 - 15
server/publications/activities.js

@@ -1,24 +1,19 @@
-// We use activities fields at three different places:
-// 1. The home page that contains
-// 2. The board
-// 3.
-// We use publish paginate for these three publications.
+// We use activities fields at two different places:
+// 1. The board sidebar
+// 2. The card activity tab
+// We use this publication to paginate for these two publications.
 
-Meteor.publish('activities', function(mode, id, limit) {
-  check(mode, Match.Where(function(x) {
+Meteor.publish('activities', (kind, id, limit) => {
+  check(kind, Match.Where((x) => {
     return ['board', 'card'].indexOf(x) !== -1;
   }));
   check(id, String);
   check(limit, Number);
 
-  var selector = {};
-  if (mode === 'board')
-    selector.boardId = id;
-  else if (mode === 'card')
-    selector.cardId = id;
-
-  return Activities.find(selector, {
+  return Activities.find({
+    [`${kind}Id`]: id,
+  }, {
+    limit,
     sort: {createdAt: -1},
-    limit: limit
   });
 });

+ 36 - 36
server/publications/boards.js

@@ -5,20 +5,20 @@
 Meteor.publish('boards', function() {
   // Ensure that the user is connected. If it is not, we need to return an empty
   // array to tell the client to remove the previously published docs.
-  if (! Match.test(this.userId, String))
+  if (!Match.test(this.userId, String))
     return [];
 
   // Defensive programming to verify that starredBoards has the expected
   // format -- since the field is in the `profile` a user can modify it.
-  var starredBoards = Users.findOne(this.userId).profile.starredBoards || [];
+  const starredBoards = Users.findOne(this.userId).profile.starredBoards || [];
   check(starredBoards, [String]);
 
   return Boards.find({
     archived: false,
     $or: [
       { 'members.userId': this.userId },
-      { _id: { $in: starredBoards } }
-    ]
+      { _id: { $in: starredBoards } },
+    ],
   }, {
     fields: {
       _id: 1,
@@ -27,13 +27,13 @@ Meteor.publish('boards', function() {
       title: 1,
       color: 1,
       members: 1,
-      permission: 1
-    }
+      permission: 1,
+    },
   });
 });
 
 Meteor.publish('archivedBoards', function() {
-  if (! Match.test(this.userId, String))
+  if (!Match.test(this.userId, String))
     return [];
 
   return Boards.find({
@@ -41,23 +41,23 @@ Meteor.publish('archivedBoards', function() {
     members: {
       $elemMatch: {
         userId: this.userId,
-        isAdmin: true
-      }
-    }
+        isAdmin: true,
+      },
+    },
   }, {
     fields: {
       _id: 1,
       archived: 1,
       slug: 1,
-      title: 1
-    }
-  })
+      title: 1,
+    },
+  });
 });
 
 Meteor.publishComposite('board', function(boardId) {
   check(boardId, String);
   return {
-    find: function() {
+    find() {
       return Boards.find({
         _id: boardId,
         archived: false,
@@ -65,18 +65,18 @@ Meteor.publishComposite('board', function(boardId) {
         // it.
         $or: [
           { permission: 'public' },
-          { 'members.userId': this.userId }
-        ]
+          { 'members.userId': this.userId },
+        ],
       }, { limit: 1 });
     },
     children: [
       // Lists
       {
-        find: function(board) {
+        find(board) {
           return Lists.find({
-            boardId: board._id
+            boardId: board._id,
           });
-        }
+        },
       },
 
       // Cards and cards comments
@@ -103,48 +103,48 @@ Meteor.publishComposite('board', function(boardId) {
       // And in the meantime our code below works pretty well -- it's not even a
       // hack!
       {
-        find: function(board) {
+        find(board) {
           return Cards.find({
-            boardId: board._id
+            boardId: board._id,
           });
         },
 
         children: [
           // comments
           {
-            find: function(card) {
+            find(card) {
               return CardComments.find({
-                cardId: card._id
+                cardId: card._id,
               });
-            }
+            },
           },
           // Attachments
           {
-            find: function(card) {
+            find(card) {
               return Attachments.find({
-                cardId: card._id
+                cardId: card._id,
               });
-            }
-          }
-        ]
+            },
+          },
+        ],
       },
 
       // Board members. This publication also includes former board members that
       // aren't members anymore but may have some activities attached to them in
       // the history.
       {
-        find: function(board) {
+        find(board) {
           return Users.find({
-            _id: { $in: _.pluck(board.members, 'userId') }
+            _id: { $in: _.pluck(board.members, 'userId') },
           });
         },
         // Presence indicators
         children: [{
-          find: function(user) {
+          find(user) {
             return Presences.find({userId: user._id});
-          }
-        }]
-      }
-    ]
+          },
+        }],
+      },
+    ],
   };
 });

+ 1 - 1
server/publications/cards.js

@@ -1,4 +1,4 @@
-Meteor.publish('card', function(cardId) {
+Meteor.publish('card', (cardId) => {
   check(cardId, String);
   return Cards.find({ _id: cardId });
 });

+ 1 - 1
server/publications/unsavedEdits.js

@@ -1,5 +1,5 @@
 Meteor.publish('unsaved-edits', function() {
   return UnsavedEditCollection.find({
-    userId: this.userId
+    userId: this.userId,
   });
 });