Skip to content
Open
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
7 changes: 7 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@
['name' => 'Context#importScheme', 'url' => '/api/2/contexts/{contextId}/scheme/import', 'verb' => 'POST'],
['name' => 'Context#transfer', 'url' => '/api/2/contexts/{contextId}/transfer', 'verb' => 'PUT'],
['name' => 'Context#updateContentOrder', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'PUT'],
['name' => 'ContextPage#index', 'url' => '/api/2/contexts/{contextId}/pages', 'verb' => 'GET'],
['name' => 'ContextPage#create', 'url' => '/api/2/contexts/{contextId}/pages', 'verb' => 'POST'],
['name' => 'ContextPage#show', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'GET'],
['name' => 'ContextPage#update', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'PATCH'],
['name' => 'ContextPage#destroy', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'DELETE'],
['name' => 'ContextPage#saveWidgets', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}/widgets', 'verb' => 'PUT'],
['name' => 'ContextPage#widgetTypes', 'url' => '/api/2/widget-types', 'verb' => 'GET'],

['name' => 'Config#getTableConfig', 'url' => '/api/2/config/table/{id}', 'verb' => 'GET'],
['name' => 'Config#getViewConfig', 'url' => '/api/2/config/view/{id}', 'verb' => 'GET'],
Expand Down
234 changes: 234 additions & 0 deletions lib/Controller/ContextPageController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<?php

declare(strict_types=1);

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

namespace OCA\Tables\Controller;

use OCA\Tables\AppInfo\Application;
use OCA\Tables\Errors\BadRequestError;
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
use OCA\Tables\Middleware\Attribute\RequirePermission;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\GridWidgetTypes;
use OCA\Tables\Service\PageService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\DB\Exception;
use OCP\IL10N;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

/**
* @psalm-import-type TablesPage from ResponseDefinitions
* @psalm-import-type TablesGridWidgetType from ResponseDefinitions
*/
class ContextPageController extends AOCSController {
public function __construct(
IRequest $request,
LoggerInterface $logger,
IL10N $n,
string $userId,
private readonly PageService $pageService,
private readonly GridWidgetTypes $gridWidgetTypes,
) {
parent::__construct($request, $logger, $n, $userId);
}

/**
* [api v2] Get the pages of an application with their widgets
*
* @param int $contextId ID of the application
* @return DataResponse<Http::STATUS_OK, list<TablesPage>, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Pages returned
* 404: Not found
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_READ, typeParam: 'context', idParam: 'contextId')]
public function index(int $contextId): DataResponse {
try {
return new DataResponse($this->pageService->findAll($contextId, $this->userId));
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Get one page of an application with its widgets
*
* Content without a position is placed full width below the positioned widgets.
*
* @param int $contextId ID of the application
* @param int $pageId ID of the page
* @return DataResponse<Http::STATUS_OK, TablesPage, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Page returned
* 404: Not found
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_READ, typeParam: 'context', idParam: 'contextId')]
public function show(int $contextId, int $pageId): DataResponse {
try {
return new DataResponse($this->pageService->find($contextId, $pageId, $this->userId));
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Create a grid page in an application
*
* @param int $contextId ID of the application
* @param string $title Title of the page
* @param string|null $technicalName Name to address the page by, unique within the application; null makes one from the title
* @return DataResponse<Http::STATUS_OK, TablesPage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Page created
* 400: Invalid title or technical name
* 403: No permissions
* 404: Not found
*
* @CanManageContext
*/
#[NoAdminRequired]
public function create(int $contextId, string $title, ?string $technicalName = null): DataResponse {
try {
return new DataResponse($this->pageService->create($contextId, $title, $technicalName, $this->userId));
} catch (BadRequestError $e) {
return $this->handleBadRequestError($e);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Change the title or technical name of a page
*
* @param int $contextId ID of the application
* @param int $pageId ID of the page
* @param string|null $title New title, null keeps it
* @param string|null $technicalName New technical name, unique within the application; null keeps it
* @return DataResponse<Http::STATUS_OK, TablesPage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Page updated
* 400: Invalid title or technical name
* 403: No permissions
* 404: Not found
*
* @CanManageContext
*/
#[NoAdminRequired]
public function update(int $contextId, int $pageId, ?string $title = null, ?string $technicalName = null): DataResponse {
try {
return new DataResponse($this->pageService->update($contextId, $pageId, $title, $technicalName, $this->userId));
} catch (BadRequestError $e) {
return $this->handleBadRequestError($e);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (DoesNotExistException|MultipleObjectsReturnedException $e) {
return $this->handleNotFoundError(new NotFoundError($e->getMessage(), $e->getCode(), $e));
} catch (Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Delete a grid page with its widgets
*
* The start page of an application cannot be deleted.
*
* @param int $contextId ID of the application
* @param int $pageId ID of the page
* @return DataResponse<Http::STATUS_OK, TablesPage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Page deleted
* 400: The page is the start page
* 403: No permissions
* 404: Not found
*
* @CanManageContext
*/
#[NoAdminRequired]
public function destroy(int $contextId, int $pageId): DataResponse {
try {
return new DataResponse($this->pageService->delete($contextId, $pageId, $this->userId));
} catch (BadRequestError $e) {
return $this->handleBadRequestError($e);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Save the widgets of a page and their layout
*
* Replaces every widget of the page. Each widget has a `type`, a `configuration` and
* a `content` following the schemas of its type, and `gridX`, `gridY`, `gridWidth`
* and `gridHeight` on a grid of 12 columns. A data widget names either `nodeRelId`, a
* node of this application, or `node` as `{nodeType, nodeId}`, a table or view that is
* added to the application with read permissions when it is not part of it yet.
*
* @param int $contextId ID of the application
* @param int $pageId ID of the page
* @param list<array<string, mixed>> $widgets The widgets of the page
* @return DataResponse<Http::STATUS_OK, TablesPage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Widgets saved
* 400: Invalid widget
* 403: No permissions on the application or on a table or view to add
* 404: Not found
*
* @CanManageContext
*/
#[NoAdminRequired]
public function saveWidgets(int $contextId, int $pageId, array $widgets = []): DataResponse {
try {
return new DataResponse($this->pageService->saveWidgets($contextId, $pageId, $widgets, $this->userId));
} catch (BadRequestError $e) {
return $this->handleBadRequestError($e);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] The widget types a page can hold
*
* Every type carries two schemas: the configuration of the widget on the page and its content.
*
* @return DataResponse<Http::STATUS_OK, list<TablesGridWidgetType>, array{}>
*
* 200: Widget types returned
*/
#[NoAdminRequired]
public function widgetTypes(): DataResponse {
return new DataResponse(array_values($this->gridWidgetTypes->all()));
}
}
1 change: 1 addition & 0 deletions lib/Db/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Page extends Entity implements \JsonSerializable {

public function __construct() {
$this->addType('id', 'integer');
$this->addType('context_id', 'integer');
}

public static function isValidTechnicalName(string $technicalName): bool {
Expand Down
17 changes: 17 additions & 0 deletions lib/Db/PageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

namespace OCA\Tables\Db;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

Expand All @@ -19,6 +22,20 @@ public function __construct(IDBConnection $db) {
parent::__construct($db, $this->table, Page::class);
}

/**
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws Exception
*/
public function findById(int $pageId): Page {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->table)
->where($qb->expr()->eq('id', $qb->createNamedParameter($pageId, IQueryBuilder::PARAM_INT)));

return $this->findEntity($qb);
}

/**
* @return int[]
*/
Expand Down
40 changes: 40 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,46 @@
* ownerType: int,
* }
*
* @psalm-type TablesGridWidgetValue = string|bool|int|null
*
* @psalm-type TablesGridWidgetProperty = array{
* type: 'string'|'text'|'color'|'boolean'|'enum',
* title: string,
* default: TablesGridWidgetValue,
* options?: list<array{value: string, label: string}>,
* required?: bool,
* }
*
* @psalm-type TablesGridWidgetType = array{
* type: 'header'|'text'|'data',
* title: string,
* defaultWidth: int,
* defaultHeight: int,
* configuration: array<string, TablesGridWidgetProperty>,
* properties: array<string, TablesGridWidgetProperty>,
* }
*
* @psalm-type TablesPageContent = array{
* id: int,
* order: int,
* node_rel_id: int|null,
* type: string,
* configuration: array<string, TablesGridWidgetValue>,
* content: array<string, TablesGridWidgetValue>,
* grid_x: int,
* grid_y: int,
* grid_width: int,
* grid_height: int,
* }
*
* @psalm-type TablesPage = array{
* id: int,
* page_type: string,
* title: string|null,
* technical_name: string,
* content: list<TablesPageContent>,
* }
*
* @psalm-type TablesContextNavigation = array{
* id: int,
* shareId: int,
Expand Down
Loading
Loading