diff options
Diffstat (limited to 'apps/files/src/actions/deleteAction.ts')
-rw-r--r-- | apps/files/src/actions/deleteAction.ts | 215 |
1 files changed, 69 insertions, 146 deletions
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index c46de9c652e..fa4fdfe8cdc 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -1,126 +1,25 @@ /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { emit } from '@nextcloud/event-bus' -import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files' -import { showInfo } from '@nextcloud/dialogs' -import { translate as t, translatePlural as n } from '@nextcloud/l10n' -import axios from '@nextcloud/axios' +import { Permission, Node, View, FileAction } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' +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' - -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') - } +import TrashCanSvg from '@mdi/svg/svg/trash-can-outline.svg?raw' + +import { TRASHBIN_VIEW_ID } from '../../../files_trashbin/src/files_views/trashbinView.ts' +import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, shouldAskForConfirmation } from './deleteUtils.ts' +import logger from '../logger.ts' + +const queue = new PQueue({ concurrency: 5 }) - return t('files', 'Delete') -} +export const ACTION_DELETE = 'delete' export const action = new FileAction({ - id: 'delete', + id: ACTION_DELETE, displayName, iconSvgInline: (nodes: Node[]) => { if (canUnshareOnly(nodes)) { @@ -134,20 +33,40 @@ export const action = new FileAction({ return TrashCanSvg }, - enabled(nodes: Node[]) { + enabled(nodes: Node[], view: View): boolean { + if (view.id === TRASHBIN_VIEW_ID) { + const config = loadState('files_trashbin', 'config', { allow_delete: true }) + if (config.allow_delete === false) { + return false + } + } + return nodes.length > 0 && nodes .map(node => node.permissions) .every(permission => (permission & Permission.DELETE) !== 0) }, - async exec(node: Node) { + async exec(node: Node, view: View) { try { - await axios.delete(node.encodedSource) + let confirm = true + + // Trick to detect if the action was called from a keyboard event + // we need to make sure the method calling have its named containing 'keydown' + // here we use `onKeydown` method from the FileEntryActions component + const callStack = new Error().stack || '' + const isCalledFromEventListener = callStack.toLocaleLowerCase().includes('keydown') + + if (shouldAskForConfirmation() || isCalledFromEventListener) { + confirm = await askConfirmation([node], view) + } + + // If the user cancels the deletion, we don't want to do anything + if (confirm === false) { + return null + } + + await deleteNode(node) - // 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) return true } catch (error) { logger.error('Error while deleting a file', { error, source: node.source, node }) @@ -155,36 +74,40 @@ 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 (shouldAskForConfirmation()) { + 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)) } - return Promise.all(nodes.map(node => this.exec(node, view, dir))) + // Map each node to a promise that resolves with the result of exec(node) + const promises = nodes.map(node => { + // Create a promise that resolves with the result of exec(node) + const promise = new Promise<boolean>(resolve => { + queue.add(async () => { + try { + await deleteNode(node) + resolve(true) + } catch (error) { + logger.error('Error while deleting a file', { error, source: node.source, node }) + resolve(false) + } + }) + }) + return promise + }) + + return Promise.all(promises) }, + destructive: true, order: 100, }) |