Fix New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly. Part 1.

Thanks to nazim-oss and xet7 !

Related #6060
This commit is contained in:
Lauri Ojansivu 2026-01-14 23:43:11 +02:00
parent 2f59e42024
commit eabb6a239d
25 changed files with 562 additions and 291 deletions

View file

@ -54,9 +54,11 @@ template(name="commentReactions")
span.reaction-codepoint !{reaction.reactionCodepoint}
span.reaction-count #{reaction.userIds.length}
if (currentUser.isBoardMember)
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
span(title="{{_ 'reaction' }}") 😀
span(title="{{_ 'add' }}")
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
span(title="{{_ 'reaction' }}") 😀
span(title="{{_ 'add' }}")
template(name="addReactionPopup")
.reactions-popup

View file

@ -196,7 +196,11 @@ BlazeComponent.extendComponent({
return ret;
},
currentMenuPath() {
const sel = this.selectedMenu.get();
const selectedMenuVar = this.selectedMenu;
if (!selectedMenuVar) {
return { icon: '🗂️', text: TAPi18n.__('allboards.workspaces') };
}
const sel = selectedMenuVar.get();
const currentUser = ReactiveCache.getCurrentUser();
// Helper to find space by id in tree

View file

@ -19,14 +19,12 @@ template(name="cardDetails")
| 🔽
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
| ❌
if canModifyCard
if cardMaximized
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
if canModifyCard
if cardMaximized
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-button.js-copy-link(
@ -35,8 +33,9 @@ template(name="cardDetails")
href="{{ originRelativeUrl }}"
)
span.emoji-icon 🔗
span.card-drag-handle.js-card-drag-handle(title="Drag card")
| ↕️
if canModifyCard
span.card-drag-handle.js-card-drag-handle(title="Drag card")
| ↕️
span.copied-tooltip {{_ 'copied'}}
else
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
@ -50,24 +49,23 @@ template(name="cardDetails")
| 🖥️
else
| 📱
if canModifyCard
if cardMaximized
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon 🔗
span.copied-tooltip {{_ 'copied'}}
if cardMaximized
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
| 🔽
else
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
| 🔼
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
| ☰
a.card-copy-mobile-button.js-copy-link(
id="cardURL_copy"
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
span.emoji-icon 🔗
span.copied-tooltip {{_ 'copied'}}
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{else}}js-card-title-drag-handle{{/if}}")
+viewer
if currentBoard.allowsCardNumber
span.card-number
@ -636,13 +634,15 @@ template(name="cardDetails")
if currentBoard.allowsComments
if currentUser.isBoardMember
unless currentUser.isNoComments
+commentForm
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
+commentForm
+comments
hr
.card-details-right
unless currentUser.isNoComments
if currentUser.isBoardAdmin
.activity-title
h3.card-details-item-title
| 📜
@ -655,7 +655,7 @@ template(name="cardDetails")
input.toggle-switch(type="checkbox" id="toggleShowActivitiesCard")
label.toggle-label(for="toggleShowActivitiesCard")
unless currentUser.isNoComments
if currentUser.isBoardAdmin
if isLoaded.get
if isLinkedCard
+activities(card=this mode="linkedcard")
@ -741,55 +741,107 @@ template(name="cardDetailsActionsPopup")
else
| 👁️
| {{_ 'show-list-on-minicard'}}
hr
if canModifyCard
hr
else
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
ul.pop-over-list
li
a.js-export-card
| 📤
| {{_ 'export-card'}}
hr
ul.pop-over-list
li
a.js-move-card-to-top
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
| ➡️
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
| 📋
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
| 📋
| 📋
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
unless canModifyCard
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
hr
ul.pop-over-list
li
a.js-move-card-to-top
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
| ➡️
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
| 📋
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
| 📋
| 📋
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon 🔗
| {{_ 'cardMorePopup-title'}}
if canModifyCard
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon 🔗
| {{_ 'cardMorePopup-title'}}
a.js-move-card-to-top
| ⬆️
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
| ⬇️
| {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li
a.js-move-card
| ➡️
| {{_ 'moveCardPopup-title'}}
unless currentUser.isWorker
li
a.js-copy-card
| 📋
| {{_ 'copyCardPopup-title'}}
unless currentUser.isWorker
ul.pop-over-list
li
a.js-copy-checklist-cards
| 📋
| 📋
| {{_ 'copyManyCardsPopup-title'}}
unless archived
hr
ul.pop-over-list
li
a.js-archive
| ➡️
| 📦
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
span.emoji-icon 🔗
| {{_ 'cardMorePopup-title'}}
template(name="exportCardPopup")
ul.pop-over-list

View file

@ -100,6 +100,11 @@ BlazeComponent.extendComponent({
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
},
showActivities() {
const user = ReactiveCache.getCurrentUser();
return user && user.hasShowActivities();
},
cardCollapsed() {
const user = ReactiveCache.getCurrentUser();
if (user && user.profile) {
@ -350,6 +355,37 @@ BlazeComponent.extendComponent({
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
},
'mousedown .js-card-title-drag-handle'(event) {
// Allow dragging from title for ReadOnly users
// Don't interfere with text selection
if (event.target.tagName === 'A' || $(event.target).closest('a').length > 0) {
return; // Don't drag if clicking on links
}
event.preventDefault();
const $card = $(event.target).closest('.card-details');
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
},
'click .js-close-card-details'() {
// Get board ID from either the card data or current board in session
const card = this.currentData() || this.data();
@ -517,9 +553,6 @@ BlazeComponent.extendComponent({
Session.set('cardDetailsIsDragging', false);
Session.set('cardDetailsIsMouseDown', false);
},
'click #toggleShowActivitiesCard'() {
this.data().toggleShowActivities();
},
'click #toggleHideCheckedChecklistItems'() {
this.data().toggleHideCheckedChecklistItems();
},

View file

@ -209,6 +209,12 @@ BlazeComponent.extendComponent({
evt.stopImmediatePropagation();
evt.preventDefault();
Utils.goBoardId(Session.get('currentBoard'));
} else {
// Allow normal href navigation, but if it's the same card URL,
// we'll handle it by directly setting the session
evt.preventDefault();
const card = this.currentData();
Session.set('currentCard', card._id);
}
},

View file

@ -58,9 +58,11 @@ template(name="listHeader")
i.list-header-watch-icon | 👁️
div.list-header-menu
unless currentUser.isCommentOnly
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
else
a.list-header-menu-icon.js-select-list ▶️
unless currentUser.isWorker
@ -72,13 +74,15 @@ template(name="listHeader")
unless collapsed
div.list-header-menu
unless currentUser.isCommentOnly
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}") ↕️
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}") ↕️
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
template(name="editListTitleForm")
.list-composer
@ -89,18 +93,20 @@ template(name="editListTitleForm")
| ❌
template(name="listActionPopup")
ul.pop-over-list
li
a.js-add-card.list-header-plus-bottom
|
| ⬇️
| {{_ 'add-card-to-bottom-of-list'}}
hr
ul.pop-over-list
li
a.js-set-list-width
| ↔️
| {{_ 'set-list-width'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
ul.pop-over-list
li
a.js-add-card.list-header-plus-bottom
|
| ⬇️
| {{_ 'add-card-to-bottom-of-list'}}
hr
ul.pop-over-list
li
a.js-set-list-width
| ↔️
| {{_ 'set-list-width'}}
ul.pop-over-list
li
a.js-toggle-watch-list
@ -111,38 +117,40 @@ template(name="listActionPopup")
| 🙈
| {{_ 'watch'}}
unless currentUser.isCommentOnly
unless currentUser.isWorker
ul.pop-over-list
li
a.js-set-color-list
| 🎨
| {{_ 'set-color-list'}}
ul.pop-over-list
if cards.length
li
a.js-select-cards
| ☑️
| {{_ '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}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a.js-close-list
| ➡️
| 📦
| {{_ 'archive-list'}}
hr
ul.pop-over-list
li
a.js-more
| 🔗
| {{_ 'listMorePopup-title'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
unless currentUser.isWorker
ul.pop-over-list
li
a.js-set-color-list
| 🎨
| {{_ 'set-color-list'}}
ul.pop-over-list
if cards.length
li
a.js-select-cards
| ☑️
| {{_ '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}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a.js-close-list
| ➡️
| 📦
| {{_ 'archive-list'}}
hr
ul.pop-over-list
li
a.js-more
| 🔗
| {{_ 'listMorePopup-title'}}
template(name="boardLists")
ul.pop-over-list

View file

@ -46,7 +46,7 @@ template(name='homeSidebar')
span {{#if isShowWeekOfYear}}✅{{else}}⬜{{/if}}
span {{_ 'show-week-of-year'}}
hr
unless currentUser.isNoComments
if currentUser.isBoardAdmin
h3.activity-title
| 💬
| {{_ 'activities'}}

View file

@ -3,26 +3,30 @@ template(name="archivesSidebar")
+basicTabs(tabs=tabs)
+tabContent(slug="cards")
unless isWorker
p.quiet
a.js-restore-all-cards {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-cards {{_ 'delete-all'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-cards {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-cards {{_ 'delete-all'}}
each archivedCards
.minicard-wrapper.js-minicard
+minicard(this)
if currentUser.isBoardMember
unless isWorker
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-card {{_ 'restore'}}
if currentUser.isBoardAdmin
| -
a.js-delete-card {{_ 'delete'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-card {{_ 'restore'}}
if currentUser.isBoardAdmin
| -
a.js-delete-card {{_ 'delete'}}
if cardIsInArchivedList
p.quiet.small ({{_ 'warn-list-archived'}})
else
@ -30,53 +34,61 @@ template(name="archivesSidebar")
+tabContent(slug="lists")
unless isWorker
p.quiet
a.js-restore-all-lists {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-lists {{_ 'delete-all'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-lists {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-lists {{_ 'delete-all'}}
ul.archived-lists
each archivedLists
li.archived-lists-item
= title
if currentUser.isBoardMember
unless isWorker
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-list {{_ 'restore'}}
if currentUser.isBoardAdmin
| -
a.js-delete-list {{_ 'delete'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-list {{_ 'restore'}}
if currentUser.isBoardAdmin
| -
a.js-delete-list {{_ 'delete'}}
else
li.no-items-message {{_ 'no-archived-lists'}}
+tabContent(slug="swimlanes")
unless isWorker
p.quiet
a.js-restore-all-swimlanes {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-swimlanes {{_ 'delete-all'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
a.js-restore-all-swimlanes {{_ 'restore-all'}}
if currentUser.isBoardAdmin
| -
a.js-delete-all-swimlanes {{_ 'delete-all'}}
ul.archived-lists
each archivedSwimlanes
li.archived-lists-item
= title
if currentUser.isBoardMember
unless isWorker
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-swimlane {{_ 'restore'}}
if currentUser.isBoardAdmin
| -
a.js-delete-swimlane {{_ 'delete'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
|
| {{ moment this.archivedAt 'LLL' }}
br
a.js-restore-swimlane {{_ 'restore'}}
if currentUser.isBoardAdmin
| -
a.js-delete-swimlane {{_ 'delete'}}
else
li.no-items-message {{_ 'no-archived-swimlanes'}}
else

View file

@ -25,23 +25,25 @@ template(name="swimlaneFixedHeader")
.swimlane-header-menu
if currentUser
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
if collapseSwimlane
| ▶
else
| 🔽
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
if isTouchScreenOrShowDesktopDragHandles
unless isTouchScreen
a.swimlane-header-handle.handle.js-swimlane-header-handle
| ↕️
if isTouchScreen
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
| ↕️
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
| ☰
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
unless currentUser.isWorker
a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
if collapseSwimlane
| ▶
else
| 🔽
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
if isTouchScreenOrShowDesktopDragHandles
unless isTouchScreen
a.swimlane-header-handle.handle.js-swimlane-header-handle
| ↕️
if isTouchScreen
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
| ↕️
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
| ☰
template(name="editSwimlaneTitleForm")
.list-composer
@ -54,44 +56,48 @@ template(name="editSwimlaneTitleForm")
template(name="swimlaneActionPopup")
if currentUser
unless currentUser.isCommentOnly
ul.pop-over-list
if currentUser.isBoardAdmin
li: a.js-set-swimlane-color
| 🎨
| {{_ 'select-color'}}
li: a.js-set-swimlane-height
| ↕️
| {{_ 'set-swimlane-height'}}
if currentUser.isBoardAdmin
unless this.isTemplateContainer
hr
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
ul.pop-over-list
li: a.js-close-swimlane
| ▶️
| 📦
| {{_ 'archive-swimlane'}}
ul.pop-over-list
li: a.js-copy-swimlane
| 📋
| {{_ 'copy-swimlane'}}
ul.pop-over-list
li: a.js-move-swimlane
| ⬆️
| {{_ 'move-swimlane'}}
if currentUser.isBoardAdmin
li: a.js-set-swimlane-color
| 🎨
| {{_ 'select-color'}}
li: a.js-set-swimlane-height
| ↕️
| {{_ 'set-swimlane-height'}}
if currentUser.isBoardAdmin
unless this.isTemplateContainer
hr
ul.pop-over-list
li: a.js-close-swimlane
| ▶️
| 📦
| {{_ 'archive-swimlane'}}
ul.pop-over-list
li: a.js-copy-swimlane
| 📋
| {{_ 'copy-swimlane'}}
ul.pop-over-list
li: a.js-move-swimlane
| ⬆️
| {{_ 'move-swimlane'}}
template(name="swimlaneAddPopup")
if currentUser
unless currentUser.isCommentOnly
form
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
autocomplete="off" autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-swimlane-template {{_ 'template'}}
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
form
input.swimlane-name-input.full-line(type="text" placeholder="{{_ 'add-swimlane'}}"
autocomplete="off" autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-swimlane-template {{_ 'template'}}
template(name="setSwimlaneColorPopup")
form.edit-label.swimlane-color-popup

View file

@ -48,7 +48,9 @@ template(name="listsGroup")
template(name="addListForm")
unless currentUser.isWorker
unless currentUser.isCommentOnly
.list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
.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'}}"

View file

@ -247,7 +247,9 @@ Utils = {
currentUser &&
currentUser.isBoardMember() &&
!currentUser.isCommentOnly() &&
!currentUser.isWorker()
!currentUser.isWorker() &&
!currentUser.isReadOnly() &&
!currentUser.isReadAssignedOnly()
);
return ret;
},
@ -256,7 +258,9 @@ Utils = {
const ret = (
currentUser &&
currentUser.isBoardMember() &&
!currentUser.isCommentOnly()
!currentUser.isCommentOnly() &&
!currentUser.isReadOnly() &&
!currentUser.isReadAssignedOnly()
);
return ret;
},
@ -265,7 +269,9 @@ Utils = {
const ret = (
currentUser &&
currentUser.isBoardMember() &&
!currentUser.isCommentOnly()
!currentUser.isCommentOnly() &&
!currentUser.isReadOnly() &&
!currentUser.isReadAssignedOnly()
);
return ret;
},

View file

@ -127,36 +127,7 @@ FlowRouter.route('/public', {
},
});
FlowRouter.route('/b/:id/:slug', {
name: 'board',
action(params) {
const currentBoard = params.id;
const previousBoard = Session.get('currentBoard');
Session.set('currentBoard', currentBoard);
Session.set('currentCard', null);
Session.set('popupCardId', null);
Session.set('popupCardBoardId', null);
// If we close a card, we'll execute again this route action but we don't
// want to excape every current actions (filters, etc.)
if (previousBoard !== currentBoard) {
Filter.reset();
Session.set('sortBy', '');
EscapeActions.executeAll();
} else {
EscapeActions.executeUpTo('popup-close');
}
Utils.manageCustomUI();
Utils.manageMatomo();
this.render('defaultLayout', {
headerBar: 'boardHeaderBar',
content: 'board',
});
},
});
// Card route MUST be registered BEFORE board route so it matches first
FlowRouter.route('/b/:boardId/:slug/:cardId', {
name: 'card',
action(params) {
@ -187,6 +158,48 @@ FlowRouter.route('/b/:boardId/:slug/:cardId', {
},
});
FlowRouter.route('/b/:id/:slug', {
name: 'board',
action(params) {
const pathSegments = FlowRouter.current().path.split('/').filter(s => s);
// If we have 4+ segments (b, boardId, slug, cardId), this is a card view
if (pathSegments.length >= 4) {
return;
}
// If slug contains "/" it means a cardId was matched by this greedy pattern
if (params.slug && params.slug.includes('/')) {
return;
}
const currentBoard = params.id;
const previousBoard = Session.get('currentBoard');
Session.set('currentBoard', currentBoard);
Session.set('currentCard', null);
Session.set('popupCardId', null);
Session.set('popupCardBoardId', null);
// If we close a card, we'll execute again this route action but we don't
// want to excape every current actions (filters, etc.)
if (previousBoard !== currentBoard) {
Filter.reset();
Session.set('sortBy', '');
EscapeActions.executeAll();
} else {
EscapeActions.executeUpTo('popup-close');
}
Utils.manageCustomUI();
Utils.manageMatomo();
this.render('defaultLayout', {
headerBar: 'boardHeaderBar',
content: 'board',
});
},
});
FlowRouter.route('/shortcuts', {
name: 'shortcuts',
action() {

View file

@ -124,7 +124,7 @@
"addMemberPopup-title": "Members",
"memberPopup-title": "Member Settings",
"admin": "Admin",
"admin-desc": "Can view and edit cards, remove members, and change settings for the board.",
"admin-desc": "Can view and edit cards, remove members, and change settings for the board. Can view activities.",
"admin-announcement": "Announcement",
"admin-announcement-active": "Active System-Wide Announcement",
"admin-announcement-title": "Announcement from Administrator",
@ -335,7 +335,7 @@
"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.",
"no-comments-desc": "Can not see comments.",
"read-only": "Read Only",
"read-only-desc": "Can view cards only. Can not edit.",
"read-assigned-only": "Only Assigned Read",

View file

@ -176,7 +176,8 @@ Attachments = new FilesCollection({
if (Meteor.isServer) {
Attachments.allow({
insert(userId, fileObj) {
return allowIsBoardMember(userId, ReactiveCache.getBoard(fileObj.boardId));
// ReadOnly users cannot upload attachments
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(fileObj.boardId));
},
update(userId, fileObj, fields) {
// Only allow updates to specific fields that don't affect security
@ -190,7 +191,8 @@ if (Meteor.isServer) {
return false;
}
return allowIsBoardMember(userId, ReactiveCache.getBoard(fileObj.boardId));
// ReadOnly users cannot update attachments
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(fileObj.boardId));
},
remove(userId, fileObj) {
// Additional security check: ensure the file belongs to the board the user has access to
@ -209,7 +211,8 @@ if (Meteor.isServer) {
return false;
}
return allowIsBoardMember(userId, board);
// ReadOnly users cannot delete attachments
return allowIsBoardMemberWithWriteAccess(userId, board);
},
fetch: ['meta', 'boardId'],
});

View file

@ -82,7 +82,8 @@ CardComments.attachSchema(
CardComments.allow({
insert(userId, doc) {
return allowIsBoardMember(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly users cannot add comments. Only members who can comment are allowed.
return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
},
update(userId, doc) {
return userId === doc.userId || allowIsBoardAdmin(userId, ReactiveCache.getBoard(doc.boardId));

View file

@ -518,7 +518,7 @@ Cards.attachSchema(
);
// Centralized update policy for Cards
// Security: deny any direct client updates to 'vote' fields; require membership otherwise
// Security: deny any direct client updates to 'vote' fields; require write access otherwise
canUpdateCard = function(userId, doc, fields) {
if (!userId) return false;
const fieldNames = fields || [];
@ -530,19 +530,22 @@ canUpdateCard = function(userId, doc, fields) {
if (_.some(fieldNames, f => typeof f === 'string' && (f === 'poker' || f.indexOf('poker.') === 0))) {
return false;
}
return allowIsBoardMember(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly users cannot edit cards
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
};
Cards.allow({
insert(userId, doc) {
return allowIsBoardMember(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly users cannot create cards
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
update(userId, doc, fields) {
return canUpdateCard(userId, doc, fields);
},
remove(userId, doc) {
return allowIsBoardMember(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly users cannot delete cards
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
fetch: ['boardId'],
});

View file

@ -70,13 +70,16 @@ ChecklistItems.attachSchema(
ChecklistItems.allow({
insert(userId, doc) {
return allowIsBoardMemberByCard(userId, ReactiveCache.getCard(doc.cardId));
// ReadOnly users cannot create checklist items
return allowIsBoardMemberWithWriteAccessByCard(userId, ReactiveCache.getCard(doc.cardId));
},
update(userId, doc) {
return allowIsBoardMemberByCard(userId, ReactiveCache.getCard(doc.cardId));
// ReadOnly users cannot edit checklist items
return allowIsBoardMemberWithWriteAccessByCard(userId, ReactiveCache.getCard(doc.cardId));
},
remove(userId, doc) {
return allowIsBoardMemberByCard(userId, ReactiveCache.getCard(doc.cardId));
// ReadOnly users cannot delete checklist items
return allowIsBoardMemberWithWriteAccessByCard(userId, ReactiveCache.getCard(doc.cardId));
},
fetch: ['userId', 'cardId'],
});

View file

@ -170,13 +170,16 @@ Checklists.helpers({
Checklists.allow({
insert(userId, doc) {
return allowIsBoardMemberByCard(userId, ReactiveCache.getCard(doc.cardId));
// ReadOnly users cannot create checklists
return allowIsBoardMemberWithWriteAccessByCard(userId, ReactiveCache.getCard(doc.cardId));
},
update(userId, doc) {
return allowIsBoardMemberByCard(userId, ReactiveCache.getCard(doc.cardId));
// ReadOnly users cannot edit checklists
return allowIsBoardMemberWithWriteAccessByCard(userId, ReactiveCache.getCard(doc.cardId));
},
remove(userId, doc) {
return allowIsBoardMemberByCard(userId, ReactiveCache.getCard(doc.cardId));
// ReadOnly users cannot delete checklists
return allowIsBoardMemberWithWriteAccessByCard(userId, ReactiveCache.getCard(doc.cardId));
},
fetch: ['userId', 'cardId'],
});

View file

@ -181,13 +181,16 @@ Lists.attachSchema(
Lists.allow({
insert(userId, doc) {
return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly and CommentOnly users cannot create lists
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
update(userId, doc) {
return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly and CommentOnly users cannot edit lists
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
remove(userId, doc) {
return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly and CommentOnly users cannot delete lists
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
fetch: ['boardId'],
});

View file

@ -132,13 +132,16 @@ Swimlanes.attachSchema(
Swimlanes.allow({
insert(userId, doc) {
return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly and CommentOnly users cannot create swimlanes
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
update(userId, doc) {
return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly and CommentOnly users cannot edit swimlanes
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
remove(userId, doc) {
return allowIsBoardMemberCommentOnly(userId, ReactiveCache.getBoard(doc.boardId));
// ReadOnly and CommentOnly users cannot delete swimlanes
return allowIsBoardMemberWithWriteAccess(userId, ReactiveCache.getBoard(doc.boardId));
},
fetch: ['boardId'],
});

View file

@ -271,6 +271,13 @@ Users.attachSchema(
type: Boolean,
optional: true,
},
'profile.showActivities': {
/**
* does the user want to show activities in card details?
*/
type: Boolean,
optional: true,
},
'profile.customFieldsGrid': {
/**
* has user at card Custom Fields have Grid (false) or one per row (true) layout?
@ -875,6 +882,16 @@ if (Meteor.isClient) {
return board && board.hasCommentOnly(this._id);
},
isReadOnly() {
const board = Utils.getCurrentBoard();
return board && board.hasReadOnly(this._id);
},
isReadAssignedOnly() {
const board = Utils.getCurrentBoard();
return board && board.hasReadAssignedOnly(this._id);
},
isNotWorker() {
const board = Utils.getCurrentBoard();
return board && board.hasMember(this._id) && !board.hasWorker(this._id);
@ -1206,6 +1223,11 @@ Users.helpers({
return profile.cardMaximized || false;
},
hasShowActivities() {
const profile = this.profile || {};
return profile.showActivities || false;
},
hasHiddenMinicardLabelText() {
const profile = this.profile || {};
return profile.hiddenMinicardLabelText || false;
@ -1753,6 +1775,14 @@ Users.mutations({
};
},
toggleShowActivities(value = false) {
return {
$set: {
'profile.showActivities': !value,
},
};
},
toggleLabelText(value = false) {
return {
$set: {

View file

@ -20,6 +20,17 @@ allowIsBoardMemberNoComments = function(userId, board) {
return board && board.hasMember(userId) && !board.hasNoComments(userId);
};
// Check if user has write access to board (can create/edit cards and lists)
allowIsBoardMemberWithWriteAccess = function(userId, board) {
return board && board.members && board.members.some(e => e.userId === userId && e.isActive && !e.isNoComments && !e.isCommentOnly && !e.isWorker && !e.isReadOnly && !e.isReadAssignedOnly);
};
// Check if user has write access via a card's board
allowIsBoardMemberWithWriteAccessByCard = function(userId, card) {
const board = card && card.board && card.board();
return allowIsBoardMemberWithWriteAccess(userId, board);
};
allowIsBoardMemberByCard = function(userId, card) {
const board = card.board();
return board && board.hasMember(userId);

View file

@ -21,6 +21,28 @@ Meteor.publish('activities', function(kind, id, limit, showActivities) {
return this.ready();
}
// Check user permissions - only BoardAdmin can view activities
if (this.userId) {
const user = ReactiveCache.getUser(this.userId);
const board = ReactiveCache.getBoard(id);
if (user && board) {
// Find user membership in board
const membership = board.members.find(m => m.userId === this.userId);
// Only BoardAdmin can view activities
if (!membership || !membership.isAdmin) {
return this.ready();
}
} else {
// If board or user not found, deny
return this.ready();
}
} else {
// If not logged in, deny
return this.ready();
}
// Get linkedBoard
let linkedElmtId = [id];
if (kind == 'board') {

View file

@ -296,14 +296,23 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
const linkedBoardCards = this.join(Cards);
linkedBoardCards.selector = _ids => ({ boardId: _ids });
// Build card selector based on user's permissions
const cardSelector = {
boardId: { $in: [boardId, board.subtasksDefaultBoardId] },
archived: isArchived,
};
// Check if current user has assigned-only permissions
if (thisUserId && board.members) {
const member = _.findWhere(board.members, { userId: thisUserId, isActive: true });
if (member && (member.isNormalAssignedOnly || member.isCommentAssignedOnly || member.isReadAssignedOnly)) {
// User with assigned-only permissions should only see cards assigned to them
cardSelector.assignees = { $in: [thisUserId] };
}
}
this.cursor(
ReactiveCache.getCards({
boardId: { $in: [boardId, board.subtasksDefaultBoardId] },
archived: isArchived,
},
{},
true,
),
ReactiveCache.getCards(cardSelector, {}, true),
function(cardId, card) {
if (card.type === 'cardType-linkedCard') {
const impCardId = card.linkedId;

View file

@ -75,6 +75,24 @@ import Team from "../../models/team";
Meteor.publish('card', cardId => {
check(cardId, String);
const userId = Meteor.userId();
const card = ReactiveCache.getCard({ _id: cardId });
// If user has assigned-only permissions, check if they're assigned to this card
if (userId && card && card.boardId) {
const board = ReactiveCache.getBoard({ _id: card.boardId });
if (board && board.members) {
const member = _.findWhere(board.members, { userId: userId, isActive: true });
if (member && (member.isNormalAssignedOnly || member.isCommentAssignedOnly || member.isReadAssignedOnly)) {
// User with assigned-only permissions can only view cards assigned to them
if (!card.assignees || !card.assignees.includes(userId)) {
return []; // Don't publish if user is not assigned
}
}
}
}
const ret = ReactiveCache.getCards(
{ _id: cardId },
{},
@ -88,6 +106,24 @@ Meteor.publish('card', cardId => {
*/
Meteor.publishRelations('popupCardData', function(cardId) {
check(cardId, String);
const userId = this.userId;
const card = ReactiveCache.getCard({ _id: cardId });
// If user has assigned-only permissions, check if they're assigned to this card
if (userId && card && card.boardId) {
const board = ReactiveCache.getBoard({ _id: card.boardId });
if (board && board.members) {
const member = _.findWhere(board.members, { userId: userId, isActive: true });
if (member && (member.isNormalAssignedOnly || member.isCommentAssignedOnly || member.isReadAssignedOnly)) {
// User with assigned-only permissions can only view cards assigned to them
if (!card.assignees || !card.assignees.includes(userId)) {
return this.ready(); // Don't publish if user is not assigned
}
}
}
}
this.cursor(
ReactiveCache.getCards(
{ _id: cardId },