mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 00:32:29 -04:00
refactor(files_trashbin): restore action refactoring
1. do not rely on magic string but use constant ID for trashbin view 2. add unit tests for restore action Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
4b773f6dd7
commit
b15ce12f28
3 changed files with 169 additions and 16 deletions
|
|
@ -3,8 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getNavigation, registerFileListAction } from '@nextcloud/files'
|
||||
import { emptyTrashAction } from './files_actions/emptyTrashAction.ts'
|
||||
import { getNavigation, registerFileAction, registerFileListAction } from '@nextcloud/files'
|
||||
import { restoreAction } from './files_actions/restoreAction.ts'
|
||||
import { emptyTrashAction } from './files_listActions/emptyTrashAction.ts'
|
||||
import { trashbinView } from './files_views/trashbinView.ts'
|
||||
|
||||
import './trashbin.scss'
|
||||
|
|
@ -13,3 +14,4 @@ const Navigation = getNavigation()
|
|||
Navigation.register(trashbinView)
|
||||
|
||||
registerFileListAction(emptyTrashAction)
|
||||
registerFileAction(restoreAction)
|
||||
|
|
|
|||
145
apps/files_trashbin/src/files_actions/restoreAction.spec.ts
Normal file
145
apps/files_trashbin/src/files_actions/restoreAction.spec.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Folder } from '@nextcloud/files'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as ncEventBus from '@nextcloud/event-bus'
|
||||
import isSvg from 'is-svg'
|
||||
|
||||
import { trashbinView } from '../files_views/trashbinView.ts'
|
||||
import { restoreAction } from './restoreAction.ts'
|
||||
import { PERMISSION_ALL, PERMISSION_NONE } from '../../../../core/src/OC/constants.js'
|
||||
|
||||
const axiosMock = vi.hoisted(() => ({
|
||||
request: vi.fn(),
|
||||
}))
|
||||
vi.mock('@nextcloud/axios', () => ({ default: axiosMock }))
|
||||
vi.mock('@nextcloud/auth')
|
||||
|
||||
describe('files_trashbin: file actions - restore action', () => {
|
||||
it('has id set', () => {
|
||||
expect(restoreAction.id).toBe('restore')
|
||||
})
|
||||
|
||||
it('has order set', () => {
|
||||
// very high priority!
|
||||
expect(restoreAction.order).toBe(1)
|
||||
})
|
||||
|
||||
it('is an inline action', () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' })
|
||||
|
||||
expect(restoreAction.inline).toBeTypeOf('function')
|
||||
expect(restoreAction.inline!(node, trashbinView)).toBe(true)
|
||||
})
|
||||
|
||||
it('has the display name set', () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' })
|
||||
|
||||
expect(restoreAction.displayName([node], trashbinView)).toBe('Restore')
|
||||
})
|
||||
|
||||
it('has an icon set', () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' })
|
||||
|
||||
const icon = restoreAction.iconSvgInline([node], trashbinView)
|
||||
expect(icon).toBeTypeOf('string')
|
||||
expect(isSvg(icon)).toBe(true)
|
||||
})
|
||||
|
||||
it('is enabled for trashbin view', () => {
|
||||
const nodes = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL }),
|
||||
]
|
||||
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!(nodes, trashbinView)).toBe(true)
|
||||
})
|
||||
|
||||
it('is not enabled when permissions are missing', () => {
|
||||
const nodes = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_NONE }),
|
||||
]
|
||||
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!(nodes, trashbinView)).toBe(false)
|
||||
})
|
||||
|
||||
it('is not enabled when no nodes are selected', () => {
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!([], trashbinView)).toBe(false)
|
||||
})
|
||||
|
||||
it('is not enabled for other views', () => {
|
||||
const nodes = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL }),
|
||||
]
|
||||
|
||||
const otherView = new Proxy(trashbinView, {
|
||||
get(target, p) {
|
||||
if (p === 'id') {
|
||||
return 'other-view'
|
||||
}
|
||||
return target[p]
|
||||
},
|
||||
})
|
||||
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!(nodes, otherView)).toBe(false)
|
||||
})
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(() => {
|
||||
axiosMock.request.mockReset()
|
||||
})
|
||||
|
||||
it('send restore request', async () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
|
||||
|
||||
expect(await restoreAction.exec(node, trashbinView, '/')).toBe(true)
|
||||
expect(axiosMock.request).toBeCalled()
|
||||
expect(axiosMock.request.mock.calls[0][0].method).toBe('MOVE')
|
||||
expect(axiosMock.request.mock.calls[0][0].url).toBe(node.encodedSource)
|
||||
expect(axiosMock.request.mock.calls[0][0].headers.destination).toContain('/restore/')
|
||||
})
|
||||
|
||||
it('deletes node from current view after successfull request', async () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
|
||||
|
||||
const emitSpy = vi.spyOn(ncEventBus, 'emit')
|
||||
|
||||
expect(await restoreAction.exec(node, trashbinView, '/')).toBe(true)
|
||||
expect(axiosMock.request).toBeCalled()
|
||||
expect(emitSpy).toBeCalled()
|
||||
expect(emitSpy).toBeCalledWith('files:node:deleted', node)
|
||||
})
|
||||
|
||||
it('does not delete node from view if reuest failed', async () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
|
||||
|
||||
axiosMock.request.mockImplementationOnce(() => { throw new Error() })
|
||||
const emitSpy = vi.spyOn(ncEventBus, 'emit')
|
||||
|
||||
expect(await restoreAction.exec(node, trashbinView, '/')).toBe(false)
|
||||
expect(axiosMock.request).toBeCalled()
|
||||
expect(emitSpy).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('batch: only returns success if all requests worked', async () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
|
||||
|
||||
expect(await restoreAction.execBatch!([node, node], trashbinView, '/')).toStrictEqual([true, true])
|
||||
expect(axiosMock.request).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
it('batch: only returns success if all requests worked - one failed', async () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
|
||||
|
||||
axiosMock.request.mockImplementationOnce(() => { throw new Error() })
|
||||
expect(await restoreAction.execBatch!([node, node], trashbinView, '/')).toStrictEqual([false, true])
|
||||
expect(axiosMock.request).toBeCalledTimes(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -2,40 +2,44 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { Permission, Node, View, FileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { encodePath } from '@nextcloud/paths'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { Permission, Node, View, registerFileAction, FileAction } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
import History from '@mdi/svg/svg/history.svg?raw'
|
||||
import svgHistory from '@mdi/svg/svg/history.svg?raw'
|
||||
|
||||
import { TRASHBIN_VIEW_ID } from '../files_views/trashbinView.ts'
|
||||
import logger from '../../../files/src/logger.ts'
|
||||
|
||||
registerFileAction(new FileAction({
|
||||
export const restoreAction = new FileAction({
|
||||
id: 'restore',
|
||||
|
||||
displayName() {
|
||||
return t('files_trashbin', 'Restore')
|
||||
},
|
||||
iconSvgInline: () => History,
|
||||
|
||||
iconSvgInline: () => svgHistory,
|
||||
|
||||
enabled(nodes: Node[], view) {
|
||||
// Only available in the trashbin view
|
||||
if (view.id !== 'trashbin') {
|
||||
if (view.id !== TRASHBIN_VIEW_ID) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only available if all nodes have read permission
|
||||
return nodes.length > 0 && nodes
|
||||
.map(node => node.permissions)
|
||||
.every(permission => (permission & Permission.READ) !== 0)
|
||||
return nodes.length > 0
|
||||
&& nodes
|
||||
.map((node) => node.permissions)
|
||||
.every((permission) => Boolean(permission & Permission.READ))
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
try {
|
||||
const destination = generateRemoteUrl(encodePath(`dav/trashbin/${getCurrentUser()?.uid}/restore/${node.basename}`))
|
||||
await axios({
|
||||
const destination = generateRemoteUrl(encodePath(`dav/trashbin/${getCurrentUser()!.uid}/restore/${node.basename}`))
|
||||
await axios.request({
|
||||
method: 'MOVE',
|
||||
url: node.encodedSource,
|
||||
headers: {
|
||||
|
|
@ -48,14 +52,16 @@ registerFileAction(new FileAction({
|
|||
emit('files:node:deleted', node)
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
logger.error('Failed to restore node', { error, node })
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
|
||||
},
|
||||
|
||||
order: 1,
|
||||
|
||||
inline: () => true,
|
||||
}))
|
||||
})
|
||||
Loading…
Reference in a new issue