Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/files/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@
'OCA\\Files\\Db\\TransferOwnershipMapper' => $baseDir . '/../lib/Db/TransferOwnershipMapper.php',
'OCA\\Files\\DirectEditingCapabilities' => $baseDir . '/../lib/DirectEditingCapabilities.php',
'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => $baseDir . '/../lib/Event/LoadAdditionalScriptsEvent.php',
'OCA\\Files\\Event\\LoadFilesApp' => $baseDir . '/../lib/Event/LoadFilesApp.php',
'OCA\\Files\\Event\\LoadSearchPlugins' => $baseDir . '/../lib/Event/LoadSearchPlugins.php',
'OCA\\Files\\Event\\LoadSidebar' => $baseDir . '/../lib/Event/LoadSidebar.php',
'OCA\\Files\\Exception\\TransferOwnershipException' => $baseDir . '/../lib/Exception/TransferOwnershipException.php',
'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php',
'OCA\\Files\\Listener\\LoadFilesAppListener' => $baseDir . '/../lib/Listener/LoadFilesAppListener.php',
'OCA\\Files\\Listener\\LoadSearchPluginsListener' => $baseDir . '/../lib/Listener/LoadSearchPluginsListener.php',
'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => $baseDir . '/../lib/Listener/NodeAddedToFavoriteListener.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/files/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,12 @@ class ComposerStaticInitFiles
'OCA\\Files\\Db\\TransferOwnershipMapper' => __DIR__ . '/..' . '/../lib/Db/TransferOwnershipMapper.php',
'OCA\\Files\\DirectEditingCapabilities' => __DIR__ . '/..' . '/../lib/DirectEditingCapabilities.php',
'OCA\\Files\\Event\\LoadAdditionalScriptsEvent' => __DIR__ . '/..' . '/../lib/Event/LoadAdditionalScriptsEvent.php',
'OCA\\Files\\Event\\LoadFilesApp' => __DIR__ . '/..' . '/../lib/Event/LoadFilesApp.php',
'OCA\\Files\\Event\\LoadSearchPlugins' => __DIR__ . '/..' . '/../lib/Event/LoadSearchPlugins.php',
'OCA\\Files\\Event\\LoadSidebar' => __DIR__ . '/..' . '/../lib/Event/LoadSidebar.php',
'OCA\\Files\\Exception\\TransferOwnershipException' => __DIR__ . '/..' . '/../lib/Exception/TransferOwnershipException.php',
'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
'OCA\\Files\\Listener\\LoadFilesAppListener' => __DIR__ . '/..' . '/../lib/Listener/LoadFilesAppListener.php',
'OCA\\Files\\Listener\\LoadSearchPluginsListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSearchPluginsListener.php',
'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
'OCA\\Files\\Listener\\NodeAddedToFavoriteListener' => __DIR__ . '/..' . '/../lib/Listener/NodeAddedToFavoriteListener.php',
Expand Down
3 changes: 3 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
use OCA\Files\ConfigLexicon;
use OCA\Files\Dashboard\FavoriteWidget;
use OCA\Files\DirectEditingCapabilities;
use OCA\Files\Event\LoadFilesApp;
use OCA\Files\Event\LoadSearchPlugins;
use OCA\Files\Event\LoadSidebar;
use OCA\Files\Listener\LoadFilesAppListener;
use OCA\Files\Listener\LoadSearchPluginsListener;
use OCA\Files\Listener\LoadSidebarListener;
use OCA\Files\Listener\NodeAddedToFavoriteListener;
Expand Down Expand Up @@ -80,6 +82,7 @@ public function register(IRegistrationContext $context): void {
$context->registerCapability(AdvancedCapabilities::class);
$context->registerCapability(DirectEditingCapabilities::class);

$context->registerEventListener(LoadFilesApp::class, LoadFilesAppListener::class);
$context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
$context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
$context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class);
Expand Down
61 changes: 2 additions & 59 deletions apps/files/lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,9 @@

namespace OCA\Files\Controller;

use OC\Files\FilenameValidator;
use OC\Files\Filesystem;
use OCA\Files\AppInfo\Application;
use OCA\Files\ConfigLexicon;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadSearchPlugins;
use OCA\Files\Event\LoadSidebar;
use OCA\Files\Service\UserConfig;
use OCA\Files\Service\ViewConfig;
use OCA\Files\Event\LoadFilesApp;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
Expand All @@ -26,21 +20,15 @@
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Services\IInitialState;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\Template\ITemplateManager;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Util;

/**
* @package OCA\Files\Controller
Expand All @@ -53,18 +41,11 @@ public function __construct(
IRequest $request,
private IURLGenerator $urlGenerator,
private IL10N $l10n,
private IConfig $config,
private IEventDispatcher $eventDispatcher,
private IUserSession $userSession,
private IAppManager $appManager,
private IRootFolder $rootFolder,
private IInitialState $initialState,
private ITemplateManager $templateManager,
private UserConfig $userConfig,
private ViewConfig $viewConfig,
private FilenameValidator $filenameValidator,
private IRegistry $twoFactorRegistry,
private IAppConfig $appConfig,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -142,10 +123,6 @@ public function index($dir = '', $view = '', $fileid = null) {
}
}

// Load the files we need
Util::addInitScript('files', 'init');
Util::addScript('files', 'main');

$user = $this->userSession->getUser();
$userId = $user->getUID();

Expand Down Expand Up @@ -174,42 +151,8 @@ public function index($dir = '', $view = '', $fileid = null) {
}

$this->initialState->provideInitialState('storageStats', $storageInfo);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
$this->initialState->provideInitialState('recent_limit', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_LIMIT, 100));
// Not yet consumed by the frontend, provided for future implementation
$this->initialState->provideInitialState('group_recent_files', $this->appConfig->getAppValueBool(ConfigLexicon::GROUP_RECENT_FILES, false));
$this->initialState->provideInitialState('recent_files_group_mime_types', $this->appConfig->getAppValueArray(ConfigLexicon::RECENT_FILES_GROUP_MIME_TYPES, []));
$this->initialState->provideInitialState('recent_files_group_timespan_minutes', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_FILES_GROUP_TIMESPAN_MINUTES, 2));

// File sorting user config
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
$this->initialState->provideInitialState('filesSortingConfig', $filesSortingConfig);

// Forbidden file characters (deprecated use capabilities)
// TODO: Remove with next release of `@nextcloud/files`
$forbiddenCharacters = $this->filenameValidator->getForbiddenCharacters();
$this->initialState->provideInitialState('forbiddenCharacters', $forbiddenCharacters);

$event = new LoadAdditionalScriptsEvent();
$this->eventDispatcher->dispatchTyped($event);
$this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
$this->eventDispatcher->dispatchTyped(new LoadSearchPlugins());

$this->initialState->provideInitialState('templates_enabled', true);
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
$this->initialState->provideInitialState('localClientEnabled', $this->appConfig->getAppValueBool(ConfigLexicon::LOCAL_CLIENT_INTEGRATION));

$isTwoFactorEnabled = false;
foreach ($this->twoFactorRegistry->getProviderStates($user) as $providerId => $providerState) {
if ($providerId !== 'backup_codes' && $providerState === true) {
$isTwoFactorEnabled = true;
}
}

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

$response = new TemplateResponse(
Application::APP_ID,
Expand Down
21 changes: 21 additions & 0 deletions apps/files/lib/Event/LoadFilesApp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files\Event;

use OCP\EventDispatcher\Event;

/**
* Dispatch this event from any page that needs to render the Files UI
* (e.g. via `OCP.Files.renderFilesApp()`) but is not served by the Files
* app's own controller. It triggers the same script/initial-state bootstrap
* that `files.view.index` performs.
*/
class LoadFilesApp extends Event {
}
102 changes: 102 additions & 0 deletions apps/files/lib/Listener/LoadFilesAppListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files\Listener;

use OC\Files\FilenameValidator;
use OCA\Files\ConfigLexicon;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadFilesApp;
use OCA\Files\Event\LoadSearchPlugins;
use OCA\Files\Event\LoadSidebar;
use OCA\Files\Service\UserConfig;
use OCA\Files\Service\ViewConfig;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Services\IInitialState;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
use OCP\Config\IUserConfig;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Template\ITemplateManager;
use OCP\IUserSession;
use OCP\Util;

/**
* Loads the Files app's frontend scripts and generic initial state so it can
* be rendered on any page, be it the Files app's own `files.view.index`
* route or a foreign page calling `OCP.Files.renderFilesApp()`.
*
* @template-implements IEventListener<LoadFilesApp>
*/
class LoadFilesAppListener implements IEventListener {
public function __construct(
private IEventDispatcher $eventDispatcher,
private IInitialState $initialState,
private IUserConfig $config,
private IUserSession $userSession,
private UserConfig $userConfig,
private ViewConfig $viewConfig,
private FilenameValidator $filenameValidator,
private IRegistry $twoFactorRegistry,
private IAppConfig $appConfig,
private ITemplateManager $templateManager,
) {
}

#[\Override]
public function handle(Event $event): void {
if (!($event instanceof LoadFilesApp)) {
return;
}

Util::addInitScript('files', 'init');
Util::addScript('files', 'main');

$user = $this->userSession->getUser();
$userId = $user->getUID();

$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
$this->initialState->provideInitialState('recent_limit', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_LIMIT, 100));
// Not yet consumed by the frontend, provided for future implementation
$this->initialState->provideInitialState('group_recent_files', $this->appConfig->getAppValueBool(ConfigLexicon::GROUP_RECENT_FILES, false));
$this->initialState->provideInitialState('recent_files_group_mime_types', $this->appConfig->getAppValueArray(ConfigLexicon::RECENT_FILES_GROUP_MIME_TYPES, []));
$this->initialState->provideInitialState('recent_files_group_timespan_minutes', $this->appConfig->getAppValueInt(ConfigLexicon::RECENT_FILES_GROUP_TIMESPAN_MINUTES, 2));

// File sorting user config
$filesSortingConfig = json_decode($this->config->getValueString($userId, 'files', 'files_sorting_configs', '{}'), true);
$this->initialState->provideInitialState('filesSortingConfig', $filesSortingConfig);

// Forbidden file characters (deprecated use capabilities)
// TODO: Remove with next release of `@nextcloud/files`
$forbiddenCharacters = $this->filenameValidator->getForbiddenCharacters();
$this->initialState->provideInitialState('forbiddenCharacters', $forbiddenCharacters);

$this->eventDispatcher->dispatchTyped(new LoadAdditionalScriptsEvent());
$this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
$this->eventDispatcher->dispatchTyped(new LoadSearchPlugins());

$this->initialState->provideInitialState('templates_enabled', true);
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
$this->initialState->provideInitialState('localClientEnabled', $this->appConfig->getAppValueBool(ConfigLexicon::LOCAL_CLIENT_INTEGRATION));

$isTwoFactorEnabled = false;
foreach ($this->twoFactorRegistry->getProviderStates($user) as $providerId => $providerState) {
if ($providerId !== 'backup_codes' && $providerState === true) {
$isTwoFactorEnabled = true;
}
}

$this->initialState->provideInitialState('isTwoFactorEnabled', $isTwoFactorEnabled);
}
}
13 changes: 8 additions & 5 deletions apps/files/src/components/BreadCrumbs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import { getBreadcrumbDirs } from '../utils/breadcrumbs.ts'
import { logger } from '../utils/logger.ts'

export default defineComponent({
Expand All @@ -100,6 +101,12 @@ export default defineComponent({
type: String,
default: '/',
},

/** The directory to start the breadcrumbs with */
root: {
type: String,
default: '/',
},
},

emits: ['reload'],
Expand Down Expand Up @@ -159,11 +166,7 @@ export default defineComponent({

computed: {
dirs(): string[] {
const cumulativePath = (acc: string) => (value: string) => (acc += `${value}/`)
// Generate a cumulative path for each path segment: ['/', '/foo', '/foo/bar', ...] etc
const paths: string[] = this.path.split('/').filter(Boolean).map(cumulativePath('/'))
// Strip away trailing slash
return ['/', ...paths.map((path: string) => path.replace(/^(.+)\/$/, '$1'))]
return getBreadcrumbDirs(this.path, this.root)
},

sections() {
Expand Down
2 changes: 1 addition & 1 deletion apps/files/src/components/FileEntry/FileEntryActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ export default defineComponent({
<style lang="scss">
// Allow right click to define the position of the menu
// only if defined
main.app-content[style*="mouse-pos-x"] .v-popper__popper {
.app-content[style*="mouse-pos-x"] .v-popper__popper {
transform: translate3d(var(--mouse-pos-x), var(--mouse-pos-y), 0px) !important;

// If the menu is too close to the bottom, we move it up
Expand Down
4 changes: 3 additions & 1 deletion apps/files/src/components/FileEntryGrid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { nextTick, toRaw } from 'vue'
import FileEntryGrid from './FileEntryGrid.vue'
import { router } from '../router/router.ts'
import { getFilesAppRouter } from '../router/router.ts'
import { useActiveStore } from '../store/active.ts'

// useFileListWidth builds its ResizeObserver while the module is evaluated, so
Expand All @@ -25,6 +25,8 @@ vi.hoisted(() => {

vi.mock('@nextcloud/auth')

const router = getFilesAppRouter()

const source = new File({
id: 42,
source: 'http://nextcloud.local/remote.php/dav/files/test/Deep/Nested/report.pdf',
Expand Down
6 changes: 3 additions & 3 deletions apps/files/src/components/FileEntryMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export default defineComponent({
if (this.actionsMenuStore.opened === null) {
// Reset any right menu position potentially set
logger.debug('All actions menu closed, resetting right menu position...')
const root = this.$el?.closest('main.app-content') as HTMLElement
const root = this.$el?.closest('.app-content') as HTMLElement
if (root !== null) {
root.style.removeProperty('--mouse-pos-x')
root.style.removeProperty('--mouse-pos-y')
Expand Down Expand Up @@ -313,7 +313,7 @@ export default defineComponent({
// the actions menu mouse position
if (!this.gridMode) {
// Actions menu is contained within the app content
const root = this.$el?.closest('main.app-content') as HTMLElement
const root = this.$el?.closest('.app-content') as HTMLElement
const contentRect = root.getBoundingClientRect()
// Using Math.min/max to prevent the menu from going out of the AppContent
// 200 = max width of the menu
Expand All @@ -322,7 +322,7 @@ export default defineComponent({
root.style.setProperty('--mouse-pos-y', Math.max(0, event.clientY - contentRect.top) + 'px')
} else {
// Reset any right menu position potentially set
const root = this.$el?.closest('main.app-content') as HTMLElement
const root = this.$el?.closest('.app-content') as HTMLElement
root.style.removeProperty('--mouse-pos-x')
root.style.removeProperty('--mouse-pos-y')
}
Expand Down
Loading
Loading