Skip to content

Commit ac44974

Browse files
authored
Merge pull request #7702 from nextcloud/feat/noid/add-automation-to-mark-card-done
feat: allow automatic setting of 'done' status
2 parents f890d3b + 69fd402 commit ac44974

25 files changed

+1432
-784
lines changed

appinfo/routes.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@
135135
'ocs' => [
136136
['name' => 'board_ocs#index', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'GET'],
137137
['name' => 'board_ocs#read', 'url' => '/api/v{apiVersion}/board/{boardId}', 'verb' => 'GET'],
138-
['name' => 'board_ocs#stacks', 'url' => '/api/v{apiVersion}/stacks/{boardId}', 'verb' => 'GET'],
138+
['name' => 'stack_ocs#index', 'url' => '/api/v{apiVersion}/stacks/{boardId}', 'verb' => 'GET'],
139139
['name' => 'board_ocs#create', 'url' => '/api/v{apiVersion}/boards', 'verb' => 'POST'],
140140
['name' => 'board_ocs#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],
141141

@@ -146,6 +146,7 @@
146146
['name' => 'card_ocs#reorder', 'url' => '/api/v{apiVersion}/cards/{cardId}/reorder', 'verb' => 'PUT'],
147147

148148
['name' => 'stack_ocs#create', 'url' => '/api/v{apiVersion}/stacks', 'verb' => 'POST'],
149+
['name' => 'stack_ocs#setDoneStack', 'url' => '/api/v{apiVersion}/stacks/{stackId}/done', 'verb' => 'PUT'],
149150
['name' => 'stack_ocs#delete', 'url' => '/api/v{apiVersion}/stacks/{stackId}/{boardId}', 'verb' => 'DELETE', 'defaults' => ['boardId' => null]],
150151
['name' => 'stack_ocs#reorder', 'url' => '/api/v{apiVersion}/stacks/{stackId}/reorder', 'verb' => 'PUT'],
151152

lib/Capabilities.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@
1212
use OCP\Capabilities\ICapability;
1313

1414
class Capabilities implements ICapability {
15-
16-
/** @var IAppManager */
17-
private $appManager;
18-
/** @var PermissionService */
19-
private $permissionService;
15+
private IAppManager $appManager;
16+
private PermissionService $permissionService;
2017

2118

2219
public function __construct(IAppManager $appManager, PermissionService $permissionService) {
@@ -27,7 +24,7 @@ public function __construct(IAppManager $appManager, PermissionService $permissi
2724
/**
2825
* Function an app uses to return the capabilities
2926
*
30-
* @return array{deck: array{version: string, canCreateBoards: bool, apiVersions: array<string>}}
27+
* @return array{deck: array{version: string, canCreateBoards: bool, supportsDoneColumn:true, apiVersions: array<string>}}
3128
* @since 8.2.0
3229
*/
3330
public function getCapabilities() {

lib/Controller/BoardOcsController.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
use OCA\Deck\Service\BoardService;
1111
use OCA\Deck\Service\ExternalBoardService;
12-
use OCA\Deck\Service\StackService;
1312
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
1413
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
1514
use OCP\AppFramework\Http\Attribute\PublicPage;
@@ -26,7 +25,6 @@ public function __construct(
2625
private BoardService $boardService,
2726
private ExternalBoardService $externalBoardService,
2827
private LoggerInterface $logger,
29-
private StackService $stackService,
3028
private $userId,
3129
) {
3230
parent::__construct($appName, $request);
@@ -58,20 +56,6 @@ public function create(string $title, string $color): DataResponse {
5856
return new DataResponse($this->boardService->create($title, $this->userId, $color));
5957
}
6058

61-
#[NoAdminRequired]
62-
#[PublicPage]
63-
#[NoCSRFRequired]
64-
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
65-
public function stacks(int $boardId): DataResponse {
66-
$localBoard = $this->boardService->find($boardId, true, true);
67-
// Board on other instance -> get it from other instance
68-
if ($localBoard->getExternalId() !== null) {
69-
return $this->externalBoardService->getExternalStacksFromRemote($localBoard);
70-
} else {
71-
return new DataResponse($this->stackService->findAll($boardId));
72-
}
73-
}
74-
7559
#[NoAdminRequired]
7660
#[NoCSRFRequired]
7761
public function addAcl(int $boardId, int $type, string $participant, bool $permissionEdit, bool $permissionShare, bool $permissionManage, ?string $remote = null): DataResponse {

lib/Controller/StackController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ public function delete(int $stackId): Stack {
7070
public function deleted(int $boardId): array {
7171
return $this->stackService->fetchDeleted($boardId);
7272
}
73+
7374
}

lib/Controller/StackOcsController.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ public function __construct(
2929
parent::__construct($appName, $request);
3030
}
3131

32+
#[NoAdminRequired]
33+
#[PublicPage]
34+
#[NoCSRFRequired]
35+
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
36+
public function index(int $boardId): DataResponse {
37+
$localBoard = $this->boardService->find($boardId, true, true);
38+
if ($localBoard->getExternalId() !== null) {
39+
return $this->externalBoardService->getExternalStacksFromRemote($localBoard);
40+
} else {
41+
return new DataResponse($this->stackService->findAll($boardId));
42+
}
43+
}
44+
3245
#[NoAdminRequired]
3346
#[PublicPage]
3447
#[NoCSRFRequired]
@@ -44,6 +57,20 @@ public function create(string $title, int $boardId, int $order = 0):DataResponse
4457
};
4558
}
4659

60+
#[NoAdminRequired]
61+
#[PublicPage]
62+
#[NoCSRFRequired]
63+
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
64+
public function setDoneStack(int $stackId, int $boardId, bool $isDone): DataResponse {
65+
$board = $this->boardService->find($boardId, false);
66+
if ($board->getExternalId()) {
67+
$result = $this->externalBoardService->setDoneStackOnRemote($board, $stackId, $isDone);
68+
return new DataResponse($result);
69+
}
70+
$this->stackService->setDoneStack($stackId, $boardId, $isDone);
71+
return new DataResponse();
72+
}
73+
4774
#[NoAdminRequired]
4875
#[PublicPage]
4976
#[NoCSRFRequired]

lib/Db/Stack.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ class Stack extends RelationalEntity {
3232
protected $lastModified = 0;
3333
protected $cards = [];
3434
protected $order;
35+
protected $isDoneColumn = false;
3536

3637
public function __construct() {
3738
$this->addType('id', 'integer');
3839
$this->addType('boardId', 'integer');
3940
$this->addType('deletedAt', 'integer');
4041
$this->addType('lastModified', 'integer');
4142
$this->addType('order', 'integer');
43+
$this->addType('isDoneColumn', 'boolean');
4244
}
4345

4446
public function setCards($cards) {

lib/Db/StackMapper.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,36 @@ public function update(Entity $entity): Entity {
108108
return $result;
109109
}
110110

111+
public function clearDoneColumnForBoard(int $boardId): void {
112+
$qb = $this->db->getQueryBuilder();
113+
$qb->update($this->getTableName())
114+
->set('is_done_column', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))
115+
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
116+
->executeStatement();
117+
}
118+
119+
public function setIsDoneColumn(int $stackId, bool $isDone): void {
120+
$qb = $this->db->getQueryBuilder();
121+
$qb->update($this->getTableName())
122+
->set('is_done_column', $qb->createNamedParameter($isDone, IQueryBuilder::PARAM_BOOL))
123+
->where($qb->expr()->eq('id', $qb->createNamedParameter($stackId, IQueryBuilder::PARAM_INT)))
124+
->executeStatement();
125+
}
126+
127+
public function findDoneColumnForBoard(int $boardId): ?Stack {
128+
$qb = $this->db->getQueryBuilder();
129+
$qb->select('*')
130+
->from($this->getTableName())
131+
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
132+
->andWhere($qb->expr()->eq('is_done_column', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)))
133+
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
134+
try {
135+
return $this->findEntity($qb);
136+
} catch (DoesNotExistException|MultipleObjectsReturnedException $e) {
137+
return null;
138+
}
139+
}
140+
111141
public function delete(Entity $entity): Entity {
112142
// delete cards on stack
113143
$this->cardMapper->deleteByStack($entity->getId());
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
namespace OCA\Deck\Migration;
10+
11+
use Closure;
12+
use OCP\Migration\IOutput;
13+
use OCP\Migration\SimpleMigrationStep;
14+
15+
class Version11002Date20260228000000 extends SimpleMigrationStep {
16+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
17+
$schema = $schemaClosure();
18+
19+
if ($schema->hasTable('deck_stacks')) {
20+
$table = $schema->getTable('deck_stacks');
21+
if (!$table->hasColumn('is_done_column')) {
22+
$table->addColumn('is_done_column', 'boolean', [
23+
'notnull' => false,
24+
'default' => false,
25+
]);
26+
}
27+
}
28+
return $schema;
29+
}
30+
}

lib/Service/CardService.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,21 @@ public function reorder(int $id, int $stackId, int $order): array {
432432
throw new StatusException('Operation not allowed. This card is archived.');
433433
}
434434
$changes = new ChangeSet($card);
435+
$oldStackId = $card->getStackId();
435436
$card->setStackId($stackId);
437+
438+
if ($stackId !== $oldStackId) {
439+
$newStack = $this->stackMapper->find($stackId);
440+
if ($newStack->getIsDoneColumn()) {
441+
$card->setDone(new \DateTime());
442+
} else {
443+
$oldStack = $this->stackMapper->find($oldStackId);
444+
if ($oldStack->getIsDoneColumn()) {
445+
$card->setDone(null);
446+
}
447+
}
448+
}
449+
436450
$this->cardMapper->update($card);
437451
$changes->setAfter($card);
438452
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_CARD, $changes, ActivityManager::SUBJECT_CARD_UPDATE);
@@ -535,6 +549,15 @@ public function done(int $id): Card {
535549
$changes = new ChangeSet($card);
536550
$card->setDone(new \DateTime());
537551
$newCard = $this->cardMapper->update($card);
552+
// Auto-move to done column if one is configured and card is not already there
553+
$currentStack = $this->stackMapper->find($newCard->getStackId());
554+
if (!$currentStack->getIsDoneColumn()) {
555+
$doneStack = $this->stackMapper->findDoneColumnForBoard($currentStack->getBoardId());
556+
if ($doneStack !== null) {
557+
$newCard->setStackId($doneStack->getId());
558+
$newCard = $this->cardMapper->update($newCard);
559+
}
560+
}
538561
$this->notificationHelper->markDuedateAsRead($card);
539562
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_DONE);
540563
$this->changeHelper->cardChanged($id, false);

lib/Service/ExternalBoardService.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,39 @@ public function createStackOnRemote(
180180
return $this->localizeRemoteStacks([$stack], $localBoard)[0];
181181
}
182182

183+
public function getRemoteCapabilities(Board $localBoard): array {
184+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
185+
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/cloud/capabilities';
186+
$resp = $this->proxy->get('', '', $url);
187+
$data = $this->proxy->getOCSData($resp);
188+
return $data['capabilities']['deck'] ?? [];
189+
}
190+
191+
public function remoteSupportsCapability(Board $localBoard, string $capability): bool {
192+
$capabilities = $this->getRemoteCapabilities($localBoard);
193+
return !empty($capabilities[$capability]);
194+
}
195+
196+
public function setDoneStackOnRemote(Board $localBoard, int $stackId, bool $isDone): array {
197+
$this->configService->ensureFederationEnabled();
198+
$this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_MANAGE, $this->userId, false, false);
199+
200+
if (!$this->remoteSupportsCapability($localBoard, 'supportsDoneColumn')) {
201+
throw new \Exception('Remote server does not support the done column feature');
202+
}
203+
204+
$shareToken = $localBoard->getShareToken();
205+
$participantCloudId = $this->cloudIdManager->getCloudId($this->userId, null);
206+
$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());
207+
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/stacks/' . $stackId . '/done';
208+
$params = [
209+
'boardId' => $localBoard->getExternalId(),
210+
'isDone' => $isDone,
211+
];
212+
$resp = $this->proxy->put($participantCloudId->getId(), $shareToken, $url, $params);
213+
return $this->proxy->getOcsData($resp);
214+
}
215+
183216
public function deleteStackOnRemote(Board $localBoard, int $stackId): array {
184217
$this->configService->ensureFederationEnabled();
185218
$this->permissionService->checkPermission($this->boardMapper, $localBoard->getId(), Acl::PERMISSION_EDIT, $this->userId, false, false);

0 commit comments

Comments
 (0)