Browse Source
feat(theming): Allow to configure default apps and app order in frontend settings
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 <opensource@fthiessen.de>pull/40844/head
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
17 changed files with 1048 additions and 48 deletions
-
5apps/theming/appinfo/routes.php
-
43apps/theming/lib/Controller/ThemingController.php
-
38apps/theming/lib/Listener/BeforePreferenceListener.php
-
35apps/theming/lib/Settings/Admin.php
-
30apps/theming/lib/Settings/Personal.php
-
7apps/theming/src/AdminTheming.vue
-
6apps/theming/src/UserThemes.vue
-
130apps/theming/src/components/AppOrderSelector.vue
-
145apps/theming/src/components/AppOrderSelectorElement.vue
-
122apps/theming/src/components/UserAppMenuSection.vue
-
120apps/theming/src/components/admin/AppMenuSection.vue
-
13apps/theming/tests/Settings/PersonalTest.php
-
2custom.d.ts
-
212cypress/e2e/theming/navigation-bar-settings.cy.ts
-
129package-lock.json
-
1package.json
-
58tests/lib/App/AppManagerTest.php
@ -0,0 +1,130 @@ |
|||
<template> |
|||
<ol ref="listElement" data-cy-app-order class="order-selector"> |
|||
<AppOrderSelectorElement v-for="app,index in appList" |
|||
:key="`${app.id}${renderCount}`" |
|||
:app="app" |
|||
:is-first="index === 0 || !!appList[index - 1].default" |
|||
:is-last="index === value.length - 1" |
|||
v-on="app.default ? {} : { |
|||
'move:up': () => moveUp(index), |
|||
'move:down': () => moveDown(index), |
|||
}" /> |
|||
</ol> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { useSortable } from '@vueuse/integrations/useSortable' |
|||
import { PropType, computed, defineComponent, ref } from 'vue' |
|||
|
|||
import AppOrderSelectorElement from './AppOrderSelectorElement.vue' |
|||
|
|||
interface IApp { |
|||
id: string // app id |
|||
icon: string // path to the icon svg |
|||
label?: string // display name |
|||
default?: boolean // force app as default app |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'AppOrderSelector', |
|||
components: { |
|||
AppOrderSelectorElement, |
|||
}, |
|||
props: { |
|||
/** |
|||
* List of apps to reorder |
|||
*/ |
|||
value: { |
|||
type: Array as PropType<IApp[]>, |
|||
required: true, |
|||
}, |
|||
}, |
|||
emits: { |
|||
/** |
|||
* Update the apps list on reorder |
|||
* @param value The new value of the app list |
|||
*/ |
|||
'update:value': (value: IApp[]) => Array.isArray(value), |
|||
}, |
|||
setup(props, { emit }) { |
|||
/** |
|||
* The Element that contains the app list |
|||
*/ |
|||
const listElement = ref<HTMLElement | null>(null) |
|||
|
|||
/** |
|||
* The app list with setter that will ement the `update:value` event |
|||
*/ |
|||
const appList = computed({ |
|||
get: () => props.value, |
|||
// Ensure the sortable.js does not mess with the default attribute |
|||
set: (list) => { |
|||
const newValue = [...list].sort((a, b) => ((b.default ? 1 : 0) - (a.default ? 1 : 0)) || list.indexOf(a) - list.indexOf(b)) |
|||
if (newValue.some(({ id }, index) => id !== props.value[index].id)) { |
|||
emit('update:value', newValue) |
|||
} else { |
|||
// forceUpdate as the DOM has changed because of a drag event, but the reactive state has not -> wrong state |
|||
renderCount.value += 1 |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
/** |
|||
* Helper to force rerender the list in case of a invalid drag event |
|||
*/ |
|||
const renderCount = ref(0) |
|||
|
|||
/** |
|||
* Handle drag & drop sorting |
|||
*/ |
|||
useSortable(listElement, appList, { filter: '.order-selector-element--disabled' }) |
|||
|
|||
/** |
|||
* Handle element is moved up |
|||
* @param index The index of the element that is moved |
|||
*/ |
|||
const moveUp = (index: number) => { |
|||
const before = index > 1 ? props.value.slice(0, index - 1) : [] |
|||
// skip if not possible, because of default default app |
|||
if (props.value[index - 1]?.default) { |
|||
return |
|||
} |
|||
|
|||
const after = [props.value[index - 1]] |
|||
if (index < props.value.length - 1) { |
|||
after.push(...props.value.slice(index + 1)) |
|||
} |
|||
emit('update:value', [...before, props.value[index], ...after]) |
|||
} |
|||
|
|||
/** |
|||
* Handle element is moved down |
|||
* @param index The index of the element that is moved |
|||
*/ |
|||
const moveDown = (index: number) => { |
|||
const before = index > 0 ? props.value.slice(0, index) : [] |
|||
before.push(props.value[index + 1]) |
|||
|
|||
const after = index < (props.value.length - 2) ? props.value.slice(index + 2) : [] |
|||
emit('update:value', [...before, props.value[index], ...after]) |
|||
} |
|||
|
|||
return { |
|||
appList, |
|||
listElement, |
|||
|
|||
moveDown, |
|||
moveUp, |
|||
|
|||
renderCount, |
|||
} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.order-selector { |
|||
width: max-content; |
|||
min-width: 260px; // align with NcSelect |
|||
} |
|||
</style> |
|||
@ -0,0 +1,145 @@ |
|||
<template> |
|||
<li :data-cy-app-order-element="app.id" |
|||
:class="{ |
|||
'order-selector-element': true, |
|||
'order-selector-element--disabled': app.default |
|||
}"> |
|||
<svg width="20" |
|||
height="20" |
|||
viewBox="0 0 20 20" |
|||
role="presentation"> |
|||
<image preserveAspectRatio="xMinYMin meet" |
|||
x="0" |
|||
y="0" |
|||
width="20" |
|||
height="20" |
|||
:xlink:href="app.icon" |
|||
class="order-selector-element__icon" /> |
|||
</svg> |
|||
|
|||
<div class="order-selector-element__label"> |
|||
{{ app.label ?? app.id }} |
|||
</div> |
|||
|
|||
<div class="order-selector-element__actions"> |
|||
<NcButton v-show="!isFirst && !app.default" |
|||
:aria-label="t('settings', 'Move up')" |
|||
data-cy-app-order-button="up" |
|||
type="tertiary-no-background" |
|||
@click="$emit('move:up')"> |
|||
<template #icon> |
|||
<IconArrowUp :size="20" /> |
|||
</template> |
|||
</NcButton> |
|||
<div v-show="isFirst || !!app.default" aria-hidden="true" class="order-selector-element__placeholder" /> |
|||
<NcButton v-show="!isLast && !app.default" |
|||
:aria-label="t('settings', 'Move down')" |
|||
data-cy-app-order-button="down" |
|||
type="tertiary-no-background" |
|||
@click="$emit('move:down')"> |
|||
<template #icon> |
|||
<IconArrowDown :size="20" /> |
|||
</template> |
|||
</NcButton> |
|||
<div v-show="isLast || !!app.default" aria-hidden="true" class="order-selector-element__placeholder" /> |
|||
</div> |
|||
</li> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { translate as t } from '@nextcloud/l10n' |
|||
import { PropType, defineComponent } from 'vue' |
|||
|
|||
import IconArrowDown from 'vue-material-design-icons/ArrowDown.vue' |
|||
import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue' |
|||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' |
|||
|
|||
interface IApp { |
|||
id: string // app id |
|||
icon: string // path to the icon svg |
|||
label?: string // display name |
|||
default?: boolean // for app as default app |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'AppOrderSelectorElement', |
|||
components: { |
|||
IconArrowDown, |
|||
IconArrowUp, |
|||
NcButton, |
|||
}, |
|||
props: { |
|||
app: { |
|||
type: Object as PropType<IApp>, |
|||
required: true, |
|||
}, |
|||
isFirst: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
isLast: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}, |
|||
emits: { |
|||
'move:up': () => true, |
|||
'move:down': () => true, |
|||
}, |
|||
setup() { |
|||
return { |
|||
t, |
|||
} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.order-selector-element { |
|||
// hide default styling |
|||
list-style: none; |
|||
// Align children |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
// Spacing |
|||
gap: 12px; |
|||
padding-inline: 12px; |
|||
|
|||
&:hover { |
|||
background-color: var(--color-background-hover); |
|||
border-radius: var(--border-radius-large); |
|||
} |
|||
|
|||
&--disabled { |
|||
border-color: var(--color-text-maxcontrast); |
|||
color: var(--color-text-maxcontrast); |
|||
|
|||
.order-selector-element__icon { |
|||
opacity: 75%; |
|||
} |
|||
} |
|||
|
|||
&__actions { |
|||
flex: 0 0; |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: 6px; |
|||
} |
|||
|
|||
&__label { |
|||
flex: 1 1; |
|||
text-overflow: ellipsis; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
&__placeholder { |
|||
height: 44px; |
|||
width: 44px; |
|||
} |
|||
|
|||
&__icon { |
|||
filter: var(--background-invert-if-bright); |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,122 @@ |
|||
<template> |
|||
<NcSettingsSection :name="t('theming', 'Navigation bar settings')"> |
|||
<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"> |
|||
{{ t('theming', 'The default app can not be changed because it was configured by the administrator.') }} |
|||
</NcNoteCard> |
|||
<NcNoteCard v-if="hasAppOrderChanged" 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" /> |
|||
</NcSettingsSection> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { showError } from '@nextcloud/dialogs' |
|||
import { loadState } from '@nextcloud/initial-state' |
|||
import { translate as t } from '@nextcloud/l10n' |
|||
import { generateOcsUrl } from '@nextcloud/router' |
|||
import { computed, defineComponent, ref } from 'vue' |
|||
|
|||
import axios from '@nextcloud/axios' |
|||
import AppOrderSelector from './AppOrderSelector.vue' |
|||
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js' |
|||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' |
|||
|
|||
/** See NavigationManager */ |
|||
interface INavigationEntry { |
|||
/** Navigation id */ |
|||
id: string |
|||
/** Order where this entry should be shown */ |
|||
order: number |
|||
/** Target of the navigation entry */ |
|||
href: string |
|||
/** The icon used for the naviation entry */ |
|||
icon: string |
|||
/** Type of the navigation entry ('link' vs 'settings') */ |
|||
type: 'link' | 'settings' |
|||
/** Localized name of the navigation entry */ |
|||
name: string |
|||
/** Whether this is the default app */ |
|||
default?: boolean |
|||
/** App that registered this navigation entry (not necessarly the same as the id) */ |
|||
app: string |
|||
/** The key used to identify this entry in the navigations entries */ |
|||
key: number |
|||
} |
|||
|
|||
export default defineComponent({ |
|||
name: 'UserAppMenuSection', |
|||
components: { |
|||
AppOrderSelector, |
|||
NcNoteCard, |
|||
NcSettingsSection, |
|||
}, |
|||
setup() { |
|||
/** |
|||
* Track if the app order has changed, so the user can be informed to reload |
|||
*/ |
|||
const hasAppOrderChanged = ref(false) |
|||
|
|||
/** The enforced default app set by the administrator (if any) */ |
|||
const enforcedDefaultApp = loadState<string|null>('theming', 'enforcedDefaultApp', null) |
|||
|
|||
/** |
|||
* Array of all available apps, it is set by a core controller for the app menu, so it is always available |
|||
*/ |
|||
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 })), |
|||
) |
|||
|
|||
/** |
|||
* Wrapper around the sortedApps list with a setter for saving any changes |
|||
*/ |
|||
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 } |
|||
}) |
|||
|
|||
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')) |
|||
}) |
|||
}, |
|||
}) |
|||
|
|||
const saveSetting = async (key: string, value: unknown) => { |
|||
const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', { |
|||
appId: 'core', |
|||
configKey: key, |
|||
}) |
|||
return await axios.post(url, { |
|||
configValue: JSON.stringify(value), |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
appOrder, |
|||
hasAppOrderChanged, |
|||
|
|||
t, |
|||
} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.user-app-menu-order { |
|||
margin-block: 12px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,120 @@ |
|||
<template> |
|||
<NcSettingsSection :name="t('theming', 'Navigation bar settings')"> |
|||
<h3>{{ t('theming', 'Default app') }}</h3> |
|||
<p class="info-note"> |
|||
{{ t('theming', 'The default app is the app that is e.g. opened after login or when the logo in the menu is clicked.') }} |
|||
</p> |
|||
|
|||
<NcCheckboxRadioSwitch :checked.sync="hasCustomDefaultApp" type="switch" data-cy-switch-default-app=""> |
|||
{{ t('theming', 'Use custom default app') }} |
|||
</NcCheckboxRadioSwitch> |
|||
|
|||
<template v-if="hasCustomDefaultApp"> |
|||
<h4>{{ t('theming', 'Global default app') }}</h4> |
|||
<NcSelect v-model="selectedApps" |
|||
:close-on-select="false" |
|||
:placeholder="t('theming', 'Global default apps')" |
|||
:options="allApps" |
|||
:multiple="true" /> |
|||
<h5>{{ t('theming', 'Default app priority') }}</h5> |
|||
<p class="info-note"> |
|||
{{ t('theming', 'If an app is not enabled for a user, the next app with lower priority is used.') }} |
|||
</p> |
|||
<AppOrderSelector :value.sync="selectedApps" /> |
|||
</template> |
|||
</NcSettingsSection> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { showError } from '@nextcloud/dialogs' |
|||
import { loadState } from '@nextcloud/initial-state' |
|||
import { translate as t } from '@nextcloud/l10n' |
|||
import { generateUrl } from '@nextcloud/router' |
|||
import { computed, defineComponent } from 'vue' |
|||
|
|||
import axios from '@nextcloud/axios' |
|||
|
|||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' |
|||
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' |
|||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' |
|||
import AppOrderSelector from '../AppOrderSelector.vue' |
|||
|
|||
export default defineComponent({ |
|||
name: 'AppMenuSection', |
|||
components: { |
|||
AppOrderSelector, |
|||
NcCheckboxRadioSwitch, |
|||
NcSelect, |
|||
NcSettingsSection, |
|||
}, |
|||
props: { |
|||
defaultApps: { |
|||
type: Array, |
|||
required: true, |
|||
}, |
|||
}, |
|||
emits: { |
|||
'update:defaultApps': (value: string[]) => Array.isArray(value) && value.every((id) => typeof id === 'string'), |
|||
}, |
|||
setup(props, { emit }) { |
|||
const hasCustomDefaultApp = computed({ |
|||
get: () => props.defaultApps.length > 0, |
|||
set: (checked: boolean) => { |
|||
if (checked) { |
|||
emit('update:defaultApps', ['dashboard', 'files']) |
|||
} else { |
|||
selectedApps.value = [] |
|||
} |
|||
}, |
|||
}) |
|||
|
|||
/** |
|||
* All enabled apps which can be navigated |
|||
*/ |
|||
const allApps = Object.values( |
|||
loadState<Record<string, { id: string, name?: string, icon: string }>>('core', 'apps'), |
|||
).map(({ id, name, icon }) => ({ label: name, id, icon })) |
|||
|
|||
/** |
|||
* Currently selected app, wrapps the setter |
|||
*/ |
|||
const selectedApps = computed({ |
|||
get: () => props.defaultApps.map((id) => allApps.filter(app => app.id === id)[0]), |
|||
set(value) { |
|||
saveSetting('defaultApps', value.map(app => app.id)) |
|||
.then(() => emit('update:defaultApps', value.map(app => app.id))) |
|||
.catch(() => showError(t('theming', 'Could not set global default apps'))) |
|||
}, |
|||
}) |
|||
|
|||
const saveSetting = async (key: string, value: unknown) => { |
|||
const url = generateUrl('/apps/theming/ajax/updateAppMenu') |
|||
return await axios.put(url, { |
|||
setting: key, |
|||
value, |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
allApps, |
|||
selectedApps, |
|||
hasCustomDefaultApp, |
|||
|
|||
t, |
|||
} |
|||
}, |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
h3, h4 { |
|||
font-weight: bold; |
|||
} |
|||
h4, h5 { |
|||
margin-block-start: 12px; |
|||
} |
|||
|
|||
.info-note { |
|||
color: var(--color-text-maxcontrast); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,212 @@ |
|||
/** |
|||
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de> |
|||
* |
|||
* @author Ferdinand Thiessen <opensource@fthiessen.de> |
|||
* |
|||
* @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 <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
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') |
|||
}) |
|||
}) |
|||
}) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue