From 7f7019ed5e2a3165988f1593d42c1f72c44c1dd6 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Wed, 11 Dec 2024 15:05:49 -0800 Subject: [PATCH] feat(trashbin): Allow emptying trash Signed-off-by: Christopher Ng --- .../src/fileListActions/emptyTrashAction.ts | 109 ++++++++++++++++++ apps/files_trashbin/src/files-init.ts | 6 +- apps/files_trashbin/src/logger.ts | 11 ++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 apps/files_trashbin/src/fileListActions/emptyTrashAction.ts create mode 100644 apps/files_trashbin/src/logger.ts diff --git a/apps/files_trashbin/src/fileListActions/emptyTrashAction.ts b/apps/files_trashbin/src/fileListActions/emptyTrashAction.ts new file mode 100644 index 0000000000000..7c033080c0ec5 --- /dev/null +++ b/apps/files_trashbin/src/fileListActions/emptyTrashAction.ts @@ -0,0 +1,109 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { Node } from '@nextcloud/files' + +import PQueue from 'p-queue' +import { FileListAction } from '@nextcloud/files' +import { + DialogSeverity, + getDialogBuilder, + showError, + showInfo, + showSuccess, + TOAST_PERMANENT_TIMEOUT, +} from '@nextcloud/dialogs' + +import { deleteNode } from '../../../files/src/actions/deleteUtils.ts' +import { logger } from '../logger.ts' + +type Toast = ReturnType + +const queue = new PQueue({ concurrency: 5 }) + +const showLoadingToast = (): null | Toast => { + const message = t('files_trashbin', 'Deleting files…') + let toast: null | Toast = null + toast = showInfo( + ` ${message}`, + { + isHTML: true, + timeout: TOAST_PERMANENT_TIMEOUT, + onRemove: () => { + toast?.hideToast() + toast = null + }, + }, + ) + return toast +} + +const emptyTrash = async (nodes: Node[]) => { + const promises = nodes.map((node) => { + const { promise, resolve, reject } = Promise.withResolvers() + queue.add(async () => { + try { + await deleteNode(node) + resolve() + } catch (error) { + logger.error('Failed to delete node', { error, node }) + reject(error) + } + }) + return promise + }) + + const toast = showLoadingToast() + const results = await Promise.allSettled(promises) + if (results.some((result) => result.status === 'rejected')) { + toast?.hideToast() + showError(t('files_trashbin', 'Failed to delete all previously deleted files')) + return + } + toast?.hideToast() + showSuccess(t('files_trashbin', 'Permanently deleted all previously deleted files')) +} + +export const emptyTrashAction = new FileListAction({ + id: 'empty-trash', + + displayName: () => t('files_trashbin', 'Empty deleted files'), + order: 0, + + enabled: (view, nodes, { folder }) => { + if (view.id !== 'trashbin') { + return false + } + return nodes.length > 0 && folder.path === '/' + }, + + exec: async (view, nodes) => { + const dialog = getDialogBuilder(t('files_trashbin', 'Confirm permanent deletion')) + .setSeverity(DialogSeverity.Warning) + // TODO Add note for groupfolders + .setText(t('files_trashbin', 'Are you sure you want to permanently delete all previously deleted files? This cannot be undone.')) + .setButtons([ + { + label: t('files_trashbin', 'Cancel'), + type: 'secondary', + callback: () => {}, + }, + { + label: t('files_trashbin', 'Empty deleted files'), + type: 'error', + callback: () => { + emptyTrash(nodes) + }, + }, + ]) + .build() + + try { + await dialog.show() + } catch (error) { + // Allow throw on dialog close + } + }, +}) diff --git a/apps/files_trashbin/src/files-init.ts b/apps/files_trashbin/src/files-init.ts index ab5d293d1369d..f516d6f5be552 100644 --- a/apps/files_trashbin/src/files-init.ts +++ b/apps/files_trashbin/src/files-init.ts @@ -6,6 +6,7 @@ import './trashbin.scss' import { translate as t } from '@nextcloud/l10n' +import { View, getNavigation, registerFileListAction } from '@nextcloud/files' import DeleteSvg from '@mdi/svg/svg/delete.svg?raw' import { getContents } from './services/trashbin' @@ -13,7 +14,8 @@ import { columns } from './columns.ts' // Register restore action import './actions/restoreAction' -import { View, getNavigation } from '@nextcloud/files' + +import { emptyTrashAction } from './fileListActions/emptyTrashAction.ts' const Navigation = getNavigation() Navigation.register(new View({ @@ -34,3 +36,5 @@ Navigation.register(new View({ getContents, })) + +registerFileListAction(emptyTrashAction) diff --git a/apps/files_trashbin/src/logger.ts b/apps/files_trashbin/src/logger.ts new file mode 100644 index 0000000000000..064351c2fb56c --- /dev/null +++ b/apps/files_trashbin/src/logger.ts @@ -0,0 +1,11 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getLoggerBuilder } from '@nextcloud/logger' + +export const logger = getLoggerBuilder() + .setApp('files_trashbin') + .detectUser() + .build()