feat(files): ask for confirm if deleting 5 items or more

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
pull/43231/head
John Molakvoæ 4 months ago
parent bea8bf9032
commit 12fe86573f
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF

@ -207,6 +207,9 @@ describe('Delete action execute tests', () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
const confirmMock = jest.fn()
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
@ -225,6 +228,9 @@ describe('Delete action execute tests', () => {
const exec = await action.execBatch!([file1, file2], view, '/')
// Not enough nodes to trigger a confirmation dialog
expect(confirmMock).toBeCalledTimes(0)
expect(exec).toStrictEqual([true, true])
expect(axios.delete).toBeCalledTimes(2)
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')

@ -21,6 +21,7 @@
*/
import { emit } from '@nextcloud/event-bus'
import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files'
import { showInfo } from '@nextcloud/dialogs'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
@ -58,55 +59,57 @@ const isAllFolders = (nodes: Node[]) => {
return !nodes.some(node => node.type !== FileType.Folder)
}
export const action = new FileAction({
id: 'delete',
displayName(nodes: Node[], view: View) {
/**
* If we're in the trashbin, we can only delete permanently
*/
if (view.id === 'trashbin') {
return t('files', 'Delete permanently')
}
const displayName = (nodes: Node[], view: View) => {
/**
* If we're in the trashbin, we can only delete permanently
*/
if (view.id === 'trashbin') {
return t('files', 'Delete permanently')
}
/**
* If we're in the sharing view, we can only unshare
*/
if (isMixedUnshareAndDelete(nodes)) {
return t('files', 'Delete and unshare')
}
/**
* If we're in the sharing view, we can only unshare
*/
if (isMixedUnshareAndDelete(nodes)) {
return t('files', 'Delete and unshare')
}
/**
* If those nodes are all the root node of a
* share, we can only unshare them.
*/
if (canUnshareOnly(nodes)) {
return n('files', 'Leave this share', 'Leave these shares', nodes.length)
}
/**
* If those nodes are all the root node of a
* share, we can only unshare them.
*/
if (canUnshareOnly(nodes)) {
return n('files', 'Leave this share', 'Leave these shares', nodes.length)
}
/**
* If those nodes are all the root node of an
* external storage, we can only disconnect it.
*/
if (canDisconnectOnly(nodes)) {
return n('files', 'Disconnect storage', 'Disconnect storages', nodes.length)
}
/**
* If those nodes are all the root node of an
* external storage, we can only disconnect it.
*/
if (canDisconnectOnly(nodes)) {
return n('files', 'Disconnect storage', 'Disconnect storages', nodes.length)
}
/**
* If we're only selecting files, use proper wording
*/
if (isAllFiles(nodes)) {
return n('files', 'Delete file', 'Delete files', nodes.length)
}
/**
* If we're only selecting files, use proper wording
*/
if (isAllFiles(nodes)) {
return n('files', 'Delete file', 'Delete files', nodes.length)
}
/**
* If we're only selecting folders, use proper wording
*/
if (isAllFolders(nodes)) {
return n('files', 'Delete folder', 'Delete folders', nodes.length)
}
/**
* If we're only selecting folders, use proper wording
*/
if (isAllFolders(nodes)) {
return n('files', 'Delete folder', 'Delete folders', nodes.length)
}
return t('files', 'Delete')
},
return t('files', 'Delete')
}
export const action = new FileAction({
id: 'delete',
displayName,
iconSvgInline: (nodes: Node[]) => {
if (canUnshareOnly(nodes)) {
return CloseSvg
@ -139,7 +142,35 @@ export const action = new FileAction({
return false
}
},
async execBatch(nodes: Node[], view: View, dir: string) {
async execBatch(nodes: Node[], view: View, dir: string): Promise<(boolean | null)[]> {
const confirm = await new Promise<boolean>(resolve => {
if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) {
// TODO use a proper dialog from @nextcloud/dialogs when available
window.OC.dialogs.confirmDestructive(
t('files', 'You are about to delete {count} items.', { count: nodes.length }),
t('files', 'Confirm deletion'),
{
type: window.OC.dialogs.YES_NO_BUTTONS,
confirm: displayName(nodes, view),
confirmClasses: 'error',
cancel: t('files', 'Cancel'),
},
(decision: boolean) => {
resolve(decision)
},
)
return
}
resolve(true)
})
// If the user cancels the deletion, we don't want to do anything
if (confirm === false) {
showInfo(t('files', 'Deletion cancelled'))
return Promise.all(nodes.map(() => false))
}
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},

@ -0,0 +1,75 @@
/**
* @copyright Copyright (c) 2024 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getCurrentUser } from '@nextcloud/auth'
import type { Node } from '@nextcloud/files'
import { Type } from '@nextcloud/sharing'
type Share = {
/** The recipient display name */
'display-name': string
/** The recipient user id */
id: string
/** The share type */
type: Type
}
const getSharesAttribute = function(node: Node) {
return Object.values(node.attributes.sharees).flat() as Share[]
}
export const isNodeSharedWithMe = function(node: Node) {
const uid = getCurrentUser()?.uid
const shares = getSharesAttribute(node)
// If you're the owner, you can't share with yourself
if (node.owner === uid) {
return false
}
return shares.length > 0 && (
// If some shares are shared with you as a direct user share
shares.some(share => share.id === uid && share.type === Type.SHARE_TYPE_USER)
// Or of the file is shared with a group you're in
// (if it's returned by the backend, we assume you're in it)
|| shares.some(share => share.type === Type.SHARE_TYPE_GROUP)
)
}
export const isNodeSharedWithOthers = function(node: Node) {
const uid = getCurrentUser()?.uid
const shares = getSharesAttribute(node)
// If you're NOT the owner, you can't share with yourself
if (node.owner === uid) {
return false
}
return shares.length > 0
// If some shares are shared with you as a direct user share
&& shares.some(share => share.id !== uid && share.type !== Type.SHARE_TYPE_GROUP)
}
export const isNodeShared = function(node: Node) {
const shares = getSharesAttribute(node)
return shares.length > 0
}
Loading…
Cancel
Save