Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/frontend-legacy/webpack.modules.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
login: path.join(__dirname, 'core/src', 'login.js'),
login_flow: path.join(__dirname, 'core/src', 'login-flow.ts'),
main: path.join(__dirname, 'core/src', 'main.js'),
appmenu: path.join(__dirname, 'core/src/appmenu', 'main.ts'),
maintenance: path.join(__dirname, 'core/src', 'maintenance.js'),
'public-page-menu': path.resolve(__dirname, 'core/src', 'public-page-menu.ts'),
'public-page-user-menu': path.resolve(__dirname, 'core/src', 'public-page-user-menu.ts'),
Expand Down
File renamed without changes.
File renamed without changes.
39 changes: 39 additions & 0 deletions core/src/appmenu/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Standalone entry for the waffle launcher (AppMenu). Mounts independently of
* core-main so the app grid lives in its own chunk.
*/
import Vue from 'vue'
import AppMenu from './AppMenu.vue'

interface AppMenuInstance {
setNavigationCounter(id: string, counter: number): void
}

declare global {
var OC: {
setNavigationCounter?: (id: string, counter: number) => void
}
}

/**
* Mount the AppMenu into the header container, if present on this layout.
*/
function mount(): void {
const container = document.getElementById('header-start__appmenu')
if (!container) {
// No container on this layout (e.g. public pages). Nothing to mount.
return
}
const AppMenuApp = Vue.extend(AppMenu)
const instance = new AppMenuApp({}).$mount(container) as unknown as AppMenuInstance

globalThis.OC = globalThis.OC ?? {}
globalThis.OC.setNavigationCounter = (id, counter) => {
instance.setNavigationCounter(id, counter)
}
}

mount()
35 changes: 0 additions & 35 deletions core/src/components/MainMenu.js

This file was deleted.

2 changes: 0 additions & 2 deletions core/src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import { getLocale } from '@nextcloud/l10n'
import moment from 'moment'
import { setUp as setUpContactsMenu } from './components/ContactsMenu.js'
import { setUp as setUpMainMenu } from './components/MainMenu.js'
import { setUp as setUpUserMenu } from './components/UserMenu.js'
import { initSessionHeartBeat } from './session-heartbeat.ts'
import { initFallbackClipboardAPI } from './utils/ClipboardFallback.ts'
Expand Down Expand Up @@ -46,7 +45,6 @@ export function initCore() {

initSessionHeartBeat()

setUpMainMenu()
setUpUserMenu()
setUpContactsMenu()
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ vi.mock('@nextcloud/l10n', () => ({
},
}))

import AppItem from '../../components/AppItem.vue'
import AppItem from '../../appmenu/AppItem.vue'

function makeApp(overrides: Partial<INavigationEntry> = {}): INavigationEntry {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function eightApps(activeIndex: number = -1): INavigationEntry[] {
// Import AFTER mocks are registered. Static `import` would hoist above
// vi.mock() and break the wiring; dynamic import in beforeAll/await is the
// idiomatic Vitest workaround when you need to control mock state per test.
import type AppMenuModule from '../../components/AppMenu.vue'
import type AppMenuModule from '../../appmenu/AppMenu.vue'
let AppMenu: typeof AppMenuModule

beforeEach(async () => {
Expand All @@ -88,7 +88,7 @@ beforeEach(async () => {
}
initialState.loadState.mockImplementation((_app: string, key: string, fallback: unknown) => key === 'apps' ? fakeApps() : fallback)
auth.getCurrentUser.mockReturnValue({ isAdmin: false })
AppMenu = (await import('../../components/AppMenu.vue')).default
AppMenu = (await import('../../appmenu/AppMenu.vue')).default
})

afterEach(() => {
Expand Down
67 changes: 67 additions & 0 deletions core/src/tests/appmenu/main.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('@nextcloud/initial-state', () => ({
loadState: () => [],
}))
vi.mock('@nextcloud/auth', () => ({
getCurrentUser: () => ({ isAdmin: false }),
}))
vi.mock('@nextcloud/event-bus', () => ({
subscribe: () => undefined,
unsubscribe: () => undefined,
}))
vi.mock('@nextcloud/l10n', () => ({
isRTL: () => false,
n: (_app: string, singular: string) => singular,
t: (_app: string, text: string) => text,
}))
vi.mock('@nextcloud/router', () => ({
generateUrl: (url: string) => url,
imagePath: (_app: string, file: string) => `/img/${file}`,
}))

declare global {
var OC: { setNavigationCounter?: (id: string, count: number) => void }
}

// The id the bootstrap mounts into (must match main.ts).
function addContainer(): void {
const container = document.createElement('nav')
container.id = 'header-start__appmenu'
document.body.appendChild(container)
}

describe('appmenu/main', () => {
beforeEach(() => {
document.body.innerHTML = ''
globalThis.OC = {}
vi.resetModules()
})

it('mounts AppMenu when the container is present', async () => {
addContainer()

await import('../../appmenu/main.ts')

// Vue 2 $mount replaces the container with AppMenu's root <nav class="app-menu">.
expect(document.querySelector('.app-menu')).not.toBeNull()
})

it('no-ops when the container is missing', async () => {
await import('../../appmenu/main.ts')

expect(document.body.children.length).toBe(0)
})

it('exposes OC.setNavigationCounter as a callable function', async () => {
addContainer()

await import('../../appmenu/main.ts')

expect(typeof globalThis.OC.setNavigationCounter).toBe('function')
})
})
2 changes: 2 additions & 0 deletions dist/core-appmenu.js

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions dist/core-appmenu.js.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
SPDX-License-Identifier: MIT
SPDX-License-Identifier: ISC
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Rob Cresswell <robcresswell@pm.me>
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark


This file is generated from multiple sources. Included packages:
- @nextcloud/auth
- version: 2.6.0
- license: GPL-3.0-or-later
- @nextcloud/browser-storage
- version: 0.5.0
- license: GPL-3.0-or-later
- semver
- version: 7.7.2
- license: ISC
- @nextcloud/event-bus
- version: 3.3.3
- license: GPL-3.0-or-later
- @nextcloud/initial-state
- version: 3.0.0
- license: GPL-3.0-or-later
- @nextcloud/l10n
- version: 3.4.1
- license: GPL-3.0-or-later
- @nextcloud/logger
- version: 3.0.3
- license: GPL-3.0-or-later
- @nextcloud/router
- version: 3.1.0
- license: GPL-3.0-or-later
- focus-trap
- version: 7.8.0
- license: MIT
- @nextcloud/vue
- version: 8.39.0
- license: AGPL-3.0-or-later
- css-loader
- version: 7.1.2
- license: MIT
- dompurify
- version: 3.4.2
- license: (MPL-2.0 OR Apache-2.0)
- escape-html
- version: 1.0.3
- license: MIT
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- process
- version: 0.11.10
- license: MIT
- style-loader
- version: 4.0.0
- license: MIT
- tabbable
- version: 6.4.0
- license: MIT
- vue-loader
- version: 15.11.1
- license: MIT
- vue-material-design-icons
- version: 5.3.1
- license: MIT
- vue
- version: 2.7.16
- license: MIT
- webpack
- version: 5.106.2
- license: MIT
- nextcloud
- version: 1.0.0
- license: AGPL-3.0-or-later
1 change: 1 addition & 0 deletions dist/core-appmenu.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/core-appmenu.js.map.license
4 changes: 2 additions & 2 deletions dist/core-common.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/core-common.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/core-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/core-main.js.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/private/TemplateLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public function getPageTemplate(string $renderAs, string $appId): ITemplate {
Util::addScript('core', 'unified-search', 'core');
}

Util::addScript('core', 'appmenu', 'core');

// Set logo link target
$logoUrl = $this->config->getSystemValueString('logo_url', '');
$page->assign('logoUrl', $logoUrl);
Expand Down
Loading