Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OCA\GroupFolders\Listeners\NodeRenamedListener;
use OCA\GroupFolders\Mount\FolderStorageManager;
use OCA\GroupFolders\Mount\MountProvider;
use OCA\GroupFolders\Team\GroupFoldersTeamResourceProvider;
use OCA\GroupFolders\Trash\TrashBackend;
use OCA\GroupFolders\Trash\TrashManager;
use OCA\GroupFolders\Versions\GroupVersionsExpireManager;
Expand Down Expand Up @@ -92,6 +93,8 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(GroupDeletedEvent::class, DeleteListener::class);
$context->registerEventListener(UserDeletedEvent::class, DeleteListener::class);

$context->registerTeamResourceProvider(GroupFoldersTeamResourceProvider::class);

$context->registerService(MountProvider::class, function (ContainerInterface $c): MountProvider {
/** @var IAppConfig $config */
$config = $c->get(IAppConfig::class);
Expand Down
104 changes: 104 additions & 0 deletions lib/Team/GroupFoldersTeamResourceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\GroupFolders\Team;

use OCA\GroupFolders\AppInfo\Application;
use OCA\GroupFolders\Folder\FolderManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Teams\ITeamResourceProvider;
use OCP\Teams\TeamResource;

class GroupFoldersTeamResourceProvider implements ITeamResourceProvider {
/** Provider icon: the Team folders app glyph (folder with members), no fill so it inherits currentColor. */
private const PROVIDER_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 18H4V8h16m0-2h-8l-2-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8c0-1.11-.9-2-2-2Z"/><path d="M12 9.4a1.75 1.75 0 0 1 1.75 1.75A1.75 1.75 0 0 1 12 12.9a1.75 1.75 0 0 1-1.75-1.75A1.75 1.75 0 0 1 12 9.4m-3.5 1.25c.28 0 .54.075.765.21-.075.715.135 1.425.565 1.98-.25.48-.75.81-1.33.81a1.5 1.5 0 0 1-1.5-1.5 1.5 1.5 0 0 1 1.5-1.5m7 0a1.5 1.5 0 0 1 1.5 1.5 1.5 1.5 0 0 1-1.5 1.5c-.58 0-1.08-.33-1.33-.81.43-.555.64-1.265.565-1.98.225-.135.485-.21.765-.21m-6.75 5.125c0-1.035 1.455-1.875 3.25-1.875s3.25.84 3.25 1.875v.875h-6.5v-.875M6 16.65v-.75c0-.695.945-1.28 2.225-1.45-.295.34-.475.81-.475 1.325v.875H6m12 0h-1.75v-.875c0-.515-.18-.985-.475-1.325 1.28.17 2.225.755 2.225 1.45Z"/></svg>';

/** Per-resource icon: a plain folder, matching the files team resource provider. */
private const FOLDER_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" id="mdi-folder" viewBox="0 0 24 24"><path d="M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" /></svg>';

public function __construct(
private readonly IL10N $l,
private readonly IURLGenerator $url,
private readonly FolderManager $folderManager,
) {
}

public function getId(): string {
return Application::APP_ID;
}

public function getName(): string {
return $this->l->t('Team folders');
}

public function getIconSvg(): string {
return self::PROVIDER_ICON_SVG;
}

public function getSharedWith(string $teamId): array {
// Membership is gated by TeamManager before this runs, so we return every
// team folder mapped to this circle without re-checking the current user.
if ($this->folderManager->getCirclesManager() === null) {
return [];
}

$resources = [];
foreach ($this->folderManager->getAllFolders() as $folder) {
$applicable = $folder->groups[$teamId] ?? null;
if ($applicable === null || $applicable['type'] !== 'circle') {
continue;
}

$resources[] = new TeamResource(
$this,
(string)$folder->id,
$folder->mountPoint,
$this->url->linkToRouteAbsolute('files.view.index', ['dir' => '/' . $folder->mountPoint]),
iconSvg: self::FOLDER_ICON_SVG,
);
}

return $resources;
}

public function isSharedWithTeam(string $teamId, string $resourceId): bool {
if ($this->folderManager->getCirclesManager() === null) {
return false;
}

$folder = $this->folderManager->getAllFolders()[(int)$resourceId] ?? null;
if ($folder === null) {
return false;
}

$applicable = $folder->groups[$teamId] ?? null;
return $applicable !== null && $applicable['type'] === 'circle';
}

public function getTeamsForResource(string $resourceId): array {
if ($this->folderManager->getCirclesManager() === null) {
return [];
}

$folder = $this->folderManager->getAllFolders()[(int)$resourceId] ?? null;
if ($folder === null) {
return [];
}

$teamIds = [];
foreach ($folder->groups as $entityId => $applicable) {
if ($applicable['type'] === 'circle') {
$teamIds[] = (string)$entityId;
}
}

return $teamIds;
}
}
142 changes: 142 additions & 0 deletions tests/Team/GroupFoldersTeamResourceProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\GroupFolders\Tests\Team;

use OCA\Circles\CirclesManager;
use OCA\GroupFolders\Folder\FolderDefinitionWithMappings;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Team\GroupFoldersTeamResourceProvider;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Teams\TeamResource;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

class GroupFoldersTeamResourceProviderTest extends TestCase {
private FolderManager&MockObject $folderManager;
private IL10N&MockObject $l10n;
private IURLGenerator&MockObject $urlGenerator;
private GroupFoldersTeamResourceProvider $provider;

protected function setUp(): void {
parent::setUp();

$this->folderManager = $this->createMock(FolderManager::class);
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);

$this->l10n->method('t')->willReturnArgument(0);
$this->urlGenerator->method('linkToRouteAbsolute')
->willReturnCallback(function (string $route, array $args = []): string {
$dir = $args['dir'] ?? '';
return 'https://cloud.example/' . $route . '?dir=' . (is_string($dir) ? $dir : '');
});

$this->provider = new GroupFoldersTeamResourceProvider(
$this->l10n,
$this->urlGenerator,
$this->folderManager,
);
}

/**
* @param array<string, array{displayName: string, permissions: int, type: 'group'|'circle'}> $groups
*/
private function folder(int $id, string $mountPoint, array $groups): FolderDefinitionWithMappings {
return new FolderDefinitionWithMappings($id, $mountPoint, 0, false, false, 1, 1, [], $groups, []);
}

/**
* @return array{displayName: string, permissions: int, type: 'circle'}
*/
private function circleApplicable(string $displayName = 'Team'): array {
return ['displayName' => $displayName, 'permissions' => 31, 'type' => 'circle'];
}

/**
* @return array{displayName: string, permissions: int, type: 'group'}
*/
private function groupApplicable(string $displayName = 'Group'): array {
return ['displayName' => $displayName, 'permissions' => 31, 'type' => 'group'];
}

private function withCircles(): void {
$this->folderManager->method('getCirclesManager')->willReturn($this->createMock(CirclesManager::class));
}

public function testStaticIdentity(): void {
$this->assertSame('groupfolders', $this->provider->getId());
$this->assertSame('Team folders', $this->provider->getName());
$this->assertStringContainsString('<svg', $this->provider->getIconSvg());
}

public function testGetSharedWithReturnsOnlyFoldersMappedToTheTeam(): void {
$this->withCircles();
$this->folderManager->method('getAllFolders')->willReturn([
1 => $this->folder(1, 'Design', ['team-1' => $this->circleApplicable('Design Team')]),
2 => $this->folder(2, 'Marketing', ['group-x' => $this->groupApplicable()]),
3 => $this->folder(3, 'Other', ['team-2' => $this->circleApplicable()]),
]);

$resources = $this->provider->getSharedWith('team-1');

$this->assertCount(1, $resources);
$resource = $resources[0];
$this->assertInstanceOf(TeamResource::class, $resource);
$this->assertSame('1', $resource->getId());
$this->assertSame('Design', $resource->getLabel());
$this->assertStringContainsString('dir=/Design', $resource->getUrl());
$this->assertStringContainsString('<svg', (string)$resource->getIconSvg());
}

public function testGetSharedWithReturnsEmptyWhenCirclesDisabled(): void {
$this->folderManager->method('getCirclesManager')->willReturn(null);
$this->folderManager->expects($this->never())->method('getAllFolders');

$this->assertSame([], $this->provider->getSharedWith('team-1'));
}

public function testIsSharedWithTeam(): void {
$this->withCircles();
$this->folderManager->method('getAllFolders')->willReturn([
5 => $this->folder(5, 'Design', ['team-1' => $this->circleApplicable()]),
]);

$this->assertTrue($this->provider->isSharedWithTeam('team-1', '5'));
$this->assertFalse($this->provider->isSharedWithTeam('team-2', '5'));
$this->assertFalse($this->provider->isSharedWithTeam('team-1', '99'));
}

public function testIsSharedWithTeamReturnsFalseWhenCirclesDisabled(): void {
$this->folderManager->method('getCirclesManager')->willReturn(null);

$this->assertFalse($this->provider->isSharedWithTeam('team-1', '5'));
}

public function testGetTeamsForResourceReturnsCircleIdsOnly(): void {
$this->withCircles();
$this->folderManager->method('getAllFolders')->willReturn([
7 => $this->folder(7, 'Shared', [
'team-1' => $this->circleApplicable(),
'group-x' => $this->groupApplicable(),
'team-2' => $this->circleApplicable(),
]),
]);

$this->assertEqualsCanonicalizing(['team-1', 'team-2'], $this->provider->getTeamsForResource('7'));
}

public function testGetTeamsForResourceReturnsEmptyForUnknownFolder(): void {
$this->withCircles();
$this->folderManager->method('getAllFolders')->willReturn([]);

$this->assertSame([], $this->provider->getTeamsForResource('123'));
}
}
Loading