Merge pull request #6933 from nextcloud/fix/label-card-limit
fix: Ensure consistent label mappings
This commit is contained in:
@@ -54,6 +54,9 @@
|
|||||||
<live-migration>
|
<live-migration>
|
||||||
<step>OCA\Deck\Migration\DeletedCircleCleanup</step>
|
<step>OCA\Deck\Migration\DeletedCircleCleanup</step>
|
||||||
</live-migration>
|
</live-migration>
|
||||||
|
<post-migration>
|
||||||
|
<step>OCA\Deck\Migration\LabelMismatchCleanup</step>
|
||||||
|
</post-migration>
|
||||||
</repair-steps>
|
</repair-steps>
|
||||||
<commands>
|
<commands>
|
||||||
<command>OCA\Deck\Command\UserExport</command>
|
<command>OCA\Deck\Command\UserExport</command>
|
||||||
|
|||||||
78
lib/Migration/LabelMismatchCleanup.php
Normal file
78
lib/Migration/LabelMismatchCleanup.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
namespace OCA\Deck\Migration;
|
||||||
|
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
|
use OCP\Migration\IRepairStep;
|
||||||
|
|
||||||
|
class LabelMismatchCleanup implements IRepairStep {
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private IDBConnection $db,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
return 'Migrate labels with wrong board mapping';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(IOutput $output) {
|
||||||
|
// Find assingments where a label of another (wrong) board is used
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('al.id', 'al.label_id', 'al.card_id', 's.board_id as actual_board_id', 'l.board_id as wrong_id', 'l.color', 'l.title')
|
||||||
|
->from('deck_assigned_labels', 'al')
|
||||||
|
->innerJoin('al', 'deck_cards', 'c', 'c.id = al.card_id')
|
||||||
|
->innerJoin('c', 'deck_stacks', 's', 'c.stack_id = s.id')
|
||||||
|
->innerJoin('al', 'deck_labels', 'l', 'l.id = al.label_id')
|
||||||
|
->where($qb->expr()->neq('l.board_id', 's.board_id'));
|
||||||
|
|
||||||
|
$labels = $qb->executeQUery()->fetchAll();
|
||||||
|
if (count($labels) === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->info('Found ' . count($labels) . ' labels with wrong board mapping');
|
||||||
|
|
||||||
|
foreach ($labels as $label) {
|
||||||
|
// Select existing label on the correct board
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('id')
|
||||||
|
->from('deck_labels')
|
||||||
|
->where($qb->expr()->eq('title', $qb->createNamedParameter($label['title'])))
|
||||||
|
->andWhere($qb->expr()->eq('color', $qb->createNamedParameter($label['color'])))
|
||||||
|
->andWhere($qb->expr()->eq('board_id', $qb->createNamedParameter($label['actual_board_id'])));
|
||||||
|
$result = $qb->executeQuery();
|
||||||
|
$newLabel = $result->fetchOne();
|
||||||
|
$result->closeCursor();
|
||||||
|
|
||||||
|
if (!$newLabel) {
|
||||||
|
// Create a new label with the same title and color on the correct board
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->insert('deck_labels')
|
||||||
|
->values([
|
||||||
|
'title' => $qb->createNamedParameter($label['title']),
|
||||||
|
'color' => $qb->createNamedParameter($label['color']),
|
||||||
|
'board_id' => $qb->createNamedParameter($label['actual_board_id']),
|
||||||
|
]);
|
||||||
|
$qb->executeStatement();
|
||||||
|
$newLabel = $qb->getLastInsertId();
|
||||||
|
$output->debug('Created new label ' . $label['title'] . ' on board ' . $label['actual_board_id']);
|
||||||
|
} else {
|
||||||
|
$output->debug('Found existing label ' . $label['title'] . ' on board ' . $label['actual_board_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the assignment to use the new label
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->update('deck_assigned_labels')
|
||||||
|
->set('label_id', $qb->createNamedParameter($newLabel))
|
||||||
|
->where($qb->expr()->eq('id', $qb->createNamedParameter($label['id'])));
|
||||||
|
$qb->executeStatement();
|
||||||
|
$output->debug('Updated label assignment ' . $label['id'] . ' to use label ' . $newLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -609,8 +609,9 @@ class CardService {
|
|||||||
public function assignLabel($cardId, $labelId) {
|
public function assignLabel($cardId, $labelId) {
|
||||||
$this->cardServiceValidator->check(compact('cardId', 'labelId'));
|
$this->cardServiceValidator->check(compact('cardId', 'labelId'));
|
||||||
|
|
||||||
|
|
||||||
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
|
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
|
||||||
|
$this->permissionService->checkPermission($this->labelMapper, $labelId, Acl::PERMISSION_READ);
|
||||||
|
|
||||||
if ($this->boardService->isArchived($this->cardMapper, $cardId)) {
|
if ($this->boardService->isArchived($this->cardMapper, $cardId)) {
|
||||||
throw new StatusException('Operation not allowed. This board is archived.');
|
throw new StatusException('Operation not allowed. This board is archived.');
|
||||||
}
|
}
|
||||||
@@ -619,6 +620,9 @@ class CardService {
|
|||||||
throw new StatusException('Operation not allowed. This card is archived.');
|
throw new StatusException('Operation not allowed. This card is archived.');
|
||||||
}
|
}
|
||||||
$label = $this->labelMapper->find($labelId);
|
$label = $this->labelMapper->find($labelId);
|
||||||
|
if ($label->getBoardId() !== $this->cardMapper->findBoardId($card->getId())) {
|
||||||
|
throw new StatusException('Operation not allowed. Label does not exist.');
|
||||||
|
}
|
||||||
$this->cardMapper->assignLabel($cardId, $labelId);
|
$this->cardMapper->assignLabel($cardId, $labelId);
|
||||||
$this->changeHelper->cardChanged($cardId);
|
$this->changeHelper->cardChanged($cardId);
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_ASSIGN, ['label' => $label]);
|
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_ASSIGN, ['label' => $label]);
|
||||||
@@ -640,6 +644,8 @@ class CardService {
|
|||||||
|
|
||||||
|
|
||||||
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
|
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
|
||||||
|
$this->permissionService->checkPermission($this->labelMapper, $labelId, Acl::PERMISSION_READ);
|
||||||
|
|
||||||
if ($this->boardService->isArchived($this->cardMapper, $cardId)) {
|
if ($this->boardService->isArchived($this->cardMapper, $cardId)) {
|
||||||
throw new StatusException('Operation not allowed. This board is archived.');
|
throw new StatusException('Operation not allowed. This board is archived.');
|
||||||
}
|
}
|
||||||
@@ -648,6 +654,9 @@ class CardService {
|
|||||||
throw new StatusException('Operation not allowed. This card is archived.');
|
throw new StatusException('Operation not allowed. This card is archived.');
|
||||||
}
|
}
|
||||||
$label = $this->labelMapper->find($labelId);
|
$label = $this->labelMapper->find($labelId);
|
||||||
|
if ($label->getBoardId() !== $this->cardMapper->findBoardId($card->getId())) {
|
||||||
|
throw new StatusException('Operation not allowed. Label does not exist.');
|
||||||
|
}
|
||||||
$this->cardMapper->removeLabel($cardId, $labelId);
|
$this->cardMapper->removeLabel($cardId, $labelId);
|
||||||
$this->changeHelper->cardChanged($cardId);
|
$this->changeHelper->cardChanged($cardId);
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_UNASSING, ['label' => $label]);
|
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_UNASSING, ['label' => $label]);
|
||||||
|
|||||||
@@ -258,6 +258,17 @@ class CardServiceTest extends TestCase {
|
|||||||
->method('find')
|
->method('find')
|
||||||
->willReturn($card, $clonedCard);
|
->willReturn($card, $clonedCard);
|
||||||
|
|
||||||
|
$this->cardMapper->expects($this->any())
|
||||||
|
->method('findBoardId')
|
||||||
|
->willReturn(1234);
|
||||||
|
|
||||||
|
$this->labelMapper->expects($this->any())
|
||||||
|
->method('find')
|
||||||
|
->willReturn(Label::fromRow([
|
||||||
|
'id' => 1,
|
||||||
|
'boardId' => 1234,
|
||||||
|
]));
|
||||||
|
|
||||||
// check if users are assigned
|
// check if users are assigned
|
||||||
$this->assignmentService->expects($this->once())
|
$this->assignmentService->expects($this->once())
|
||||||
->method('assignUser')
|
->method('assignUser')
|
||||||
@@ -433,8 +444,17 @@ class CardServiceTest extends TestCase {
|
|||||||
public function testAssignLabel() {
|
public function testAssignLabel() {
|
||||||
$card = new Card();
|
$card = new Card();
|
||||||
$card->setArchived(false);
|
$card->setArchived(false);
|
||||||
|
$card->setId(123);
|
||||||
|
$label = new Label();
|
||||||
|
$label->setBoardId(1);
|
||||||
$this->cardMapper->expects($this->once())->method('find')->willReturn($card);
|
$this->cardMapper->expects($this->once())->method('find')->willReturn($card);
|
||||||
$this->cardMapper->expects($this->once())->method('assignLabel');
|
$this->cardMapper->expects($this->once())->method('assignLabel');
|
||||||
|
$this->cardMapper->expects($this->once())
|
||||||
|
->method('findBoardId')
|
||||||
|
->willReturn(1);
|
||||||
|
$this->labelMapper->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->willReturn($label);
|
||||||
$this->cardService->assignLabel(123, 999);
|
$this->cardService->assignLabel(123, 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,8 +470,17 @@ class CardServiceTest extends TestCase {
|
|||||||
public function testRemoveLabel() {
|
public function testRemoveLabel() {
|
||||||
$card = new Card();
|
$card = new Card();
|
||||||
$card->setArchived(false);
|
$card->setArchived(false);
|
||||||
|
$card->setId(123);
|
||||||
|
$label = new Label();
|
||||||
|
$label->setBoardId(1);
|
||||||
$this->cardMapper->expects($this->once())->method('find')->willReturn($card);
|
$this->cardMapper->expects($this->once())->method('find')->willReturn($card);
|
||||||
$this->cardMapper->expects($this->once())->method('removeLabel');
|
$this->cardMapper->expects($this->once())->method('removeLabel');
|
||||||
|
$this->cardMapper->expects($this->once())
|
||||||
|
->method('findBoardId')
|
||||||
|
->willReturn(1);
|
||||||
|
$this->labelMapper->expects($this->once())
|
||||||
|
->method('find')
|
||||||
|
->willReturn($label);
|
||||||
$this->cardService->removeLabel(123, 999);
|
$this->cardService->removeLabel(123, 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user