mirror of https://github.com/nextcloud/server.git
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
parent
363d9ebb13
commit
e9d4036389
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue