From 3b72dd5c05b9100e7f7146a6d2dc99d2e333d315 Mon Sep 17 00:00:00 2001 From: Okba Allaoua Date: Sat, 7 Nov 2026 15:45:14 +0100 Subject: [PATCH] fix(cards): hide actions read-only users cannot run Copying a card only needs read access on the source board, so Move/copy stays available and only the move button is hidden. Signed-off-by: Okba Allaoua Assisted-by: ClaudeCode:claude-opus-5-5 --- src/CardMoveDialog.spec.js | 50 +++++++++++++++++++ src/CardMoveDialog.vue | 13 ++++- .../cards/NcActionsCardMenuEntries.spec.js | 48 ++++++++++++++++++ .../cards/NcActionsCardMenuEntries.vue | 10 ++-- 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 src/CardMoveDialog.spec.js create mode 100644 src/components/cards/NcActionsCardMenuEntries.spec.js diff --git a/src/CardMoveDialog.spec.js b/src/CardMoveDialog.spec.js new file mode 100644 index 0000000000..0782d1a6b1 --- /dev/null +++ b/src/CardMoveDialog.spec.js @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { describe, expect, it, jest } from '@jest/globals' +import CardMoveDialog from './CardMoveDialog.vue' + +jest.mock('@nextcloud/axios', () => ({}), { virtual: true }) +jest.mock('@nextcloud/event-bus', () => ({})) +jest.mock('@nextcloud/router', () => ({}), { virtual: true }) +jest.mock('@nextcloud/vue', () => ({}), { virtual: true }) +jest.mock('pinia', () => ({ + mapActions: () => ({}), + mapState: () => ({}), +})) +jest.mock('./stores/board.js', () => ({})) +jest.mock('./stores/card.js', () => ({})) +jest.mock('./stores/stack.js', () => ({})) + +const { activeBoards, canMoveCard } = CardMoveDialog.computed + +function context(permissions, card = { boardId: 1, stackId: 2 }) { + return { + card, + boardById: (id) => (id === 1 ? { permissions } : undefined), + stackById: (id) => (id === 2 ? { boardId: 1 } : undefined), + } +} + +describe('CardMoveDialog', () => { + it('allows moving a card off a board the user can edit', () => { + expect(canMoveCard.call(context({ PERMISSION_EDIT: true }))).toBe(true) + }) + + it('does not allow moving a card off a read-only board', () => { + expect(canMoveCard.call(context({ PERMISSION_EDIT: false }))).toBe(false) + }) + + it('falls back to the stack when the card has no board id', () => { + expect(canMoveCard.call(context({ PERMISSION_EDIT: true }, { stackId: 2 }))).toBe(true) + }) + + it('only offers boards the user can edit as a target', () => { + const board = (id, canEdit) => ({ id, deletedAt: 0, archived: false, permissions: { PERMISSION_EDIT: canEdit } }) + const boards = [board(1, true), board(2, false)] + + expect(activeBoards.call({ boards }).map(({ id }) => id)).toEqual([1]) + }) +}) diff --git a/src/CardMoveDialog.vue b/src/CardMoveDialog.vue index c7d3246203..01924a1708 100644 --- a/src/CardMoveDialog.vue +++ b/src/CardMoveDialog.vue @@ -25,7 +25,11 @@ label="title" />