diff --git a/cypress/e2e/cardFeatures.js b/cypress/e2e/cardFeatures.js index 0e86f5b28..4e1822be2 100644 --- a/cypress/e2e/cardFeatures.js +++ b/cypress/e2e/cardFeatures.js @@ -94,6 +94,26 @@ describe('Card', function () { }) }) + it('Card with link reference', () => { + cy.visit(`/apps/deck/#/board/${boardId}`) + const absoluteUrl = `https://example.com` + cy.get('.board .stack').eq(0).within(() => { + cy.get('.button-vue[aria-label*="Add card"]') + .first().click() + + cy.get('.stack__card-add form input#new-stack-input-main') + .type(absoluteUrl) + cy.get('.stack__card-add form input[type=submit]') + .first().click() + cy.get('.card:contains("Example Domain")') + .should('be.visible') + .click() + + cy.get('#app-sidebar-vue') + .find('h2').contains('Example Domain').should('be.visible') + }) + }) + describe('Modal', () => { beforeEach(function () { cy.login(user) diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 6813c0167..e5e0b87ae 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -58,7 +58,7 @@ use Sabre\VObject\Component\VCalendar; class Card extends RelationalEntity { public const TITLE_MAX_LENGTH = 255; - protected $title; + protected string $title = ''; protected $description; protected $descriptionPrev; protected $stackId; diff --git a/lib/Model/CardDetails.php b/lib/Model/CardDetails.php index a5410d675..39e8cf59b 100644 --- a/lib/Model/CardDetails.php +++ b/lib/Model/CardDetails.php @@ -8,10 +8,12 @@ namespace OCA\Deck\Model; use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; +use OCP\Collaboration\Reference\Reference; class CardDetails extends Card { private Card $card; private ?Board $board; + private ?Reference $referenceData = null; public function __construct(Card $card, ?Board $board = null) { parent::__construct(); @@ -23,6 +25,10 @@ class CardDetails extends Card { $this->board = $board; } + public function setReferenceData(?Reference $data): void { + $this->referenceData = $data; + } + public function jsonSerialize(array $extras = []): array { $array = parent::jsonSerialize(); $array['overdue'] = $this->getDueStatus(); @@ -38,6 +44,8 @@ class CardDetails extends Card { $array['overdue'] = $this->getDueStatus(); $this->appendBoardDetails($array); + $array['referenceData'] = $this->referenceData?->jsonSerialize(); + return $array; } diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 9f1576a2e..30fd754c5 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -29,6 +29,7 @@ use OCA\Deck\NoPermissionException; use OCA\Deck\Notification\NotificationHelper; use OCA\Deck\StatusException; use OCA\Deck\Validators\CardServiceValidator; +use OCP\Collaboration\Reference\IReferenceManager; use OCP\Comments\ICommentsManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; @@ -37,9 +38,6 @@ use OCP\IUserManager; use Psr\Log\LoggerInterface; class CardService { - - private ?string $currentUser; - public function __construct( private CardMapper $cardMapper, private StackMapper $stackMapper, @@ -61,13 +59,13 @@ class CardService { private IRequest $request, private CardServiceValidator $cardServiceValidator, private AssignmentService $assignmentService, - ?string $userId, + private IReferenceManager $referenceManager, + private ?string $userId, ) { - $this->currentUser = $userId; } public function enrichCards($cards) { - $user = $this->userManager->get($this->currentUser); + $user = $this->userManager->get($this->userId); $cardIds = array_map(function (Card $card) use ($user) { // Everything done in here might be heavy as it is executed for every card @@ -107,11 +105,21 @@ class CardService { return array_map( function (Card $card): CardDetails { - return new CardDetails($card); + $cardDetails = new CardDetails($card); + + $references = $this->referenceManager->extractReferences($card->getTitle()); + $reference = array_shift($references); + if ($reference) { + $referenceData = $this->referenceManager->resolveReference($reference); + $cardDetails->setReferenceData($referenceData); + } + + return $cardDetails; }, $cards ); } + public function fetchDeleted($boardId) { $this->cardServiceValidator->check(compact('boardId')); $this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ); @@ -191,6 +199,8 @@ class CardService { $this->changeHelper->cardChanged($card->getId(), false); $this->eventDispatcher->dispatchTyped(new CardCreatedEvent($card)); + [$card] = $this->enrichCards([$card]); + return $card; } @@ -265,7 +275,7 @@ class CardService { } $changes = new ChangeSet($card); - if ($card->getLastEditor() !== $this->currentUser && $card->getLastEditor() !== null) { + if ($card->getLastEditor() !== $this->userId && $card->getLastEditor() !== null) { $this->activityManager->triggerEvent( ActivityManager::DECK_OBJECT_CARD, $card, @@ -278,7 +288,7 @@ class CardService { ); $card->setDescriptionPrev($card->getDescription()); - $card->setLastEditor($this->currentUser); + $card->setLastEditor($this->userId); } $card->setTitle($title); $card->setStackId($stackId); @@ -352,6 +362,8 @@ class CardService { $this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore())); + [$card] = $this->enrichCards([$card]); + return $card; } diff --git a/src/components/card/CardSidebar.vue b/src/components/card/CardSidebar.vue index e0903594a..b3e18c9cd 100644 --- a/src/components/card/CardSidebar.vue +++ b/src/components/card/CardSidebar.vue @@ -7,12 +7,12 @@ @@ -26,6 +26,11 @@ + import { NcActionButton, NcAppSidebar, NcAppSidebarTab } from '@nextcloud/vue' +import { NcReferenceList } from '@nextcloud/vue/dist/Components/NcRichText.js' import { getCapabilities } from '@nextcloud/capabilities' import { mapState, mapGetters } from 'vuex' import CardSidebarTabDetails from './CardSidebarTabDetails.vue' @@ -93,6 +99,7 @@ export default { NcAppSidebar, NcAppSidebarTab, NcActionButton, + NcReferenceList, CardSidebarTabAttachments, CardSidebarTabComments, CardSidebarTabActivity, @@ -122,7 +129,7 @@ export default { }, data() { return { - titleEditable: false, + isEditingTitle: false, titleEditing: '', hasActivity: capabilities && capabilities.activity, locale: getLocale(), @@ -130,13 +137,10 @@ export default { }, computed: { ...mapState({ - isFullApp: state => state.isFullApp, - currentBoard: state => state.currentBoard, + isFullApp: (state) => state.isFullApp, + currentBoard: (state) => state.currentBoard, }), ...mapGetters(['canEdit', 'assignables', 'cardActions', 'stackById']), - title() { - return this.titleEditable ? this.titleEditing : this.currentCard.title - }, currentCard() { return this.$store.getters.cardById(this.id) }, @@ -154,11 +158,26 @@ export default { this.$store.dispatch('setConfig', { cardDetailsInModal: newValue }) }, }, + displayTitle: { + get() { + if (this.isEditingTitle) { + return this.titleEditing + } + const reference = this.currentCard.referenceData + return reference ? reference.openGraphObject.name : this.currentCard.title + }, + }, }, watch: { currentCard() { this.focusHeader() }, + 'currentCard.title': { + immediate: true, + handler(newTitle) { + this.titleEditing = newTitle + }, + }, }, methods: { focusHeader() { @@ -166,22 +185,16 @@ export default { this.$refs?.cardSidebar.$el.querySelector('.app-sidebar-header__mainname')?.focus() }) }, - handleUpdateTitleEditable(value) { - this.titleEditable = value - if (value) { - this.titleEditing = this.currentCard.title - } - }, - handleUpdateTitle(value) { - this.titleEditing = value - }, - handleSubmitTitle(value) { - if (value.trim === '') { + handleSubmitTitle() { + if (this.titleEditing.trim() === '') { showError(t('deck', 'The title cannot be empty.')) return } - this.titleEditable = false - this.$store.dispatch('updateCardTitle', { ...this.currentCard, title: this.titleEditing }) + this.isEditingTitle = false + this.$store.dispatch('updateCardTitle', { + ...this.currentCard, + title: this.titleEditing, + }) }, closeSidebar() { diff --git a/src/components/cards/CardCover.vue b/src/components/cards/CardCover.vue index f7d2a76d2..67dc0a167 100644 --- a/src/components/cards/CardCover.vue +++ b/src/components/cards/CardCover.vue @@ -4,7 +4,10 @@ --> diff --git a/src/store/card.js b/src/store/card.js index 63f85eb50..556a52a2e 100644 --- a/src/store/card.js +++ b/src/store/card.js @@ -285,6 +285,7 @@ export default { async updateCardTitle({ commit }, card) { const updatedCard = await apiClient.updateCard(card) commit('updateCardProperty', { property: 'title', card: updatedCard }) + commit('updateCardProperty', { property: 'referenceData', card: updatedCard }) }, async moveCard({ commit }, card) { const updatedCard = await apiClient.updateCard(card) diff --git a/tests/integration/import/ImportExportTest.php b/tests/integration/import/ImportExportTest.php index 2dbd5f8db..1d7b5d52b 100644 --- a/tests/integration/import/ImportExportTest.php +++ b/tests/integration/import/ImportExportTest.php @@ -135,7 +135,7 @@ class ImportExportTest extends \Test\TestCase { ); } - public static function writeArrayStructure(string $prefix = '', array $array = [], array $skipKeyList = ['id', 'boardId', 'cardId', 'stackId', 'ETag', 'permissions', 'shared', 'version', 'done']): string { + public static function writeArrayStructure(string $prefix = '', array $array = [], array $skipKeyList = ['id', 'boardId', 'cardId', 'stackId', 'ETag', 'permissions', 'shared', 'version', 'done', 'referenceData']): string { $output = ''; $arrayIsList = array_keys($array) === range(0, count($array) - 1); foreach ($array as $key => $value) { diff --git a/tests/unit/Db/CardTest.php b/tests/unit/Db/CardTest.php index 8c761130a..f1321b063 100644 --- a/tests/unit/Db/CardTest.php +++ b/tests/unit/Db/CardTest.php @@ -90,6 +90,7 @@ class CardTest extends TestCase { 'lastEditor' => null, 'ETag' => $card->getETag(), 'done' => null, + 'referenceData' => null, ], (new CardDetails($card))->jsonSerialize()); } public function testJsonSerializeLabels() { @@ -118,6 +119,7 @@ class CardTest extends TestCase { 'lastEditor' => null, 'ETag' => $card->getETag(), 'done' => false, + 'referenceData' => null, ], (new CardDetails($card))->jsonSerialize()); } @@ -148,6 +150,7 @@ class CardTest extends TestCase { 'lastEditor' => null, 'ETag' => $card->getETag(), 'done' => false, + 'referenceData' => null, ], (new CardDetails($card))->jsonSerialize()); } } diff --git a/tests/unit/Service/CardServiceTest.php b/tests/unit/Service/CardServiceTest.php index e5600b3ae..e68775f2f 100644 --- a/tests/unit/Service/CardServiceTest.php +++ b/tests/unit/Service/CardServiceTest.php @@ -41,6 +41,7 @@ use OCA\Deck\Notification\NotificationHelper; use OCA\Deck\StatusException; use OCA\Deck\Validators\CardServiceValidator; use OCP\Activity\IEvent; +use OCP\Collaboration\Reference\IReferenceManager; use OCP\Comments\ICommentsManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; @@ -93,6 +94,8 @@ class CardServiceTest extends TestCase { private $logger; /** @var CardServiceValidator|MockObject */ private $cardServiceValidator; + /** @var IReferenceManager|MockObject */ + private $referenceManager; /** @var AssignmentService|MockObject */ private $assignmentService; @@ -119,6 +122,7 @@ class CardServiceTest extends TestCase { $this->request = $this->createMock(IRequest::class); $this->cardServiceValidator = $this->createMock(CardServiceValidator::class); $this->assignmentService = $this->createMock(AssignmentService::class); + $this->referenceManager = $this->createMock(IReferenceManager::class); $this->logger->expects($this->any())->method('error'); @@ -143,6 +147,7 @@ class CardServiceTest extends TestCase { $this->request, $this->cardServiceValidator, $this->assignmentService, + $this->referenceManager, 'user1' ); } @@ -207,15 +212,24 @@ class CardServiceTest extends TestCase { } public function testCreate() { - $card = new Card(); - $card->setTitle('Card title'); - $card->setOwner('admin'); - $card->setStackId(123); - $card->setOrder(999); - $card->setType('text'); + $card = Card::fromParams([ + 'title' => 'Card title', + 'owner' => 'admin', + 'stackId' => 123, + 'order' => 999, + 'type' => 'text', + ]); + $stack = Stack::fromParams([ + 'id' => 123, + 'boardId' => 1337, + ]); $this->cardMapper->expects($this->once()) ->method('insert') ->willReturn($card); + $this->stackMapper->expects($this->once()) + ->method('find') + ->with(123) + ->willReturn($stack); $b = $this->cardService->create('Card title', 123, 'text', 999, 'admin'); $this->assertEquals($b->getTitle(), 'Card title'); @@ -270,7 +284,7 @@ class CardServiceTest extends TestCase { $stackMock = new Stack(); $stackMock->setBoardId(1234); - $this->stackMapper->expects($this->once()) + $this->stackMapper->expects($this->any()) ->method('find') ->willReturn($stackMock); $b = $this->cardService->create('Card title', 123, 'text', 999, 'admin'); @@ -293,13 +307,23 @@ class CardServiceTest extends TestCase { } public function testUpdate() { - $card = new Card(); - $card->setTitle('title'); - $card->setArchived(false); + $card = Card::fromParams([ + 'title' => 'Card title', + 'archived' => 'false', + 'stackId' => 234, + ]); + $stack = Stack::fromParams([ + 'id' => 234, + 'boardId' => 1337, + ]); $this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->once())->method('update')->willReturnCallback(function ($c) { return $c; }); + $this->stackMapper->expects($this->once()) + ->method('find') + ->with(234) + ->willReturn($stack); $actual = $this->cardService->update(123, 'newtitle', 234, 'text', 'admin', 'foo', 999, '2017-01-01 00:00:00', null); $this->assertEquals('newtitle', $actual->getTitle()); $this->assertEquals(234, $actual->getStackId());