mirror of
https://github.com/wekan/wekan.git
synced 2026-01-16 14:35:29 +01:00
Fix New Board Permissions: NormalAssignedOnly, CommentAssignedOnly, ReadOnly, ReadAssignedOnly. Part 1.
Thanks to nazim-oss and xet7 ! Related #6060
This commit is contained in:
parent
2f59e42024
commit
eabb6a239d
25 changed files with 562 additions and 291 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'}}"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue