Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(files): Allow more than 50 favorite views #47400

Merged
merged 4 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions apps/files/lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
namespace OCA\Files\Controller;

use OC\Files\FilenameValidator;
use OCA\Files\Activity\Helper;
use OCA\Files\AppInfo\Application;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadSearchPlugins;
Expand Down Expand Up @@ -54,7 +53,6 @@ public function __construct(
private IUserSession $userSession,
private IAppManager $appManager,
private IRootFolder $rootFolder,
private Helper $activityHelper,
private IInitialState $initialState,
private ITemplateManager $templateManager,
private UserConfig $userConfig,
Expand Down Expand Up @@ -146,18 +144,6 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal

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

// Get all the user favorites to create a submenu
try {
$userFolder = $this->rootFolder->getUserFolder($userId);
$favElements = $this->activityHelper->getFavoriteNodes($userId, true);
$favElements = array_map(fn (Folder $node) => [
'fileid' => $node->getId(),
'path' => $userFolder->getRelativePath($node->getPath()),
], $favElements);
} catch (\RuntimeException $e) {
$favElements = [];
}

// If the file doesn't exists in the folder and
// exists in only one occurrence, redirect to that file
// in the correct folder
Expand Down Expand Up @@ -187,7 +173,6 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal
$this->initialState->provideInitialState('storageStats', $storageInfo);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
$this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
$this->initialState->provideInitialState('favoriteFolders', $favElements);

// File sorting user config
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
Expand Down
2 changes: 1 addition & 1 deletion apps/files/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { entry as newFolderEntry } from './newMenu/newFolder.ts'
import { entry as newTemplatesFolder } from './newMenu/newTemplatesFolder.ts'
import { registerTemplateEntries } from './newMenu/newFromTemplate.ts'

import registerFavoritesView from './views/favorites'
import { registerFavoritesView } from './views/favorites.ts'
import registerRecentView from './views/recent'
import registerPersonalFilesView from './views/personal-files'
import registerFilesView from './views/files'
Expand Down
75 changes: 51 additions & 24 deletions apps/files/src/views/favorites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { Folder, Navigation, getNavigation } from '@nextcloud/files'

import type { Folder as CFolder, Navigation } from '@nextcloud/files'

import * as filesUtils from '@nextcloud/files'
import { CancelablePromise } from 'cancelable-promise'
import { basename } from 'path'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import * as eventBus from '@nextcloud/event-bus'
import * as initialState from '@nextcloud/initial-state'

import { action } from '../actions/favoriteAction'
import * as favoritesService from '../services/Favorites'
import registerFavoritesView from './favorites'
import { registerFavoritesView } from './favorites'

const { Folder, getNavigation } = filesUtils

vi.mock('@nextcloud/axios')

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

test('Default empty favorite view', () => {
test('Default empty favorite view', async () => {
vi.spyOn(eventBus, 'subscribe')
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))

registerFavoritesView()
await registerFavoritesView()
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')

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

test('Default with favorites', () => {
test('Default with favorites', async () => {
const favoriteFolders = [
{ fileid: 1, path: '/foo' },
{ fileid: 2, path: '/bar' },
{ fileid: 3, path: '/foo/bar' },
new Folder({
id: 1,
root: '/files/admin',
source: 'http://nextcloud.local/remote.php/dav/files/admin/foo',
owner: 'admin',
}),
new Folder({
id: 2,
root: '/files/admin',
source: 'http://nextcloud.local/remote.php/dav/files/admin/bar',
owner: 'admin',
}),
new Folder({
id: 3,
root: '/files/admin',
source: 'http://nextcloud.local/remote.php/dav/files/admin/foo/bar',
owner: 'admin',
}),
]
vi.spyOn(initialState, 'loadState').mockReturnValue(favoriteFolders)
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve(favoriteFolders))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))

registerFavoritesView()
await registerFavoritesView()
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')

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

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

registerFavoritesView()
await registerFavoritesView()
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')

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

test('Remove a favorite folder remove the entry from the navigation column', async () => {
vi.spyOn(eventBus, 'emit')
vi.spyOn(initialState, 'loadState').mockReturnValue([{ fileid: 42, path: '/Foo/Bar' }])
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as Folder, contents: [] }))

registerFavoritesView()
vi.spyOn(filesUtils, 'getFavoriteNodes').mockReturnValue(CancelablePromise.resolve([
new Folder({
id: 42,
root: '/files/admin',
source: 'http://nextcloud.local/remote.php/dav/files/admin/Foo/Bar',
owner: 'admin',
}),
]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(CancelablePromise.resolve({ folder: {} as CFolder, contents: [] }))

await registerFavoritesView()
let favoritesView = Navigation.views.find(view => view.id === 'favorites')
let favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')

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

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

registerFavoritesView()
await registerFavoritesView()
const favoritesView = Navigation.views.find(view => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites')

Expand Down
35 changes: 13 additions & 22 deletions apps/files/src/views/favorites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,27 @@
import type { Folder, Node } from '@nextcloud/files'

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

import { getContents } from '../services/Favorites'
import { hashCode } from '../utils/hashUtils'
import logger from '../logger'

// The return type of the initial state
interface IFavoriteFolder {
fileid: number
path: string
}

export const generateFavoriteFolderView = function(folder: IFavoriteFolder, index = 0): View {
const generateFavoriteFolderView = function(folder: Folder, index = 0): View {
return new View({
id: generateIdFromPath(folder.path),
name: basename(folder.path),
name: folder.displayname,

icon: FolderSvg,
order: index,

params: {
dir: folder.path,
fileid: folder.fileid.toString(),
fileid: String(folder.fileid),
view: 'favorites',
},

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

export const generateIdFromPath = function(path: string): string {
const generateIdFromPath = function(path: string): string {
return `favorite-${hashCode(path)}`
}

export default () => {
// Load state in function for mock testing purposes
const favoriteFolders = loadState<IFavoriteFolder[]>('files', 'favoriteFolders', [])
const favoriteFoldersViews = favoriteFolders.map((folder, index) => generateFavoriteFolderView(folder, index)) as View[]
logger.debug('Generating favorites view', { favoriteFolders })

export const registerFavoritesView = async () => {
const Navigation = getNavigation()
Navigation.register(new View({
id: 'favorites',
Expand All @@ -70,6 +59,9 @@ export default () => {
getContents,
}))

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

/**
Expand Down Expand Up @@ -137,16 +129,15 @@ export default () => {

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

// Skip if already exists
if (favoriteFolders.find((folder) => folder.path === node.path)) {
return
}

// Update arrays
favoriteFolders.push(newFavoriteFolder)
favoriteFolders.push(node)
favoriteFoldersViews.push(view)

// Update and sort views
Expand Down
64 changes: 16 additions & 48 deletions apps/files/tests/Controller/ViewControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
namespace OCA\Files\Tests\Controller;

use OC\Files\FilenameValidator;
use OCA\Files\Activity\Helper;
use OCA\Files\Controller\ViewController;
use OCA\Files\Service\UserConfig;
use OCA\Files\Service\ViewConfig;
Expand All @@ -26,7 +25,7 @@
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Share\IManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

/**
Expand All @@ -35,38 +34,21 @@
* @package OCA\Files\Tests\Controller
*/
class ViewControllerTest extends TestCase {
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
private $request;
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
private $urlGenerator;
/** @var IL10N */
private $l10n;
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
private $config;
/** @var IEventDispatcher */
private $eventDispatcher;
/** @var ViewController|\PHPUnit\Framework\MockObject\MockObject */
private $viewController;
/** @var IUser|\PHPUnit\Framework\MockObject\MockObject */
private $user;
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
private $userSession;
/** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
private $appManager;
/** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
private $rootFolder;
/** @var Helper|\PHPUnit\Framework\MockObject\MockObject */
private $activityHelper;
/** @var IInitialState|\PHPUnit\Framework\MockObject\MockObject */
private $initialState;
/** @var ITemplateManager|\PHPUnit\Framework\MockObject\MockObject */
private $templateManager;
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
private $shareManager;
/** @var UserConfig|\PHPUnit\Framework\MockObject\MockObject */
private $userConfig;
/** @var ViewConfig|\PHPUnit\Framework\MockObject\MockObject */
private $viewConfig;
private IRequest&MockObject $request;
private IURLGenerator&MockObject $urlGenerator;
private IL10N&MockObject $l10n;
private IConfig&MockObject $config;
private IEventDispatcher $eventDispatcher;
private IUser&MockObject $user;
private IUserSession&MockObject $userSession;
private IAppManager&MockObject $appManager;
private IRootFolder&MockObject $rootFolder;
private IInitialState&MockObject $initialState;
private ITemplateManager&MockObject $templateManager;
private UserConfig&MockObject $userConfig;
private ViewConfig&MockObject $viewConfig;

private ViewController&MockObject $viewController;

protected function setUp(): void {
parent::setUp();
Expand All @@ -85,7 +67,6 @@ protected function setUp(): void {
->method('getUser')
->willReturn($this->user);
$this->rootFolder = $this->getMockBuilder('\OCP\Files\IRootFolder')->getMock();
$this->activityHelper = $this->createMock(Helper::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->templateManager = $this->createMock(ITemplateManager::class);
$this->userConfig = $this->createMock(UserConfig::class);
Expand All @@ -104,7 +85,6 @@ protected function setUp(): void {
$this->userSession,
$this->appManager,
$this->rootFolder,
$this->activityHelper,
$this->initialState,
$this->templateManager,
$this->userConfig,
Expand Down Expand Up @@ -162,18 +142,6 @@ public function testIndexWithRegularBrowser() {
$policy->addAllowedFrameDomain('\'self\'');
$expected->setContentSecurityPolicy($policy);

$this->activityHelper->method('getFavoriteFilePaths')
->with($this->user->getUID())
->willReturn([
'item' => [],
'folders' => [
'/test1',
'/test2/',
'/test3/sub4',
'/test5/sub6/',
],
]);

$this->assertEquals($expected, $this->viewController->index('MyDir', 'MyView'));
}

Expand Down
4 changes: 2 additions & 2 deletions dist/files-init.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-init.js.map

Large diffs are not rendered by default.

Loading