diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php index 415ca65b579..4dae198ee55 100644 --- a/apps/files/lib/Service/UserConfig.php +++ b/apps/files/lib/Service/UserConfig.php @@ -37,6 +37,12 @@ class UserConfig { 'default' => false, 'allowed' => [true, false], ], + [ + // Whether to show the "confirm file deletion" warning + 'key' => 'show_dialog_deletion', + 'default' => false, + 'allowed' => [true, false], + ], [ // Whether to show the "confirm file extension change" warning 'key' => 'show_dialog_file_extension', diff --git a/apps/files/src/actions/deleteAction.spec.ts b/apps/files/src/actions/deleteAction.spec.ts index 4ed625b2412..845d29962a7 100644 --- a/apps/files/src/actions/deleteAction.spec.ts +++ b/apps/files/src/actions/deleteAction.spec.ts @@ -11,6 +11,7 @@ import * as eventBus from '@nextcloud/event-bus' import { action } from './deleteAction' import logger from '../logger' +import { shouldAskForConfirmation } from './deleteUtils' vi.mock('@nextcloud/auth') vi.mock('@nextcloud/axios') @@ -235,7 +236,6 @@ describe('Delete action execute tests', () => { vi.spyOn(eventBus, 'emit') const confirmMock = vi.fn() - // @ts-expect-error We only mock what needed window.OC = { dialogs: { confirmDestructive: confirmMock } } const file1 = new File({ @@ -275,7 +275,6 @@ describe('Delete action execute tests', () => { // Emulate the confirmation dialog to always confirm const confirmMock = vi.fn().mockImplementation((a, b, c, resolve) => resolve(true)) - // @ts-expect-error We only mock what needed window.OC = { dialogs: { confirmDestructive: confirmMock } } const file1 = new File({ @@ -339,7 +338,11 @@ describe('Delete action execute tests', () => { expect(eventBus.emit).toHaveBeenNthCalledWith(5, 'files:node:deleted', file5) }) - test('Delete action batch trashbin disabled', async () => { + test('Delete action batch dialog enabled', async () => { + // Enable the confirmation dialog + eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: true }) + expect(shouldAskForConfirmation()).toBe(true) + vi.spyOn(axios, 'delete') vi.spyOn(eventBus, 'emit') vi.spyOn(capabilities, 'getCapabilities').mockImplementation(() => { @@ -350,7 +353,6 @@ describe('Delete action execute tests', () => { // Emulate the confirmation dialog to always confirm const confirmMock = vi.fn().mockImplementation((a, b, c, resolve) => resolve(true)) - // @ts-expect-error We only mock what needed window.OC = { dialogs: { confirmDestructive: confirmMock } } const file1 = new File({ @@ -382,6 +384,8 @@ describe('Delete action execute tests', () => { expect(eventBus.emit).toBeCalledTimes(2) expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1) expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2) + + eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: false }) }) test('Delete fails', async () => { @@ -407,7 +411,10 @@ describe('Delete action execute tests', () => { expect(logger.error).toBeCalledTimes(1) }) - test('Delete is cancelled', async () => { + test('Delete is cancelled with dialog enabled', async () => { + // Enable the confirmation dialog + eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: true }) + vi.spyOn(axios, 'delete') vi.spyOn(eventBus, 'emit') vi.spyOn(capabilities, 'getCapabilities').mockImplementation(() => { @@ -418,7 +425,6 @@ describe('Delete action execute tests', () => { // Emulate the confirmation dialog to always confirm const confirmMock = vi.fn().mockImplementation((a, b, c, resolve) => resolve(false)) - // @ts-expect-error We only mock what needed window.OC = { dialogs: { confirmDestructive: confirmMock } } const file1 = new File({ @@ -437,5 +443,7 @@ describe('Delete action execute tests', () => { expect(axios.delete).toBeCalledTimes(0) expect(eventBus.emit).toBeCalledTimes(0) + + eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: false }) }) }) diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index edcc615a34b..1f0665ced8f 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -13,7 +13,7 @@ import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw' import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw' import { TRASHBIN_VIEW_ID } from '../../../files_trashbin/src/files_views/trashbinView.ts' -import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, isTrashbinEnabled } from './deleteUtils.ts' +import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, shouldAskForConfirmation } from './deleteUtils.ts' import logger from '../logger.ts' const queue = new PQueue({ concurrency: 5 }) @@ -58,8 +58,7 @@ export const action = new FileAction({ const callStack = new Error().stack || '' const isCalledFromEventListener = callStack.toLocaleLowerCase().includes('keydown') - // If trashbin is disabled, we need to ask for confirmation - if (!isTrashbinEnabled() || isCalledFromEventListener) { + if (shouldAskForConfirmation() || isCalledFromEventListener) { confirm = await askConfirmation([node], view) } @@ -81,8 +80,7 @@ export const action = new FileAction({ async execBatch(nodes: Node[], view: View): Promise<(boolean | null)[]> { let confirm = true - // If trashbin is disabled, we need to ask for confirmation - if (!isTrashbinEnabled()) { + if (shouldAskForConfirmation()) { confirm = await askConfirmation(nodes, view) } else if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) { confirm = await askConfirmation(nodes, view) diff --git a/apps/files/src/actions/deleteUtils.ts b/apps/files/src/actions/deleteUtils.ts index ef395bae5b7..1ca7859b6c5 100644 --- a/apps/files/src/actions/deleteUtils.ts +++ b/apps/files/src/actions/deleteUtils.ts @@ -10,6 +10,8 @@ import { FileType } from '@nextcloud/files' import { getCapabilities } from '@nextcloud/capabilities' import { n, t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' +import { useUserConfigStore } from '../store/userconfig' +import { getPinia } from '../store' export const isTrashbinEnabled = () => (getCapabilities() as Capabilities)?.files?.undelete === true @@ -101,6 +103,11 @@ export const displayName = (nodes: Node[], view: View) => { return t('files', 'Delete') } +export const shouldAskForConfirmation = () => { + const userConfig = useUserConfigStore(getPinia()) + return userConfig.userConfig.show_dialog_deletion !== false +} + 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 }) diff --git a/apps/files/src/store/userconfig.ts b/apps/files/src/store/userconfig.ts index a901ab9c593..54e9a75eb8b 100644 --- a/apps/files/src/store/userconfig.ts +++ b/apps/files/src/store/userconfig.ts @@ -20,6 +20,7 @@ const initialUserConfig = loadState('files', 'config', { sort_favorites_first: true, sort_folders_first: true, + show_dialog_deletion: false, show_dialog_file_extension: true, }) diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index d2d1fe41648..6757b7f1f45 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -55,11 +55,13 @@ export interface UserConfig { crop_image_previews: boolean default_view: 'files' | 'personal' grid_view: boolean - show_dialog_file_extension: boolean, show_hidden: boolean show_mime_column: boolean sort_favorites_first: boolean sort_folders_first: boolean + + show_dialog_deletion: boolean + show_dialog_file_extension: boolean, } export interface UserConfigStore { diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue index 1aee8d0ae79..50376bcc578 100644 --- a/apps/files/src/views/Settings.vue +++ b/apps/files/src/views/Settings.vue @@ -110,6 +110,11 @@ @update:checked="setConfig('show_dialog_file_extension', $event)"> {{ t('files', 'Show a warning dialog when changing a file extension.') }} + + {{ t('files', 'Show a warning dialog when deleting files.') }} +