Skip to content

Commit b7212e3

Browse files
authored
Merge pull request #47400 from nextcloud/feat/load-more-than-50-faves
feat(files): Allow more than 50 favorite views
2 parents a740e60 + f48a7ff commit b7212e3

7 files changed

Lines changed: 84 additions & 113 deletions

File tree

apps/files/lib/Controller/ViewController.php

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
namespace OCA\Files\Controller;
99

1010
use OC\Files\FilenameValidator;
11-
use OCA\Files\Activity\Helper;
1211
use OCA\Files\AppInfo\Application;
1312
use OCA\Files\Event\LoadAdditionalScriptsEvent;
1413
use OCA\Files\Event\LoadSearchPlugins;
@@ -54,7 +53,6 @@ public function __construct(
5453
private IUserSession $userSession,
5554
private IAppManager $appManager,
5655
private IRootFolder $rootFolder,
57-
private Helper $activityHelper,
5856
private IInitialState $initialState,
5957
private ITemplateManager $templateManager,
6058
private UserConfig $userConfig,
@@ -146,18 +144,6 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
146144

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

149-
// Get all the user favorites to create a submenu
150-
try {
151-
$userFolder = $this->rootFolder->getUserFolder($userId);
152-
$favElements = $this->activityHelper->getFavoriteNodes($userId, true);
153-
$favElements = array_map(fn (Folder $node) => [
154-
'fileid' => $node->getId(),
155-
'path' => $userFolder->getRelativePath($node->getPath()),
156-
], $favElements);
157-
} catch (\RuntimeException $e) {
158-
$favElements = [];
159-
}
160-
161147
// If the file doesn't exists in the folder and
162148
// exists in only one occurrence, redirect to that file
163149
// in the correct folder
@@ -187,7 +173,6 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
187173
$this->initialState->provideInitialState('storageStats', $storageInfo);
188174
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
189175
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
190-
$this->initialState->provideInitialState('favoriteFolders', $favElements);
191176

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

apps/files/src/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { entry as newFolderEntry } from './newMenu/newFolder.ts'
2323
import { entry as newTemplatesFolder } from './newMenu/newTemplatesFolder.ts'
2424
import { registerTemplateEntries } from './newMenu/newFromTemplate.ts'
2525

26-
import registerFavoritesView from './views/favorites'
26+
import { registerFavoritesView } from './views/favorites.ts'
2727
import registerRecentView from './views/recent'
2828
import registerPersonalFilesView from './views/personal-files'
2929
import registerFilesView from './views/files'

apps/files/src/views/favorites.spec.ts

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
44
* SPDX-License-Identifier: AGPL-3.0-or-later
55
*/
6-
import { Folder, Navigation, getNavigation } from '@nextcloud/files'
6+
7+
import type { Folder as CFolder, Navigation } from '@nextcloud/files'
8+
9+
import * as filesUtils from '@nextcloud/files'
710
import { CancelablePromise } from 'cancelable-promise'
811
import { basename } from 'path'
912
import { beforeEach, describe, expect, test, vi } from 'vitest'
1013
import * as eventBus from '@nextcloud/event-bus'
11-
import * as initialState from '@nextcloud/initial-state'
1214

1315
import { action } from '../actions/favoriteAction'
1416
import * as favoritesService from '../services/Favorites'
15-
import registerFavoritesView from './favorites'
17+
import { registerFavoritesView } from './favorites'
18+
19+
const { Folder, getNavigation } = filesUtils
1620

1721
vi.mock('@nextcloud/axios')
1822

@@ -37,11 +41,12 @@ describe('Favorites view definition', () => {
3741
expect(window._nc_navigation).toBeDefined()
3842
})
3943

40-
test('Default empty favorite view', () => {
44+
test('Default empty favorite view', async () => {
4145
vi.spyOn(eventBus, 'subscribe')
42-
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))
46+
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([]))
47+
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))
4348

44-
registerFavoritesView()
49+
await registerFavoritesView()
4550
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
4651
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')
4752

@@ -64,16 +69,31 @@ describe('Favorites view definition', () => {
6469
expect(favoritesView?.getContents).toBeDefined()
6570
})
6671

67-
test('Default with favorites', () => {
72+
test('Default with favorites', async () => {
6873
const favoriteFolders = [
69-
{ fileid: 1, path: '/foo' },
70-
{ fileid: 2, path: '/bar' },
71-
{ fileid: 3, path: '/foo/bar' },
74+
new Folder({
75+
id: 1,
76+
root: '/files/admin',
77+
source: 'http://nextcloud.local/remote.php/dav/files/admin/foo',
78+
owner: 'admin',
79+
}),
80+
new Folder({
81+
id: 2,
82+
root: '/files/admin',
83+
source: 'http://nextcloud.local/remote.php/dav/files/admin/bar',
84+
owner: 'admin',
85+
}),
86+
new Folder({
87+
id: 3,
88+
root: '/files/admin',
89+
source: 'http://nextcloud.local/remote.php/dav/files/admin/foo/bar',
90+
owner: 'admin',
91+
}),
7292
]
73-
vi.spyOn(initialState, 'loadState').mockReturnValue(favoriteFolders)
74-
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))
93+
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve(favoriteFolders))
94+
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))
7595

76-
registerFavoritesView()
96+
await registerFavoritesView()
7797
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
7898
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')
7999

@@ -91,7 +111,7 @@ describe('Favorites view definition', () => {
91111
expect(favoriteView?.order).toBe(index)
92112
expect(favoriteView?.params).toStrictEqual({
93113
dir: folder.path,
94-
fileid: folder.fileid.toString(),
114+
fileid: String(folder.fileid),
95115
view: 'favorites',
96116
})
97117
expect(favoriteView?.parent).toBe('favorites')
@@ -112,10 +132,10 @@ describe('Dynamic update of favorite folders', () => {
112132

113133
test('Add a favorite folder creates a new entry in the navigation', async () => {
114134
vi.spyOn(eventBus, 'emit')
115-
vi.spyOn(initialState, 'loadState').mockReturnValue([])
116-
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))
135+
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([]))
136+
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))
117137

118-
registerFavoritesView()
138+
await registerFavoritesView()
119139
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
120140
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')
121141

@@ -140,10 +160,17 @@ describe('Dynamic update of favorite folders', () => {
140160

141161
test('Remove a favorite folder remove the entry from the navigation column', async () => {
142162
vi.spyOn(eventBus, 'emit')
143-
vi.spyOn(initialState, 'loadState').mockReturnValue([{ fileid: 42, path: '/Foo/Bar' }])
144-
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))
145-
146-
registerFavoritesView()
163+
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([
164+
new Folder({
165+
id: 42,
166+
root: '/files/admin',
167+
source: 'http://nextcloud.local/remote.php/dav/files/admin/Foo/Bar',
168+
owner: 'admin',
169+
}),
170+
]))
171+
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))
172+
173+
await registerFavoritesView()
147174
let favoritesView = Navigation.views.find(view => view.id === 'favorites')
148175
let favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')
149176

@@ -184,10 +211,10 @@ describe('Dynamic update of favorite folders', () => {
184211

185212
test('Renaming a favorite folder updates the navigation', async () => {
186213
vi.spyOn(eventBus, 'emit')
187-
vi.spyOn(initialState, 'loadState').mockReturnValue([])
188-
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))
214+
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([]))
215+
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))
189216

190-
registerFavoritesView()
217+
await registerFavoritesView()
191218
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
192219
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')
193220

apps/files/src/views/favorites.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,27 @@
55
import type { Folder, Node } from '@nextcloud/files'
66

77
import { subscribe } from '@nextcloud/event-bus'
8-
import { FileType, View, getNavigation } from '@nextcloud/files'
9-
import { loadState } from '@nextcloud/initial-state'
8+
import { FileType, View, getFavoriteNodes, getNavigation } from '@nextcloud/files'
109
import { getLanguage, translate as t } from '@nextcloud/l10n'
11-
import { basename } from 'path'
10+
import { client } from '../services/WebdavClient.ts'
1211
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
1312
import StarSvg from '@mdi/svg/svg/star.svg?raw'
1413

1514
import { getContents } from '../services/Favorites'
1615
import { hashCode } from '../utils/hashUtils'
1716
import logger from '../logger'
1817

19-
// The return type of the initial state
20-
interface IFavoriteFolder {
21-
fileid: number
22-
path: string
23-
}
24-
25-
export const generateFavoriteFolderView = function(folder: IFavoriteFolder, index = 0): View {
18+
const generateFavoriteFolderView = function(folder: Folder, index = 0): View {
2619
return new View({
2720
id: generateIdFromPath(folder.path),
28-
name: basename(folder.path),
21+
name: folder.displayname,
2922

3023
icon: FolderSvg,
3124
order: index,
25+
3226
params: {
3327
dir: folder.path,
34-
fileid: folder.fileid.toString(),
28+
fileid: String(folder.fileid),
3529
view: 'favorites',
3630
},
3731

@@ -43,16 +37,11 @@ export const generateFavoriteFolderView = function(folder: IFavoriteFolder, inde
4337
})
4438
}
4539

46-
export const generateIdFromPath = function(path: string): string {
40+
const generateIdFromPath = function(path: string): string {
4741
return `favorite-${hashCode(path)}`
4842
}
4943

50-
export default () => {
51-
// Load state in function for mock testing purposes
52-
const favoriteFolders = loadState<IFavoriteFolder[]>('files', 'favoriteFolders', [])
53-
const favoriteFoldersViews = favoriteFolders.map((folder, index) => generateFavoriteFolderView(folder, index)) as View[]
54-
logger.debug('Generating favorites view', { favoriteFolders })
55-
44+
export const registerFavoritesView = async () => {
5645
const Navigation = getNavigation()
5746
Navigation.register(new View({
5847
id: 'favorites',
@@ -70,6 +59,9 @@ export default () => {
7059
getContents,
7160
}))
7261

62+
const favoriteFolders = (await getFavoriteNodes(client)).filter(node => node.type === FileType.Folder) as Folder[]
63+
const favoriteFoldersViews = favoriteFolders.map((folder, index) => generateFavoriteFolderView(folder, index)) as View[]
64+
logger.debug('Generating favorites view', { favoriteFolders })
7365
favoriteFoldersViews.forEach(view => Navigation.register(view))
7466

7567
/**
@@ -137,16 +129,15 @@ export default () => {
137129

138130
// Add a folder to the favorites paths array and update the views
139131
const addToFavorites = function(node: Folder) {
140-
const newFavoriteFolder: IFavoriteFolder = { path: node.path, fileid: node.fileid! }
141-
const view = generateFavoriteFolderView(newFavoriteFolder)
132+
const view = generateFavoriteFolderView(node)
142133

143134
// Skip if already exists
144135
if (favoriteFolders.find((folder) => folder.path === node.path)) {
145136
return
146137
}
147138

148139
// Update arrays
149-
favoriteFolders.push(newFavoriteFolder)
140+
favoriteFolders.push(node)
150141
favoriteFoldersViews.push(view)
151142

152143
// Update and sort views

apps/files/tests/Controller/ViewControllerTest.php

Lines changed: 16 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
namespace OCA\Files\Tests\Controller;
99

1010
use OC\Files\FilenameValidator;
11-
use OCA\Files\Activity\Helper;
1211
use OCA\Files\Controller\ViewController;
1312
use OCA\Files\Service\UserConfig;
1413
use OCA\Files\Service\ViewConfig;
@@ -26,7 +25,7 @@
2625
use OCP\IURLGenerator;
2726
use OCP\IUser;
2827
use OCP\IUserSession;
29-
use OCP\Share\IManager;
28+
use PHPUnit\Framework\MockObject\MockObject;
3029
use Test\TestCase;
3130

3231
/**
@@ -35,38 +34,21 @@
3534
* @package OCA\Files\Tests\Controller
3635
*/
3736
class ViewControllerTest extends TestCase {
38-
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
39-
private $request;
40-
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
41-
private $urlGenerator;
42-
/** @var IL10N */
43-
private $l10n;
44-
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
45-
private $config;
46-
/** @var IEventDispatcher */
47-
private $eventDispatcher;
48-
/** @var ViewController|\PHPUnit\Framework\MockObject\MockObject */
49-
private $viewController;
50-
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject */
51-
private $user;
52-
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
53-
private $userSession;
54-
/** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
55-
private $appManager;
56-
/** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
57-
private $rootFolder;
58-
/** @var Helper|\PHPUnit\Framework\MockObject\MockObject */
59-
private $activityHelper;
60-
/** @var IInitialState|\PHPUnit\Framework\MockObject\MockObject */
61-
private $initialState;
62-
/** @var ITemplateManager|\PHPUnit\Framework\MockObject\MockObject */
63-
private $templateManager;
64-
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
65-
private $shareManager;
66-
/** @var UserConfig|\PHPUnit\Framework\MockObject\MockObject */
67-
private $userConfig;
68-
/** @var ViewConfig|\PHPUnit\Framework\MockObject\MockObject */
69-
private $viewConfig;
37+
private IRequest&MockObject $request;
38+
private IURLGenerator&MockObject $urlGenerator;
39+
private IL10N&MockObject $l10n;
40+
private IConfig&MockObject $config;
41+
private IEventDispatcher $eventDispatcher;
42+
private IUser&MockObject $user;
43+
private IUserSession&MockObject $userSession;
44+
private IAppManager&MockObject $appManager;
45+
private IRootFolder&MockObject $rootFolder;
46+
private IInitialState&MockObject $initialState;
47+
private ITemplateManager&MockObject $templateManager;
48+
private UserConfig&MockObject $userConfig;
49+
private ViewConfig&MockObject $viewConfig;
50+
51+
private ViewController&MockObject $viewController;
7052

7153
protected function setUp(): void {
7254
parent::setUp();
@@ -85,7 +67,6 @@ protected function setUp(): void {
8567
->method('getUser')
8668
->willReturn($this->user);
8769
$this->rootFolder = $this->getMockBuilder('\OCP\Files\IRootFolder')->getMock();
88-
$this->activityHelper = $this->createMock(Helper::class);
8970
$this->initialState = $this->createMock(IInitialState::class);
9071
$this->templateManager = $this->createMock(ITemplateManager::class);
9172
$this->userConfig = $this->createMock(UserConfig::class);
@@ -104,7 +85,6 @@ protected function setUp(): void {
10485
$this->userSession,
10586
$this->appManager,
10687
$this->rootFolder,
107-
$this->activityHelper,
10888
$this->initialState,
10989
$this->templateManager,
11090
$this->userConfig,
@@ -162,18 +142,6 @@ public function testIndexWithRegularBrowser() {
162142
$policy->addAllowedFrameDomain('\'self\'');
163143
$expected->setContentSecurityPolicy($policy);
164144

165-
$this->activityHelper->method('getFavoriteFilePaths')
166-
->with($this->user->getUID())
167-
->willReturn([
168-
'item' => [],
169-
'folders' => [
170-
'/test1',
171-
'/test2/',
172-
'/test3/sub4',
173-
'/test5/sub6/',
174-
],
175-
]);
176-
177145
$this->assertEquals($expected, $this->viewController->index('MyDir', 'MyView'));
178146
}
179147

dist/files-init.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/files-init.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)