浏览代码

Add Worker role.
This was originally added at Wekan v3.58,
reverted at Wekan v3.60 because of bugs,
and now after fixes added back.

Thanks to xet7 !

Closes #2788

Lauri Ojansivu 5 年之前
父节点
当前提交
f6f7705f23

+ 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()
     );
   },
 });

+ 122 - 94
client/components/cards/cardDetails.jade

@@ -44,7 +44,8 @@ template(name="cardDetails")
           +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
@@ -54,7 +55,8 @@ template(name="cardDetails")
           +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
@@ -64,7 +66,8 @@ template(name="cardDetails")
           +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
@@ -74,7 +77,8 @@ template(name="cardDetails")
           +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
@@ -85,8 +89,9 @@ template(name="cardDetails")
           +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
@@ -96,10 +101,14 @@ template(name="cardDetails")
           +userAvatarAssignee(userId=this cardId=../_id)
           | {{! XXX Hack to hide syntaxic coloration /// }}
         if canModifyCard
+          a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
+            i.fa.fa-plus
+        if currentUser.isWorker
           unless assigneeSelected
             a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
               i.fa.fa-plus
 
+
       .card-details-item.card-details-item-labels
         h3
           i.fa.fa-tags
@@ -110,8 +119,9 @@ template(name="cardDetails")
               +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
@@ -132,28 +142,29 @@ template(name="cardDetails")
 
     //- XXX We should use "editable" to avoid repetiting ourselves
     if canModifyCard
-      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'}}
+      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
@@ -165,15 +176,16 @@ template(name="cardDetails")
           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
@@ -183,15 +195,16 @@ template(name="cardDetails")
           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
@@ -266,28 +279,29 @@ template(name="cardDetailsActionsPopup")
           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
-          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
+    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-to-top
@@ -297,16 +311,17 @@ template(name="cardDetailsActionsPopup")
         a.js-move-card-to-bottom
           i.fa.fa-arrow-down
           | {{_ 'moveCardToBottom-title'}}
-    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'}}
+    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
@@ -379,16 +394,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-assignee(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}})")
@@ -416,11 +442,13 @@ template(name="cardAssigneePopup")
         p.quiet @{{ user.username }}
     ul.pop-over-list
       if currentUser.isNotCommentOnly
-        li: a.js-remove-assignee {{_ 'remove-member-from-card'}}
+        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")

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

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

+ 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 - 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()
     );
   },
 });

+ 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()
     );
   },
 

+ 17 - 14
client/components/lists/listHeader.jade

@@ -10,7 +10,7 @@ template(name="listHeader")
           a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
       h2.list-header-name(
         title="{{ moment modifiedAt 'LLL' }}"
-        class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}js-open-inlined-form is-editable{{/unless}}{{/if}}")
+        class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
         +viewer
           = title
         if wipLimit.enabled
@@ -65,11 +65,12 @@ template(name="listActionPopup")
           i.fa.fa-eye-slash
           |  {{_ 'watch'}}
   unless currentUser.isCommentOnly
-    ul.pop-over-list
-      li
-        a.js-set-color-list
-          i.fa.fa-paint-brush
-          | {{_ 'set-color-list'}}
+    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
@@ -82,13 +83,14 @@ template(name="listActionPopup")
           a.js-set-wip-limit
             i.fa.fa-ban
             | {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
-    hr
-    ul.pop-over-list
-      li
-        a.js-close-list
-          i.fa.fa-arrow-right
-          i.fa.fa-archive
-          | {{_ 'archive-list'}}
+    unless currentUser.isWorker
+      hr
+      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
@@ -114,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"}}

+ 4 - 3
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()
     );
   },
 

+ 22 - 13
client/components/sidebar/sidebar.jade

@@ -40,8 +40,9 @@ template(name="membersWidget")
       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
@@ -164,11 +165,12 @@ template(name="outgoingWebhooksPopup")
 
 template(name="boardMenuPopup")
   ul.pop-over-list
-    li: a.js-custom-fields {{_ 'custom-fields'}}
-    li
-      a.js-open-archives
-        i.fa.fa-archive
-        | {{_ '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
@@ -246,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
@@ -275,11 +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}}
@@ -343,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'}}

+ 23 - 2
client/components/sidebar/sidebar.js

@@ -161,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();
       }
@@ -267,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({
@@ -644,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'));
@@ -654,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);
   },
@@ -675,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)
     );
   },
 
@@ -695,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)
+    );
+  },
+});

+ 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'}}

+ 13 - 2
client/components/swimlanes/swimlanes.js

@@ -94,7 +94,8 @@ function initSortable(boardComponent, $listsDom) {
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   }
 
@@ -131,6 +132,15 @@ function initSortable(boardComponent, $listsDom) {
         // MultiSelection.isActive() || !userIsMember(),
       );
     }
+
+    $listsDom.sortable(
+      'option',
+      'disabled',
+      // Disable drag-dropping when user is not member
+      Meteor.user().isWorker(),
+      // Not disable drag-dropping while in multi-selection mode
+      // MultiSelection.isActive() || !userIsMember(),
+    );
   });
 }
 
@@ -282,7 +292,8 @@ Template.swimlane.helpers({
     return (
       Meteor.user() &&
       Meteor.user().isBoardMember() &&
-      !Meteor.user().isCommentOnly()
+      !Meteor.user().isCommentOnly() &&
+      !Meteor.user().isWorker()
     );
   },
 });

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

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

+ 17 - 14
client/components/users/userHeader.jade

@@ -39,12 +39,13 @@ template(name="memberMenuPopup")
         a.js-go-setting(href="{{pathFor 'setting'}}")
           i.fa.fa-lock
           | {{_ 'admin-panel'}}
-  hr
-  ul.pop-over-list
-    li
-      a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
-        i.fa.fa-clone
-        | {{_ 'templates'}}
+  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
@@ -109,13 +110,15 @@ template(name="changeSettingsPopup")
         | {{_ 'show-desktop-drag-handles'}}
         if showDesktopDragHandles
           i.fa.fa-check
-    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'}}")
+    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'}}

+ 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,
       );
 

+ 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);