Skip to content

Commit 6fcff8d

Browse files
committed
refactor(files): migrate favorite sidebar action to new Sidebar API
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 8ef905c commit 6fcff8d

10 files changed

Lines changed: 107 additions & 30 deletions

File tree

apps/files/src/actions/favoriteAction.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ describe('Favorite action execute tests', () => {
217217

218218
// Check node change propagation
219219
expect(file.attributes.favorite).toBe(1)
220-
expect(eventBus.emit).toBeCalledTimes(1)
220+
expect(eventBus.emit).toHaveBeenCalled()
221221
expect(eventBus.emit).toBeCalledWith('files:favorites:added', file)
222222
})
223223

@@ -251,7 +251,7 @@ describe('Favorite action execute tests', () => {
251251

252252
// Check node change propagation
253253
expect(file.attributes.favorite).toBe(0)
254-
expect(eventBus.emit).toBeCalledTimes(1)
254+
expect(eventBus.emit).toHaveBeenCalled()
255255
expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
256256
})
257257

@@ -285,9 +285,9 @@ describe('Favorite action execute tests', () => {
285285

286286
// Check node change propagation
287287
expect(file.attributes.favorite).toBe(0)
288-
expect(eventBus.emit).toBeCalledTimes(2)
289-
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file)
290-
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:favorites:removed', file)
288+
expect(eventBus.emit).toHaveBeenCalled()
289+
expect(eventBus.emit).toHaveBeenCalledWith('files:node:deleted', file)
290+
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', file)
291291
})
292292

293293
test('Favorite does NOT triggers node removal if favorite view but NOT root dir', async () => {
@@ -320,7 +320,7 @@ describe('Favorite action execute tests', () => {
320320

321321
// Check node change propagation
322322
expect(file.attributes.favorite).toBe(0)
323-
expect(eventBus.emit).toBeCalledTimes(1)
323+
expect(eventBus.emit).toHaveBeenCalled()
324324
expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
325325
})
326326

apps/files/src/actions/favoriteAction.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
/**
1+
/*
22
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
5-
import type { Node, View } from '@nextcloud/files'
5+
6+
import type { INode, IView } from '@nextcloud/files'
67

78
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
89
import StarSvg from '@mdi/svg/svg/star.svg?raw'
@@ -26,17 +27,18 @@ const queue = new PQueue({ concurrency: 5 })
2627
*
2728
* @param nodes - The nodes to check
2829
*/
29-
function shouldFavorite(nodes: Node[]): boolean {
30+
function shouldFavorite(nodes: INode[]): boolean {
3031
return nodes.some((node) => node.attributes.favorite !== 1)
3132
}
3233

3334
/**
35+
* Favorite or unfavorite a node
3436
*
35-
* @param node
36-
* @param view
37-
* @param willFavorite
37+
* @param node - The node to favorite/unfavorite
38+
* @param view - The current view
39+
* @param willFavorite - Whether to favorite or unfavorite the node
3840
*/
39-
export async function favoriteNode(node: Node, view: View, willFavorite: boolean): Promise<boolean> {
41+
export async function favoriteNode(node: INode, view: IView, willFavorite: boolean): Promise<boolean> {
4042
try {
4143
// TODO: migrate to webdav tags plugin
4244
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
5557

5658
// Update the node webdav attribute
5759
Vue.set(node.attributes, 'favorite', willFavorite ? 1 : 0)
60+
emit('files:node:updated', node)
5861

5962
// Dispatch event to whoever is interested
6063
if (willFavorite) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import starOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
7+
import starSvg from '@mdi/svg/svg/star.svg?raw'
8+
import { registerSidebarAction } from '@nextcloud/files'
9+
import { t } from '@nextcloud/l10n'
10+
import { favoriteNode } from './favoriteAction.ts'
11+
12+
/**
13+
* Register the favorite/unfavorite action in the sidebar
14+
*/
15+
export function registerSidebarFavoriteAction() {
16+
registerSidebarAction({
17+
id: 'files-favorite',
18+
order: 0,
19+
20+
enabled({ node }) {
21+
return node.isDavResource && node.root.startsWith('/files/')
22+
},
23+
24+
displayName({ node }) {
25+
if (node.attributes.favorite) {
26+
return t('files', 'Unfavorite')
27+
}
28+
return t('files', 'Favorite')
29+
},
30+
31+
iconSvgInline({ node }) {
32+
if (node.attributes.favorite) {
33+
return starSvg
34+
}
35+
return starOutlineSvg
36+
},
37+
38+
onClick({ node, view }) {
39+
favoriteNode(node, view, !node.attributes.favorite)
40+
},
41+
})
42+
}

apps/files/src/composables/useHotKeys.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import type { View } from '@nextcloud/files'
7+
import type { Mock } from 'vitest'
78
import type { Location } from 'vue-router'
89

910
import axios from '@nextcloud/axios'
@@ -143,6 +144,9 @@ describe('HotKeysService testing', () => {
143144
})
144145

145146
it('Pressing s should toggle favorite', () => {
147+
(favoriteAction.enabled as Mock).mockReturnValue(true);
148+
(favoriteAction.exec as Mock).mockImplementationOnce(() => Promise.resolve(null))
149+
146150
vi.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve())
147151
dispatchEvent({ key: 's', code: 'KeyS' })
148152

@@ -152,7 +156,6 @@ describe('HotKeysService testing', () => {
152156
dispatchEvent({ key: 's', code: 'KeyS', shiftKey: true })
153157
dispatchEvent({ key: 's', code: 'KeyS', metaKey: true })
154158

155-
expect(favoriteAction.enabled).toHaveReturnedWith(true)
156159
expect(favoriteAction.exec).toHaveBeenCalledOnce()
157160
})
158161

apps/files/src/init.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { action as openInFilesAction } from './actions/openInFilesAction.ts'
1616
import { action as editLocallyAction } from './actions/openLocallyAction.ts'
1717
import { action as renameAction } from './actions/renameAction.ts'
1818
import { action as sidebarAction } from './actions/sidebarAction.ts'
19+
import { registerSidebarFavoriteAction } from './actions/sidebarFavoriteAction.ts'
1920
import { action as viewInFolderAction } from './actions/viewInFolderAction.ts'
2021
import { registerFilenameFilter } from './filters/FilenameFilter.ts'
2122
import { registerHiddenFilesFilter } from './filters/HiddenFilesFilter.ts'
@@ -69,6 +70,9 @@ registerModifiedFilter()
6970
registerFilenameFilter()
7071
registerFilterToSearchToggle()
7172

73+
// Register sidebar action
74+
registerSidebarFavoriteAction()
75+
7276
// Register preview service worker
7377
registerPreviewServiceWorker()
7478

apps/files/src/services/WebdavClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import { getClient, getDefaultPropfind, getRootPath, resultToNode } from '@nextc
1010
export const client = getClient()
1111

1212
/**
13+
* Fetches a node from the given path
1314
*
14-
* @param path
15+
* @param path - The path to fetch the node from
1516
*/
1617
export async function fetchNode(path: string): Promise<Node> {
1718
const propfindPayload = getDefaultPropfind()

apps/files/src/views/favorites.spec.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,9 @@ describe('Favorites view definition', () => {
130130

131131
describe('Dynamic update of favorite folders', () => {
132132
let Navigation
133+
133134
beforeEach(() => {
134135
vi.restoreAllMocks()
135-
136136
delete window._nc_navigation
137137
Navigation = getNavigation()
138138
})
@@ -167,8 +167,9 @@ describe('Dynamic update of favorite folders', () => {
167167
contents: [],
168168
})
169169

170-
expect(eventBus.emit).toHaveBeenCalledTimes(1)
170+
expect(eventBus.emit).toHaveBeenCalledTimes(2)
171171
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
172+
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
172173
})
173174

174175
test('Remove a favorite folder remove the entry from the navigation column', async () => {
@@ -213,8 +214,9 @@ describe('Dynamic update of favorite folders', () => {
213214
contents: [],
214215
})
215216

216-
expect(eventBus.emit).toHaveBeenCalledTimes(1)
217+
expect(eventBus.emit).toHaveBeenCalledTimes(2)
217218
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', folder)
219+
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
218220
expect(fo).toHaveBeenCalled()
219221

220222
favoritesView = Navigation.views.find((view) => view.id === 'favorites')
@@ -257,7 +259,8 @@ describe('Dynamic update of favorite folders', () => {
257259
folder: {} as NcFolder,
258260
contents: [],
259261
})
260-
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:favorites:added', folder)
262+
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
263+
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
261264

262265
// Create a folder with the same id but renamed
263266
const renamedFolder = new Folder({
@@ -269,6 +272,6 @@ describe('Dynamic update of favorite folders', () => {
269272

270273
// Exec the rename action
271274
eventBus.emit('files:node:renamed', renamedFolder)
272-
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:renamed', renamedFolder)
275+
expect(eventBus.emit).toHaveBeenCalledWith('files:node:renamed', renamedFolder)
273276
})
274277
})

apps/files_sharing/src/views/FilesSidebarTab.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
16
<script setup lang="ts">
27
import type { IFolder, INode, IView } from '@nextcloud/files'
38

cypress/e2e/files/FilesUtils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ export function getActionEntryForFileId(fileid: number, actionId: string) {
3333
* @param actionId
3434
*/
3535
export function getActionEntryForFile(file: string, actionId: string) {
36-
return getActionButtonForFile(file)
36+
getActionButtonForFile(file)
3737
.should('have.attr', 'aria-controls')
38-
.then((menuId) => cy.get(`#${menuId}`).find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
38+
return cy.findByRole('menu')
39+
.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
3940
}
4041

4142
/**

cypress/e2e/files/favorites.cy.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import type { User } from '@nextcloud/e2e-test-server/cypress'
77

8-
import { getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts'
8+
import { closeSidebar, getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts'
99

1010
describe('files: Favorites', { testIsolation: true }, () => {
1111
let user: User
@@ -110,29 +110,44 @@ describe('files: Favorites', { testIsolation: true }, () => {
110110
.contains('new folder')
111111
.should('not.exist')
112112

113-
cy.intercept('PROPPATCH', '**/remote.php/dav/files/*/new%20folder').as('addToFavorites')
113+
cy.intercept('POST', '**/apps/files/api/v1/files/new%20folder').as('addToFavorites')
114114
// open sidebar
115115
triggerActionForFile('new folder', 'details')
116-
// open actions
117116
cy.get('[data-cy-sidebar]')
118117
.findByRole('button', { name: 'Actions' })
119118
.click()
120119
// trigger menu button
121120
cy.findAllByRole('menu')
122-
.findByRole('menuitem', { name: 'Add to favorites' })
121+
.findByRole('menuitem', { name: 'Favorite' })
123122
.should('be.visible')
124123
.click()
124+
125125
cy.wait('@addToFavorites')
126+
closeSidebar()
126127

127128
// See favorites star
128129
getRowForFile('new folder')
129130
.findByRole('img', { name: 'Favorite' })
130131
.should('be.visible')
131132

132-
// See folder in navigation
133-
cy.get('[data-cy-files-navigation-item="favorites"]')
133+
cy.reload()
134+
135+
// can unfavorite
136+
triggerActionForFile('new folder', 'details')
137+
cy.get('[data-cy-sidebar]')
138+
.findByRole('button', { name: 'Actions' })
139+
.click()
140+
// trigger menu button
141+
cy.findAllByRole('menu')
142+
.findByRole('menuitem', { name: 'Unfavorite' })
134143
.should('be.visible')
135-
.contains('new folder')
136-
.should('exist')
144+
.click()
145+
146+
cy.wait('@addToFavorites')
147+
closeSidebar()
148+
149+
getRowForFile('new folder')
150+
.findByRole('img', { name: 'Favorite' })
151+
.should('not.exist')
137152
})
138153
})

0 commit comments

Comments
 (0)