mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 00:32:29 -04:00
Merge pull request #46370 from nextcloud/refactor/app-menu
refactor: split app menu into smaller components
This commit is contained in:
commit
038836c41c
39 changed files with 598 additions and 430 deletions
|
|
@ -18,12 +18,10 @@ import { loadState } from '@nextcloud/initial-state'
|
|||
import { generateUrl } from '@nextcloud/router'
|
||||
import { defineComponent } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import type { INavigationEntry } from '../../../../../core/src/types/navigation'
|
||||
|
||||
const knownRoutes = Object.fromEntries(
|
||||
Object.entries(
|
||||
loadState<Record<string, { app?: string, href: string }>>('core', 'apps'),
|
||||
).map(([k, v]) => [v.app ?? k, v.href]),
|
||||
)
|
||||
const apps = loadState<INavigationEntry[]>('core', 'apps')
|
||||
const knownRoutes = Object.fromEntries(apps.map((app) => [app.app ?? app.id, app.href]))
|
||||
|
||||
/**
|
||||
* This component either shows a native link to the installed app or external size - or a router link to the appstore page of the app if not installed
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { IApp } from './AppOrderSelector.vue'
|
||||
import type { INavigationEntry } from '../../../../core/src/types/navigation.d.ts'
|
||||
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
|
@ -47,26 +48,6 @@ 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'
|
||||
|
||||
/** 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 app order user setting */
|
||||
type IAppOrder = Record<string, { order: number, app?: string }>
|
||||
|
||||
|
|
@ -98,7 +79,7 @@ export default defineComponent({
|
|||
/**
|
||||
* 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'))
|
||||
const initialAppOrder = loadState<INavigationEntry[]>('core', 'apps')
|
||||
.filter(({ type }) => type === 'link')
|
||||
.map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp }))
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { INavigationEntry } from '../../../../../core/src/types/navigation'
|
||||
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
|
@ -75,9 +77,8 @@ export default defineComponent({
|
|||
/**
|
||||
* 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 }))
|
||||
const allApps = loadState<INavigationEntry[]>('core', 'apps')
|
||||
.map(({ id, name, icon }) => ({ label: name, id, icon }))
|
||||
|
||||
/**
|
||||
* Currently selected app, wrapps the setter
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { INavigationEntry } from '../../../core/src/types/navigation'
|
||||
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import Vue, { defineAsyncComponent } from 'vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
const navigationEntries = loadState('core', 'apps', {})
|
||||
const navigationEntries = loadState<INavigationEntry[]>('core', 'apps', [])
|
||||
|
||||
const DialogVue = defineAsyncComponent(() => import('./components/AppChangelogDialog.vue'))
|
||||
|
||||
|
|
@ -39,8 +41,9 @@ function showDialog(appId: string, version?: string) {
|
|||
dialog.$destroy?.()
|
||||
resolve(dismissed)
|
||||
|
||||
if (dismissed && appId in navigationEntries) {
|
||||
window.location = navigationEntries[appId].href
|
||||
const app = navigationEntries.find(({ app }) => app === appId)
|
||||
if (dismissed && app !== undefined) {
|
||||
window.location.href = app.href
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,99 +6,99 @@
|
|||
<template>
|
||||
<nav class="app-menu"
|
||||
:aria-label="t('core', 'Applications menu')">
|
||||
<ul class="app-menu-main">
|
||||
<li v-for="app in mainAppList"
|
||||
<ul class="app-menu__list">
|
||||
<AppMenuEntry v-for="app in mainAppList"
|
||||
:key="app.id"
|
||||
:data-app-id="app.id"
|
||||
class="app-menu-entry"
|
||||
:class="{ 'app-menu-entry__active': app.active }">
|
||||
<a :href="app.href"
|
||||
:class="{ 'has-unread': app.unread > 0 }"
|
||||
:aria-label="appLabel(app)"
|
||||
:title="app.name"
|
||||
:aria-current="app.active ? 'page' : false"
|
||||
:target="app.target ? '_blank' : undefined"
|
||||
:rel="app.target ? 'noopener noreferrer' : undefined">
|
||||
<img :src="app.icon" alt="">
|
||||
<div class="app-menu-entry--label">
|
||||
{{ app.name }}
|
||||
<span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
:app="app" />
|
||||
</ul>
|
||||
<NcActions class="app-menu-more" :aria-label="t('core', 'More apps')">
|
||||
<NcActions class="app-menu__overflow" :aria-label="t('core', 'More apps')">
|
||||
<NcActionLink v-for="app in popoverAppList"
|
||||
:key="app.id"
|
||||
:aria-label="appLabel(app)"
|
||||
:aria-current="app.active ? 'page' : false"
|
||||
:href="app.href"
|
||||
class="app-menu-popover-entry">
|
||||
<template #icon>
|
||||
<div class="app-icon" :class="{ 'has-unread': app.unread > 0 }">
|
||||
<img :src="app.icon" alt="">
|
||||
</div>
|
||||
</template>
|
||||
{{ app.name }}
|
||||
<span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
|
||||
</NcActionLink>
|
||||
:icon="app.icon"
|
||||
:name="app.name"
|
||||
class="app-menu__overflow-entry" />
|
||||
</NcActions>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
<script lang="ts">
|
||||
import type { INavigationEntry } from '../types/navigation'
|
||||
|
||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import AppMenuEntry from './AppMenuEntry.vue'
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
||||
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
|
||||
import logger from '../logger'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'AppMenu',
|
||||
|
||||
components: {
|
||||
NcActions, NcActionLink,
|
||||
AppMenuEntry,
|
||||
NcActions,
|
||||
NcActionLink,
|
||||
},
|
||||
data() {
|
||||
|
||||
setup() {
|
||||
return {
|
||||
apps: loadState('core', 'apps', {}),
|
||||
appLimit: 0,
|
||||
observer: null,
|
||||
t,
|
||||
n,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const appList = loadState<INavigationEntry[]>('core', 'apps', [])
|
||||
|
||||
return {
|
||||
appList,
|
||||
appLimit: 0,
|
||||
observer: null as ResizeObserver | null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
appList() {
|
||||
return Object.values(this.apps)
|
||||
},
|
||||
mainAppList() {
|
||||
return this.appList.slice(0, this.appLimit)
|
||||
},
|
||||
popoverAppList() {
|
||||
return this.appList.slice(this.appLimit)
|
||||
},
|
||||
appLabel() {
|
||||
return (app) => app.name
|
||||
+ (app.active ? ' (' + t('core', 'Currently open') + ')' : '')
|
||||
+ (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '')
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.observer = new ResizeObserver(this.resize)
|
||||
this.observer.observe(this.$el)
|
||||
this.resize()
|
||||
subscribe('nextcloud:app-menu.refresh', this.setApps)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.observer.disconnect()
|
||||
this.observer!.disconnect()
|
||||
unsubscribe('nextcloud:app-menu.refresh', this.setApps)
|
||||
},
|
||||
|
||||
methods: {
|
||||
setNavigationCounter(id, counter) {
|
||||
this.$set(this.apps[id], 'unread', counter)
|
||||
setNavigationCounter(id: string, counter: number) {
|
||||
const app = this.appList.find(({ app }) => app === id)
|
||||
if (app) {
|
||||
this.$set(app, 'unread', counter)
|
||||
} else {
|
||||
logger.warn(`Could not find app "${id}" for setting navigation count`)
|
||||
}
|
||||
},
|
||||
setApps({ apps }) {
|
||||
this.apps = apps
|
||||
|
||||
setApps({ apps }: { apps: INavigationEntry[]}) {
|
||||
this.appList = apps
|
||||
},
|
||||
|
||||
resize() {
|
||||
const availableWidth = this.$el.offsetWidth
|
||||
const availableWidth = (this.$el as HTMLElement).offsetWidth
|
||||
let appCount = Math.floor(availableWidth / 50) - 1
|
||||
const popoverAppCount = this.appList.length - appCount
|
||||
if (popoverAppCount === 1) {
|
||||
|
|
@ -110,183 +110,48 @@ export default {
|
|||
this.appLimit = appCount
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$header-icon-size: 20px;
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-menu {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-shrink: 1;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.app-menu-main {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.app-menu-entry {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
&.app-menu-entry__active {
|
||||
opacity: 1;
|
||||
// Adjust the overflow NcActions styles as they are directly rendered on the background
|
||||
&__overflow :deep(.button-vue--vue-tertiary) {
|
||||
opacity: .7;
|
||||
margin: 3px;
|
||||
filter: var(--background-image-invert-if-bright);
|
||||
|
||||
&::before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-bottom-color: var(--color-main-background);
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-background-plain-text);
|
||||
left: 50%;
|
||||
bottom: 6px;
|
||||
display: block;
|
||||
transition: all 0.1s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.app-menu-entry--label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
margin: 2px;
|
||||
// this is shown directly on the background
|
||||
/* Remove all background and align text color if not expanded */
|
||||
&:not([aria-expanded="true"]) {
|
||||
color: var(--color-background-plain-text);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
width: $header-icon-size;
|
||||
height: $header-icon-size;
|
||||
padding: calc((100% - $header-icon-size) / 2);
|
||||
box-sizing: content-box;
|
||||
filter: var(--background-image-invert-if-bright);
|
||||
}
|
||||
|
||||
.app-menu-entry--label {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
// this is shown directly on the background
|
||||
color: var(--color-background-plain-text);
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
top: 45%;
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
transform: translateX(-50%);
|
||||
transition: all 0.1s ease-in-out;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
opacity: 1;
|
||||
.app-menu-entry--label {
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
font-weight: bolder;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Show labels
|
||||
&:hover,
|
||||
&:focus-within,
|
||||
.app-menu-entry:hover,
|
||||
.app-menu-entry:focus {
|
||||
opacity: 1;
|
||||
|
||||
img {
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.app-menu-entry--label {
|
||||
&:focus-visible {
|
||||
opacity: 1;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&::before, .app-menu-entry::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .app-menu-more .button-vue--vue-tertiary {
|
||||
opacity: .7;
|
||||
margin: 3px;
|
||||
filter: var(--background-image-invert-if-bright);
|
||||
|
||||
/* Remove all background and align text color if not expanded */
|
||||
&:not([aria-expanded="true"]) {
|
||||
color: var(--color-background-plain-text);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: transparent !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
opacity: 1;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.app-menu-popover-entry {
|
||||
.app-icon {
|
||||
position: relative;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Icons are bright so invert them if bright color theme == bright background is used */
|
||||
filter: var(--background-invert-if-bright);
|
||||
|
||||
&.has-unread::after {
|
||||
background-color: var(--color-main-text);
|
||||
}
|
||||
|
||||
img {
|
||||
width: $header-icon-size;
|
||||
height: $header-icon-size;
|
||||
&__overflow-entry {
|
||||
:deep(.action-link__icon) {
|
||||
// Icons are bright so invert them if bright color theme == bright background is used
|
||||
filter: var(--background-invert-if-bright) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.has-unread::after {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--color-background-plain-text);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.unread-counter {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
128
core/src/components/AppMenuEntry.vue
Normal file
128
core/src/components/AppMenuEntry.vue
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<li class="app-menu-entry"
|
||||
:class="{
|
||||
'app-menu-entry--active': app.active,
|
||||
}">
|
||||
<a class="app-menu-entry__link"
|
||||
:href="app.href"
|
||||
:title="app.name"
|
||||
:aria-current="app.active ? 'page' : false"
|
||||
:target="app.target ? '_blank' : undefined"
|
||||
:rel="app.target ? 'noopener noreferrer' : undefined">
|
||||
<AppMenuIcon class="app-menu-entry__icon" :app="app" />
|
||||
<span class="app-menu-entry__label">
|
||||
{{ app.name }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { INavigationEntry } from '../types/navigation'
|
||||
import AppMenuIcon from './AppMenuIcon.vue'
|
||||
|
||||
defineProps<{
|
||||
app: INavigationEntry
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-menu-entry {
|
||||
width: var(--header-height);
|
||||
height: var(--header-height);
|
||||
position: relative;
|
||||
|
||||
&__link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// Set color as this is shown directly on the background
|
||||
color: var(--color-background-plain-text);
|
||||
// Make space for focus-visible outline
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
// this is shown directly on the background
|
||||
color: var(--color-background-plain-text);
|
||||
text-align: center;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
transform: translateX(-50%);
|
||||
transition: all 0.1s ease-in-out;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
&--active {
|
||||
// When hover or focus, show the label and make it bolder than the other entries
|
||||
.app-menu-entry__label {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
// When active show a line below the entry as an "active" indicator
|
||||
&::before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-bottom-color: var(--color-main-background);
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-background-plain-text);
|
||||
left: 50%;
|
||||
bottom: 6px;
|
||||
display: block;
|
||||
transition: all 0.1s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the hovered entry bold to see that it is hovered
|
||||
&:hover &__label,
|
||||
&:focus-within &__label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// Showing the label
|
||||
.app-menu-entry:hover .app-menu-entry,
|
||||
.app-menu-entry:focus-within .app-menu-entry,
|
||||
.app-menu__list:hover .app-menu-entry,
|
||||
.app-menu__list:focus-within .app-menu-entry {
|
||||
// Move icon up so that the name does not overflow the icon
|
||||
&__icon {
|
||||
margin-block-end: calc(1.5 * 12px); // font size of label * line height
|
||||
}
|
||||
|
||||
// Make the label visible
|
||||
&__label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Hide indicator when the text is shown
|
||||
&--active::before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
63
core/src/components/AppMenuIcon.vue
Normal file
63
core/src/components/AppMenuIcon.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<span class="app-menu-icon"
|
||||
role="img"
|
||||
:aria-hidden="ariaHidden"
|
||||
:aria-label="ariaLabel">
|
||||
<img class="app-menu-icon__icon" :src="app.icon">
|
||||
<IconDot v-if="app.unread" class="app-menu-icon__unread" :size="10" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { INavigationEntry } from '../types/navigation'
|
||||
import { n } from '@nextcloud/l10n'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import IconDot from 'vue-material-design-icons/Circle.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
app: INavigationEntry
|
||||
}>()
|
||||
|
||||
const ariaHidden = computed(() => String(props.app.unread > 0))
|
||||
|
||||
const ariaLabel = computed(() => {
|
||||
if (ariaHidden.value === 'true') {
|
||||
return ''
|
||||
}
|
||||
return props.app.name
|
||||
+ (props.app.unread > 0 ? ` (${n('core', '{count} notification', '{count} notifications', props.app.unread, { count: props.app.unread })})` : '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$icon-size: 20px;
|
||||
$unread-indicator-size: 10px;
|
||||
|
||||
.app-menu-icon {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
height: $icon-size;
|
||||
width: $icon-size;
|
||||
|
||||
&__icon {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
height: $icon-size;
|
||||
width: $icon-size;
|
||||
filter: var(--background-image-invert-if-bright);
|
||||
}
|
||||
|
||||
&__unread {
|
||||
color: var(--color-error);
|
||||
position: absolute;
|
||||
inset-block-end: calc($unread-indicator-size / -2.5);
|
||||
inset-inline-end: calc($unread-indicator-size / -2.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
30
core/src/types/navigation.d.ts
vendored
Normal file
30
core/src/types/navigation.d.ts
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/** See NavigationManager */
|
||||
export interface INavigationEntry {
|
||||
/** Navigation id */
|
||||
id: string
|
||||
/** If this is the currently active app */
|
||||
active: boolean
|
||||
/** 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
|
||||
/** If this app has unread notification */
|
||||
unread: number
|
||||
/** True when the link should be opened in a new tab */
|
||||
target?: boolean
|
||||
}
|
||||
|
|
@ -150,22 +150,19 @@ export const applyChangesToNextcloud = async function() {
|
|||
'./remote.php',
|
||||
'./status.php',
|
||||
'./version.php',
|
||||
]
|
||||
|
||||
let needToApplyChanges = false
|
||||
|
||||
folderPaths.forEach((folderPath) => {
|
||||
const fullPath = path.join(htmlPath, folderPath)
|
||||
].filter((folderPath) => {
|
||||
const fullPath = path.resolve(__dirname, '..', folderPath)
|
||||
|
||||
if (existsSync(fullPath)) {
|
||||
needToApplyChanges = true
|
||||
console.log(`├─ Copying ${folderPath}`)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Don't try to apply changes, when there are none. Otherwise we
|
||||
// still execute the 'chown' command, which is not needed.
|
||||
if (!needToApplyChanges) {
|
||||
if (folderPaths.length === 0) {
|
||||
console.log('└─ No local changes found to apply')
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
validateUserThemingDefaultCss,
|
||||
expectBackgroundColor,
|
||||
} from './themingUtils'
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
|
|
@ -225,6 +226,7 @@ describe('Remove the default background with a custom background color', functio
|
|||
})
|
||||
|
||||
describe('Remove the default background with a bright color', function() {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
let selectedColor = ''
|
||||
|
||||
before(function() {
|
||||
|
|
@ -271,15 +273,16 @@ describe('Remove the default background with a bright color', function() {
|
|||
|
||||
it('See the header being inverted', function() {
|
||||
cy.waitUntil(() =>
|
||||
cy.window().then((win) => {
|
||||
const firstEntry = win.document.querySelector(
|
||||
'.app-menu-main li img',
|
||||
)
|
||||
if (!firstEntry) {
|
||||
return false
|
||||
}
|
||||
return getComputedStyle(firstEntry).filter === 'invert(1)'
|
||||
}),
|
||||
navigationHeader
|
||||
.getNavigationEntries()
|
||||
.find('img')
|
||||
.then((el) => {
|
||||
let ret = true
|
||||
el.each(function() {
|
||||
ret = ret && window.getComputedStyle(this).filter === 'invert(1)'
|
||||
})
|
||||
return ret
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
91
cypress/e2e/theming/admin-settings_default-app.cy.ts
Normal file
91
cypress/e2e/theming/admin-settings_default-app.cy.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/cypress'
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Admin theming set default apps', () => {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the current default app is the dashboard', () => {
|
||||
// check default route
|
||||
cy.visit('/')
|
||||
cy.url().should('match', /apps\/dashboard/)
|
||||
|
||||
// Also check the top logo link
|
||||
navigationHeader.logo().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] .checkbox-content').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]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['dashboard', '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]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'dashboard'])
|
||||
})
|
||||
|
||||
// Check the redirect to the default app works
|
||||
cy.request({ url: '/', followRedirect: false }).then((response) => {
|
||||
expect(response.status).to.eq(302)
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response.headers.location).to.contain('/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] .checkbox-content').click()
|
||||
cy.get('[data-cy-switch-default-app] input').should('be.not.checked')
|
||||
})
|
||||
|
||||
it('See the default app is changed back to default', () => {
|
||||
// Check the redirect to the default app works
|
||||
cy.request({ url: '/', followRedirect: false }).then((response) => {
|
||||
expect(response.status).to.eq(302)
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response.headers.location).to.contain('/apps/dashboard')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -5,89 +5,19 @@
|
|||
|
||||
import { User } from '@nextcloud/cypress'
|
||||
import { installTestApp, uninstallTestApp } from '../../support/commonUtils'
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
/**
|
||||
* Intercept setting the app order as `updateAppOrder`
|
||||
*/
|
||||
function interceptAppOrder() {
|
||||
cy.intercept('POST', '/ocs/v2.php/apps/provisioning_api/api/v1/config/users/core/apporder').as('updateAppOrder')
|
||||
}
|
||||
|
||||
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/)
|
||||
|
||||
// Also check the top logo link
|
||||
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] .checkbox-content').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]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['dashboard', '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]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'dashboard'])
|
||||
})
|
||||
|
||||
// Check the redirect to the default app works
|
||||
cy.request({ url: '/', followRedirect: false }).then((response) => {
|
||||
expect(response.status).to.eq(302)
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response.headers.location).to.contain('/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] .checkbox-content').click()
|
||||
cy.get('[data-cy-switch-default-app] input').should('be.not.checked')
|
||||
})
|
||||
|
||||
it('See the default app is changed back to default', () => {
|
||||
// Check the redirect to the default app works
|
||||
cy.request({ url: '/', followRedirect: false }).then((response) => {
|
||||
expect(response.status).to.eq(302)
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response.headers.location).to.contain('/apps/dashboard')
|
||||
})
|
||||
})
|
||||
})
|
||||
before(() => uninstallTestApp())
|
||||
|
||||
describe('User theming set app order', () => {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
|
|
@ -109,40 +39,43 @@ describe('User theming set app order', () => {
|
|||
})
|
||||
|
||||
it('See that the dashboard app is the first one', () => {
|
||||
const appOrder = ['Dashboard', 'Files']
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['dashboard', 'files'])
|
||||
})
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
|
||||
// Check the top app menu order
|
||||
cy.get('.app-menu-main .app-menu-entry').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
|
||||
expect(appIDs).to.deep.eq(['dashboard', 'files'])
|
||||
})
|
||||
navigationHeader.getNavigationEntries()
|
||||
.each((entry, index) => expect(entry).contain.text(appOrder[index]))
|
||||
})
|
||||
|
||||
it('Change the app order', () => {
|
||||
interceptAppOrder()
|
||||
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.wait('@updateAppOrder')
|
||||
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'dashboard'])
|
||||
})
|
||||
const appOrder = ['Files', 'Dashboard']
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
})
|
||||
|
||||
it('See the app menu order is changed', () => {
|
||||
cy.reload()
|
||||
cy.get('.app-menu-main .app-menu-entry').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'dashboard'])
|
||||
})
|
||||
const appOrder = ['Files', 'Dashboard']
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
|
||||
// Check the top app menu order
|
||||
navigationHeader.getNavigationEntries()
|
||||
.each((entry, index) => expect(entry).contain.text(appOrder[index]))
|
||||
})
|
||||
})
|
||||
|
||||
describe('User theming set app order with default app', () => {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
|
|
@ -176,11 +109,11 @@ describe('User theming set app order with default app', () => {
|
|||
it('See the app order settings: files 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]').then(elements => {
|
||||
expect(elements).to.have.length(4)
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'dashboard', 'testapp1', 'testapp'])
|
||||
})
|
||||
|
||||
const appOrder = ['Files', 'Dashboard', 'Test App 2', 'Test App']
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
})
|
||||
|
||||
it('Can not change the default app', () => {
|
||||
|
|
@ -195,32 +128,31 @@ describe('User theming set app order with default app', () => {
|
|||
})
|
||||
|
||||
it('Change the order of the other apps', () => {
|
||||
cy.intercept('POST', '**/apps/provisioning_api/api/v1/config/users/core/apporder').as('setAppOrder')
|
||||
interceptAppOrder()
|
||||
|
||||
// Move the testapp up twice, it should be the first one after files
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="testapp"] [data-cy-app-order-button="up"]').click()
|
||||
cy.wait('@setAppOrder')
|
||||
cy.wait('@updateAppOrder')
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="testapp"] [data-cy-app-order-button="up"]').click()
|
||||
cy.wait('@setAppOrder')
|
||||
cy.wait('@updateAppOrder')
|
||||
|
||||
// Can't get up anymore, files is enforced as default app
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="testapp"] [data-cy-app-order-button="up"]').should('not.be.visible')
|
||||
|
||||
// Check the final list order
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
|
||||
expect(elements).to.have.length(4)
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'testapp', 'dashboard', 'testapp1'])
|
||||
})
|
||||
const appOrder = ['Files', 'Test App', 'Dashboard', 'Test App 2']
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
})
|
||||
|
||||
it('See the app menu order is changed', () => {
|
||||
cy.reload()
|
||||
cy.get('.app-menu-main .app-menu-entry').then(elements => {
|
||||
expect(elements).to.have.length(4)
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'testapp', 'dashboard', 'testapp1'])
|
||||
})
|
||||
|
||||
const appOrder = ['Files', 'Test App', 'Dashboard', 'Test App 2']
|
||||
// Check the top app menu order
|
||||
navigationHeader.getNavigationEntries()
|
||||
.each((entry, index) => expect(entry).contain.text(appOrder[index]))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -247,8 +179,10 @@ describe('User theming app order list accessibility', () => {
|
|||
})
|
||||
|
||||
it('click the first button', () => {
|
||||
interceptAppOrder()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').should('be.visible').focus()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').click()
|
||||
cy.wait('@updateAppOrder')
|
||||
})
|
||||
|
||||
it('see the same app kept the focus', () => {
|
||||
|
|
@ -259,8 +193,10 @@ describe('User theming app order list accessibility', () => {
|
|||
})
|
||||
|
||||
it('click the last button', () => {
|
||||
interceptAppOrder()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').should('be.visible').focus()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').click()
|
||||
cy.wait('@updateAppOrder')
|
||||
})
|
||||
|
||||
it('see the same app kept the focus', () => {
|
||||
|
|
@ -272,6 +208,7 @@ describe('User theming app order list accessibility', () => {
|
|||
})
|
||||
|
||||
describe('User theming reset app order', () => {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
|
|
@ -293,17 +230,14 @@ describe('User theming reset app order', () => {
|
|||
})
|
||||
|
||||
it('See that the dashboard app is the first one', () => {
|
||||
const appOrder = ['Dashboard', 'Files']
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['dashboard', 'files'])
|
||||
})
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
|
||||
// Check the top app menu order
|
||||
cy.get('.app-menu-main .app-menu-entry').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-app-id')).get()
|
||||
expect(appIDs).to.deep.eq(['dashboard', 'files'])
|
||||
})
|
||||
navigationHeader.getNavigationEntries()
|
||||
.each((entry, index) => expect(entry).contain.text(appOrder[index]))
|
||||
})
|
||||
|
||||
it('See the reset button is disabled', () => {
|
||||
|
|
@ -312,15 +246,17 @@ describe('User theming reset app order', () => {
|
|||
})
|
||||
|
||||
it('Change the app order', () => {
|
||||
interceptAppOrder()
|
||||
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.wait('@updateAppOrder')
|
||||
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['files', 'dashboard'])
|
||||
})
|
||||
const appOrder = ['Files', 'Dashboard']
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
})
|
||||
|
||||
it('See the reset button is no longer disabled', () => {
|
||||
|
|
@ -329,14 +265,25 @@ describe('User theming reset app order', () => {
|
|||
})
|
||||
|
||||
it('Reset the app order', () => {
|
||||
cy.intercept('GET', '/ocs/v2.php/core/navigation/apps').as('loadApps')
|
||||
interceptAppOrder()
|
||||
cy.get('[data-test-id="btn-apporder-reset"]').click({ force: true })
|
||||
|
||||
cy.wait('@updateAppOrder')
|
||||
.its('request.body')
|
||||
.should('have.property', 'configValue', '[]')
|
||||
cy.wait('@loadApps')
|
||||
})
|
||||
|
||||
it('See the app order is restored', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').then(elements => {
|
||||
const appIDs = elements.map((idx, el) => el.getAttribute('data-cy-app-order-element')).get()
|
||||
expect(appIDs).to.deep.eq(['dashboard', 'files'])
|
||||
})
|
||||
const appOrder = ['Dashboard', 'Files']
|
||||
// Check the app order settings UI
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]')
|
||||
.each((element, index) => expect(element).to.contain.text(appOrder[index]))
|
||||
|
||||
// Check the top app menu order
|
||||
navigationHeader.getNavigationEntries()
|
||||
.each((entry, index) => expect(entry).contain.text(appOrder[index]))
|
||||
})
|
||||
|
||||
it('See the reset button is disabled again', () => {
|
||||
|
|
@ -4,7 +4,8 @@
|
|||
*/
|
||||
import { User } from '@nextcloud/cypress'
|
||||
|
||||
import { defaultPrimary, defaultBackground, pickRandomColor, validateBodyThemingCss } from './themingUtils'
|
||||
import { defaultPrimary, defaultBackground, validateBodyThemingCss } from './themingUtils'
|
||||
import { NavigationHeader } from '../../pages/NavigationHeader'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
|
|
@ -122,6 +123,8 @@ describe('User select a custom color', function() {
|
|||
})
|
||||
|
||||
describe('User select a bright custom color and remove background', function() {
|
||||
const navigationHeader = new NavigationHeader()
|
||||
|
||||
before(function() {
|
||||
cy.createRandomUser().then((user: User) => {
|
||||
cy.login(user)
|
||||
|
|
@ -159,12 +162,12 @@ describe('User select a bright custom color and remove background', function() {
|
|||
})
|
||||
|
||||
it('See the header being inverted', function() {
|
||||
cy.waitUntil(() => cy.window().then((win) => {
|
||||
const firstEntry = win.document.querySelector('.app-menu-main li img')
|
||||
if (!firstEntry) {
|
||||
return false
|
||||
}
|
||||
return getComputedStyle(firstEntry).filter === 'invert(1)'
|
||||
cy.waitUntil(() => navigationHeader.getNavigationEntries().find('img').then((el) => {
|
||||
let ret = true
|
||||
el.each(function() {
|
||||
ret = ret && window.getComputedStyle(this).filter === 'invert(1)'
|
||||
})
|
||||
return ret
|
||||
}))
|
||||
})
|
||||
|
||||
|
|
@ -181,12 +184,12 @@ describe('User select a bright custom color and remove background', function() {
|
|||
})
|
||||
|
||||
it('See the header NOT being inverted this time', function() {
|
||||
cy.waitUntil(() => cy.window().then((win) => {
|
||||
const firstEntry = win.document.querySelector('.app-menu-main li')
|
||||
if (!firstEntry) {
|
||||
return false
|
||||
}
|
||||
return getComputedStyle(firstEntry).filter === 'none'
|
||||
cy.waitUntil(() => navigationHeader.getNavigationEntries().find('img').then((el) => {
|
||||
let ret = true
|
||||
el.each(function() {
|
||||
ret = ret && window.getComputedStyle(this).filter === 'none'
|
||||
})
|
||||
return ret
|
||||
}))
|
||||
})
|
||||
})
|
||||
58
cypress/pages/NavigationHeader.ts
Normal file
58
cypress/pages/NavigationHeader.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Page object model for the Nextcloud navigation header
|
||||
*/
|
||||
export class NavigationHeader {
|
||||
|
||||
/**
|
||||
* Locator of the header bar wrapper
|
||||
*/
|
||||
header() {
|
||||
return cy.get('header#header')
|
||||
}
|
||||
|
||||
/**
|
||||
* Locator for the logo navigation entry (entry redirects to default app)
|
||||
*/
|
||||
logo() {
|
||||
return this.header()
|
||||
.find('#nextcloud')
|
||||
}
|
||||
|
||||
/**
|
||||
* Locator of the app navigation bar
|
||||
*/
|
||||
navigation() {
|
||||
return this.header()
|
||||
.findByRole('navigation', { name: 'Applications menu' })
|
||||
}
|
||||
|
||||
/**
|
||||
* The toggle for the navigation overflow menu
|
||||
*/
|
||||
overflowNavigationToggle() {
|
||||
return this.navigation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all navigation entries
|
||||
*/
|
||||
getNavigationEntries() {
|
||||
return this.navigation()
|
||||
.findAllByRole('listitem')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the navigation entry for a given app
|
||||
* @param name The app name
|
||||
*/
|
||||
getNavigationEntry(name: string) {
|
||||
return this.navigation()
|
||||
.findByRole('listitem', { name })
|
||||
}
|
||||
|
||||
}
|
||||
4
dist/1521-1521.js
vendored
4
dist/1521-1521.js
vendored
File diff suppressed because one or more lines are too long
2
dist/1521-1521.js.map
vendored
2
dist/1521-1521.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/5390-5390.js → dist/5085-5085.js
vendored
4
dist/5390-5390.js → dist/5085-5085.js
vendored
File diff suppressed because one or more lines are too long
1
dist/5085-5085.js.map
vendored
Normal file
1
dist/5085-5085.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/5085-5085.js.map.license
vendored
Symbolic link
1
dist/5085-5085.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
5085-5085.js.license
|
||||
1
dist/5390-5390.js.map
vendored
1
dist/5390-5390.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/5390-5390.js.map.license
vendored
1
dist/5390-5390.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
5390-5390.js.license
|
||||
4
dist/8737-8737.js
vendored
4
dist/8737-8737.js
vendored
File diff suppressed because one or more lines are too long
2
dist/8737-8737.js.map
vendored
2
dist/8737-8737.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-apps-view-4529.js
vendored
4
dist/settings-apps-view-4529.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-apps-view-4529.js.map
vendored
2
dist/settings-apps-view-4529.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/theming-admin-theming.js
vendored
4
dist/theming-admin-theming.js
vendored
File diff suppressed because one or more lines are too long
2
dist/theming-admin-theming.js.map
vendored
2
dist/theming-admin-theming.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/theming-personal-theming.js
vendored
4
dist/theming-personal-theming.js
vendored
File diff suppressed because one or more lines are too long
2
dist/theming-personal-theming.js.map
vendored
2
dist/theming-personal-theming.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/updatenotification-init.js
vendored
4
dist/updatenotification-init.js
vendored
|
|
@ -1,2 +1,2 @@
|
|||
(()=>{"use strict";var e,t,r,o={38248:(e,t,r)=>{var o=r(61338),n=r(32981),i=r(63814),a=r(85471),c=r(65043);const l=(0,n.C)("core","apps",{}),d=(0,a.$V)((()=>Promise.all([r.e(4208),r.e(2452)]).then(r.bind(r,92452))));(0,o.B1)("notifications:action:execute",(e=>{if("app_updated"===e.notification.objectType){var t;e.cancelAction=!0;const[r,o,n,s]=null!==(t=e.action.url.match(/(?<=\/)([^?]+)?version=((\d+.?)+)/))&&void 0!==t?t:[];(function(e,t){const r=document.createElement("div");return document.body.appendChild(r),new Promise((o=>{let n=!1;const i=new a.Ay({el:r,render:r=>r(d,{props:{appId:e,version:t},on:{dismiss:()=>{n=!0},"update:open":t=>{var r;t||(null===(r=i.$destroy)||void 0===r||r.call(i),o(n),n&&e in l&&(window.location=l[e].href))}}})})}))})(o||e.notification.objectId,n).then((t=>{t&&c.Ay.delete((0,i.KT)("apps/notifications/api/v2/notifications/{id}",{id:e.notification.notificationId}))}))}}))}},n={};function i(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,i),r.loaded=!0,r.exports}i.m=o,e=[],i.O=(t,r,o,n)=>{if(!r){var a=1/0;for(s=0;s<e.length;s++){r=e[s][0],o=e[s][1],n=e[s][2];for(var c=!0,l=0;l<r.length;l++)(!1&n||a>=n)&&Object.keys(i.O).every((e=>i.O[e](r[l])))?r.splice(l--,1):(c=!1,n<a&&(a=n));if(c){e.splice(s--,1);var d=o();void 0!==d&&(t=d)}}return t}n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[r,o,n]},i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var r in t)i.o(t,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},i.f={},i.e=e=>Promise.all(Object.keys(i.f).reduce(((t,r)=>(i.f[r](e,t),t)),[])),i.u=e=>e+"-"+e+".js?v=226f1ecdf12a579cea99",i.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",i.l=(e,o,n,a)=>{if(t[e])t[e].push(o);else{var c,l;if(void 0!==n)for(var d=document.getElementsByTagName("script"),s=0;s<d.length;s++){var u=d[s];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==r+n){c=u;break}}c||(l=!0,(c=document.createElement("script")).charset="utf-8",c.timeout=120,i.nc&&c.setAttribute("nonce",i.nc),c.setAttribute("data-webpack",r+n),c.src=e),t[e]=[o];var p=(r,o)=>{c.onerror=c.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],c.parentNode&&c.parentNode.removeChild(c),n&&n.forEach((e=>e(o))),r)return r(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=p.bind(null,c.onerror),c.onload=p.bind(null,c.onload),l&&document.head.appendChild(c)}},i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),i.j=1864,(()=>{var e;i.g.importScripts&&(e=i.g.location+"");var t=i.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),i.p=e})(),(()=>{i.b=document.baseURI||self.location.href;var e={1864:0};i.f.j=(t,r)=>{var o=i.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise(((r,n)=>o=e[t]=[r,n]));r.push(o[2]=n);var a=i.p+i.u(t),c=new Error;i.l(a,(r=>{if(i.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src;c.message="Loading chunk "+t+" failed.\n("+n+": "+a+")",c.name="ChunkLoadError",c.type=n,c.request=a,o[1](c)}}),"chunk-"+t,t)}},i.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,a=r[0],c=r[1],l=r[2],d=0;if(a.some((t=>0!==e[t]))){for(o in c)i.o(c,o)&&(i.m[o]=c[o]);if(l)var s=l(i)}for(t&&t(r);d<a.length;d++)n=a[d],i.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return i.O(s)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),i.nc=void 0;var a=i.O(void 0,[4208],(()=>i(38248)));a=i.O(a)})();
|
||||
//# sourceMappingURL=updatenotification-init.js.map?v=073ce6f2cbe43154f8fe
|
||||
(()=>{"use strict";var e,t,r,o={38248:(e,t,r)=>{var o=r(61338),n=r(32981),i=r(63814),a=r(85471),c=r(65043);const l=(0,n.C)("core","apps",[]),d=(0,a.$V)((()=>Promise.all([r.e(4208),r.e(2452)]).then(r.bind(r,92452))));(0,o.B1)("notifications:action:execute",(e=>{if("app_updated"===e.notification.objectType){var t;e.cancelAction=!0;const[r,o,n,s]=null!==(t=e.action.url.match(/(?<=\/)([^?]+)?version=((\d+.?)+)/))&&void 0!==t?t:[];(function(e,t){const r=document.createElement("div");return document.body.appendChild(r),new Promise((o=>{let n=!1;const i=new a.Ay({el:r,render:r=>r(d,{props:{appId:e,version:t},on:{dismiss:()=>{n=!0},"update:open":t=>{if(!t){var r;null===(r=i.$destroy)||void 0===r||r.call(i),o(n);const t=l.find((t=>{let{app:r}=t;return r===e}));n&&void 0!==t&&(window.location.href=t.href)}}}})})}))})(o||e.notification.objectId,n).then((t=>{t&&c.Ay.delete((0,i.KT)("apps/notifications/api/v2/notifications/{id}",{id:e.notification.notificationId}))}))}}))}},n={};function i(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,i),r.loaded=!0,r.exports}i.m=o,e=[],i.O=(t,r,o,n)=>{if(!r){var a=1/0;for(s=0;s<e.length;s++){r=e[s][0],o=e[s][1],n=e[s][2];for(var c=!0,l=0;l<r.length;l++)(!1&n||a>=n)&&Object.keys(i.O).every((e=>i.O[e](r[l])))?r.splice(l--,1):(c=!1,n<a&&(a=n));if(c){e.splice(s--,1);var d=o();void 0!==d&&(t=d)}}return t}n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[r,o,n]},i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var r in t)i.o(t,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},i.f={},i.e=e=>Promise.all(Object.keys(i.f).reduce(((t,r)=>(i.f[r](e,t),t)),[])),i.u=e=>e+"-"+e+".js?v=226f1ecdf12a579cea99",i.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",i.l=(e,o,n,a)=>{if(t[e])t[e].push(o);else{var c,l;if(void 0!==n)for(var d=document.getElementsByTagName("script"),s=0;s<d.length;s++){var u=d[s];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==r+n){c=u;break}}c||(l=!0,(c=document.createElement("script")).charset="utf-8",c.timeout=120,i.nc&&c.setAttribute("nonce",i.nc),c.setAttribute("data-webpack",r+n),c.src=e),t[e]=[o];var p=(r,o)=>{c.onerror=c.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],c.parentNode&&c.parentNode.removeChild(c),n&&n.forEach((e=>e(o))),r)return r(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=p.bind(null,c.onerror),c.onload=p.bind(null,c.onload),l&&document.head.appendChild(c)}},i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),i.j=1864,(()=>{var e;i.g.importScripts&&(e=i.g.location+"");var t=i.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),i.p=e})(),(()=>{i.b=document.baseURI||self.location.href;var e={1864:0};i.f.j=(t,r)=>{var o=i.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise(((r,n)=>o=e[t]=[r,n]));r.push(o[2]=n);var a=i.p+i.u(t),c=new Error;i.l(a,(r=>{if(i.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src;c.message="Loading chunk "+t+" failed.\n("+n+": "+a+")",c.name="ChunkLoadError",c.type=n,c.request=a,o[1](c)}}),"chunk-"+t,t)}},i.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,a=r[0],c=r[1],l=r[2],d=0;if(a.some((t=>0!==e[t]))){for(o in c)i.o(c,o)&&(i.m[o]=c[o]);if(l)var s=l(i)}for(t&&t(r);d<a.length;d++)n=a[d],i.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return i.O(s)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),i.nc=void 0;var a=i.O(void 0,[4208],(()=>i(38248)));a=i.O(a)})();
|
||||
//# sourceMappingURL=updatenotification-init.js.map?v=18f2158b823aebc06c2e
|
||||
2
dist/updatenotification-init.js.map
vendored
2
dist/updatenotification-init.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -74,7 +74,7 @@ class TemplateLayout extends \OC_Template {
|
|||
}
|
||||
|
||||
$this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry());
|
||||
$this->initialState->provideInitialState('core', 'apps', $this->navigationManager->getAll());
|
||||
$this->initialState->provideInitialState('core', 'apps', array_values($this->navigationManager->getAll()));
|
||||
|
||||
if ($this->config->getSystemValueBool('unified_search.enabled', false) || !$this->config->getSystemValueBool('enable_non-accessible_features', true)) {
|
||||
$this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT));
|
||||
|
|
|
|||
Loading…
Reference in a new issue