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

[stable28] fix(files): Make the navigation reactive to view changes and show also sub routes as active #43119

Merged
merged 6 commits into from
Jan 25, 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
64 changes: 42 additions & 22 deletions apps/files/lib/Activity/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0
*
Expand All @@ -23,30 +24,30 @@
namespace OCA\Files\Activity;

use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\ITagManager;

class Helper {
/** If a user has a lot of favorites the query might get too slow and long */
public const FAVORITE_LIMIT = 50;

/** @var ITagManager */
protected $tagManager;

/**
* @param ITagManager $tagManager
*/
public function __construct(ITagManager $tagManager) {
$this->tagManager = $tagManager;
public function __construct(
protected ITagManager $tagManager,
protected IRootFolder $rootFolder,
) {
}

/**
* Returns an array with the favorites
* Return an array with nodes marked as favorites
*
* @param string $user
* @return array
* @param string $user User ID
* @param bool $foldersOnly Only return folders (default false)
* @return Node[]
* @psalm-return ($foldersOnly is true ? Folder[] : Node[])
* @throws \RuntimeException when too many or no favorites where found
*/
public function getFavoriteFilePaths($user) {
public function getFavoriteNodes(string $user, bool $foldersOnly = false): array {
$tags = $this->tagManager->load('files', [], false, $user);
$favorites = $tags->getFavorites();

Expand All @@ -57,26 +58,45 @@ public function getFavoriteFilePaths($user) {
}

// Can not DI because the user is not known on instantiation
$rootFolder = \OC::$server->getUserFolder($user);
$folders = $items = [];
$userFolder = $this->rootFolder->getUserFolder($user);
$favoriteNodes = [];
foreach ($favorites as $favorite) {
$nodes = $rootFolder->getById($favorite);
$nodes = $userFolder->getById($favorite);
if (!empty($nodes)) {
/** @var \OCP\Files\Node $node */
$node = array_shift($nodes);
$path = substr($node->getPath(), strlen($user . '/files/'));

$items[] = $path;
if ($node instanceof Folder) {
$folders[] = $path;
if (!$foldersOnly || $node instanceof Folder) {
$favoriteNodes[] = $node;
}
}
}

if (empty($items)) {
if (empty($favoriteNodes)) {
throw new \RuntimeException('No favorites', 1);
}

return $favoriteNodes;
}

/**
* Returns an array with the favorites
*
* @param string $user
* @return array
* @throws \RuntimeException when too many or no favorites where found
*/
public function getFavoriteFilePaths(string $user): array {
$userFolder = $this->rootFolder->getUserFolder($user);
$nodes = $this->getFavoriteNodes($user);
$folders = $items = [];
foreach ($nodes as $node) {
$path = $userFolder->getRelativePath($node->getPath());

$items[] = $path;
if ($node instanceof Folder) {
$folders[] = $path;
}
}

return [
'items' => $items,
'folders' => $folders,
Expand Down
11 changes: 8 additions & 3 deletions apps/files/lib/Controller/ViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,14 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal

// Get all the user favorites to create a submenu
try {
$favElements = $this->activityHelper->getFavoriteFilePaths($userId);
$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['folders'] = [];
$favElements = [];
}

// If the file doesn't exists in the folder and
Expand Down Expand Up @@ -260,7 +265,7 @@ 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['folders'] ?? []);
$this->initialState->provideInitialState('favoriteFolders', $favElements);

// File sorting user config
$filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
Expand Down
5 changes: 2 additions & 3 deletions apps/files/src/actions/openFolderAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { join } from 'path'
import { Permission, Node, FileType, View, FileAction, DefaultType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
Expand Down Expand Up @@ -49,15 +48,15 @@ export const action = new FileAction({
&& (node.permissions & Permission.READ) !== 0
},

async exec(node: Node, view: View, dir: string) {
async exec(node: Node, view: View) {
if (!node || node.type !== FileType.Folder) {
return false
}

window.OCP.Files.Router.goToRoute(
null,
{ view: view.id, fileid: node.fileid },
{ dir: join(dir, node.basename) },
{ dir: node.path },
)
return null
},
Expand Down
3 changes: 2 additions & 1 deletion apps/files/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ Vue.use(PiniaVuePlugin)
const pinia = createPinia()

// Init Navigation Service
const Navigation = getNavigation()
// This only works with Vue 2 - with Vue 3 this will not modify the source but return just a oberserver
const Navigation = Vue.observable(getNavigation())
Vue.prototype.$navigation = Navigation

// Init Files App Settings Service
Expand Down
8 changes: 5 additions & 3 deletions apps/files/src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { RawLocation, Route } from 'vue-router'

import { generateUrl } from '@nextcloud/router'
import queryString from 'query-string'
import Router, { RawLocation, Route } from 'vue-router'
import Router from 'vue-router'
import Vue from 'vue'
import { ErrorHandler } from 'vue-router/types/router'

Expand All @@ -46,10 +48,10 @@ const router = new Router({
{
path: '/',
// Pretending we're using the default view
redirect: { name: 'filelist' },
redirect: { name: 'filelist', params: { view: 'files' } },
},
{
path: '/:view/:fileid?',
path: '/:view/:fileid(\\d+)?',
name: 'filelist',
props: true,
},
Expand Down
3 changes: 1 addition & 2 deletions apps/files/src/views/FilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,7 @@ export default defineComponent({
},

currentView(): View {
return (this.$navigation.active
|| this.$navigation.views.find(view => view.id === 'files')) as View
return this.$navigation.active || this.$navigation.views.find((view) => view.id === (this.$route.params?.view ?? 'files'))
},

/**
Expand Down
28 changes: 19 additions & 9 deletions apps/files/src/views/Navigation.vue
susnux marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
:key="view.id"
:allow-collapse="true"
:data-cy-files-navigation-item="view.id"
:exact="true"
:exact="useExactRouteMatching(view)"
:icon="view.iconClass"
:name="view.name"
:open="isExpanded(view)"
Expand All @@ -41,7 +41,7 @@
<NcAppNavigationItem v-for="child in childViews[view.id]"
:key="child.id"
:data-cy-files-navigation-item="child.id"
:exact="true"
:exact-path="true"
:icon="child.iconClass"
:name="child.name"
:to="generateToNavigation(child)">
Expand Down Expand Up @@ -128,7 +128,7 @@ export default {
},

currentView(): View {
return this.views.find(view => view.id === this.currentViewId)
return this.views.find(view => view.id === this.currentViewId)!
},

views(): View[] {
Expand All @@ -145,19 +145,19 @@ export default {
})
},

childViews(): View[] {
childViews(): Record<string, View[]> {
return this.views
// filter parent views
.filter(view => !!view.parent)
// create a map of parents and their children
.reduce((list, view) => {
list[view.parent] = [...(list[view.parent] || []), view]
list[view.parent!] = [...(list[view.parent!] || []), view]
// Sort children by order
list[view.parent].sort((a, b) => {
list[view.parent!].sort((a, b) => {
return a.order - b.order
})
return list
}, {})
}, {} as Record<string, View[]>)
},
},

Expand All @@ -180,6 +180,16 @@ export default {
},

methods: {
/**
* Only use exact route matching on routes with child views
* Because if a view does not have children (like the files view) then multiple routes might be matched for it
* Like for the 'files' view this does not work because of optional 'fileid' param so /files and /files/1234 are both in the 'files' view
* @param view The view to check
*/
useExactRouteMatching(view: View): boolean {
return this.childViews[view.id]?.length > 0
},

showView(view: View) {
// Closing any opened sidebar
window?.OCA?.Files?.Sidebar?.close?.()
Expand Down Expand Up @@ -215,8 +225,8 @@ export default {
*/
generateToNavigation(view: View) {
if (view.params) {
const { dir, fileid } = view.params
return { name: 'filelist', params: view.params, query: { dir, fileid } }
const { dir } = view.params
return { name: 'filelist', params: view.params, query: { dir } }
}
return { name: 'filelist', params: { view: view.id } }
},
Expand Down
13 changes: 7 additions & 6 deletions apps/files/src/views/favorites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ describe('Favorites view definition', () => {

test('Default with favorites', () => {
const favoriteFolders = [
'/foo',
'/bar',
'/foo/bar',
{ fileid: 1, path: '/foo' },
{ fileid: 2, path: '/bar' },
{ fileid: 3, path: '/foo/bar' },
]
jest.spyOn(initialState, 'loadState').mockReturnValue(favoriteFolders)
jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] }))
Expand All @@ -102,11 +102,12 @@ describe('Favorites view definition', () => {
const favoriteView = favoriteFoldersViews[index]
expect(favoriteView).toBeDefined()
expect(favoriteView?.id).toBeDefined()
expect(favoriteView?.name).toBe(basename(folder))
expect(favoriteView?.name).toBe(basename(folder.path))
expect(favoriteView?.icon).toBe('<svg>SvgMock</svg>')
expect(favoriteView?.order).toBe(index)
expect(favoriteView?.params).toStrictEqual({
dir: folder,
dir: folder.path,
fileid: folder.fileid.toString(),
view: 'favorites',
})
expect(favoriteView?.parent).toBe('favorites')
Expand Down Expand Up @@ -157,7 +158,7 @@ describe('Dynamic update of favourite folders', () => {
test('Remove a favorite folder remove the entry from the navigation column', async () => {
jest.spyOn(eventBus, 'emit')
jest.spyOn(eventBus, 'subscribe')
jest.spyOn(initialState, 'loadState').mockReturnValue(['/Foo/Bar'])
jest.spyOn(initialState, 'loadState').mockReturnValue([{ fileid: 42, path: '/Foo/Bar' }])
jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] }))

registerFavoritesView()
Expand Down
Loading
Loading