diff --git a/apps/files/src/actions/favoriteAction.spec.ts b/apps/files/src/actions/favoriteAction.spec.ts index f1e2326695c58..6687c89db30ca 100644 --- a/apps/files/src/actions/favoriteAction.spec.ts +++ b/apps/files/src/actions/favoriteAction.spec.ts @@ -7,7 +7,7 @@ import type { IFolder, IView } from '@nextcloud/files' import axios from '@nextcloud/axios' import * as eventBus from '@nextcloud/event-bus' -import { File, Permission } from '@nextcloud/files' +import { File, Folder, Permission } from '@nextcloud/files' import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest' import { logger } from '../utils/logger.ts' import { action } from './favoriteAction.ts' @@ -26,6 +26,20 @@ const favoriteView = { name: 'Favorites', } as IView +const favoritesRootFolder = new Folder({ + id: 0, + source: 'http://localhost/remote.php/dav/files/admin', + owner: 'admin', + root: '/files/admin', +}) + +const nestedFolder = new Folder({ + id: 2, + source: 'http://localhost/remote.php/dav/files/admin/Foo/Bar', + owner: 'admin', + root: '/files/admin', +}) + // Mock webroot variable beforeAll(() => { window.OC = { @@ -272,7 +286,7 @@ describe('Favorite action execute tests', () => { const exec = await action.exec({ nodes: [file], view: favoriteView, - folder: {} as IFolder, + folder: favoritesRootFolder, contents: [], }) @@ -289,6 +303,38 @@ describe('Favorite action execute tests', () => { expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', file) }) + test('Favorite triggers node removal if favorite view root even for nested files', async () => { + vi.spyOn(axios, 'post') + vi.spyOn(eventBus, 'emit') + + const file = new File({ + id: 1, + source: 'http://localhost/remote.php/dav/files/admin/Foo/Bar/foobar.txt', + root: '/files/admin', + owner: 'admin', + mime: 'text/plain', + attributes: { + favorite: 1, + }, + }) + + const exec = await action.exec({ + nodes: [file], + view: favoriteView, + folder: favoritesRootFolder, + contents: [], + }) + + expect(exec).toBe(true) + + expect(axios.post).toBeCalledTimes(1) + expect(axios.post).toBeCalledWith('/index.php/apps/files/api/v1/files/Foo/Bar/foobar.txt', { tags: [] }) + + expect(file.attributes.favorite).toBe(0) + expect(eventBus.emit).toHaveBeenCalledWith('files:node:deleted', file) + expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', file) + }) + test('Favorite does NOT triggers node removal if favorite view but NOT root dir', async () => { vi.spyOn(axios, 'post') vi.spyOn(eventBus, 'emit') @@ -307,7 +353,7 @@ describe('Favorite action execute tests', () => { const exec = await action.exec({ nodes: [file], view: favoriteView, - folder: {} as IFolder, + folder: nestedFolder, contents: [], }) @@ -321,6 +367,7 @@ describe('Favorite action execute tests', () => { expect(file.attributes.favorite).toBe(0) expect(eventBus.emit).toHaveBeenCalled() expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file) + expect(eventBus.emit).not.toHaveBeenCalledWith('files:node:deleted', file) }) test('Favorite fails and show error', async () => { diff --git a/apps/files/src/actions/favoriteAction.ts b/apps/files/src/actions/favoriteAction.ts index f3b109871b2f0..668bbad5e3e3d 100644 --- a/apps/files/src/actions/favoriteAction.ts +++ b/apps/files/src/actions/favoriteAction.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { IFileAction, INode, IView } from '@nextcloud/files' +import type { IFileAction, IFolder, INode, IView } from '@nextcloud/files' import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw' import StarSvg from '@mdi/svg/svg/star.svg?raw' @@ -47,11 +47,11 @@ export const action: IFileAction = { && nodes.every((node) => node.permissions !== Permission.NONE) }, - async exec({ nodes, view }): Promise { + async exec({ nodes, view, folder }): Promise { const willFavorite = shouldFavorite([nodes[0]]) - return await favoriteNode(nodes[0], view, willFavorite) + return await favoriteNode(nodes[0], view, willFavorite, folder) }, - async execBatch({ nodes, view }): Promise { + async execBatch({ nodes, view, folder }): Promise { const willFavorite = shouldFavorite(nodes) // Map each node to a promise that resolves with the result of exec(node) @@ -60,7 +60,7 @@ export const action: IFileAction = { const promise = new Promise((resolve) => { queue.add(async () => { try { - await favoriteNode(node, view, willFavorite) + await favoriteNode(node, view, willFavorite, folder) resolve(true) } catch (error) { logger.error('Error while adding file to favorite', { error, source: node.source, node }) @@ -88,8 +88,9 @@ export const action: IFileAction = { * @param node - The node to favorite/unfavorite * @param view - The current view * @param willFavorite - Whether to favorite or unfavorite the node + * @param folder - The currently open folder */ -export async function favoriteNode(node: INode, view: IView, willFavorite: boolean): Promise { +export async function favoriteNode(node: INode, view: IView, willFavorite: boolean, folder?: IFolder): Promise { try { // TODO: migrate to webdav tags plugin const url = generateUrl('/apps/files/api/v1/files') + encodePath(node.path) @@ -99,10 +100,8 @@ export async function favoriteNode(node: INode, view: IView, willFavorite: boole : [], }) - // Let's delete if we are in the favourites view - // AND if it is removed from the user favorites - // AND it's in the root of the favorites view - if (view.id === 'favorites' && !willFavorite && node.dirname === '/') { + // Remove from the virtual favorites listing, not when browsing a real folder + if (view.id === 'favorites' && !willFavorite && isFavoritesRoot(folder)) { emit('files:node:deleted', node) } @@ -133,3 +132,12 @@ export async function favoriteNode(node: INode, view: IView, willFavorite: boole function shouldFavorite(nodes: INode[]): boolean { return nodes.some((node) => node.attributes.favorite !== 1) } + +/** + * Whether the current folder is the virtual root of the favorites view. + * + * @param folder - The currently open folder + */ +function isFavoritesRoot(folder?: IFolder): boolean { + return !folder?.path || folder.path === '/' +} diff --git a/apps/files/src/actions/sidebarFavoriteAction.ts b/apps/files/src/actions/sidebarFavoriteAction.ts index ba1c0d3b5f7ba..faf41b0368b46 100644 --- a/apps/files/src/actions/sidebarFavoriteAction.ts +++ b/apps/files/src/actions/sidebarFavoriteAction.ts @@ -35,8 +35,8 @@ export function registerSidebarFavoriteAction() { return starOutlineSvg }, - onClick({ node, view }) { - favoriteNode(node, view, !node.attributes.favorite) + onClick({ node, view, folder }) { + favoriteNode(node, view, !node.attributes.favorite, folder) }, }) } diff --git a/apps/files/src/views/favorites.spec.ts b/apps/files/src/views/favorites.spec.ts index c36f0abce1e6c..a1c488b8f1cc8 100644 --- a/apps/files/src/views/favorites.spec.ts +++ b/apps/files/src/views/favorites.spec.ts @@ -202,7 +202,8 @@ describe('Dynamic update of favorite folders', () => { contents: [], }) - expect(eventBus.emit).toHaveBeenCalledTimes(2) + expect(eventBus.emit).toHaveBeenCalledTimes(3) + expect(eventBus.emit).toHaveBeenCalledWith('files:node:deleted', folder) expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', folder) expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder) expect(fo).toHaveBeenCalled()