diff --git a/cypress/e2e/cardColor.js b/cypress/e2e/cardColor.js new file mode 100644 index 000000000..569e43ff1 --- /dev/null +++ b/cypress/e2e/cardColor.js @@ -0,0 +1,68 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { randUser } from '../utils/index.js' +import { sampleBoard } from '../utils/sampleBoard' + +const user = randUser() +const boardData = sampleBoard() + +const auth = { + user: user.userId, + password: user.password, +} + +const useModal = (useModal) => { + return cy.request({ + method: 'POST', + url: `${Cypress.env('baseUrl')}/ocs/v2.php/apps/deck/api/v1.0/config/cardDetailsInModal?format=json`, + auth, + body: { value: useModal }, + }).then((response) => { + expect(response.status).to.eq(200) + }) +} + +describe('Card color', function () { + let boardId + before(function () { + cy.createUser(user) + cy.login(user) + cy.createExampleBoard({ + user, + board: boardData, + }).then((board) => { + boardId = board.id + }) + }) + + beforeEach(function () { + cy.login(user) + }) + + it('Set a color', function () { + cy.visit(`/apps/deck/#/board/${boardId}`) + + const newCardTitle = 'Card with color' + + cy.get('.button-vue[aria-label*="Add card"]') + .first().click() + cy.get('.stack__card-add form input#new-stack-input-main') + .type(newCardTitle) + cy.get('.stack__card-add form input[type=submit]') + .first().click() + + cy.get(`.card:contains("${newCardTitle}")`).should('be.visible') + .click() + + cy.get('.app-sidebar-header .action-item__menutoggle').click() + cy.get('.v-popper__popper button:contains("Change card color")').click() + cy.get('.color-picker__simple-color-circle').first().should('be.visible') + .click() + cy.get('.color-picker__navigation').find('button:contains("Choose")') + .click() + + cy.get(`.card:contains("${newCardTitle}")`).should('have.css', 'background-color', 'rgb(182, 70, 157)') + }) +}) diff --git a/lib/Controller/CardApiController.php b/lib/Controller/CardApiController.php index d9cf71eea..87f22dbfd 100644 --- a/lib/Controller/CardApiController.php +++ b/lib/Controller/CardApiController.php @@ -68,8 +68,8 @@ public function get() { * * Get a specific card. */ - public function create($title, $type = 'plain', $order = 999, $description = '', $duedate = null, $startdate = null, $labels = [], $users = []) { - $card = $this->cardService->create($title, $this->request->getParam('stackId'), $type, $order, $this->userId, $description, $duedate, $startdate); + public function create($title, $type = 'plain', $order = 999, $description = '', $duedate = null, $startdate = null, $labels = [], $users = [], ?string $color = null) { + $card = $this->cardService->create($title, $this->request->getParam('stackId'), $type, $order, $this->userId, $description, $duedate, $startdate, $color); foreach ($labels as $labelId) { $this->cardService->assignLabel($card->getId(), $labelId); @@ -88,9 +88,9 @@ public function create($title, $type = 'plain', $order = 999, $description = '', #[NoAdminRequired] #[CORS] #[NoCSRFRequired] - public function update(string $title, $type, string $owner, string $description = '', int $order = 0, $duedate = null, $startdate = null, $archived = null): DataResponse { + public function update(string $title, $type, string $owner, string $description = '', int $order = 0, $duedate = null, $startdate = null, $archived = null, ?string $color = null): DataResponse { $done = array_key_exists('done', $this->request->getParams()) ? new OptionalNullableValue($this->request->getParam('done', null)) : null; - $card = $this->cardService->update($this->request->getParam('cardId'), $title, $this->request->getParam('stackId'), $type, $owner, $description, $order, $duedate, 0, $archived, $done, $startdate); + $card = $this->cardService->update($this->request->getParam('cardId'), $title, $this->request->getParam('stackId'), $type, $owner, $description, $order, $duedate, 0, $archived, $done, $startdate, $color); return new DataResponse($card, HTTP::STATUS_OK); } diff --git a/lib/Controller/CardController.php b/lib/Controller/CardController.php index da42d733e..b8c80456d 100644 --- a/lib/Controller/CardController.php +++ b/lib/Controller/CardController.php @@ -46,8 +46,8 @@ public function rename(int $cardId, string $title): Card { } #[NoAdminRequired] - public function create(string $title, int $stackId, string $type = 'plain', int $order = 999, string $description = '', $duedate = null, $startdate = null, array $labels = [], array $users = []): Card { - $card = $this->cardService->create($title, $stackId, $type, $order, $this->userId, $description, $duedate, $startdate); + public function create(string $title, int $stackId, string $type = 'plain', int $order = 999, string $description = '', $duedate = null, $startdate = null, array $labels = [], array $users = [], ?string $color = null): Card { + $card = $this->cardService->create($title, $stackId, $type, $order, $this->userId, $description, $duedate, $startdate, $color); foreach ($labels as $label) { $this->assignLabel($card->getId(), $label); @@ -64,11 +64,11 @@ public function create(string $title, int $stackId, string $type = 'plain', int * @param $duedate */ #[NoAdminRequired] - public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, $archived = null, $startdate = null): Card { + public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, $archived = null, $startdate = null, ?string $color = null): Card { $done = array_key_exists('done', $this->request->getParams()) ? new OptionalNullableValue($this->request->getParam('done', null)) : null; - return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt, $archived, $done, $startdate); + return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt, $archived, $done, $startdate, $color); } #[NoAdminRequired] diff --git a/lib/Controller/CardOcsController.php b/lib/Controller/CardOcsController.php index 36c938a6c..6805e6bd6 100644 --- a/lib/Controller/CardOcsController.php +++ b/lib/Controller/CardOcsController.php @@ -35,7 +35,7 @@ public function __construct( #[NoAdminRequired] #[PublicPage] - public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, $startdate = null, ?array $labels = [], ?array $users = []) { + public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, $startdate = null, ?array $labels = [], ?array $users = [], ?string $color = null) { if ($boardId) { $board = $this->boardService->find($boardId, false); if ($board->getExternalId()) { @@ -47,7 +47,7 @@ public function create(string $title, int $stackId, ?int $boardId = null, ?strin if (!$owner) { $owner = $this->userId; } - $card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate, $startdate); + $card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate, $startdate, $color); // foreach ($labels as $label) { // $this->assignLabel($card->getId(), $label); @@ -113,7 +113,7 @@ public function removeLabel(?int $boardId, int $cardId, int $labelId): DataRespo #[NoAdminRequired] #[PublicPage] - public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, int $boardId, array|string|null $owner = null, $archived = null, $startdate = null): DataResponse { + public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, int $boardId, array|string|null $owner = null, $archived = null, $startdate = null, ?string $color = null): DataResponse { $done = array_key_exists('done', $this->request->getParams()) ? new OptionalNullableValue($this->request->getParam('done', null)) : null; @@ -154,7 +154,8 @@ public function update(int $id, string $title, int $stackId, string $type, int $ $deletedAt, $archived, $done, - $startdate + $startdate, + $color )); } diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 443952ce4..ad8e7f67d 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -18,6 +18,8 @@ * @method void setTitle(string $title) * @method string getDescription() * @method string getDescriptionPrev() + * @method string getColor() + * @method string setColor(string $color) * @method int getStackId() * @method void setStackId(int $stackId) * @method int getOrder() @@ -67,6 +69,7 @@ class Card extends RelationalEntity { protected string $title = ''; protected $description; + protected $color = null; protected $descriptionPrev; protected $stackId; protected $type; diff --git a/lib/Migration/Version11002Date20260317170905.php b/lib/Migration/Version11002Date20260317170905.php new file mode 100644 index 000000000..cbc0930d2 --- /dev/null +++ b/lib/Migration/Version11002Date20260317170905.php @@ -0,0 +1,30 @@ +hasTable('deck_cards')) { + $table = $schema->getTable('deck_cards'); + if (!$table->hasColumn('color')) { + $table->addColumn('color', 'string', [ + 'notnull' => false, + 'length' => 6, + ]); + } + } + return $schema; + } +} diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index aa09178d8..877d212c5 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -185,7 +185,7 @@ public function findCalendarEntries(int $boardId): array { * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws BadrequestException */ - public function create(string $title, int $stackId, string $type, int $order, string $owner, string $description = '', $duedate = null, $startdate = null): Card { + public function create(string $title, int $stackId, string $type, int $order, string $owner, string $description = '', $duedate = null, $startdate = null, ?string $color = null): Card { $this->cardServiceValidator->check(compact('title', 'stackId', 'type', 'order', 'owner')); $this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT); @@ -201,6 +201,7 @@ public function create(string $title, int $stackId, string $type, int $order, st $card->setDescription($description); $card->setDuedate($duedate); $card->setStartdate($startdate); + $card->setColor($color); $card = $this->cardMapper->insert($card); $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE, [], $card->getOwner()); @@ -243,7 +244,7 @@ public function delete(int $id): Card { * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws BadRequestException */ - public function update(int $id, string $title, int $stackId, string $type, string $owner, string $description = '', int $order = 0, ?string $duedate = null, ?int $deletedAt = null, ?bool $archived = null, ?OptionalNullableValue $done = null, ?string $startdate = null): Card { + public function update(int $id, string $title, int $stackId, string $type, string $owner, string $description = '', int $order = 0, ?string $duedate = null, ?int $deletedAt = null, ?bool $archived = null, ?OptionalNullableValue $done = null, ?string $startdate = null, ?string $color = null): Card { $this->cardServiceValidator->check(compact('id', 'title', 'stackId', 'type', 'owner', 'order')); $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT, allowDeletedCard: true); @@ -286,6 +287,7 @@ public function update(int $id, string $title, int $stackId, string $type, strin $card->setOrder($order); $card->setDuedate($duedate ? new \DateTime($duedate) : null); $card->setStartdate($startdate ? new \DateTime($startdate) : null); + $card->setColor($color); $resetDuedateNotification = false; if ( $card->getDuedate() === null diff --git a/src/components/cards/CardItem.vue b/src/components/cards/CardItem.vue index 92f2134c9..5132bd19d 100644 --- a/src/components/cards/CardItem.vue +++ b/src/components/cards/CardItem.vue @@ -7,6 +7,7 @@
+ + + + {{ t('deck', 'Change card color') }} + + {{ t('deck', 'Card details') }} @@ -62,11 +70,12 @@
diff --git a/src/store/card.js b/src/store/card.js index 31ce1e8e8..85986d8d0 100644 --- a/src/store/card.js +++ b/src/store/card.js @@ -388,6 +388,11 @@ export default function cardModuleFactory() { const updatedCard = await apiClient.updateCard(card, stack.boardId) commit('updateCardProperty', { property: 'startdate', card: updatedCard }) }, + async updateCardColor({ commit, getters }, card) { + const stack = getters.stackById(card.stackId) + const updatedCard = await apiClient.updateCard(card, stack.boardId) + commit('updateCardProperty', { property: 'color', card: updatedCard }) + }, addCardData({ commit }, cardData) { const card = { ...cardData } diff --git a/tests/data/deck.json b/tests/data/deck.json index 72a5bef07..207de1608 100644 --- a/tests/data/deck.json +++ b/tests/data/deck.json @@ -74,6 +74,7 @@ "id": 114, "title": "1", "description": "", + "color": null, "stackId": 64, "type": "plain", "lastModified": 1689667779, @@ -119,6 +120,7 @@ "id": 115, "title": "2", "description": "", + "color": null, "stackId": 64, "type": "plain", "lastModified": 1689667752, @@ -153,6 +155,7 @@ "id": 116, "title": "3", "description": "", + "color": null, "stackId": 64, "type": "plain", "lastModified": 1689667760, @@ -211,6 +214,7 @@ "id": 117, "title": "4", "description": "", + "color": null, "stackId": 65, "type": "plain", "lastModified": 1689667767, @@ -273,6 +277,7 @@ "id": 118, "title": "5", "description": "", + "color": null, "stackId": 65, "type": "plain", "lastModified": 1689667773, @@ -317,6 +322,7 @@ "id": 119, "title": "6", "description": "# Test description\n\nHello world", + "color": null, "stackId": 65, "type": "plain", "lastModified": 1689667796, @@ -502,6 +508,7 @@ "id": 107, "title": "Write tests", "description": "", + "color": null, "stackId": 61, "type": "plain", "lastModified": 1689667521, @@ -536,6 +543,7 @@ "id": 111, "title": "Write blog post", "description": "", + "color": null, "stackId": 61, "type": "plain", "lastModified": 1689667521, @@ -570,6 +578,7 @@ "id": 112, "title": "Announce feature", "description": "", + "color": null, "stackId": 61, "type": "plain", "lastModified": 1689667527, @@ -604,6 +613,7 @@ "id": 113, "title": "\ud83c\udf89 Party", "description": "", + "color": null, "stackId": 61, "type": "plain", "lastModified": 1689667537, @@ -650,6 +660,7 @@ "id": 108, "title": "Write feature", "description": "", + "color": null, "stackId": 62, "type": "plain", "lastModified": 1689667488, @@ -696,6 +707,7 @@ "id": 109, "title": "Plan feature", "description": "", + "color": null, "stackId": 63, "type": "plain", "lastModified": 1689667506, @@ -730,6 +742,7 @@ "id": 110, "title": "Design feature", "description": "", + "color": null, "stackId": 63, "type": "plain", "lastModified": 1689667506, diff --git a/tests/unit/Db/CardTest.php b/tests/unit/Db/CardTest.php index 4b566bf8c..6e06b8101 100644 --- a/tests/unit/Db/CardTest.php +++ b/tests/unit/Db/CardTest.php @@ -43,6 +43,7 @@ private function createCard() { $card->setOrder(12); $card->setArchived(false); $card->setDone(null); + $card->setColor('ffffff'); // TODO: relation shared labels acl return $card; } @@ -100,6 +101,7 @@ public function testJsonSerialize() { 'ETag' => $card->getETag(), 'done' => null, 'referenceData' => null, + 'color' => 'ffffff', ], (new CardDetails($card))->jsonSerialize()); } public function testJsonSerializeLabels() { @@ -130,6 +132,7 @@ public function testJsonSerializeLabels() { 'ETag' => $card->getETag(), 'done' => false, 'referenceData' => null, + 'color' => 'ffffff', ], (new CardDetails($card))->jsonSerialize()); } @@ -162,6 +165,7 @@ public function testJsonSerializeAsignedUsers() { 'ETag' => $card->getETag(), 'done' => false, 'referenceData' => null, + 'color' => 'ffffff', ], (new CardDetails($card))->jsonSerialize()); } } diff --git a/tests/unit/Service/CardServiceTest.php b/tests/unit/Service/CardServiceTest.php index cd000d7df..b6299b17a 100644 --- a/tests/unit/Service/CardServiceTest.php +++ b/tests/unit/Service/CardServiceTest.php @@ -221,6 +221,7 @@ public function testCreate() { 'order' => 999, 'type' => 'text', 'id' => 0, + 'color' => '00ff00', ]); $stack = Stack::fromParams([ 'id' => 123, @@ -233,13 +234,14 @@ public function testCreate() { ->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', '', null, null, '00ff00'); $this->assertEquals($b->getTitle(), 'Card title'); $this->assertEquals($b->getOwner(), 'admin'); $this->assertEquals($b->getType(), 'text'); $this->assertEquals($b->getOrder(), 999); $this->assertEquals($b->getStackId(), 123); + $this->assertEquals($b->getColor(), '00ff00'); } public function testClone() { @@ -350,6 +352,7 @@ public function testUpdate() { 'title' => 'Card title', 'archived' => 'false', 'stackId' => 234, + 'color' => '00ff00', ]); $stack = Stack::fromParams([ 'id' => 234, @@ -364,13 +367,14 @@ public function testUpdate() { ->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, null, null, null, 'ffffff'); $this->assertEquals('newtitle', $actual->getTitle()); $this->assertEquals(234, $actual->getStackId()); $this->assertEquals('text', $actual->getType()); $this->assertEquals(999, $actual->getOrder()); $this->assertEquals('foo', $actual->getDescription()); $this->assertEquals(new \DateTime('2017-01-01T00:00:00+00:00'), $actual->getDuedate()); + $this->assertEquals('ffffff', $actual->getColor()); } public function testUpdateWithStartdate() {