Forráskód Böngészése

Add Worker role.
Add more Font Awesome icons.
Fix browser console errors when editing user profile name etc.

Thanks to xet7 !

Closes #2788

Lauri Ojansivu 5 éve
szülő
commit
2bf004120d
37 módosított fájl, 683 hozzáadás és 320 törlés
  1. 1 0
      .eslintrc.json
  2. 14 12
      client/components/cards/attachments.jade
  3. 2 1
      client/components/cards/cardDate.js
  4. 187 96
      client/components/cards/cardDetails.jade
  5. 44 2
      client/components/cards/cardDetails.js
  6. 3 1
      client/components/cards/checklists.jade
  7. 6 3
      client/components/cards/checklists.js
  8. 6 10
      client/components/cards/minicard.js
  9. 3 1
      client/components/cards/subtasks.jade
  10. 6 3
      client/components/cards/subtasks.js
  11. 3 5
      client/components/lists/list.js
  12. 2 1
      client/components/lists/listBody.js
  13. 35 12
      client/components/lists/listHeader.jade
  14. 7 8
      client/components/lists/listHeader.js
  15. 6 2
      client/components/settings/informationBody.jade
  16. 10 3
      client/components/settings/peopleBody.jade
  17. 1 1
      client/components/settings/peopleBody.styl
  18. 19 6
      client/components/settings/settingBody.jade
  19. 4 1
      client/components/settings/settingBody.styl
  20. 78 27
      client/components/sidebar/sidebar.jade
  21. 29 12
      client/components/sidebar/sidebar.js
  22. 30 24
      client/components/sidebar/sidebarArchives.jade
  23. 9 0
      client/components/sidebar/sidebarArchives.js
  24. 8 7
      client/components/sidebar/sidebarFilters.jade
  25. 3 5
      client/components/swimlanes/swimlaneHeader.js
  26. 17 16
      client/components/swimlanes/swimlanes.jade
  27. 9 15
      client/components/swimlanes/swimlanes.js
  28. 1 0
      client/components/users/userAvatar.jade
  29. 47 18
      client/components/users/userHeader.jade
  30. 21 3
      client/components/users/userHeader.js
  31. 18 22
      client/lib/utils.js
  32. 12 0
      models/accountSettings.js
  33. 28 1
      models/boards.js
  34. 1 1
      models/cards.js
  35. 1 1
      models/checklists.js
  36. 10 0
      models/users.js
  37. 2 0
      sandstorm.js

+ 1 - 0
.eslintrc.json

@@ -145,6 +145,7 @@
     "allowIsBoardMemberByCard": true,
     "allowIsBoardMemberCommentOnly": true,
     "allowIsBoardMemberNoComments": true,
+    "allowIsBoardMemberWorker": true,
     "Emoji": true,
     "Checklists": true,
     "Settings": true,

+ 14 - 12
client/components/cards/attachments.jade

@@ -38,18 +38,20 @@ template(name="attachmentsGalery")
               | {{_ 'download'}}
             if currentUser.isBoardMember
               unless currentUser.isCommentOnly
-                if isImage
-                  a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
-                    i.fa.fa-thumb-tack
-                    if($eq ../coverId _id)
-                      | {{_ 'remove-cover'}}
-                    else
-                      | {{_ 'add-cover'}}
-                a.js-confirm-delete
-                  i.fa.fa-close
-                  | {{_ 'delete'}}
+                unless currentUser.isWorker
+                  if isImage
+                    a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
+                      i.fa.fa-thumb-tack
+                      if($eq ../coverId _id)
+                        | {{_ 'remove-cover'}}
+                      else
+                        | {{_ 'add-cover'}}
+                  a.js-confirm-delete
+                    i.fa.fa-close
+                    | {{_ 'delete'}}
 
     if currentUser.isBoardMember
       unless currentUser.isCommentOnly
-        li.attachment-item.add-attachment
-          a.js-add-attachment {{_ 'add-attachment' }}
+        unless currentUser.isWorker
+          li.attachment-item.add-attachment
+            a.js-add-attachment {{_ 'add-attachment' }}

+ 2 - 1
client/components/cards/cardDate.js

@@ -97,7 +97,8 @@ Template.dateBadge.helpers({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 });

+ 187 - 96
client/components/cards/cardDetails.jade

@@ -1,7 +1,7 @@
 template(name="cardDetails")
   section.card-details.js-card-details.js-perfect-scrollbar: .card-details-canvas
     .card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
-      +inlinedForm(classNames="js-card-details-title")
+      +inlinedForm(classNames="{{#if canModifyCardWorker}}js-card-details-title{{/if}}")
         +editCardTitleForm
       else
         unless isMiniScreen
@@ -13,11 +13,11 @@ template(name="cardDetails")
           if currentUser.isBoardMember
             a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu
         h2.card-details-title.js-card-title(
-          class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
+          class="{{#if canModifyCardWorker}}js-open-inlined-form is-editable{{/if}}")
             +viewer
               = getTitle
-              if isWatching
-                i.fa.fa-eye.card-details-watch
+            if isWatching
+              i.card-details-watch.fa.fa-eye
         .card-details-path
           each parentList
             |   >  
@@ -37,49 +37,66 @@ template(name="cardDetails")
 
     .card-details-items
       .card-details-item.card-details-item-received
-        h3.card-details-item-title {{_ 'card-received'}}
+        h3
+          i.fa.fa-sign-out
+          card-details-item-title {{_ 'card-received'}}
         if getReceived
           +cardReceivedDate
         else
           if canModifyCard
-            a.js-received-date {{_ 'add'}}
+            unless currentUser.isWorker
+              a.js-received-date {{_ 'add'}}
 
       .card-details-item.card-details-item-start
-        h3.card-details-item-title {{_ 'card-start'}}
+        h3
+          i.fa.fa-hourglass-start
+          card-details-item-title {{_ 'card-start'}}
         if getStart
           +cardStartDate
         else
           if canModifyCard
-            a.js-start-date {{_ 'add'}}
+            unless currentUser.isWorker
+              a.js-start-date {{_ 'add'}}
 
       .card-details-item.card-details-item-due
-        h3.card-details-item-title {{_ 'card-due'}}
+        h3
+          i.fa.fa-sign-in
+          card-details-item-title {{_ 'card-due'}}
         if getDue
           +cardDueDate
         else
           if canModifyCard
-            a.js-due-date {{_ 'add'}}
+            unless currentUser.isWorker
+              a.js-due-date {{_ 'add'}}
 
       .card-details-item.card-details-item-end
-        h3.card-details-item-title {{_ 'card-end'}}
+        h3
+          i.fa.fa-hourglass-end
+          card-details-item-title {{_ 'card-end'}}
         if getEnd
           +cardEndDate
         else
           if canModifyCard
-            a.js-end-date {{_ 'add'}}
+            unless currentUser.isWorker
+              a.js-end-date {{_ 'add'}}
 
     .card-details-items
       .card-details-item.card-details-item-members
-        h3.card-details-item-title {{_ 'members'}}
+        h3
+          i.fa.fa-users
+          card-details-item-title {{_ 'members'}}
         each getMembers
           +userAvatar(userId=this cardId=../_id)
           | {{! XXX Hack to hide syntaxic coloration /// }}
         if canModifyCard
-          a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
-            i.fa.fa-plus
+          unless currentUser.isWorker
+            a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
+              i.fa.fa-plus
 
       .card-details-item.card-details-item-assignees
-        h3.card-details-item-title {{_ 'assignee'}}
+        h3
+          i.fa.fa-user
+          card-details-item-title {{_ 'assignee'}}
         each getAssignees
           +userAvatarAssignee(userId=this cardId=../_id)
           | {{! XXX Hack to hide syntaxic coloration /// }}
@@ -89,15 +106,18 @@ template(name="cardDetails")
               i.fa.fa-plus
 
       .card-details-item.card-details-item-labels
-        h3.card-details-item-title {{_ 'labels'}}
+        h3
+          i.fa.fa-tags
+          card-details-item-title {{_ 'labels'}}
         a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
           each labels
             span.card-label(class="card-label-{{color}}" title=name)
               +viewer
                 = name
         if canModifyCard
-          a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
-            i.fa.fa-plus
+          unless currentUser.isWorker
+            a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
+              i.fa.fa-plus
 
     .card-details-items
       each customFieldsWD
@@ -118,26 +138,29 @@ template(name="cardDetails")
 
     //- XXX We should use "editable" to avoid repetiting ourselves
     if canModifyCard
-      h3.card-details-item-title {{_ 'description'}}
-      +inlinedCardDescription(classNames="card-description js-card-description")
-        +editor(autofocus=true)
-          | {{getUnsavedValue 'cardDescription' _id getDescription}}
-        .edit-controls.clearfix
-          button.primary(type="submit") {{_ 'save'}}
-          a.fa.fa-times-thin.js-close-inlined-form
-      else
-        a.js-open-inlined-form
-          if getDescription
-            +viewer
-              = getDescription
-          else
-            | {{_ 'edit'}}
-        if (hasUnsavedValue 'cardDescription' _id)
-          p.quiet
-            | {{_ 'unsaved-description'}}
-            a.js-open-inlined-form {{_ 'view-it'}}
-            = ' - '
-            a.js-close-inlined-form {{_ 'discard'}}
+      unless currentUser.isWorker
+        h3
+          i.fa.fa-align-left
+          card-details-item-title {{_ 'description'}}
+        +inlinedCardDescription(classNames="card-description js-card-description")
+          +editor(autofocus=true)
+            | {{getUnsavedValue 'cardDescription' _id getDescription}}
+          .edit-controls.clearfix
+            button.primary(type="submit") {{_ 'save'}}
+            a.fa.fa-times-thin.js-close-inlined-form
+        else
+          a.js-open-inlined-form
+            if getDescription
+              +viewer
+                = getDescription
+            else
+              | {{_ 'edit'}}
+          if (hasUnsavedValue 'cardDescription' _id)
+            p.quiet
+              | {{_ 'unsaved-description'}}
+              a.js-open-inlined-form {{_ 'view-it'}}
+              = ' - '
+              a.js-close-inlined-form {{_ 'discard'}}
     else if getDescription
       h3.card-details-item-title {{_ 'description'}}
       +viewer
@@ -145,33 +168,39 @@ template(name="cardDetails")
 
     .card-details-items
       .card-details-item.card-details-item-name
-        h3.card-details-item-title {{_ 'requested-by'}}
+        h3
+          i.fa.fa-shopping-cart
+          card-details-item-title {{_ 'requested-by'}}
         if canModifyCard
-          +inlinedForm(classNames="js-card-details-requester")
-            +editCardRequesterForm
-          else
-            a.js-open-inlined-form
-              if getRequestedBy
-                +viewer
-                  = getRequestedBy
-              else
-                | {{_ 'add'}}
+          unless currentUser.isWorker
+            +inlinedForm(classNames="js-card-details-requester")
+              +editCardRequesterForm
+            else
+              a.js-open-inlined-form
+                if getRequestedBy
+                  +viewer
+                    = getRequestedBy
+                else
+                  | {{_ 'add'}}
         else if getRequestedBy
           +viewer
             = getRequestedBy
 
       .card-details-item.card-details-item-name
-        h3.card-details-item-title {{_ 'assigned-by'}}
+        h3
+          i.fa.fa-user-plus
+          card-details-item-title {{_ 'assigned-by'}}
         if canModifyCard
-          +inlinedForm(classNames="js-card-details-assigner")
-            +editCardAssignerForm
-          else
-            a.js-open-inlined-form
-              if getAssignedBy
-                +viewer
-                  = getAssignedBy
-              else
-                | {{_ 'add'}}
+          unless currentUser.isWorker
+            +inlinedForm(classNames="js-card-details-assigner")
+              +editCardAssignerForm
+            else
+              a.js-open-inlined-form
+                if getAssignedBy
+                  +viewer
+                    = getAssignedBy
+                else
+                  | {{_ 'add'}}
         else if getRequestedBy
           +viewer
             = getAssignedBy
@@ -193,7 +222,9 @@ template(name="cardDetails")
     hr
     unless currentUser.isNoComments
       .activity-title
-        h3 {{ _ 'activity'}}
+        h3
+          i.fa.fa-history
+          | {{ _ 'activity'}}
         if currentUser.isBoardMember
           .material-toggle-switch
             span.toggle-switch-title {{_ 'hide-system-messages'}}
@@ -235,32 +266,79 @@ template(name="editCardAssignerForm")
 
 template(name="cardDetailsActionsPopup")
   ul.pop-over-list
-    li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
+    li
+      a.js-toggle-watch-card
+        if isWatching
+          i.fa.fa-eye
+          |  {{_ 'unwatch'}}
+        else
+          i.fa.fa-eye-slash
+          |  {{_ 'watch'}}
   if canModifyCard
-    hr
-    ul.pop-over-list
-      //li: a.js-members {{_ 'card-edit-members'}}
-      //li: a.js-labels {{_ 'card-edit-labels'}}
-      //li: a.js-attachments {{_ 'card-edit-attachments'}}
-      li: a.js-custom-fields {{_ 'card-edit-custom-fields'}}
-      //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
-      //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
-      //li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
-      //li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
-      li: a.js-spent-time {{_ 'editCardSpentTimePopup-title'}}
-      li: a.js-set-card-color {{_ 'setCardColorPopup-title'}}
-    hr
-    ul.pop-over-list
-      li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
-      li: a.js-move-card-to-bottom {{_ 'moveCardToBottom-title'}}
-    hr
+    unless currentUser.isWorker
+      hr
+      ul.pop-over-list
+        //li: a.js-members {{_ 'card-edit-members'}}
+        //li: a.js-labels {{_ 'card-edit-labels'}}
+        //li: a.js-attachments {{_ 'card-edit-attachments'}}
+        li
+          a.js-custom-fields
+            i.fa.fa-list-alt
+            | {{_ 'card-edit-custom-fields'}}
+        //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
+        //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
+        //li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
+        //li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
+        li
+          a.js-spent-time
+            i.fa.fa-clock-o
+            | {{_ 'editCardSpentTimePopup-title'}}
+        li
+          a.js-set-card-color
+            i.fa.fa-paint-brush
+            | {{_ 'setCardColorPopup-title'}}
+      hr
     ul.pop-over-list
-      li: a.js-move-card {{_ 'moveCardPopup-title'}}
-      li: a.js-copy-card {{_ 'copyCardPopup-title'}}
-      li: a.js-copy-checklist-cards {{_ 'copyChecklistToManyCardsPopup-title'}}
+      li
+        a.js-move-card-to-top
+          i.fa.fa-arrow-up
+          | {{_ 'moveCardToTop-title'}}
+      li
+        a.js-move-card-to-bottom
+          i.fa.fa-arrow-down
+          | {{_ 'moveCardToBottom-title'}}
+    unless currentUser.isWorker
+      hr
+      ul.pop-over-list
+        li
+          a.js-move-card
+            i.fa.fa-arrow-right
+            | {{_ 'moveCardPopup-title'}}
+        li
+          a.js-copy-card
+            i.fa.fa-copy
+            | {{_ 'copyCardPopup-title'}}
+      hr
+      ul.pop-over-list
+        li
+          a.js-copy-checklist-cards
+            i.fa.fa-list
+            i.fa.fa-copy
+            | {{_ 'copyChecklistToManyCardsPopup-title'}}
       unless archived
-        li: a.js-archive {{_ 'archive-card'}}
-      li: a.js-more {{_ 'cardMorePopup-title'}}
+        hr
+        ul.pop-over-list
+          li
+            a.js-archive
+              i.fa.fa-arrow-right
+              i.fa.fa-archive
+              | {{_ 'archive-card'}}
+      hr
+      ul.pop-over-list
+        li
+          a.js-more
+            i.fa.fa-link
+            | {{_ 'cardMorePopup-title'}}
 
 template(name="moveCardPopup")
   +boardsAndLists
@@ -312,16 +390,27 @@ template(name="cardMembersPopup")
             i.fa.fa-check
 
 template(name="cardAssigneesPopup")
-  ul.pop-over-list.js-card-assignee-list
-    each board.activeMembers
-      li.item(class="{{#if isCardAssignee}}active{{/if}}")
-        a.name.js-select-assignee(href="#")
-          +userAvatar(userId=user._id)
-          span.full-name
-            = user.profile.fullname
-            | (<span class="username">{{ user.username }}</span>)
-          if isCardAssignee
-            i.fa.fa-check
+  unless currentUser.isWorker
+    ul.pop-over-list.js-card-assignee-list
+      each board.activeMembers
+        li.item(class="{{#if isCardAssignee}}active{{/if}}")
+          a.name.js-select-assignee(href="#")
+            +userAvatar(userId=user._id)
+            span.full-name
+              = user.profile.fullname
+              | (<span class="username">{{ user.username }}</span>)
+            if isCardAssignee
+              i.fa.fa-check
+  if currentUser.isWorker
+    ul.pop-over-list.js-card-assignee-list
+        li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
+          a.name.js-select-assigneeWorker(href="#")
+            +userAvatar(userId=currentUser._id)
+            span.full-name
+              = currentUser.profile.fullname
+              | (<span class="username">{{ currentUser.username }}</span>)
+            if currentUser.isCardAssignee
+              i.fa.fa-check
 
 template(name="userAvatarAssignee")
   a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
@@ -349,11 +438,13 @@ template(name="cardAssigneePopup")
         p.quiet @{{ user.username }}
     ul.pop-over-list
       if currentUser.isNotCommentOnly
+        unless currentUser.isWorker
           li: a.js-remove-assignee {{_ 'remove-member-from-card'}}
 
-      if $eq currentUser._id user._id
-        with currentUser
-          li: a.js-edit-profile {{_ 'edit-profile'}}
+      unless currentUser.isWorker
+        if $eq currentUser._id user._id
+          with currentUser
+            li: a.js-edit-profile {{_ 'edit-profile'}}
 
 template(name="userAvatarAssigneeInitials")
   svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15")

+ 44 - 2
client/components/cards/cardDetails.js

@@ -26,6 +26,7 @@ BlazeComponent.extendComponent({
 
   onCreated() {
     this.currentBoard = Boards.findOne(Session.get('currentBoard'));
+    this.currentUser = Meteor.user();
     this.isLoaded = new ReactiveVar(false);
     const boardBody = this.parentComponent().parentComponent();
     //in Miniview parent is Board, not BoardBody.
@@ -55,6 +56,15 @@ BlazeComponent.extendComponent({
     );
   },
 
+  canModifyCardWorker() {
+    return (
+      Meteor.user() &&
+      Meteor.user().isBoardMember() &&
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
+    );
+  },
+
   scrollParentContainer() {
     const cardPanelWidth = 510;
     const bodyBoardComponent = this.parentComponent().parentComponent();
@@ -322,7 +332,12 @@ BlazeComponent.extendComponent({
         'click .js-assignee': Popup.open('cardAssignee'),
         'click .js-add-assignees': Popup.open('cardAssignees'),
         'click .js-add-labels': Popup.open('cardLabels'),
-        'click .js-received-date': Popup.open('editCardReceivedDate'),
+        'click .js-received-date'(event) {
+          event.preventDefault();
+          if (!Meteor.user().isWorker) {
+            Popup.open('editCardReceivedDate');
+          }
+        },
         'click .js-start-date': Popup.open('editCardStartDate'),
         'click .js-due-date': Popup.open('editCardDueDate'),
         'click .js-end-date': Popup.open('editCardEndDate'),
@@ -383,6 +398,13 @@ Template.cardDetails.helpers({
     return user && user.isBoardAdmin() ? 'admin' : 'normal';
   },
 
+  isWorker() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    return (
+      !currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
+    );
+  },
+
   presenceStatusClassName() {
     const user = Users.findOne(this.userId);
     const userPresence = presences.findOne({ userId: this.userId });
@@ -459,6 +481,15 @@ Template.cardDetailsActionsPopup.helpers({
       !Meteor.user().isCommentOnly()
     );
   },
+
+  canModifyCardWorker() {
+    return (
+      Meteor.user() &&
+      Meteor.user().isBoardMember() &&
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
+    );
+  },
 });
 
 Template.cardDetailsActionsPopup.events({
@@ -467,7 +498,12 @@ Template.cardDetailsActionsPopup.events({
   'click .js-labels': Popup.open('cardLabels'),
   'click .js-attachments': Popup.open('cardAttachments'),
   'click .js-custom-fields': Popup.open('cardCustomFields'),
-  'click .js-received-date': Popup.open('editCardReceivedDate'),
+  'click .js-received-date'(event) {
+    event.preventDefault();
+    if (!Meteor.user().isWorker) {
+      Popup.open('editCardReceivedDate');
+    }
+  },
   'click .js-start-date': Popup.open('editCardStartDate'),
   'click .js-due-date': Popup.open('editCardDueDate'),
   'click .js-end-date': Popup.open('editCardEndDate'),
@@ -879,6 +915,12 @@ Template.cardAssigneesPopup.events({
     card.toggleAssignee(assigneeId);
     event.preventDefault();
   },
+  'click .js-select-assigneeWorker'(event) {
+    const card = Cards.findOne(Session.get('currentCard'));
+    const assigneeId = currentUser._id;
+    card.toggleAssignee(assigneeId);
+    event.preventDefault();
+  },
 });
 
 Template.cardAssigneesPopup.helpers({

+ 3 - 1
client/components/cards/checklists.jade

@@ -1,5 +1,7 @@
 template(name="checklists")
-  h3 {{_ 'checklists'}}
+  h3
+    i.fa.fa-check
+    | {{_ 'checklists'}}
   if toggleDeleteDialog.get
     .board-overlay#card-details-overlay
     +checklistDeleteDialog(checklist = checklistToDelete)

+ 6 - 3
client/components/cards/checklists.js

@@ -67,7 +67,8 @@ BlazeComponent.extendComponent({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 }).register('checklistDetail');
@@ -120,7 +121,8 @@ BlazeComponent.extendComponent({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 
@@ -228,7 +230,8 @@ Template.checklistItemDetail.helpers({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 });

+ 6 - 10
client/components/cards/minicard.js

@@ -36,24 +36,20 @@ Template.minicard.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
   },
   hiddenMinicardLabelText() {
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).hiddenMinicardLabelText;
+    } else if (cookies.has('hiddenMinicardLabelText')) {
+      return true;
     } else {
-      if (cookies.has('hiddenMinicardLabelText')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
   },
 });

+ 3 - 1
client/components/cards/subtasks.jade

@@ -1,5 +1,7 @@
 template(name="subtasks")
-  h3 {{_ 'subtasks'}}
+  h3
+    i.fa.fa-sitemap
+    | {{_ 'subtasks'}}
   if toggleDeleteDialog.get
     .board-overlay#card-details-overlay
     +subtaskDeleteDialog(subtask = subtaskToDelete)

+ 6 - 3
client/components/cards/subtasks.js

@@ -3,7 +3,8 @@ BlazeComponent.extendComponent({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 }).register('subtaskDetail');
@@ -55,7 +56,8 @@ BlazeComponent.extendComponent({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 
@@ -154,7 +156,8 @@ Template.subtaskItemDetail.helpers({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 });

+ 3 - 5
client/components/lists/list.js

@@ -176,12 +176,10 @@ Template.list.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
   },
 });

+ 2 - 1
client/components/lists/listBody.js

@@ -189,7 +189,8 @@ BlazeComponent.extendComponent({
       !this.reachedWipLimit() &&
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 

+ 35 - 12
client/components/lists/listHeader.jade

@@ -56,25 +56,47 @@ template(name="editListTitleForm")
 
 template(name="listActionPopup")
   ul.pop-over-list
-    li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
+    li
+      a.js-toggle-watch-list
+        if isWatching
+          i.fa.fa-eye
+          |  {{_ 'unwatch'}}
+        else
+          i.fa.fa-eye-slash
+          |  {{_ 'watch'}}
   unless currentUser.isCommentOnly
-    hr
-    ul.pop-over-list
-      li: a.js-set-color-list {{_ 'set-color-list'}}
-    hr
+    unless currentUser.isWorker
+      ul.pop-over-list
+        li
+          a.js-set-color-list
+            i.fa.fa-paint-brush
+            | {{_ 'set-color-list'}}
     ul.pop-over-list
       if cards.count
-        li: a.js-select-cards {{_ 'list-select-cards'}}
-        hr
+        li
+          a.js-select-cards
+            i.fa.fa-check-square
+            | {{_ 'list-select-cards'}}
     if currentUser.isBoardAdmin
       ul.pop-over-list
-        li: a.js-set-wip-limit {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
+        li
+          a.js-set-wip-limit
+            i.fa.fa-ban
+            | {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
+    unless currentUser.isWorker
       hr
-    ul.pop-over-list
-      li: a.js-close-list {{_ 'archive-list'}}
+      ul.pop-over-list
+        li
+          a.js-close-list
+            i.fa.fa-arrow-right
+            i.fa.fa-archive
+            | {{_ 'archive-list'}}
     hr
     ul.pop-over-list
-      li: a.js-more {{_ 'listMorePopup-title'}}
+      li
+        a.js-more
+          i.fa.fa-link
+          | {{_ 'listMorePopup-title'}}
 
 template(name="boardLists")
   ul.pop-over-list
@@ -94,7 +116,8 @@ template(name="listMorePopup")
       input.inline-input(type="text" readonly value="{{ rootUrl }}")
     | {{_ 'added'}}
     span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
-    a.js-delete {{_ 'delete'}}
+    unless currentUser.isWorker
+      a.js-delete {{_ 'delete'}}
 
 template(name="listDeletePopup")
   p {{_ "list-delete-pop"}}

+ 7 - 8
client/components/lists/listHeader.js

@@ -9,9 +9,10 @@ BlazeComponent.extendComponent({
   canSeeAddCard() {
     const list = Template.currentData();
     return (
-      !list.getWipLimit('enabled') ||
-      list.getWipLimit('soft') ||
-      !this.reachedWipLimit()
+      (!list.getWipLimit('enabled') ||
+        list.getWipLimit('soft') ||
+        !this.reachedWipLimit()) &&
+      !Meteor.user().isWorker()
     );
   },
 
@@ -109,12 +110,10 @@ Template.listHeader.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
   },
 });

+ 6 - 2
client/components/settings/informationBody.jade

@@ -4,12 +4,16 @@ template(name='information')
       | {{_ 'error-notAuthorized'}}
     else
       .content-title
-        span {{_ 'info'}}
+        span
+          i.fa.fa-info-circle
+          | {{_ 'info'}}
       .content-body
         .side-menu
           ul
             li.active
-              a.js-setting-menu(data-id="information-display") {{_ 'info'}}
+              a.js-setting-menu(data-id="information-display")
+                i.fa.fa-info-circle
+                | {{_ 'info'}}
         .main-body
           +statistics
 

+ 10 - 3
client/components/settings/peopleBody.jade

@@ -5,16 +5,22 @@ template(name="people")
     else
       .content-title.ext-box
         .ext-box-left
-          span {{_ 'people'}}
+          span
+            i.fa.fa-users
+            | {{_ 'people'}}
           input#searchInput(placeholder="{{_ 'search'}}")
-          button#searchButton {{_ 'search'}}
+          button#searchButton
+            i.fa.fa-search
+            | {{_ 'search'}}
         .ext-box-right
           span {{_ 'people-number'}} #{peopleNumber}
       .content-body
         .side-menu
           ul
             li.active
-              a.js-setting-menu(data-id="people-setting") {{_ 'people'}}
+              a.js-setting-menu(data-id="people-setting")
+                i.fa.fa-users
+                | {{_ 'people'}}
         .main-body
           if loading.get
             +spinner
@@ -90,6 +96,7 @@ template(name="peopleRow")
       td {{_ userData.authenticationMethod }}
     td
       a.edit-user
+        i.fa.fa-edit
         | {{_ 'edit'}}
 
 template(name="editUserPopup")

+ 1 - 1
client/components/settings/peopleBody.styl

@@ -33,7 +33,7 @@ table
     padding: 0;
 
   button
-    min-width: 60px;
+    min-width: 90px;
 
 .content-wrapper
   margin-top: 10px

+ 19 - 6
client/components/settings/settingBody.jade

@@ -4,22 +4,35 @@ template(name="setting")
       | {{_ 'error-notAuthorized'}}
     else
       .content-title
+        i.fa.fa-cog
         span {{_ 'settings'}}
       .content-body
         .side-menu
           ul
             li.active
-              a.js-setting-menu(data-id="registration-setting") {{_ 'registration'}}
+              a.js-setting-menu(data-id="registration-setting")
+                i.fa.fa-sign-in
+                | {{_ 'registration'}}
             li
-              a.js-setting-menu(data-id="email-setting") {{_ 'email'}}
+              a.js-setting-menu(data-id="email-setting")
+                i.fa.fa-envelope
+                | {{_ 'email'}}
             li
-              a.js-setting-menu(data-id="account-setting") {{_ 'accounts'}}
+              a.js-setting-menu(data-id="account-setting")
+                i.fa.fa-users
+                | {{_ 'accounts'}}
             li
-              a.js-setting-menu(data-id="announcement-setting") {{_ 'admin-announcement'}}
+              a.js-setting-menu(data-id="announcement-setting")
+                i.fa.fa-bullhorn
+                | {{_ 'admin-announcement'}}
             li
-              a.js-setting-menu(data-id="layout-setting") {{_ 'layout'}}
+              a.js-setting-menu(data-id="layout-setting")
+                i.fa.fa-object-group
+                | {{_ 'layout'}}
             li
-              a.js-setting-menu(data-id="webhook-setting") {{_ 'global-webhook'}}
+              a.js-setting-menu(data-id="webhook-setting")
+                i.fa.fa-globe
+                | {{_ 'global-webhook'}}
         .main-body
           if loading.get
             +spinner

+ 4 - 1
client/components/settings/settingBody.styl

@@ -41,15 +41,18 @@
           &:hover
             background #fff
             box-shadow 0 1px 2px rgba(0,0,0,0.15);
+
           a
             @extends .flex
             padding: 1rem 0 1rem 1rem
             width: 100% - 5rem
 
-
             span
               font-size: 13px
 
+            i
+              margin-right: 20px
+
     .main-body
       padding: 0.1em 1em
       -webkit-user-select: text // Safari 3.1+

+ 78 - 27
client/components/sidebar/sidebar.jade

@@ -37,11 +37,12 @@ template(name='homeSidebar')
 template(name="membersWidget")
   .board-widget.board-widget-members
     h3
-      i.fa.fa-user
+      i.fa.fa-users
       | {{_ 'members'}}
       unless currentUser.isCommentOnly
-        a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
-          i.board-header-btn-icon.fa.fa-cog
+        unless currentUser.isWorker
+          a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
+            i.board-header-btn-icon.fa.fa-cog
 
     .board-widget-content
       each currentBoard.activeMembers
@@ -130,7 +131,9 @@ template(name="chooseBoardSource")
 
 template(name="archiveBoardPopup")
   p {{_ 'close-board-pop'}}
-  button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
+  button.js-confirm.negate.full(type="submit")
+    i.fa.fa-archive
+    | {{_ 'archive'}}
 
 template(name="outgoingWebhooksPopup")
   each integrations
@@ -162,38 +165,80 @@ template(name="outgoingWebhooksPopup")
 
 template(name="boardMenuPopup")
   ul.pop-over-list
-    li: a.js-custom-fields {{_ 'custom-fields'}}
-    li: a.js-open-archives {{_ 'archived-items'}}
+    if isNotWorker
+      li: a.js-custom-fields {{_ 'custom-fields'}}
+      li
+        a.js-open-archives
+          i.fa.fa-archive
+          | {{_ 'archived-items'}}
     if currentUser.isBoardAdmin
-      li: a.js-change-board-color {{_ 'board-change-color'}}
+      li
+        a.js-change-board-color
+          i.fa.fa-paint-brush
+          | {{_ 'board-change-color'}}
+
     //-
       XXX Language should be handled by sandstorm, but for now display a
       language selection link in the board menu. This link is normally present
       in the header bar that is not displayed on sandstorm.
     if isSandstorm
-      li: a.js-change-language {{_ 'language'}}
+      li
+        a.js-change-language
+          i.fa.fa-flag
+          | {{_ 'language'}}
   unless isSandstorm
     if currentUser.isBoardAdmin
       hr
       ul.pop-over-list
-        li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
-        unless currentBoard.isTemplatesBoard
-          li: a.js-archive-board {{_ 'archive-board'}}
-        li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
-      hr
-      ul.pop-over-list
-        li: a.js-subtask-settings {{_ 'subtask-settings'}}
+        li
+          a(href="{{exportUrl}}", download="{{exportFilename}}")
+            i.fa.fa-share-alt
+            | {{_ 'export-board'}}
+        li
+          a.js-outgoing-webhooks
+            i.fa.fa-globe
+            | {{_ 'outgoing-webhooks'}}
+        li
+          a.js-subtask-settings
+            i.fa.fa-sitemap
+            | {{_ 'subtask-settings'}}
+      unless currentBoard.isTemplatesBoard
+        hr
+        ul.pop-over-list
+          li
+            a.js-archive-board
+              i.fa.fa-arrow-right
+              i.fa.fa-archive
+              | {{_ 'archive-board'}}
 
   if isSandstorm
     hr
     ul.pop-over-list
-      li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
-      li: a.js-import-board {{_ 'import-board-c'}}
-      li: a.js-archive-board {{_ 'archive-board'}}
-      li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
+      li
+        a(href="{{exportUrl}}", download="{{exportFilename}}")
+          i.fa.fa-share-alt
+          i.fa.fa-sign-out
+          | {{_ 'export-board'}}
+      li
+        a.js-import-board
+          i.fa.fa-share-alt
+          i.fa.fa-sign-in
+          | {{_ 'import-board-c'}}
+      li
+        a.js-archive-board
+          i.fa.fa-arrow-right
+          i.fa.fa-archive
+          | {{_ 'archive-board'}}
+      li
+        a.js-outgoing-webhooks
+          i.fa.fa-globe
+          | {{_ 'outgoing-webhooks'}}
     hr
     ul.pop-over-list
-      li: a.js-subtask-settings {{_ 'subtask-settings'}}
+      li
+        a.js-subtask-settings
+          i.fa.fa-sitemap
+          | {{_ 'subtask-settings'}}
 
 template(name="labelsWidget")
   .board-widget.board-widget-labels
@@ -203,7 +248,7 @@ template(name="labelsWidget")
     .board-widget-content
       each currentBoard.labels
           a.card-label(class="card-label-{{color}}"
-            class="{{#if currentUser.isNotCommentOnly}}js-label{{/if}}")
+            class="{{#if currentUser.isNotCommentOnly}}{{#if currentUser.isNotWorker}}js-label{{/if}}{{/if}}")
             span.card-label-name
               +viewer
                 = name
@@ -232,12 +277,12 @@ template(name="memberPopup")
           a.js-change-role
             | {{_ 'change-permissions'}}
             span.quiet (#{memberType})
-      li
-        if $eq currentUser._id userId
-          a.js-leave-member {{_ 'leave-board'}}
-        else if currentUser.isBoardAdmin
-          a.js-remove-member {{_ 'remove-from-board'}}
-
+      unless currentUser.isWorker
+        li
+          if $eq currentUser._id userId
+            a.js-leave-member {{_ 'leave-board'}}
+          else if currentUser.isBoardAdmin
+            a.js-remove-member {{_ 'remove-from-board'}}
 
 template(name="removeMemberPopup")
   p {{_ 'remove-member-pop' name=user.profile.fullname username=user.username boardTitle=board.title}}
@@ -301,6 +346,12 @@ template(name="changePermissionsPopup")
         if isCommentOnly
           i.fa.fa-check
         span.sub-name {{_ 'comment-only-desc'}}
+    li
+      a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}")
+        | {{_ 'worker'}}
+        if isWorker
+          i.fa.fa-check
+        span.sub-name {{_ 'worker-desc'}}
   if isLastAdmin
     hr
     p.quiet.bottom {{_ 'last-admin-desc'}}

+ 29 - 12
client/components/sidebar/sidebar.js

@@ -112,12 +112,10 @@ BlazeComponent.extendComponent({
           currentUser = Meteor.user();
           if (currentUser) {
             Meteor.call('toggleMinicardLabelText');
+          } else if (cookies.has('hiddenMinicardLabelText')) {
+            cookies.remove('hiddenMinicardLabelText');
           } else {
-            if (cookies.has('hiddenMinicardLabelText')) {
-              cookies.remove('hiddenMinicardLabelText');
-            } else {
-              cookies.set('hiddenMinicardLabelText', 'true');
-            }
+            cookies.set('hiddenMinicardLabelText', 'true');
           }
         },
         'click .js-shortcuts'() {
@@ -135,12 +133,10 @@ Template.homeSidebar.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).hiddenMinicardLabelText;
+    } else if (cookies.has('hiddenMinicardLabelText')) {
+      return true;
     } else {
-      if (cookies.has('hiddenMinicardLabelText')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
   },
 });
@@ -165,10 +161,13 @@ Template.memberPopup.helpers({
       const currentBoard = Boards.findOne(Session.get('currentBoard'));
       const commentOnly = currentBoard.hasCommentOnly(this.userId);
       const noComments = currentBoard.hasNoComments(this.userId);
+      const worker = currentBoard.hasWorker(this.userId);
       if (commentOnly) {
         return TAPi18n.__('comment-only').toLowerCase();
       } else if (noComments) {
         return TAPi18n.__('no-comments').toLowerCase();
+      } else if (worker) {
+        return TAPi18n.__('worker').toLowerCase();
       } else {
         return TAPi18n.__(type).toLowerCase();
       }
@@ -271,6 +270,14 @@ Template.membersWidget.helpers({
     const user = Meteor.user();
     return user && user.isInvitedTo(Session.get('currentBoard'));
   },
+  isWorker() {
+    const user = Meteor.user();
+    if (user) {
+      return Meteor.call(Boards.hasWorker(user.memberId));
+    } else {
+      return false;
+    }
+  },
 });
 
 Template.membersWidget.events({
@@ -648,7 +655,7 @@ BlazeComponent.extendComponent({
 }).register('addMemberPopup');
 
 Template.changePermissionsPopup.events({
-  'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only'(
+  'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only, click .js-set-worker'(
     event,
   ) {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
@@ -658,11 +665,13 @@ Template.changePermissionsPopup.events({
       'js-set-comment-only',
     );
     const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
+    const isWorker = $(event.currentTarget).hasClass('js-set-worker');
     currentBoard.setMemberPermission(
       memberId,
       isAdmin,
       isNoComments,
       isCommentOnly,
+      isWorker,
     );
     Popup.back(1);
   },
@@ -679,7 +688,8 @@ Template.changePermissionsPopup.helpers({
     return (
       !currentBoard.hasAdmin(this.userId) &&
       !currentBoard.hasNoComments(this.userId) &&
-      !currentBoard.hasCommentOnly(this.userId)
+      !currentBoard.hasCommentOnly(this.userId) &&
+      !currentBoard.hasWorker(this.userId)
     );
   },
 
@@ -699,6 +709,13 @@ Template.changePermissionsPopup.helpers({
     );
   },
 
+  isWorker() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    return (
+      !currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
+    );
+  },
+
   isLastAdmin() {
     const currentBoard = Boards.findOne(Session.get('currentBoard'));
     return (

+ 30 - 24
client/components/sidebar/sidebarArchives.jade

@@ -2,54 +2,60 @@ template(name="archivesSidebar")
   if isArchiveReady.get
     +basicTabs(tabs=tabs)
       +tabContent(slug="cards")
-        p.quiet
-          a.js-restore-all-cards {{_ 'restore-all'}}
-          | -
-          a.js-delete-all-cards {{_ 'delete-all'}}
+        unless isWorker
+          p.quiet
+            a.js-restore-all-cards {{_ 'restore-all'}}
+            | -
+            a.js-delete-all-cards {{_ 'delete-all'}}
         each archivedCards
           .minicard-wrapper.js-minicard
             +minicard(this)
           if currentUser.isBoardMember
-            p.quiet
-              a.js-restore-card {{_ 'restore'}}
-              | -
-              a.js-delete-card {{_ 'delete'}}
+            unless isWorker
+              p.quiet
+                a.js-restore-card {{_ 'restore'}}
+                | -
+                a.js-delete-card {{_ 'delete'}}
             if cardIsInArchivedList
               p.quiet.small ({{_ 'warn-list-archived'}})
         else
           p.no-items-message {{_ 'no-archived-cards'}}
 
       +tabContent(slug="lists")
-        p.quiet
-          a.js-restore-all-lists {{_ 'restore-all'}}
-          | -
-          a.js-delete-all-lists {{_ 'delete-all'}}
+        unless isWorker
+          p.quiet
+            a.js-restore-all-lists {{_ 'restore-all'}}
+            | -
+            a.js-delete-all-lists {{_ 'delete-all'}}
         ul.archived-lists
           each archivedLists
             li.archived-lists-item
               = title
               if currentUser.isBoardMember
-                p.quiet
-                  a.js-restore-list {{_ 'restore'}}
-                  | -
-                  a.js-delete-list {{_ 'delete'}}
+                unless isWorker
+                  p.quiet
+                    a.js-restore-list {{_ 'restore'}}
+                    | -
+                    a.js-delete-list {{_ 'delete'}}
           else
             li.no-items-message {{_ 'no-archived-lists'}}
 
       +tabContent(slug="swimlanes")
-        p.quiet
-          a.js-restore-all-swimlanes {{_ 'restore-all'}}
-          | -
-          a.js-delete-all-swimlanes {{_ 'delete-all'}}
+        unless isWorker
+          p.quiet
+            a.js-restore-all-swimlanes {{_ 'restore-all'}}
+            | -
+            a.js-delete-all-swimlanes {{_ 'delete-all'}}
         ul.archived-lists
           each archivedSwimlanes
             li.archived-lists-item
               = title
               if currentUser.isBoardMember
-                p.quiet
-                  a.js-restore-swimlane {{_ 'restore'}}
-                  | -
-                  a.js-delete-swimlane {{_ 'delete'}}
+                unless isWorker
+                  p.quiet
+                    a.js-restore-swimlane {{_ 'restore'}}
+                    | -
+                    a.js-delete-swimlane {{_ 'delete'}}
           else
             li.no-items-message {{_ 'no-archived-swimlanes'}}
   else

+ 9 - 0
client/components/sidebar/sidebarArchives.js

@@ -139,3 +139,12 @@ BlazeComponent.extendComponent({
     ];
   },
 }).register('archivesSidebar');
+
+Template.archivesSidebar.helpers({
+  isWorker() {
+    const currentBoard = Boards.findOne(Session.get('currentBoard'));
+    return (
+      !currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
+    );
+  },
+});

+ 8 - 7
client/components/sidebar/sidebarFilters.jade

@@ -117,13 +117,14 @@ template(name="multiselectionSidebar")
               i.fa.fa-check
             else if someSelectedElementHave 'member' _id
               i.fa.fa-ellipsis-h
-  hr
-  a.sidebar-btn.js-move-selection
-    i.fa.fa-share
-    span {{_ 'move-selection'}}
-  a.sidebar-btn.js-archive-selection
-    i.fa.fa-archive
-    span {{_ 'archive-selection'}}
+  unless currentUser.isWorker
+    hr
+    a.sidebar-btn.js-move-selection
+      i.fa.fa-share
+      span {{_ 'move-selection'}}
+    a.sidebar-btn.js-archive-selection
+      i.fa.fa-archive
+      span {{_ 'archive-selection'}}
 
 template(name="disambiguateMultiLabelPopup")
   p {{_ 'what-to-do'}}

+ 3 - 5
client/components/swimlanes/swimlaneHeader.js

@@ -35,12 +35,10 @@ Template.swimlaneHeader.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
   },
 });

+ 17 - 16
client/components/swimlanes/swimlanes.jade

@@ -43,19 +43,20 @@ template(name="listsGroup")
           +addListForm
 
 template(name="addListForm")
-  .list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
-    .list-header-add
-      +inlinedForm(autoclose=false)
-        input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
-          autocomplete="off" autofocus)
-        .edit-controls.clearfix
-          button.primary.confirm(type="submit") {{_ 'save'}}
-          unless currentBoard.isTemplatesBoard
-            unless currentBoard.isTemplateBoard
-              span.quiet
-                | {{_ 'or'}}
-                a.js-list-template {{_ 'template'}}
-      else
-        a.open-list-composer.js-open-inlined-form
-          i.fa.fa-plus
-          | {{_ 'add-list'}}
+  unless currentUser.isWorker
+    .list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
+      .list-header-add
+        +inlinedForm(autoclose=false)
+          input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
+            autocomplete="off" autofocus)
+          .edit-controls.clearfix
+            button.primary.confirm(type="submit") {{_ 'save'}}
+            unless currentBoard.isTemplatesBoard
+              unless currentBoard.isTemplateBoard
+                span.quiet
+                  | {{_ 'or'}}
+                  a.js-list-template {{_ 'template'}}
+        else
+          a.open-list-composer.js-open-inlined-form
+            i.fa.fa-plus
+            | {{_ 'add-list'}}

+ 9 - 15
client/components/swimlanes/swimlanes.js

@@ -104,12 +104,10 @@ function initSortable(boardComponent, $listsDom) {
     if (currentUser) {
       showDesktopDragHandles = (currentUser.profile || {})
         .showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      showDesktopDragHandles = true;
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        showDesktopDragHandles = true;
-      } else {
-        showDesktopDragHandles = false;
-      }
+      showDesktopDragHandles = false;
     }
 
     if (!Utils.isMiniScreen() && showDesktopDragHandles) {
@@ -182,12 +180,10 @@ BlazeComponent.extendComponent({
           if (currentUser) {
             showDesktopDragHandles = (currentUser.profile || {})
               .showDesktopDragHandles;
+          } else if (cookies.has('showDesktopDragHandles')) {
+            showDesktopDragHandles = true;
           } else {
-            if (cookies.has('showDesktopDragHandles')) {
-              showDesktopDragHandles = true;
-            } else {
-              showDesktopDragHandles = false;
-            }
+            showDesktopDragHandles = false;
           }
 
           const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
@@ -276,12 +272,10 @@ Template.swimlane.helpers({
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).showDesktopDragHandles;
+    } else if (cookies.has('showDesktopDragHandles')) {
+      return true;
     } else {
-      if (cookies.has('showDesktopDragHandles')) {
-        return true;
-      } else {
-        return false;
-      }
+      return false;
     }
   },
   canSeeAddList() {

+ 1 - 0
client/components/users/userAvatar.jade

@@ -73,6 +73,7 @@ template(name="cardMemberPopup")
         p.quiet @{{ user.username }}
     ul.pop-over-list
       if currentUser.isNotCommentOnly
+        if currentUser.isNotWorker
           li: a.js-remove-member {{_ 'remove-member-from-card'}}
 
       if $eq currentUser._id user._id

+ 47 - 18
client/components/users/userHeader.jade

@@ -13,21 +13,46 @@ template(name="headerUserBar")
 template(name="memberMenuPopup")
   ul.pop-over-list
     with currentUser
-      li: a.js-edit-profile {{_ 'edit-profile'}}
-      li: a.js-change-settings {{_ 'change-settings'}}
-      li: a.js-change-avatar {{_ 'edit-avatar'}}
+      li
+        a.js-edit-profile
+          i.fa.fa-user
+          | {{_ 'edit-profile'}}
+      li
+        a.js-change-settings
+          i.fa.fa-cog
+          | {{_ 'change-settings'}}
+      li
+        a.js-change-avatar
+          i.fa.fa-picture-o
+          | {{_ 'edit-avatar'}}
       unless isSandstorm
-        li: a.js-change-password {{_ 'changePasswordPopup-title'}}
-        li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
+        li
+          a.js-change-password
+            i.fa.fa-key
+            | {{_ 'changePasswordPopup-title'}}
+        li
+          a.js-change-language
+            i.fa.fa-flag
+            | {{_ 'changeLanguagePopup-title'}}
     if currentUser.isAdmin
-      li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}}
-  hr
-  ul.pop-over-list
-    li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}}
+      li
+        a.js-go-setting(href="{{pathFor 'setting'}}")
+          i.fa.fa-lock
+          | {{_ 'admin-panel'}}
+  unless currentUser.isWorker
+    hr
+    ul.pop-over-list
+      li
+        a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
+          i.fa.fa-clone
+          | {{_ 'templates'}}
   unless isSandstorm
     hr
     ul.pop-over-list
-      li: a.js-logout {{_ 'log-out'}}
+      li
+        a.js-logout
+          i.fa.fa-sign-out
+          | {{_ 'log-out'}}
 
 template(name="editProfilePopup")
   form
@@ -75,21 +100,25 @@ template(name="changeSettingsPopup")
   ul.pop-over-list
     li
       a.js-toggle-system-messages
+        i.fa.fa-comments-o
         | {{_ 'hide-system-messages'}}
         if hiddenSystemMessages
           i.fa.fa-check
     li
       a.js-toggle-desktop-drag-handles
+        i.fa.fa-arrows
         | {{_ 'show-desktop-drag-handles'}}
         if showDesktopDragHandles
           i.fa.fa-check
-    li
-      label.bold
-        | {{_ 'show-cards-minimum-count'}}
-      input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
-      input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
-
+    unless currentUser.isWorker
+      li
+        label.bold
+          i.fa.fa-sort-numeric-asc
+          | {{_ 'show-cards-minimum-count'}}
+        input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
+        input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
 
 template(name="userDeletePopup")
-  p {{_ 'delete-user-confirm-popup'}}
-  button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+  unless currentUser.isWorker
+    p {{_ 'delete-user-confirm-popup'}}
+    button.js-confirm.negate.full(type="submit") {{_ 'delete'}}

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

@@ -45,13 +45,31 @@ Template.memberMenuPopup.events({
 
 Template.editProfilePopup.helpers({
   allowEmailChange() {
-    return AccountSettings.findOne('accounts-allowEmailChange').booleanValue;
+    Meteor.call('AccountSettings.allowEmailChange', (_, result) => {
+      if (result) {
+        return true;
+      } else {
+        return false;
+      }
+    });
   },
   allowUserNameChange() {
-    return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue;
+    Meteor.call('AccountSettings.allowUserNameChange', (_, result) => {
+      if (result) {
+        return true;
+      } else {
+        return false;
+      }
+    });
   },
   allowUserDelete() {
-    return AccountSettings.findOne('accounts-allowUserDelete').booleanValue;
+    Meteor.call('AccountSettings.allowUserDelete', (_, result) => {
+      if (result) {
+        return true;
+      } else {
+        return false;
+      }
+    });
   },
 });
 

+ 18 - 22
client/lib/utils.js

@@ -24,18 +24,14 @@ Utils = {
     currentUser = Meteor.user();
     if (currentUser) {
       return (currentUser.profile || {}).boardView;
+    } else if (cookies.get('boardView') === 'board-view-lists') {
+      return 'board-view-lists';
+    } else if (cookies.get('boardView') === 'board-view-swimlanes') {
+      return 'board-view-swimlanes';
+    } else if (cookies.get('boardView') === 'board-view-cal') {
+      return 'board-view-cal';
     } else {
-      if (cookies.get('boardView') === 'board-view-lists') {
-        return 'board-view-lists';
-      } else if (
-        cookies.get('boardView') === 'board-view-swimlanes'
-      ) {
-        return 'board-view-swimlanes';
-      } else if (cookies.get('boardView') === 'board-view-cal') {
-        return 'board-view-cal';
-      } else {
-        return false;
-      }
+      return false;
     }
   },
 
@@ -43,8 +39,8 @@ Utils = {
   goBoardId(_id) {
     const board = Boards.findOne(_id);
     return (
-      board
-      && FlowRouter.go('board', {
+      board &&
+      FlowRouter.go('board', {
         id: board._id,
         slug: board.slug,
       })
@@ -55,8 +51,8 @@ Utils = {
     const card = Cards.findOne(_id);
     const board = Boards.findOne(card.boardId);
     return (
-      board
-      && FlowRouter.go('card', {
+      board &&
+      FlowRouter.go('card', {
         cardId: card._id,
         boardId: board._id,
         slug: board.slug,
@@ -227,8 +223,8 @@ Utils = {
       };
 
       if (
-        'ontouchstart' in window
-        || (window.DocumentTouch && document instanceof window.DocumentTouch)
+        'ontouchstart' in window ||
+        (window.DocumentTouch && document instanceof window.DocumentTouch)
       ) {
         return true;
       }
@@ -249,8 +245,8 @@ Utils = {
 
   calculateTouchDistance(touchA, touchB) {
     return Math.sqrt(
-      Math.pow(touchA.screenX - touchB.screenX, 2)
-        + Math.pow(touchA.screenY - touchB.screenY, 2),
+      Math.pow(touchA.screenX - touchB.screenX, 2) +
+        Math.pow(touchA.screenY - touchB.screenY, 2),
     );
   },
 
@@ -267,9 +263,9 @@ Utils = {
     });
     $(document).on('touchend', selector, function(e) {
       if (
-        touchStart
-        && lastTouch
-        && Utils.calculateTouchDistance(touchStart, lastTouch) <= 20
+        touchStart &&
+        lastTouch &&
+        Utils.calculateTouchDistance(touchStart, lastTouch) <= 20
       ) {
         e.preventDefault();
         const clickEvent = document.createEvent('MouseEvents');

+ 12 - 0
models/accountSettings.js

@@ -82,4 +82,16 @@ if (Meteor.isServer) {
   });
 }
 
+AccountSettings.helpers({
+  allowEmailChange() {
+    return AccountSettings.findOne('accounts-allowEmailChange').booleanValue;
+  },
+  allowUserNameChange() {
+    return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue;
+  },
+  allowUserDelete() {
+    return AccountSettings.findOne('accounts-allowUserDelete').booleanValue;
+  },
+});
+
 export default AccountSettings;

+ 28 - 1
models/boards.js

@@ -185,6 +185,7 @@ Boards.attachSchema(
               isActive: true,
               isNoComments: false,
               isCommentOnly: false,
+              isWorker: false,
             },
           ];
         }
@@ -222,6 +223,13 @@ Boards.attachSchema(
       type: Boolean,
       optional: true,
     },
+    'members.$.isWorker': {
+      /**
+       * Is the member only allowed to move card, assign himself to card and comment
+       */
+      type: Boolean,
+      optional: true,
+    },
     permission: {
       /**
        * visibility of the board
@@ -538,6 +546,7 @@ Boards.helpers({
       isActive: true,
       isAdmin: false,
       isNoComments: true,
+      isWorker: false,
     });
   },
 
@@ -547,6 +556,17 @@ Boards.helpers({
       isActive: true,
       isAdmin: false,
       isCommentOnly: true,
+      isWorker: false,
+    });
+  },
+
+  hasWorker(memberId) {
+    return !!_.findWhere(this.members, {
+      userId: memberId,
+      isActive: true,
+      isAdmin: false,
+      isCommentOnly: false,
+      isWorker: true,
     });
   },
 
@@ -849,6 +869,7 @@ Boards.mutations({
           isActive: true,
           isNoComments: false,
           isCommentOnly: false,
+          isWorker: false,
         },
       },
     };
@@ -881,6 +902,7 @@ Boards.mutations({
     isAdmin,
     isNoComments,
     isCommentOnly,
+    isWorker,
     currentUserId = Meteor.userId(),
   ) {
     const memberIndex = this.memberIndex(memberId);
@@ -894,6 +916,7 @@ Boards.mutations({
         [`members.${memberIndex}.isAdmin`]: isAdmin,
         [`members.${memberIndex}.isNoComments`]: isNoComments,
         [`members.${memberIndex}.isCommentOnly`]: isCommentOnly,
+        [`members.${memberIndex}.isWorker`]: isWorker,
       },
     };
   },
@@ -1281,6 +1304,7 @@ if (Meteor.isServer) {
    * @param {boolean} [isActive] is the board active (default true)
    * @param {boolean} [isNoComments] disable comments (default false)
    * @param {boolean} [isCommentOnly] only enable comments (default false)
+   * @param {boolean} [isWorker] only move cards, assign himself to card and comment (default false)
    * @param {string} [permission] "private" board <== Set to "public" if you
    *                 want public Wekan board
    * @param {string} [color] the color of the board
@@ -1300,6 +1324,7 @@ if (Meteor.isServer) {
             isActive: req.body.isActive || true,
             isNoComments: req.body.isNoComments || false,
             isCommentOnly: req.body.isCommentOnly || false,
+            isWorker: req.body.isWorker || false,
           },
         ],
         permission: req.body.permission || 'private',
@@ -1403,6 +1428,7 @@ if (Meteor.isServer) {
    * @param {boolean} isAdmin admin capability
    * @param {boolean} isNoComments NoComments capability
    * @param {boolean} isCommentOnly CommentsOnly capability
+   * @param {boolean} isWorker Worker capability
    */
   JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function(
     req,
@@ -1411,7 +1437,7 @@ if (Meteor.isServer) {
     try {
       const boardId = req.params.boardId;
       const memberId = req.params.memberId;
-      const { isAdmin, isNoComments, isCommentOnly } = req.body;
+      const { isAdmin, isNoComments, isCommentOnly, isWorker } = req.body;
       Authentication.checkBoardAccess(req.userId, boardId);
       const board = Boards.findOne({ _id: boardId });
       function isTrue(data) {
@@ -1426,6 +1452,7 @@ if (Meteor.isServer) {
         isTrue(isAdmin),
         isTrue(isNoComments),
         isTrue(isCommentOnly),
+        isTrue(isWorker),
         req.userId,
       );
 

+ 1 - 1
models/cards.js

@@ -2008,7 +2008,7 @@ if (Meteor.isServer) {
     const paramBoardId = req.params.boardId;
     // Check user has permission to add card to the board
     const board = Boards.findOne({
-      _id: paramBoardId
+      _id: paramBoardId,
     });
     const addPermission = allowIsBoardMemberCommentOnly(req.userId, board);
     Authentication.checkAdminOrCondition(req.userId, addPermission);

+ 1 - 1
models/checklists.js

@@ -288,7 +288,7 @@ if (Meteor.isServer) {
       const paramBoardId = req.params.boardId;
       // Check user has permission to add checklist to the card
       const board = Boards.findOne({
-        _id: paramBoardId
+        _id: paramBoardId,
       });
       const addPermission = allowIsBoardMemberCommentOnly(req.userId, board);
       Authentication.checkAdminOrCondition(req.userId, addPermission);

+ 10 - 0
models/users.js

@@ -352,6 +352,16 @@ if (Meteor.isClient) {
       return board && board.hasCommentOnly(this._id);
     },
 
+    isNotWorker() {
+      const board = Boards.findOne(Session.get('currentBoard'));
+      return board && board.hasMember(this._id) && !board.hasWorker(this._id);
+    },
+
+    isWorker() {
+      const board = Boards.findOne(Session.get('currentBoard'));
+      return board && board.hasWorker(this._id);
+    },
+
     isBoardAdmin() {
       const board = Boards.findOne(Session.get('currentBoard'));
       return board && board.hasAdmin(this._id);

+ 2 - 0
sandstorm.js

@@ -236,12 +236,14 @@ if (isSandstorm && Meteor.isServer) {
     const isAdmin = permissions.indexOf('configure') > -1;
     const isCommentOnly = false;
     const isNoComments = false;
+    const isWorker = false;
     const permissionDoc = {
       userId,
       isActive,
       isAdmin,
       isNoComments,
       isCommentOnly,
+      isWorker,
     };
 
     const boardMembers = Boards.findOne(sandstormBoard._id).members;