From 850040f87b67681ecaeb1bd31b79223224ec7b56 Mon Sep 17 00:00:00 2001 From: Heinrich Toews Date: Fri, 21 Aug 2026 17:09:24 +0200 Subject: [PATCH 1/2] 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 | 103 ++++++++++++++++- src/services/BoardApi.js | 62 ++++++++++ src/stores/board.js | 31 +++++ tests/unit/Service/BoardViewServiceTest.php | 106 ++++++++++++++++++ 10 files changed, 564 insertions(+), 1 deletion(-) 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 c449af0e2a..c247a2ce66 100644 --- a/src/components/Controls.vue +++ b/src/components/Controls.vue @@ -101,6 +101,28 @@
+
+

{{ t('deck', 'Saved views') }}

+
+ + + {{ view.name }} + + + + +
+
+

{{ t('deck', 'Filter by tag') }}

{{ t('deck', 'No due date') }}
+
+ + + + {{ t('deck', 'Save view') }} + +
+ {{ t('deck', 'Clear filter') }} @@ -293,6 +330,9 @@ import ArchiveIcon from 'vue-material-design-icons/ArchiveOutline.vue' import ImageIcon from 'vue-material-design-icons/ImageMultipleOutline.vue' import FilterIcon from 'vue-material-design-icons/FilterOutline.vue' import FilterOffIcon from 'vue-material-design-icons/FilterOffOutline.vue' +import BookmarkOutline from 'vue-material-design-icons/BookmarkOutline.vue' +import ContentSave from 'vue-material-design-icons/ContentSave.vue' +import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue' import TableColumnPlusAfter from 'vue-material-design-icons/TableColumnPlusAfter.vue' import ArrowCollapseVerticalIcon from 'vue-material-design-icons/ArrowCollapseVertical.vue' import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertical.vue' @@ -321,6 +361,9 @@ export default { ImageIcon, FilterIcon, FilterOffIcon, + BookmarkOutline, + ContentSave, + TrashCanOutline, ArrowCollapseVerticalIcon, ArrowExpandVerticalIcon, ViewColumnIcon, @@ -362,6 +405,7 @@ export default { filterVisible: false, isAddStackVisible: false, filter: { tags: [], users: [], due: '', unassigned: false, completed: 'both' }, + newViewName: '', showAddCardModal: false, defaultPageTitle: false, isNotifyPushEnabled: isNotifyPushEnabled(), @@ -374,6 +418,7 @@ export default { 'canManage', 'viewMode', 'showArchived', + 'boardViews', ]), ...mapStateVuex({ isFullApp: state => state.isFullApp, @@ -403,6 +448,10 @@ export default { board(current, previous) { if (current?.id !== previous?.id) { this.clearFilter() + this.newViewName = '' + if (current?.id) { + this.loadBoardViews(current.id) + } } if (current) { this.setPageTitle(current.title) @@ -424,7 +473,15 @@ export default { this.setPageTitle('') }, methods: { - ...mapActions(useBoardStore, { setViewMode: 'setViewMode', toggleShowArchived: 'toggleShowArchived', setFilterInStore: 'setFilterInStore' }), + ...mapActions(useBoardStore, { + setViewMode: 'setViewMode', + toggleShowArchived: 'toggleShowArchived', + setFilterInStore: 'setFilterInStore', + loadBoardViews: 'loadBoardViews', + createBoardView: 'createBoardView', + deleteBoardView: 'deleteBoardView', + applyBoardView: 'applyBoardView', + }), ...mapActions(useStackStore, ['createStack']), beforeSetFilter(e) { if (this.filter.due === e.target.value) { @@ -486,6 +543,26 @@ export default { this.setFilterInStore({ ...filterReset }) this.filter = filterReset }, + applyView(view) { + this.applyBoardView(view) + this.filter = { + tags: [...(view.filters?.tags || [])], + users: [...(view.filters?.users || [])], + due: view.filters?.due || '', + unassigned: view.filters?.unassigned || false, + completed: view.filters?.completed || 'both', + } + }, + async removeView(view) { + await this.deleteBoardView(view.boardId, view.id) + }, + async saveView() { + if (!this.isFilterActive || this.newViewName.trim() === '' || !this.board) { + return + } + await this.createBoardView(this.board.id, this.newViewName.trim()) + this.newViewName = '' + }, clickShowAddCardModel() { this.showAddCardModal = true }, @@ -655,6 +732,30 @@ export default { margin-bottom: 5px; } + .filter--saved-views { + border-bottom: 1px solid var(--color-border); + margin-bottom: 8px; + padding-bottom: 4px; + + .filter--saved-view { + display: flex; + align-items: center; + gap: 4px; + + .filter--saved-view-name { + flex-grow: 1; + justify-content: flex-start; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + .filter--save-view { + margin-top: 8px; + } + .filter-button { padding: 0; border-radius: 50%; diff --git a/src/services/BoardApi.js b/src/services/BoardApi.js index a6551b56d8..7bbca96877 100644 --- a/src/services/BoardApi.js +++ b/src/services/BoardApi.js @@ -368,4 +368,66 @@ export class BoardApi { }) } + // Board view API calls + + loadBoardViews(boardId) { + return axios.get(this.ocsUrl(`/boards/${boardId}/views`)) + .then( + (response) => { + return Promise.resolve(response.data.ocs.data) + }, + (err) => { + return Promise.reject(err) + }, + ) + .catch((err) => { + return Promise.reject(err) + }) + } + + createBoardView(boardId, name, filters) { + return axios.post(this.ocsUrl(`/boards/${boardId}/views`), { name, filters }) + .then( + (response) => { + return Promise.resolve(response.data.ocs.data) + }, + (err) => { + return Promise.reject(err) + }, + ) + .catch((err) => { + return Promise.reject(err) + }) + } + + updateBoardView(boardId, view) { + return axios.put(this.ocsUrl(`/boards/${boardId}/views/${view.id}`), view) + .then( + (response) => { + return Promise.resolve(response.data.ocs.data) + }, + (err) => { + return Promise.reject(err) + }, + ) + .catch((err) => { + return Promise.reject(err) + }) + } + + deleteBoardView(boardId, viewId) { + return axios.delete(this.ocsUrl(`/boards/${boardId}/views/${viewId}`)) + .then( + (response) => { + return Promise.resolve(response.data.ocs.data) + }, + (err) => { + return Promise.reject(err) + }, + ) + .catch((err) => { + return Promise.reject(err) + }) + } + } diff --git a/src/stores/board.js b/src/stores/board.js index ded7af243d..9575d5a2df 100644 --- a/src/stores/board.js +++ b/src/stores/board.js @@ -28,6 +28,7 @@ export const useBoardStore = defineStore('board', { assignableUsers: [], filter: { tags: [], users: [], due: '', unassigned: false, completed: 'both' }, boardFilter: BOARD_FILTERS.ALL, + boardViews: [], boards: loadState('deck', 'initialBoards', {}), }), getters: { @@ -143,10 +144,40 @@ export const useBoardStore = defineStore('board', { }, async loadBoardById(boardId) { this.filter = { tags: [], users: [], due: '', unassigned: false, completed: 'both' } + this.boardViews = [] this.setCurrentBoard(null) const board = await apiClient.loadById(boardId) this.setCurrentBoard(board) this.setAssignableUsers(board.users) + await this.loadBoardViews(boardId) + }, + async loadBoardViews(boardId) { + this.boardViews = await apiClient.loadBoardViews(boardId) + return this.boardViews + }, + async createBoardView(boardId, name) { + const view = await apiClient.createBoardView(boardId, name, this.filter) + this.boardViews.push(view) + return view + }, + async updateBoardView(view) { + const updated = await apiClient.updateBoardView(view.boardId, view) + const index = this.boardViews.findIndex((v) => v.id === updated.id) + if (index > -1) { + Vue.set(this.boardViews, index, updated) + } + return updated + }, + async deleteBoardView(boardId, viewId) { + await apiClient.deleteBoardView(boardId, viewId) + this.boardViews = this.boardViews.filter((v) => v.id !== viewId) + }, + applyBoardView(view) { + this.setFilterInStore(this.normalizeFilter(view.filters)) + }, + normalizeFilter(filter) { + const defaults = { tags: [], users: [], due: '', unassigned: false, completed: 'both' } + return { ...defaults, ...(filter || {}) } }, async refreshBoard(boardId) { const board = await apiClient.loadById(boardId) diff --git a/tests/unit/Service/BoardViewServiceTest.php b/tests/unit/Service/BoardViewServiceTest.php new file mode 100644 index 0000000000..58be3a0209 --- /dev/null +++ b/tests/unit/Service/BoardViewServiceTest.php @@ -0,0 +1,106 @@ +boardViewMapper = $this->createMock(BoardViewMapper::class); + $this->boardService = $this->createMock(BoardService::class); + $this->boardViewService = new BoardViewService( + $this->boardViewMapper, + $this->boardService, + 'user123', + ); + } + + public function testFindAll() { + $view = new BoardView(); + $this->boardService->expects($this->once())->method('find')->with(123); + $this->boardViewMapper->expects($this->once())->method('findAll')->with(123, 'user123')->willReturn([$view]); + $this->assertEquals([$view], $this->boardViewService->findAll(123)); + } + + public function testCreate() { + $filters = ['tags' => [1], 'users' => [], 'due' => 'overdue', 'unassigned' => false, 'completed' => 'open']; + + $this->boardService->expects($this->once())->method('find')->with(123); + $this->boardViewMapper->expects($this->once())->method('insert')->willReturnCallback(function (BoardView $view) { + return $view; + }); + + $view = $this->boardViewService->create(123, 'My view', $filters); + $this->assertEquals(123, $view->getBoardId()); + $this->assertEquals('My view', $view->getName()); + $this->assertEquals('user123', $view->getOwner()); + $this->assertEquals(json_encode($filters), $view->getFilters()); + $this->assertNotNull($view->getCreatedAt()); + $this->assertNotNull($view->getLastModifiedAt()); + } + + public function testCreateEmptyName() { + $this->expectException(BadRequestException::class); + $this->boardViewService->create(123, '', []); + } + + public function testUpdate() { + $view = new BoardView(); + $view->setId(1); + $view->setBoardId(123); + $view->setName('Old name'); + $view->setFilters(json_encode(['tags' => []])); + + $this->boardService->expects($this->once())->method('find')->with(123); + $this->boardViewMapper->expects($this->once())->method('find')->with(1, 'user123')->willReturn($view); + $this->boardViewMapper->expects($this->once())->method('update')->willReturnCallback(function (BoardView $view) { + return $view; + }); + + $updated = $this->boardViewService->update(123, 1, 'New name', ['tags' => [2]]); + $this->assertEquals('New name', $updated->getName()); + $this->assertEquals(json_encode(['tags' => [2]]), $updated->getFilters()); + } + + public function testUpdateViewOfOtherBoard() { + $view = new BoardView(); + $view->setId(1); + $view->setBoardId(456); + + $this->boardService->expects($this->once())->method('find')->with(123); + $this->boardViewMapper->expects($this->once())->method('find')->with(1, 'user123')->willReturn($view); + + $this->expectException(BadRequestException::class); + $this->boardViewService->update(123, 1, 'New name', []); + } + + public function testDelete() { + $view = new BoardView(); + $view->setId(1); + $view->setBoardId(123); + + $this->boardService->expects($this->once())->method('find')->with(123); + $this->boardViewMapper->expects($this->once())->method('find')->with(1, 'user123')->willReturn($view); + $this->boardViewMapper->expects($this->once())->method('delete')->with($view); + + $this->boardViewService->delete(123, 1); + } +} From 899d018439ddf657ec3741afdafa3b1d86187e29 Mon Sep 17 00:00:00 2001 From: Heinrich Toews Date: Fri, 21 Aug 2026 17:09:24 +0200 Subject: [PATCH 2/2] feat(filter): add default view for all boards Let users pick one of their saved views as the default view in the Deck settings. It is applied automatically when opening any board; tags are only applied where they match the current board. Signed-off-by: Heinrich Toews --- appinfo/routes.php | 1 + lib/Controller/BoardViewApiController.php | 5 ++ lib/Db/BoardViewMapper.php | 12 ++++ lib/Service/BoardViewService.php | 7 +++ lib/Service/ConfigService.php | 20 +++++- src/components/Controls.vue | 75 +++++++++++++++++++++-- src/components/DeckAppSettings.vue | 46 ++++++++++++++ src/services/BoardApi.js | 15 +++++ src/stores/board.js | 7 +++ 9 files changed, 180 insertions(+), 8 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 4297058a09..0df5b22449 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -144,6 +144,7 @@ ['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'], diff --git a/lib/Controller/BoardViewApiController.php b/lib/Controller/BoardViewApiController.php index f5f1fcca42..024fd7ac4e 100644 --- a/lib/Controller/BoardViewApiController.php +++ b/lib/Controller/BoardViewApiController.php @@ -30,6 +30,11 @@ 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)); diff --git a/lib/Db/BoardViewMapper.php b/lib/Db/BoardViewMapper.php index f1c65d90cf..fbe1648e4c 100644 --- a/lib/Db/BoardViewMapper.php +++ b/lib/Db/BoardViewMapper.php @@ -32,6 +32,18 @@ public function findAll(int $boardId, string $userId): array { 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('*') diff --git a/lib/Service/BoardViewService.php b/lib/Service/BoardViewService.php index 557ae52083..4f503b5cfd 100644 --- a/lib/Service/BoardViewService.php +++ b/lib/Service/BoardViewService.php @@ -30,6 +30,13 @@ public function findAll(int $boardId): array { 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); if ($name === '') { diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 1dfa60d4ef..c0dd723782 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -52,7 +52,8 @@ public function getAll(): array { $data = [ 'calendar' => $this->isCalendarEnabled(), 'cardDetailsInModal' => $this->isCardDetailsInModal(), - 'cardIdBadge' => $this->isCardIdBadgeEnabled() + 'cardIdBadge' => $this->isCardIdBadgeEnabled(), + 'defaultBoardView' => $this->getDefaultBoardView(), ]; if ($this->groupManager->isAdmin($userId)) { $data['groupLimit'] = $this->get('groupLimit'); @@ -62,7 +63,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) { @@ -90,10 +91,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) { @@ -181,6 +193,10 @@ public function set($key, $value) { $this->config->setUserValue($userId, Application::APP_ID, 'cardIdBadge', (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); diff --git a/src/components/Controls.vue b/src/components/Controls.vue index c247a2ce66..96cf45339c 100644 --- a/src/components/Controls.vue +++ b/src/components/Controls.vue @@ -101,19 +101,32 @@
-
+

{{ t('deck', 'Saved views') }}

-
+
+ :title="view.isDefault + ? t('deck', 'Apply default view {name}', { name: view.name }) + : t('deck', 'Apply view {name}', { name: view.name })" + @click="view.isDefault ? applyDefaultView() : applyView(view)"> {{ view.name }} - + + +