Merge pull request #6286 from nextcloud/feat/reference-cards
feat: Implement reference resolving for cards that have a link in the title
This commit is contained in:
4
.github/workflows/integration.yml
vendored
4
.github/workflows/integration.yml
vendored
@@ -112,6 +112,10 @@ jobs:
|
|||||||
working-directory: apps/${{ env.APP_NAME }}/tests/integration
|
working-directory: apps/${{ env.APP_NAME }}/tests/integration
|
||||||
run: ./run.sh
|
run: ./run.sh
|
||||||
|
|
||||||
|
- name: Print log
|
||||||
|
if: always()
|
||||||
|
run: cat data/nextcloud.log
|
||||||
|
|
||||||
- name: Query count
|
- name: Query count
|
||||||
if: ${{ matrix.databases == 'mysql' }}
|
if: ${{ matrix.databases == 'mysql' }}
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
|||||||
@@ -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', () => {
|
describe('Modal', () => {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
cy.login(user)
|
cy.login(user)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ use Sabre\VObject\Component\VCalendar;
|
|||||||
class Card extends RelationalEntity {
|
class Card extends RelationalEntity {
|
||||||
public const TITLE_MAX_LENGTH = 255;
|
public const TITLE_MAX_LENGTH = 255;
|
||||||
|
|
||||||
protected $title;
|
protected string $title = '';
|
||||||
protected $description;
|
protected $description;
|
||||||
protected $descriptionPrev;
|
protected $descriptionPrev;
|
||||||
protected $stackId;
|
protected $stackId;
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ namespace OCA\Deck\Model;
|
|||||||
|
|
||||||
use OCA\Deck\Db\Board;
|
use OCA\Deck\Db\Board;
|
||||||
use OCA\Deck\Db\Card;
|
use OCA\Deck\Db\Card;
|
||||||
|
use OCP\Collaboration\Reference\Reference;
|
||||||
|
|
||||||
class CardDetails extends Card {
|
class CardDetails extends Card {
|
||||||
private Card $card;
|
private Card $card;
|
||||||
private ?Board $board;
|
private ?Board $board;
|
||||||
|
private ?Reference $referenceData = null;
|
||||||
|
|
||||||
public function __construct(Card $card, ?Board $board = null) {
|
public function __construct(Card $card, ?Board $board = null) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
@@ -23,6 +25,10 @@ class CardDetails extends Card {
|
|||||||
$this->board = $board;
|
$this->board = $board;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setReferenceData(?Reference $data): void {
|
||||||
|
$this->referenceData = $data;
|
||||||
|
}
|
||||||
|
|
||||||
public function jsonSerialize(array $extras = []): array {
|
public function jsonSerialize(array $extras = []): array {
|
||||||
$array = parent::jsonSerialize();
|
$array = parent::jsonSerialize();
|
||||||
$array['overdue'] = $this->getDueStatus();
|
$array['overdue'] = $this->getDueStatus();
|
||||||
@@ -38,6 +44,8 @@ class CardDetails extends Card {
|
|||||||
$array['overdue'] = $this->getDueStatus();
|
$array['overdue'] = $this->getDueStatus();
|
||||||
$this->appendBoardDetails($array);
|
$this->appendBoardDetails($array);
|
||||||
|
|
||||||
|
$array['referenceData'] = $this->referenceData?->jsonSerialize();
|
||||||
|
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,14 +95,6 @@ class BoardService {
|
|||||||
* @return Board[]
|
* @return Board[]
|
||||||
*/
|
*/
|
||||||
public function findAll(int $since = -1, bool $fullDetails = false, bool $includeArchived = true): array {
|
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);
|
$complete = $this->getUserBoards($since, $includeArchived);
|
||||||
return $this->enrichBoards($complete, $fullDetails);
|
return $this->enrichBoards($complete, $fullDetails);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ use OCA\Deck\NoPermissionException;
|
|||||||
use OCA\Deck\Notification\NotificationHelper;
|
use OCA\Deck\Notification\NotificationHelper;
|
||||||
use OCA\Deck\StatusException;
|
use OCA\Deck\StatusException;
|
||||||
use OCA\Deck\Validators\CardServiceValidator;
|
use OCA\Deck\Validators\CardServiceValidator;
|
||||||
|
use OCP\Collaboration\Reference\IReferenceManager;
|
||||||
use OCP\Comments\ICommentsManager;
|
use OCP\Comments\ICommentsManager;
|
||||||
use OCP\EventDispatcher\IEventDispatcher;
|
use OCP\EventDispatcher\IEventDispatcher;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
@@ -37,9 +38,6 @@ use OCP\IUserManager;
|
|||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class CardService {
|
class CardService {
|
||||||
|
|
||||||
private ?string $currentUser;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CardMapper $cardMapper,
|
private CardMapper $cardMapper,
|
||||||
private StackMapper $stackMapper,
|
private StackMapper $stackMapper,
|
||||||
@@ -61,13 +59,13 @@ class CardService {
|
|||||||
private IRequest $request,
|
private IRequest $request,
|
||||||
private CardServiceValidator $cardServiceValidator,
|
private CardServiceValidator $cardServiceValidator,
|
||||||
private AssignmentService $assignmentService,
|
private AssignmentService $assignmentService,
|
||||||
?string $userId,
|
private IReferenceManager $referenceManager,
|
||||||
|
private ?string $userId,
|
||||||
) {
|
) {
|
||||||
$this->currentUser = $userId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function enrichCards($cards) {
|
public function enrichCards($cards) {
|
||||||
$user = $this->userManager->get($this->currentUser);
|
$user = $this->userManager->get($this->userId);
|
||||||
|
|
||||||
$cardIds = array_map(function (Card $card) use ($user) {
|
$cardIds = array_map(function (Card $card) use ($user) {
|
||||||
// Everything done in here might be heavy as it is executed for every card
|
// Everything done in here might be heavy as it is executed for every card
|
||||||
@@ -107,11 +105,21 @@ class CardService {
|
|||||||
|
|
||||||
return array_map(
|
return array_map(
|
||||||
function (Card $card): CardDetails {
|
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
|
$cards
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fetchDeleted($boardId) {
|
public function fetchDeleted($boardId) {
|
||||||
$this->cardServiceValidator->check(compact('boardId'));
|
$this->cardServiceValidator->check(compact('boardId'));
|
||||||
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
|
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
|
||||||
@@ -191,6 +199,8 @@ class CardService {
|
|||||||
$this->changeHelper->cardChanged($card->getId(), false);
|
$this->changeHelper->cardChanged($card->getId(), false);
|
||||||
$this->eventDispatcher->dispatchTyped(new CardCreatedEvent($card));
|
$this->eventDispatcher->dispatchTyped(new CardCreatedEvent($card));
|
||||||
|
|
||||||
|
[$card] = $this->enrichCards([$card]);
|
||||||
|
|
||||||
return $card;
|
return $card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +275,7 @@ class CardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$changes = new ChangeSet($card);
|
$changes = new ChangeSet($card);
|
||||||
if ($card->getLastEditor() !== $this->currentUser && $card->getLastEditor() !== null) {
|
if ($card->getLastEditor() !== $this->userId && $card->getLastEditor() !== null) {
|
||||||
$this->activityManager->triggerEvent(
|
$this->activityManager->triggerEvent(
|
||||||
ActivityManager::DECK_OBJECT_CARD,
|
ActivityManager::DECK_OBJECT_CARD,
|
||||||
$card,
|
$card,
|
||||||
@@ -278,7 +288,7 @@ class CardService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
$card->setDescriptionPrev($card->getDescription());
|
$card->setDescriptionPrev($card->getDescription());
|
||||||
$card->setLastEditor($this->currentUser);
|
$card->setLastEditor($this->userId);
|
||||||
}
|
}
|
||||||
$card->setTitle($title);
|
$card->setTitle($title);
|
||||||
$card->setStackId($stackId);
|
$card->setStackId($stackId);
|
||||||
@@ -352,6 +362,8 @@ class CardService {
|
|||||||
|
|
||||||
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore()));
|
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore()));
|
||||||
|
|
||||||
|
[$card] = $this->enrichCards([$card]);
|
||||||
|
|
||||||
return $card;
|
return $card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
<NcAppSidebar v-if="currentBoard && currentCard"
|
<NcAppSidebar v-if="currentBoard && currentCard"
|
||||||
ref="cardSidebar"
|
ref="cardSidebar"
|
||||||
:active="tabId"
|
:active="tabId"
|
||||||
:name="title"
|
:name="displayTitle"
|
||||||
:subname="subtitle"
|
:subname="subtitle"
|
||||||
:subtitle="subtitleTooltip"
|
:subtitle="subtitleTooltip"
|
||||||
:name-editable="titleEditable"
|
:name-editable.sync="isEditingTitle"
|
||||||
@update:nameEditable="handleUpdateTitleEditable"
|
@update:name="(value) => titleEditing = value"
|
||||||
@update:name="handleUpdateTitle"
|
@dismiss-editing="titleEditing = currentCard.title"
|
||||||
@submit-name="handleSubmitTitle"
|
@submit-name="handleSubmitTitle"
|
||||||
@opened="focusHeader"
|
@opened="focusHeader"
|
||||||
@close="closeSidebar">
|
@close="closeSidebar">
|
||||||
@@ -26,6 +26,11 @@
|
|||||||
|
|
||||||
<CardMenuEntries :card="currentCard" :hide-details-entry="true" />
|
<CardMenuEntries :card="currentCard" :hide-details-entry="true" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<NcReferenceList v-if="currentCard.referenceData"
|
||||||
|
:text="currentCard.title"
|
||||||
|
:interactive="false" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<NcAppSidebarTab id="details"
|
<NcAppSidebarTab id="details"
|
||||||
:order="0"
|
:order="0"
|
||||||
@@ -68,6 +73,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { NcActionButton, NcAppSidebar, NcAppSidebarTab } from '@nextcloud/vue'
|
import { NcActionButton, NcAppSidebar, NcAppSidebarTab } from '@nextcloud/vue'
|
||||||
|
import { NcReferenceList } from '@nextcloud/vue/dist/Components/NcRichText.js'
|
||||||
import { getCapabilities } from '@nextcloud/capabilities'
|
import { getCapabilities } from '@nextcloud/capabilities'
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import CardSidebarTabDetails from './CardSidebarTabDetails.vue'
|
import CardSidebarTabDetails from './CardSidebarTabDetails.vue'
|
||||||
@@ -93,6 +99,7 @@ export default {
|
|||||||
NcAppSidebar,
|
NcAppSidebar,
|
||||||
NcAppSidebarTab,
|
NcAppSidebarTab,
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
|
NcReferenceList,
|
||||||
CardSidebarTabAttachments,
|
CardSidebarTabAttachments,
|
||||||
CardSidebarTabComments,
|
CardSidebarTabComments,
|
||||||
CardSidebarTabActivity,
|
CardSidebarTabActivity,
|
||||||
@@ -122,7 +129,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
titleEditable: false,
|
isEditingTitle: false,
|
||||||
titleEditing: '',
|
titleEditing: '',
|
||||||
hasActivity: capabilities && capabilities.activity,
|
hasActivity: capabilities && capabilities.activity,
|
||||||
locale: getLocale(),
|
locale: getLocale(),
|
||||||
@@ -130,13 +137,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
isFullApp: state => state.isFullApp,
|
isFullApp: (state) => state.isFullApp,
|
||||||
currentBoard: state => state.currentBoard,
|
currentBoard: (state) => state.currentBoard,
|
||||||
}),
|
}),
|
||||||
...mapGetters(['canEdit', 'assignables', 'cardActions', 'stackById']),
|
...mapGetters(['canEdit', 'assignables', 'cardActions', 'stackById']),
|
||||||
title() {
|
|
||||||
return this.titleEditable ? this.titleEditing : this.currentCard.title
|
|
||||||
},
|
|
||||||
currentCard() {
|
currentCard() {
|
||||||
return this.$store.getters.cardById(this.id)
|
return this.$store.getters.cardById(this.id)
|
||||||
},
|
},
|
||||||
@@ -154,11 +158,26 @@ export default {
|
|||||||
this.$store.dispatch('setConfig', { cardDetailsInModal: newValue })
|
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: {
|
watch: {
|
||||||
currentCard() {
|
currentCard() {
|
||||||
this.focusHeader()
|
this.focusHeader()
|
||||||
},
|
},
|
||||||
|
'currentCard.title': {
|
||||||
|
immediate: true,
|
||||||
|
handler(newTitle) {
|
||||||
|
this.titleEditing = newTitle
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focusHeader() {
|
focusHeader() {
|
||||||
@@ -166,22 +185,16 @@ export default {
|
|||||||
this.$refs?.cardSidebar.$el.querySelector('.app-sidebar-header__mainname')?.focus()
|
this.$refs?.cardSidebar.$el.querySelector('.app-sidebar-header__mainname')?.focus()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleUpdateTitleEditable(value) {
|
handleSubmitTitle() {
|
||||||
this.titleEditable = value
|
if (this.titleEditing.trim() === '') {
|
||||||
if (value) {
|
|
||||||
this.titleEditing = this.currentCard.title
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleUpdateTitle(value) {
|
|
||||||
this.titleEditing = value
|
|
||||||
},
|
|
||||||
handleSubmitTitle(value) {
|
|
||||||
if (value.trim === '') {
|
|
||||||
showError(t('deck', 'The title cannot be empty.'))
|
showError(t('deck', 'The title cannot be empty.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.titleEditable = false
|
this.isEditingTitle = false
|
||||||
this.$store.dispatch('updateCardTitle', { ...this.currentCard, title: this.titleEditing })
|
this.$store.dispatch('updateCardTitle', {
|
||||||
|
...this.currentCard,
|
||||||
|
title: this.titleEditing,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
closeSidebar() {
|
closeSidebar() {
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<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"
|
<div v-for="(attachment, index) in attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
:class="['image-wrapper', { 'rounded-left': index === 0 }, { 'rounded-right': index === attachments.length - 1 }]"
|
: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
|
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: {
|
watch: {
|
||||||
cardId: {
|
cardId: {
|
||||||
|
|||||||
@@ -19,12 +19,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<CardCover v-if="showCardCover" :card-id="card.id" />
|
<CardCover v-if="showCardCover" :card-id="card.id" />
|
||||||
<div class="card-upper">
|
<div class="card-upper">
|
||||||
<h4 v-if="inlineEditingBlocked" dir="auto">
|
<h4 v-if="editingTitle === 0" key="title-view" dir="auto">
|
||||||
{{ card.title }}
|
<span class="dragDisabled" contenteditable="false">{{ displayTitle }}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<h4 v-else
|
<h4 v-if="editingTitle >= 1"
|
||||||
|
key="title-edit"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="editable"
|
class="editable dragDisabled"
|
||||||
:aria-label="t('deck', 'Edit card title')">
|
:aria-label="t('deck', 'Edit card title')">
|
||||||
<span ref="titleContentEditable"
|
<span ref="titleContentEditable"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -33,12 +34,16 @@
|
|||||||
@focus="onTitleFocus"
|
@focus="onTitleFocus"
|
||||||
@blur="onTitleBlur"
|
@blur="onTitleBlur"
|
||||||
@click.stop
|
@click.stop
|
||||||
@keyup.esc="cancelEdit"
|
@keyup.esc="onTitleBlur"
|
||||||
|
@keyup.enter="onTitleBlur"
|
||||||
@keyup.stop>{{ card.title }}</span>
|
@keyup.stop>{{ card.title }}</span>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<DueDate v-if="compactMode" :card="card" />
|
<DueDate v-if="compactMode" :card="card" />
|
||||||
<CardMenu v-if="showMenuAtTitle" :card="card" class="right card-menu" />
|
<CardMenu v-if="showMenuAtTitle"
|
||||||
|
:card="card"
|
||||||
|
class="right card-menu"
|
||||||
|
@edit-title="triggerEditTitle" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasLabels" class="card-labels">
|
<div v-if="hasLabels" class="card-labels">
|
||||||
@@ -51,7 +56,10 @@
|
|||||||
<span @click.stop="applyLabelFilter(label)">{{ label.title }}</span>
|
<span @click.stop="applyLabelFilter(label)">{{ label.title }}</span>
|
||||||
</li>
|
</li>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
<CardMenu v-if="showMenuAtLabels" :card="card" class="right" />
|
<CardMenu v-if="showMenuAtLabels"
|
||||||
|
:card="card"
|
||||||
|
class="right"
|
||||||
|
@edit-title="triggerEditTitle" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasBadges"
|
<div v-if="hasBadges"
|
||||||
@@ -59,7 +67,10 @@
|
|||||||
class="card-controls compact-item"
|
class="card-controls compact-item"
|
||||||
@click="openCard">
|
@click="openCard">
|
||||||
<CardBadges :card="card">
|
<CardBadges :card="card">
|
||||||
<CardMenu v-if="showMenuAtBadges" :card="card" class="right" />
|
<CardMenu v-if="showMenuAtBadges"
|
||||||
|
:card="card"
|
||||||
|
class="right"
|
||||||
|
@edit-title="triggerEditTitle" />
|
||||||
</CardBadges>
|
</CardBadges>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,6 +89,12 @@ import CardCover from './CardCover.vue'
|
|||||||
import DueDate from './badges/DueDate.vue'
|
import DueDate from './badges/DueDate.vue'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
|
|
||||||
|
const TITLE_EDITING_STATE = {
|
||||||
|
OFF: 0,
|
||||||
|
PENDING: 1,
|
||||||
|
ON: 2,
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardItem',
|
name: 'CardItem',
|
||||||
components: { CardBadges, AttachmentDragAndDrop, CardMenu, CardCover, DueDate },
|
components: { CardBadges, AttachmentDragAndDrop, CardMenu, CardCover, DueDate },
|
||||||
@@ -106,6 +123,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
highlight: false,
|
highlight: false,
|
||||||
|
editingTitle: TITLE_EDITING_STATE.OFF,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -132,12 +150,13 @@ export default {
|
|||||||
const board = this.$store.getters.boards.find((item) => item.id === this.card.boardId)
|
const board = this.$store.getters.boards.find((item) => item.id === this.card.boardId)
|
||||||
return board ? !board.archived && board.permissions.PERMISSION_EDIT : false
|
return board ? !board.archived && board.permissions.PERMISSION_EDIT : false
|
||||||
},
|
},
|
||||||
inlineEditingBlocked() {
|
|
||||||
return this.isArchived || this.showArchived || !this.canEdit || this.standalone
|
|
||||||
},
|
|
||||||
card() {
|
card() {
|
||||||
return this.item ? this.item : this.$store.getters.cardById(this.id)
|
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() {
|
currentCard() {
|
||||||
return this.card && this.$route && this.$route.params.cardId === this.card.id
|
return this.card && this.$route && this.$route.params.cardId === this.card.id
|
||||||
},
|
},
|
||||||
@@ -181,23 +200,24 @@ export default {
|
|||||||
this.$nextTick(() => this.$el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }))
|
this.$nextTick(() => this.$el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'card.title'(value) {
|
|
||||||
if (document.activeElement === this.$refs.titleContentEditable || this.$refs.titleContentEditable.textContent === value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$refs.titleContentEditable.textContent = value
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
hasSelection() {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
return selection.toString() !== ''
|
||||||
|
},
|
||||||
focus(card) {
|
focus(card) {
|
||||||
if (this.shortcutLock) {
|
if (this.shortcutLock || this.hasSelection()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
card = this.$refs[`card${card}`]
|
card = this.$refs[`card${card}`]
|
||||||
card.focus()
|
card.focus()
|
||||||
},
|
},
|
||||||
openCard() {
|
openCard(event) {
|
||||||
if (this.dragging) {
|
if (event.target.tagName.toLowerCase() === 'a') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.dragging || this.hasSelection()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const boardId = this.card && this.card.boardId ? this.card.boardId : (this.$route?.params.id ?? this.currentBoard.id)
|
const boardId = this.card && this.card.boardId ? this.card.boardId : (this.$route?.params.id ?? this.currentBoard.id)
|
||||||
@@ -209,12 +229,26 @@ export default {
|
|||||||
|
|
||||||
this.$root.$emit('open-card', this.card.id)
|
this.$root.$emit('open-card', this.card.id)
|
||||||
},
|
},
|
||||||
|
triggerEditTitle() {
|
||||||
|
this.editingTitle = TITLE_EDITING_STATE.PENDING
|
||||||
|
this.$store.dispatch('toggleShortcutLock', true)
|
||||||
|
setTimeout(() => {
|
||||||
|
const sel = window.getSelection()
|
||||||
|
sel.selectAllChildren(this.$refs.titleContentEditable)
|
||||||
|
sel.collapseToEnd()
|
||||||
|
this.editingTitle = TITLE_EDITING_STATE.ON
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
onTitleBlur(e) {
|
onTitleBlur(e) {
|
||||||
// TODO Handle empty title
|
const value = e.target.innerText.trim().replace(/\n$/, '')
|
||||||
if (e.target.innerText !== this.card.title) {
|
if (this.editingTitle !== TITLE_EDITING_STATE.ON || value === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editingTitle = TITLE_EDITING_STATE.OFF
|
||||||
|
if (value !== this.card.title) {
|
||||||
this.$store.dispatch('updateCardTitle', {
|
this.$store.dispatch('updateCardTitle', {
|
||||||
...this.card,
|
...this.card,
|
||||||
title: e.target.innerText,
|
title: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.$store.dispatch('toggleShortcutLock', false)
|
this.$store.dispatch('toggleShortcutLock', false)
|
||||||
@@ -222,10 +256,6 @@ export default {
|
|||||||
onTitleFocus() {
|
onTitleFocus() {
|
||||||
this.$store.dispatch('toggleShortcutLock', true)
|
this.$store.dispatch('toggleShortcutLock', true)
|
||||||
},
|
},
|
||||||
cancelEdit() {
|
|
||||||
this.$refs.titleContentEditable.textContent = this.card.title
|
|
||||||
this.$store.dispatch('toggleShortcutLock', false)
|
|
||||||
},
|
|
||||||
handleCardKeyboardShortcut(key) {
|
handleCardKeyboardShortcut(key) {
|
||||||
if (OCP.Accessibility.disableKeyboardShortcuts()) {
|
if (OCP.Accessibility.disableKeyboardShortcuts()) {
|
||||||
return
|
return
|
||||||
@@ -237,7 +267,7 @@ export default {
|
|||||||
|
|
||||||
switch (key.code) {
|
switch (key.code) {
|
||||||
case 'KeyE':
|
case 'KeyE':
|
||||||
this.$refs.titleContentEditable?.focus()
|
this.triggerEditTitle()
|
||||||
break
|
break
|
||||||
case 'KeyA':
|
case 'KeyA':
|
||||||
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
this.$store.dispatch('archiveUnarchiveCard', { ...this.card, archived: !this.card.archived })
|
||||||
@@ -336,6 +366,11 @@ export default {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
|
:deep(a) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
&.editable {
|
&.editable {
|
||||||
span {
|
span {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
|
|||||||
@@ -5,23 +5,42 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="card" class="card-menu" @click.stop.prevent>
|
<div v-if="card" class="card-menu" @click.stop.prevent>
|
||||||
|
<NcButton v-if="card.referenceData"
|
||||||
|
type="tertiary"
|
||||||
|
:title="t('deck','Open link')"
|
||||||
|
@click="openLink">
|
||||||
|
<template #icon>
|
||||||
|
<LinkIcon :size="20" />
|
||||||
|
</template>
|
||||||
|
</NcButton>
|
||||||
<NcActions>
|
<NcActions>
|
||||||
<CardMenuEntries :card="card" />
|
<CardMenuEntries :card="card" @edit-title="editTitle" />
|
||||||
</NcActions>
|
</NcActions>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<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'
|
import CardMenuEntries from './CardMenuEntries.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardMenu',
|
name: 'CardMenu',
|
||||||
components: { NcActions, CardMenuEntries },
|
components: { NcActions, NcButton, LinkIcon, CardMenuEntries },
|
||||||
props: {
|
props: {
|
||||||
card: {
|
card: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['edit-title'],
|
||||||
|
methods: {
|
||||||
|
openLink() {
|
||||||
|
window.open(this.card?.referenceData?.openGraphObject?.link)
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
editTitle(id) {
|
||||||
|
this.$emit('edit-title', id)
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
<CardBulletedIcon slot="icon" :size="20" decorative />
|
<CardBulletedIcon slot="icon" :size="20" decorative />
|
||||||
{{ t('deck', 'Card details') }}
|
{{ t('deck', 'Card details') }}
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
|
<NcActionButton v-if="canEdit" :close-after-click="true" @click="editTitle">
|
||||||
|
<template #icon>
|
||||||
|
<PencilIcon :size="20" decorative />
|
||||||
|
</template>
|
||||||
|
{{ t('deck', 'Edit title') }}
|
||||||
|
</NcActionButton>
|
||||||
<NcActionButton v-if="canEdit && !isCurrentUserAssigned"
|
<NcActionButton v-if="canEdit && !isCurrentUserAssigned"
|
||||||
icon="icon-user"
|
icon="icon-user"
|
||||||
:close-after-click="true"
|
:close-after-click="true"
|
||||||
@@ -59,6 +65,7 @@ import { NcActionButton } from '@nextcloud/vue'
|
|||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
|
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
|
||||||
import CardBulletedIcon from 'vue-material-design-icons/CardBulleted.vue'
|
import CardBulletedIcon from 'vue-material-design-icons/CardBulleted.vue'
|
||||||
|
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { getCurrentUser } from '@nextcloud/auth'
|
import { getCurrentUser } from '@nextcloud/auth'
|
||||||
import { showUndo } from '@nextcloud/dialogs'
|
import { showUndo } from '@nextcloud/dialogs'
|
||||||
@@ -68,7 +75,7 @@ import { emit } from '@nextcloud/event-bus'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CardMenuEntries',
|
name: 'CardMenuEntries',
|
||||||
components: { NcActionButton, ArchiveIcon, CardBulletedIcon },
|
components: { NcActionButton, ArchiveIcon, CardBulletedIcon, PencilIcon },
|
||||||
props: {
|
props: {
|
||||||
card: {
|
card: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -79,6 +86,7 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emits: ['edit-title'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
modalShow: false,
|
modalShow: false,
|
||||||
@@ -136,6 +144,9 @@ export default {
|
|||||||
|
|
||||||
this.$root.$emit('open-card', this.card.id)
|
this.$root.$emit('open-card', this.card.id)
|
||||||
},
|
},
|
||||||
|
editTitle() {
|
||||||
|
this.$emit('edit-title', this.card.id)
|
||||||
|
},
|
||||||
deleteCard() {
|
deleteCard() {
|
||||||
this.$store.dispatch('deleteCard', this.card)
|
this.$store.dispatch('deleteCard', this.card)
|
||||||
const undoCard = { ...this.card, deletedAt: 0 }
|
const undoCard = { ...this.card, deletedAt: 0 }
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ export default {
|
|||||||
async updateCardTitle({ commit }, card) {
|
async updateCardTitle({ commit }, card) {
|
||||||
const updatedCard = await apiClient.updateCard(card)
|
const updatedCard = await apiClient.updateCard(card)
|
||||||
commit('updateCardProperty', { property: 'title', card: updatedCard })
|
commit('updateCardProperty', { property: 'title', card: updatedCard })
|
||||||
|
commit('updateCardProperty', { property: 'referenceData', card: updatedCard })
|
||||||
},
|
},
|
||||||
async moveCard({ commit }, card) {
|
async moveCard({ commit }, card) {
|
||||||
const updatedCard = await apiClient.updateCard(card)
|
const updatedCard = await apiClient.updateCard(card)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
70520
|
71221
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class RequestContext implements Context {
|
|||||||
*/
|
*/
|
||||||
public function theResponseShouldBeAListOfObjects() {
|
public function theResponseShouldBeAListOfObjects() {
|
||||||
$jsonResponse = $this->getResponseBodyFromJson();
|
$jsonResponse = $this->getResponseBodyFromJson();
|
||||||
Assert::assertEquals(array_keys($jsonResponse), range(0, count($jsonResponse) - 1));
|
Assert::assertEquals(range(0, count($jsonResponse) - 1), array_keys($jsonResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 = '';
|
$output = '';
|
||||||
$arrayIsList = array_keys($array) === range(0, count($array) - 1);
|
$arrayIsList = array_keys($array) === range(0, count($array) - 1);
|
||||||
foreach ($array as $key => $value) {
|
foreach ($array as $key => $value) {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class CardTest extends TestCase {
|
|||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
'done' => null,
|
'done' => null,
|
||||||
|
'referenceData' => null,
|
||||||
], (new CardDetails($card))->jsonSerialize());
|
], (new CardDetails($card))->jsonSerialize());
|
||||||
}
|
}
|
||||||
public function testJsonSerializeLabels() {
|
public function testJsonSerializeLabels() {
|
||||||
@@ -118,6 +119,7 @@ class CardTest extends TestCase {
|
|||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
'done' => false,
|
'done' => false,
|
||||||
|
'referenceData' => null,
|
||||||
], (new CardDetails($card))->jsonSerialize());
|
], (new CardDetails($card))->jsonSerialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +150,7 @@ class CardTest extends TestCase {
|
|||||||
'lastEditor' => null,
|
'lastEditor' => null,
|
||||||
'ETag' => $card->getETag(),
|
'ETag' => $card->getETag(),
|
||||||
'done' => false,
|
'done' => false,
|
||||||
|
'referenceData' => null,
|
||||||
], (new CardDetails($card))->jsonSerialize());
|
], (new CardDetails($card))->jsonSerialize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ use OCA\Deck\Notification\NotificationHelper;
|
|||||||
use OCA\Deck\StatusException;
|
use OCA\Deck\StatusException;
|
||||||
use OCA\Deck\Validators\CardServiceValidator;
|
use OCA\Deck\Validators\CardServiceValidator;
|
||||||
use OCP\Activity\IEvent;
|
use OCP\Activity\IEvent;
|
||||||
|
use OCP\Collaboration\Reference\IReferenceManager;
|
||||||
use OCP\Comments\ICommentsManager;
|
use OCP\Comments\ICommentsManager;
|
||||||
use OCP\EventDispatcher\IEventDispatcher;
|
use OCP\EventDispatcher\IEventDispatcher;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
@@ -93,6 +94,8 @@ class CardServiceTest extends TestCase {
|
|||||||
private $logger;
|
private $logger;
|
||||||
/** @var CardServiceValidator|MockObject */
|
/** @var CardServiceValidator|MockObject */
|
||||||
private $cardServiceValidator;
|
private $cardServiceValidator;
|
||||||
|
/** @var IReferenceManager|MockObject */
|
||||||
|
private $referenceManager;
|
||||||
|
|
||||||
/** @var AssignmentService|MockObject */
|
/** @var AssignmentService|MockObject */
|
||||||
private $assignmentService;
|
private $assignmentService;
|
||||||
@@ -119,6 +122,7 @@ class CardServiceTest extends TestCase {
|
|||||||
$this->request = $this->createMock(IRequest::class);
|
$this->request = $this->createMock(IRequest::class);
|
||||||
$this->cardServiceValidator = $this->createMock(CardServiceValidator::class);
|
$this->cardServiceValidator = $this->createMock(CardServiceValidator::class);
|
||||||
$this->assignmentService = $this->createMock(AssignmentService::class);
|
$this->assignmentService = $this->createMock(AssignmentService::class);
|
||||||
|
$this->referenceManager = $this->createMock(IReferenceManager::class);
|
||||||
|
|
||||||
$this->logger->expects($this->any())->method('error');
|
$this->logger->expects($this->any())->method('error');
|
||||||
|
|
||||||
@@ -143,6 +147,7 @@ class CardServiceTest extends TestCase {
|
|||||||
$this->request,
|
$this->request,
|
||||||
$this->cardServiceValidator,
|
$this->cardServiceValidator,
|
||||||
$this->assignmentService,
|
$this->assignmentService,
|
||||||
|
$this->referenceManager,
|
||||||
'user1'
|
'user1'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -207,15 +212,24 @@ class CardServiceTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testCreate() {
|
public function testCreate() {
|
||||||
$card = new Card();
|
$card = Card::fromParams([
|
||||||
$card->setTitle('Card title');
|
'title' => 'Card title',
|
||||||
$card->setOwner('admin');
|
'owner' => 'admin',
|
||||||
$card->setStackId(123);
|
'stackId' => 123,
|
||||||
$card->setOrder(999);
|
'order' => 999,
|
||||||
$card->setType('text');
|
'type' => 'text',
|
||||||
|
]);
|
||||||
|
$stack = Stack::fromParams([
|
||||||
|
'id' => 123,
|
||||||
|
'boardId' => 1337,
|
||||||
|
]);
|
||||||
$this->cardMapper->expects($this->once())
|
$this->cardMapper->expects($this->once())
|
||||||
->method('insert')
|
->method('insert')
|
||||||
->willReturn($card);
|
->willReturn($card);
|
||||||
|
$this->stackMapper->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->with(123)
|
||||||
|
->willReturn($stack);
|
||||||
$b = $this->cardService->create('Card title', 123, 'text', 999, 'admin');
|
$b = $this->cardService->create('Card title', 123, 'text', 999, 'admin');
|
||||||
|
|
||||||
$this->assertEquals($b->getTitle(), 'Card title');
|
$this->assertEquals($b->getTitle(), 'Card title');
|
||||||
@@ -270,7 +284,7 @@ class CardServiceTest extends TestCase {
|
|||||||
|
|
||||||
$stackMock = new Stack();
|
$stackMock = new Stack();
|
||||||
$stackMock->setBoardId(1234);
|
$stackMock->setBoardId(1234);
|
||||||
$this->stackMapper->expects($this->once())
|
$this->stackMapper->expects($this->any())
|
||||||
->method('find')
|
->method('find')
|
||||||
->willReturn($stackMock);
|
->willReturn($stackMock);
|
||||||
$b = $this->cardService->create('Card title', 123, 'text', 999, 'admin');
|
$b = $this->cardService->create('Card title', 123, 'text', 999, 'admin');
|
||||||
@@ -293,13 +307,23 @@ class CardServiceTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdate() {
|
public function testUpdate() {
|
||||||
$card = new Card();
|
$card = Card::fromParams([
|
||||||
$card->setTitle('title');
|
'title' => 'Card title',
|
||||||
$card->setArchived(false);
|
'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('find')->willReturn($card);
|
||||||
$this->cardMapper->expects($this->once())->method('update')->willReturnCallback(function ($c) {
|
$this->cardMapper->expects($this->once())->method('update')->willReturnCallback(function ($c) {
|
||||||
return $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);
|
$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('newtitle', $actual->getTitle());
|
||||||
$this->assertEquals(234, $actual->getStackId());
|
$this->assertEquals(234, $actual->getStackId());
|
||||||
|
|||||||
Reference in New Issue
Block a user