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
6 changes: 6 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@
['name' => 'board_ocs#createForTeam', 'url' => '/api/v{apiVersion}/boards/team', 'verb' => 'POST'],
['name' => 'board_ocs#addAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl', 'verb' => 'POST'],

['name' => 'board_view_api#all', 'url' => '/api/v{apiVersion}/views', 'verb' => 'GET'],
['name' => 'board_view_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/views', 'verb' => 'GET'],
['name' => 'board_view_api#create', 'url' => '/api/v{apiVersion}/boards/{boardId}/views', 'verb' => 'POST'],
['name' => 'board_view_api#update', 'url' => '/api/v{apiVersion}/boards/{boardId}/views/{viewId}', 'verb' => 'PUT'],
['name' => 'board_view_api#delete', 'url' => '/api/v{apiVersion}/boards/{boardId}/views/{viewId}', 'verb' => 'DELETE'],

['name' => 'card_ocs#create', 'url' => '/api/v{apiVersion}/cards', 'verb' => 'POST'],
['name' => 'card_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}', 'verb' => 'PUT'],
['name' => 'card_ocs#assignLabel', 'url' => '/api/v{apiVersion}/cards/{cardId}/label/{labelId}', 'verb' => 'POST'],
Expand Down
52 changes: 52 additions & 0 deletions lib/Controller/BoardViewApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

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

namespace OCA\Deck\Controller;

use OCA\Deck\Service\BoardViewService;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;

class BoardViewApiController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private BoardViewService $boardViewService,
) {
parent::__construct($appName, $request);
}

#[NoAdminRequired]
public function index(int $boardId): DataResponse {
return new DataResponse($this->boardViewService->findAll($boardId));
}

#[NoAdminRequired]
public function all(): DataResponse {
return new DataResponse($this->boardViewService->findAllForUser());
}

#[NoAdminRequired]
public function create(int $boardId, string $name, array $filters): DataResponse {
return new DataResponse($this->boardViewService->create($boardId, $name, $filters));
}

#[NoAdminRequired]
public function update(int $boardId, int $viewId, string $name, array $filters): DataResponse {
return new DataResponse($this->boardViewService->update($boardId, $viewId, $name, $filters));
}

#[NoAdminRequired]
public function delete(int $boardId, int $viewId): DataResponse {
$this->boardViewService->delete($boardId, $viewId);
return new DataResponse([]);
}
}
34 changes: 34 additions & 0 deletions lib/Db/BoardView.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

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

namespace OCA\Deck\Db;

class BoardView extends RelationalEntity implements \JsonSerializable {
protected $boardId;
protected $name;
protected $filters;
protected $owner;
protected $createdAt;
protected $lastModifiedAt;

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

public function jsonSerialize(): array {
$json = parent::jsonSerialize();
if (isset($json['filters']) && is_string($json['filters'])) {
$json['filters'] = json_decode($json['filters'], true) ?? [];
}
return $json;
}
}
55 changes: 55 additions & 0 deletions lib/Db/BoardViewMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

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

namespace OCA\Deck\Db;

use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

/** @template-extends QBMapper<BoardView> */
class BoardViewMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'deck_board_views', BoardView::class);
}

/**
* @return BoardView[]
*/
public function findAll(int $boardId, string $userId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('owner', $qb->createNamedParameter($userId)))
->orderBy('name', 'ASC');
return $this->findEntities($qb);
}

/**
* @return BoardView[]
*/
public function findAllForUser(string $userId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('owner', $qb->createNamedParameter($userId)))
->orderBy('name', 'ASC');
return $this->findEntities($qb);
}

public function find(int $id, string $userId): BoardView {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('owner', $qb->createNamedParameter($userId)));
return $this->findEntity($qb);
}
}
59 changes: 59 additions & 0 deletions lib/Migration/Version11003Date20260821120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

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

namespace OCA\Deck\Migration;

use Closure;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version11003Date20260821120000 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
$schema = $schemaClosure();

if (!$schema->hasTable('deck_board_views')) {
$table = $schema->createTable('deck_board_views');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
'length' => 4,
]);
$table->addColumn('board_id', 'integer', [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('name', 'string', [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('filters', 'text', [
'notnull' => true,
]);
$table->addColumn('owner', 'string', [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('created_at', 'integer', [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('last_modified_at', 'integer', [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['board_id', 'owner'], 'deck_board_views_idx_board_owner');
$table->addIndex(['owner'], 'deck_board_views_idx_owner');
}
return $schema;
}
}
120 changes: 120 additions & 0 deletions lib/Service/BoardViewService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

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

namespace OCA\Deck\Service;

use JsonException;
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\BoardView;
use OCA\Deck\Db\BoardViewMapper;
use OCP\AppFramework\Db\DoesNotExistException;

class BoardViewService {
private const MAX_VIEWS_PER_BOARD = 50;
private const MAX_NAME_LENGTH = 255;
private const MAX_FILTERS_SIZE = 60000;

public function __construct(
private BoardViewMapper $boardViewMapper,
private BoardService $boardService,
private ConfigService $configService,
private ?string $userId,
) {
}

/**
* @return BoardView[]
*/
public function findAll(int $boardId): array {
$this->boardService->find($boardId, false);
return $this->boardViewMapper->findAll($boardId, $this->userId ?? '');
}

/**
* @return BoardView[]
*/
public function findAllForUser(): array {
return $this->boardViewMapper->findAllForUser($this->userId ?? '');
}

public function create(int $boardId, string $name, array $filters): BoardView {
$this->boardService->find($boardId, false);
$name = $this->validateName($name, true);
if (count($this->boardViewMapper->findAll($boardId, $this->userId ?? '')) >= self::MAX_VIEWS_PER_BOARD) {
throw new BadRequestException('You have reached the maximum number of saved views for this board');
}

$view = new BoardView();
$view->setBoardId($boardId);
$view->setName($name);
$view->setFilters($this->encodeFilters($filters));
$view->setOwner($this->userId ?? '');
$view->setCreatedAt(time());
$view->setLastModifiedAt(time());
return $this->boardViewMapper->insert($view);
}

public function update(int $boardId, int $viewId, string $name, array $filters): BoardView {
$this->boardService->find($boardId, false);
$view = $this->boardViewMapper->find($viewId, $this->userId ?? '');
if ($view->getBoardId() !== $boardId) {
throw new BadRequestException('The view does not belong to the given board');
}

$name = $this->validateName($name, false);
if ($name !== '') {
$view->setName($name);
}
$view->setFilters($this->encodeFilters($filters));
$view->setLastModifiedAt(time());
return $this->boardViewMapper->update($view);
}

/**
* @throws DoesNotExistException
*/
public function delete(int $boardId, int $viewId): void {
$this->boardService->find($boardId, false);
$view = $this->boardViewMapper->find($viewId, $this->userId ?? '');
if ($view->getBoardId() !== $boardId) {
throw new BadRequestException('The view does not belong to the given board');
}
$this->boardViewMapper->delete($view);

if ($this->configService->getDefaultBoardView() === $viewId) {
$this->configService->set('defaultBoardView', null);
}
}

private function validateName(string $name, bool $required): string {
$name = trim($name);
if ($name === '') {
if ($required) {
throw new BadRequestException('The name must not be empty');
}
return '';
}
if (mb_strlen($name) > self::MAX_NAME_LENGTH) {
throw new BadRequestException('The name must not be longer than ' . self::MAX_NAME_LENGTH . ' characters');
}
return $name;
}

private function encodeFilters(array $filters): string {
try {
$encoded = json_encode($filters, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new BadRequestException('The filters are not valid JSON');
}
if (strlen($encoded) > self::MAX_FILTERS_SIZE) {
throw new BadRequestException('The filters are too large');
}
return $encoded;
}
}
20 changes: 18 additions & 2 deletions lib/Service/ConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public function getAll(): array {
'calendar' => $this->isCalendarEnabled(),
'cardDetailsInModal' => $this->isCardDetailsInModal(),
'cardIdBadge' => $this->isCardIdBadgeEnabled(),
'stackAddCardAtTop' => $this->isStackAddCardAtTopEnabled()
'stackAddCardAtTop' => $this->isStackAddCardAtTopEnabled(),
'defaultBoardView' => $this->getDefaultBoardView(),
];
if ($this->groupManager->isAdmin($userId)) {
$data['groupLimit'] = $this->get('groupLimit');
Expand All @@ -67,7 +68,7 @@ public function getAll(): array {
}

/**
* @return bool|array{id: string, displayname: string}[]
* @return bool|int|null|array{id: string, displayname: string}[]
* @throws NoPermissionException
*/
public function get(string $key) {
Expand Down Expand Up @@ -95,10 +96,21 @@ public function get(string $key) {
return false;
}
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardIdBadge', false);
case 'defaultBoardView':
return $this->getDefaultBoardView();
}
return false;
}

public function getDefaultBoardView(): ?int {
$userId = $this->getUserId();
if ($userId === null) {
return null;
}
$value = $this->config->getUserValue($userId, Application::APP_ID, 'defaultBoardView', '');
return $value === '' ? null : (int)$value;
}

public function isCalendarEnabled(?int $boardId = null): bool {
$userId = $this->getUserId();
if ($userId === null) {
Expand Down Expand Up @@ -211,6 +223,10 @@ public function set($key, $value) {
$this->config->setUserValue($userId, Application::APP_ID, 'stackAddCardAtTop', (string)$value);
$result = $value;
break;
case 'defaultBoardView':
$this->config->setUserValue($userId, Application::APP_ID, 'defaultBoardView', $value === null ? '' : (string)(int)$value);
$result = $value === null ? '' : $value;
break;
case 'board':
// extra check that user only send one of the allowed board settings and not something random
$parts = explode(':', $key, 3);
Expand Down
Loading