diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-07-03 17:04:38 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-07-04 18:03:10 +0200 |
commit | 275c4404d4a8a952fbe48d9257720f855e7ce7ca (patch) | |
tree | 4d4a43d89827c7c22b9fbe84546e5a13ae2ad3d2 | |
parent | 927beefae2a79882dbe89ed643689bb6ee033edb (diff) | |
download | nextcloud-server-275c4404d4a8a952fbe48d9257720f855e7ce7ca.tar.gz nextcloud-server-275c4404d4a8a952fbe48d9257720f855e7ce7ca.zip |
feat(files): allow to configure default view
This allows to configure which view should be the default ("start view")
in the files app, currently either "all files" or "personal files".
But it might be extended to the new home view in the future.
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r-- | apps/files/lib/Service/UserConfig.php | 40 | ||||
-rw-r--r-- | apps/files/src/init.ts | 2 | ||||
-rw-r--r-- | apps/files/src/router/router.ts | 9 | ||||
-rw-r--r-- | apps/files/src/store/userconfig.ts | 7 | ||||
-rw-r--r-- | apps/files/src/types.ts | 10 | ||||
-rw-r--r-- | apps/files/src/utils/filesViews.spec.ts | 75 | ||||
-rw-r--r-- | apps/files/src/utils/filesViews.ts | 30 | ||||
-rw-r--r-- | apps/files/src/views/Settings.vue | 27 | ||||
-rw-r--r-- | apps/files/src/views/files.ts | 11 | ||||
-rw-r--r-- | apps/files/src/views/personal-files.ts | 23 | ||||
-rw-r--r-- | cypress/e2e/files/files-settings.cy.ts | 81 |
11 files changed, 262 insertions, 53 deletions
diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php index dee89b990c6..415ca65b579 100644 --- a/apps/files/lib/Service/UserConfig.php +++ b/apps/files/lib/Service/UserConfig.php @@ -20,47 +20,53 @@ class UserConfig { 'allowed' => [true, false], ], [ - // Whether to show the "confirm file extension change" warning - 'key' => 'show_dialog_file_extension', + // The view to start the files app in + 'key' => 'default_view', + 'default' => 'files', + 'allowed' => ['files', 'personal'], + ], + [ + // Whether to show the folder tree + 'key' => 'folder_tree', 'default' => true, 'allowed' => [true, false], ], [ - // Whether to show the hidden files or not in the files list - 'key' => 'show_hidden', + // Whether to show the files list in grid view or not + 'key' => 'grid_view', 'default' => false, 'allowed' => [true, false], ], [ - // Whether to sort favorites first in the list or not - 'key' => 'sort_favorites_first', + // Whether to show the "confirm file extension change" warning + 'key' => 'show_dialog_file_extension', 'default' => true, 'allowed' => [true, false], ], [ - // Whether to sort folders before files in the list or not - 'key' => 'sort_folders_first', - 'default' => true, + // Whether to show the hidden files or not in the files list + 'key' => 'show_hidden', + 'default' => false, 'allowed' => [true, false], ], [ - // Whether to show the files list in grid view or not - 'key' => 'grid_view', + // Whether to show the mime column or not + 'key' => 'show_mime_column', 'default' => false, 'allowed' => [true, false], ], [ - // Whether to show the folder tree - 'key' => 'folder_tree', + // Whether to sort favorites first in the list or not + 'key' => 'sort_favorites_first', 'default' => true, 'allowed' => [true, false], ], [ - // Whether to show the mime column or not - 'key' => 'show_mime_column', - 'default' => false, + // Whether to sort folders before files in the list or not + 'key' => 'sort_folders_first', + 'default' => true, 'allowed' => [true, false], - ] + ], ]; protected ?IUser $user = null; diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts index 710284f691c..74eca0969b4 100644 --- a/apps/files/src/init.ts +++ b/apps/files/src/init.ts @@ -25,7 +25,7 @@ import { registerTemplateEntries } from './newMenu/newFromTemplate.ts' import { registerFavoritesView } from './views/favorites.ts' import registerRecentView from './views/recent' -import registerPersonalFilesView from './views/personal-files' +import { registerPersonalFilesView } from './views/personal-files' import { registerFilesView } from './views/files' import { registerFolderTreeView } from './views/folderTree.ts' import { registerSearchView } from './views/search.ts' diff --git a/apps/files/src/router/router.ts b/apps/files/src/router/router.ts index 20c252d6954..fccb4a0a2b2 100644 --- a/apps/files/src/router/router.ts +++ b/apps/files/src/router/router.ts @@ -10,9 +10,10 @@ import queryString from 'query-string' import Router, { isNavigationFailure, NavigationFailureType } from 'vue-router' import Vue from 'vue' -import { useFilesStore } from '../store/files' -import { usePathsStore } from '../store/paths' -import logger from '../logger' +import { useFilesStore } from '../store/files.ts' +import { usePathsStore } from '../store/paths.ts' +import { defaultView } from '../utils/filesViews.ts' +import logger from '../logger.ts' Vue.use(Router) @@ -57,7 +58,7 @@ const router = new Router({ { path: '/', // Pretending we're using the default view - redirect: { name: 'filelist', params: { view: 'files' } }, + redirect: { name: 'filelist', params: { view: defaultView() } }, }, { path: '/:view/:fileid(\\d+)?', diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts index d136561c2e5..a901ab9c593 100644 --- a/apps/files/src/store/userconfig.ts +++ b/apps/files/src/store/userconfig.ts @@ -12,12 +12,13 @@ import { ref, set } from 'vue' import axios from '@nextcloud/axios' const initialUserConfig = loadState<UserConfig>('files', 'config', { - show_hidden: false, crop_image_previews: true, - sort_favorites_first: true, - sort_folders_first: true, + default_view: 'files', grid_view: false, + show_hidden: false, show_mime_column: true, + sort_favorites_first: true, + sort_folders_first: true, show_dialog_file_extension: true, }) diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index 7e9696d31d6..dd2b9bbce3b 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -50,16 +50,18 @@ export interface PathOptions { // User config store export interface UserConfig { - [key: string]: boolean|undefined + [key: string]: boolean | string | undefined + crop_image_previews: boolean + default_view: 'files' | 'personal' + grid_view: boolean show_dialog_file_extension: boolean, show_hidden: boolean - crop_image_previews: boolean + show_mime_column: boolean sort_favorites_first: boolean sort_folders_first: boolean - grid_view: boolean - show_mime_column: boolean } + export interface UserConfigStore { userConfig: UserConfig } diff --git a/apps/files/src/utils/filesViews.spec.ts b/apps/files/src/utils/filesViews.spec.ts new file mode 100644 index 00000000000..e8c2ab3a6c1 --- /dev/null +++ b/apps/files/src/utils/filesViews.spec.ts @@ -0,0 +1,75 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { beforeEach, describe, expect, test } from 'vitest' +import { defaultView, hasPersonalFilesView } from './filesViews.ts' + +describe('hasPersonalFilesView', () => { + beforeEach(() => removeInitialState()) + + test('enabled if user has unlimited quota', () => { + mockInitialState('files', 'storageStats', { quota: -1 }) + expect(hasPersonalFilesView()).toBe(true) + }) + + test('enabled if user has limited quota', () => { + mockInitialState('files', 'storageStats', { quota: 1234 }) + expect(hasPersonalFilesView()).toBe(true) + }) + + test('disabled if user has no quota', () => { + mockInitialState('files', 'storageStats', { quota: 0 }) + expect(hasPersonalFilesView()).toBe(false) + }) +}) + +describe('defaultView', () => { + beforeEach(() => { + document.querySelectorAll('input[type="hidden"]').forEach((el) => { + el.remove() + }) + }) + + test('Returns files view if set', () => { + mockInitialState('files', 'config', { default_view: 'files' }) + expect(defaultView()).toBe('files') + }) + + test('Returns personal view if set and enabled', () => { + mockInitialState('files', 'config', { default_view: 'personal' }) + mockInitialState('files', 'storageStats', { quota: -1 }) + expect(defaultView()).toBe('personal') + }) + + test('Falls back to files if personal view is disabled', () => { + mockInitialState('files', 'config', { default_view: 'personal' }) + mockInitialState('files', 'storageStats', { quota: 0 }) + expect(defaultView()).toBe('files') + }) +}) + +/** + * Remove the mocked initial state + */ +function removeInitialState(): void { + document.querySelectorAll('input[type="hidden"]').forEach((el) => { + el.remove() + }) +} + +/** + * Helper to mock an initial state value + * @param app - The app + * @param key - The key + * @param value - The value + */ +function mockInitialState(app: string, key: string, value: unknown): void { + const el = document.createElement('input') + el.value = btoa(JSON.stringify(value)) + el.id = `initial-state-${app}-${key}` + el.type = 'hidden' + + document.head.appendChild(el) +} diff --git a/apps/files/src/utils/filesViews.ts b/apps/files/src/utils/filesViews.ts new file mode 100644 index 00000000000..9489c35cbde --- /dev/null +++ b/apps/files/src/utils/filesViews.ts @@ -0,0 +1,30 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { UserConfig } from '../types.ts' + +import { loadState } from '@nextcloud/initial-state' + +/** + * Check whether the personal files view can be shown + */ +export function hasPersonalFilesView(): boolean { + const storageStats = loadState('files', 'storageStats', { quota: -1 }) + // Don't show this view if the user has no storage quota + return storageStats.quota !== 0 +} + +/** + * Get the default files view + */ +export function defaultView() { + const { default_view: defaultView } = loadState<Partial<UserConfig>>('files', 'config', { default_view: 'files' }) + + // the default view - only use the personal one if it is enabled + if (defaultView !== 'personal' || hasPersonalFilesView()) { + return defaultView + } + return 'files' +} diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue index a9967fdbef9..46992622a3a 100644 --- a/apps/files/src/views/Settings.vue +++ b/apps/files/src/views/Settings.vue @@ -9,6 +9,27 @@ @update:open="onClose"> <!-- Settings API--> <NcAppSettingsSection id="settings" :name="t('files', 'Files settings')"> + <fieldset class="files-settings__default-view" + data-cy-files-settings-setting="default_view"> + <legend> + {{ t('files', 'Default view') }} + </legend> + <NcCheckboxRadioSwitch :model-value="userConfig.default_view" + name="default_view" + type="radio" + value="files" + @update:model-value="setConfig('default_view', $event)"> + {{ t('files', 'All files') }} + </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch :model-value="userConfig.default_view" + name="default_view" + type="radio" + value="personal" + @update:model-value="setConfig('default_view', $event)"> + {{ t('files', 'Personal files') }} + </NcCheckboxRadioSwitch> + </fieldset> + <NcCheckboxRadioSwitch data-cy-files-settings-setting="sort_favorites_first" :checked="userConfig.sort_favorites_first" @update:checked="setConfig('sort_favorites_first', $event)"> @@ -380,6 +401,12 @@ export default { </script> <style lang="scss" scoped> +.files-settings { + &__default-view { + margin-bottom: 0.5rem; + } +} + .setting-link:hover { text-decoration: underline; } diff --git a/apps/files/src/views/files.ts b/apps/files/src/views/files.ts index 699e173de63..1be58fdaf9a 100644 --- a/apps/files/src/views/files.ts +++ b/apps/files/src/views/files.ts @@ -2,11 +2,13 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { translate as t } from '@nextcloud/l10n' -import FolderSvg from '@mdi/svg/svg/folder.svg?raw' -import { getContents } from '../services/Files' import { View, getNavigation } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' +import { getContents } from '../services/Files.ts' +import { defaultView } from '../utils/filesViews.ts' + +import FolderSvg from '@mdi/svg/svg/folder.svg?raw' export const VIEW_ID = 'files' @@ -21,7 +23,8 @@ export function registerFilesView() { caption: t('files', 'List of your files and folders.'), icon: FolderSvg, - order: 0, + // if this is the default view we set it at the top of the list - otherwise below it + order: defaultView() === VIEW_ID ? 0 : 5, getContents, })) diff --git a/apps/files/src/views/personal-files.ts b/apps/files/src/views/personal-files.ts index 66d4e77b376..36888eb7ee0 100644 --- a/apps/files/src/views/personal-files.ts +++ b/apps/files/src/views/personal-files.ts @@ -2,23 +2,27 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { translate as t } from '@nextcloud/l10n' + +import { t } from '@nextcloud/l10n' import { View, getNavigation } from '@nextcloud/files' +import { getContents } from '../services/PersonalFiles.ts' +import { defaultView, hasPersonalFilesView } from '../utils/filesViews.ts' -import { getContents } from '../services/PersonalFiles' import AccountIcon from '@mdi/svg/svg/account.svg?raw' -import { loadState } from '@nextcloud/initial-state' -export default () => { - // Don't show this view if the user has no storage quota - const storageStats = loadState('files', 'storageStats', { quota: -1 }) - if (storageStats.quota === 0) { +export const VIEW_ID = 'personal' + +/** + * Register the personal files view if allowed + */ +export function registerPersonalFilesView(): void { + if (!hasPersonalFilesView()) { return } const Navigation = getNavigation() Navigation.register(new View({ - id: 'personal', + id: VIEW_ID, name: t('files', 'Personal files'), caption: t('files', 'List of your files and folders that are not shared.'), @@ -26,7 +30,8 @@ export default () => { emptyCaption: t('files', 'Files that are not shared will show up here.'), icon: AccountIcon, - order: 5, + // if this is the default view we set it at the top of the list - otherwise default position of fifth + order: defaultView() === VIEW_ID ? 0 : 5, getContents, })) diff --git a/cypress/e2e/files/files-settings.cy.ts b/cypress/e2e/files/files-settings.cy.ts index 7f02cdf7f1b..b363e630b44 100644 --- a/cypress/e2e/files/files-settings.cy.ts +++ b/cypress/e2e/files/files-settings.cy.ts @@ -4,19 +4,63 @@ */ import type { User } from '@nextcloud/cypress' -import { getRowForFile } from './FilesUtils' -const showHiddenFiles = () => { - // Open the files settings - cy.get('[data-cy-files-navigation-settings-button] a').click({ force: true }) - // Toggle the hidden files setting - cy.get('[data-cy-files-settings-setting="show_hidden"]').within(() => { - cy.get('input').should('not.be.checked') - cy.get('input').check({ force: true }) +import { getRowForFile } from './FilesUtils.ts' + +describe('files: Set default view', { testIsolation: true }, () => { + beforeEach(() => { + cy.createRandomUser().then(($user) => { + cy.login($user) + }) }) - // Close the dialog - cy.get('[data-cy-files-navigation-settings] button[aria-label="Close"]').click() -} + + it('Defaults to the "files" view', () => { + cy.visit('/apps/files') + + // See URL and current view + cy.url().should('match', /\/apps\/files\/files/) + cy.get('[data-cy-files-content-breadcrumbs]') + .findByRole('button', { + name: 'All files', + description: 'Reload current directory', + }) + + // See the option is also selected + // Open the files settings + cy.findByRole('link', { name: 'Files settings' }).click({ force: true }) + // Toggle the setting + cy.findByRole('dialog', { name: 'Files settings' }) + .should('be.visible') + .within(() => { + cy.findByRole('group', { name: 'Default view' }) + .findByRole('radio', { name: 'All files' }) + .should('be.checked') + }) + }) + + it('Can set it to personal files', () => { + cy.visit('/apps/files') + + // Open the files settings + cy.findByRole('link', { name: 'Files settings' }).click({ force: true }) + // Toggle the setting + cy.findByRole('dialog', { name: 'Files settings' }) + .should('be.visible') + .within(() => { + cy.findByRole('group', { name: 'Default view' }) + .findByRole('radio', { name: 'Personal files' }) + .check({ force: true }) + }) + + cy.visit('/apps/files') + cy.url().should('match', /\/apps\/files\/personal/) + cy.get('[data-cy-files-content-breadcrumbs]') + .findByRole('button', { + name: 'Personal files', + description: 'Reload current directory', + }) + }) +}) describe('files: Hide or show hidden files', { testIsolation: true }, () => { let user: User @@ -97,3 +141,18 @@ describe('files: Hide or show hidden files', { testIsolation: true }, () => { }) }) }) + +/** + * Helper to toggle the hidden files settings + */ +function showHiddenFiles() { + // Open the files settings + cy.get('[data-cy-files-navigation-settings-button] a').click({ force: true }) + // Toggle the hidden files setting + cy.get('[data-cy-files-settings-setting="show_hidden"]').within(() => { + cy.get('input').should('not.be.checked') + cy.get('input').check({ force: true }) + }) + // Close the dialog + cy.get('[data-cy-files-navigation-settings] button[aria-label="Close"]').click() +} |