diff --git a/appinfo/routes.php b/appinfo/routes.php index 12d2ea058..249a508e2 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -48,6 +48,9 @@ ['name' => 'TeamFolder#getTeamFolder', 'url' => '/teams/{circleId}/folder', 'verb' => 'GET'], ['name' => 'TeamFolder#unlinkTeamFolder', 'url' => '/teams/{circleId}/folder', 'verb' => 'DELETE'], ['name' => 'TeamFolder#upgradeTeamFolder', 'url' => '/teams/{circleId}/folder', 'verb' => 'POST'], + ['name' => 'TeamFolder#getLinkableTeamFolders', 'url' => '/teams/{circleId}/folder/linkable', 'verb' => 'GET'], + ['name' => 'TeamFolder#linkTeamFolder', 'url' => '/teams/{circleId}/folder/link', 'verb' => 'POST'], + ['name' => 'TeamFolder#updateTeamFolderQuota', 'url' => '/teams/{circleId}/folder/quota', 'verb' => 'PUT'], // Teams Dashboard widget endpoint ['name' => 'TeamsDashboard#getCompleteTeamsData', 'url' => '/teams/dashboard/widget', 'verb' => 'GET'], @@ -55,6 +58,7 @@ // AdminController ['name' => 'Admin#circles', 'url' => '/admin/{emulated}/circles', 'verb' => 'GET'], ['name' => 'Admin#circlesAll', 'url' => '/admin/circlesall', 'verb' => 'GET'], + ['name' => 'Admin#teamFolders', 'url' => '/admin/teamfolders', 'verb' => 'GET'], ['name' => 'Admin#create', 'url' => '/admin/{emulated}/circles', 'verb' => 'POST'], ['name' => 'Admin#destroy', 'url' => '/admin/{emulated}/circles/{circleId}', 'verb' => 'DELETE'], [ diff --git a/js/teams-settings-team-folders.mjs.map b/js/teams-settings-team-folders.mjs.map index 9832cc12f..7853ad868 100644 --- a/js/teams-settings-team-folders.mjs.map +++ b/js/teams-settings-team-folders.mjs.map @@ -1 +1 @@ -{"version":3,"file":"teams-settings-team-folders.mjs","sources":["../src/components/AdminTeamFolders.vue","../src/settings-team-folders.ts"],"sourcesContent":["\n\n\n\n\n\n\n","/*!\n * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n\nimport { createApp } from 'vue'\nimport AdminTeamFolders from './components/AdminTeamFolders.vue'\n\nimport 'vite/modulepreload-polyfill'\n\nconst app = createApp(AdminTeamFolders)\napp.mount('#vue-admin-team-folders')\n"],"names":["unlimitedQuota","t","quotaPreset","teamFolderDefaultQuotaBytes","loadState","quotaOptions","computed","options","label","formatFileSize","option","q","selectedQuota","ref","validateQuota","quota","parsed","parseFileSize","updateAppConfig","key","value","confirmPassword","url","generateOcsUrl","data","axios","showError","logger","error","onSaveQuota","showSuccess","bytes","_createBlock","_unref","NcSettingsSection","_createElementVNode","_hoisted_1","_createVNode","NcSelect","$event","NcButton","_hoisted_2","_toDisplayString","app","createApp","AdminTeamFolders"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"teams-settings-team-folders.mjs","sources":["../src/components/AdminTeamFolders.vue","../src/settings-team-folders.ts"],"sourcesContent":["\n\n\n\n\n\n\n","/*!\n * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n\nimport { createApp } from 'vue'\nimport AdminTeamFolders from './components/AdminTeamFolders.vue'\n\nimport 'vite/modulepreload-polyfill'\n\nconst app = createApp(AdminTeamFolders)\napp.mount('#vue-admin-team-folders')\n"],"names":["unlimitedQuota","t","quotaPreset","teamFolderDefaultQuotaBytes","loadState","quotaOptions","computed","options","label","formatFileSize","option","q","selectedQuota","ref","validateQuota","quota","parsed","parseFileSize","updateAppConfig","key","value","confirmPassword","url","generateOcsUrl","data","axios","showError","logger","error","onSaveQuota","showSuccess","bytes","_createBlock","_unref","NcSettingsSection","_createElementVNode","_hoisted_1","_createVNode","NcSelect","$event","NcButton","_hoisted_2","_toDisplayString","app","createApp","AdminTeamFolders"],"mappings":"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"} diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index 9baa5528b..7db7f8f90 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -35,6 +35,7 @@ use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\IUserSession; +use OCP\Teams\ITeamManager; /** * Class AdminController @@ -70,6 +71,7 @@ public function __construct( private MemberService $memberService, private MembershipService $membershipService, private SearchService $searchService, + private ITeamManager $teamManager, ConfigService $configService, ) { parent::__construct($appName, $request); @@ -263,6 +265,45 @@ public function circlesAll( } } + /** + * Return teams and their optionally linked team folders. + * + * @return DataResponse + * @throws OCSException + */ + public function teamFolders(): DataResponse { + try { + $this->setLocalFederatedUser($this->userSession->getUser()->getUID()); + $provider = $this->teamManager->getTeamFolderProvider(); + if ($provider === null) { + return new DataResponse([]); + } + + $probe = new CircleProbe(); + $probe->filterPersonalCircles() + ->filterSingleCircles() + ->filterSystemCircles() + ->filterHiddenCircles() + ->filterBackendCircles(); + + $teamFolders = []; + foreach ($this->circleService->getAllCircles($probe) as $circle) { + $folder = $provider->getTeamFolder($circle->getSingleId()); + + $teamFolders[] = [ + 'teamId' => $circle->getSingleId(), + 'teamName' => $circle->getDisplayName(), + 'folder' => $folder?->jsonSerialize(), + ]; + } + + return new DataResponse($teamFolders); + } catch (Exception $e) { + $this->e($e); + throw new OCSException($e->getMessage(), (int)$e->getCode()); + } + } + /** * @param string $emulated * @param string $circleId diff --git a/lib/Controller/TeamFolderController.php b/lib/Controller/TeamFolderController.php index 50e8bfe12..dfaf8c975 100644 --- a/lib/Controller/TeamFolderController.php +++ b/lib/Controller/TeamFolderController.php @@ -51,7 +51,7 @@ public function getTeamFolder(string $circleId): DataResponse { } #[NoAdminRequired] - public function upgradeTeamFolder(string $circleId): DataResponse { + public function upgradeTeamFolder(string $circleId, string $name = ''): DataResponse { if (!$this->policy->isTeamFolderProvisioningEnabled()) { throw new OCSException('Team space provisioning is disabled', Http::STATUS_FORBIDDEN); } @@ -66,7 +66,7 @@ public function upgradeTeamFolder(string $circleId): DataResponse { $folder = $this->getProvider()->createTeamFolder( new Team( teamId: $circle->getSingleId(), - displayName: $circle->getDisplayName(), + displayName: trim($name) ?: $circle->getDisplayName(), link: null, ), $this->policy->getDefaultQuota(), @@ -79,6 +79,44 @@ public function upgradeTeamFolder(string $circleId): DataResponse { ]); } + #[NoAdminRequired] + public function getLinkableTeamFolders(string $circleId): DataResponse { + $this->assertAuthenticatedUserIsTeamOwnerOrServerAdmin($circleId); + + return new DataResponse(array_map( + static fn (\OCP\Teams\TeamFolder $folder): array => $folder->jsonSerialize(), + $this->getProvider()->getLinkableTeamFolders($circleId), + )); + } + + #[NoAdminRequired] + public function linkTeamFolder(string $circleId, int $folderId): DataResponse { + $this->assertAuthenticatedUserIsTeamOwnerOrServerAdmin($circleId); + $folder = $this->getProvider()->linkTeamFolder($circleId, $folderId); + + return new DataResponse([ + 'success' => true, + 'folderId' => $folder->getId(), + 'folder' => $folder->jsonSerialize(), + ]); + } + + #[NoAdminRequired] + public function updateTeamFolderQuota(string $circleId, int $quota): DataResponse { + $this->assertAuthenticatedUserIsTeamOwnerOrServerAdmin($circleId); + if ($quota < 0) { + throw new OCSException('Quota must be a non-negative number', Http::STATUS_BAD_REQUEST); + } + + $folder = $this->getProvider()->updateTeamFolderQuota($circleId, $quota); + + return new DataResponse([ + 'success' => true, + 'folderId' => $folder->getId(), + 'folder' => $folder->jsonSerialize(), + ]); + } + #[NoAdminRequired] public function unlinkTeamFolder(string $circleId, bool $deleteFolder = false): DataResponse { $this->assertAuthenticatedUserIsTeamOwnerOrServerAdmin($circleId); diff --git a/src/components/AdminTeamFolders.vue b/src/components/AdminTeamFolders.vue index 214bc7e37..eb990619f 100644 --- a/src/components/AdminTeamFolders.vue +++ b/src/components/AdminTeamFolders.vue @@ -5,19 +5,26 @@ diff --git a/src/main.ts b/src/main.ts index 6d1f9ef67..4adf9dc60 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import axios from '@nextcloud/axios' import { createPinia } from 'pinia' import { createApp } from 'vue' import App from './teams/App.vue' @@ -12,10 +11,6 @@ import { router } from './teams/router.ts' import LegacyGlobalMixin from './teams/team-page/mixins/LegacyGlobalMixin.js' import store from './teams/team-page/store/index.ts' -// OCS endpoints require this header; the ported Contacts services rely on a -// global default rather than setting it per request. -axios.defaults.headers.common['OCS-APIRequest'] = 'true' - logger.debug('Mounting Teams app') const app = createApp(App) diff --git a/src/teams/api.ts b/src/teams/api.ts index e874692ce..08d8a72a7 100644 --- a/src/teams/api.ts +++ b/src/teams/api.ts @@ -14,9 +14,6 @@ import { getRecommendations, getSuggestions } from './team-page/services/collabo /** `SHARES_TYPES_MEMBER_MAP` is built dynamically, so type its shape explicitly. */ const shareTypeToMemberType = SHARES_TYPES_MEMBER_MAP as Record -/** OCS endpoints require this header. */ -const HEADERS = { 'OCS-APIRequest': 'true' } - /** Minimal shape of an OCS response envelope. */ interface OcsResponse { ocs: { data: T } @@ -141,8 +138,8 @@ function mapFullMember(raw: RawMember): Member { */ export async function fetchTeams(): Promise { const [circlesRes, dashRes] = await Promise.allSettled([ - axios.get>(generateOcsUrl('apps/circles/circles') + '?limit=-1', { headers: HEADERS }), - axios.get>(generateOcsUrl('apps/circles/teams/dashboard/widget') + '?limit=200&offset=0', { headers: HEADERS }), + axios.get>(generateOcsUrl('apps/circles/circles') + '?limit=-1'), + axios.get>(generateOcsUrl('apps/circles/teams/dashboard/widget') + '?limit=200&offset=0'), ]) // The team list is required; without it we have nothing to show. @@ -181,10 +178,7 @@ export async function fetchTeams(): Promise { * @param teamId - The team single id */ export async function fetchTeamMembers(teamId: string): Promise { - const res = await axios.get>( - generateOcsUrl('apps/circles/circles/{circleId}/members', { circleId: teamId }), - { headers: HEADERS }, - ) + const res = await axios.get>(generateOcsUrl('apps/circles/circles/{circleId}/members', { circleId: teamId })) return (res.data.ocs.data ?? []).map(mapFullMember) } @@ -197,7 +191,6 @@ export async function createTeam(name: string): Promise { const res = await axios.post>( generateOcsUrl('apps/circles/circles'), { name }, - { headers: HEADERS }, ) return res.data.ocs.data.id } @@ -212,7 +205,6 @@ export async function setTeamDescription(teamId: string, description: string): P await axios.put( generateOcsUrl('apps/circles/circles/{circleId}/description', { circleId: teamId }), { value: description }, - { headers: HEADERS }, ) } @@ -225,7 +217,6 @@ export async function leaveTeam(teamId: string): Promise { await axios.put( generateOcsUrl('apps/circles/circles/{circleId}/leave', { circleId: teamId }), {}, - { headers: HEADERS }, ) } @@ -235,10 +226,7 @@ export async function leaveTeam(teamId: string): Promise { * @param teamId - The team single id */ export async function deleteTeam(teamId: string): Promise { - await axios.delete( - generateOcsUrl('apps/circles/circles/{circleId}', { circleId: teamId }), - { headers: HEADERS }, - ) + await axios.delete(generateOcsUrl('apps/circles/circles/{circleId}', { circleId: teamId })) } /** @@ -247,6 +235,22 @@ export async function deleteTeam(teamId: string): Promise { export interface TeamFolder { id: number mountPoint: string + quota: number | null +} + +/** Team and its linked folder as exposed to administrators. */ +export interface AdminTeamFolder { + teamId: string + teamName: string + folder: TeamFolder | null +} + +/** + * Fetch all teams and their optionally linked team folders for the admin settings view. + */ +export async function getAdminTeamFolders(): Promise { + const { data } = await axios.get>(generateOcsUrl('/apps/circles/admin/teamfolders')) + return data.ocs.data } /** @@ -260,10 +264,7 @@ export interface TeamFolder { */ export async function getTeamFolder(teamId: string): Promise { try { - const { data } = await axios.get>( - generateOcsUrl('apps/circles/teams/{circleId}/folder', { circleId: teamId }), - { headers: HEADERS }, - ) + const { data } = await axios.get>(generateOcsUrl('apps/circles/teams/{circleId}/folder', { circleId: teamId })) return data.ocs.data } catch (error) { if (error && typeof error === 'object' @@ -282,13 +283,51 @@ export async function getTeamFolder(teamId: string): Promise * returned. Requires team owner privileges. * * @param teamId - The team single id + * @param name - Optional team folder name * @return The created (or existing) team folder. */ -export async function upgradeTeamFolder(teamId: string): Promise { +export async function upgradeTeamFolder(teamId: string, name?: string): Promise { const { data } = await axios.post>( generateOcsUrl('apps/circles/teams/{circleId}/folder', { circleId: teamId }), - {}, - { headers: HEADERS }, + { name }, + ) + return data.ocs.data.folder +} + +/** + * Fetch team folders that are not yet linked to a team. + * + * @param teamId - The team single id + */ +export async function getLinkableTeamFolders(teamId: string): Promise { + const { data } = await axios.get>(generateOcsUrl('apps/circles/teams/{circleId}/folder/linkable', { circleId: teamId })) + return data.ocs.data +} + +/** + * Link a team to a team folder. + * + * @param teamId - The team single id + * @param folderId - The id of the existing team folder + */ +export async function linkTeamFolder(teamId: string, folderId: number): Promise { + const { data } = await axios.post>( + generateOcsUrl('apps/circles/teams/{circleId}/folder/link', { circleId: teamId }), + { folderId }, + ) + return data.ocs.data.folder +} + +/** + * Update the storage quota of the team folder linked to a team. + * + * @param teamId - The team single id + * @param quota - Quota in bytes, or zero for unlimited + */ +export async function updateTeamFolderQuota(teamId: string, quota: number): Promise { + const { data } = await axios.put>( + generateOcsUrl('apps/circles/teams/{circleId}/folder/quota', { circleId: teamId }), + { quota }, ) return data.ocs.data.folder } @@ -331,7 +370,6 @@ export async function addTeamMembers(teamId: string, candidates: MemberCandidate const res = await axios.post>>( generateOcsUrl('apps/circles/circles/{circleId}/members/multi', { circleId: teamId }), { members }, - { headers: HEADERS }, ) return Object.keys(res.data.ocs.data ?? {}).length } diff --git a/src/teams/team-page/components/CircleDetails.spec.ts b/src/teams/team-page/components/CircleDetails.spec.ts index 909eaed4a..9ccc324f5 100644 --- a/src/teams/team-page/components/CircleDetails.spec.ts +++ b/src/teams/team-page/components/CircleDetails.spec.ts @@ -17,7 +17,7 @@ import CircleDetails from './CircleDetails.vue' */ const loadState = vi.hoisted(() => vi.fn((app: string, key: string, fallback: unknown) => fallback)) const getTeamFolder = vi.hoisted(() => vi.fn<(teamId: string) => Promise>(async () => null)) -const upgradeTeamFolder = vi.hoisted(() => vi.fn<(teamId: string) => Promise>(async () => ({ id: 1, mountPoint: 'Team' }))) +const upgradeTeamFolder = vi.hoisted(() => vi.fn<(teamId: string) => Promise>(async () => ({ id: 1, mountPoint: 'Team', quota: null }))) const showError = vi.hoisted(() => vi.fn()) const showSuccess = vi.hoisted(() => vi.fn()) const axiosGet = vi.hoisted(() => vi.fn(async () => ({ data: { ocs: { data: { resources: [] } } } }))) @@ -144,7 +144,7 @@ describe('CircleDetails folder button', () => { it('renders no folder button when a team folder already exists', async () => { const wrapper = mountDetails({}, { providerAvailable: true, - teamFolder: { id: 42, mountPoint: 'Marketing' }, + teamFolder: { id: 42, mountPoint: 'Marketing', quota: null }, }) await vi.waitFor(() => expect(wrapper.vm.teamFolder).not.toBeNull()) expect(wrapper.vm.folderButtonType).toBeNull() @@ -260,7 +260,7 @@ describe('CircleDetails team folder upgrade banner', () => { it('hides the banner when a team folder exists', async () => { const wrapper = mountDetails({}, { providerAvailable: true, - teamFolder: { id: 42, mountPoint: 'Marketing' }, + teamFolder: { id: 42, mountPoint: 'Marketing', quota: null }, }) await vi.waitFor(() => expect(wrapper.vm.teamFolder).not.toBeNull()) @@ -302,7 +302,7 @@ describe('CircleDetails team folder upgrade banner', () => { await wrapper.vm.createTeamFolder() expect(upgradeTeamFolder).toHaveBeenCalledWith('team-1') - expect(wrapper.vm.teamFolder).toEqual({ id: 1, mountPoint: 'Team' }) + expect(wrapper.vm.teamFolder).toEqual({ id: 1, mountPoint: 'Team', quota: null }) expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ name: 'team', params: { teamId: 'team-1' },