Skip to content

Commit e212f79

Browse files
committed
Fix card insertion position
1 parent 4b351c0 commit e212f79

10 files changed

Lines changed: 213 additions & 16 deletions

File tree

cypress/e2e/cardFeatures.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ const useModal = (useModal) => {
2525
})
2626
}
2727

28+
const addCardsAtTop = (enabled) => {
29+
return cy.request({
30+
method: 'POST',
31+
url: `${Cypress.env('baseUrl')}/ocs/v2.php/apps/deck/api/v1.0/config/stackAddCardAtTop?format=json`,
32+
auth,
33+
body: { value: enabled },
34+
}).then((response) => {
35+
expect(response.status).to.eq(200)
36+
})
37+
}
38+
2839
describe('Card', function () {
2940
let boardId
3041
before(function () {
@@ -63,6 +74,72 @@ describe('Card', function () {
6374
})
6475
})
6576

77+
describe('New card position', function() {
78+
afterEach(function() {
79+
addCardsAtTop(false)
80+
})
81+
82+
it('Adds new cards to the configured side of the list', function() {
83+
const bottomCardTitle = 'Card added at bottom'
84+
const firstTopCardTitle = 'First card added at top'
85+
const secondTopCardTitle = 'Second card added at top'
86+
const finalBottomCardTitle = 'Card added at bottom again'
87+
88+
addCardsAtTop(false)
89+
cy.intercept({ method: 'POST', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards' }).as('createCard')
90+
cy.intercept({ method: 'PUT', url: '**/ocs/v2.php/apps/deck/api/v1.0/cards/*/reorder' }).as('reorderCard')
91+
cy.intercept({ method: 'POST', url: '**/ocs/v2.php/apps/deck/api/v1.0/config/stackAddCardAtTop' }).as('setCardPosition')
92+
cy.visit(`/apps/deck/#/board/${boardId}`)
93+
94+
cy.get('.board .stack').eq(0).within(() => {
95+
cy.get('[data-cy="action:add-card"]').click()
96+
cy.get('.stack__card-add input[type="text"]').type(bottomCardTitle)
97+
cy.get('.stack__card-add input[type="submit"]').click()
98+
cy.wait('@createCard')
99+
cy.get('.card').last().should('contain', bottomCardTitle)
100+
})
101+
102+
cy.get('[data-cy="navigation:settings"]').click()
103+
cy.get('[data-cy="setting:add-card-at-top"] input[role="switch"]').check({ force: true })
104+
cy.wait('@setCardPosition')
105+
cy.visit(`/apps/deck/#/board/${boardId}`)
106+
107+
for (const title of [firstTopCardTitle, secondTopCardTitle]) {
108+
cy.get('.board .stack').eq(0).within(() => {
109+
cy.get('[data-cy="action:add-card"]').click()
110+
cy.get('.stack__card-add input[type="text"]').type(title)
111+
cy.get('.stack__card-add input[type="submit"]').click()
112+
cy.wait('@createCard')
113+
cy.get('.stack__card-add input[type="text"]').type('{esc}')
114+
})
115+
}
116+
117+
cy.get('.board .stack').eq(0).within(() => {
118+
cy.get('.card').eq(0).should('contain', secondTopCardTitle)
119+
cy.get('.card').eq(1).should('contain', firstTopCardTitle)
120+
})
121+
cy.get('@reorderCard.all').should('have.length', 0)
122+
123+
cy.reload()
124+
cy.get('.board .stack').eq(0).within(() => {
125+
cy.get('.card').eq(0).should('contain', secondTopCardTitle)
126+
cy.get('.card').eq(1).should('contain', firstTopCardTitle)
127+
})
128+
129+
cy.get('[data-cy="navigation:settings"]').click()
130+
cy.get('[data-cy="setting:add-card-at-top"] input[role="switch"]').uncheck({ force: true })
131+
cy.wait('@setCardPosition')
132+
cy.visit(`/apps/deck/#/board/${boardId}`)
133+
cy.get('.board .stack').eq(0).within(() => {
134+
cy.get('[data-cy="action:add-card"]').click()
135+
cy.get('.stack__card-add input[type="text"]').type(finalBottomCardTitle)
136+
cy.get('.stack__card-add input[type="submit"]').click()
137+
cy.wait('@createCard')
138+
cy.get('.card').last().should('contain', finalBottomCardTitle)
139+
})
140+
})
141+
})
142+
66143
it('Create card from overview', function () {
67144
cy.visit(`/apps/deck/#/`)
68145
const newCardTitle = 'Test create from overview'

lib/Controller/CardOcsController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function create(string $title, int $stackId, ?int $boardId = null, ?strin
4848
if (!$owner) {
4949
$owner = $this->userId;
5050
}
51-
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate, $startdate, $color);
51+
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate, $startdate, $color, insertAtPosition: $order !== 999);
5252

5353
// foreach ($labels as $label) {
5454
// $this->assignLabel($card->getId(), $label);

lib/Service/CardService.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use OCP\Collaboration\Reference\IReferenceManager;
3131
use OCP\Comments\ICommentsManager;
3232
use OCP\EventDispatcher\IEventDispatcher;
33+
use OCP\IDBConnection;
3334
use OCP\IRequest;
3435
use OCP\IURLGenerator;
3536
use OCP\IUserManager;
@@ -58,6 +59,7 @@ public function __construct(
5859
private CardServiceValidator $cardServiceValidator,
5960
private AssignmentService $assignmentService,
6061
private IReferenceManager $referenceManager,
62+
private IDBConnection $connection,
6163
private ?string $userId,
6264
) {
6365
}
@@ -187,7 +189,7 @@ public function findCalendarEntries(int $boardId): array {
187189
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
188190
* @throws BadrequestException
189191
*/
190-
public function create(string $title, int $stackId, string $type, int $order, string $owner, string $description = '', $duedate = null, $startdate = null, ?string $color = null): Card {
192+
public function create(string $title, int $stackId, string $type, int $order, string $owner, string $description = '', $duedate = null, $startdate = null, ?string $color = null, bool $insertAtPosition = false): Card {
191193
$this->cardServiceValidator->check(compact('title', 'stackId', 'type', 'order', 'owner'));
192194

193195
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT);
@@ -204,7 +206,20 @@ public function create(string $title, int $stackId, string $type, int $order, st
204206
$card->setDuedate($duedate);
205207
$card->setStartdate($startdate);
206208
$card->setColor($color);
207-
$card = $this->cardMapper->insert($card);
209+
210+
if (!$insertAtPosition) {
211+
$card = $this->cardMapper->insert($card);
212+
} else {
213+
$this->connection->beginTransaction();
214+
try {
215+
$card = $this->cardMapper->insert($card);
216+
$this->reorderCards($card->getId(), $stackId, $order);
217+
$this->connection->commit();
218+
} catch (\Throwable $e) {
219+
$this->connection->rollBack();
220+
throw $e;
221+
}
222+
}
208223

209224
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE, [], $card->getOwner());
210225
$this->changeHelper->cardChanged($card->getId(), false);
@@ -467,6 +482,17 @@ public function reorder(int $id, int $stackId, int $order): array {
467482
$changes->setAfter($card);
468483
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_CARD, $changes, ActivityManager::SUBJECT_CARD_UPDATE);
469484

485+
$result = $this->reorderCards($id, $stackId, $order);
486+
$this->changeHelper->cardChanged($id, false);
487+
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore()));
488+
489+
return $result;
490+
}
491+
492+
/**
493+
* @return list<Card>
494+
*/
495+
private function reorderCards(int $id, int $stackId, int $order): array {
470496
$cardsToReorder = $this->cardMapper->findAll($stackId);
471497
$result = [];
472498
$i = 0;
@@ -489,8 +515,6 @@ public function reorder(int $id, int $stackId, int $order): array {
489515
$this->cardMapper->update($cardToReorder);
490516
$result[$cardToReorder->getOrder()] = $cardToReorder;
491517
}
492-
$this->changeHelper->cardChanged($id, false);
493-
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore()));
494518

495519
return array_values($result);
496520
}

src/components/DeckAppSettings.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<NcFormBoxSwitch v-model="cardDetailsInModal"
1414
:label="t('deck', 'Use bigger card view')" />
1515
<NcFormBoxSwitch v-model="stackAddCardAtTop"
16+
data-cy="setting:add-card-at-top"
1617
:label="t('deck', 'Add new cards at the top of a list')" />
1718
</NcFormBox>
1819
</NcAppSettingsSection>

src/components/board/StackCardAdd.vue

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ import ClickOutside from 'vue-click-outside'
4444
import PlusIcon from 'vue-material-design-icons/Plus.vue'
4545
import { NcButton } from '@nextcloud/vue'
4646
import { showError } from '@nextcloud/dialogs'
47+
import { mapActions } from 'pinia'
48+
49+
import { useCardStore } from '../../stores/card.js'
4750
4851
export default {
4952
name: 'StackCardAdd',
@@ -87,24 +90,23 @@ export default {
8790
},
8891
},
8992
methods: {
93+
...mapActions(useCardStore, {
94+
addCardInStore: 'addCard',
95+
}),
9096
close() {
9197
this.visible = false
9298
},
9399
async addCard() {
94100
this.creating = true
95101
this.$emit('creating')
96102
try {
97-
const newCard = await this.$store.dispatch('addCard', {
103+
const newCard = await this.addCardInStore({
98104
title: this.title,
99105
stackId: this.stack.id,
100106
boardId: this.stack.boardId,
101107
// Without an order the API appends the card to the end of the stack
102108
...(this.addAtTop ? { order: 0 } : {}),
103109
})
104-
if (this.addAtTop) {
105-
// Creating a card does not move the existing cards down, so reorder
106-
await this.$store.dispatch('reorderCard', { ...newCard, order: 0 })
107-
}
108110
this.title = ''
109111
this.$emit('created', newCard)
110112
if (!this.cardDetailsInModal) {

src/components/navigation/AppNavigation.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<template #footer>
5454
<ul class="app-navigation-entry__settings">
5555
<NcAppNavigationItem :name="t('deck', 'Deck settings')"
56+
data-cy="navigation:settings"
5657
@click.prevent.stop="openSettings">
5758
<template #icon>
5859
<IconCog :size="20" />

src/stores/card.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,13 @@ export const useCardStore = defineStore('card', {
280280
},
281281
async addCard(card) {
282282
const createdCard = await apiClient.addCard(card)
283+
if (card.order !== undefined) {
284+
for (const existingCard of this.cards) {
285+
if (existingCard.stackId === createdCard.stackId && existingCard.order >= card.order) {
286+
Vue.set(existingCard, 'order', existingCard.order + 1)
287+
}
288+
}
289+
}
283290
this.addCardToStore(createdCard)
284291
return createdCard
285292
},
@@ -302,9 +309,6 @@ export const useCardStore = defineStore('card', {
302309
let i = 0
303310
const newCards = []
304311
for (const c of this.cardsByStack(card.stackId)) {
305-
if (c.id === card.id) {
306-
newCards.push(card)
307-
}
308312
if (i === card.order) {
309313
i++
310314
}
@@ -316,9 +320,9 @@ export const useCardStore = defineStore('card', {
316320
this.updateCardsReorder(newCards)
317321

318322
const stack = useStackStore().stackById(card.stackId)
319-
apiClient.reorderCard(card, stack.boardId).then((cards) => {
320-
this.updateCardsReorder(Object.values(cards))
321-
})
323+
const cards = await apiClient.reorderCard(card, stack.boardId)
324+
this.updateCardsReorder(Object.values(cards))
325+
return cards
322326
},
323327
async archiveUnarchiveCard(card) {
324328
let call = 'archiveCard'

tests/integration/features/api/config.feature

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Feature: OCS API - Config
1515
And the response should contain the key "ocs.data.calendar"
1616
And the response should contain the key "ocs.data.cardDetailsInModal"
1717
And the response should contain the key "ocs.data.cardIdBadge"
18+
And the response should contain the key "ocs.data.stackAddCardAtTop"
19+
And the response value "ocs.data.stackAddCardAtTop" should be "false"
1820

1921
Scenario: GET /api/v1.0/config - The group limit is only exposed to administrators
2022
When sending "GET" to the OCS API endpoint "/config"
@@ -39,6 +41,20 @@ Feature: OCS API - Config
3941
When sending "GET" to the OCS API endpoint "/config"
4042
Then the response value "ocs.data.calendar" should be "true"
4143

44+
Scenario: POST /api/v1.0/config/{key} - Set the new card position
45+
When sending "POST" to the OCS API endpoint "/config/stackAddCardAtTop" with body:
46+
| value | true |
47+
Then the response should have a status code "200"
48+
And the response value "ocs.data" should be "true"
49+
When sending "GET" to the OCS API endpoint "/config"
50+
Then the response value "ocs.data.stackAddCardAtTop" should be "true"
51+
When sending "POST" to the OCS API endpoint "/config/stackAddCardAtTop" with body:
52+
| value | false |
53+
Then the response should have a status code "200"
54+
And the response value "ocs.data" should be "false"
55+
When sending "GET" to the OCS API endpoint "/config"
56+
Then the response value "ocs.data.stackAddCardAtTop" should be "false"
57+
4258
Scenario: POST /api/v1.0/config/{key} - Set a board config value
4359
Given sending "POST" to the API endpoint "/boards" with body:
4460
| title | Config board |

tests/unit/Service/BatchQueryPerformanceTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use OCP\Collaboration\Reference\IReferenceManager;
4343
use OCP\Comments\ICommentsManager;
4444
use OCP\EventDispatcher\IEventDispatcher;
45+
use OCP\IDBConnection;
4546
use OCP\IL10N;
4647
use OCP\IRequest;
4748
use OCP\IURLGenerator;
@@ -380,6 +381,7 @@ private function setUpCardService(): void {
380381
$this->createMock(CardServiceValidator::class),
381382
$this->createMock(AssignmentService::class),
382383
$this->referenceManager,
384+
$this->createMock(IDBConnection::class),
383385
'user1',
384386
);
385387
}

0 commit comments

Comments
 (0)