feat(trashbin): Allow emptying trash

Signed-off-by: Christopher Ng <chrng8@gmail.com>
pull/49171/head
Christopher Ng 2024-12-11 15:05:49 +07:00
parent 0af875d713
commit 943023a3f4
3 changed files with 125 additions and 1 deletions

@ -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
}
},
})

@ -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)

@ -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()