diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index 04d90d40b..97147ba17 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -93,14 +93,21 @@ private function formatFolder(array $folder): array { * Gets all Groupfolders * * @param bool $applicable Filter by applicable groups + * @param non-negative-int $offset Number of items to skip. + * @param ?positive-int $limit Number of items to return. * @return DataResponse, array{}> * @throws OCSNotFoundException Storage not found + * @throws OCSBadRequestException Wrong limit used * * 200: Groupfolders returned */ #[NoAdminRequired] #[FrontpageRoute(verb: 'GET', url: '/folders')] - public function getFolders(bool $applicable = false): DataResponse { + public function getFolders(bool $applicable = false, int $offset = 0, ?int $limit = null): DataResponse { + if ($limit !== null && $limit <= 0) { + throw new OCSBadRequestException('The limit must be greater than 0.'); + } + $storageId = $this->getRootFolderStorageId(); if ($storageId === null) { throw new OCSNotFoundException(); @@ -111,8 +118,17 @@ public function getFolders(bool $applicable = false): DataResponse { // Make them string-indexed for OpenAPI JSON output $folders[(string)$id] = $this->formatFolder($folder); } + + // Make sure the order is always the same, otherwise pagination could break. + ksort($folders); + $isAdmin = $this->delegationService->isAdminNextcloud() || $this->delegationService->isDelegatedAdmin(); if ($isAdmin && !$applicable) { + // If only the default values are provided the pagination can be skipped. + if ($offset !== 0 || $limit !== null) { + $folders = array_slice($folders, $offset, $limit, true); + } + return new DataResponse($folders); } @@ -124,6 +140,11 @@ public function getFolders(bool $applicable = false): DataResponse { $folders = array_filter(array_map($this->filterNonAdminFolder(...), $folders)); } + // If only the default values are provided the pagination can be skipped. + if ($offset !== 0 || $limit !== null) { + $folders = array_slice($folders, $offset, $limit, true); + } + return new DataResponse($folders); } diff --git a/openapi.json b/openapi.json index 7665910ae..437271632 100644 --- a/openapi.json +++ b/openapi.json @@ -472,6 +472,29 @@ ] } }, + { + "name": "offset", + "in": "query", + "description": "Number of items to skip.", + "schema": { + "type": "integer", + "format": "int64", + "default": 0, + "minimum": 0 + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of items to return.", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true, + "default": null, + "minimum": 1 + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -544,6 +567,34 @@ } } } + }, + "400": { + "description": "Wrong limit used", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } } } }, diff --git a/src/settings/Api.ts b/src/settings/Api.ts index 49ccea060..2d5ac382b 100644 --- a/src/settings/Api.ts +++ b/src/settings/Api.ts @@ -15,8 +15,13 @@ export class Api { return generateUrl(`apps/groupfolders/${endpoint}`) } - async listFolders(): Promise { - const response = await axios.get>(this.getUrl('folders')) + async listFolders(offset = 0, limit?: number): Promise { + const response = await axios.get>(this.getUrl('folders'), { + params: { + offset, + limit, + }, + }) return Object.keys(response.data.ocs.data).map(id => response.data.ocs.data[id]) } diff --git a/src/settings/App.scss b/src/settings/App.scss index 4094829c3..fc6e8b64f 100644 --- a/src/settings/App.scss +++ b/src/settings/App.scss @@ -178,7 +178,17 @@ } } } - } + } + + .groupfolders-pagination__list { + display: flex; + gap: var(--default-grid-baseline); + justify-content: center; + } + + .groupfolders-pagination__button { + height: var(--default-clickable-area); + } } diff --git a/src/settings/App.tsx b/src/settings/App.tsx index c6909abfb..9d489eab7 100644 --- a/src/settings/App.tsx +++ b/src/settings/App.tsx @@ -29,6 +29,8 @@ const defaultQuotaOptions = { Unlimited: -3, } +const pageSize = 50 + export type SortKey = 'mount_point' | 'quota' | 'groups' | 'acl'; export interface AppState { @@ -46,6 +48,7 @@ export interface AppState { sortOrder: number; isAdminNextcloud: boolean; checkAppsInstalled: boolean; + currentPage: number; } export class App extends Component implements OC.Plugin { @@ -67,10 +70,12 @@ export class App extends Component implements OC.Plugin { + // list first pageSize + 1 folders so we know if there are more pages + this.api.listFolders(0, pageSize + 1).then((folders) => { this.setState({ folders }) }) this.api.listGroups().then((groups) => { @@ -170,6 +175,21 @@ export class App extends Component implements OC.Plugin { if (this.state.sort === sort) { this.setState({ sortOrder: -this.state.sortOrder }) @@ -233,11 +253,12 @@ export class App extends Component implements OC.Plugin { const id = folder.id return @@ -361,6 +382,60 @@ export class App extends Component implements OC.Plugin + } diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index a24b8d0bb..282f64bce 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -410,6 +410,10 @@ export interface operations { query?: { /** @description Filter by applicable groups */ applicable?: 0 | 1; + /** @description Number of items to skip. */ + offset?: number; + /** @description Number of items to return. */ + limit?: number | null; }; header: { /** @description Required to be true for the API request to pass */ @@ -436,6 +440,20 @@ export interface operations { }; }; }; + /** @description Wrong limit used */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; /** @description Storage not found */ 404: { headers: {