diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index ae87ccd90..72b6c6c27 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -112,6 +112,10 @@ jobs: working-directory: apps/${{ env.APP_NAME }}/tests/integration run: ./run.sh + - name: Print log + if: always() + run: cat data/nextcloud.log + - name: Query count if: ${{ matrix.databases == 'mysql' }} uses: actions/github-script@v7 diff --git a/cypress/e2e/cardFeatures.js b/cypress/e2e/cardFeatures.js index 0e86f5b28..d8ace2c9c 100644 --- a/cypress/e2e/cardFeatures.js +++ b/cypress/e2e/cardFeatures.js @@ -94,6 +94,68 @@ 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-header', { timeout: 10000 }) + .should('be.visible') + .find('h2').contains('Example Domain').should('be.visible') + }) + + it('Rename card with link', () => { + cy.visit(`/apps/deck/#/board/${boardId}`) + const absoluteUrl = `https://example.com` + const plainTitle = 'New title' + 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') + }) + + // Rename link to plain title + cy.get('.card:contains("Example Domain")') + .find('.action-item__menutoggle') + .click() + cy.get('.v-popper__popper button:contains("Edit title")') + .click() + cy.get(`h4:contains("${absoluteUrl}") span[contenteditable="true"]`) + .type(`{selectAll}${plainTitle}{enter}`) + cy.get(`.card:contains("${plainTitle}")`) + .should('be.visible') + + // Rename plain title to link + cy.get('.card:contains("New title")') + .find('.action-item__menutoggle') + .click() + cy.get('.v-popper__popper button:contains("Edit title")') + .click() + cy.get('h4:contains("New title") span[contenteditable="true"]') + .type(`{selectAll}${absoluteUrl}{enter}`) + cy.get('.board').click() + cy.get('.card: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/BoardService.php b/lib/Service/BoardService.php index d8547fc28..103aaec2c 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -95,14 +95,6 @@ class BoardService { * @return Board[] */ public function findAll(int $since = -1, bool $fullDetails = false, bool $includeArchived = true): array { - if ($this->boardsCacheFull && $fullDetails) { - return $this->boardsCacheFull; - } - - if ($this->boardsCachePartial && !$fullDetails) { - return $this->boardsCachePartial; - } - $complete = $this->getUserBoards($since, $includeArchived); return $this->enrichBoards($complete, $fullDetails); } 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 @@ -->