mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
Merge pull request #43231 from nextcloud/feat/warn-batch-delete
This commit is contained in:
commit
4ce10d05de
5 changed files with 163 additions and 51 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
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 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 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)
|
||||
}
|
||||
|
||||
return t('files', 'Delete')
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 folders, use proper wording
|
||||
*/
|
||||
if (isAllFolders(nodes)) {
|
||||
return n('files', 'Delete folder', 'Delete folders', nodes.length)
|
||||
}
|
||||
|
||||
return t('files', '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)))
|
||||
},
|
||||
|
||||
|
|
|
|||
75
apps/files_sharing/src/utils/NodeShareUtils.ts
Normal file
75
apps/files_sharing/src/utils/NodeShareUtils.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
4
dist/files-init.js
vendored
4
dist/files-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-init.js.map
vendored
2
dist/files-init.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue