Skip to content

Commit 664b514

Browse files
authored
Merge pull request #53798 from nextcloud/feat/allow-to-configure-default-view
feat(files): allow to configure default view
2 parents 927beef + f845202 commit 664b514

File tree

15 files changed

+268
-59
lines changed

15 files changed

+268
-59
lines changed

apps/files/lib/Service/UserConfig.php

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,47 +20,53 @@ class UserConfig {
2020
'allowed' => [true, false],
2121
],
2222
[
23-
// Whether to show the "confirm file extension change" warning
24-
'key' => 'show_dialog_file_extension',
23+
// The view to start the files app in
24+
'key' => 'default_view',
25+
'default' => 'files',
26+
'allowed' => ['files', 'personal'],
27+
],
28+
[
29+
// Whether to show the folder tree
30+
'key' => 'folder_tree',
2531
'default' => true,
2632
'allowed' => [true, false],
2733
],
2834
[
29-
// Whether to show the hidden files or not in the files list
30-
'key' => 'show_hidden',
35+
// Whether to show the files list in grid view or not
36+
'key' => 'grid_view',
3137
'default' => false,
3238
'allowed' => [true, false],
3339
],
3440
[
35-
// Whether to sort favorites first in the list or not
36-
'key' => 'sort_favorites_first',
41+
// Whether to show the "confirm file extension change" warning
42+
'key' => 'show_dialog_file_extension',
3743
'default' => true,
3844
'allowed' => [true, false],
3945
],
4046
[
41-
// Whether to sort folders before files in the list or not
42-
'key' => 'sort_folders_first',
43-
'default' => true,
47+
// Whether to show the hidden files or not in the files list
48+
'key' => 'show_hidden',
49+
'default' => false,
4450
'allowed' => [true, false],
4551
],
4652
[
47-
// Whether to show the files list in grid view or not
48-
'key' => 'grid_view',
53+
// Whether to show the mime column or not
54+
'key' => 'show_mime_column',
4955
'default' => false,
5056
'allowed' => [true, false],
5157
],
5258
[
53-
// Whether to show the folder tree
54-
'key' => 'folder_tree',
59+
// Whether to sort favorites first in the list or not
60+
'key' => 'sort_favorites_first',
5561
'default' => true,
5662
'allowed' => [true, false],
5763
],
5864
[
59-
// Whether to show the mime column or not
60-
'key' => 'show_mime_column',
61-
'default' => false,
65+
// Whether to sort folders before files in the list or not
66+
'key' => 'sort_folders_first',
67+
'default' => true,
6268
'allowed' => [true, false],
63-
]
69+
],
6470
];
6571
protected ?IUser $user = null;
6672

apps/files/src/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { registerTemplateEntries } from './newMenu/newFromTemplate.ts'
2525

2626
import { registerFavoritesView } from './views/favorites.ts'
2727
import registerRecentView from './views/recent'
28-
import registerPersonalFilesView from './views/personal-files'
28+
import { registerPersonalFilesView } from './views/personal-files'
2929
import { registerFilesView } from './views/files'
3030
import { registerFolderTreeView } from './views/folderTree.ts'
3131
import { registerSearchView } from './views/search.ts'

apps/files/src/router/router.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import queryString from 'query-string'
1010
import Router, { isNavigationFailure, NavigationFailureType } from 'vue-router'
1111
import Vue from 'vue'
1212

13-
import { useFilesStore } from '../store/files'
14-
import { usePathsStore } from '../store/paths'
15-
import logger from '../logger'
13+
import { useFilesStore } from '../store/files.ts'
14+
import { usePathsStore } from '../store/paths.ts'
15+
import { defaultView } from '../utils/filesViews.ts'
16+
import logger from '../logger.ts'
1617

1718
Vue.use(Router)
1819

@@ -57,7 +58,7 @@ const router = new Router({
5758
{
5859
path: '/',
5960
// Pretending we're using the default view
60-
redirect: { name: 'filelist', params: { view: 'files' } },
61+
redirect: { name: 'filelist', params: { view: defaultView() } },
6162
},
6263
{
6364
path: '/:view/:fileid(\\d+)?',

apps/files/src/store/userconfig.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import { ref, set } from 'vue'
1212
import axios from '@nextcloud/axios'
1313

1414
const initialUserConfig = loadState<UserConfig>('files', 'config', {
15-
show_hidden: false,
1615
crop_image_previews: true,
17-
sort_favorites_first: true,
18-
sort_folders_first: true,
16+
default_view: 'files',
1917
grid_view: false,
18+
show_hidden: false,
2019
show_mime_column: true,
20+
sort_favorites_first: true,
21+
sort_folders_first: true,
2122

2223
show_dialog_file_extension: true,
2324
})

apps/files/src/types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,18 @@ export interface PathOptions {
5050

5151
// User config store
5252
export interface UserConfig {
53-
[key: string]: boolean|undefined
53+
[key: string]: boolean | string | undefined
5454

55+
crop_image_previews: boolean
56+
default_view: 'files' | 'personal'
57+
grid_view: boolean
5558
show_dialog_file_extension: boolean,
5659
show_hidden: boolean
57-
crop_image_previews: boolean
60+
show_mime_column: boolean
5861
sort_favorites_first: boolean
5962
sort_folders_first: boolean
60-
grid_view: boolean
61-
show_mime_column: boolean
6263
}
64+
6365
export interface UserConfigStore {
6466
userConfig: UserConfig
6567
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { beforeEach, describe, expect, test } from 'vitest'
7+
import { defaultView, hasPersonalFilesView } from './filesViews.ts'
8+
9+
describe('hasPersonalFilesView', () => {
10+
beforeEach(() => removeInitialState())
11+
12+
test('enabled if user has unlimited quota', () => {
13+
mockInitialState('files', 'storageStats', { quota: -1 })
14+
expect(hasPersonalFilesView()).toBe(true)
15+
})
16+
17+
test('enabled if user has limited quota', () => {
18+
mockInitialState('files', 'storageStats', { quota: 1234 })
19+
expect(hasPersonalFilesView()).toBe(true)
20+
})
21+
22+
test('disabled if user has no quota', () => {
23+
mockInitialState('files', 'storageStats', { quota: 0 })
24+
expect(hasPersonalFilesView()).toBe(false)
25+
})
26+
})
27+
28+
describe('defaultView', () => {
29+
beforeEach(() => {
30+
document.querySelectorAll('input[type="hidden"]').forEach((el) => {
31+
el.remove()
32+
})
33+
})
34+
35+
test('Returns files view if set', () => {
36+
mockInitialState('files', 'config', { default_view: 'files' })
37+
expect(defaultView()).toBe('files')
38+
})
39+
40+
test('Returns personal view if set and enabled', () => {
41+
mockInitialState('files', 'config', { default_view: 'personal' })
42+
mockInitialState('files', 'storageStats', { quota: -1 })
43+
expect(defaultView()).toBe('personal')
44+
})
45+
46+
test('Falls back to files if personal view is disabled', () => {
47+
mockInitialState('files', 'config', { default_view: 'personal' })
48+
mockInitialState('files', 'storageStats', { quota: 0 })
49+
expect(defaultView()).toBe('files')
50+
})
51+
})
52+
53+
/**
54+
* Remove the mocked initial state
55+
*/
56+
function removeInitialState(): void {
57+
document.querySelectorAll('input[type="hidden"]').forEach((el) => {
58+
el.remove()
59+
})
60+
}
61+
62+
/**
63+
* Helper to mock an initial state value
64+
* @param app - The app
65+
* @param key - The key
66+
* @param value - The value
67+
*/
68+
function mockInitialState(app: string, key: string, value: unknown): void {
69+
const el = document.createElement('input')
70+
el.value = btoa(JSON.stringify(value))
71+
el.id = `initial-state-${app}-${key}`
72+
el.type = 'hidden'
73+
74+
document.head.appendChild(el)
75+
}

apps/files/src/utils/filesViews.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { UserConfig } from '../types.ts'
7+
8+
import { loadState } from '@nextcloud/initial-state'
9+
10+
/**
11+
* Check whether the personal files view can be shown
12+
*/
13+
export function hasPersonalFilesView(): boolean {
14+
const storageStats = loadState('files', 'storageStats', { quota: -1 })
15+
// Don't show this view if the user has no storage quota
16+
return storageStats.quota !== 0
17+
}
18+
19+
/**
20+
* Get the default files view
21+
*/
22+
export function defaultView() {
23+
const { default_view: defaultView } = loadState<Partial<UserConfig>>('files', 'config', { default_view: 'files' })
24+
25+
// the default view - only use the personal one if it is enabled
26+
if (defaultView !== 'personal' || hasPersonalFilesView()) {
27+
return defaultView
28+
}
29+
return 'files'
30+
}

apps/files/src/views/Settings.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@
99
@update:open="onClose">
1010
<!-- Settings API-->
1111
<NcAppSettingsSection id="settings" :name="t('files', 'Files settings')">
12+
<fieldset class="files-settings__default-view"
13+
data-cy-files-settings-setting="default_view">
14+
<legend>
15+
{{ t('files', 'Default view') }}
16+
</legend>
17+
<NcCheckboxRadioSwitch :model-value="userConfig.default_view"
18+
name="default_view"
19+
type="radio"
20+
value="files"
21+
@update:model-value="setConfig('default_view', $event)">
22+
{{ t('files', 'All files') }}
23+
</NcCheckboxRadioSwitch>
24+
<NcCheckboxRadioSwitch :model-value="userConfig.default_view"
25+
name="default_view"
26+
type="radio"
27+
value="personal"
28+
@update:model-value="setConfig('default_view', $event)">
29+
{{ t('files', 'Personal files') }}
30+
</NcCheckboxRadioSwitch>
31+
</fieldset>
32+
1233
<NcCheckboxRadioSwitch data-cy-files-settings-setting="sort_favorites_first"
1334
:checked="userConfig.sort_favorites_first"
1435
@update:checked="setConfig('sort_favorites_first', $event)">
@@ -380,6 +401,12 @@ export default {
380401
</script>
381402
382403
<style lang="scss" scoped>
404+
.files-settings {
405+
&__default-view {
406+
margin-bottom: 0.5rem;
407+
}
408+
}
409+
383410
.setting-link:hover {
384411
text-decoration: underline;
385412
}

apps/files/src/views/files.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import { translate as t } from '@nextcloud/l10n'
6-
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
75

8-
import { getContents } from '../services/Files'
96
import { View, getNavigation } from '@nextcloud/files'
7+
import { t } from '@nextcloud/l10n'
8+
import { getContents } from '../services/Files.ts'
9+
import { defaultView } from '../utils/filesViews.ts'
10+
11+
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
1012

1113
export const VIEW_ID = 'files'
1214

@@ -21,7 +23,8 @@ export function registerFilesView() {
2123
caption: t('files', 'List of your files and folders.'),
2224

2325
icon: FolderSvg,
24-
order: 0,
26+
// if this is the default view we set it at the top of the list - otherwise below it
27+
order: defaultView() === VIEW_ID ? 0 : 5,
2528

2629
getContents,
2730
}))

apps/files/src/views/personal-files.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,36 @@
22
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import { translate as t } from '@nextcloud/l10n'
5+
6+
import { t } from '@nextcloud/l10n'
67
import { View, getNavigation } from '@nextcloud/files'
8+
import { getContents } from '../services/PersonalFiles.ts'
9+
import { defaultView, hasPersonalFilesView } from '../utils/filesViews.ts'
710

8-
import { getContents } from '../services/PersonalFiles'
911
import AccountIcon from '@mdi/svg/svg/account.svg?raw'
10-
import { loadState } from '@nextcloud/initial-state'
1112

12-
export default () => {
13-
// Don't show this view if the user has no storage quota
14-
const storageStats = loadState('files', 'storageStats', { quota: -1 })
15-
if (storageStats.quota === 0) {
13+
export const VIEW_ID = 'personal'
14+
15+
/**
16+
* Register the personal files view if allowed
17+
*/
18+
export function registerPersonalFilesView(): void {
19+
if (!hasPersonalFilesView()) {
1620
return
1721
}
1822

1923
const Navigation = getNavigation()
2024
Navigation.register(new View({
21-
id: 'personal',
25+
id: VIEW_ID,
2226
name: t('files', 'Personal files'),
2327
caption: t('files', 'List of your files and folders that are not shared.'),
2428

2529
emptyTitle: t('files', 'No personal files found'),
2630
emptyCaption: t('files', 'Files that are not shared will show up here.'),
2731

2832
icon: AccountIcon,
29-
order: 5,
33+
// if this is the default view we set it at the top of the list - otherwise default position of fifth
34+
order: defaultView() === VIEW_ID ? 0 : 5,
3035

3136
getContents,
3237
}))

0 commit comments

Comments
 (0)