mirror of
https://github.com/nextcloud/server.git
synced 2026-04-22 14:50:17 -04:00
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>
This commit is contained in:
parent
927beefae2
commit
275c4404d4
11 changed files with 263 additions and 54 deletions
|
|
@ -19,6 +19,24 @@ class UserConfig {
|
|||
'default' => true,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// 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 files list in grid view or not
|
||||
'key' => 'grid_view',
|
||||
'default' => false,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to show the "confirm file extension change" warning
|
||||
'key' => 'show_dialog_file_extension',
|
||||
|
|
@ -31,6 +49,12 @@ class UserConfig {
|
|||
'default' => false,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to show the mime column or not
|
||||
'key' => 'show_mime_column',
|
||||
'default' => false,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to sort favorites first in the list or not
|
||||
'key' => 'sort_favorites_first',
|
||||
|
|
@ -43,24 +67,6 @@ class UserConfig {
|
|||
'default' => true,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to show the files list in grid view or not
|
||||
'key' => 'grid_view',
|
||||
'default' => false,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to show the folder tree
|
||||
'key' => 'folder_tree',
|
||||
'default' => true,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
[
|
||||
// Whether to show the mime column or not
|
||||
'key' => 'show_mime_column',
|
||||
'default' => false,
|
||||
'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,
|
||||
default_view: 'files',
|
||||
grid_view: false,
|
||||
show_hidden: false,
|
||||
show_mime_column: true,
|
||||
sort_favorites_first: true,
|
||||
sort_folders_first: true,
|
||||
grid_view: false,
|
||||
show_mime_column: 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
|
||||
}
|
||||
|
|
|
|||
75
apps/files/src/utils/filesViews.spec.ts
Normal file
75
apps/files/src/utils/filesViews.spec.ts
Normal file
|
|
@ -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)
|
||||
}
|
||||
30
apps/files/src/utils/filesViews.ts
Normal file
30
apps/files/src/utils/filesViews.ts
Normal file
|
|
@ -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…
Reference in a new issue