Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/CardMoveDialog.spec.js
Original file line number Diff line number Diff line change
@@ -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])
})
})
13 changes: 11 additions & 2 deletions src/CardMoveDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
label="title" />
</div>
<template #actions>
<NcButton :disabled="!isBoardAndStackChoosen" variant="secondary" @click="moveCard">
<NcButton
v-if="canMoveCard"
:disabled="!isBoardAndStackChoosen"
variant="secondary"
@click="moveCard">
{{ t('deck', 'Move card') }}
</NcButton>
<NcButton :disabled="!isBoardAndStackChoosen" variant="primary" @click="cloneCard">
Expand Down Expand Up @@ -62,7 +66,12 @@ export default {
...mapState(useStackStore, ['stackById']),
...mapState(useBoardStore, ['boardById', 'boards']),
activeBoards() {
return this.boards.filter((item) => item.deletedAt === 0 && item.archived === false)
return this.boards.filter((item) => item.deletedAt === 0 && item.archived === false && item.permissions?.PERMISSION_EDIT)
},

canMoveCard() {
const boardId = this.card?.boardId ?? this.stackById(this.card?.stackId)?.boardId
return !!this.boardById(boardId)?.permissions?.PERMISSION_EDIT
},

isBoardAndStackChoosen() {
Expand Down
48 changes: 48 additions & 0 deletions src/components/cards/NcActionsCardMenuEntries.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 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 NcActionsCardMenuEntries from './NcActionsCardMenuEntries.vue'

jest.mock('@nextcloud/auth', () => ({}))
jest.mock('@nextcloud/dialogs', () => ({}), { 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('@nextcloud/dialogs/style.css', () => ({}), { virtual: true })
jest.mock('../../stores/actions.js', () => ({}))
jest.mock('../../stores/board.js', () => ({}))
jest.mock('../../stores/card.js', () => ({}))
jest.mock('../../stores/stack.js', () => ({}))
jest.mock('../../stores/trashbin.js', () => ({}))
jest.mock('vue-material-design-icons/AccountMinusOutline.vue', () => ({}))
jest.mock('vue-material-design-icons/AccountPlusOutline.vue', () => ({}))
jest.mock('vue-material-design-icons/ArchiveOutline.vue', () => ({}))
jest.mock('vue-material-design-icons/CardBulletedOutline.vue', () => ({}))
jest.mock('vue-material-design-icons/Check.vue', () => ({}))
jest.mock('vue-material-design-icons/Circle.vue', () => ({}))
jest.mock('vue-material-design-icons/Delete.vue', () => ({}))
jest.mock('vue-material-design-icons/OpenInNew.vue', () => ({}))
jest.mock('vue-material-design-icons/PencilOutline.vue', () => ({}))

const { canEdit } = NcActionsCardMenuEntries.computed

describe('NcActionsCardMenuEntries', () => {
it('allows editing a card on a board the user can edit', () => {
expect(canEdit.call({ card: { archived: false }, canEditBoard: true })).toBe(true)
})

it('does not allow editing a card on a read-only board', () => {
expect(canEdit.call({ card: { archived: false }, canEditBoard: false })).toBe(false)
})

it('does not allow editing an archived card', () => {
expect(canEdit.call({ card: { archived: true }, canEditBoard: true })).toBe(false)
})
})
10 changes: 7 additions & 3 deletions src/components/cards/NcActionsCardMenuEntries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

<template>
<div>
<NcColorPicker v-model="editingCardColor" clearable @submit="updateCardColor">
<NcColorPicker
v-if="canEdit"
v-model="editingCardColor"
clearable
@submit="updateCardColor">
<NcActionButton @click="openColorPicker">
<template #icon>
<SelectColor :fillColor="cardColor" :size="20" decorative />
Expand Down Expand Up @@ -54,7 +58,7 @@
{{ card.done ? t('deck', 'Mark as not done') : t('deck', 'Mark as done') }}
</NcActionButton>
<NcActionButton
v-if="canEdit"
v-if="!card.archived"
:closeAfterClick="true"
@click="openCardMoveDialog">
<template #icon>
Expand Down Expand Up @@ -157,7 +161,7 @@ export default {
}),

canEdit() {
return !this.card.archived
return !this.card.archived && this.canEditBoard
},

isInDoneColumn() {
Expand Down