Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2ae01d3
feat: add owner_type field to boards for circle ownership support
jospoortvliet Apr 29, 2026
3629a1e
feat: wire circle ownership into BoardMapper
jospoortvliet Apr 29, 2026
67960bd
feat: grant owner-level permissions to circle members on circle-owned…
jospoortvliet Apr 29, 2026
003221f
feat: support transferring board ownership to a circle in BoardService
jospoortvliet Apr 29, 2026
1cb1761
feat: accept newOwnerType in the transfer-ownership REST endpoint
jospoortvliet Apr 29, 2026
92e5075
feat: add --to-circle flag to deck:transfer-ownership OCC command
jospoortvliet Apr 29, 2026
d2b7924
feat: show team icon for circle-owned boards and add Transfer ownersh…
jospoortvliet Apr 29, 2026
f0136b1
fix: use Member::TYPE_USER constant instead of magic integer 1
jospoortvliet Apr 29, 2026
8806ca4
fix: scope user-owner queries to owner_type = PERMISSION_TYPE_USER
jospoortvliet Apr 29, 2026
534be45
perf: cache getUserCircles result per user within a request
jospoortvliet Apr 29, 2026
5d4bd01
refactor: centralise ACL permission type constants in src/helpers/con…
jospoortvliet Apr 29, 2026
aaa0028
refactor: remove redundant owner equality check in transferOwnership
jospoortvliet Apr 29, 2026
1507865
chore: drop explicit unsigned=false from migration column definition
jospoortvliet Apr 29, 2026
073846c
Fix issue in SharingTabSidebar & add migration back
jospoortvliet Apr 30, 2026
7dae6b0
fix: show Transfer ownership button for circle/team ACL entries
jospoortvliet Apr 30, 2026
68474e2
add the package lot that was missed earlier, and update .gitignore to…
jospoortvliet May 1, 2026
ebe4d7d
feat: support team-aware ownership transfer and notifications
jospoortvliet May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ tests/integration/composer.lock
tests/.phpunit.result.cache
vendor/
.php_cs.cache
.php-cs-fixer.cache
\.idea/
settings.json
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ All notable changes to this project will be documented in this file.
## 1.16.0-beta.1

### Added
- feat: add owner_type to boards for circle ownership support @jospoortvliet
- feat: resolve circle board owners in BoardMapper and include circle-owned boards in user board queries @jospoortvliet
- feat: grant full owner permissions to circle members on circle-owned boards @jospoortvliet
- feat: support transferring board ownership to a circle in BoardService @jospoortvliet
- feat: accept newOwnerType in the transfer-ownership REST endpoint @jospoortvliet
- feat: add --to-circle flag to deck:transfer-ownership OCC command @jospoortvliet
- feat: show team icon for circle-owned boards and add Transfer ownership button in sharing sidebar @jospoortvliet
- feat: update default content @luka-nextcloud [#6740](https://github.com/nextcloud/deck/pull/6740)
- feat: add board import and export @luka-nextcloud [#6872](https://github.com/nextcloud/deck/pull/6872)
- feat: use outline icons @luka-nextcloud [#7114](https://github.com/nextcloud/deck/pull/7114)
Expand Down
85 changes: 72 additions & 13 deletions lib/Command/TransferOwnership.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
*/
namespace OCA\Deck\Command;

use OCA\Deck\Db\Acl;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\CirclesService;
use OCA\Deck\Service\PermissionService;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
Expand All @@ -22,14 +25,18 @@ final class TransferOwnership extends Command {
protected $boardMapper;
protected $permissionService;
protected $questionHelper;
protected $userManager;
protected $circlesService;

public function __construct(BoardService $boardService, BoardMapper $boardMapper, PermissionService $permissionService, QuestionHelper $questionHelper) {
public function __construct(BoardService $boardService, BoardMapper $boardMapper, PermissionService $permissionService, QuestionHelper $questionHelper, IUserManager $userManager, CirclesService $circlesService) {
parent::__construct();

$this->boardService = $boardService;
$this->boardMapper = $boardMapper;
$this->permissionService = $permissionService;
$this->questionHelper = $questionHelper;
$this->userManager = $userManager;
$this->circlesService = $circlesService;
}

protected function configure() {
Expand Down Expand Up @@ -57,15 +64,62 @@ protected function configure() {
InputOption::VALUE_NONE,
'Reassign card details of the old owner to the new one'
)
->addOption(
'to-team',
null,
InputOption::VALUE_NONE,
'Treat <newOwner> as a team ID (internally stored as a circle ID) instead of a user UID'
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$owner = $input->getArgument('owner');
$newOwner = $input->getArgument('newOwner');
$boardId = $input->getArgument('boardId');

$remapAssignment = $input->getOption('remap');
$toTeam = $input->getOption('to-team');
$newOwnerType = Acl::PERMISSION_TYPE_USER;
$teamDisplayName = null;
if ($toTeam) {
$newOwnerType = Acl::PERMISSION_TYPE_CIRCLE;
if ($this->circlesService->isCirclesEnabled()) {
try {
$circle = $this->circlesService->getCircle($newOwner);
if ($circle !== null) {
$teamDisplayName = $circle->getDisplayName();
}
} catch (\Throwable $e) {
$teamDisplayName = null;
}
}
} else {
$userExists = $this->userManager->userExists($newOwner);
$circleExists = false;
$circle = null;
if ($this->circlesService->isCirclesEnabled()) {
try {
$circle = $this->circlesService->getCircle($newOwner);
$circleExists = $circle !== null;
if ($circle !== null) {
$teamDisplayName = $circle->getDisplayName();
}
} catch (\Throwable $e) {
$circleExists = false;
}
}

if ($userExists && $circleExists) {
$output->writeln('<error>Ambiguous target: ' . $newOwner . ' matches both a user and a team (circle ID). Use --to-team to transfer to the team.</error>');
return 1;
}

if ($circleExists && !$userExists) {
$newOwnerType = Acl::PERMISSION_TYPE_CIRCLE;
$output->writeln('<comment>Detected team target: treating ' . $newOwner . ' as team ' . ($teamDisplayName ?: $newOwner) . '.</comment>');
}
}
$newOwnerLabel = $newOwnerType === Acl::PERMISSION_TYPE_CIRCLE ? 'team ' . ($teamDisplayName ?: $newOwner) : $newOwner;

$this->boardService->setUserId($owner);
$this->permissionService->setUserId($owner);
Expand All @@ -83,26 +137,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

if ($boardId) {
$output->writeln('Transfer board ' . $board->getTitle() . ' from ' . $board->getOwner() . " to $newOwner");
$output->writeln('Transfer board ' . $board->getTitle() . ' from ' . $board->getOwner() . " to $newOwnerLabel");
} else {
$output->writeln("Transfer all boards from $owner to $newOwner");
$output->writeln("Transfer all boards from $owner to $newOwnerLabel");
}

$question = new ConfirmationQuestion('Do you really want to continue? (y/n) ', false);
if (!$this->questionHelper->ask($input, $output, $question)) {
return 1;
}

if ($boardId) {
$this->boardService->transferBoardOwnership($boardId, $newOwner, $remapAssignment);
$output->writeln('<info>Board ' . $board->getTitle() . ' from ' . $board->getOwner() . " transferred to $newOwner completed</info>");
return 0;
}

foreach ($this->boardService->transferOwnership($owner, $newOwner, $remapAssignment) as $board) {
$output->writeln(' - ' . $board->getTitle() . ' transferred');
try {
if ($boardId) {
$this->boardService->transferBoardOwnership($boardId, $newOwner, $remapAssignment, $newOwnerType);
$output->writeln('<info>Board ' . $board->getTitle() . " transferred to $newOwnerLabel</info>");
return 0;
}

foreach ($this->boardService->transferOwnership($owner, $newOwner, $remapAssignment, $newOwnerType) as $board) {
$output->writeln(' - ' . $board->getTitle() . ' transferred');
}
$output->writeln("<info>All boards from $owner transferred to $newOwnerLabel</info>");
} catch (\Exception $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
$output->writeln("<info>All boards from $owner to $newOwner transferred</info>");

return 0;
}
Expand Down
8 changes: 5 additions & 3 deletions lib/Controller/BoardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ public function clone(int $boardId, bool $withCards = false, bool $withAssignmen
/**
* @NoAdminRequired
*/
public function transferOwner(int $boardId, string $newOwner): DataResponse {
public function transferOwner(int $boardId, string $newOwner, int $newOwnerType = Acl::PERMISSION_TYPE_USER): DataResponse {
if ($newOwnerType !== Acl::PERMISSION_TYPE_USER && $newOwnerType !== Acl::PERMISSION_TYPE_CIRCLE) {
return new DataResponse(['message' => 'Invalid owner type'], HTTP::STATUS_BAD_REQUEST);
}
if ($this->permissionService->userIsBoardOwner($boardId, $this->userId)) {
return new DataResponse($this->boardService->transferBoardOwnership($boardId, $newOwner), HTTP::STATUS_OK);
return new DataResponse($this->boardService->transferBoardOwnership($boardId, $newOwner, false, $newOwnerType), HTTP::STATUS_OK);
}

return new DataResponse([], HTTP::STATUS_UNAUTHORIZED);
}

Expand Down
4 changes: 4 additions & 0 deletions lib/Db/Board.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
* @method void setLastModified(int $lastModified)
* @method string getOwner()
* @method void setOwner(string $owner)
* @method int getOwnerType()
* @method void setOwnerType(int $ownerType)
* @method string getColor()
* @method void setColor(string $color)
* @method void setShareToken(string $shareToken)
Expand All @@ -32,6 +34,7 @@
class Board extends RelationalEntity {
protected $title;
protected $owner;
protected $ownerType = 0;
protected $color;
protected $archived = false;
/** @var Label[]|null */
Expand All @@ -53,6 +56,7 @@ class Board extends RelationalEntity {
public function __construct() {
$this->addType('id', 'integer');
$this->addType('shared', 'integer');
$this->addType('ownerType', 'integer');
$this->addType('archived', 'boolean');
$this->addType('deletedAt', 'integer');
$this->addType('lastModified', 'integer');
Expand Down
Loading