Skip to content
Merged
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
23 changes: 22 additions & 1 deletion lib/Controller/FolderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Http::STATUS_OK, array<string, GroupFoldersFolder>, 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();
Expand All @@ -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);
}

Expand All @@ -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);
}

Expand Down
51 changes: 51 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {}
}
}
}
}
}
}
}
}
},
Expand Down
9 changes: 7 additions & 2 deletions src/settings/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ export class Api {
return generateUrl(`apps/groupfolders/${endpoint}`)
}

async listFolders(): Promise<Folder[]> {
const response = await axios.get<OCSResponse<Folder[]>>(this.getUrl('folders'))
async listFolders(offset = 0, limit?: number): Promise<Folder[]> {
const response = await axios.get<OCSResponse<Folder[]>>(this.getUrl('folders'), {
params: {
offset,
limit,
},
})
return Object.keys(response.data.ocs.data).map(id => response.data.ocs.data[id])
}

Expand Down
12 changes: 11 additions & 1 deletion src/settings/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}

Expand Down
79 changes: 77 additions & 2 deletions src/settings/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const defaultQuotaOptions = {
Unlimited: -3,
}

const pageSize = 50

export type SortKey = 'mount_point' | 'quota' | 'groups' | 'acl';

export interface AppState {
Expand All @@ -46,6 +48,7 @@ export interface AppState {
sortOrder: number;
isAdminNextcloud: boolean;
checkAppsInstalled: boolean;
currentPage: number;
}

export class App extends Component<unknown, AppState> implements OC.Plugin<OC.Search.Core> {
Expand All @@ -67,10 +70,12 @@ export class App extends Component<unknown, AppState> implements OC.Plugin<OC.Se
sortOrder: 1,
isAdminNextcloud: false,
checkAppsInstalled: false,
currentPage: 0,
}

componentDidMount() {
this.api.listFolders().then((folders) => {
// 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) => {
Expand Down Expand Up @@ -170,6 +175,21 @@ export class App extends Component<unknown, AppState> implements OC.Plugin<OC.Se
this.api.setACL(folder.id, acl)
}

async goToPage(page: number) {
const loadedPage = Math.floor(this.state.folders.length / pageSize)
if (loadedPage <= page) {
const folders = await this.api.listFolders(this.state.folders.length, (page + 1) * pageSize - this.state.folders.length + 1)
this.setState({
folders: [...this.state.folders, ...folders],
currentPage: page,
})
} else {
this.setState({
currentPage: page,
})
}
}

onSortClick = (sort: SortKey) => {
if (this.state.sort === sort) {
this.setState({ sortOrder: -this.state.sortOrder })
Expand Down Expand Up @@ -233,11 +253,12 @@ export class App extends Component<unknown, AppState> implements OC.Plugin<OC.Se
if (this.state.filter === '') {
return true
}
return folder.mount_point.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1
return folder.mount_point.toLowerCase().includes(this.state.filter.toLowerCase())
}),
identifiers,
direction,
)
.slice(this.state.currentPage * pageSize, this.state.currentPage * pageSize + pageSize)
.map(folder => {
const id = folder.id
return <tr key={id}>
Expand Down Expand Up @@ -361,6 +382,60 @@ export class App extends Component<unknown, AppState> implements OC.Plugin<OC.Se
</tr>
</FlipMove>
</table>
<nav className="groupfolders-pagination" aria-label={t('groupfolders', 'Pagination of team folders')}>
<ul className="groupfolders-pagination__list">
<li>
<button
aria-label={t('groupfolders', 'Previous')}
className="groupfolders-pagination__button"
disabled={this.state.currentPage === 0}
title={t('groupfolders', 'Previous')}
onClick={() => this.goToPage(this.state.currentPage - 1)}>⮜</button>
</li>
{
// show the "1" button if we are not on the first page
this.state.currentPage > 0 && <li><button onClick={() => this.goToPage(0)}>1</button></li>
}
{
// show the ellipsis button if there are more than 2 pages before the current
this.state.currentPage > 2 && <li><button disabled>&#8230;</button></li>}
{
// show the page right before the current - if there is such a page
this.state.currentPage > 1 && <li><button onClick={() => this.goToPage(this.state.currentPage - 1)}>{this.state.currentPage}</button></li>
}
{ /* the current page as a button */}
<li><button aria-current="page" aria-disabled className="primary">{this.state.currentPage + 1}</button></li>
{
// show the next page if it exists (we know at least that the next exists or not)
(this.state.currentPage + 1) < (this.state.folders.length / pageSize)
&& <li>
<button onClick={() => this.goToPage(this.state.currentPage + 1)}>{this.state.currentPage + 2}</button>
</li>
}
{
// If we know more than two next pages exist we show the ellipsis for the intermediate pages
(this.state.currentPage + 3) < (this.state.folders.length / pageSize)
&& <li>
<button disabled>&#8230;</button>
</li>
}
{
// If more than one next page exist we show the last page as a button
(this.state.currentPage + 2) < (this.state.folders.length / pageSize)
&& <li>
<button onClick={() => this.goToPage(Math.floor(this.state.folders.length / pageSize))}>{Math.floor(this.state.folders.length / pageSize) + 1}</button>
</li>
}
<li>
<button
aria-label={t('groupfolders', 'Next')}
className="groupfolders-pagination__button"
disabled={this.state.currentPage >= Math.floor(this.state.folders.length / pageSize)}
title={t('groupfolders', 'Next')}
onClick={() => this.goToPage(this.state.currentPage + 1)}>⮞</button>
</li>
</ul>
</nav>
</div>
}

Expand Down
18 changes: 18 additions & 0 deletions src/types/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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: {
Expand Down
Loading