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

[stable29] fix(files_trashbin): disable bulk download for trashbin #51208

Open
wants to merge 2 commits into
base: stable29
Choose a base branch
from
Open
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
10 changes: 8 additions & 2 deletions apps/files/src/actions/downloadAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Node, View } from '@nextcloud/files'
import { generateUrl } from '@nextcloud/router'
import { FileAction, Permission, Node, FileType, View, DefaultType } from '@nextcloud/files'
import { FileAction, Permission, FileType, DefaultType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'

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

enabled(nodes: Node[]) {
enabled(nodes: Node[], view: View) {
if (nodes.length === 0) {
return false
}
Expand All @@ -123,6 +124,11 @@ export const action = new FileAction({
return false
}

// Trashbin does not allow batch download
if (nodes.length > 1 && view.id === 'trashbin') {
return false
}

return nodes.every(isDownloadable)
},

Expand Down
103 changes: 103 additions & 0 deletions cypress/e2e/files/FilesUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@
*
*/

import type { User } from '@nextcloud/cypress'

export const selectAllFiles = () => {
cy.get('[data-cy-files-list-selection-checkbox]')
.findByRole('checkbox', { checked: false })
.click({ force: true })
}
export const deselectAllFiles = () => {
cy.get('[data-cy-files-list-selection-checkbox]')
.findByRole('checkbox', { checked: true })
.click({ force: true })
}

export const getRowForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)

Expand Down Expand Up @@ -182,3 +195,93 @@ export const clickOnBreadcrumbs = (label: string) => {
cy.get('[data-cy-files-content-breadcrumbs]').contains(label).click()
cy.wait('@propfind')
}

/**
* Check validity of an input element
* @param validity The expected validity message (empty string means it is valid)
* @example
* ```js
* cy.findByRole('textbox')
* .should(haveValidity(/must not be empty/i))
* ```
*/
export const haveValidity = (validity: string | RegExp) => {
if (typeof validity === 'string') {
return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.equal(validity)
}
return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.match(validity)
}

export const deleteFileWithRequest = (user: User, path: string) => {
// Ensure path starts with a slash and has no double slashes
path = `/${path}`.replace(/\/+/g, '/')

cy.request('/csrftoken').then(({ body }) => {
const requestToken = body.token
cy.request({
method: 'DELETE',
url: `${Cypress.env('baseUrl')}/remote.php/dav/files/${user.userId}${path}`,
auth: {
user: user.userId,
password: user.password,
},
headers: {
requestToken,
},
retryOnStatusCodeFailure: true,
})
})
}

export const triggerFileListAction = (actionId: string) => {
cy.get(`button[data-cy-files-list-action="${CSS.escape(actionId)}"]`).last()
.should('exist').click({ force: true })
}

export const reloadCurrentFolder = () => {
cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
cy.get('[data-cy-files-content-breadcrumbs]').findByRole('button', { description: 'Reload current directory' }).click()
cy.wait('@propfind')
}

/**
* Enable the grid mode for the files list.
* Will fail if already enabled!
*/
export function enableGridMode() {
cy.intercept('**/apps/files/api/v1/config/grid_view').as('setGridMode')
cy.findByRole('button', { name: 'Switch to grid view' })
.should('be.visible')
.click()
cy.wait('@setGridMode')
}

/**
* Calculate the needed viewport height to limit the visible rows of the file list.
* Requires a logged in user.
*
* @param rows The number of rows that should be displayed at the same time
*/
export function calculateViewportHeight(rows: number): Cypress.Chainable<number> {
cy.visit('/apps/files')

return cy.get('[data-cy-files-list]')
.should('be.visible')
.then((filesList) => {
const windowHeight = Cypress.$('body').outerHeight()!
// Size of other page elements
const outerHeight = Math.ceil(windowHeight - filesList.outerHeight()!)
// Size of before and filters
const beforeHeight = Math.ceil(Cypress.$('.files-list__before').outerHeight()!)
const filterHeight = Math.ceil(Cypress.$('.files-list__filters').outerHeight()!)
// Size of the table header
const tableHeaderHeight = Math.ceil(Cypress.$('[data-cy-files-list-thead]').outerHeight()!)
// table row height
const rowHeight = Math.ceil(Cypress.$('[data-cy-files-list-tbody] tr').outerHeight()!)

// sum it up
const viewportHeight = outerHeight + beforeHeight + filterHeight + tableHeaderHeight + rows * rowHeight
cy.log(`Calculated viewport height: ${viewportHeight} (${outerHeight} + ${beforeHeight} + ${filterHeight} + ${tableHeaderHeight} + ${rows} * ${rowHeight})`)
return cy.wrap(viewportHeight)
})
}
70 changes: 70 additions & 0 deletions cypress/e2e/files_trashbin/files.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { User } from '@nextcloud/cypress'

// @ts-expect-error package has wrong typings
import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
import { deleteFileWithRequest, getRowForFileId, selectAllFiles, triggerActionForFileId } from '../files/FilesUtils.ts'

describe('files_trashbin: download files', { testIsolation: true }, () => {
let user: User
const fileids: number[] = []

deleteDownloadsFolderBeforeEach()

before(() => {
cy.createRandomUser().then(($user) => {
user = $user

cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/file.txt')
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
.then(() => deleteFileWithRequest(user, '/file.txt'))
cy.uploadContent(user, new Blob(['<content>']), 'text/plain', '/other-file.txt')
.then(({ headers }) => fileids.push(Number.parseInt(headers['oc-fileid'])))
.then(() => deleteFileWithRequest(user, '/other-file.txt'))
})
})

beforeEach(() => {
cy.login(user)
cy.visit('/apps/files/trashbin')
})

it('can download file', () => {
getRowForFileId(fileids[0]).should('be.visible')
getRowForFileId(fileids[1]).should('be.visible')

triggerActionForFileId(fileids[0], 'download')

const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 8)
.and('equal', '<content>')
})

it('can download a file using default action', () => {
getRowForFileId(fileids[0])
.should('be.visible')
.findByRole('button', { name: 'Download' })
.click({ force: true })

const downloadsFolder = Cypress.config('downloadsFolder')
cy.readFile(`${downloadsFolder}/file.txt`, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 8)
.and('equal', '<content>')
})

// 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)
it('does not offer bulk download', () => {
cy.get('[data-cy-files-list-row-checkbox]').should('have.length', 2)
selectAllFiles()
cy.get('.files-list__selected').should('have.text', '2 selected')
cy.get('[data-cy-files-list-selection-action="restore"]').should('be.visible')
cy.get('[data-cy-files-list-selection-action="download"]').should('not.exist')
})
})
4 changes: 2 additions & 2 deletions dist/files-init.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-init.js.map

Large diffs are not rendered by default.

Loading