зеркало из https://github.com/nextcloud/deck.git
feat: Implement reference resolving for cards that have a link in the title
Signed-off-by: Julius Härtl <jus@bitgrid.net> fix: Enrich on update Signed-off-by: Julius Knorr <jus@bitgrid.net> fix: Enrich on create Signed-off-by: Julius Knorr <jus@bitgrid.net>
This commit is contained in:
Родитель
46c4c7d4fd
Коммит
75be929077
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<NcAppSidebar v-if="currentBoard && currentCard"
|
||||
ref="cardSidebar"
|
||||
:active="tabId"
|
||||
:name="title"
|
||||
:name="displayTitle"
|
||||
:subname="subtitle"
|
||||
:subtitle="subtitleTooltip"
|
||||
:name-editable="titleEditable"
|
||||
@update:nameEditable="handleUpdateTitleEditable"
|
||||
@update:name="handleUpdateTitle"
|
||||
:name-editable.sync="isEditingTitle"
|
||||
@update:name="(value) => titleEditing = value"
|
||||
@dismiss-editing="titleEditing = currentCard.title"
|
||||
@submit-name="handleSubmitTitle"
|
||||
@opened="focusHeader"
|
||||
@close="closeSidebar">
|
||||
|
@ -26,6 +26,11 @@
|
|||
|
||||
<CardMenuEntries :card="currentCard" :hide-details-entry="true" />
|
||||
</template>
|
||||
<template #description>
|
||||
<NcReferenceList v-if="currentCard.referenceData"
|
||||
:text="currentCard.title"
|
||||
:interactive="false" />
|
||||
</template>
|
||||
|
||||
<NcAppSidebarTab id="details"
|
||||
:order="0"
|
||||
|
@ -68,6 +73,7 @@
|
|||
|
||||
<script>
|
||||
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() {
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="cardId && ( attachments.length > 0 )" class="card-cover">
|
||||
<div v-if="referencePreview" class="card-cover">
|
||||
<div class="image-wrapper rounded-left rounded-right" :style="{ backgroundImage: `url(${referencePreview})`}" />
|
||||
</div>
|
||||
<div v-else-if="cardId && ( attachments.length > 0 )" class="card-cover">
|
||||
<div v-for="(attachment, index) in attachments"
|
||||
:key="attachment.id"
|
||||
:class="['image-wrapper', { 'rounded-left': index === 0 }, { 'rounded-right': index === attachments.length - 1 }]"
|
||||
|
@ -43,6 +46,12 @@ export default {
|
|||
attachment.extendedData.fileid ? generateUrl(`/core/preview?fileId=${attachment.extendedData.fileid}&x=${x}&y=${y}&a=1`) : null
|
||||
)
|
||||
},
|
||||
card() {
|
||||
return this.$store.getters.cardById(this.cardId)
|
||||
},
|
||||
referencePreview() {
|
||||
return this.card?.referenceData?.richObject?.thumb
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
cardId: {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<CardCover v-if="showCardCover" :card-id="card.id" />
|
||||
<div class="card-upper">
|
||||
<h4 v-if="inlineEditingBlocked" dir="auto">
|
||||
{{ card.title }}
|
||||
{{ displayTitle }}
|
||||
</h4>
|
||||
<h4 v-else
|
||||
dir="auto"
|
||||
|
@ -133,11 +133,15 @@ export default {
|
|||
return board ? !board.archived && board.permissions.PERMISSION_EDIT : false
|
||||
},
|
||||
inlineEditingBlocked() {
|
||||
return this.isArchived || this.showArchived || !this.canEdit || this.standalone
|
||||
return this.card.referenceData || this.isArchived || this.showArchived || !this.canEdit || this.standalone
|
||||
},
|
||||
card() {
|
||||
return this.item ? this.item : this.$store.getters.cardById(this.id)
|
||||
},
|
||||
displayTitle() {
|
||||
const reference = this.card.referenceData
|
||||
return reference ? reference.openGraphObject.name : this.card.title
|
||||
},
|
||||
currentCard() {
|
||||
return this.card && this.$route && this.$route.params.cardId === this.card.id
|
||||
},
|
||||
|
|
|
@ -5,23 +5,35 @@
|
|||
|
||||
<template>
|
||||
<div v-if="card" class="card-menu" @click.stop.prevent>
|
||||
<NcButton v-if="card.referenceData" type="tertiary" @click="openLink">
|
||||
<template #icon>
|
||||
<LinkIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcActions>
|
||||
<CardMenuEntries :card="card" />
|
||||
</NcActions>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { NcActions } from '@nextcloud/vue'
|
||||
import { NcActions, NcButton } from '@nextcloud/vue'
|
||||
import LinkIcon from 'vue-material-design-icons/Link.vue'
|
||||
import CardMenuEntries from './CardMenuEntries.vue'
|
||||
|
||||
export default {
|
||||
name: 'CardMenu',
|
||||
components: { NcActions, CardMenuEntries },
|
||||
components: { NcActions, NcButton, LinkIcon, CardMenuEntries },
|
||||
props: {
|
||||
card: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openLink() {
|
||||
window.open(this.card?.referenceData?.openGraphObject?.link)
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
Загрузка…
Ссылка в новой задаче