From 0ebcebeb7df1b13d8883050c4bc42219931db9bc Mon Sep 17 00:00:00 2001 From: Heinrich Toews Date: Tue, 27 Oct 2026 16:34:14 +0100 Subject: [PATCH 1/3] feat(filter): add saved board views Allow saving the active card filter as a named view per board and re-apply it from the filter popover. Signed-off-by: Heinrich Toews --- appinfo/routes.php | 5 + lib/Controller/BoardViewApiController.php | 48 ++++++++ lib/Db/BoardView.php | 34 ++++++ lib/Db/BoardViewMapper.php | 43 +++++++ .../Version11003Date20260821120000.php | 58 ++++++++++ lib/Service/BoardViewService.php | 75 +++++++++++++ src/components/Controls.vue | 104 ++++++++++++++++- src/services/BoardApi.js | 62 ++++++++++ src/stores/board.js | 31 +++++ tests/unit/Service/BoardViewServiceTest.php | 106 ++++++++++++++++++ 10 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 lib/Controller/BoardViewApiController.php create mode 100644 lib/Db/BoardView.php create mode 100644 lib/Db/BoardViewMapper.php create mode 100644 lib/Migration/Version11003Date20260821120000.php create mode 100644 lib/Service/BoardViewService.php create mode 100644 tests/unit/Service/BoardViewServiceTest.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 8a2b9be5fe..4297058a09 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -144,6 +144,11 @@ ['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#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'], diff --git a/lib/Controller/BoardViewApiController.php b/lib/Controller/BoardViewApiController.php new file mode 100644 index 0000000000..f5f1fcca42 --- /dev/null +++ b/lib/Controller/BoardViewApiController.php @@ -0,0 +1,48 @@ +boardViewService->findAll($boardId)); + } + + #[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([]); + } +} diff --git a/lib/Db/BoardView.php b/lib/Db/BoardView.php new file mode 100644 index 0000000000..76e21b36c6 --- /dev/null +++ b/lib/Db/BoardView.php @@ -0,0 +1,34 @@ +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; + } +} diff --git a/lib/Db/BoardViewMapper.php b/lib/Db/BoardViewMapper.php new file mode 100644 index 0000000000..f1c65d90cf --- /dev/null +++ b/lib/Db/BoardViewMapper.php @@ -0,0 +1,43 @@ + */ +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); + } + + 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); + } +} diff --git a/lib/Migration/Version11003Date20260821120000.php b/lib/Migration/Version11003Date20260821120000.php new file mode 100644 index 0000000000..f523cbfaed --- /dev/null +++ b/lib/Migration/Version11003Date20260821120000.php @@ -0,0 +1,58 @@ +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'); + } + return $schema; + } +} diff --git a/lib/Service/BoardViewService.php b/lib/Service/BoardViewService.php new file mode 100644 index 0000000000..557ae52083 --- /dev/null +++ b/lib/Service/BoardViewService.php @@ -0,0 +1,75 @@ +boardService->find($boardId); + return $this->boardViewMapper->findAll($boardId, $this->userId ?? ''); + } + + public function create(int $boardId, string $name, array $filters): BoardView { + $this->boardService->find($boardId); + if ($name === '') { + throw new BadRequestException('The name must not be empty'); + } + + $view = new BoardView(); + $view->setBoardId($boardId); + $view->setName($name); + $view->setFilters(json_encode($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); + $view = $this->boardViewMapper->find($viewId, $this->userId ?? ''); + if ($view->getBoardId() !== $boardId) { + throw new BadRequestException('The view does not belong to the given board'); + } + + if ($name !== '') { + $view->setName($name); + } + $view->setFilters(json_encode($filters)); + $view->setLastModifiedAt(time()); + return $this->boardViewMapper->update($view); + } + + /** + * @throws DoesNotExistException + */ + public function delete(int $boardId, int $viewId): void { + $this->boardService->find($boardId); + $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); + } +} diff --git a/src/components/Controls.vue b/src/components/Controls.vue index 2c09e1fbe7..3829c9b272 100644 --- a/src/components/Controls.vue +++ b/src/components/Controls.vue @@ -107,6 +107,28 @@