Skip to content

Commit 9ae20fa

Browse files
committed
fix(files_trashbin): disable bulk download for trashbin
The backend does not allow bulk download within the trashbin, so we need to disable this also on the frontend. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 064bd7f commit 9ae20fa

3 files changed

Lines changed: 181 additions & 2 deletions

File tree

apps/files/src/actions/downloadAction.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
* along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
*
2121
*/
22+
import type { Node, View } from '@nextcloud/files'
2223
import { generateUrl } from '@nextcloud/router'
23-
import { FileAction, Permission, Node, FileType, View, DefaultType } from '@nextcloud/files'
24+
import { FileAction, Permission, FileType, DefaultType } from '@nextcloud/files'
2425
import { translate as t } from '@nextcloud/l10n'
2526
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
2627

@@ -110,7 +111,7 @@ export const action = new FileAction({
110111
displayName: () => t('files', 'Download'),
111112
iconSvgInline: () => ArrowDownSvg,
112113

113-
enabled(nodes: Node[]) {
114+
enabled(nodes: Node[], view: View) {
114115
if (nodes.length === 0) {
115116
return false
116117
}
@@ -123,6 +124,11 @@ export const action = new FileAction({
123124
return false
124125
}
125126

127+
// Trashbin does not allow batch download
128+
if (nodes.length > 1 && view.id === 'trashbin') {
129+
return false
130+
}
131+
126132
return nodes.every(isDownloadable)
127133
},
128134

cypress/e2e/files/FilesUtils.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@
2020
*
2121
*/
2222

23+
import type { User } from '@nextcloud/cypress'
24+
25+
export const selectAllFiles = () => {
26+
cy.get('[data-cy-files-list-selection-checkbox]')
27+
.findByRole('checkbox', { checked: false })
28+
.click({ force: true })
29+
}
30+
export const deselectAllFiles = () => {
31+
cy.get('[data-cy-files-list-selection-checkbox]')
32+
.findByRole('checkbox', { checked: true })
33+
.click({ force: true })
34+
}
35+
2336
export const getRowForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
2437
export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)
2538

@@ -181,3 +194,93 @@ export const clickOnBreadcrumbs = (label: string) => {
181194
cy.get('[data-cy-files-content-breadcrumbs]').contains(label).click()
182195
cy.wait('@propfind')
183196
}
197+
198+
/**
199+
* Check validity of an input element
200+
* @param validity The expected validity message (empty string means it is valid)
201+
* @example
202+
* ```js
203+
* cy.findByRole('textbox')
204+
* .should(haveValidity(/must not be empty/i))
205+
* ```
206+
*/
207+
export const haveValidity = (validity: string | RegExp) => {
208+
if (typeof validity === 'string') {
209+
return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.equal(validity)
210+
}
211+
return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.match(validity)
212+
}
213+
214+
export const deleteFileWithRequest = (user: User, path: string) => {
215+
// Ensure path starts with a slash and has no double slashes
216+
path = `/${path}`.replace(/\/+/g, '/')
217+
218+
cy.request('/csrftoken').then(({ body }) => {
219+
const requestToken = body.token
220+
cy.request({
221+
method: 'DELETE',
222+
url: `${Cypress.env('baseUrl')}/remote.php/dav/files/${user.userId}${path}`,
223+
auth: {
224+
user: user.userId,
225+
password: user.password,
226+
},
227+
headers: {
228+
requestToken,
229+
},
230+
retryOnStatusCodeFailure: true,
231+
})
232+
})
233+
}
234+
235+
export const triggerFileListAction = (actionId: string) => {
236+
cy.get(`button[data-cy-files-list-action="${CSS.escape(actionId)}"]`).last()
237+
.should('exist').click({ force: true })
238+
}
239+
240+
export const reloadCurrentFolder = () => {
241+
cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
242+
cy.get('[data-cy-files-content-breadcrumbs]').findByRole('button', { description: 'Reload current directory' }).click()
243+
cy.wait('@propfind')
244+
}
245+
246+
/**
247+
* Enable the grid mode for the files list.
248+
* Will fail if already enabled!
249+
*/
250+
export function enableGridMode() {
251+
cy.intercept('**/apps/files/api/v1/config/grid_view').as('setGridMode')
252+
cy.findByRole('button', { name: 'Switch to grid view' })
253+
.should('be.visible')
254+
.click()
255+
cy.wait('@setGridMode')
256+
}
257+
258+
/**
259+
* Calculate the needed viewport height to limit the visible rows of the file list.
260+
* Requires a logged in user.
261+
*
262+
* @param rows The number of rows that should be displayed at the same time
263+
*/
264+
export function calculateViewportHeight(rows: number): Cypress.Chainable<number> {
265+
cy.visit('/apps/files')
266+
267+
return cy.get('[data-cy-files-list]')
268+
.should('be.visible')
269+
.then((filesList) => {
270+
const windowHeight = Cypress.$('body').outerHeight()!
271+
// Size of other page elements
272+
const outerHeight = Math.ceil(windowHeight - filesList.outerHeight()!)
273+
// Size of before and filters
274+
const beforeHeight = Math.ceil(Cypress.$('.files-list__before').outerHeight()!)
275+
const filterHeight = Math.ceil(Cypress.$('.files-list__filters').outerHeight()!)
276+
// Size of the table header
277+
const tableHeaderHeight = Math.ceil(Cypress.$('[data-cy-files-list-thead]').outerHeight()!)
278+
// table row height
279+
const rowHeight = Math.ceil(Cypress.$('[data-cy-files-list-tbody] tr').outerHeight()!)
280+
281+
// sum it up
282+
const viewportHeight = outerHeight + beforeHeight + filterHeight + tableHeaderHeight + rows * rowHeight
283+
cy.log(`Calculated viewport height: ${viewportHeight} (${outerHeight} + ${beforeHeight} + ${filterHeight} + ${tableHeaderHeight} + ${rows} * ${rowHeight})`)
284+
return cy.wrap(viewportHeight)
285+
})
286+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { User } from '@nextcloud/cypress'
7+
8+
// @ts-expect-error package has wrong typings
9+
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
10+
import { deleteFileWithRequest, getRowForFileId, selectAllFiles, triggerActionForFileId } from '../files/FilesUtils.ts'
11+
12+
describe('files_trashbin: download files', { testIsolation: true }, () => {
13+
let user: User
14+
const fileids: number[] = []
15+
16+
deleteDownloadsFolderBeforeEach()
17+
18+
before(() => {
19+
cy.createRandomUser().then(($user) => {
20+
user = $user
21+
22+
cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/file.txt')
23+
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
24+
.then(() => deleteFileWithRequest(user, '/file.txt'))
25+
cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/other-file.txt')
26+
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
27+
.then(() => deleteFileWithRequest(user, '/other-file.txt'))
28+
})
29+
})
30+
31+
beforeEach(() => {
32+
cy.login(user)
33+
cy.visit('/apps/files/trashbin')
34+
})
35+
36+
it('can download file', () => {
37+
getRowForFileId(fileids[0]).should('be.visible')
38+
getRowForFileId(fileids[1]).should('be.visible')
39+
40+
triggerActionForFileId(fileids[0], 'download')
41+
42+
const downloadsFolder = Cypress.config('downloadsFolder')
43+
cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 })
44+
.should('exist')
45+
.and('have.length.gt', 8)
46+
.and('equal', '<content>')
47+
})
48+
49+
it('can download a file using default action', () => {
50+
getRowForFileId(fileids[0])
51+
.should('be.visible')
52+
.findByRole('button', { name: 'Download' })
53+
.click({ force: true })
54+
55+
const downloadsFolder = Cypress.config('downloadsFolder')
56+
cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 })
57+
.should('exist')
58+
.and('have.length.gt', 8)
59+
.and('equal', '<content>')
60+
})
61+
62+
// TODO: Fix this as this dependens on the webdav zip folder plugin not working for trashbin (and never worked with old NC legacy download ajax as well)
63+
it('does not offer bulk download', () => {
64+
cy.get('[data-cy-files-list-row-checkbox]').should('have.length', 2)
65+
selectAllFiles()
66+
cy.get('.files-list__selected').should('have.text', '2 selected')
67+
cy.get('[data-cy-files-list-selection-action="restore"]').should('be.visible')
68+
cy.get('[data-cy-files-list-selection-action="download"]').should('not.exist')
69+
})
70+
})

0 commit comments

Comments
 (0)