From 5f7ce12c6391a1a73791c67c12b224e3275924e9 Mon Sep 17 00:00:00 2001 From: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:16:47 +0200 Subject: [PATCH 1/4] feat(boards): per-user boardOrder config value Signed-off-by: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> Co-Authored-By: Claude Fable 5 --- lib/Service/ConfigService.php | 27 ++++++- tests/unit/Service/ConfigServiceTest.php | 92 ++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/unit/Service/ConfigServiceTest.php diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index b4afb8fc45..f81e1296b6 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(), + 'boardOrder' => $this->getBoardOrder() ]; if ($this->groupManager->isAdmin($userId)) { $data['groupLimit'] = $this->get('groupLimit'); @@ -90,6 +91,8 @@ public function get(string $key) { return false; } return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardIdBadge', false); + case 'boardOrder': + return $this->getBoardOrder(); } return false; } @@ -181,6 +184,14 @@ public function set($key, $value) { $this->config->setUserValue($userId, Application::APP_ID, 'cardIdBadge', (string)$value); $result = $value; break; + case 'boardOrder': + if (!is_array($value)) { + throw new BadRequestException('boardOrder must be a list of board ids'); + } + $ids = array_values(array_map('intval', array_filter($value, 'is_numeric'))); + $this->config->setUserValue($userId, Application::APP_ID, 'boardOrder', json_encode($ids)); + $result = $ids; + break; case 'board': // extra check that user only send one of the allowed board settings and not something random $parts = explode(':', $key, 3); @@ -236,6 +247,20 @@ private function getGroupLimit() { return array_filter($groups); } + /** @return int[] */ + private function getBoardOrder(): array { + $userId = $this->getUserId(); + if ($userId === null) { + return []; + } + $value = $this->config->getUserValue($userId, Application::APP_ID, 'boardOrder', '[]'); + $decoded = json_decode($value, true); + if (!is_array($decoded)) { + return []; + } + return array_values(array_map('intval', array_filter($decoded, 'is_numeric'))); + } + public function getAttachmentFolder(?string $userId = null): string { if ($userId === null && $this->getUserId() === null) { throw new NoPermissionException('Must be logged in get the attachment folder'); diff --git a/tests/unit/Service/ConfigServiceTest.php b/tests/unit/Service/ConfigServiceTest.php new file mode 100644 index 0000000000..46456d5d68 --- /dev/null +++ b/tests/unit/Service/ConfigServiceTest.php @@ -0,0 +1,92 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Service; + +use OCA\Deck\BadRequestException; +use OCP\IConfig; +use OCP\IGroupManager; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ConfigServiceTest extends TestCase { + + private IConfig&MockObject $config; + private IGroupManager&MockObject $groupManager; + /** @var ConfigService|MockObject */ + private $configService; + + public function setUp(): void { + parent::setUp(); + $this->config = $this->createMock(IConfig::class); + $this->groupManager = $this->createMock(IGroupManager::class); + + // ConfigService::getUserId() resolves the current user through + // \OCP\Server::get(IUserSession::class) instead of a constructor + // dependency, so it can't be mocked via the constructor. Stub just + // that method and keep everything else on the real implementation. + $this->configService = $this->getMockBuilder(ConfigService::class) + ->setConstructorArgs([$this->config, $this->groupManager]) + ->onlyMethods(['getUserId']) + ->getMock(); + $this->configService->method('getUserId')->willReturn('admin'); + } + + public function testBoardOrderRoundTrip() { + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('admin', 'deck', 'boardOrder', '[4,2,9]'); + $result = $this->configService->set('boardOrder', [4, '2', 9.0]); + $this->assertSame([4, 2, 9], $result); + } + + public function testBoardOrderRejectsNonArray() { + $this->expectException(BadRequestException::class); + $this->configService->set('boardOrder', 'not-a-list'); + } + + public function testGetBoardOrderDecodesAndFiltersStoredValue() { + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('admin', 'deck', 'boardOrder', '[]') + ->willReturn('[3,1,5,"x"]'); + $this->assertSame([3, 1, 5], $this->configService->get('boardOrder')); + } + + public function testGetAllIncludesBoardOrder() { + $this->config->method('getUserValue')->willReturnCallback(function ($userId, $appId, $key, $default) { + if ($key === 'boardOrder') { + return '[7,3]'; + } + return $default; + }); + $this->config->method('getAppValue')->willReturnArgument(2); + $this->groupManager->method('isAdmin')->willReturn(false); + + $data = $this->configService->getAll(); + $this->assertSame([7, 3], $data['boardOrder']); + } +} From f1096c6228cac6e11fcc88aca04216708a7cd420 Mon Sep 17 00:00:00 2001 From: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:25:40 +0200 Subject: [PATCH 2/4] feat(boards): per-user drag and drop ordering in the navigation Signed-off-by: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> Co-Authored-By: Claude Fable 5 --- src/components/navigation/AppNavigation.vue | 1 + .../navigation/AppNavigationBoardCategory.vue | 47 ++++++++++++++++++- src/helpers/boardSort.js | 24 ++++++++++ src/helpers/boardSort.spec.js | 27 +++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/helpers/boardSort.js create mode 100644 src/helpers/boardSort.spec.js diff --git a/src/components/navigation/AppNavigation.vue b/src/components/navigation/AppNavigation.vue index e0cffe2387..52c2fcd777 100644 --- a/src/components/navigation/AppNavigation.vue +++ b/src/components/navigation/AppNavigation.vue @@ -20,6 +20,7 @@ :boards="noneArchivedBoards" :open-on-add-boards="true" :default-open="true" + :sortable="true" icon="icon-deck">