From e9d4036389097708a6075d8882c32b1c7db4fb0f Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 25 Sep 2023 14:21:23 +0200 Subject: [PATCH] feat(theming): Allow to configure default apps and app order in frontend settings * Also add API for setting the value using ajax. * Add cypress tests for app order and defaul apps Signed-off-by: Ferdinand Thiessen --- apps/theming/appinfo/routes.php | 5 + .../lib/Controller/ThemingController.php | 43 ++++ .../lib/Listener/BeforePreferenceListener.php | 38 +++- apps/theming/lib/Settings/Admin.php | 35 +-- apps/theming/lib/Settings/Personal.php | 30 ++- apps/theming/src/AdminTheming.vue | 7 + apps/theming/src/UserThemes.vue | 6 +- .../src/components/AppOrderSelector.vue | 130 +++++++++++ .../components/AppOrderSelectorElement.vue | 145 ++++++++++++ .../src/components/UserAppMenuSection.vue | 122 ++++++++++ .../src/components/admin/AppMenuSection.vue | 120 ++++++++++ apps/theming/tests/Settings/PersonalTest.php | 13 +- custom.d.ts | 2 +- .../e2e/theming/navigation-bar-settings.cy.ts | 212 ++++++++++++++++++ package-lock.json | 129 +++++++++++ package.json | 1 + tests/lib/App/AppManagerTest.php | 58 ++++- 17 files changed, 1048 insertions(+), 48 deletions(-) create mode 100644 apps/theming/src/components/AppOrderSelector.vue create mode 100644 apps/theming/src/components/AppOrderSelectorElement.vue create mode 100644 apps/theming/src/components/UserAppMenuSection.vue create mode 100644 apps/theming/src/components/admin/AppMenuSection.vue create mode 100644 cypress/e2e/theming/navigation-bar-settings.cy.ts diff --git a/apps/theming/appinfo/routes.php b/apps/theming/appinfo/routes.php index 8647ae135a8..0d0eacff076 100644 --- a/apps/theming/appinfo/routes.php +++ b/apps/theming/appinfo/routes.php @@ -29,6 +29,11 @@ */ return [ 'routes' => [ + [ + 'name' => 'Theming#updateAppMenu', + 'url' => '/ajax/updateAppMenu', + 'verb' => 'PUT', + ], [ 'name' => 'Theming#updateStylesheet', 'url' => '/ajax/updateStylesheet', diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index 1d6d5100a46..e8f6ec6289b 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -38,6 +38,7 @@ */ namespace OCA\Theming\Controller; +use InvalidArgumentException; use OCA\Theming\ImageManager; use OCA\Theming\Service\ThemesService; use OCA\Theming\ThemingDefaults; @@ -180,6 +181,47 @@ class ThemingController extends Controller { ]); } + /** + * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin) + * @param string $setting + * @param mixed $value + * @return DataResponse + * @throws NotPermittedException + */ + public function updateAppMenu($setting, $value) { + $error = null; + switch ($setting) { + case 'defaultApps': + if (is_array($value)) { + try { + $this->appManager->setDefaultApps($value); + } catch (InvalidArgumentException $e) { + $error = $this->l10n->t('Invalid app given'); + } + } else { + $error = $this->l10n->t('Invalid type for setting "defaultApp" given'); + } + break; + default: + $error = $this->l10n->t('Invalid setting key'); + } + if ($error !== null) { + return new DataResponse([ + 'data' => [ + 'message' => $error, + ], + 'status' => 'error' + ], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse([ + 'data' => [ + 'message' => $this->l10n->t('Saved'), + ], + 'status' => 'success' + ]); + } + /** * Check that a string is a valid http/https url */ @@ -299,6 +341,7 @@ class ThemingController extends Controller { */ public function undoAll(): DataResponse { $this->themingDefaults->undoAll(); + $this->appManager->setDefaultApps([]); return new DataResponse( [ diff --git a/apps/theming/lib/Listener/BeforePreferenceListener.php b/apps/theming/lib/Listener/BeforePreferenceListener.php index a1add86e600..3c2cdede9f9 100644 --- a/apps/theming/lib/Listener/BeforePreferenceListener.php +++ b/apps/theming/lib/Listener/BeforePreferenceListener.php @@ -26,23 +26,34 @@ declare(strict_types=1); namespace OCA\Theming\Listener; use OCA\Theming\AppInfo\Application; +use OCP\App\IAppManager; use OCP\Config\BeforePreferenceDeletedEvent; use OCP\Config\BeforePreferenceSetEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; class BeforePreferenceListener implements IEventListener { + public function __construct( + private IAppManager $appManager, + ) { + } + public function handle(Event $event): void { if (!$event instanceof BeforePreferenceSetEvent && !$event instanceof BeforePreferenceDeletedEvent) { + // Invalid event type return; } - if ($event->getAppId() !== Application::APP_ID) { - return; + switch ($event->getAppId()) { + case Application::APP_ID: $this->handleThemingValues($event); break; + case 'core': $this->handleCoreValues($event); break; } + } + private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void { if ($event->getConfigKey() !== 'shortcuts_disabled') { + // Not allowed config key return; } @@ -53,4 +64,27 @@ class BeforePreferenceListener implements IEventListener { $event->setValid(true); } + + private function handleCoreValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void { + if ($event->getConfigKey() !== 'apporder') { + // Not allowed config key + return; + } + + if ($event instanceof BeforePreferenceDeletedEvent) { + $event->setValid(true); + return; + } + + $value = json_decode($event->getConfigValue(), true, flags:JSON_THROW_ON_ERROR); + if (is_array(($value))) { + foreach ($value as $appName => $order) { + if (!$this->appManager->isEnabledForUser($appName) || !is_array($order) || empty($order) || !is_numeric($order[key($order)])) { + // Invalid config value, refuse the change + return; + } + } + } + $event->setValid(true); + } } diff --git a/apps/theming/lib/Settings/Admin.php b/apps/theming/lib/Settings/Admin.php index ee46e62114d..9bd92a47c1f 100644 --- a/apps/theming/lib/Settings/Admin.php +++ b/apps/theming/lib/Settings/Admin.php @@ -40,28 +40,16 @@ use OCP\Settings\IDelegatedSettings; use OCP\Util; class Admin implements IDelegatedSettings { - private string $appName; - private IConfig $config; - private IL10N $l; - private ThemingDefaults $themingDefaults; - private IInitialState $initialState; - private IURLGenerator $urlGenerator; - private ImageManager $imageManager; - public function __construct(string $appName, - IConfig $config, - IL10N $l, - ThemingDefaults $themingDefaults, - IInitialState $initialState, - IURLGenerator $urlGenerator, - ImageManager $imageManager) { - $this->appName = $appName; - $this->config = $config; - $this->l = $l; - $this->themingDefaults = $themingDefaults; - $this->initialState = $initialState; - $this->urlGenerator = $urlGenerator; - $this->imageManager = $imageManager; + public function __construct( + private string $appName, + private IConfig $config, + private IL10N $l, + private ThemingDefaults $themingDefaults, + private IInitialState $initialState, + private IURLGenerator $urlGenerator, + private ImageManager $imageManager, + ) { } /** @@ -80,7 +68,7 @@ class Admin implements IDelegatedSettings { $carry[$key] = $this->imageManager->getSupportedUploadImageFormats($key); return $carry; }, []); - + $this->initialState->provideInitialState('adminThemingParameters', [ 'isThemable' => $themable, 'notThemableErrorMessage' => $errorMessage, @@ -89,6 +77,7 @@ class Admin implements IDelegatedSettings { 'slogan' => $this->themingDefaults->getSlogan(), 'color' => $this->themingDefaults->getDefaultColorPrimary(), 'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''), + 'allowedMimeTypes' => $allowedMimeTypes, 'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''), 'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''), 'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''), @@ -98,7 +87,7 @@ class Admin implements IDelegatedSettings { 'docUrlIcons' => $this->urlGenerator->linkToDocs('admin-theming-icons'), 'canThemeIcons' => $this->imageManager->shouldReplaceIcons(), 'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(), - 'allowedMimeTypes' => $allowedMimeTypes, + 'defaultApps' => array_filter(explode(',', $this->config->getSystemValueString('defaultapp', ''))), ]); Util::addScript($this->appName, 'admin-theming'); diff --git a/apps/theming/lib/Settings/Personal.php b/apps/theming/lib/Settings/Personal.php index 5b0dc742574..4b7a7b0e8a1 100644 --- a/apps/theming/lib/Settings/Personal.php +++ b/apps/theming/lib/Settings/Personal.php @@ -28,6 +28,7 @@ namespace OCA\Theming\Settings; use OCA\Theming\ITheme; use OCA\Theming\Service\ThemesService; use OCA\Theming\ThemingDefaults; +use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\IConfig; @@ -36,22 +37,15 @@ use OCP\Util; class Personal implements ISettings { - protected string $appName; - private IConfig $config; - private ThemesService $themesService; - private IInitialState $initialStateService; - private ThemingDefaults $themingDefaults; - - public function __construct(string $appName, - IConfig $config, - ThemesService $themesService, - IInitialState $initialStateService, - ThemingDefaults $themingDefaults) { - $this->appName = $appName; - $this->config = $config; - $this->themesService = $themesService; - $this->initialStateService = $initialStateService; - $this->themingDefaults = $themingDefaults; + public function __construct( + protected string $appName, + private string $userId, + private IConfig $config, + private ThemesService $themesService, + private IInitialState $initialStateService, + private ThemingDefaults $themingDefaults, + private IAppManager $appManager, + ) { } public function getForm(): TemplateResponse { @@ -74,9 +68,13 @@ class Personal implements ISettings { }); } + // Get the default app enforced by admin + $forcedDefaultApp = $this->appManager->getDefaultAppForUser(null, false); + $this->initialStateService->provideInitialState('themes', array_values($themes)); $this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme); $this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled()); + $this->initialStateService->provideInitialState('enforcedDefaultApp', $forcedDefaultApp); Util::addScript($this->appName, 'personal-theming'); diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue index 1ced195985e..daef18ebdce 100644 --- a/apps/theming/src/AdminTheming.vue +++ b/apps/theming/src/AdminTheming.vue @@ -106,6 +106,7 @@ + @@ -118,6 +119,7 @@ import CheckboxField from './components/admin/CheckboxField.vue' import ColorPickerField from './components/admin/ColorPickerField.vue' import FileInputField from './components/admin/FileInputField.vue' import TextField from './components/admin/TextField.vue' +import AppMenuSection from './components/admin/AppMenuSection.vue' const { backgroundMime, @@ -136,6 +138,7 @@ const { slogan, url, userThemingDisabled, + defaultApps, } = loadState('theming', 'adminThemingParameters') const textFields = [ @@ -247,6 +250,7 @@ export default { name: 'AdminTheming', components: { + AppMenuSection, CheckboxField, ColorPickerField, FileInputField, @@ -259,6 +263,8 @@ export default { 'update:theming', ], + textFields, + data() { return { textFields, @@ -267,6 +273,7 @@ export default { advancedTextFields, advancedFileInputFields, userThemingField, + defaultApps, canThemeIcons, docUrl, diff --git a/apps/theming/src/UserThemes.vue b/apps/theming/src/UserThemes.vue index be76f02563d..10b34efad6c 100644 --- a/apps/theming/src/UserThemes.vue +++ b/apps/theming/src/UserThemes.vue @@ -75,6 +75,8 @@ {{ t('theming', 'Disable all keyboard shortcuts') }} + + @@ -87,6 +89,7 @@ import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection. import BackgroundSettings from './components/BackgroundSettings.vue' import ItemPreview from './components/ItemPreview.vue' +import UserAppMenuSection from './components/UserAppMenuSection.vue' const availableThemes = loadState('theming', 'themes', []) const enforceTheme = loadState('theming', 'enforceTheme', '') @@ -94,8 +97,6 @@ const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false) const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled') -console.debug('Available themes', availableThemes) - export default { name: 'UserThemes', @@ -104,6 +105,7 @@ export default { NcCheckboxRadioSwitch, NcSettingsSection, BackgroundSettings, + UserAppMenuSection, }, data() { diff --git a/apps/theming/src/components/AppOrderSelector.vue b/apps/theming/src/components/AppOrderSelector.vue new file mode 100644 index 00000000000..98f2ce3f3d5 --- /dev/null +++ b/apps/theming/src/components/AppOrderSelector.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/apps/theming/src/components/AppOrderSelectorElement.vue b/apps/theming/src/components/AppOrderSelectorElement.vue new file mode 100644 index 00000000000..ee795b6272a --- /dev/null +++ b/apps/theming/src/components/AppOrderSelectorElement.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/apps/theming/src/components/UserAppMenuSection.vue b/apps/theming/src/components/UserAppMenuSection.vue new file mode 100644 index 00000000000..babdeb184c9 --- /dev/null +++ b/apps/theming/src/components/UserAppMenuSection.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/apps/theming/src/components/admin/AppMenuSection.vue b/apps/theming/src/components/admin/AppMenuSection.vue new file mode 100644 index 00000000000..bed170504c9 --- /dev/null +++ b/apps/theming/src/components/admin/AppMenuSection.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/apps/theming/tests/Settings/PersonalTest.php b/apps/theming/tests/Settings/PersonalTest.php index 4e9be5ef994..872cd7af29d 100644 --- a/apps/theming/tests/Settings/PersonalTest.php +++ b/apps/theming/tests/Settings/PersonalTest.php @@ -54,6 +54,7 @@ class PersonalTest extends TestCase { private ThemesService $themesService; private IInitialState $initialStateService; private ThemingDefaults $themingDefaults; + private IAppManager $appManager; private Personal $admin; /** @var ITheme[] */ @@ -65,6 +66,7 @@ class PersonalTest extends TestCase { $this->themesService = $this->createMock(ThemesService::class); $this->initialStateService = $this->createMock(IInitialState::class); $this->themingDefaults = $this->createMock(ThemingDefaults::class); + $this->appManager = $this->createMock(IAppManager::class); $this->initThemes(); @@ -75,10 +77,12 @@ class PersonalTest extends TestCase { $this->admin = new Personal( Application::APP_ID, + 'admin', $this->config, $this->themesService, $this->initialStateService, $this->themingDefaults, + $this->appManager, ); } @@ -112,12 +116,17 @@ class PersonalTest extends TestCase { ->with('enforce_theme', '') ->willReturn($enforcedTheme); - $this->initialStateService->expects($this->exactly(3)) + $this->appManager->expects($this->once()) + ->method('getDefaultAppForUser') + ->willReturn('forcedapp'); + + $this->initialStateService->expects($this->exactly(4)) ->method('provideInitialState') ->withConsecutive( ['themes', $themesState], ['enforceTheme', $enforcedTheme], - ['isUserThemingDisabled', false] + ['isUserThemingDisabled', false], + ['enforcedDefaultApp', 'forcedapp'], ); $expected = new TemplateResponse('theming', 'settings-personal'); diff --git a/custom.d.ts b/custom.d.ts index 6a7b595c981..aa25f35ecca 100644 --- a/custom.d.ts +++ b/custom.d.ts @@ -20,7 +20,7 @@ * */ declare module '*.svg?raw' { - const content: any + const content: string export default content } diff --git a/cypress/e2e/theming/navigation-bar-settings.cy.ts b/cypress/e2e/theming/navigation-bar-settings.cy.ts new file mode 100644 index 00000000000..50c48d5ac6d --- /dev/null +++ b/cypress/e2e/theming/navigation-bar-settings.cy.ts @@ -0,0 +1,212 @@ +/** + * @copyright Copyright (c) 2023 Ferdinand Thiessen + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * 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 . + * + */ + +import { User } from '@nextcloud/cypress' + +const admin = new User('admin', 'admin') + +describe('Admin theming set default apps', () => { + before(function() { + // Just in case previous test failed + cy.resetAdminTheming() + cy.login(admin) + }) + + it('See the current default app is the dashboard', () => { + cy.visit('/') + cy.url().should('match', /apps\/dashboard/) + cy.get('#nextcloud').click() + cy.url().should('match', /apps\/dashboard/) + }) + + it('See the default app settings', () => { + cy.visit('/settings/admin/theming') + + cy.get('.settings-section').contains('Navigation bar settings').should('exist') + cy.get('[data-cy-switch-default-app]').should('exist') + cy.get('[data-cy-switch-default-app]').scrollIntoView() + }) + + it('Toggle the "use custom default app" switch', () => { + cy.get('[data-cy-switch-default-app] input').should('not.be.checked') + cy.get('[data-cy-switch-default-app] label').click() + cy.get('[data-cy-switch-default-app] input').should('be.checked') + }) + + it('See the default app order selector', () => { + cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard') + else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files') + }) + }) + + it('Change the default app', () => { + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"]').scrollIntoView() + + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible') + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click() + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible') + + }) + + it('See the default app is changed', () => { + cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files') + else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard') + }) + + cy.get('#nextcloud').click() + cy.url().should('match', /apps\/files/) + }) + + it('Toggle the "use custom default app" switch back to reset the default apps', () => { + cy.visit('/settings/admin/theming') + cy.get('[data-cy-switch-default-app]').scrollIntoView() + + cy.get('[data-cy-switch-default-app] input').should('be.checked') + cy.get('[data-cy-switch-default-app] label').click() + cy.get('[data-cy-switch-default-app] input').should('be.not.checked') + }) + + it('See the default app is changed back to default', () => { + cy.get('#nextcloud').click() + cy.url().should('match', /apps\/dashboard/) + }) +}) + +describe('User theming set app order', () => { + before(() => { + cy.resetAdminTheming() + // Create random user for this test + cy.createRandomUser().then((user) => { + cy.login(user) + }) + }) + + after(() => cy.logout()) + + it('See the app order settings', () => { + cy.visit('/settings/user/theming') + + cy.get('.settings-section').contains('Navigation bar settings').should('exist') + cy.get('[data-cy-app-order]').scrollIntoView() + }) + + it('See that the dashboard app is the first one', () => { + cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard') + else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files') + }) + + cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'dashboard') + else cy.wrap($el).should('have.attr', 'data-app-id', 'files') + }) + }) + + it('Change the app order', () => { + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible') + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click() + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible') + + cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files') + else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard') + }) + }) + + it('See the app menu order is changed', () => { + cy.reload() + cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'files') + else cy.wrap($el).should('have.attr', 'data-app-id', 'dashboard') + }) + }) +}) + +describe('User theming set app order with default app', () => { + before(() => { + cy.resetAdminTheming() + // install a third app + cy.runOccCommand('app:install --force --allow-unstable calendar') + // set calendar as default app + cy.runOccCommand('config:system:set --value "calendar,files" defaultapp') + + // Create random user for this test + cy.createRandomUser().then((user) => { + cy.login(user) + }) + }) + + after(() => { + cy.logout() + cy.runOccCommand('app:remove calendar') + }) + + it('See calendar is the default app', () => { + cy.visit('/') + cy.url().should('match', /apps\/calendar/) + + cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'calendar') + }) + }) + + it('See the app order settings: calendar is the first one', () => { + cy.visit('/settings/user/theming') + cy.get('[data-cy-app-order]').scrollIntoView() + cy.get('[data-cy-app-order] [data-cy-app-order-element]').should('have.length', 3).each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'calendar') + else if (idx === 1) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard') + }) + }) + + it('Can not change the default app', () => { + cy.get('[data-cy-app-order] [data-cy-app-order-element="calendar"] [data-cy-app-order-button="up"]').should('not.be.visible') + cy.get('[data-cy-app-order] [data-cy-app-order-element="calendar"] [data-cy-app-order-button="down"]').should('not.be.visible') + + cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').should('not.be.visible') + cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').should('be.visible') + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="down"]').should('not.be.visible') + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible') + }) + + it('Change the other apps order', () => { + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click() + cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible') + + cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'calendar') + else if (idx === 1) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files') + else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard') + }) + }) + + it('See the app menu order is changed', () => { + cy.reload() + cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => { + if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'calendar') + else if (idx === 1) cy.wrap($el).should('have.attr', 'data-app-id', 'files') + else cy.wrap($el).should('have.attr', 'data-app-id', 'dashboard') + }) + }) +}) diff --git a/package-lock.json b/package-lock.json index fa18e25a349..05661332f1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@nextcloud/vue": "^8.0.0-beta.8", "@skjnldsv/sanitize-svg": "^1.0.2", "@vueuse/components": "^10.4.1", + "@vueuse/integrations": "^10.4.1", "autosize": "^6.0.1", "backbone": "^1.4.1", "blueimp-md5": "^2.19.0", @@ -6661,6 +6662,134 @@ } } }, + "node_modules/@vueuse/integrations": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.5.0.tgz", + "integrity": "sha512-fm5sXLCK0Ww3rRnzqnCQRmfjDURaI4xMsx+T+cec0ngQqHx/JgUtm8G0vRjwtonIeTBsH1Q8L3SucE+7K7upJQ==", + "dependencies": { + "@vueuse/core": "10.5.0", + "@vueuse/shared": "10.5.0", + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "*", + "axios": "*", + "change-case": "*", + "drauu": "*", + "focus-trap": "*", + "fuse.js": "*", + "idb-keyval": "*", + "jwt-decode": "*", + "nprogress": "*", + "qrcode": "*", + "sortablejs": "*", + "universal-cookie": "*" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz", + "integrity": "sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==" + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/core": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.5.0.tgz", + "integrity": "sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==", + "dependencies": { + "@types/web-bluetooth": "^0.0.18", + "@vueuse/metadata": "10.5.0", + "@vueuse/shared": "10.5.0", + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.5.0.tgz", + "integrity": "sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.5.0.tgz", + "integrity": "sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==", + "dependencies": { + "vue-demi": ">=0.14.6" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@vueuse/metadata": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.4.1.tgz", diff --git a/package.json b/package.json index 67f70c71c82..62746290564 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@nextcloud/vue": "^8.0.0-beta.8", "@skjnldsv/sanitize-svg": "^1.0.2", "@vueuse/components": "^10.4.1", + "@vueuse/integrations": "^10.4.1", "autosize": "^6.0.1", "backbone": "^1.4.1", "blueimp-md5": "^2.19.0", diff --git a/tests/lib/App/AppManagerTest.php b/tests/lib/App/AppManagerTest.php index 73ac7b79909..104b0941644 100644 --- a/tests/lib/App/AppManagerTest.php +++ b/tests/lib/App/AppManagerTest.php @@ -609,20 +609,47 @@ class AppManagerTest extends TestCase { '', '', '{}', + 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 @@ -630,13 +657,31 @@ class AppManagerTest extends TestCase { 'unexist,settings', '', '{}', + true, '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":[1],"files":[2]}', + true, 'files', ], // user-customized apporder fallback @@ -644,15 +689,24 @@ class AppManagerTest extends TestCase { '', '', '{"settings":[1],"files":[2]}', + true, 'settings', ], + // user-customized apporder, but called without fallback + [ + '', + '', + '{"settings":[1],"files":[2]}', + false, + '', + ], ]; } /** * @dataProvider provideDefaultApps */ - public function testGetDefaultAppForUser($defaultApps, $userDefaultApps, $userApporder, $expectedApp) { + public function testGetDefaultAppForUser($defaultApps, $userDefaultApps, $userApporder, $withFallbacks, $expectedApp) { $user = $this->newUser('user1'); $this->userSession->expects($this->once()) @@ -671,6 +725,6 @@ class AppManagerTest extends TestCase { ['user1', 'core', 'apporder', '[]', $userApporder], ]); - $this->assertEquals($expectedApp, $this->manager->getDefaultAppForUser()); + $this->assertEquals($expectedApp, $this->manager->getDefaultAppForUser(null, $withFallbacks)); } }