Skip to content

Commit a6cc367

Browse files
committed
fix(core): show the app management page as Apps in the app menu header
Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Jan C. Borchardt <925062+jancborchardt@users.noreply.github.com>
1 parent 7525f7e commit a6cc367

2 files changed

Lines changed: 65 additions & 18 deletions

File tree

core/src/components/AppMenu.vue

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,19 @@
5454
:aria-expanded="opened ? 'true' : 'false'"
5555
@click="onTriggerClick('currentApp')">
5656
<template #icon>
57-
<!-- Settings sub-sections share one generic cog. An inline MDI icon
58-
inherits the button's currentColor (--color-background-plain-text),
57+
<!-- Sections of the settings app share one generic cog. An inline MDI
58+
icon inherits the button's currentColor (--color-background-plain-text),
5959
so it stays legible on both bright and dark headers without a filter. -->
6060
<IconCog
61-
v-if="currentApp.type === 'settings'"
61+
v-if="isSettingsSection"
6262
class="app-menu__current-app-cog"
6363
:size="20" />
64-
<img
65-
v-else
66-
class="app-menu__current-app-icon"
67-
:src="currentApp.icon"
68-
alt=""
69-
aria-hidden="true">
64+
<!-- Outer element carries the header fade, inner one the icon shape. -->
65+
<span v-else class="app-menu__current-app-icon">
66+
<span
67+
class="app-menu__current-app-glyph"
68+
:style="currentAppIconStyle" />
69+
</span>
7070
</template>
7171
<span class="app-menu__current-app-name">
7272
{{ displayName }}
@@ -94,6 +94,8 @@ import logger from '../logger.js'
9494
// Settings IDs that represent actions, not navigable pages.
9595
const SETTINGS_ACTION_IDS = new Set(['logout'])
9696
97+
const SETTINGS_SECTION_IDS = new Set(['settings_personal', 'settings_administration', 'accessibility_settings'])
98+
9799
export default defineComponent({
98100
name: 'AppMenu',
99101
@@ -169,18 +171,28 @@ export default defineComponent({
169171
?? Object.values(this.settingsList).find((entry) => entry.active && !SETTINGS_ACTION_IDS.has(entry.id))
170172
},
171173
172-
// Trigger label. Settings sub-section names ("Personal info",
173-
// "Appearance and accessibility", ...) are too long and varied to
174-
// surface in the header; collapse them all to a single "Settings".
174+
isSettingsSection(): boolean {
175+
return this.currentApp !== undefined && SETTINGS_SECTION_IDS.has(this.currentApp.id)
176+
},
177+
175178
displayName(): string {
176179
if (!this.currentApp) {
177180
return ''
178181
}
179-
return this.currentApp.type === 'settings'
182+
return this.isSettingsSection
180183
? t('core', 'Settings')
181184
: this.currentApp.name
182185
},
183186
187+
// The icon is painted through a mask, so entries with a dark icon
188+
// (the app management page ships one for the settings list) are legible
189+
// on the header as well. Escaped so a crafted path cannot break out of
190+
// the url() token, same as AppIcon.vue.
191+
currentAppIconStyle(): Record<string, string> {
192+
const icon = this.currentApp?.icon ?? ''
193+
return { '--app-icon-url': `url("${icon.replace(/["\\]/g, '\\$&')}")` }
194+
},
195+
184196
// aria-label overrides the inner span text, so the displayed name
185197
// has to be duplicated here for screen readers.
186198
currentAppLabel(): string {
@@ -459,13 +471,29 @@ export default defineComponent({
459471
}
460472
461473
&__current-app-icon {
474+
display: flex;
462475
width: calc(var(--default-grid-baseline) * 5);
463476
height: calc(var(--default-grid-baseline) * 5);
464-
// Theme-aware inversion + vertical alpha fade via --header-menu-icon-mask.
465-
filter: var(--background-image-invert-if-bright);
477+
// Vertical alpha fade, like the cog and the other header icons.
466478
mask: var(--header-menu-icon-mask);
467479
}
468480
481+
&__current-app-glyph {
482+
width: 100%;
483+
height: 100%;
484+
// Masked rather than shown: app icons ship a hardcoded fill, so the
485+
// color has to come from the background. Matches AppIcon.vue.
486+
background-color: var(--color-background-plain-text);
487+
mask: var(--app-icon-url) center / contain no-repeat;
488+
}
489+
490+
// Masked backgrounds are not force-adjusted the way <img> is.
491+
@media (forced-colors: active) {
492+
&__current-app-glyph {
493+
background-color: CanvasText;
494+
}
495+
}
496+
469497
&__current-app-cog {
470498
mask: var(--header-menu-icon-mask);
471499
}

core/src/tests/components/AppMenu.spec.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,8 @@ describe('core: AppMenu', () => {
184184
// Object keyed by entry id — matches PHP's serialization shape
185185
// (TemplateLayout ships the filtered associative array as-is).
186186
return {
187-
admin_settings: makeApp({
188-
id: 'admin_settings',
187+
settings_administration: makeApp({
188+
id: 'settings_administration',
189189
name: 'Administration settings',
190190
type: 'settings',
191191
href: '/settings/admin/overview',
@@ -202,13 +202,32 @@ describe('core: AppMenu', () => {
202202
expect(wrapper.find('.app-menu__current-app-name').text()).toBe('Settings')
203203
})
204204

205+
it('keeps the own name of settings entries outside the settings app', () => {
206+
// On /settings/apps the active entry is the app management one, which
207+
// shows "Apps" here and in the account menu, not "Settings".
208+
initialState.loadState.mockImplementation((_a: string, key: string, fallback: unknown) => {
209+
if (key === 'apps') {
210+
return [makeApp({ id: 'files', name: 'Files', active: false })]
211+
}
212+
if (key === 'settingsNavEntries') {
213+
return { appstore: makeApp({ id: 'appstore', name: 'Apps', type: 'settings', href: '/settings/apps', icon: '/apps/appstore/img/app-dark.svg', active: true }) }
214+
}
215+
return fallback
216+
})
217+
const wrapper = mount(AppMenu, { attachTo: document.body })
218+
expect(wrapper.find('.app-menu__current-app-name').text()).toBe('Apps')
219+
// Its own icon, not the generic cog of the settings sections
220+
expect(wrapper.find('.app-menu__current-app-glyph').attributes('style'))
221+
.toContain('/apps/appstore/img/app-dark.svg')
222+
})
223+
205224
it('prefers the active app over a settings entry when both are marked active', () => {
206225
initialState.loadState.mockImplementation((_a: string, key: string, fallback: unknown) => {
207226
if (key === 'apps') {
208227
return [makeApp({ id: 'files', name: 'Files', active: true })]
209228
}
210229
if (key === 'settingsNavEntries') {
211-
return { admin_settings: makeApp({ id: 'admin_settings', name: 'Administration settings', type: 'settings', active: true }) }
230+
return { settings_administration: makeApp({ id: 'settings_administration', name: 'Administration settings', type: 'settings', active: true }) }
212231
}
213232
return fallback
214233
})

0 commit comments

Comments
 (0)