From c1168d181b3ff34f5ee7794a5740281c4ab5e253 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Mon, 22 Dec 2025 21:45:09 +0200 Subject: [PATCH] New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly. Thanks to xet7 ! Fixes #1122, fixes #6033, fixes #3300 --- client/components/sidebar/sidebar.jade | 24 +++++++ client/components/sidebar/sidebar.js | 64 ++++++++++++++++++- imports/i18n/data/en.i18n.json | 8 +++ models/boards.js | 88 +++++++++++++++++++++++++- models/users.js | 10 ++- 5 files changed, 190 insertions(+), 4 deletions(-) diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index e5e90e1a5..b7f11b816 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -809,6 +809,12 @@ template(name="changePermissionsPopup") if isNormal | ✅ span.sub-name {{_ 'normal-desc'}} + li + a(class="{{#if isLastAdmin}}disabled{{else}}js-set-normal-assigned-only{{/if}}") + | {{_ 'normal-assigned-only'}} + if isNormalAssignedOnly + | ✅ + span.sub-name {{_ 'normal-assigned-only-desc'}} li a(class="{{#if isLastAdmin}}disabled{{else}}js-set-no-comments{{/if}}") | {{_ 'no-comments'}} @@ -821,6 +827,24 @@ template(name="changePermissionsPopup") if isCommentOnly | ✅ span.sub-name {{_ 'comment-only-desc'}} + li + a(class="{{#if isLastAdmin}}disabled{{else}}js-set-comment-assigned-only{{/if}}") + | {{_ 'comment-assigned-only'}} + if isCommentAssignedOnly + | ✅ + span.sub-name {{_ 'comment-assigned-only-desc'}} + li + a(class="{{#if isLastAdmin}}disabled{{else}}js-set-read-only{{/if}}") + | {{_ 'read-only'}} + if isReadOnly + | ✅ + span.sub-name {{_ 'read-only-desc'}} + li + a(class="{{#if isLastAdmin}}disabled{{else}}js-set-read-assigned-only{{/if}}") + | {{_ 'read-assigned-only'}} + if isReadAssignedOnly + | ✅ + span.sub-name {{_ 'read-assigned-only-desc'}} li a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}") | {{_ 'worker'}} diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 5831e601a..bc228742a 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -239,8 +239,20 @@ Template.memberPopup.helpers({ const commentOnly = currentBoard.hasCommentOnly(this.userId); const noComments = currentBoard.hasNoComments(this.userId); const worker = currentBoard.hasWorker(this.userId); - if (commentOnly) { + const normalAssignedOnly = currentBoard.hasNormalAssignedOnly(this.userId); + const commentAssignedOnly = currentBoard.hasCommentAssignedOnly(this.userId); + const readOnly = currentBoard.hasReadOnly(this.userId); + const readAssignedOnly = currentBoard.hasReadAssignedOnly(this.userId); + if (readAssignedOnly) { + return TAPi18n.__('read-assigned-only'); + } else if (readOnly) { + return TAPi18n.__('read-only'); + } else if (commentAssignedOnly) { + return TAPi18n.__('comment-assigned-only'); + } else if (commentOnly) { return TAPi18n.__('comment-only'); + } else if (normalAssignedOnly) { + return TAPi18n.__('normal-assigned-only'); } else if (noComments) { return TAPi18n.__('no-comments'); } else if (worker) { @@ -1925,7 +1937,7 @@ Template.removeBoardTeamPopup.helpers({ }); Template.changePermissionsPopup.events({ - 'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only, click .js-set-worker'( + 'click .js-set-admin, click .js-set-normal, click .js-set-normal-assigned-only, click .js-set-no-comments, click .js-set-comment-only, click .js-set-comment-assigned-only, click .js-set-read-only, click .js-set-read-assigned-only, click .js-set-worker'( event, ) { const currentBoard = Utils.getCurrentBoard(); @@ -1934,6 +1946,14 @@ Template.changePermissionsPopup.events({ const isCommentOnly = $(event.currentTarget).hasClass( 'js-set-comment-only', ); + const isNormalAssignedOnly = $(event.currentTarget).hasClass( + 'js-set-normal-assigned-only', + ); + const isCommentAssignedOnly = $(event.currentTarget).hasClass( + 'js-set-comment-assigned-only', + ); + const isReadOnly = $(event.currentTarget).hasClass('js-set-read-only'); + const isReadAssignedOnly = $(event.currentTarget).hasClass('js-set-read-assigned-only'); const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments'); const isWorker = $(event.currentTarget).hasClass('js-set-worker'); currentBoard.setMemberPermission( @@ -1942,6 +1962,10 @@ Template.changePermissionsPopup.events({ isNoComments, isCommentOnly, isWorker, + isNormalAssignedOnly, + isCommentAssignedOnly, + isReadOnly, + isReadAssignedOnly, ); Popup.back(1); }, @@ -1959,10 +1983,22 @@ Template.changePermissionsPopup.helpers({ !currentBoard.hasAdmin(this.userId) && !currentBoard.hasNoComments(this.userId) && !currentBoard.hasCommentOnly(this.userId) && + !currentBoard.hasNormalAssignedOnly(this.userId) && + !currentBoard.hasCommentAssignedOnly(this.userId) && + !currentBoard.hasReadOnly(this.userId) && + !currentBoard.hasReadAssignedOnly(this.userId) && !currentBoard.hasWorker(this.userId) ); }, + isNormalAssignedOnly() { + const currentBoard = Utils.getCurrentBoard(); + return ( + !currentBoard.hasAdmin(this.userId) && + currentBoard.hasNormalAssignedOnly(this.userId) + ); + }, + isNoComments() { const currentBoard = Utils.getCurrentBoard(); return ( @@ -1979,6 +2015,30 @@ Template.changePermissionsPopup.helpers({ ); }, + isCommentAssignedOnly() { + const currentBoard = Utils.getCurrentBoard(); + return ( + !currentBoard.hasAdmin(this.userId) && + currentBoard.hasCommentAssignedOnly(this.userId) + ); + }, + + isReadOnly() { + const currentBoard = Utils.getCurrentBoard(); + return ( + !currentBoard.hasAdmin(this.userId) && + currentBoard.hasReadOnly(this.userId) + ); + }, + + isReadAssignedOnly() { + const currentBoard = Utils.getCurrentBoard(); + return ( + !currentBoard.hasAdmin(this.userId) && + currentBoard.hasReadAssignedOnly(this.userId) + ); + }, + isWorker() { const currentBoard = Utils.getCurrentBoard(); return ( diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index acf3c6934..625325786 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -328,10 +328,16 @@ "comment-placeholder": "Write Comment", "comment-only": "Comment only", "comment-only-desc": "Can comment on cards only.", + "comment-assigned-only": "Comment only (Assigned Only)", + "comment-assigned-only-desc": "Can comment on assigned cards only.", "comment-delete": "Are you sure you want to delete the comment?", "deleteCommentPopup-title": "Delete comment?", "no-comments": "No comments", "no-comments-desc": "Can not see comments and activities.", + "read-only": "Read Only", + "read-only-desc": "Can view cards only. Can not comment or edit.", + "read-assigned-only": "Read Only (Assigned Only)", + "read-assigned-only-desc": "Can view assigned cards only. Can not comment or edit.", "worker": "Worker", "worker-desc": "Can only move cards, assign itself to card and comment.", "computer": "Computer", @@ -568,6 +574,8 @@ "no-results": "No results", "normal": "Normal", "normal-desc": "Can view and edit cards. Can't change settings.", + "normal-assigned-only": "Normal (Assigned Only)", + "normal-assigned-only-desc": "Can view and edit only assigned cards. Can't change settings.", "not-accepted-yet": "Invitation not accepted yet", "notify-participate": "Receive updates to any cards you participate as creator or member", "notify-watch": "Receive updates to any boards, lists, or cards you’re watching", diff --git a/models/boards.js b/models/boards.js index 569bb5e78..5ccfbd085 100644 --- a/models/boards.js +++ b/models/boards.js @@ -225,6 +225,34 @@ Boards.attachSchema( type: Boolean, optional: true, }, + 'members.$.isNormalAssignedOnly': { + /** + * Is the member only allowed to see assigned cards (Normal permission) + */ + type: Boolean, + optional: true, + }, + 'members.$.isCommentAssignedOnly': { + /** + * Is the member only allowed to comment on assigned cards + */ + type: Boolean, + optional: true, + }, + 'members.$.isReadOnly': { + /** + * Is the member only allowed to read the board (no comments, no editing) + */ + type: Boolean, + optional: true, + }, + 'members.$.isReadAssignedOnly': { + /** + * Is the member only allowed to read assigned cards (no comments, no editing) + */ + type: Boolean, + optional: true, + }, permission: { /** * visibility of the board @@ -979,6 +1007,44 @@ Boards.helpers({ }); }, + hasNormalAssignedOnly(memberId) { + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: false, + isNormalAssignedOnly: true, + isCommentAssignedOnly: false, + }); + }, + + hasCommentAssignedOnly(memberId) { + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: false, + isNormalAssignedOnly: false, + isCommentAssignedOnly: true, + }); + }, + + hasReadOnly(memberId) { + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: false, + isReadOnly: true, + }); + }, + + hasReadAssignedOnly(memberId) { + return !!_.findWhere(this.members, { + userId: memberId, + isActive: true, + isAdmin: false, + isReadAssignedOnly: true, + }); + }, + hasAnyAllowsDate() { const ret = this.allowsReceivedDate || this.allowsStartDate || this.allowsDueDate || this.allowsEndDate; return ret; @@ -1416,6 +1482,10 @@ Boards.mutations({ isNoComments: false, isCommentOnly: false, isWorker: false, + isNormalAssignedOnly: false, + isCommentAssignedOnly: false, + isReadOnly: false, + isReadAssignedOnly: false, }, }, }; @@ -1449,6 +1519,10 @@ Boards.mutations({ isNoComments, isCommentOnly, isWorker, + isNormalAssignedOnly = false, + isCommentAssignedOnly = false, + isReadOnly = false, + isReadAssignedOnly = false, currentUserId = Meteor.userId(), ) { const memberIndex = this.memberIndex(memberId); @@ -1463,6 +1537,10 @@ Boards.mutations({ [`members.${memberIndex}.isNoComments`]: isNoComments, [`members.${memberIndex}.isCommentOnly`]: isCommentOnly, [`members.${memberIndex}.isWorker`]: isWorker, + [`members.${memberIndex}.isNormalAssignedOnly`]: isNormalAssignedOnly, + [`members.${memberIndex}.isCommentAssignedOnly`]: isCommentAssignedOnly, + [`members.${memberIndex}.isReadOnly`]: isReadOnly, + [`members.${memberIndex}.isReadAssignedOnly`]: isReadAssignedOnly, }, }; }, @@ -2372,6 +2450,10 @@ JsonRoutes.add('POST', '/api/boards/:boardId/copy', function(req, res) { * @param {boolean} isNoComments NoComments capability * @param {boolean} isCommentOnly CommentsOnly capability * @param {boolean} isWorker Worker capability + * @param {boolean} isNormalAssignedOnly NormalAssignedOnly capability + * @param {boolean} isCommentAssignedOnly CommentAssignedOnly capability + * @param {boolean} isReadOnly ReadOnly capability + * @param {boolean} isReadAssignedOnly ReadAssignedOnly capability */ JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function( req, @@ -2381,7 +2463,7 @@ JsonRoutes.add('POST', '/api/boards/:boardId/copy', function(req, res) { Authentication.checkUserId(req.userId); const boardId = req.params.boardId; const memberId = req.params.memberId; - const { isAdmin, isNoComments, isCommentOnly, isWorker } = req.body; + const { isAdmin, isNoComments, isCommentOnly, isWorker, isNormalAssignedOnly, isCommentAssignedOnly, isReadOnly, isReadAssignedOnly } = req.body; const board = ReactiveCache.getBoard(boardId); function isTrue(data) { try { @@ -2396,6 +2478,10 @@ JsonRoutes.add('POST', '/api/boards/:boardId/copy', function(req, res) { isTrue(isNoComments), isTrue(isCommentOnly), isTrue(isWorker), + isTrue(isNormalAssignedOnly), + isTrue(isCommentAssignedOnly), + isTrue(isReadOnly), + isTrue(isReadAssignedOnly), req.userId, ); diff --git a/models/users.js b/models/users.js index e30689359..7bd62c6c1 100644 --- a/models/users.js +++ b/models/users.js @@ -2947,6 +2947,10 @@ if (Meteor.isServer) { * @param {boolean} isNoComments disable comments * @param {boolean} isCommentOnly only enable comments * @param {boolean} isWorker is the user a board worker + * @param {boolean} isNormalAssignedOnly only see assigned cards (Normal permission) + * @param {boolean} isCommentAssignedOnly only comment on assigned cards + * @param {boolean} isReadOnly read-only access (no comments or editing) + * @param {boolean} isReadAssignedOnly read-only assigned cards only * @return_type {_id: string, * title: string} */ @@ -2959,7 +2963,7 @@ if (Meteor.isServer) { const userId = req.params.userId; const boardId = req.params.boardId; const action = req.body.action; - const { isAdmin, isNoComments, isCommentOnly, isWorker } = req.body; + const { isAdmin, isNoComments, isCommentOnly, isWorker, isNormalAssignedOnly, isCommentAssignedOnly, isReadOnly, isReadAssignedOnly } = req.body; let data = ReactiveCache.getUser(userId); if (data !== undefined) { if (action === 'add') { @@ -2978,6 +2982,10 @@ if (Meteor.isServer) { isTrue(isNoComments), isTrue(isCommentOnly), isTrue(isWorker), + isTrue(isNormalAssignedOnly), + isTrue(isCommentAssignedOnly), + isTrue(isReadOnly), + isTrue(isReadAssignedOnly), userId, ); }