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>
pull/53798/head
Ferdinand Thiessen 3 months ago
parent 927beefae2
commit 275c4404d4
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
  1. 40
      apps/files/lib/Service/UserConfig.php
  2. 2
      apps/files/src/init.ts
  3. 9
      apps/files/src/router/router.ts
  4. 7
      apps/files/src/store/userconfig.ts
  5. 10
      apps/files/src/types.ts
  6. 75
      apps/files/src/utils/filesViews.spec.ts
  7. 30
      apps/files/src/utils/filesViews.ts
  8. 27
      apps/files/src/views/Settings.vue
  9. 11
      apps/files/src/views/files.ts
  10. 23
      apps/files/src/views/personal-files.ts
  11. 81
      cypress/e2e/files/files-settings.cy.ts

@ -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;

@ -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'

@ -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+)?',

@ -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,
})

@ -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
}

@ -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)
}

@ -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'
}

@ -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;
}

@ -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,
}))

@ -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,
}))

@ -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()
}

Loading…
Cancel
Save