mirror of
https://github.com/nextcloud/server.git
synced 2026-04-22 06:37:56 -04:00
fix(files): always ask for confirmation if trashbin app is disabled
Signed-off-by: skjnldsv <skjnldsv@protonmail.com> Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com> Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
This commit is contained in:
parent
e61807c953
commit
6df8bdf5df
13 changed files with 377 additions and 135 deletions
22
__mocks__/@nextcloud/capabilities.ts
Normal file
22
__mocks__/@nextcloud/capabilities.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Capabilities } from '../../apps/files/src/types'
|
||||
|
||||
export const getCapabilities = (): Capabilities => {
|
||||
return {
|
||||
files: {
|
||||
bigfilechunking: true,
|
||||
blacklisted_files: [],
|
||||
forbidden_filename_basenames: [],
|
||||
forbidden_filename_characters: [],
|
||||
forbidden_filename_extensions: [],
|
||||
forbidden_filenames: [],
|
||||
undelete: true,
|
||||
version_deletion: true,
|
||||
version_labeling: true,
|
||||
versioning: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -22,8 +22,9 @@
|
|||
import { action } from './deleteAction'
|
||||
import { expect } from '@jest/globals'
|
||||
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
|
||||
import eventBus from '@nextcloud/event-bus'
|
||||
import * as capabilities from '@nextcloud/capabilities'
|
||||
import axios from '@nextcloud/axios'
|
||||
import eventBus from '@nextcloud/event-bus'
|
||||
|
||||
import logger from '../logger'
|
||||
|
||||
|
|
@ -111,6 +112,16 @@ describe('Delete action conditions tests', () => {
|
|||
expect(action.displayName([file], trashbinView)).toBe('Delete permanently')
|
||||
})
|
||||
|
||||
test('Trashbin disabled displayName', () => {
|
||||
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
|
||||
return {
|
||||
files: {},
|
||||
}
|
||||
})
|
||||
expect(action.displayName([file], view)).toBe('Delete permanently')
|
||||
expect(capabilities.getCapabilities).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('Shared root node displayName', () => {
|
||||
expect(action.displayName([file2], view)).toBe('Leave this share')
|
||||
expect(action.displayName([folder2], view)).toBe('Leave this share')
|
||||
|
|
@ -181,6 +192,9 @@ describe('Delete action enabled tests', () => {
|
|||
})
|
||||
|
||||
describe('Delete action execute tests', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
test('Delete action', async () => {
|
||||
jest.spyOn(axios, 'delete')
|
||||
jest.spyOn(eventBus, 'emit')
|
||||
|
|
@ -241,9 +255,123 @@ describe('Delete action execute tests', () => {
|
|||
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
|
||||
})
|
||||
|
||||
test('Delete action batch large set', async () => {
|
||||
jest.spyOn(axios, 'delete')
|
||||
jest.spyOn(eventBus, 'emit')
|
||||
|
||||
// Emulate the confirmation dialog to always confirm
|
||||
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(true))
|
||||
window.OC = { dialogs: { confirmDestructive: confirmMock } }
|
||||
|
||||
const file1 = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const file3 = new File({
|
||||
id: 3,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/baz.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const file4 = new File({
|
||||
id: 4,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/qux.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const file5 = new File({
|
||||
id: 5,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/quux.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2, file3, file4, file5], view, '/')
|
||||
|
||||
// Enough nodes to trigger a confirmation dialog
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
|
||||
expect(exec).toStrictEqual([true, true, true, true, true])
|
||||
expect(axios.delete).toBeCalledTimes(5)
|
||||
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
|
||||
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')
|
||||
expect(axios.delete).toHaveBeenNthCalledWith(3, 'https://cloud.domain.com/remote.php/dav/files/test/baz.txt')
|
||||
expect(axios.delete).toHaveBeenNthCalledWith(4, 'https://cloud.domain.com/remote.php/dav/files/test/qux.txt')
|
||||
expect(axios.delete).toHaveBeenNthCalledWith(5, 'https://cloud.domain.com/remote.php/dav/files/test/quux.txt')
|
||||
|
||||
expect(eventBus.emit).toBeCalledTimes(5)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(3, 'files:node:deleted', file3)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(4, 'files:node:deleted', file4)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(5, 'files:node:deleted', file5)
|
||||
})
|
||||
|
||||
test('Delete action batch trashbin disabled', async () => {
|
||||
jest.spyOn(axios, 'delete')
|
||||
jest.spyOn(eventBus, 'emit')
|
||||
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
|
||||
return {
|
||||
files: {},
|
||||
}
|
||||
})
|
||||
|
||||
// Emulate the confirmation dialog to always confirm
|
||||
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(true))
|
||||
window.OC = { dialogs: { confirmDestructive: confirmMock } }
|
||||
|
||||
const file1 = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
|
||||
// Will trigger a confirmation dialog because trashbin app is disabled
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect(axios.delete).toBeCalledTimes(2)
|
||||
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
|
||||
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')
|
||||
|
||||
expect(eventBus.emit).toBeCalledTimes(2)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
|
||||
})
|
||||
|
||||
test('Delete fails', async () => {
|
||||
jest.spyOn(axios, 'delete').mockImplementation(() => { throw new Error('Mock error') })
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
|
||||
jest.spyOn(eventBus, 'emit')
|
||||
|
||||
const file = new File({
|
||||
id: 1,
|
||||
|
|
@ -262,4 +390,35 @@ describe('Delete action execute tests', () => {
|
|||
expect(eventBus.emit).toBeCalledTimes(0)
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('Delete is cancelled', async () => {
|
||||
jest.spyOn(axios, 'delete')
|
||||
jest.spyOn(eventBus, 'emit')
|
||||
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
|
||||
return {
|
||||
files: {},
|
||||
}
|
||||
})
|
||||
|
||||
// Emulate the confirmation dialog to always confirm
|
||||
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(false))
|
||||
window.OC = { dialogs: { confirmDestructive: confirmMock } }
|
||||
|
||||
const file1 = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
|
||||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1], view, '/')
|
||||
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
|
||||
expect(exec).toStrictEqual([null])
|
||||
expect(axios.delete).toBeCalledTimes(0)
|
||||
|
||||
expect(eventBus.emit).toBeCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,106 +19,17 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files'
|
||||
import { Permission, Node, View, FileAction } from '@nextcloud/files'
|
||||
import { showInfo } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
import PQueue from 'p-queue'
|
||||
|
||||
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
|
||||
import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
|
||||
import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'
|
||||
|
||||
import logger from '../logger.js'
|
||||
import PQueue from 'p-queue'
|
||||
|
||||
const canUnshareOnly = (nodes: Node[]) => {
|
||||
return nodes.every(node => node.attributes['is-mount-root'] === true
|
||||
&& node.attributes['mount-type'] === 'shared')
|
||||
}
|
||||
|
||||
const canDisconnectOnly = (nodes: Node[]) => {
|
||||
return nodes.every(node => node.attributes['is-mount-root'] === true
|
||||
&& node.attributes['mount-type'] === 'external')
|
||||
}
|
||||
|
||||
const isMixedUnshareAndDelete = (nodes: Node[]) => {
|
||||
if (nodes.length === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasSharedItems = nodes.some(node => canUnshareOnly([node]))
|
||||
const hasDeleteItems = nodes.some(node => !canUnshareOnly([node]))
|
||||
return hasSharedItems && hasDeleteItems
|
||||
}
|
||||
|
||||
const isAllFiles = (nodes: Node[]) => {
|
||||
return !nodes.some(node => node.type !== FileType.File)
|
||||
}
|
||||
|
||||
const isAllFolders = (nodes: Node[]) => {
|
||||
return !nodes.some(node => node.type !== FileType.Folder)
|
||||
}
|
||||
|
||||
const displayName = (nodes: Node[], view: View) => {
|
||||
/**
|
||||
* If we're in the trashbin, we can only delete permanently
|
||||
*/
|
||||
if (view.id === 'trashbin') {
|
||||
return t('files', 'Delete permanently')
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're in the sharing view, we can only unshare
|
||||
*/
|
||||
if (isMixedUnshareAndDelete(nodes)) {
|
||||
return t('files', 'Delete and unshare')
|
||||
}
|
||||
|
||||
/**
|
||||
* If those nodes are all the root node of a
|
||||
* share, we can only unshare them.
|
||||
*/
|
||||
if (canUnshareOnly(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Leave this share')
|
||||
}
|
||||
return t('files', 'Leave these shares')
|
||||
}
|
||||
|
||||
/**
|
||||
* If those nodes are all the root node of an
|
||||
* external storage, we can only disconnect it.
|
||||
*/
|
||||
if (canDisconnectOnly(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Disconnect storage')
|
||||
}
|
||||
return t('files', 'Disconnect storages')
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're only selecting files, use proper wording
|
||||
*/
|
||||
if (isAllFiles(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Delete file')
|
||||
}
|
||||
return t('files', 'Delete files')
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're only selecting folders, use proper wording
|
||||
*/
|
||||
if (isAllFolders(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Delete folder')
|
||||
}
|
||||
return t('files', 'Delete folders')
|
||||
}
|
||||
|
||||
return t('files', 'Delete')
|
||||
}
|
||||
import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, isTrashbinEnabled } from './deleteUtils'
|
||||
|
||||
const queue = new PQueue({ concurrency: 5 })
|
||||
|
||||
|
|
@ -143,14 +54,22 @@ export const action = new FileAction({
|
|||
.every(permission => (permission & Permission.DELETE) !== 0)
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View, dir: string) {
|
||||
async exec(node: Node, view: View) {
|
||||
try {
|
||||
await axios.delete(node.encodedSource)
|
||||
let confirm = true
|
||||
|
||||
// Let's delete even if it's moved to the trashbin
|
||||
// since it has been removed from the current view
|
||||
// and changing the view will trigger a reload anyway.
|
||||
emit('files:node:deleted', node)
|
||||
// If trashbin is disabled, we need to ask for confirmation
|
||||
if (!isTrashbinEnabled()) {
|
||||
confirm = await askConfirmation([node], view)
|
||||
}
|
||||
|
||||
// If the user cancels the deletion, we don't want to do anything
|
||||
if (confirm === false) {
|
||||
showInfo(t('files', 'Deletion cancelled'))
|
||||
return null
|
||||
}
|
||||
|
||||
await deleteNode(node)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
|
|
@ -159,32 +78,20 @@ export const action = new FileAction({
|
|||
}
|
||||
},
|
||||
|
||||
async execBatch(nodes: Node[], view: View, dir: string): Promise<(boolean | null)[]> {
|
||||
const confirm = await new Promise<boolean>(resolve => {
|
||||
if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) {
|
||||
// TODO use a proper dialog from @nextcloud/dialogs when available
|
||||
window.OC.dialogs.confirmDestructive(
|
||||
t('files', 'You are about to delete {count} items.', { count: nodes.length }),
|
||||
t('files', 'Confirm deletion'),
|
||||
{
|
||||
type: window.OC.dialogs.YES_NO_BUTTONS,
|
||||
confirm: displayName(nodes, view),
|
||||
confirmClasses: 'error',
|
||||
cancel: t('files', 'Cancel'),
|
||||
},
|
||||
(decision: boolean) => {
|
||||
resolve(decision)
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
async execBatch(nodes: Node[], view: View): Promise<(boolean | null)[]> {
|
||||
let confirm = true
|
||||
|
||||
// If trashbin is disabled, we need to ask for confirmation
|
||||
if (!isTrashbinEnabled()) {
|
||||
confirm = await askConfirmation(nodes, view)
|
||||
} else if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) {
|
||||
confirm = await askConfirmation(nodes, view)
|
||||
}
|
||||
|
||||
// If the user cancels the deletion, we don't want to do anything
|
||||
if (confirm === false) {
|
||||
showInfo(t('files', 'Deletion cancelled'))
|
||||
return Promise.all(nodes.map(() => false))
|
||||
return Promise.all(nodes.map(() => null))
|
||||
}
|
||||
|
||||
// Map each node to a promise that resolves with the result of exec(node)
|
||||
|
|
@ -192,8 +99,13 @@ export const action = new FileAction({
|
|||
// Create a promise that resolves with the result of exec(node)
|
||||
const promise = new Promise<boolean>(resolve => {
|
||||
queue.add(async () => {
|
||||
const result = await this.exec(node, view, dir)
|
||||
resolve(result !== null ? result : false)
|
||||
try {
|
||||
await deleteNode(node)
|
||||
resolve(true)
|
||||
} catch (error) {
|
||||
logger.error('Error while deleting a file', { error, source: node.source, node })
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
return promise
|
||||
|
|
|
|||
134
apps/files/src/actions/deleteUtils.ts
Normal file
134
apps/files/src/actions/deleteUtils.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Capabilities } from '../types'
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileType } from '@nextcloud/files'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
export const isTrashbinEnabled = () => (getCapabilities() as Capabilities)?.files?.undelete === true
|
||||
|
||||
export const canUnshareOnly = (nodes: Node[]) => {
|
||||
return nodes.every(node => node.attributes['is-mount-root'] === true
|
||||
&& node.attributes['mount-type'] === 'shared')
|
||||
}
|
||||
|
||||
export const canDisconnectOnly = (nodes: Node[]) => {
|
||||
return nodes.every(node => node.attributes['is-mount-root'] === true
|
||||
&& node.attributes['mount-type'] === 'external')
|
||||
}
|
||||
|
||||
export const isMixedUnshareAndDelete = (nodes: Node[]) => {
|
||||
if (nodes.length === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasSharedItems = nodes.some(node => canUnshareOnly([node]))
|
||||
const hasDeleteItems = nodes.some(node => !canUnshareOnly([node]))
|
||||
return hasSharedItems && hasDeleteItems
|
||||
}
|
||||
|
||||
export const isAllFiles = (nodes: Node[]) => {
|
||||
return !nodes.some(node => node.type !== FileType.File)
|
||||
}
|
||||
|
||||
export const isAllFolders = (nodes: Node[]) => {
|
||||
return !nodes.some(node => node.type !== FileType.Folder)
|
||||
}
|
||||
|
||||
export const displayName = (nodes: Node[], view: View) => {
|
||||
/**
|
||||
* If we're in the trashbin, we can only delete permanently
|
||||
*/
|
||||
if (view.id === 'trashbin' || !isTrashbinEnabled()) {
|
||||
return t('files', 'Delete permanently')
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're in the sharing view, we can only unshare
|
||||
*/
|
||||
if (isMixedUnshareAndDelete(nodes)) {
|
||||
return t('files', 'Delete and unshare')
|
||||
}
|
||||
|
||||
/**
|
||||
* If those nodes are all the root node of a
|
||||
* share, we can only unshare them.
|
||||
*/
|
||||
if (canUnshareOnly(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Leave this share')
|
||||
}
|
||||
return t('files', 'Leave these shares')
|
||||
}
|
||||
|
||||
/**
|
||||
* If those nodes are all the root node of an
|
||||
* external storage, we can only disconnect it.
|
||||
*/
|
||||
if (canDisconnectOnly(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Disconnect storage')
|
||||
}
|
||||
return t('files', 'Disconnect storages')
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're only selecting files, use proper wording
|
||||
*/
|
||||
if (isAllFiles(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Delete file')
|
||||
}
|
||||
return t('files', 'Delete files')
|
||||
}
|
||||
|
||||
/**
|
||||
* If we're only selecting folders, use proper wording
|
||||
*/
|
||||
if (isAllFolders(nodes)) {
|
||||
if (nodes.length === 1) {
|
||||
return t('files', 'Delete folder')
|
||||
}
|
||||
return t('files', 'Delete folders')
|
||||
}
|
||||
|
||||
return t('files', 'Delete')
|
||||
}
|
||||
|
||||
export const askConfirmation = async (nodes: Node[], view: View) => {
|
||||
const message = view.id === 'trashbin' || !isTrashbinEnabled()
|
||||
? n('files', 'You are about to permanently delete {count} item', 'You are about to permanently delete {count} items', nodes.length, { count: nodes.length })
|
||||
: n('files', 'You are about to delete {count} item', 'You are about to delete {count} items', nodes.length, { count: nodes.length })
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
// TODO: Use the new dialog API
|
||||
window.OC.dialogs.confirmDestructive(
|
||||
message,
|
||||
t('files', 'Confirm deletion'),
|
||||
{
|
||||
type: window.OC.dialogs.YES_NO_BUTTONS,
|
||||
confirm: displayName(nodes, view),
|
||||
confirmClasses: 'error',
|
||||
cancel: t('files', 'Cancel'),
|
||||
},
|
||||
(decision: boolean) => {
|
||||
resolve(decision)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteNode = async (node: Node) => {
|
||||
await axios.delete(node.encodedSource)
|
||||
|
||||
// Let's delete even if it's moved to the trashbin
|
||||
// since it has been removed from the current view
|
||||
// and changing the view will trigger a reload anyway.
|
||||
emit('files:node:deleted', node)
|
||||
}
|
||||
|
|
@ -122,3 +122,18 @@ export interface TemplateFile {
|
|||
ratio?: number
|
||||
templates?: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export type Capabilities = {
|
||||
files: {
|
||||
bigfilechunking: boolean
|
||||
blacklisted_files: string[]
|
||||
forbidden_filename_basenames: string[]
|
||||
forbidden_filename_characters: string[]
|
||||
forbidden_filename_extensions: string[]
|
||||
forbidden_filenames: string[]
|
||||
undelete: boolean
|
||||
version_deletion: boolean
|
||||
version_labeling: boolean
|
||||
versioning: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
dist/7883-7883.js
vendored
4
dist/7883-7883.js
vendored
File diff suppressed because one or more lines are too long
2
dist/7883-7883.js.map
vendored
2
dist/7883-7883.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-unsupported-browser-redirect.js
vendored
4
dist/core-unsupported-browser-redirect.js
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/*! For license information please see core-unsupported-browser-redirect.js.LICENSE.txt */
|
||||
(()=>{"use strict";var e,r,t,o={47210:(e,r,t)=>{var o,n=t(21777);t.nc=btoa((0,n.do)()),window.TESTING||null!==(o=OC)&&void 0!==o&&null!==(o=o.config)&&void 0!==o&&o.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",(async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(4208),t.e(7883)]).then(t.bind(t,77883));e()}))}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){t=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,d=0;d<t.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](t[d])))?t.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var c=o();void 0!==c&&(r=c)}}return r}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((r,t)=>(a.f[t](e,r),r)),[])),a.u=e=>e+"-"+e+".js?v=48d5e80ef4bd4f20f511",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,d;if(void 0!==n)for(var c=document.getElementsByTagName("script"),u=0;u<c.length;u++){var s=c[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+n){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=3604,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={3604:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise(((t,n)=>o=e[r]=[t,n]));t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,(t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,i=t[0],l=t[1],d=t[2],c=0;if(i.some((r=>0!==e[r]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var u=d(a)}for(r&&r(t);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(47210)));i=a.O(i)})();
|
||||
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=4be2113a1f4861999ea7
|
||||
(()=>{"use strict";var e,r,t,o={47210:(e,r,t)=>{var o,n=t(21777);t.nc=btoa((0,n.do)()),window.TESTING||null!==(o=OC)&&void 0!==o&&null!==(o=o.config)&&void 0!==o&&o.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",(async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(4208),t.e(7883)]).then(t.bind(t,77883));e()}))}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){t=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,d=0;d<t.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](t[d])))?t.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var c=o();void 0!==c&&(r=c)}}return r}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((r,t)=>(a.f[t](e,r),r)),[])),a.u=e=>e+"-"+e+".js?v=b699c56d35999b013ff6",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,d;if(void 0!==n)for(var c=document.getElementsByTagName("script"),u=0;u<c.length;u++){var s=c[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+n){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=3604,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={3604:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise(((t,n)=>o=e[r]=[t,n]));t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,(t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,i=t[0],l=t[1],d=t[2],c=0;if(i.some((r=>0!==e[r]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var u=d(a)}for(r&&r(t);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(47210)));i=a.O(i)})();
|
||||
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=9745be31c7866e850ea8
|
||||
File diff suppressed because one or more lines are too long
4
dist/core-unsupported-browser.js
vendored
4
dist/core-unsupported-browser.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-unsupported-browser.js.map
vendored
2
dist/core-unsupported-browser.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue