refactor(files): migrate favorite sidebar action to new Sidebar API

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2025-12-29 16:24:12 +01:00
parent 7077685bf8
commit f9a137ea87
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
10 changed files with 107 additions and 30 deletions

View file

@ -217,7 +217,7 @@ describe('Favorite action execute tests', () => {
// Check node change propagation
expect(file.attributes.favorite).toBe(1)
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toHaveBeenCalled()
expect(eventBus.emit).toBeCalledWith('files:favorites:added', file)
})
@ -251,7 +251,7 @@ describe('Favorite action execute tests', () => {
// Check node change propagation
expect(file.attributes.favorite).toBe(0)
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toHaveBeenCalled()
expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
})
@ -285,9 +285,9 @@ describe('Favorite action execute tests', () => {
// Check node change propagation
expect(file.attributes.favorite).toBe(0)
expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:favorites:removed', file)
expect(eventBus.emit).toHaveBeenCalled()
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 () => {
@ -320,7 +320,7 @@ describe('Favorite action execute tests', () => {
// Check node change propagation
expect(file.attributes.favorite).toBe(0)
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toHaveBeenCalled()
expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
})

View file

@ -1,8 +1,9 @@
/**
/*
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node, View } from '@nextcloud/files'
import type { INode, IView } from '@nextcloud/files'
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
import StarSvg from '@mdi/svg/svg/star.svg?raw'
@ -26,17 +27,18 @@ const queue = new PQueue({ concurrency: 5 })
*
* @param nodes - The nodes to check
*/
function shouldFavorite(nodes: Node[]): boolean {
function shouldFavorite(nodes: INode[]): boolean {
return nodes.some((node) => node.attributes.favorite !== 1)
}
/**
* Favorite or unfavorite a node
*
* @param node
* @param view
* @param willFavorite
* @param node - The node to favorite/unfavorite
* @param view - The current view
* @param willFavorite - Whether to favorite or unfavorite the node
*/
export async function favoriteNode(node: Node, view: View, willFavorite: boolean): Promise<boolean> {
export async function favoriteNode(node: INode, view: IView, willFavorite: boolean): Promise<boolean> {
try {
// TODO: migrate to webdav tags plugin
const url = generateUrl('/apps/files/api/v1/files') + encodePath(node.path)
@ -55,6 +57,7 @@ export async function favoriteNode(node: Node, view: View, willFavorite: boolean
// Update the node webdav attribute
Vue.set(node.attributes, 'favorite', willFavorite ? 1 : 0)
emit('files:node:updated', node)
// Dispatch event to whoever is interested
if (willFavorite) {

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import starOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
import starSvg from '@mdi/svg/svg/star.svg?raw'
import { registerSidebarAction } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { favoriteNode } from './favoriteAction.ts'
/**
* Register the favorite/unfavorite action in the sidebar
*/
export function registerSidebarFavoriteAction() {
registerSidebarAction({
id: 'files-favorite',
order: 0,
enabled({ node }) {
return node.isDavResource && node.root.startsWith('/files/')
},
displayName({ node }) {
if (node.attributes.favorite) {
return t('files', 'Unfavorite')
}
return t('files', 'Favorite')
},
iconSvgInline({ node }) {
if (node.attributes.favorite) {
return starSvg
}
return starOutlineSvg
},
onClick({ node, view }) {
favoriteNode(node, view, !node.attributes.favorite)
},
})
}

View file

@ -4,6 +4,7 @@
*/
import type { View } from '@nextcloud/files'
import type { Mock } from 'vitest'
import type { Location } from 'vue-router'
import axios from '@nextcloud/axios'
@ -143,6 +144,9 @@ describe('HotKeysService testing', () => {
})
it('Pressing s should toggle favorite', () => {
(favoriteAction.enabled as Mock).mockReturnValue(true);
(favoriteAction.exec as Mock).mockImplementationOnce(() => Promise.resolve(null))
vi.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve())
dispatchEvent({ key: 's', code: 'KeyS' })
@ -152,7 +156,6 @@ describe('HotKeysService testing', () => {
dispatchEvent({ key: 's', code: 'KeyS', shiftKey: true })
dispatchEvent({ key: 's', code: 'KeyS', metaKey: true })
expect(favoriteAction.enabled).toHaveReturnedWith(true)
expect(favoriteAction.exec).toHaveBeenCalledOnce()
})

View file

@ -16,6 +16,7 @@ import { action as openInFilesAction } from './actions/openInFilesAction.ts'
import { action as editLocallyAction } from './actions/openLocallyAction.ts'
import { action as renameAction } from './actions/renameAction.ts'
import { action as sidebarAction } from './actions/sidebarAction.ts'
import { registerSidebarFavoriteAction } from './actions/sidebarFavoriteAction.ts'
import { action as viewInFolderAction } from './actions/viewInFolderAction.ts'
import { registerFilenameFilter } from './filters/FilenameFilter.ts'
import { registerHiddenFilesFilter } from './filters/HiddenFilesFilter.ts'
@ -69,6 +70,9 @@ registerModifiedFilter()
registerFilenameFilter()
registerFilterToSearchToggle()
// Register sidebar action
registerSidebarFavoriteAction()
// Register preview service worker
registerPreviewServiceWorker()

View file

@ -10,8 +10,9 @@ import { getClient, getDefaultPropfind, getRootPath, resultToNode } from '@nextc
export const client = getClient()
/**
* Fetches a node from the given path
*
* @param path
* @param path - The path to fetch the node from
*/
export async function fetchNode(path: string): Promise<Node> {
const propfindPayload = getDefaultPropfind()

View file

@ -130,9 +130,9 @@ describe('Favorites view definition', () => {
describe('Dynamic update of favorite folders', () => {
let Navigation
beforeEach(() => {
vi.restoreAllMocks()
delete window._nc_navigation
Navigation = getNavigation()
})
@ -167,8 +167,9 @@ describe('Dynamic update of favorite folders', () => {
contents: [],
})
expect(eventBus.emit).toHaveBeenCalledTimes(1)
expect(eventBus.emit).toHaveBeenCalledTimes(2)
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
})
test('Remove a favorite folder remove the entry from the navigation column', async () => {
@ -213,8 +214,9 @@ describe('Dynamic update of favorite folders', () => {
contents: [],
})
expect(eventBus.emit).toHaveBeenCalledTimes(1)
expect(eventBus.emit).toHaveBeenCalledTimes(2)
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', folder)
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
expect(fo).toHaveBeenCalled()
favoritesView = Navigation.views.find((view) => view.id === 'favorites')
@ -257,7 +259,8 @@ describe('Dynamic update of favorite folders', () => {
folder: {} as NcFolder,
contents: [],
})
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:favorites:added', folder)
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
// Create a folder with the same id but renamed
const renamedFolder = new Folder({
@ -269,6 +272,6 @@ describe('Dynamic update of favorite folders', () => {
// Exec the rename action
eventBus.emit('files:node:renamed', renamedFolder)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:renamed', renamedFolder)
expect(eventBus.emit).toHaveBeenCalledWith('files:node:renamed', renamedFolder)
})
})

View file

@ -1,3 +1,8 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { IFolder, INode, IView } from '@nextcloud/files'

View file

@ -33,9 +33,10 @@ export function getActionEntryForFileId(fileid: number, actionId: string) {
* @param actionId
*/
export function getActionEntryForFile(file: string, actionId: string) {
return getActionButtonForFile(file)
getActionButtonForFile(file)
.should('have.attr', 'aria-controls')
.then((menuId) => cy.get(`#${menuId}`).find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
return cy.findByRole('menu')
.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
}
/**

View file

@ -5,7 +5,7 @@
import type { User } from '@nextcloud/e2e-test-server/cypress'
import { getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts'
import { closeSidebar, getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts'
describe('files: Favorites', { testIsolation: true }, () => {
let user: User
@ -110,29 +110,44 @@ describe('files: Favorites', { testIsolation: true }, () => {
.contains('new folder')
.should('not.exist')
cy.intercept('PROPPATCH', '**/remote.php/dav/files/*/new%20folder').as('addToFavorites')
cy.intercept('POST', '**/apps/files/api/v1/files/new%20folder').as('addToFavorites')
// open sidebar
triggerActionForFile('new folder', 'details')
// open actions
cy.get('[data-cy-sidebar]')
.findByRole('button', { name: 'Actions' })
.click()
// trigger menu button
cy.findAllByRole('menu')
.findByRole('menuitem', { name: 'Add to favorites' })
.findByRole('menuitem', { name: 'Favorite' })
.should('be.visible')
.click()
cy.wait('@addToFavorites')
closeSidebar()
// See favorites star
getRowForFile('new folder')
.findByRole('img', { name: 'Favorite' })
.should('be.visible')
// See folder in navigation
cy.get('[data-cy-files-navigation-item="favorites"]')
cy.reload()
// can unfavorite
triggerActionForFile('new folder', 'details')
cy.get('[data-cy-sidebar]')
.findByRole('button', { name: 'Actions' })
.click()
// trigger menu button
cy.findAllByRole('menu')
.findByRole('menuitem', { name: 'Unfavorite' })
.should('be.visible')
.contains('new folder')
.should('exist')
.click()
cy.wait('@addToFavorites')
closeSidebar()
getRowForFile('new folder')
.findByRole('img', { name: 'Favorite' })
.should('not.exist')
})
})