Skip to content

Commit f6b1f43

Browse files
committed
feat(files): only render file list
1 parent a194514 commit f6b1f43

7 files changed

Lines changed: 147 additions & 82 deletions

File tree

apps/files/src/main.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import SettingsModel from './models/Setting.ts'
1111
import router from './router/router.ts'
1212
import RouterService from './services/RouterService.ts'
1313
import SettingsService from './services/Settings.js'
14-
import { renderFilesApp } from './services/renderFilesApp.ts'
14+
import { renderFilesView } from './services/renderFilesView.ts'
1515
import { getPinia } from './store/index.ts'
1616

1717
__webpack_nonce__ = getCSPNonce()
@@ -27,7 +27,7 @@ if (!window.OCP.Files.Router) {
2727
}
2828

2929
// Expose the ability to render the Files UI on a foreign page
30-
window.OCP.Files.renderFilesApp ??= renderFilesApp
30+
window.OCP.Files.renderFilesApp ??= renderFilesView
3131

3232
// Init Pinia store
3333
Vue.use(PiniaVuePlugin)
@@ -37,8 +37,14 @@ const Settings = new SettingsService()
3737
Object.assign(window.OCA.Files, { Settings })
3838
Object.assign(window.OCA.Files.Settings, { Setting: SettingsModel })
3939

40-
const FilesAppVue = Vue.extend(FilesApp)
41-
new FilesAppVue({
42-
router: (window.OCP.Files.Router as RouterService)._router,
43-
pinia: getPinia(),
44-
}).$mount('#content')
40+
// Other apps load this same script to get `OCP.Files.renderFilesApp()`.
41+
// Only take over `#content` when we are actually on the Files app's own
42+
// page - identifiable by the app-specific class core's layout adds to it.
43+
const contentEl = document.getElementById('content')
44+
if (contentEl?.classList.contains('app-files')) {
45+
const FilesAppVue = Vue.extend(FilesApp)
46+
new FilesAppVue({
47+
router: (window.OCP.Files.Router as RouterService)._router,
48+
pinia: getPinia(),
49+
}).$mount(contentEl)
50+
}

apps/files/src/services/renderFilesApp.spec.ts

Lines changed: 0 additions & 39 deletions
This file was deleted.

apps/files/src/services/renderFilesApp.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { View, getNavigation } from '@nextcloud/files'
7+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
8+
import { renderFilesView } from './renderFilesView.ts'
9+
10+
const VIEW_ID = 'test-view'
11+
12+
vi.mock('../views/FilesList.vue', () => ({
13+
default: {
14+
name: 'FilesList',
15+
props: { embedded: { type: Boolean, default: false } },
16+
render(h) {
17+
return h('div', { attrs: { id: 'files-list-stub', 'data-embedded': String(this.embedded) } })
18+
},
19+
},
20+
}))
21+
22+
describe('renderFilesView', () => {
23+
beforeEach(() => {
24+
document.body.innerHTML = ''
25+
getNavigation().register(new View({
26+
id: VIEW_ID,
27+
name: 'Test view',
28+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" /></svg>',
29+
getContents: async () => ({ folder: {} as never, contents: [] }),
30+
}))
31+
})
32+
33+
afterEach(() => {
34+
getNavigation().remove(VIEW_ID)
35+
})
36+
37+
it('mounts only the file list, embedded, into the given element', () => {
38+
const el = document.createElement('div')
39+
document.body.appendChild(el)
40+
41+
renderFilesView(el, VIEW_ID)
42+
43+
const stub = document.body.querySelector('#files-list-stub')
44+
expect(stub).not.toBeNull()
45+
expect(stub?.getAttribute('data-embedded')).toBe('true')
46+
})
47+
48+
it('activates the requested view without touching the browser URL', () => {
49+
const el = document.createElement('div')
50+
document.body.appendChild(el)
51+
const before = window.location.href
52+
53+
renderFilesView(el, VIEW_ID)
54+
55+
expect(window.location.href).toBe(before)
56+
expect(getNavigation().active?.id).toBe(VIEW_ID)
57+
})
58+
59+
it('returns a handle that unmounts the file list', () => {
60+
const el = document.createElement('div')
61+
document.body.appendChild(el)
62+
63+
const rendered = renderFilesView(el, VIEW_ID)
64+
rendered.destroy()
65+
66+
expect(document.body.querySelector('#files-list-stub')).toBeNull()
67+
})
68+
})
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { getNavigation } from '@nextcloud/files'
7+
import Vue from 'vue'
8+
import FilesList from '../views/FilesList.vue'
9+
import { createRouter } from '../router/router.ts'
10+
import { getPinia } from '../store/index.ts'
11+
12+
/** Handle returned by `renderFilesView()` to tear down the embedded instance. */
13+
export interface RenderedFilesView {
14+
/** Unmount the file list and free the DOM node it was rendered into. */
15+
destroy: () => void
16+
}
17+
18+
/**
19+
* Render only the Files file list (breadcrumbs, toolbar, file list) into a
20+
* foreign DOM element, activating the given Files navigation view.
21+
*
22+
* This never renders `NcContent` or `NcAppContent` - those belong to the
23+
* Files app's own page chrome (see `FilesApp.vue`) and are skipped here so
24+
* the file list can be embedded as a widget inside another app's page.
25+
*
26+
* The host page must dispatch `OCA\Files\Event\LoadFilesApp` server-side
27+
* beforehand so the Files app's scripts and initial state are loaded, and
28+
* the target view must already be registered via `getNavigation().register()`.
29+
*
30+
* Exposed as `OCP.Files.renderFilesApp()`.
31+
*
32+
* @param el the element to mount the file list into
33+
* @param viewId the id of the Files navigation view to display
34+
*/
35+
export function renderFilesView(el: HTMLElement, viewId: string): RenderedFilesView {
36+
const router = createRouter('abstract')
37+
const FilesListVue = Vue.extend(FilesList)
38+
const vm = new FilesListVue({
39+
router,
40+
pinia: getPinia(),
41+
propsData: {
42+
embedded: true,
43+
},
44+
}).$mount(el)
45+
46+
// Normally done by the navigation sidebar's route watcher; there is none
47+
// here, so keep the active view in sync ourselves.
48+
getNavigation().setActive(viewId)
49+
router.push({ name: 'filelist', params: { view: viewId } }).catch(() => {})
50+
51+
return {
52+
destroy: () => {
53+
vm.$destroy()
54+
vm.$el.remove()
55+
},
56+
}
57+
}

apps/files/src/views/FilesList.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- SPDX-License-Identifier: AGPL-3.0-or-later
44
-->
55
<template>
6-
<NcAppContent :page-heading="pageHeading" data-cy-files-content>
6+
<component :is="embedded ? 'div' : 'NcAppContent'" v-bind="embedded ? {} : { pageHeading }" data-cy-files-content>
77
<div class="files-list__header" :class="{ 'files-list__header--public': isPublic }">
88
<!-- Uploader -->
99
<component :is="isNarrow ? 'Teleport' : 'div'" :to="isNarrow ? 'body' : undefined">
@@ -150,7 +150,7 @@
150150
</NcEmptyContent>
151151
</template>
152152
</FilesListVirtual>
153-
</NcAppContent>
153+
</component>
154154
</template>
155155

156156
<script lang="ts">
@@ -237,6 +237,11 @@ export default defineComponent({
237237
type: Boolean,
238238
default: false,
239239
},
240+
// Skip the NcAppContent wrapper when rendered standalone via `renderFilesView()`.
241+
embedded: {
242+
type: Boolean,
243+
default: false,
244+
},
240245
},
241246
242247
setup() {

window.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import type Tab from './apps/files/src/models/Tab.js'
7+
import type { RenderedFilesView } from './apps/files/src/services/renderFilesView.ts'
78
import type RouterService from './apps/files/src/services/RouterService.ts'
89
import type Settings from './apps/files/src/services/Settings.js'
910
import type Sidebar from './apps/files/src/services/Sidebar.js'
@@ -32,7 +33,7 @@ declare global {
3233
OCP: {
3334
Files: {
3435
Router: RouterService
35-
renderFilesApp: (el: HTMLElement, viewId: string) => void
36+
renderFilesApp: (el: HTMLElement, viewId: string) => RenderedFilesView
3637
}
3738
} & Nextcloud.v29.OCP
3839

0 commit comments

Comments
 (0)