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

Clone cards together with the board #3430

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
19 changes: 19 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,25 @@ A 403 response might be returned if the users ability to create new boards has b

##### 200 Success

### POST /boards/{boardId}/clone - Clone a board
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to have this available through the API the required routes would also need to be handled for the BoardApiController similar to https://github.com/nextcloud/deck/pull/3430/files#diff-f126d1440653a8583122ad9fa4e49497074f2eabe1c511d76288a547ab8f1575


Creates a copy of the board.

#### Request body

| Parameter | Type | Description |
| --------- | ------ | ---------------------------------------------------- |
| withCards | Bool | Setting if the cards should be copied (Default: false) |
| withAssignments | Bool | Setting if the card assignments should be cloned (Default: false) |
| withLabels | Bool | Setting if the card labels should be cloned (Default: false) |
| withDueDate | Bool | Setting if the card due dates should be cloned (Default: false) |
| moveCardsToLeftStack | Bool | Setting if all cards should be moved to the most left column (useful for To-Do / Doing / Done boards) (Default: false) |
| restoreArchivedCards | Bool | Setting if the archived cards should be unarchived (Default: false) |

#### Response

##### 200 Success

### DELETE /boards/{boardId}/acl/{aclId} - Delete an acl rule

#### Response
Expand Down
8 changes: 4 additions & 4 deletions lib/Controller/BoardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ public function deleteAcl($aclId) {

/**
* @NoAdminRequired
* @param $boardId
* @return Board
*/
public function clone($boardId) {
return $this->boardService->clone($boardId, $this->userId);
public function clone(int $boardId, bool $withCards = false, bool $withAssignments = false, bool $withLabels = false, bool $withDueDate = false, bool $moveCardsToLeftStack = false, bool $restoreArchivedCards = false): DataResponse {
return new DataResponse(
$this->boardService->clone($boardId, $this->userId, $withCards, $withAssignments, $withLabels, $withDueDate, $moveCardsToLeftStack, $restoreArchivedCards)
);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions lib/Db/Label.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

namespace OCA\Deck\Db;

/**
* @method getTitle(): string
*/
class Label extends RelationalEntity {
protected $title;
protected $color;
Expand Down
2 changes: 0 additions & 2 deletions lib/Service/AssignmentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ public function __construct(
$this->changeHelper = $changeHelper;
$this->activityManager = $activityManager;
$this->eventDispatcher = $eventDispatcher;

$this->assignmentServiceValidator->check(compact('userId'));
$this->currentUser = $userId;
}

Expand Down
187 changes: 124 additions & 63 deletions lib/Service/BoardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\IPermissionMapper;
Expand All @@ -28,6 +29,7 @@
use OCA\Deck\Event\AclDeletedEvent;
use OCA\Deck\Event\AclUpdatedEvent;
use OCA\Deck\Event\BoardUpdatedEvent;
use OCA\Deck\Event\CardCreatedEvent;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Notification\NotificationHelper;
use OCA\Deck\Validators\BoardServiceValidator;
Expand All @@ -37,80 +39,37 @@
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Server;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

class BoardService {
private BoardMapper $boardMapper;
private StackMapper $stackMapper;
private LabelMapper $labelMapper;
private AclMapper $aclMapper;
private IConfig $config;
private IL10N $l10n;
private PermissionService $permissionService;
private NotificationHelper $notificationHelper;
private AssignmentMapper $assignedUsersMapper;
private IUserManager $userManager;
private IGroupManager $groupManager;
private ?string $userId;
private ActivityManager $activityManager;
private IEventDispatcher $eventDispatcher;
private ChangeHelper $changeHelper;
private CardMapper $cardMapper;
private ?array $boardsCacheFull = null;
private ?array $boardsCachePartial = null;
private IURLGenerator $urlGenerator;
private IDBConnection $connection;
private BoardServiceValidator $boardServiceValidator;
private SessionMapper $sessionMapper;

public function __construct(
BoardMapper $boardMapper,
StackMapper $stackMapper,
CardMapper $cardMapper,
IConfig $config,
IL10N $l10n,
LabelMapper $labelMapper,
AclMapper $aclMapper,
PermissionService $permissionService,
NotificationHelper $notificationHelper,
AssignmentMapper $assignedUsersMapper,
IUserManager $userManager,
IGroupManager $groupManager,
ActivityManager $activityManager,
IEventDispatcher $eventDispatcher,
ChangeHelper $changeHelper,
IURLGenerator $urlGenerator,
IDBConnection $connection,
BoardServiceValidator $boardServiceValidator,
SessionMapper $sessionMapper,
?string $userId
private BoardMapper $boardMapper,
private StackMapper $stackMapper,
private CardMapper $cardMapper,
private IConfig $config,
private IL10N $l10n,
private LabelMapper $labelMapper,
private AclMapper $aclMapper,
private PermissionService $permissionService,
private AssignmentService $assignmentService,
private NotificationHelper $notificationHelper,
private AssignmentMapper $assignedUsersMapper,
private ActivityManager $activityManager,
private IEventDispatcher $eventDispatcher,
private ChangeHelper $changeHelper,
private IURLGenerator $urlGenerator,
private IDBConnection $connection,
private BoardServiceValidator $boardServiceValidator,
private SessionMapper $sessionMapper,
private ?string $userId
) {
$this->boardMapper = $boardMapper;
$this->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper;
$this->labelMapper = $labelMapper;
$this->config = $config;
$this->aclMapper = $aclMapper;
$this->l10n = $l10n;
$this->permissionService = $permissionService;
$this->notificationHelper = $notificationHelper;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->activityManager = $activityManager;
$this->eventDispatcher = $eventDispatcher;
$this->changeHelper = $changeHelper;
$this->userId = $userId;
$this->urlGenerator = $urlGenerator;
$this->connection = $connection;
$this->boardServiceValidator = $boardServiceValidator;
$this->sessionMapper = $sessionMapper;
}

/**
Expand Down Expand Up @@ -520,13 +479,19 @@ public function deleteAcl(int $id): ?Acl {
/**
* @param $id
* @param $userId
* @param $withCards
* @param $withAssignments
* @param $withLabels
* @param $withDueDate
* @param $moveCardsToLeftStack
* @param $restoreArchivedCards
* @return Board
* @throws DoesNotExistException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function clone($id, $userId) {
public function clone($id, $userId, $withCards = false, $withAssignments = false, $withLabels = false, $withDueDate = false, $moveCardsToLeftStack = false, $restoreArchivedCards = false) {
grnd-alt marked this conversation as resolved.
Show resolved Hide resolved
$this->boardServiceValidator->check(compact('id', 'userId'));

if (!$this->permissionService->canCreate()) {
Expand All @@ -549,6 +514,16 @@ public function clone($id, $userId) {
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can separate the board cloning logics in a different class - The BoardMapper or the Service.

$this->boardMapper->insert($newBoard);

foreach ($this->aclMapper->findAll($board->getId()) as $acl) {
$this->addAcl($newBoard->getId(),
$acl->getType(),
$acl->getParticipant(),
$acl->getPermissionEdit(),
$acl->getPermissionShare(),
$acl->getPermissionManage());
}


$labels = $this->labelMapper->findAll($id);
foreach ($labels as $label) {
$newLabel = new Label();
Expand All @@ -566,6 +541,10 @@ public function clone($id, $userId) {
$this->stackMapper->insert($newStack);
}

if ($withCards) {
$this->cloneCards($board, $newBoard, $withAssignments, $withLabels, $withDueDate, $moveCardsToLeftStack, $restoreArchivedCards);
}

return $this->find($newBoard->getId());
}

Expand Down Expand Up @@ -669,6 +648,88 @@ private function enrichBoards(array $boards, bool $fullDetails = true): array {
return $boards;
}

private function cloneCards(Board $board, Board $newBoard, bool $withAssignments = false, bool $withLabels = false, bool $withDueDate = false, bool $moveCardsToLeftStack = false, bool $restoreArchivedCards = false): void {
// TODO: Undelete cards
// TODO: Copy attachments (or not?)
// TODO: Copy comments (or not?)
// TODO: Move to specific column
Comment on lines +652 to +655
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still valid?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove those from the code and track as enhancement follow up issues.


$stacks = $this->stackMapper->findAll($board->getId());
usort($stacks, function (Stack $a, Stack $b) {
return $a->getOrder() - $b->getOrder();
});

$newStacks = $this->stackMapper->findAll($newBoard->getId());
usort($newStacks, function (Stack $a, Stack $b) {
return $a->getOrder() - $b->getOrder();
});

$i = 0;
foreach ($stacks as $stack) {
$cards = $this->cardMapper->findAll($stack->getId());
$archivedCards = $this->cardMapper->findAllArchived($stack->getId());

/** @var Card[] $cards */
$cards = array_merge($cards, $archivedCards);

foreach ($cards as $card) {
$targetStackId = $moveCardsToLeftStack ? $newStacks[0]->getId() : $newStacks[$i]->getId();

// Create a cloned card.
// Done with setters as only fields set via setters get written to db
$newCard = new Card();
juliusknorr marked this conversation as resolved.
Show resolved Hide resolved
$newCard->setTitle($card->getTitle());
$newCard->setDescription($card->getDescription());
$newCard->setStackId($targetStackId);
$newCard->setType($card->getType());
$newCard->setOwner($card->getOwner());
$newCard->setOrder($card->getOrder());
$newCard->setDuedate($withDueDate ? $card->getDuedate() : null);
$newCard->setArchived($restoreArchivedCards ? false : $card->getArchived());
$newCard->setStackId($targetStackId);

// Persist the cloned card.
$newCard = $this->cardMapper->insert($newCard);


// Copy labels.
if ($withLabels) {
$labels = $this->labelMapper->findAssignedLabelsForCard($card->getId());
$newLabels = $this->labelMapper->findAll($newBoard->getId());
$newLabelTitles = [];
foreach ($newLabels as $label) {
$newLabelTitles[$label->getTitle()] = $label;
}

foreach ($labels as $label) {
$newLabelId = $newLabelTitles[$label->getTitle()]?->getId() ?? null;
if ($newLabelId) {
$this->cardMapper->assignLabel($newCard->getId(), $newLabelId);
}
}
}


// Copy assignments.
if ($withAssignments) {
$assignments = $this->assignedUsersMapper->findAll($card->getId());

foreach ($assignments as $assignment) {
$this->assignmentService->assignUser($newCard->getId(), $assignment->getParticipant(), $assignment->getType());
}
}


// Copied from CardService because CardService cannot be injected due to cyclic dependencies.
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE);
$this->changeHelper->cardChanged($card->getId(), false);
$this->eventDispatcher->dispatchTyped(new CardCreatedEvent($card));
Comment on lines +724 to +726
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$card should be replaced by $newCard

}

$i++;
}
}

private function enrichWithStacks($board, $since = -1) {
$stacks = $this->stackMapper->findAll($board->getId(), null, null, $since);

Expand Down
30 changes: 28 additions & 2 deletions src/components/navigation/AppNavigationBoard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
:undo="deleted"
:menu-placement="'auto'"
@undo="unDelete">
<NcAppNavigationIconBullet slot="icon" :color="board.color" />
<template #icon>
<NcAppNavigationIconBullet :color="board.color" />
<BoardCloneModal v-if="cloneModalOpen" :board-title="board.title" @close="onCloseCloneModal" />
</template>

<template #counter>
<AccountIcon v-if="board.acl.length > 0" />
Expand All @@ -31,7 +34,7 @@
</NcActionButton>
<NcActionButton v-if="canCreate && !board.archived"
:close-after-click="true"
@click="actionClone">
@click="showCloneModal">
<template #icon>
<CloneIcon :size="20" decorative />
</template>
Expand Down Expand Up @@ -132,6 +135,7 @@ import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
import CloneIcon from 'vue-material-design-icons/ContentDuplicate.vue'
import AccountIcon from 'vue-material-design-icons/Account.vue'
import { loadState } from '@nextcloud/initial-state'
import BoardCloneModal from './BoardCloneModal.vue'

const canCreateState = loadState('deck', 'canCreate')

Expand All @@ -146,6 +150,7 @@ export default {
AccountIcon,
ArchiveIcon,
CloneIcon,
BoardCloneModal,
},
directives: {
ClickOutside,
Expand All @@ -172,6 +177,7 @@ export default {
isDueSubmenuActive: false,
updateDueSetting: null,
canCreate: canCreateState,
cloneModalOpen: false,
}
},
computed: {
Expand Down Expand Up @@ -249,6 +255,26 @@ export default {
console.error(e)
}
},
showCloneModal() {
this.cloneModalOpen = true
},
async onCloseCloneModal(data) {
this.cloneModalOpen = false
if (data) {
this.loading = true
try {
const newBoard = await this.$store.dispatch('cloneBoard', {
boardData: this.board,
settings: data,
})
this.loading = false
await this.$router.push({ name: 'board', params: { id: newBoard.id } })
} catch (e) {
OC.Notification.showTemporary(t('deck', `Failed to clone board ${this.board.title}`))
console.error(e)
}
}
},
actionArchive() {
this.loading = true
this.$store.dispatch('archiveBoard', this.board)
Expand Down
Loading
Loading