]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(theming): Allow to reset the user defined app order to the default value
authorFerdinand Thiessen <opensource@fthiessen.de>
Fri, 20 Oct 2023 22:51:15 +0000 (00:51 +0200)
committerFerdinand Thiessen <opensource@fthiessen.de>
Sat, 21 Oct 2023 01:52:25 +0000 (03:52 +0200)
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
apps/theming/lib/Settings/Personal.php
apps/theming/src/components/AppOrderSelector.vue
apps/theming/src/components/UserAppMenuSection.vue
apps/theming/tests/Settings/PersonalTest.php
cypress/e2e/theming/navigation-bar-settings.cy.ts

index 4b7a7b0e8a1a7ca1d56bb745a1a0e96168feb0d2..c175416f978dcb7729f033b214313acc56e1cdc0 100644 (file)
@@ -74,7 +74,10 @@ class Personal implements ISettings {
                $this->initialStateService->provideInitialState('themes', array_values($themes));
                $this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
                $this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
-               $this->initialStateService->provideInitialState('enforcedDefaultApp', $forcedDefaultApp);
+               $this->initialStateService->provideInitialState('navigationBar', [
+                       'userAppOrder' => json_decode($this->config->getUserValue($this->userId, 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR),
+                       'enforcedDefaultApp' => $forcedDefaultApp
+               ]);
 
                Util::addScript($this->appName, 'personal-theming');
 
index 98f2ce3f3d51da08100d2270dee93e1e3be8579c..bd4e4e7760de64cb93af675da259f91aab9d0fac 100644 (file)
@@ -18,11 +18,13 @@ import { PropType, computed, defineComponent, ref } from 'vue'
 
 import AppOrderSelectorElement from './AppOrderSelectorElement.vue'
 
-interface IApp {
+export interface IApp {
        id: string // app id
        icon: string // path to the icon svg
-       label?: string // display name
+       label: string // display name
        default?: boolean // force app as default app
+       app: string
+       key: number
 }
 
 export default defineComponent({
index babdeb184c9a722c48f1c96d74093ce4923a867c..a3e023980d05729346366988c18d2dffc48634f0 100644 (file)
@@ -3,13 +3,27 @@
                <p>
                        {{ t('theming', 'You can configure the app order used for the navigation bar. The first entry will be the default app, opened after login or when clicking on the logo.') }}
                </p>
-               <NcNoteCard v-if="!!appOrder[0]?.default" type="info">
+               <NcNoteCard v-if="enforcedDefaultApp" :id="elementIdEnforcedDefaultApp" type="info">
                        {{ t('theming', 'The default app can not be changed because it was configured by the administrator.') }}
                </NcNoteCard>
-               <NcNoteCard v-if="hasAppOrderChanged" type="info">
+               <NcNoteCard v-if="hasAppOrderChanged" :id="elementIdAppOrderChanged" type="info">
                        {{ t('theming', 'The app order was changed, to see it in action you have to reload the page.') }}
                </NcNoteCard>
-               <AppOrderSelector class="user-app-menu-order" :value.sync="appOrder" />
+
+               <AppOrderSelector class="user-app-menu-order"
+                       :aria-details="ariaDetailsAppOrder"
+                       :value="appOrder"
+                       @update:value="updateAppOrder" />
+
+               <NcButton data-test-id="btn-apporder-reset"
+                       :disabled="!hasCustomAppOrder"
+                       type="tertiary"
+                       @click="resetAppOrder">
+                       <template #icon>
+                               <IconUndo :size="20" />
+                       </template>
+                       {{ t('theming', 'Reset default app order') }}
+               </NcButton>
        </NcSettingsSection>
 </template>
 
@@ -21,7 +35,9 @@ import { generateOcsUrl } from '@nextcloud/router'
 import { computed, defineComponent, ref } from 'vue'
 
 import axios from '@nextcloud/axios'
-import AppOrderSelector from './AppOrderSelector.vue'
+import AppOrderSelector, { IApp } from './AppOrderSelector.vue'
+import IconUndo from 'vue-material-design-icons/Undo.vue'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
 import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
 import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
 
@@ -47,53 +63,109 @@ interface INavigationEntry {
        key: number
 }
 
+/** The app order user setting */
+type IAppOrder = Record<string, Record<number, number>>
+
+/** OCS responses */
+interface IOCSResponse<T> {
+       ocs: {
+               meta: unknown
+               data: T
+       }
+}
+
 export default defineComponent({
        name: 'UserAppMenuSection',
        components: {
                AppOrderSelector,
+               IconUndo,
+               NcButton,
                NcNoteCard,
                NcSettingsSection,
        },
        setup() {
+               const {
+                       /** The app order currently defined by the user */
+                       userAppOrder,
+                       /** The enforced default app set by the administrator (if any) */
+                       enforcedDefaultApp,
+               } = loadState<{ userAppOrder: IAppOrder, enforcedDefaultApp: string }>('theming', 'navigationBar')
+
+               /**
+                * Array of all available apps, it is set by a core controller for the app menu, so it is always available
+                */
+                const initialAppOrder = Object.values(loadState<Record<string, INavigationEntry>>('core', 'apps'))
+                       .filter(({ type }) => type === 'link')
+                       .map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp }))
+
+               /**
+                * Check if a custom app order is used or the default is shown
+                */
+               const hasCustomAppOrder = ref(!Array.isArray(userAppOrder) || Object.values(userAppOrder).length > 0)
+
                /**
                 * Track if the app order has changed, so the user can be informed to reload
                 */
-               const hasAppOrderChanged = ref(false)
+               const hasAppOrderChanged = computed(() => initialAppOrder.some(({ id }, index) => id !== appOrder.value[index].id))
+
+               /** ID of the "app order has changed" NcNodeCard, used for the aria-details of the apporder */
+               const elementIdAppOrderChanged = 'theming-apporder-changed-infocard'
 
-               /** The enforced default app set by the administrator (if any) */
-               const enforcedDefaultApp = loadState<string|null>('theming', 'enforcedDefaultApp', null)
+               /** ID of the "you can not change the default app" NcNodeCard, used for the aria-details of the apporder */
+               const elementIdEnforcedDefaultApp = 'theming-apporder-changed-infocard'
 
                /**
-                * Array of all available apps, it is set by a core controller for the app menu, so it is always available
+                * The aria-details value of the app order selector
+                * contains the space separated list of element ids of NcNoteCards
                 */
-               const allApps = ref(
-                       Object.values(loadState<Record<string, INavigationEntry>>('core', 'apps'))
-                               .filter(({ type }) => type === 'link')
-                               .map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp })),
-               )
+               const ariaDetailsAppOrder = computed(() => (hasAppOrderChanged.value ? `${elementIdAppOrderChanged} ` : '') + (enforcedDefaultApp ? elementIdEnforcedDefaultApp : ''))
 
                /**
-                * Wrapper around the sortedApps list with a setter for saving any changes
+                * The current apporder (sorted by user)
                 */
-               const appOrder = computed({
-                       get: () => allApps.value,
-                       set: (value) => {
-                               const order = {} as Record<string, Record<number, number>>
-                               value.forEach(({ app, key }, index) => {
-                                       order[app] = { ...order[app], [key]: index }
+               const appOrder = ref([...initialAppOrder])
+
+               /**
+                * Update the app order, called when the user sorts entries
+                * @param value The new app order value
+                */
+               const updateAppOrder = (value: IApp[]) => {
+                       const order: IAppOrder = {}
+                       value.forEach(({ app, key }, index) => {
+                               order[app] = { ...order[app], [key]: index }
+                       })
+
+                       saveSetting('apporder', order)
+                               .then(() => {
+                                       appOrder.value = value as never
+                                       hasCustomAppOrder.value = true
+                               })
+                               .catch((error) => {
+                                       console.warn('Could not set the app order', error)
+                                       showError(t('theming', 'Could not set the app order'))
                                })
+               }
 
-                               saveSetting('apporder', order)
-                                       .then(() => {
-                                               allApps.value = value
-                                               hasAppOrderChanged.value = true
-                                       })
-                                       .catch((error) => {
-                                               console.warn('Could not set the app order', error)
-                                               showError(t('theming', 'Could not set the app order'))
-                                       })
-                       },
-               })
+               /**
+                * Reset the app order to the default
+                */
+               const resetAppOrder = async () => {
+                       try {
+                               await saveSetting('apporder', [])
+                               hasCustomAppOrder.value = false
+
+                               // Reset our app order list
+                               const { data } = await axios.get<IOCSResponse<INavigationEntry[]>>(generateOcsUrl('/core/navigation/apps'), {
+                                       headers: {
+                                               'OCS-APIRequest': 'true',
+                                       },
+                               })
+                               appOrder.value = data.ocs.data.map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp }))
+                       } catch (error) {
+                               console.warn(error)
+                               showError(t('theming', 'Could not reset the app order'))
+                       }
+               }
 
                const saveSetting = async (key: string, value: unknown) => {
                        const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
@@ -107,7 +179,16 @@ export default defineComponent({
 
                return {
                        appOrder,
+                       updateAppOrder,
+                       resetAppOrder,
+
+                       enforcedDefaultApp,
                        hasAppOrderChanged,
+                       hasCustomAppOrder,
+
+                       ariaDetailsAppOrder,
+                       elementIdAppOrderChanged,
+                       elementIdEnforcedDefaultApp,
 
                        t,
                }
index 872cd7af29dc4c6c9f706b109b4bc572d6948c4a..15876930179954bf127cb687027cf21749dcb57c 100644 (file)
@@ -116,6 +116,11 @@ class PersonalTest extends TestCase {
                        ->with('enforce_theme', '')
                        ->willReturn($enforcedTheme);
 
+               $this->config->expects($this->once())
+                       ->method('getUserValue')
+                       ->with('admin', 'core', 'apporder')
+                       ->willReturn('[]');
+
                $this->appManager->expects($this->once())
                        ->method('getDefaultAppForUser')
                        ->willReturn('forcedapp');
@@ -126,7 +131,7 @@ class PersonalTest extends TestCase {
                                ['themes', $themesState],
                                ['enforceTheme', $enforcedTheme],
                                ['isUserThemingDisabled', false],
-                               ['enforcedDefaultApp', 'forcedapp'],
+                               ['navigationBar', ['userAppOrder' => [], 'enforcedDefaultApp' => 'forcedapp']],
                        );
 
                $expected = new TemplateResponse('theming', 'settings-personal');
index a5657ee5a15728ba3bb29e064e51d36dfecc6d57..766590702298af5d0db4b3e04ae6865e74c87bb1 100644 (file)
@@ -94,15 +94,18 @@ describe('Admin theming set default apps', () => {
 })
 
 describe('User theming set app order', () => {
+       let user: User
+
        before(() => {
                cy.resetAdminTheming()
                // Create random user for this test
-               cy.createRandomUser().then((user) => {
-                       cy.login(user)
+               cy.createRandomUser().then(($user) => {
+                       user = $user
+                       cy.login($user)
                })
        })
 
-       after(() => cy.logout())
+       after(() => cy.deleteUser(user))
 
        it('See the app order settings', () => {
                cy.visit('/settings/user/theming')
@@ -144,6 +147,8 @@ describe('User theming set app order', () => {
 })
 
 describe('User theming set app order with default app', () => {
+       let user: User
+
        before(() => {
                cy.resetAdminTheming()
                // install a third app
@@ -152,13 +157,14 @@ describe('User theming set app order with default app', () => {
                cy.runOccCommand('config:system:set --value "calendar,files" defaultapp')
 
                // Create random user for this test
-               cy.createRandomUser().then((user) => {
-                       cy.login(user)
+               cy.createRandomUser().then(($user) => {
+                       user = $user
+                       cy.login($user)
                })
        })
 
        after(() => {
-               cy.logout()
+               cy.deleteUser(user)
                cy.runOccCommand('app:remove calendar')
        })
 
@@ -186,11 +192,12 @@ describe('User theming set app order with default app', () => {
 
                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')
+               cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="down"]').should('not.be.visible')
        })
 
-       it('Change the other apps order', () => {
+       it('Change the order of the other apps', () => {
                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')
 
@@ -251,3 +258,73 @@ describe('User theming app order list accessibility', () => {
                cy.get('[data-cy-app-order] [data-cy-app-order-element]:last-of-type [data-cy-app-order-button="up"]').should('not.have.focus')
        })
 })
+
+describe('User theming reset app order', () => {
+       let user: User
+
+       before(() => {
+               cy.resetAdminTheming()
+               // Create random user for this test
+               cy.createRandomUser().then(($user) => {
+                       user = $user
+                       cy.login($user)
+               })
+       })
+
+       after(() => cy.deleteUser(user))
+
+       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('See the reset button is disabled', () => {
+               cy.get('[data-test-id="btn-apporder-reset"]').scrollIntoView()
+               cy.get('[data-test-id="btn-apporder-reset"]').should('be.visible').and('have.attr', 'disabled')
+       })
+
+       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 reset button is no longer disabled', () => {
+               cy.get('[data-test-id="btn-apporder-reset"]').scrollIntoView()
+               cy.get('[data-test-id="btn-apporder-reset"]').should('be.visible').and('not.have.attr', 'disabled')
+       })
+
+       it('Reset the app order', () => {
+               cy.get('[data-test-id="btn-apporder-reset"]').click({ force: true })
+       })
+
+       it('See the app order is restored', () => {
+               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('See the reset button is disabled again', () => {
+               cy.get('[data-test-id="btn-apporder-reset"]').should('be.visible').and('have.attr', 'disabled')
+       })
+})