From 6879b0e28ea465d1cdc3d4dac0ffe72706ceebd8 Mon Sep 17 00:00:00 2001 From: Stefan Lender Date: Fri, 9 Oct 2026 09:56:18 +0200 Subject: [PATCH] feat(teams): Add Deck resources to Team Activity Expose shared boards and their cards as Team Activity scopes. Co-authored-by: Copilot GPT-5.6 Sol Signed-off-by: Stefan Lender --- lib/Teams/DeckTeamResourceProvider.php | 28 ++++- .../Teams/DeckTeamResourceProviderTest.php | 108 ++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/unit/Teams/DeckTeamResourceProviderTest.php diff --git a/lib/Teams/DeckTeamResourceProvider.php b/lib/Teams/DeckTeamResourceProvider.php index 40cef2343c..e15427879f 100644 --- a/lib/Teams/DeckTeamResourceProvider.php +++ b/lib/Teams/DeckTeamResourceProvider.php @@ -7,7 +7,10 @@ use OCA\Deck\AppInfo\Application; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\CardMapper; use OCP\IURLGenerator; +use OCP\Teams\ITeamActivityResourceProvider; +use OCP\Teams\TeamActivityScope; use OCP\Teams\TeamResource; /** @@ -15,10 +18,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -class DeckTeamResourceProvider implements \OCP\Teams\ITeamResourceProvider { +class DeckTeamResourceProvider implements ITeamActivityResourceProvider { public function __construct( private BoardMapper $boardMapper, + private CardMapper $cardMapper, private IURLGenerator $urlGenerator, ) { } @@ -57,6 +61,28 @@ public function getTeamsForResource(string $resourceId): array { return $this->boardMapper->findTeamsForBoard((int)$resourceId); } + public function getActivityScopes(array $resources, string $userId): array { + // Read access and deleted objects are checked by DeckProvider via canSee*Activity(), so $userId is not used here. + $boardIds = array_values(array_unique(array_filter(array_map( + static fn (TeamResource $resource): ?int => ctype_digit($resource->getId()) && (int)$resource->getId() > 0 ? (int)$resource->getId() : null, + $resources, + )))); + if ($boardIds === []) { + return []; + } + + $query = $this->cardMapper->queryCardsByBoards($boardIds); + $query->select('c.id'); + $result = $query->executeQuery(); + $cardIds = array_map('intval', $result->fetchFirstColumn()); + $result->closeCursor(); + + return [ + new TeamActivityScope('deck_board', $boardIds), + new TeamActivityScope('deck_card', $cardIds), + ]; + } + public function getBoardBulletIcon(Board $board): string { return ''; } diff --git a/tests/unit/Teams/DeckTeamResourceProviderTest.php b/tests/unit/Teams/DeckTeamResourceProviderTest.php new file mode 100644 index 0000000000..849d18827d --- /dev/null +++ b/tests/unit/Teams/DeckTeamResourceProviderTest.php @@ -0,0 +1,108 @@ +cardMapper = $this->createMock(CardMapper::class); + $this->resourceProvider = $this->createMock(ITeamResourceProvider::class); + $this->provider = new DeckTeamResourceProvider( + $this->createMock(BoardMapper::class), + $this->cardMapper, + $this->createMock(IURLGenerator::class), + ); + } + + private function resource(string $id): TeamResource { + return new TeamResource($this->resourceProvider, $id, 'Board ' . $id, '/deck/' . $id); + } + + private function expectCardIds(array $boardIds, array $cardIds): void { + $query = $this->createMock(IQueryBuilder::class); + $result = $this->createMock(IResult::class); + $this->cardMapper->expects($this->once())->method('queryCardsByBoards')->with($boardIds)->willReturn($query); + $query->expects($this->once())->method('select')->with('c.id')->willReturnSelf(); + $query->expects($this->once())->method('executeQuery')->willReturn($result); + $result->expects($this->once())->method('fetchFirstColumn')->willReturn($cardIds); + $result->expects($this->once())->method('closeCursor'); + } + + public function testActivityScopesContainBoardsAndTheirCards(): void { + $this->expectCardIds([7, 8], [70, 71, 80]); + + $scopes = $this->provider->getActivityScopes([ + $this->resource('7'), + $this->resource('8'), + $this->resource('not-a-board-id'), + ], 'alice'); + + self::assertCount(2, $scopes); + self::assertSame('deck_board', $scopes[0]->getObjectType()); + self::assertSame([7, 8], $scopes[0]->getObjectIds()); + self::assertSame('deck_card', $scopes[1]->getObjectType()); + self::assertSame([70, 71, 80], $scopes[1]->getObjectIds()); + } + + public function testActivityScopesWithoutResources(): void { + $this->cardMapper->expects($this->never())->method('queryCardsByBoards'); + + self::assertSame([], $this->provider->getActivityScopes([], 'alice')); + } + + public function testActivityScopesIgnoreInvalidBoardIds(): void { + $this->cardMapper->expects($this->never())->method('queryCardsByBoards'); + + $scopes = $this->provider->getActivityScopes([ + $this->resource('0'), + $this->resource('abc'), + $this->resource('-1'), + $this->resource(''), + ], 'alice'); + + self::assertSame([], $scopes); + } + + public function testActivityScopesDeduplicateBoardIds(): void { + $this->expectCardIds([7], [70]); + + $scopes = $this->provider->getActivityScopes([ + $this->resource('7'), + $this->resource('7'), + ], 'alice'); + + self::assertSame([7], $scopes[0]->getObjectIds()); + } + + public function testActivityScopesForBoardWithoutCards(): void { + $this->expectCardIds([7], []); + + $scopes = $this->provider->getActivityScopes([$this->resource('7')], 'alice'); + + self::assertCount(2, $scopes); + self::assertSame([7], $scopes[0]->getObjectIds()); + self::assertSame('deck_card', $scopes[1]->getObjectType()); + self::assertSame([], $scopes[1]->getObjectIds()); + } +}