diff options
author | provokateurin <kate@provokateurin.de> | 2024-08-27 12:13:04 +0200 |
---|---|---|
committer | provokateurin <kate@provokateurin.de> | 2024-09-09 11:04:36 +0200 |
commit | b0baaaed9dfdce8c2630255d0d1921679579761d (patch) | |
tree | 552abcca392fd0e8b0241ff853c636f12fa6bf1c | |
parent | 0f732199d22cf85d991f48389c33fb6e76b7eefc (diff) | |
download | nextcloud-server-b0baaaed9dfdce8c2630255d0d1921679579761d.tar.gz nextcloud-server-b0baaaed9dfdce8c2630255d0d1921679579761d.zip |
feat(NavigationManager): Add default entries handling
Signed-off-by: provokateurin <kate@provokateurin.de>
-rw-r--r-- | apps/files/lib/App.php | 4 | ||||
-rw-r--r-- | lib/private/NavigationManager.php | 97 | ||||
-rw-r--r-- | lib/public/INavigationManager.php | 38 | ||||
-rw-r--r-- | tests/lib/NavigationManagerTest.php | 169 |
4 files changed, 296 insertions, 12 deletions
diff --git a/apps/files/lib/App.php b/apps/files/lib/App.php index e24f204b744..3fb93df6f1d 100644 --- a/apps/files/lib/App.php +++ b/apps/files/lib/App.php @@ -16,6 +16,7 @@ use OCP\IURLGenerator; use OCP\IUserSession; use OCP\L10N\IFactory; use OCP\Server; +use Psr\Log\LoggerInterface; class App { private static ?INavigationManager $navigationManager = null; @@ -32,7 +33,8 @@ class App { Server::get(IFactory::class), Server::get(IUserSession::class), Server::get(IGroupManager::class), - Server::get(IConfig::class) + Server::get(IConfig::class), + Server::get(LoggerInterface::class), ); self::$navigationManager->clear(false); } diff --git a/lib/private/NavigationManager.php b/lib/private/NavigationManager.php index 5d71c83e77a..e1fbb02fda4 100644 --- a/lib/private/NavigationManager.php +++ b/lib/private/NavigationManager.php @@ -7,6 +7,7 @@ */ namespace OC; +use InvalidArgumentException; use OC\App\AppManager; use OC\Group\Manager; use OCP\App\IAppManager; @@ -14,8 +15,10 @@ use OCP\IConfig; use OCP\IGroupManager; use OCP\INavigationManager; use OCP\IURLGenerator; +use OCP\IUser; use OCP\IUserSession; use OCP\L10N\IFactory; +use Psr\Log\LoggerInterface; /** * Manages the ownCloud navigation @@ -41,25 +44,30 @@ class NavigationManager implements INavigationManager { private $groupManager; /** @var IConfig */ private $config; - /** The default app for the current user (cached for the `add` function) */ - private ?string $defaultApp; + /** The default entry for the current user (cached for the `add` function) */ + private ?string $defaultEntry; /** User defined app order (cached for the `add` function) */ private array $customAppOrder; + private LoggerInterface $logger; - public function __construct(IAppManager $appManager, + public function __construct( + IAppManager $appManager, IURLGenerator $urlGenerator, IFactory $l10nFac, IUserSession $userSession, IGroupManager $groupManager, - IConfig $config) { + IConfig $config, + LoggerInterface $logger, + ) { $this->appManager = $appManager; $this->urlGenerator = $urlGenerator; $this->l10nFac = $l10nFac; $this->userSession = $userSession; $this->groupManager = $groupManager; $this->config = $config; + $this->logger = $logger; - $this->defaultApp = null; + $this->defaultEntry = null; } /** @@ -93,7 +101,7 @@ class NavigationManager implements INavigationManager { } // This is the default app that will always be shown first - $entry['default'] = ($entry['app'] ?? false) === $this->defaultApp; + $entry['default'] = ($entry['id'] ?? false) === $this->defaultEntry; // Set order from user defined app order $entry['order'] = (int)($this->customAppOrder[$id]['order'] ?? $entry['order'] ?? 100); } @@ -156,10 +164,10 @@ class NavigationManager implements INavigationManager { unset($navEntry); } - $activeApp = $this->getActiveEntry(); - if ($activeApp !== null) { + $activeEntry = $this->getActiveEntry(); + if ($activeEntry !== null) { foreach ($list as $index => &$navEntry) { - if ($navEntry['id'] == $activeApp) { + if ($navEntry['id'] == $activeEntry) { $navEntry['active'] = true; } else { $navEntry['active'] = false; @@ -213,7 +221,7 @@ class NavigationManager implements INavigationManager { ]); } - $this->defaultApp = $this->appManager->getDefaultAppForUser($this->userSession->getUser(), false); + $this->defaultEntry = $this->getDefaultEntryIdForUser($this->userSession->getUser(), false); if ($this->userSession->isLoggedIn()) { // Profile @@ -401,4 +409,73 @@ class NavigationManager implements INavigationManager { public function setUnreadCounter(string $id, int $unreadCounter): void { $this->unreadCounters[$id] = $unreadCounter; } + + public function get(string $id): array|null { + $this->init(); + foreach ($this->closureEntries as $c) { + $this->add($c()); + } + $this->closureEntries = []; + + return $this->entries[$id]; + } + + public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallbacks = true): string { + $this->init(); + $defaultEntryIds = explode(',', $this->config->getSystemValueString('defaultapp', '')); + $defaultEntryIds = array_filter($defaultEntryIds); + + $user ??= $this->userSession->getUser(); + + if ($user !== null) { + $userDefaultEntryIds = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp')); + $defaultEntryIds = array_filter(array_merge($userDefaultEntryIds, $defaultEntryIds)); + if (empty($defaultEntryIds) && $withFallbacks) { + /* Fallback on user defined apporder */ + $customOrders = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags: JSON_THROW_ON_ERROR); + if (!empty($customOrders)) { + // filter only entries with app key (when added using closures or NavigationManager::add the app is not guaranteed to be set) + $customOrders = array_filter($customOrders, static fn ($entry) => isset($entry['app'])); + // sort apps by order + usort($customOrders, static fn ($a, $b) => $a['order'] - $b['order']); + // set default apps to sorted apps + $defaultEntryIds = array_map(static fn ($entry) => $entry['app'], $customOrders); + } + } + } + + if (empty($defaultEntryIds) && $withFallbacks) { + $defaultEntryIds = ['dashboard','files']; + } + + $entryIds = array_keys($this->entries); + + // Find the first app that is enabled for the current user + foreach ($defaultEntryIds as $defaultEntryId) { + if (in_array($defaultEntryId, $entryIds, true)) { + return $defaultEntryId; + } + } + + // Set fallback to always-enabled files app + return $withFallbacks ? 'files' : ''; + } + + public function getDefaultEntryIds(): array { + return explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files')); + } + + public function setDefaultEntryIds(array $ids): void { + $this->init(); + $entryIds = array_keys($this->entries); + + foreach ($ids as $id) { + if (!in_array($id, $entryIds, true)) { + $this->logger->debug('Cannot set unavailable entry as default entry', ['missing_entry' => $id]); + throw new InvalidArgumentException('Entry not available'); + } + } + + $this->config->setSystemValue('defaultapp', join(',', $ids)); + } } diff --git a/lib/public/INavigationManager.php b/lib/public/INavigationManager.php index eaef1cb35ec..d22e96aa9d3 100644 --- a/lib/public/INavigationManager.php +++ b/lib/public/INavigationManager.php @@ -80,4 +80,42 @@ interface INavigationManager { * @since 22.0.0 */ public function setUnreadCounter(string $id, int $unreadCounter): void; + + /** + * Get a navigation entry by id. + * + * @param string $id ID of the navigation entry + * @since 31.0.0 + */ + public function get(string $id): array|null; + + /** + * Returns the id of the user's default entry + * + * If `user` is not passed, the currently logged in user will be used + * + * @param ?IUser $user User to query default entry for + * @param bool $withFallbacks Include fallback values if no default entry was configured manually + * Before falling back to predefined default entries, + * the user defined entry order is considered and the first entry would be used as the fallback. + * @since 31.0.0 + */ + public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallbacks = true): string; + + /** + * Get the global default entries with fallbacks + * + * @return string[] The default entries + * @since 31.0.0 + */ + public function getDefaultEntryIds(): array; + + /** + * Set the global default entries with fallbacks + * + * @param string[] $ids + * @throws \InvalidArgumentException If any of the entries is not available + * @since 31.0.0 + */ + public function setDefaultEntryIds(array $ids): void; } diff --git a/tests/lib/NavigationManagerTest.php b/tests/lib/NavigationManagerTest.php index 71bebe7ce54..416b4730147 100644 --- a/tests/lib/NavigationManagerTest.php +++ b/tests/lib/NavigationManagerTest.php @@ -18,6 +18,7 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; use OCP\L10N\IFactory; +use Psr\Log\LoggerInterface; class NavigationManagerTest extends TestCase { /** @var AppManager|\PHPUnit\Framework\MockObject\MockObject */ @@ -35,6 +36,7 @@ class NavigationManagerTest extends TestCase { /** @var \OC\NavigationManager */ protected $navigationManager; + protected LoggerInterface $logger; protected function setUp(): void { parent::setUp(); @@ -45,13 +47,15 @@ class NavigationManagerTest extends TestCase { $this->userSession = $this->createMock(IUserSession::class); $this->groupManager = $this->createMock(Manager::class); $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->navigationManager = new NavigationManager( $this->appManager, $this->urlGenerator, $this->l10nFac, $this->userSession, $this->groupManager, - $this->config + $this->config, + $this->logger, ); $this->navigationManager->clear(false); @@ -557,4 +561,167 @@ class NavigationManagerTest extends TestCase { $entries = $this->navigationManager->getAll(); $this->assertEquals($expected, $entries); } + + public static function provideDefaultEntries(): array { + return [ + // none specified, default to files + [ + '', + '', + '{}', + true, + 'files', + ], + // none specified, without fallback + [ + '', + '', + '{}', + false, + '', + ], + // unexisting or inaccessible app specified, default to files + [ + 'unexist', + '', + '{}', + true, + 'files', + ], + // unexisting or inaccessible app specified, without fallbacks + [ + 'unexist', + '', + '{}', + false, + '', + ], + // non-standard app + [ + 'settings', + '', + '{}', + true, + 'settings', + ], + // non-standard app, without fallback + [ + 'settings', + '', + '{}', + false, + 'settings', + ], + // non-standard app with fallback + [ + 'unexist,settings', + '', + '{}', + true, + 'settings', + ], + // system default app and user apporder + [ + // system default is settings + 'unexist,settings', + '', + // apporder says default app is files (order is lower) + '{"files_id":{"app":"files","order":1},"settings_id":{"app":"settings","order":2}}', + true, + // system default should override apporder + 'settings' + ], + // user-customized defaultapp + [ + '', + 'files', + '', + true, + 'files', + ], + // user-customized defaultapp with systemwide + [ + 'unexist,settings', + 'files', + '', + true, + 'files', + ], + // user-customized defaultapp with system wide and apporder + [ + 'unexist,settings', + 'files', + '{"settings_id":{"app":"settings","order":1},"files_id":{"app":"files","order":2}}', + true, + 'files', + ], + // user-customized apporder fallback + [ + '', + '', + '{"settings_id":{"app":"settings","order":1},"files":{"app":"files","order":2}}', + true, + 'settings', + ], + // user-customized apporder fallback with missing app key (entries added by closures does not always have an app key set (Nextcloud 27 spreed app for example)) + [ + '', + '', + '{"spreed":{"order":1},"files":{"app":"files","order":2}}', + true, + 'files', + ], + // user-customized apporder, but called without fallback + [ + '', + '', + '{"settings":{"app":"settings","order":1},"files":{"app":"files","order":2}}', + false, + '', + ], + // user-customized apporder with an app that has multiple routes + [ + '', + '', + '{"settings_id":{"app":"settings","order":1},"settings_id_2":{"app":"settings","order":3},"id_files":{"app":"files","order":2}}', + true, + 'settings', + ], + ]; + } + + /** + * @dataProvider provideDefaultEntries + */ + public function testGetDefaultEntryIdForUser($defaultApps, $userDefaultApps, $userApporder, $withFallbacks, $expectedApp) { + $this->navigationManager->add([ + 'id' => 'files', + ]); + $this->navigationManager->add([ + 'id' => 'settings', + ]); + + $this->appManager->method('getInstalledApps')->willReturn([]); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user1'); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->config->expects($this->once()) + ->method('getSystemValueString') + ->with('defaultapp', $this->anything()) + ->willReturn($defaultApps); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->willReturnMap([ + ['user1', 'core', 'defaultapp', '', $userDefaultApps], + ['user1', 'core', 'apporder', '[]', $userApporder], + ]); + + $this->assertEquals($expectedApp, $this->navigationManager->getDefaultEntryIdForUser(null, $withFallbacks)); + } } |