diff --git a/appinfo/routes.php b/appinfo/routes.php index 685a6b8756..7e9aadc5d2 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -57,6 +57,8 @@ ['name' => 'card#removeLabel', 'url' => '/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'], ['name' => 'card#assignUser', 'url' => '/cards/{cardId}/assign', 'verb' => 'POST'], ['name' => 'card#unassignUser', 'url' => '/cards/{cardId}/unassign', 'verb' => 'PUT'], + ['name' => 'card#assignDependentCard', 'url' => '/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'POST'], + ['name' => 'card#removeDependentCard', 'url' => '/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'DELETE'], // attachments ['name' => 'attachment#getAll', 'url' => '/cards/{cardId}/attachments', 'verb' => 'GET'], @@ -105,6 +107,8 @@ ['name' => 'card_api#assignUser', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignUser', 'verb' => 'PUT'], ['name' => 'card_api#unassignUser', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/unassignUser', 'verb' => 'PUT'], ['name' => 'card_api#reorder', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/reorder', 'verb' => 'PUT'], + ['name' => 'card_api#assignDependentCard', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'POST'], + ['name' => 'card_api#removeDependentCard', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'DELETE'], ['name' => 'card_api#archive', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/archive', 'verb' => 'PUT'], ['name' => 'card_api#unarchive', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/unarchive', 'verb' => 'PUT'], ['name' => 'card_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/{stackId}/cards/{cardId}', 'verb' => 'DELETE'], @@ -146,6 +150,8 @@ ['name' => 'card_ocs#unAssignUser', 'url' => '/api/v{apiVersion}/cards/{cardId}/unassign', 'verb' => 'PUT'], ['name' => 'card_ocs#removeLabel', 'url' => '/api/v{apiVersion}/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'], ['name' => 'card_ocs#reorder', 'url' => '/api/v{apiVersion}/cards/{cardId}/reorder', 'verb' => 'PUT'], + ['name' => 'card_ocs#assignDependentCard', 'url' => '/api/v{apiVersion}/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'POST'], + ['name' => 'card_ocs#removeDependentCard', 'url' => '/api/v{apiVersion}/cards/{cardId}/dependentCards/{dependentCardId}', 'verb' => 'DELETE'], ['name' => 'stack_ocs#create', 'url' => '/api/v{apiVersion}/stacks', 'verb' => 'POST'], ['name' => 'stack_ocs#setDoneStack', 'url' => '/api/v{apiVersion}/stacks/{stackId}/done', 'verb' => 'PUT'], diff --git a/cypress/e2e/cardFeatures.js b/cypress/e2e/cardFeatures.js index fbc08fa9b7..7fa97d846c 100644 --- a/cypress/e2e/cardFeatures.js +++ b/cypress/e2e/cardFeatures.js @@ -205,10 +205,14 @@ describe('Card', function () { cy.reload() cy.get('.modal__card').should('be.visible') + + // Scroll to the bottom to ensure all content is loaded and visible + cy.get('.modal__card .app-sidebar-tabs, .modal__card .app-sidebar__tab--active').first().scrollTo('bottom', { ensureScrollable: false }) + cy.contains('.modal__card .ProseMirror p', 'Paragraph').scrollIntoView().should('be.visible') + cy.get('.modal__card .ProseMirror h1').contains('Hello world writing more text').should('be.visible') cy.get('.modal__card .ProseMirror li').eq(0).contains('List item').should('be.visible') cy.get('.modal__card .ProseMirror li').eq(1).contains('with entries').should('be.visible') - cy.get('.modal__card .ProseMirror p').contains('Paragraph').should('be.visible') }) it('Smart picker', () => { diff --git a/lib/Controller/CardApiController.php b/lib/Controller/CardApiController.php index 87f22dbfda..4bd3fdd2cb 100644 --- a/lib/Controller/CardApiController.php +++ b/lib/Controller/CardApiController.php @@ -149,6 +149,28 @@ public function unassignUser(int $cardId, string $userId, int $type = 0): DataRe return new DataResponse($card, HTTP::STATUS_OK); } + /** + * Assign a dependent card + */ + #[NoAdminRequired] + #[CORS] + #[NoCSRFRequired] + public function assignDependentCard(int $cardId, int $dependentCardId): DataResponse { + $card = $this->cardService->assignDependentCard($cardId, $dependentCardId); + return new DataResponse($card, HTTP::STATUS_OK); + } + + /** + * Remove a dependent card + */ + #[NoAdminRequired] + #[CORS] + #[NoCSRFRequired] + public function removeDependentCard(int $cardId, int $dependentCardId): DataResponse { + $card = $this->cardService->removeDependentCard($cardId, $dependentCardId); + return new DataResponse($card, HTTP::STATUS_OK); + } + /** * Archive card */ diff --git a/lib/Controller/CardController.php b/lib/Controller/CardController.php index b8c80456d9..58e412665d 100644 --- a/lib/Controller/CardController.php +++ b/lib/Controller/CardController.php @@ -128,4 +128,14 @@ public function assignUser(int $cardId, string $userId, int $type = 0): Assignme public function unassignUser(int $cardId, string $userId, int $type = 0): Assignment { return $this->assignmentService->unassignUser($cardId, $userId, $type); } + + #[NoAdminRequired] + public function assignDependentCard(int $cardId, int $dependentCardId): Card { + return $this->cardService->assignDependentCard($cardId, $dependentCardId); + } + + #[NoAdminRequired] + public function removeDependentCard(int $cardId, int $dependentCardId): Card { + return $this->cardService->removeDependentCard($cardId, $dependentCardId); + } } diff --git a/lib/Controller/CardOcsController.php b/lib/Controller/CardOcsController.php index 6805e6bd6e..2ea2a98256 100644 --- a/lib/Controller/CardOcsController.php +++ b/lib/Controller/CardOcsController.php @@ -8,6 +8,7 @@ namespace OCA\Deck\Controller; use OCA\Deck\Model\OptionalNullableValue; +use OCA\Deck\NotImplementedException; use OCA\Deck\Service\AssignmentService; use OCA\Deck\Service\BoardService; use OCA\Deck\Service\CardService; @@ -170,4 +171,28 @@ public function reorder(int $cardId, int $stackId, int $order, ?int $boardId): D } return new DataResponse($this->cardService->reorder($cardId, $stackId, $order)); } + + #[NoAdminRequired] + #[PublicPage] + public function assignDependentCard(int $cardId, int $dependentCardId, ?int $boardId = null): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + throw new NotImplementedException('Dependent cards are not supported for external boards'); + } + } + return new DataResponse($this->cardService->assignDependentCard($cardId, $dependentCardId)); + } + + #[NoAdminRequired] + #[PublicPage] + public function removeDependentCard(int $cardId, int $dependentCardId, ?int $boardId = null): DataResponse { + if ($boardId) { + $board = $this->boardService->find($boardId, false); + if ($board->getExternalId()) { + throw new NotImplementedException('Dependent cards are not supported for external boards'); + } + } + return new DataResponse($this->cardService->removeDependentCard($cardId, $dependentCardId)); + } } diff --git a/lib/Db/Card.php b/lib/Db/Card.php index ad8e7f67d9..c30e771307 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -37,6 +37,9 @@ * @method ?DateTime getStartdate() * @method void setStartdate(?DateTime $startdate) * + * @method void setDependentCards(array $cardIds) + * @method null|array getDependentCards() + * * @method void setLabels(Label[] $labels) * @method null|Label[] getLabels() * @@ -90,6 +93,7 @@ class Card extends RelationalEntity { protected $deletedAt = 0; protected $commentsUnread = 0; protected $commentsCount = 0; + protected ?array $dependentCards = null; protected $relatedStack = null; protected $relatedBoard = null; @@ -113,6 +117,7 @@ public function __construct() { $this->addType('deletedAt', 'integer'); $this->addType('duedate', 'datetime'); $this->addType('startdate', 'datetime'); + $this->addRelation('dependentCards'); $this->addRelation('labels'); $this->addRelation('assignedUsers'); $this->addRelation('attachments'); diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 761c9cfad4..67db013b57 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -614,6 +614,7 @@ public function searchRaw($boardIds, $term, $limit = null, $offset = null) { public function delete(Entity $entity): Entity { $this->labelMapper->deleteLabelAssignmentsForCard($entity->getId()); + $this->removeDependenciesForCard($entity->getId()); $this->cache->remove('findBoardId:' . $entity->getId()); return parent::delete($entity); } @@ -643,6 +644,76 @@ public function removeLabel(int $card, int $label): void { $qb->executeStatement(); } + /** + * @param int[] $cardIds + * @return array + */ + public function findDependenciesForCards(array $cardIds): array { + if ($cardIds === []) { + return []; + } + + $qb = $this->db->getQueryBuilder(); + $qb->select('card_id', 'dependent_card_id') + ->from('deck_dependent_cards') + ->where($qb->expr()->in('card_id', $qb->createNamedParameter($cardIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->orderBy('card_id') + ->addOrderBy('dependent_card_id'); + + $result = []; + $queryResult = $qb->executeQuery(); + while ($row = $queryResult->fetch()) { + $cardId = (int)$row['card_id']; + $result[$cardId][] = (int)$row['dependent_card_id']; + } + + return $result; + } + + public function addDependency(int $cardId, int $dependentCardId): bool { + if ($this->hasDependency($cardId, $dependentCardId)) { + return false; + } + + $qb = $this->db->getQueryBuilder(); + $qb->insert('deck_dependent_cards') + ->values([ + 'card_id' => $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT), + 'dependent_card_id' => $qb->createNamedParameter($dependentCardId, IQueryBuilder::PARAM_INT), + ]); + $qb->executeStatement(); + + return true; + } + + public function removeDependency(int $cardId, int $dependentCardId): bool { + $qb = $this->db->getQueryBuilder(); + $qb->delete('deck_dependent_cards') + ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('dependent_card_id', $qb->createNamedParameter($dependentCardId, IQueryBuilder::PARAM_INT))); + + return $qb->executeStatement() > 0; + } + + public function removeDependenciesForCard(int $cardId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('deck_dependent_cards') + ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT))) + ->orWhere($qb->expr()->eq('dependent_card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT))); + $qb->executeStatement(); + } + + private function hasDependency(int $cardId, int $dependentCardId): bool { + $qb = $this->db->getQueryBuilder(); + $qb->select('id') + ->from('deck_dependent_cards') + ->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('dependent_card_id', $qb->createNamedParameter($dependentCardId, IQueryBuilder::PARAM_INT))) + ->setMaxResults(1); + + return $qb->executeQuery()->fetchOne() !== false; + } + public function isOwner(string $userId, int $id): bool { $qb = $this->db->getQueryBuilder(); $qb->select('c.id') diff --git a/lib/Migration/Version11002Date20260410000000.php b/lib/Migration/Version11002Date20260410000000.php new file mode 100644 index 0000000000..befe95202c --- /dev/null +++ b/lib/Migration/Version11002Date20260410000000.php @@ -0,0 +1,43 @@ +hasTable('deck_dependent_cards')) { + $table = $schema->createTable('deck_dependent_cards'); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 4, + ]); + $table->addColumn('card_id', 'integer', [ + 'notnull' => true, + 'length' => 4, + 'default' => 0, + ]); + $table->addColumn('dependent_card_id', 'integer', [ + 'notnull' => true, + 'length' => 4, + 'default' => 0, + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['card_id', 'dependent_card_id'], 'deck_depend_cards_uidx'); + $table->addIndex(['card_id'], 'deck_depend_cards_idx_c'); + $table->addIndex(['dependent_card_id'], 'deck_depend_cards_idx_d'); + } + return $schema; + } +} diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 877d212c5b..3b12d2a891 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -100,6 +100,7 @@ public function enrichCards(array $cards): array { $assignedLabels = $this->labelMapper->findAssignedLabelsForCards($cardIds); $assignedUsers = $this->assignedUsersMapper->findIn($cardIds); + $dependenciesByCard = $this->cardMapper->findDependenciesForCards($cardIds); // Pre-group labels and users by card ID $labelsByCard = []; @@ -114,6 +115,7 @@ public function enrichCards(array $cards): array { foreach ($cards as $card) { $card->setLabels($labelsByCard[$card->getId()] ?? []); $card->setAssignedUsers($usersByCard[$card->getId()] ?? []); + $card->setDependentCards($dependenciesByCard[$card->getId()] ?? []); } return array_map( @@ -675,4 +677,62 @@ public function getCardUrl(int $cardId): string { public function getRedirectUrlForCard(int $cardId): string { return $this->urlGenerator->linkToRouteAbsolute('deck.page.redirectToCard', ['cardId' => $cardId]); } + + /** + * @throws StatusException + * @throws \OCA\Deck\NoPermissionException + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws BadRequestException + */ + public function assignDependentCard(int $cardId, int $dependentCardId): Card { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + $this->permissionService->checkPermission($this->cardMapper, $dependentCardId, Acl::PERMISSION_READ); + + if ($this->boardService->isArchived($this->cardMapper, $cardId)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } + + $card = $this->cardMapper->find($cardId); + if ($card->getArchived()) { + throw new StatusException('Operation not allowed. This card is archived.'); + } + + if ($this->cardMapper->addDependency($cardId, $dependentCardId)) { + $this->changeHelper->cardChanged($cardId); + $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_UPDATE); + } + + [$card] = $this->enrichCards([$card]); + return $card; + } + + /** + * @throws StatusException + * @throws \OCA\Deck\NoPermissionException + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + * @throws BadRequestException + */ + public function removeDependentCard(int $cardId, int $dependentCardId): Card { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + $this->permissionService->checkPermission($this->cardMapper, $dependentCardId, Acl::PERMISSION_READ); + + if ($this->boardService->isArchived($this->cardMapper, $cardId)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } + + $card = $this->cardMapper->find($cardId); + if ($card->getArchived()) { + throw new StatusException('Operation not allowed. This card is archived.'); + } + + if ($this->cardMapper->removeDependency($cardId, $dependentCardId)) { + $this->changeHelper->cardChanged($cardId); + $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_UPDATE); + } + + [$card] = $this->enrichCards([$card]); + return $card; + } } diff --git a/src/components/card/CardSidebarTabDetails.vue b/src/components/card/CardSidebarTabDetails.vue index 3c1d6ec770..c70ebdf077 100644 --- a/src/components/card/CardSidebarTabDetails.vue +++ b/src/components/card/CardSidebarTabDetails.vue @@ -28,6 +28,11 @@ @change="updateCardDue" @input="debouncedUpdateCardDue" /> + +
id !== dependentCardId) + } + + this.$store.dispatch('removeDependentCard', { + card: this.copiedCard, + dependentCardId, + }) + }, stringify(date) { return moment(date).locale(this.locale).format('LLL') }, diff --git a/src/components/card/DependentCardsSelector.vue b/src/components/card/DependentCardsSelector.vue new file mode 100644 index 0000000000..797cfcd520 --- /dev/null +++ b/src/components/card/DependentCardsSelector.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/src/services/CardApi.js b/src/services/CardApi.js index 59cfbea387..9057a738cc 100644 --- a/src/services/CardApi.js +++ b/src/services/CardApi.js @@ -234,4 +234,38 @@ export class CardApi { }) } + assignDependentCard(cardId, dependentCardId, boardId) { + return axios.post(this.ocsUrl(`/cards/${cardId}/dependentCards/${dependentCardId}`), { boardId: boardId ?? null }) + .then( + (response) => { + return Promise.resolve(response.data.ocs.data) + }, + (err) => { + return Promise.reject(err) + }, + ) + .catch((err) => { + return Promise.reject(err) + }) + } + + removeDependentCard(cardId, dependentCardId, boardId) { + return axios.delete(this.ocsUrl(`/cards/${cardId}/dependentCards/${dependentCardId}`), { + data: { + boardId: boardId ?? null, + }, + }) + .then( + (response) => { + return Promise.resolve(response.data.ocs.data) + }, + (err) => { + return Promise.reject(err) + }, + ) + .catch((err) => { + return Promise.reject(err) + }) + } + } diff --git a/src/store/card.js b/src/store/card.js index 1028c50479..d1d3e8e043 100644 --- a/src/store/card.js +++ b/src/store/card.js @@ -373,6 +373,16 @@ export default function cardModuleFactory() { await apiClient.removeLabelFromCard(data) commit('updateCardProperty', { property: 'labels', card: data.card }) }, + async assignDependentCard({ commit }, { card, dependentCard }) { + const boardId = this.state.currentBoard.id + const updatedCard = await apiClient.assignDependentCard(card.id, dependentCard.id, boardId) + commit('updateCardProperty', { property: 'dependentCards', card: updatedCard }) + }, + async removeDependentCard({ commit }, { card, dependentCardId }) { + const boardId = this.state.currentBoard.id + const updatedCard = await apiClient.removeDependentCard(card.id, dependentCardId, boardId) + commit('updateCardProperty', { property: 'dependentCards', card: updatedCard }) + }, async updateCardDesc({ commit, getters }, card) { const stack = getters.stackById(card.stackId) const updatedCard = await apiClient.updateCard(card, stack.boardId) diff --git a/tests/data/deck.json b/tests/data/deck.json index 207de16084..0fa4514aef 100644 --- a/tests/data/deck.json +++ b/tests/data/deck.json @@ -108,6 +108,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "ddfd0c27e53d8db94ac5e9aaa021746e", "overdue": 0, "boardId": 188, @@ -143,6 +144,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "9a8ed495f7d83f8310ae6291d6dc4624", "overdue": 3, "boardId": 188, @@ -190,6 +192,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "f908c4359e9ca0703f50da2bbe967594", "overdue": 0, "boardId": 188, @@ -265,6 +268,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "6b20cc46fa5d2e5f65251526b50cc130", "overdue": 0, "boardId": 188, @@ -310,6 +314,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "488145982535a91d9ab47db647ecf539", "overdue": 0, "boardId": 188, @@ -357,6 +362,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "b97a2b19e1cafc8f95e3f4db71097214", "overdue": 0, "boardId": 188, @@ -531,6 +537,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "f0450d41827f55580554c993304c8073", "overdue": 0, "boardId": 189, @@ -566,6 +573,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "f0450d41827f55580554c993304c8073", "overdue": 0, "boardId": 189, @@ -601,6 +609,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "1956848c45be91fefc967ee8831ea4cf", "overdue": 0, "boardId": 189, @@ -636,6 +645,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "6c315c83f146485e6b2b6fdc24ffa617", "overdue": 0, "boardId": 189, @@ -683,6 +693,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "d2a8b634cdd96ab5ef48910bbbd715b1", "overdue": 0, "boardId": 189, @@ -730,6 +741,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "193163d8a8acedbfaba196b1f0d65bc8", "overdue": 0, "boardId": 189, @@ -765,6 +777,7 @@ "deletedAt": 0, "commentsUnread": 0, "commentsCount": 0, + "dependentCards": null, "ETag": "193163d8a8acedbfaba196b1f0d65bc8", "overdue": 0, "boardId": 189, diff --git a/tests/unit/Db/CardTest.php b/tests/unit/Db/CardTest.php index 6e06b81017..03170f11ab 100644 --- a/tests/unit/Db/CardTest.php +++ b/tests/unit/Db/CardTest.php @@ -44,6 +44,7 @@ private function createCard() { $card->setArchived(false); $card->setDone(null); $card->setColor('ffffff'); + $card->setDependentCards([2, 3]); // TODO: relation shared labels acl return $card; } @@ -94,6 +95,7 @@ public function testJsonSerialize() { 'attachments' => [], 'attachmentCount' => 0, 'assignedUsers' => null, + 'dependentCards' => [2, 3], 'deletedAt' => 0, 'commentsUnread' => 0, 'commentsCount' => 0, @@ -125,6 +127,7 @@ public function testJsonSerializeLabels() { 'attachments' => [], 'attachmentCount' => 0, 'assignedUsers' => null, + 'dependentCards' => [2, 3], 'deletedAt' => 0, 'commentsUnread' => 0, 'commentsCount' => 0, @@ -158,6 +161,7 @@ public function testJsonSerializeAsignedUsers() { 'attachments' => [], 'attachmentCount' => 0, 'assignedUsers' => ['user1'], + 'dependentCards' => [2, 3], 'deletedAt' => 0, 'commentsUnread' => 0, 'commentsCount' => 0, diff --git a/tests/unit/Service/CardServiceTest.php b/tests/unit/Service/CardServiceTest.php index b6299b17ac..840cdd6fc1 100644 --- a/tests/unit/Service/CardServiceTest.php +++ b/tests/unit/Service/CardServiceTest.php @@ -207,6 +207,7 @@ public function testFind() { $cardExpected->setRelatedBoard($boardMock); $cardExpected->setRelatedStack($stackMock); $cardExpected->setLabels([]); + $cardExpected->setDependentCards([]); $expected = new CardDetails($cardExpected); $actual = $this->cardService->find(123); @@ -633,4 +634,46 @@ public function testDoneDoesNotMoveCardAlreadyInDoneColumn(): void { $this->assertNotNull($result->getDone()); $this->assertEquals(20, $result->getStackId()); } + + public function testAssignDependentCard() { + $card = Card::fromParams([ + 'id' => 42, + 'title' => 'Card title', + 'stackId' => 234, + ]); + $stack = Stack::fromParams([ + 'id' => 234, + 'boardId' => 1337, + ]); + $this->cardMapper->expects($this->once())->method('find')->willReturn($card); + $this->cardMapper->expects($this->once())->method('addDependency')->with(42, 43)->willReturn(true); + $this->cardMapper->expects($this->once())->method('findDependenciesForCards')->with([42])->willReturn([42 => [44, 43]]); + $this->stackMapper->expects($this->once()) + ->method('find') + ->with(234) + ->willReturn($stack); + $result = $this->cardService->assignDependentCard(42, 43); + $this->assertEquals([44, 43], $result->getDependentCards()); + } + + public function testRemoveDependentCard() { + $card = Card::fromParams([ + 'id' => 42, + 'title' => 'Card title', + 'stackId' => 234, + ]); + $stack = Stack::fromParams([ + 'id' => 234, + 'boardId' => 1337, + ]); + $this->cardMapper->expects($this->once())->method('find')->willReturn($card); + $this->cardMapper->expects($this->once())->method('removeDependency')->with(42, 43)->willReturn(true); + $this->cardMapper->expects($this->once())->method('findDependenciesForCards')->with([42])->willReturn([42 => [44]]); + $this->stackMapper->expects($this->once()) + ->method('find') + ->with(234) + ->willReturn($stack); + $result = $this->cardService->removeDependentCard(42, 43); + $this->assertEquals([44], $result->getDependentCards()); + } }