aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristopher Ng <chrng8@gmail.com>2024-12-11 15:05:49 -0800
committerChristopher Ng <chrng8@gmail.com>2024-12-12 10:08:38 -0800
commit943023a3f49c12e197103d9bd1fbc07ee99aca13 (patch)
tree4d33094c24b1665f07ad70907ae4b02b8e01733c
parent0af875d713ca449a7af82a53a82665583b3b01ea (diff)
downloadnextcloud-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.ts109
-rw-r--r--apps/files_trashbin/src/files-init.ts6
-rw-r--r--apps/files_trashbin/src/logger.ts11
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()