diff options
author | Christopher Ng <chrng8@gmail.com> | 2024-12-11 15:05:49 -0800 |
---|---|---|
committer | Christopher Ng <chrng8@gmail.com> | 2024-12-12 10:08:38 -0800 |
commit | 943023a3f49c12e197103d9bd1fbc07ee99aca13 (patch) | |
tree | 4d33094c24b1665f07ad70907ae4b02b8e01733c | |
parent | 0af875d713ca449a7af82a53a82665583b3b01ea (diff) | |
download | nextcloud-server-943023a3f49c12e197103d9bd1fbc07ee99aca13.tar.gz nextcloud-server-943023a3f49c12e197103d9bd1fbc07ee99aca13.zip |
feat(trashbin): Allow emptying trash
Signed-off-by: Christopher Ng <chrng8@gmail.com>
-rw-r--r-- | apps/files_trashbin/src/fileListActions/emptyTrashAction.ts | 109 | ||||
-rw-r--r-- | apps/files_trashbin/src/files-init.ts | 6 | ||||
-rw-r--r-- | apps/files_trashbin/src/logger.ts | 11 |
3 files changed, 125 insertions, 1 deletions
diff --git a/apps/files_trashbin/src/fileListActions/emptyTrashAction.ts b/apps/files_trashbin/src/fileListActions/emptyTrashAction.ts new file mode 100644 index 00000000000..7c033080c0e --- /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<typeof showInfo> + +const queue = new PQueue({ concurrency: 5 }) + +const showLoadingToast = (): null | Toast => { + const message = t('files_trashbin', 'Deleting files…') + let toast: null | Toast = null + toast = showInfo( + `<span class="icon icon-loading-small toast-loading-icon"></span> ${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<void>() + 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 ab5d293d136..f516d6f5be5 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 00000000000..064351c2fb5 --- /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() |