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

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

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);
Fixed Show fixed Hide fixed
} 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
5 changes: 2 additions & 3 deletions apps/files/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { createPinia, PiniaVuePlugin } from 'pinia'
import { getNavigation } from '@nextcloud/files'
import { getRequestToken } from '@nextcloud/auth'

import FilesListView from './views/FilesList.vue'
import NavigationView from './views/Navigation.vue'
import router from './router/router'
import RouterService from './services/RouterService'
import SettingsModel from './models/Setting.js'
Expand Down Expand Up @@ -35,7 +33,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())
Comment on lines +36 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random thought, wouldn't it be better to return reactive navigation from the 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
39 changes: 25 additions & 14 deletions apps/files/src/views/Navigation.vue
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 @@ -75,6 +75,8 @@
</template>

<script lang="ts">
import type { View } from '@nextcloud/files'

import { emit } from '@nextcloud/event-bus'
import { translate } from '@nextcloud/l10n'
import Cog from 'vue-material-design-icons/Cog.vue'
Expand All @@ -85,7 +87,6 @@ import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js
import { setPageHeading } from '../../../../core/src/OCP/accessibility.js'
import { useViewConfigStore } from '../store/viewConfig.ts'
import logger from '../logger.js'
import type { View } from '@nextcloud/files'
import NavigationQuota from '../components/NavigationQuota.vue'
import SettingsModal from './Settings.vue'

Expand Down Expand Up @@ -120,7 +121,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 @@ -137,27 +138,27 @@ 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]
susnux marked this conversation as resolved.
Show resolved Hide resolved
// 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[]>)
},
},

watch: {
currentView(view, oldView) {
if (view.id !== oldView?.id) {
this.$navigation.setActive(view)
logger.debug('Navigation changed', { id: view.id, view })
logger.debug(`Navigation changed from ${oldView.id} to ${view.id}`, { from: oldView, to: view })

this.showView(view)
}
Expand All @@ -172,6 +173,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 All @@ -183,7 +194,7 @@ export default {
/**
* Expand/collapse a a view with children and permanently
* save this setting in the server.
* @param view
* @param view View to toggle
*/
onToggleExpand(view: View) {
// Invert state
Expand All @@ -196,7 +207,7 @@ export default {
/**
* Check if a view is expanded by user config
* or fallback to the default value.
* @param view
* @param view View to check if expanded
*/
isExpanded(view: View): boolean {
return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean'
Expand All @@ -206,12 +217,12 @@ export default {

/**
* Generate the route to a view
* @param view
* @param view View to generate "to" navigation for
*/
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