Skip to content

Commit 8bdfc84

Browse files
committed
feat(files): Make Files app reusable
Signed-off-by: Tomasz Trillò <tomasz.trillo@dataport.de> Assisted-by: Copilot:Claude-Sonnet-5
1 parent 5d7425b commit 8bdfc84

13 files changed

Lines changed: 353 additions & 137 deletions

File tree

apps/files/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@
7070
'OCA\\Files\\Db\\TransferOwnershipMapper' => $baseDir . '/../lib/Db/TransferOwnershipMapper.php',
7171
'OCA\\Files\\DirectEditingCapabilities' => $baseDir . '/../lib/DirectEditingCapabilities.php',
7272
'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => $baseDir . '/../lib/Event/LoadAdditionalScriptsEvent.php',
73+
'OCA\\Files\\Event\\LoadFilesApp' => $baseDir . '/../lib/Event/LoadFilesApp.php',
7374
'OCA\\Files\\Event\\LoadSearchPlugins' => $baseDir . '/../lib/Event/LoadSearchPlugins.php',
7475
'OCA\\Files\\Event\\LoadSidebar' => $baseDir . '/../lib/Event/LoadSidebar.php',
7576
'OCA\\Files\\Exception\\TransferOwnershipException' => $baseDir . '/../lib/Exception/TransferOwnershipException.php',
7677
'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php',
78+
'OCA\\Files\\Listener\\LoadFilesAppListener' => $baseDir . '/../lib/Listener/LoadFilesAppListener.php',
7779
'OCA\\Files\\Listener\\LoadSearchPluginsListener' => $baseDir . '/../lib/Listener/LoadSearchPluginsListener.php',
7880
'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
7981
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => $baseDir . '/../lib/Listener/NodeAddedToFavoriteListener.php',

apps/files/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ class ComposerStaticInitFiles
8585
'OCA\\Files\\Db\\TransferOwnershipMapper' => __DIR__ . '/..' . '/../lib/Db/TransferOwnershipMapper.php',
8686
'OCA\\Files\\DirectEditingCapabilities' => __DIR__ . '/..' . '/../lib/DirectEditingCapabilities.php',
8787
'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => __DIR__ . '/..' . '/../lib/Event/LoadAdditionalScriptsEvent.php',
88+
'OCA\\Files\\Event\\LoadFilesApp' => __DIR__ . '/..' . '/../lib/Event/LoadFilesApp.php',
8889
'OCA\\Files\\Event\\LoadSearchPlugins' => __DIR__ . '/..' . '/../lib/Event/LoadSearchPlugins.php',
8990
'OCA\\Files\\Event\\LoadSidebar' => __DIR__ . '/..' . '/../lib/Event/LoadSidebar.php',
9091
'OCA\\Files\\Exception\\TransferOwnershipException' => __DIR__ . '/..' . '/../lib/Exception/TransferOwnershipException.php',
9192
'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
93+
'OCA\\Files\\Listener\\LoadFilesAppListener' => __DIR__ . '/..' . '/../lib/Listener/LoadFilesAppListener.php',
9294
'OCA\\Files\\Listener\\LoadSearchPluginsListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSearchPluginsListener.php',
9395
'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
9496
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeAddedToFavoriteListener.php',

apps/files/lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
use OCA\Files\ConfigLexicon;
2525
use OCA\Files\Dashboard\FavoriteWidget;
2626
use OCA\Files\DirectEditingCapabilities;
27+
use OCA\Files\Event\LoadFilesApp;
2728
use OCA\Files\Event\LoadSearchPlugins;
2829
use OCA\Files\Event\LoadSidebar;
30+
use OCA\Files\Listener\LoadFilesAppListener;
2931
use OCA\Files\Listener\LoadSearchPluginsListener;
3032
use OCA\Files\Listener\LoadSidebarListener;
3133
use OCA\Files\Listener\NodeAddedToFavoriteListener;
@@ -79,6 +81,7 @@ public function register(IRegistrationContext $context): void {
7981
$context->registerCapability(AdvancedCapabilities::class);
8082
$context->registerCapability(DirectEditingCapabilities::class);
8183

84+
$context->registerEventListener(LoadFilesApp::class, LoadFilesAppListener::class);
8285
$context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
8386
$context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
8487
$context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class);

apps/files/lib/Controller/ViewController.php

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,9 @@
88

99
namespace OCA\Files\Controller;
1010

11-
use OC\Files\FilenameValidator;
1211
use OC\Files\Filesystem;
1312
use OCA\Files\AppInfo\Application;
14-
use OCA\Files\ConfigLexicon;
15-
use OCA\Files\Event\LoadAdditionalScriptsEvent;
16-
use OCA\Files\Event\LoadSearchPlugins;
17-
use OCA\Files\Event\LoadSidebar;
18-
use OCA\Files\Service\UserConfig;
19-
use OCA\Files\Service\ViewConfig;
20-
use OCA\Viewer\Event\LoadViewer;
13+
use OCA\Files\Event\LoadFilesApp;
2114
use OCP\App\IAppManager;
2215
use OCP\AppFramework\Controller;
2316
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
@@ -27,21 +20,15 @@
2720
use OCP\AppFramework\Http\RedirectResponse;
2821
use OCP\AppFramework\Http\Response;
2922
use OCP\AppFramework\Http\TemplateResponse;
30-
use OCP\AppFramework\Services\IAppConfig;
3123
use OCP\AppFramework\Services\IInitialState;
32-
use OCP\Authentication\TwoFactorAuth\IRegistry;
33-
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
3424
use OCP\EventDispatcher\IEventDispatcher;
3525
use OCP\Files\Folder;
3626
use OCP\Files\IRootFolder;
3727
use OCP\Files\NotFoundException;
38-
use OCP\Files\Template\ITemplateManager;
39-
use OCP\IConfig;
4028
use OCP\IL10N;
4129
use OCP\IRequest;
4230
use OCP\IURLGenerator;
4331
use OCP\IUserSession;
44-
use OCP\Util;
4532

4633
/**
4734
* @package OCA\Files\Controller
@@ -54,18 +41,11 @@ public function __construct(
5441
IRequest $request,
5542
private IURLGenerator $urlGenerator,
5643
private IL10N $l10n,
57-
private IConfig $config,
5844
private IEventDispatcher $eventDispatcher,
5945
private IUserSession $userSession,
6046
private IAppManager $appManager,
6147
private IRootFolder $rootFolder,
6248
private IInitialState $initialState,
63-
private ITemplateManager $templateManager,
64-
private UserConfig $userConfig,
65-
private ViewConfig $viewConfig,
66-
private FilenameValidator $filenameValidator,
67-
private IRegistry $twoFactorRegistry,
68-
private IAppConfig $appConfig,
6949
) {
7050
parent::__construct($appName, $request);
7151
}
@@ -143,10 +123,6 @@ public function index($dir = '', $view = '', $fileid = null) {
143123
}
144124
}
145125

146-
// Load the files we need
147-
Util::addInitScript('files', 'init');
148-
Util::addScript('files', 'main');
149-
150126
$user = $this->userSession->getUser();
151127
$userId = $user->getUID();
152128

@@ -175,46 +151,8 @@ public function index($dir = '', $view = '', $fileid = null) {
175151
}
176152

177153
$this->initialState->provideInitialState('storageStats', $storageInfo);
178-
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
179-
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
180-
$this->initialState->provideInitialState('recent_limit', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_LIMIT, 100));
181-
// Not yet consumed by the frontend, provided for future implementation
182-
$this->initialState->provideInitialState('group_recent_files', $this->appConfig->getAppValueBool(ConfigLexicon::GROUP_RECENT_FILES, false));
183-
$this->initialState->provideInitialState('recent_files_group_mime_types', $this->appConfig->getAppValueArray(ConfigLexicon::RECENT_FILES_GROUP_MIME_TYPES, []));
184-
$this->initialState->provideInitialState('recent_files_group_timespan_minutes', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_FILES_GROUP_TIMESPAN_MINUTES, 2));
185-
186-
// File sorting user config
187-
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
188-
$this->initialState->provideInitialState('filesSortingConfig', $filesSortingConfig);
189-
190-
// Forbidden file characters (deprecated use capabilities)
191-
// TODO: Remove with next release of `@nextcloud/files`
192-
$forbiddenCharacters = $this->filenameValidator->getForbiddenCharacters();
193-
$this->initialState->provideInitialState('forbiddenCharacters', $forbiddenCharacters);
194-
195-
$event = new LoadAdditionalScriptsEvent();
196-
$this->eventDispatcher->dispatchTyped($event);
197-
$this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
198-
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
199-
$this->eventDispatcher->dispatchTyped(new LoadSearchPlugins());
200-
// Load Viewer scripts
201-
if (class_exists(LoadViewer::class)) {
202-
$this->eventDispatcher->dispatchTyped(new LoadViewer());
203-
}
204-
205-
$this->initialState->provideInitialState('templates_enabled', true);
206-
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
207-
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
208-
$this->initialState->provideInitialState('localClientEnabled', $this->appConfig->getAppValueBool(ConfigLexicon::LOCAL_CLIENT_INTEGRATION));
209-
210-
$isTwoFactorEnabled = false;
211-
foreach ($this->twoFactorRegistry->getProviderStates($user) as $providerId => $providerState) {
212-
if ($providerId !== 'backup_codes' && $providerState === true) {
213-
$isTwoFactorEnabled = true;
214-
}
215-
}
216154

217-
$this->initialState->provideInitialState('isTwoFactorEnabled', $isTwoFactorEnabled);
155+
$this->eventDispatcher->dispatchTyped(new LoadFilesApp());
218156

219157
$response = new TemplateResponse(
220158
Application::APP_ID,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Files\Event;
11+
12+
use OCP\EventDispatcher\Event;
13+
14+
/**
15+
* Dispatch this event from any page that needs to render the Files UI
16+
* (e.g. via `OCP.Files.renderFilesApp()`) but is not served by the Files
17+
* app's own controller. It triggers the same script/initial-state bootstrap
18+
* that `files.view.index` performs.
19+
*/
20+
class LoadFilesApp extends Event {
21+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Files\Listener;
11+
12+
use OC\Files\FilenameValidator;
13+
use OCA\Files\ConfigLexicon;
14+
use OCA\Files\Event\LoadAdditionalScriptsEvent;
15+
use OCA\Files\Event\LoadFilesApp;
16+
use OCA\Files\Event\LoadSearchPlugins;
17+
use OCA\Files\Event\LoadSidebar;
18+
use OCA\Files\Service\UserConfig;
19+
use OCA\Files\Service\ViewConfig;
20+
use OCA\Viewer\Event\LoadViewer;
21+
use OCP\AppFramework\Services\IAppConfig;
22+
use OCP\AppFramework\Services\IInitialState;
23+
use OCP\Authentication\TwoFactorAuth\IRegistry;
24+
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
25+
use OCP\EventDispatcher\Event;
26+
use OCP\EventDispatcher\IEventDispatcher;
27+
use OCP\EventDispatcher\IEventListener;
28+
use OCP\Files\Template\ITemplateManager;
29+
use OCP\IConfig;
30+
use OCP\IUserSession;
31+
use OCP\Util;
32+
33+
/**
34+
* Loads the Files app's frontend scripts and generic initial state so it can
35+
* be rendered on any page, be it the Files app's own `files.view.index`
36+
* route or a foreign page calling `OCP.Files.renderFilesApp()`.
37+
*
38+
* @template-implements IEventListener<LoadFilesApp>
39+
*/
40+
class LoadFilesAppListener implements IEventListener {
41+
public function __construct(
42+
private IEventDispatcher $eventDispatcher,
43+
private IInitialState $initialState,
44+
private IConfig $config,
45+
private IUserSession $userSession,
46+
private UserConfig $userConfig,
47+
private ViewConfig $viewConfig,
48+
private FilenameValidator $filenameValidator,
49+
private IRegistry $twoFactorRegistry,
50+
private IAppConfig $appConfig,
51+
private ITemplateManager $templateManager,
52+
) {
53+
}
54+
55+
#[\Override]
56+
public function handle(Event $event): void {
57+
if (!($event instanceof LoadFilesApp)) {
58+
return;
59+
}
60+
61+
Util::addInitScript('files', 'init');
62+
Util::addScript('files', 'main');
63+
64+
$user = $this->userSession->getUser();
65+
$userId = $user->getUID();
66+
67+
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
68+
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
69+
$this->initialState->provideInitialState('recent_limit', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_LIMIT, 100));
70+
// Not yet consumed by the frontend, provided for future implementation
71+
$this->initialState->provideInitialState('group_recent_files', $this->appConfig->getAppValueBool(ConfigLexicon::GROUP_RECENT_FILES, false));
72+
$this->initialState->provideInitialState('recent_files_group_mime_types', $this->appConfig->getAppValueArray(ConfigLexicon::RECENT_FILES_GROUP_MIME_TYPES, []));
73+
$this->initialState->provideInitialState('recent_files_group_timespan_minutes', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_FILES_GROUP_TIMESPAN_MINUTES, 2));
74+
75+
// File sorting user config
76+
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
77+
$this->initialState->provideInitialState('filesSortingConfig', $filesSortingConfig);
78+
79+
// Forbidden file characters (deprecated use capabilities)
80+
// TODO: Remove with next release of `@nextcloud/files`
81+
$forbiddenCharacters = $this->filenameValidator->getForbiddenCharacters();
82+
$this->initialState->provideInitialState('forbiddenCharacters', $forbiddenCharacters);
83+
84+
$this->eventDispatcher->dispatchTyped(new LoadAdditionalScriptsEvent());
85+
$this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
86+
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
87+
$this->eventDispatcher->dispatchTyped(new LoadSearchPlugins());
88+
// Load Viewer scripts
89+
if (class_exists(LoadViewer::class)) {
90+
$this->eventDispatcher->dispatchTyped(new LoadViewer());
91+
}
92+
93+
$this->initialState->provideInitialState('templates_enabled', true);
94+
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
95+
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
96+
$this->initialState->provideInitialState('localClientEnabled', $this->appConfig->getAppValueBool(ConfigLexicon::LOCAL_CLIENT_INTEGRATION));
97+
98+
$isTwoFactorEnabled = false;
99+
foreach ($this->twoFactorRegistry->getProviderStates($user) as $providerId => $providerState) {
100+
if ($providerId !== 'backup_codes' && $providerState === true) {
101+
$isTwoFactorEnabled = true;
102+
}
103+
}
104+
105+
$this->initialState->provideInitialState('isTwoFactorEnabled', $isTwoFactorEnabled);
106+
}
107+
}

apps/files/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +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'
1415
import { getPinia } from './store/index.ts'
1516

1617
__webpack_nonce__ = getCSPNonce()
@@ -25,6 +26,9 @@ if (!window.OCP.Files.Router) {
2526
Object.assign(window.OCP.Files, { Router })
2627
}
2728

29+
// Expose the ability to render the Files UI on a foreign page
30+
window.OCP.Files.renderFilesApp = window.OCP.Files.renderFilesApp ?? renderFilesApp
31+
2832
// Init Pinia store
2933
Vue.use(PiniaVuePlugin)
3034

apps/files/src/router/router.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,33 +51,44 @@ function ignoreDuplicateNavigation(error: unknown): void {
5151
}
5252
}
5353

54-
const router = new Router({
55-
mode: 'history',
56-
57-
// if index.php is in the url AND we got this far, then it's working:
58-
// let's keep using index.php in the url
59-
base: generateUrl('/apps/files'),
60-
linkActiveClass: 'active',
61-
62-
routes: [
63-
{
64-
path: '/',
65-
// Pretending we're using the default view
66-
redirect: { name: 'filelist', params: { view: defaultView() } },
67-
},
68-
{
69-
path: '/:view/:fileid(\\d+)?',
70-
name: 'filelist',
71-
props: true,
54+
/**
55+
* Create a Files app vue-router instance.
56+
*
57+
* @param mode 'history' (default) drives the browser URL and is used by the main Files app.
58+
* 'abstract' keeps navigation in-memory, for embedding the Files UI into a foreign page
59+
* (see `OCP.Files.renderFilesApp()`) without touching the host page's URL.
60+
*/
61+
export function createRouter(mode: 'history' | 'abstract' = 'history'): Router {
62+
return new Router({
63+
mode,
64+
65+
// if index.php is in the url AND we got this far, then it's working:
66+
// let's keep using index.php in the url
67+
base: mode === 'history' ? generateUrl('/apps/files') : undefined,
68+
linkActiveClass: 'active',
69+
70+
routes: [
71+
{
72+
path: '/',
73+
// Pretending we're using the default view
74+
redirect: { name: 'filelist', params: { view: defaultView() } },
75+
},
76+
{
77+
path: '/:view/:fileid(\\d+)?',
78+
name: 'filelist',
79+
props: true,
80+
},
81+
],
82+
83+
// Custom stringifyQuery to prevent encoding of slashes in the url
84+
stringifyQuery(query) {
85+
const result = queryString.stringify(query).replace(/%2F/gmi, '/')
86+
return result ? ('?' + result) : ''
7287
},
73-
],
88+
})
89+
}
7490

75-
// Custom stringifyQuery to prevent encoding of slashes in the url
76-
stringifyQuery(query) {
77-
const result = queryString.stringify(query).replace(/%2F/gmi, '/')
78-
return result ? ('?' + result) : ''
79-
},
80-
})
91+
const router = createRouter('history')
8192

8293
// Handle aborted navigation (NavigationGuards) gracefully
8394
router.onError((error) => {

0 commit comments

Comments
 (0)