Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cards): add card cloning ability #6452

Merged
merged 12 commits into from
Dec 19, 2024
Merged
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@
['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' => 'card#clone', 'url' => '/api/v{apiVersion}/cards/{cardId}/clone', 'verb' => 'POST'],

['name' => 'overview_api#upcomingCards', 'url' => '/api/v{apiVersion}/overview/upcoming', 'verb' => 'GET'],

['name' => 'search#search', 'url' => '/api/v{apiVersion}/search', 'verb' => 'GET'],
Expand Down
45 changes: 31 additions & 14 deletions cypress/e2e/cardFeatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ const useModal = (useModal) => {
})
}

describe('Card', function() {
describe('Card', function () {
let boardId
before(function() {
before(function () {
cy.createUser(user)
cy.login(user)
cy.createExampleBoard({
Expand All @@ -38,11 +38,11 @@ describe('Card', function() {
})
})

beforeEach(function() {
beforeEach(function () {
cy.login(user)
})

it('Can add a card', function() {
it('Can add a card', function () {
cy.visit(`/apps/deck/#/board/${boardId}`)
const newCardTitle = 'Write some cypress tests'

Expand All @@ -63,14 +63,18 @@ describe('Card', function() {
})
})

it('Create card from overview', function() {
it('Create card from overview', function () {
cy.visit(`/apps/deck/#/`)
const newCardTitle = 'Test create from overview'
cy.intercept({ method: 'POST', url: '**/apps/deck/cards' }).as('save')
cy.intercept({ method: 'GET', url: '**/apps/deck/boards/*' }).as('getBoard')

cy.get('.button-vue[aria-label*="Add card"]')
.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 .multiselect-board').should('be.visible').click()
cy.get('.vs__dropdown-menu [data-cy="board-select-title"]:contains("' + boardData.title + '")').should('be.visible').click()
Expand All @@ -91,14 +95,14 @@ describe('Card', function() {
})

describe('Modal', () => {
beforeEach(function() {
beforeEach(function () {
cy.login(user)
useModal(true).then(() => {
cy.visit(`/apps/deck/#/board/${boardId}`)
})
})

it('Can show card details modal', function() {
it('Can show card details modal', function () {
cy.getNavigationEntry(boardData.title)
.first().click({ force: true })

Expand All @@ -124,7 +128,7 @@ describe('Card', function() {
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.intercept({ method: 'PUT', url: '**/apps/deck/cards/*' }).as('save')
cy.get('.modal__card').should('be.visible')
Expand Down Expand Up @@ -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 button.button-vue--vue-primary').should('be.visible').click()
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.get('.board .stack').eq(0).within(() => {
cy.get(`.card:contains("${newCardTitle}")`).should('be.visible')
Expand All @@ -172,7 +176,7 @@ describe('Card', function() {
})

describe('Sidebar', () => {
beforeEach(function() {
beforeEach(function () {
cy.login(user)
useModal(false).then(() => {
cy.visit(`/apps/deck/#/board/${boardId}`)
Expand All @@ -185,7 +189,7 @@ describe('Card', function() {
.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'

cy.get('.button-vue[aria-label*="Add card"]')
Expand Down Expand Up @@ -223,7 +227,7 @@ describe('Card', function() {
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'

cy.get('.button-vue[aria-label*="Add card"]')
Expand Down Expand Up @@ -252,7 +256,7 @@ describe('Card', function() {
})

describe('Card actions', () => {
beforeEach(function() {
beforeEach(function () {
cy.login(user)
useModal(false).then(() => {
cy.visit(`/apps/deck/#/board/${boardId}`)
Expand Down Expand Up @@ -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)
})
})
})
9 changes: 9 additions & 0 deletions lib/Controller/CardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ public function create($title, $stackId, $type = 'plain', $order = 999, string $
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);
}
/**
* @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
Expand Down
112 changes: 54 additions & 58 deletions lib/Service/CardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,69 +37,33 @@
use Psr\Log\LoggerInterface;

class CardService {
private CardMapper $cardMapper;
private StackMapper $stackMapper;
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;

private string $currentUser;

public function __construct(
CardMapper $cardMapper,
StackMapper $stackMapper,
BoardMapper $boardMapper,
LabelMapper $labelMapper,
LabelService $labelService,
PermissionService $permissionService,
BoardService $boardService,
NotificationHelper $notificationHelper,
AssignmentMapper $assignedUsersMapper,
AttachmentService $attachmentService,
ActivityManager $activityManager,
ICommentsManager $commentsManager,
IUserManager $userManager,
ChangeHelper $changeHelper,
IEventDispatcher $eventDispatcher,
IURLGenerator $urlGenerator,
LoggerInterface $logger,
IRequest $request,
CardServiceValidator $cardServiceValidator,
private CardMapper $cardMapper,
private StackMapper $stackMapper,
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 ActivityManager $activityManager,
private ICommentsManager $commentsManager,
private IUserManager $userManager,
private ChangeHelper $changeHelper,
private IEventDispatcher $eventDispatcher,
private IURLGenerator $urlGenerator,
private LoggerInterface $logger,
private IRequest $request,
private CardServiceValidator $cardServiceValidator,
private AssignmentService $assignmentService,
grnd-alt marked this conversation as resolved.
Show resolved Hide resolved
?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->urlGenerator = $urlGenerator;
$this->logger = $logger;
$this->request = $request;
$this->cardServiceValidator = $cardServiceValidator;
}

public function enrichCards($cards) {
Expand Down Expand Up @@ -391,6 +355,38 @@ public function update($id, $title, $stackId, $type, $owner, $description = '',
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());
grnd-alt marked this conversation as resolved.
Show resolved Hide resolved
}
$newCard->setDescription($originCard->getDescription());
$card = $this->enrichCards([$this->cardMapper->update($newCard)]);
return $card[0];
}

/**
* @param $id
* @param $title
Expand Down
12 changes: 12 additions & 0 deletions lib/Service/LabelService.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ public function create($title, $color, $boardId) {
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
* @return \OCP\AppFramework\Db\Entity
Expand Down
Loading
Loading