Merge pull request #6452 from nextcloud/feat/1813-clonecopy-a-card
feat(cards): add card cloning ability
This commit is contained in:
@@ -137,6 +137,8 @@ return [
|
|||||||
['name' => 'comments_api#update', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'],
|
['name' => 'comments_api#update', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'PUT'],
|
||||||
['name' => 'comments_api#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'],
|
['name' => 'comments_api#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}/comments/{commentId}', 'verb' => 'DELETE'],
|
||||||
|
|
||||||
|
['name' => 'card#clone', 'url' => '/api/v{apiVersion}/cards/{cardId}/clone', 'verb' => 'POST'],
|
||||||
|
|
||||||
['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],
|
['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],
|
||||||
|
|
||||||
['name' => 'search#search', 'url' => '/api/v{apiVersion}/search', 'verb' => 'GET'],
|
['name' => 'search#search', 'url' => '/api/v{apiVersion}/search', 'verb' => 'GET'],
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ const useModal = (useModal) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Card', function() {
|
describe('Card', function () {
|
||||||
let boardId
|
let boardId
|
||||||
before(function() {
|
before(function () {
|
||||||
cy.createUser(user)
|
cy.createUser(user)
|
||||||
cy.login(user)
|
cy.login(user)
|
||||||
cy.createExampleBoard({
|
cy.createExampleBoard({
|
||||||
@@ -38,11 +38,11 @@ describe('Card', function() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login(user)
|
cy.login(user)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can add a card', function() {
|
it('Can add a card', function () {
|
||||||
cy.visit(`/apps/deck/#/board/${boardId}`)
|
cy.visit(`/apps/deck/#/board/${boardId}`)
|
||||||
const newCardTitle = 'Write some cypress tests'
|
const newCardTitle = 'Write some cypress tests'
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ describe('Card', function() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Create card from overview', function() {
|
it('Create card from overview', function () {
|
||||||
cy.visit(`/apps/deck/#/`)
|
cy.visit(`/apps/deck/#/`)
|
||||||
const newCardTitle = 'Test create from overview'
|
const newCardTitle = 'Test create from overview'
|
||||||
cy.intercept({ method: 'POST', url: '**/apps/deck/cards' }).as('save')
|
cy.intercept({ method: 'POST', url: '**/apps/deck/cards' }).as('save')
|
||||||
@@ -71,6 +71,10 @@ describe('Card', function() {
|
|||||||
|
|
||||||
cy.get('.button-vue[aria-label*="Add card"]')
|
cy.get('.button-vue[aria-label*="Add card"]')
|
||||||
.first().click()
|
.first().click()
|
||||||
|
|
||||||
|
// Somehow this avoids the electron crash
|
||||||
|
cy.wait(2000)
|
||||||
|
|
||||||
cy.get('.modal-mask.card-selector .card-title').should('be.visible').click().type(newCardTitle)
|
cy.get('.modal-mask.card-selector .card-title').should('be.visible').click().type(newCardTitle)
|
||||||
cy.get('.modal-mask.card-selector .multiselect-board').should('be.visible').click()
|
cy.get('.modal-mask.card-selector .multiselect-board').should('be.visible').click()
|
||||||
cy.get('.vs__dropdown-menu [data-cy="board-select-title"]:contains("' + boardData.title + '")').should('be.visible').click()
|
cy.get('.vs__dropdown-menu [data-cy="board-select-title"]:contains("' + boardData.title + '")').should('be.visible').click()
|
||||||
@@ -91,14 +95,14 @@ describe('Card', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Modal', () => {
|
describe('Modal', () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login(user)
|
cy.login(user)
|
||||||
useModal(true).then(() => {
|
useModal(true).then(() => {
|
||||||
cy.visit(`/apps/deck/#/board/${boardId}`)
|
cy.visit(`/apps/deck/#/board/${boardId}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can show card details modal', function() {
|
it('Can show card details modal', function () {
|
||||||
cy.getNavigationEntry(boardData.title)
|
cy.getNavigationEntry(boardData.title)
|
||||||
.first().click({ force: true })
|
.first().click({ force: true })
|
||||||
|
|
||||||
@@ -124,7 +128,7 @@ describe('Card', function() {
|
|||||||
cy.get('.attachment-list .basename').contains('welcome.txt')
|
cy.get('.attachment-list .basename').contains('welcome.txt')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.only('Shows the modal with the editor', () => {
|
it('Shows the modal with the editor', () => {
|
||||||
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
||||||
cy.intercept({ method: 'PUT', url: '**/apps/deck/cards/*' }).as('save')
|
cy.intercept({ method: 'PUT', url: '**/apps/deck/cards/*' }).as('save')
|
||||||
cy.get('.modal__card').should('be.visible')
|
cy.get('.modal__card').should('be.visible')
|
||||||
@@ -161,9 +165,9 @@ describe('Card', function() {
|
|||||||
cy.get('.reference-picker-modal--content .reference-picker .multiselect-list').should('be.visible').contains(boardData.stacks[0].title)
|
cy.get('.reference-picker-modal--content .reference-picker .multiselect-list').should('be.visible').contains(boardData.stacks[0].title)
|
||||||
cy.get('.reference-picker-modal--content .reference-picker button.button-vue--vue-primary').should('be.visible').click()
|
cy.get('.reference-picker-modal--content .reference-picker button.button-vue--vue-primary').should('be.visible').click()
|
||||||
cy.wait('@save', { timeout: 7000 })
|
cy.wait('@save', { timeout: 7000 })
|
||||||
cy.get('.modal__card .ProseMirror').contains('/index.php/apps/deck/card/').should('be.visible')
|
cy.get('.modal__card .ProseMirror').contains('/index.php/apps/deck/card/').should('have.length', 1)
|
||||||
|
|
||||||
cy.visit(`/apps/deck/#/board/${boardId}`)
|
cy.visit(`/apps/deck/board/${boardId}`)
|
||||||
cy.reload()
|
cy.reload()
|
||||||
cy.get('.board .stack').eq(0).within(() => {
|
cy.get('.board .stack').eq(0).within(() => {
|
||||||
cy.get(`.card:contains("${newCardTitle}")`).should('be.visible')
|
cy.get(`.card:contains("${newCardTitle}")`).should('be.visible')
|
||||||
@@ -172,7 +176,7 @@ describe('Card', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Sidebar', () => {
|
describe('Sidebar', () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login(user)
|
cy.login(user)
|
||||||
useModal(false).then(() => {
|
useModal(false).then(() => {
|
||||||
cy.visit(`/apps/deck/#/board/${boardId}`)
|
cy.visit(`/apps/deck/#/board/${boardId}`)
|
||||||
@@ -185,7 +189,7 @@ describe('Card', function() {
|
|||||||
.find('.ProseMirror h1').contains('Hello world writing more text').should('be.visible')
|
.find('.ProseMirror h1').contains('Hello world writing more text').should('be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Set a due date', function() {
|
it('Set a due date', function () {
|
||||||
const newCardTitle = 'Card with a due date'
|
const newCardTitle = 'Card with a due date'
|
||||||
|
|
||||||
cy.get('.button-vue[aria-label*="Add card"]')
|
cy.get('.button-vue[aria-label*="Add card"]')
|
||||||
@@ -223,7 +227,7 @@ describe('Card', function() {
|
|||||||
cy.get(`.card:contains("${newCardTitle}")`).find('[data-due-state]').should('not.exist')
|
cy.get(`.card:contains("${newCardTitle}")`).find('[data-due-state]').should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Add a label', function() {
|
it('Add a label', function () {
|
||||||
const newCardTitle = 'Card with labels'
|
const newCardTitle = 'Card with labels'
|
||||||
|
|
||||||
cy.get('.button-vue[aria-label*="Add card"]')
|
cy.get('.button-vue[aria-label*="Add card"]')
|
||||||
@@ -252,7 +256,7 @@ describe('Card', function() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Card actions', () => {
|
describe('Card actions', () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login(user)
|
cy.login(user)
|
||||||
useModal(false).then(() => {
|
useModal(false).then(() => {
|
||||||
cy.visit(`/apps/deck/#/board/${boardId}`)
|
cy.visit(`/apps/deck/#/board/${boardId}`)
|
||||||
@@ -298,5 +302,18 @@ describe('Card', function() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('clone card', () => {
|
||||||
|
cy.intercept({ method: 'POST', url: '**/apps/deck/**/cards/*/clone' }).as('clone')
|
||||||
|
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
||||||
|
cy.get('#app-sidebar-vue')
|
||||||
|
.find('.ProseMirror h1').contains('Hello world').should('be.visible')
|
||||||
|
|
||||||
|
cy.get('.app-sidebar-header .action-item__menutoggle').click()
|
||||||
|
cy.get('.v-popper__popper button:contains("Move/copy card")').click()
|
||||||
|
cy.get('.modal-container button:contains("Copy card")').click()
|
||||||
|
cy.wait('@clone', { timeout: 7000 })
|
||||||
|
cy.get('.card:contains("Hello world")').should('have.length', 2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ class CardController extends Controller {
|
|||||||
public function update($id, $title, $stackId, $type, $order, $description, $duedate, $deletedAt) {
|
public function update($id, $title, $stackId, $type, $order, $description, $duedate, $deletedAt) {
|
||||||
return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt);
|
return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @param $cardId
|
||||||
|
* @param $targetStackId
|
||||||
|
* @return \OCP\AppFramework\Db\Entity
|
||||||
|
*/
|
||||||
|
public function clone(int $cardId, ?int $targetStackId = null) {
|
||||||
|
return $this->cardService->cloneCard($cardId, $targetStackId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
|
|||||||
@@ -37,69 +37,33 @@ use OCP\IUserManager;
|
|||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class CardService {
|
class CardService {
|
||||||
private CardMapper $cardMapper;
|
|
||||||
private StackMapper $stackMapper;
|
private string $currentUser;
|
||||||
private BoardMapper $boardMapper;
|
|
||||||
private LabelMapper $labelMapper;
|
|
||||||
private LabelService $labelService;
|
|
||||||
private PermissionService $permissionService;
|
|
||||||
private BoardService $boardService;
|
|
||||||
private NotificationHelper $notificationHelper;
|
|
||||||
private AssignmentMapper $assignedUsersMapper;
|
|
||||||
private AttachmentService $attachmentService;
|
|
||||||
private ?string $currentUser;
|
|
||||||
private ActivityManager $activityManager;
|
|
||||||
private ICommentsManager $commentsManager;
|
|
||||||
private ChangeHelper $changeHelper;
|
|
||||||
private IEventDispatcher $eventDispatcher;
|
|
||||||
private IUserManager $userManager;
|
|
||||||
private IURLGenerator $urlGenerator;
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
private IRequest $request;
|
|
||||||
private CardServiceValidator $cardServiceValidator;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
CardMapper $cardMapper,
|
private CardMapper $cardMapper,
|
||||||
StackMapper $stackMapper,
|
private StackMapper $stackMapper,
|
||||||
BoardMapper $boardMapper,
|
private BoardMapper $boardMapper,
|
||||||
LabelMapper $labelMapper,
|
private LabelMapper $labelMapper,
|
||||||
LabelService $labelService,
|
private LabelService $labelService,
|
||||||
PermissionService $permissionService,
|
private PermissionService $permissionService,
|
||||||
BoardService $boardService,
|
private BoardService $boardService,
|
||||||
NotificationHelper $notificationHelper,
|
private NotificationHelper $notificationHelper,
|
||||||
AssignmentMapper $assignedUsersMapper,
|
private AssignmentMapper $assignedUsersMapper,
|
||||||
AttachmentService $attachmentService,
|
private AttachmentService $attachmentService,
|
||||||
ActivityManager $activityManager,
|
private ActivityManager $activityManager,
|
||||||
ICommentsManager $commentsManager,
|
private ICommentsManager $commentsManager,
|
||||||
IUserManager $userManager,
|
private IUserManager $userManager,
|
||||||
ChangeHelper $changeHelper,
|
private ChangeHelper $changeHelper,
|
||||||
IEventDispatcher $eventDispatcher,
|
private IEventDispatcher $eventDispatcher,
|
||||||
IURLGenerator $urlGenerator,
|
private IURLGenerator $urlGenerator,
|
||||||
LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
IRequest $request,
|
private IRequest $request,
|
||||||
CardServiceValidator $cardServiceValidator,
|
private CardServiceValidator $cardServiceValidator,
|
||||||
|
private AssignmentService $assignmentService,
|
||||||
?string $userId,
|
?string $userId,
|
||||||
) {
|
) {
|
||||||
$this->cardMapper = $cardMapper;
|
|
||||||
$this->stackMapper = $stackMapper;
|
|
||||||
$this->boardMapper = $boardMapper;
|
|
||||||
$this->labelMapper = $labelMapper;
|
|
||||||
$this->labelService = $labelService;
|
|
||||||
$this->permissionService = $permissionService;
|
|
||||||
$this->boardService = $boardService;
|
|
||||||
$this->notificationHelper = $notificationHelper;
|
|
||||||
$this->assignedUsersMapper = $assignedUsersMapper;
|
|
||||||
$this->attachmentService = $attachmentService;
|
|
||||||
$this->activityManager = $activityManager;
|
|
||||||
$this->commentsManager = $commentsManager;
|
|
||||||
$this->userManager = $userManager;
|
|
||||||
$this->changeHelper = $changeHelper;
|
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
|
||||||
$this->currentUser = $userId;
|
$this->currentUser = $userId;
|
||||||
$this->urlGenerator = $urlGenerator;
|
|
||||||
$this->logger = $logger;
|
|
||||||
$this->request = $request;
|
|
||||||
$this->cardServiceValidator = $cardServiceValidator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enrichCards($cards) {
|
public function enrichCards($cards) {
|
||||||
@@ -391,6 +355,38 @@ class CardService {
|
|||||||
return $card;
|
return $card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cloneCard(int $id, ?int $targetStackId = null):Card {
|
||||||
|
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_READ);
|
||||||
|
$originCard = $this->cardMapper->find($id);
|
||||||
|
if ($targetStackId === null) {
|
||||||
|
$targetStackId = $originCard->getStackId();
|
||||||
|
}
|
||||||
|
$this->permissionService->checkPermission($this->stackMapper, $targetStackId, Acl::PERMISSION_EDIT);
|
||||||
|
$newCard = $this->create($originCard->getTitle(), $targetStackId, $originCard->getType(), $originCard->getOrder(), $originCard->getOwner());
|
||||||
|
$boardId = $this->stackMapper->findBoardId($targetStackId);
|
||||||
|
foreach ($this->labelMapper->findAssignedLabelsForCard($id) as $label) {
|
||||||
|
if ($boardId != $this->stackMapper->findBoardId($originCard->getStackId())) {
|
||||||
|
try {
|
||||||
|
$label = $this->labelService->cloneLabelIfNotExists($label->getId(), $boardId);
|
||||||
|
} catch (NoPermissionException $e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assignLabel($newCard->getId(), $label->getId());
|
||||||
|
}
|
||||||
|
foreach ($this->assignedUsersMapper->findAll($id) as $assignement) {
|
||||||
|
try {
|
||||||
|
$this->permissionService->checkPermission($this->cardMapper, $newCard->getId(), Acl::PERMISSION_READ, $assignement->getParticipant());
|
||||||
|
} catch (NoPermissionException $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->assignmentService->assignUser($newCard->getId(), $assignement->getParticipant());
|
||||||
|
}
|
||||||
|
$newCard->setDescription($originCard->getDescription());
|
||||||
|
$card = $this->enrichCards([$this->cardMapper->update($newCard)]);
|
||||||
|
return $card[0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $id
|
* @param $id
|
||||||
* @param $title
|
* @param $title
|
||||||
|
|||||||
@@ -93,6 +93,18 @@ class LabelService {
|
|||||||
return $this->labelMapper->insert($label);
|
return $this->labelMapper->insert($label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cloneLabelIfNotExists(int $labelId, int $targetBoardId): Label {
|
||||||
|
$this->permissionService->checkPermission(null, $targetBoardId, Acl::PERMISSION_MANAGE);
|
||||||
|
$boardLabels = $this->boardService->find($targetBoardId)->getLabels();
|
||||||
|
$originLabel = $this->find($labelId);
|
||||||
|
$filteredValues = array_values(array_filter($boardLabels, fn ($item) => $item->getTitle() === $originLabel->getTitle()));
|
||||||
|
if (empty($filteredValues)) {
|
||||||
|
$label = $this->create($originLabel->getTitle(), $originLabel->getColor(), $targetBoardId);
|
||||||
|
return $label;
|
||||||
|
}
|
||||||
|
return $originLabel;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $id
|
* @param $id
|
||||||
* @return \OCP\AppFramework\Db\Entity
|
* @return \OCP\AppFramework\Db\Entity
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<NcModal v-if="modalShow" :title="t('deck', 'Move card to another board')" @close="modalShow=false">
|
<NcDialog :open.sync="modalShow" :name="t('deck', 'Move/copy card')">
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
<h3>{{ t('deck', 'Move card to another board') }}</h3>
|
|
||||||
<NcSelect v-model="selectedBoard"
|
<NcSelect v-model="selectedBoard"
|
||||||
:input-label="t('deck', 'Select a board')"
|
:input-label="t('deck', 'Select a board')"
|
||||||
:placeholder="t('deck', 'Select a board')"
|
:placeholder="t('deck', 'Select a board')"
|
||||||
@@ -20,26 +19,28 @@
|
|||||||
:options="stacksFromBoard"
|
:options="stacksFromBoard"
|
||||||
:max-height="100"
|
:max-height="100"
|
||||||
label="title" />
|
label="title" />
|
||||||
|
|
||||||
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="moveCard">
|
|
||||||
{{ t('deck', 'Move card') }}
|
|
||||||
</button>
|
|
||||||
<button @click="modalShow=false">
|
|
||||||
{{ t('deck', 'Cancel') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</NcModal>
|
<template #actions>
|
||||||
|
<NcButton :disabled="!isBoardAndStackChoosen" type="secondary" @click="moveCard">
|
||||||
|
{{ t('deck', 'Move card') }}
|
||||||
|
</NcButton>
|
||||||
|
<NcButton :disabled="!isBoardAndStackChoosen" type="primary" @click="cloneCard">
|
||||||
|
{{ t('deck', 'Copy card') }}
|
||||||
|
</NcButton>
|
||||||
|
</template>
|
||||||
|
</NcDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { NcModal, NcSelect } from '@nextcloud/vue'
|
import { NcDialog, NcSelect, NcButton } from '@nextcloud/vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardMoveDialog',
|
name: 'CardMoveDialog',
|
||||||
components: { NcModal, NcSelect },
|
components: { NcDialog, NcSelect, NcButton },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
card: null,
|
card: null,
|
||||||
@@ -50,6 +51,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters(['stackById', 'boardById']),
|
||||||
activeBoards() {
|
activeBoards() {
|
||||||
return this.$store.getters.boards.filter((item) => item.deletedAt === 0 && item.archived === false)
|
return this.$store.getters.boards.filter((item) => item.deletedAt === 0 && item.archived === false)
|
||||||
},
|
},
|
||||||
@@ -66,6 +68,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
openModal(card) {
|
openModal(card) {
|
||||||
this.card = card
|
this.card = card
|
||||||
|
this.selectedStack = this.stackById(this.card.stackId)
|
||||||
|
this.selectedBoard = this.boardById(this.selectedStack.boardId)
|
||||||
|
this.loadStacksFromBoard(this.selectedBoard)
|
||||||
this.modalShow = true
|
this.modalShow = true
|
||||||
},
|
},
|
||||||
async loadStacksFromBoard(board) {
|
async loadStacksFromBoard(board) {
|
||||||
@@ -81,33 +86,23 @@ export default {
|
|||||||
this.copiedCard = Object.assign({}, this.card)
|
this.copiedCard = Object.assign({}, this.card)
|
||||||
this.copiedCard.stackId = this.selectedStack.id
|
this.copiedCard.stackId = this.selectedStack.id
|
||||||
this.$store.dispatch('moveCard', this.copiedCard)
|
this.$store.dispatch('moveCard', this.copiedCard)
|
||||||
if (parseInt(this.boardId) === parseInt(this.selectedStack.boardId)) {
|
if (parseInt(this.selectedBoard.id) === parseInt(this.selectedStack.boardId)) {
|
||||||
await this.$store.commit('addNewCard', { ...this.copiedCard })
|
await this.$store.commit('addNewCard', { ...this.copiedCard })
|
||||||
}
|
}
|
||||||
this.modalShow = false
|
this.modalShow = false
|
||||||
},
|
},
|
||||||
|
async cloneCard() {
|
||||||
|
this.$store.dispatch('cloneCard', { cardId: this.card.id, targetStackId: this.selectedStack.id })
|
||||||
|
this.modalShow = false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.modal__content {
|
.modal__content {
|
||||||
min-width: 250px;
|
|
||||||
min-height: 120px;
|
|
||||||
margin: 20px 20px 100px 20px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__content button {
|
|
||||||
float: right;
|
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
icon="icon-external"
|
icon="icon-external"
|
||||||
:close-after-click="true"
|
:close-after-click="true"
|
||||||
@click="openCardMoveDialog">
|
@click="openCardMoveDialog">
|
||||||
{{ t('deck', 'Move card') }}
|
{{ t('deck', 'Move/copy card') }}
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
<NcActionButton v-for="action in cardActions"
|
<NcActionButton v-for="action in cardActions"
|
||||||
:key="action.label"
|
:key="action.label"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
||||||
|
|
||||||
export class CardApi {
|
export class CardApi {
|
||||||
|
|
||||||
@@ -28,6 +28,23 @@ export class CardApi {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cloneCard(cardId, targetStackId) {
|
||||||
|
return axios.post(generateOcsUrl(`apps/deck/api/v1.0/cards/${cardId}/clone`), {
|
||||||
|
targetStackId,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(response) => {
|
||||||
|
return Promise.resolve(response.data)
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
deleteCard(cardId) {
|
deleteCard(cardId) {
|
||||||
return axios.delete(this.url(`/cards/${cardId}`))
|
return axios.delete(this.url(`/cards/${cardId}`))
|
||||||
.then(
|
.then(
|
||||||
|
|||||||
@@ -272,6 +272,11 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
async cloneCard({ commit }, { cardId, targetStackId }) {
|
||||||
|
const createdCard = await apiClient.cloneCard(cardId, targetStackId)
|
||||||
|
commit('addCard', createdCard)
|
||||||
|
return createdCard
|
||||||
|
},
|
||||||
async addCard({ commit }, card) {
|
async addCard({ commit }, card) {
|
||||||
const createdCard = await apiClient.addCard(card)
|
const createdCard = await apiClient.addCard(card)
|
||||||
commit('addCard', createdCard)
|
commit('addCard', createdCard)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ use OCA\Deck\Db\BoardMapper;
|
|||||||
use OCA\Deck\Db\Card;
|
use OCA\Deck\Db\Card;
|
||||||
use OCA\Deck\Db\CardMapper;
|
use OCA\Deck\Db\CardMapper;
|
||||||
use OCA\Deck\Db\ChangeHelper;
|
use OCA\Deck\Db\ChangeHelper;
|
||||||
|
use OCA\Deck\Db\Label;
|
||||||
use OCA\Deck\Db\LabelMapper;
|
use OCA\Deck\Db\LabelMapper;
|
||||||
use OCA\Deck\Db\Stack;
|
use OCA\Deck\Db\Stack;
|
||||||
use OCA\Deck\Db\StackMapper;
|
use OCA\Deck\Db\StackMapper;
|
||||||
@@ -93,6 +94,9 @@ class CardServiceTest extends TestCase {
|
|||||||
/** @var CardServiceValidator|MockObject */
|
/** @var CardServiceValidator|MockObject */
|
||||||
private $cardServiceValidator;
|
private $cardServiceValidator;
|
||||||
|
|
||||||
|
/** @var AssignmentService|MockObject */
|
||||||
|
private $assignmentService;
|
||||||
|
|
||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
$this->cardMapper = $this->createMock(CardMapper::class);
|
$this->cardMapper = $this->createMock(CardMapper::class);
|
||||||
@@ -114,6 +118,7 @@ class CardServiceTest extends TestCase {
|
|||||||
$this->logger = $this->createMock(LoggerInterface::class);
|
$this->logger = $this->createMock(LoggerInterface::class);
|
||||||
$this->request = $this->createMock(IRequest::class);
|
$this->request = $this->createMock(IRequest::class);
|
||||||
$this->cardServiceValidator = $this->createMock(CardServiceValidator::class);
|
$this->cardServiceValidator = $this->createMock(CardServiceValidator::class);
|
||||||
|
$this->assignmentService = $this->createMock(AssignmentService::class);
|
||||||
|
|
||||||
$this->logger->expects($this->any())->method('error');
|
$this->logger->expects($this->any())->method('error');
|
||||||
|
|
||||||
@@ -137,6 +142,7 @@ class CardServiceTest extends TestCase {
|
|||||||
$this->logger,
|
$this->logger,
|
||||||
$this->request,
|
$this->request,
|
||||||
$this->cardServiceValidator,
|
$this->cardServiceValidator,
|
||||||
|
$this->assignmentService,
|
||||||
'user1'
|
'user1'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -219,6 +225,61 @@ class CardServiceTest extends TestCase {
|
|||||||
$this->assertEquals($b->getStackId(), 123);
|
$this->assertEquals($b->getStackId(), 123);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testClone() {
|
||||||
|
$card = new Card();
|
||||||
|
$card->setId(1);
|
||||||
|
$card->setTitle('Card title');
|
||||||
|
$card->setOwner('admin');
|
||||||
|
$card->setStackId(12345);
|
||||||
|
$clonedCard = clone $card;
|
||||||
|
$clonedCard->setId(2);
|
||||||
|
$clonedCard->setStackId(1234);
|
||||||
|
$this->cardMapper->expects($this->exactly(2))
|
||||||
|
->method('insert')
|
||||||
|
->willReturn($card, $clonedCard);
|
||||||
|
|
||||||
|
$this->cardMapper->expects($this->once())
|
||||||
|
->method('update')->willReturn($clonedCard);
|
||||||
|
$this->cardMapper->expects($this->exactly(2))
|
||||||
|
->method('find')
|
||||||
|
->willReturn($card, $clonedCard);
|
||||||
|
|
||||||
|
// check if users are assigned
|
||||||
|
$this->assignmentService->expects($this->once())
|
||||||
|
->method('assignUser')
|
||||||
|
->with(2, 'admin');
|
||||||
|
$a1 = new Assignment();
|
||||||
|
$a1->setCardId(2);
|
||||||
|
$a1->setType(0);
|
||||||
|
$a1->setParticipant('admin');
|
||||||
|
$this->assignedUsersMapper->expects($this->once())
|
||||||
|
->method('findAll')
|
||||||
|
->with(1)
|
||||||
|
->willReturn([$a1]);
|
||||||
|
|
||||||
|
// check if labels get cloned
|
||||||
|
$label = new Label();
|
||||||
|
$label->setId(1);
|
||||||
|
$this->labelMapper->expects($this->once())
|
||||||
|
->method('findAssignedLabelsForCard')
|
||||||
|
->willReturn([$label]);
|
||||||
|
$this->cardMapper->expects($this->once())
|
||||||
|
->method('assignLabel')
|
||||||
|
->with($clonedCard->getId(), $label->getId())
|
||||||
|
->willReturn($label);
|
||||||
|
|
||||||
|
$stackMock = new Stack();
|
||||||
|
$stackMock->setBoardId(1234);
|
||||||
|
$this->stackMapper->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->willReturn($stackMock);
|
||||||
|
$b = $this->cardService->create('Card title', 123, 'text', 999, 'admin');
|
||||||
|
$c = $this->cardService->cloneCard($b->getId(), 1234);
|
||||||
|
$this->assertEquals($b->getTitle(), $c->getTitle());
|
||||||
|
$this->assertEquals($b->getOwner(), $c->getOwner());
|
||||||
|
$this->assertNotEquals($b->getStackId(), $c->getStackId());
|
||||||
|
}
|
||||||
|
|
||||||
public function testDelete() {
|
public function testDelete() {
|
||||||
$cardToBeDeleted = new Card();
|
$cardToBeDeleted = new Card();
|
||||||
$this->cardMapper->expects($this->once())
|
$this->cardMapper->expects($this->once())
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
namespace OCA\Deck\Service;
|
namespace OCA\Deck\Service;
|
||||||
|
|
||||||
|
use OCA\Deck\Db\Board;
|
||||||
use OCA\Deck\Db\ChangeHelper;
|
use OCA\Deck\Db\ChangeHelper;
|
||||||
use OCA\Deck\Db\Label;
|
use OCA\Deck\Db\Label;
|
||||||
use OCA\Deck\Db\LabelMapper;
|
use OCA\Deck\Db\LabelMapper;
|
||||||
@@ -105,6 +106,53 @@ class LabelServiceTest extends TestCase {
|
|||||||
$this->assertEquals($b->getColor(), 'ffffff');
|
$this->assertEquals($b->getColor(), 'ffffff');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCloneLabelIfNotExists() {
|
||||||
|
$label = new Label();
|
||||||
|
$label->setId(1);
|
||||||
|
$label->setTitle('title');
|
||||||
|
$label->setColor('00ff00');
|
||||||
|
$this->labelMapper->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->willReturn($label);
|
||||||
|
|
||||||
|
$expectedLabel = new Label();
|
||||||
|
$expectedLabel->setTitle('title');
|
||||||
|
$expectedLabel->setColor('00ff00');
|
||||||
|
$expectedLabel->setBoardId(1);
|
||||||
|
$this->labelMapper->expects($this->once())
|
||||||
|
->method('insert')
|
||||||
|
->with($expectedLabel)
|
||||||
|
->willReturn($label);
|
||||||
|
$board = new Board();
|
||||||
|
$board->setLabels([]);
|
||||||
|
$this->boardService->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->willReturn($board);
|
||||||
|
|
||||||
|
$this->labelService->cloneLabelIfNotExists(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCloneLabelIfExists() {
|
||||||
|
$label = new Label();
|
||||||
|
$label->setId(1);
|
||||||
|
$label->setTitle('title');
|
||||||
|
$label->setColor('00ff00');
|
||||||
|
$this->labelMapper->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->willReturn($label);
|
||||||
|
$this->labelMapper->expects($this->never())
|
||||||
|
->method('insert')
|
||||||
|
->with($label);
|
||||||
|
$board = new Board();
|
||||||
|
$board->setLabels([$label]);
|
||||||
|
$this->boardService->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->willReturn($board);
|
||||||
|
|
||||||
|
$this->labelService->cloneLabelIfNotExists(1, 1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public function testDelete() {
|
public function testDelete() {
|
||||||
$label = new Label();
|
$label = new Label();
|
||||||
$label->setId(1);
|
$label->setId(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user