aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/actions
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/src/actions')
-rw-r--r--apps/files/src/actions/deleteAction.spec.ts20
-rw-r--r--apps/files/src/actions/deleteAction.ts11
-rw-r--r--apps/files/src/actions/deleteUtils.ts7
-rw-r--r--apps/files/src/actions/moveOrCopyAction.ts4
-rw-r--r--apps/files/src/actions/openInFilesAction.spec.ts2
-rw-r--r--apps/files/src/actions/openInFilesAction.ts18
-rw-r--r--apps/files/src/actions/openLocallyAction.ts139
-rw-r--r--apps/files/src/actions/renameAction.spec.ts22
-rw-r--r--apps/files/src/actions/renameAction.ts20
-rw-r--r--apps/files/src/actions/viewInFolderAction.ts9
10 files changed, 150 insertions, 102 deletions
diff --git a/apps/files/src/actions/deleteAction.spec.ts b/apps/files/src/actions/deleteAction.spec.ts
index 4ed625b2412..845d29962a7 100644
--- a/apps/files/src/actions/deleteAction.spec.ts
+++ b/apps/files/src/actions/deleteAction.spec.ts
@@ -11,6 +11,7 @@ import * as eventBus from '@nextcloud/event-bus'
import { action } from './deleteAction'
import logger from '../logger'
+import { shouldAskForConfirmation } from './deleteUtils'
vi.mock('@nextcloud/auth')
vi.mock('@nextcloud/axios')
@@ -235,7 +236,6 @@ describe('Delete action execute tests', () => {
vi.spyOn(eventBus, 'emit')
const confirmMock = vi.fn()
- // @ts-expect-error We only mock what needed
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
@@ -275,7 +275,6 @@ describe('Delete action execute tests', () => {
// Emulate the confirmation dialog to always confirm
const confirmMock = vi.fn().mockImplementation((a, b, c, resolve) => resolve(true))
- // @ts-expect-error We only mock what needed
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
@@ -339,7 +338,11 @@ describe('Delete action execute tests', () => {
expect(eventBus.emit).toHaveBeenNthCalledWith(5, 'files:node:deleted', file5)
})
- test('Delete action batch trashbin disabled', async () => {
+ test('Delete action batch dialog enabled', async () => {
+ // Enable the confirmation dialog
+ eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: true })
+ expect(shouldAskForConfirmation()).toBe(true)
+
vi.spyOn(axios, 'delete')
vi.spyOn(eventBus, 'emit')
vi.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
@@ -350,7 +353,6 @@ describe('Delete action execute tests', () => {
// Emulate the confirmation dialog to always confirm
const confirmMock = vi.fn().mockImplementation((a, b, c, resolve) => resolve(true))
- // @ts-expect-error We only mock what needed
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
@@ -382,6 +384,8 @@ describe('Delete action execute tests', () => {
expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
+
+ eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: false })
})
test('Delete fails', async () => {
@@ -407,7 +411,10 @@ describe('Delete action execute tests', () => {
expect(logger.error).toBeCalledTimes(1)
})
- test('Delete is cancelled', async () => {
+ test('Delete is cancelled with dialog enabled', async () => {
+ // Enable the confirmation dialog
+ eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: true })
+
vi.spyOn(axios, 'delete')
vi.spyOn(eventBus, 'emit')
vi.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
@@ -418,7 +425,6 @@ describe('Delete action execute tests', () => {
// Emulate the confirmation dialog to always confirm
const confirmMock = vi.fn().mockImplementation((a, b, c, resolve) => resolve(false))
- // @ts-expect-error We only mock what needed
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
@@ -437,5 +443,7 @@ describe('Delete action execute tests', () => {
expect(axios.delete).toBeCalledTimes(0)
expect(eventBus.emit).toBeCalledTimes(0)
+
+ eventBus.emit('files:config:updated', { key: 'show_dialog_deletion', value: false })
})
})
diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts
index edcc615a34b..3e9e441a63c 100644
--- a/apps/files/src/actions/deleteAction.ts
+++ b/apps/files/src/actions/deleteAction.ts
@@ -10,10 +10,10 @@ import PQueue from 'p-queue'
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
-import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'
+import TrashCanSvg from '@mdi/svg/svg/trash-can-outline.svg?raw'
import { TRASHBIN_VIEW_ID } from '../../../files_trashbin/src/files_views/trashbinView.ts'
-import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, isTrashbinEnabled } from './deleteUtils.ts'
+import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, shouldAskForConfirmation } from './deleteUtils.ts'
import logger from '../logger.ts'
const queue = new PQueue({ concurrency: 5 })
@@ -58,8 +58,7 @@ export const action = new FileAction({
const callStack = new Error().stack || ''
const isCalledFromEventListener = callStack.toLocaleLowerCase().includes('keydown')
- // If trashbin is disabled, we need to ask for confirmation
- if (!isTrashbinEnabled() || isCalledFromEventListener) {
+ if (shouldAskForConfirmation() || isCalledFromEventListener) {
confirm = await askConfirmation([node], view)
}
@@ -81,8 +80,7 @@ export const action = new FileAction({
async execBatch(nodes: Node[], view: View): Promise<(boolean | null)[]> {
let confirm = true
- // If trashbin is disabled, we need to ask for confirmation
- if (!isTrashbinEnabled()) {
+ if (shouldAskForConfirmation()) {
confirm = await askConfirmation(nodes, view)
} else if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) {
confirm = await askConfirmation(nodes, view)
@@ -114,5 +112,6 @@ export const action = new FileAction({
return Promise.all(promises)
},
+ destructive: true,
order: 100,
})
diff --git a/apps/files/src/actions/deleteUtils.ts b/apps/files/src/actions/deleteUtils.ts
index ef395bae5b7..1ca7859b6c5 100644
--- a/apps/files/src/actions/deleteUtils.ts
+++ b/apps/files/src/actions/deleteUtils.ts
@@ -10,6 +10,8 @@ import { FileType } from '@nextcloud/files'
import { getCapabilities } from '@nextcloud/capabilities'
import { n, t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
+import { useUserConfigStore } from '../store/userconfig'
+import { getPinia } from '../store'
export const isTrashbinEnabled = () => (getCapabilities() as Capabilities)?.files?.undelete === true
@@ -101,6 +103,11 @@ export const displayName = (nodes: Node[], view: View) => {
return t('files', 'Delete')
}
+export const shouldAskForConfirmation = () => {
+ const userConfig = useUserConfigStore(getPinia())
+ return userConfig.userConfig.show_dialog_deletion !== false
+}
+
export const askConfirmation = async (nodes: Node[], view: View) => {
const message = view.id === 'trashbin' || !isTrashbinEnabled()
? n('files', 'You are about to permanently delete {count} item', 'You are about to permanently delete {count} items', nodes.length, { count: nodes.length })
diff --git a/apps/files/src/actions/moveOrCopyAction.ts b/apps/files/src/actions/moveOrCopyAction.ts
index 724b65fa515..af68120bb1b 100644
--- a/apps/files/src/actions/moveOrCopyAction.ts
+++ b/apps/files/src/actions/moveOrCopyAction.ts
@@ -16,8 +16,8 @@ import { openConflictPicker, hasConflict } from '@nextcloud/upload'
import { basename, join } from 'path'
import Vue from 'vue'
-import CopyIconSvg from '@mdi/svg/svg/folder-multiple.svg?raw'
-import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw'
+import CopyIconSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
+import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
import { MoveCopyAction, canCopy, canMove, getQueue } from './moveOrCopyActionUtils'
import { getContents } from '../services/Files'
diff --git a/apps/files/src/actions/openInFilesAction.spec.ts b/apps/files/src/actions/openInFilesAction.spec.ts
index e732270d4c0..3ccd15fa2d2 100644
--- a/apps/files/src/actions/openInFilesAction.spec.ts
+++ b/apps/files/src/actions/openInFilesAction.spec.ts
@@ -19,7 +19,7 @@ const recentView = {
describe('Open in files action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
- expect(action.id).toBe('open-in-files-recent')
+ expect(action.id).toBe('open-in-files')
expect(action.displayName([], recentView)).toBe('Open in Files')
expect(action.iconSvgInline([], recentView)).toBe('')
expect(action.default).toBe(DefaultType.HIDDEN)
diff --git a/apps/files/src/actions/openInFilesAction.ts b/apps/files/src/actions/openInFilesAction.ts
index 10e19e7eace..9e10b1ac74e 100644
--- a/apps/files/src/actions/openInFilesAction.ts
+++ b/apps/files/src/actions/openInFilesAction.ts
@@ -2,19 +2,21 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { translate as t } from '@nextcloud/l10n'
-import { type Node, FileType, FileAction, DefaultType } from '@nextcloud/files'
-/**
- * TODO: Move away from a redirect and handle
- * navigation straight out of the recent view
- */
+import type { Node } from '@nextcloud/files'
+
+import { t } from '@nextcloud/l10n'
+import { FileType, FileAction, DefaultType } from '@nextcloud/files'
+import { VIEW_ID as SEARCH_VIEW_ID } from '../views/search'
+
export const action = new FileAction({
- id: 'open-in-files-recent',
+ id: 'open-in-files',
displayName: () => t('files', 'Open in Files'),
iconSvgInline: () => '',
- enabled: (nodes, view) => view.id === 'recent',
+ enabled(nodes, view) {
+ return view.id === 'recent' || view.id === SEARCH_VIEW_ID
+ },
async exec(node: Node) {
let dir = node.dirname
diff --git a/apps/files/src/actions/openLocallyAction.ts b/apps/files/src/actions/openLocallyAction.ts
index a80cf0cbeed..986b304210c 100644
--- a/apps/files/src/actions/openLocallyAction.ts
+++ b/apps/files/src/actions/openLocallyAction.ts
@@ -13,71 +13,6 @@ import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw'
import IconWeb from '@mdi/svg/svg/web.svg?raw'
import { isPublicShare } from '@nextcloud/sharing/public'
-const confirmLocalEditDialog = (
- localEditCallback: (openingLocally: boolean) => void = () => {},
-) => {
- let callbackCalled = false
-
- return (new DialogBuilder())
- .setName(t('files', 'Open file locally'))
- .setText(t('files', 'The file should now open on your device. If it doesn\'t, please check that you have the desktop app installed.'))
- .setButtons([
- {
- label: t('files', 'Retry and close'),
- type: 'secondary',
- callback: () => {
- callbackCalled = true
- localEditCallback(true)
- },
- },
- {
- label: t('files', 'Open online'),
- icon: IconWeb,
- type: 'primary',
- callback: () => {
- callbackCalled = true
- localEditCallback(false)
- },
- },
- ])
- .build()
- .show()
- .then(() => {
- // Ensure the callback is called even if the dialog is dismissed in other ways
- if (!callbackCalled) {
- localEditCallback(false)
- }
- })
-}
-
-const attemptOpenLocalClient = async (path: string) => {
- openLocalClient(path)
- confirmLocalEditDialog(
- (openLocally: boolean) => {
- if (!openLocally) {
- window.OCA.Viewer.open({ path })
- return
- }
- openLocalClient(path)
- },
- )
-}
-
-const openLocalClient = async function(path: string) {
- const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json'
-
- try {
- const result = await axios.post(link, { path })
- const uid = getCurrentUser()?.uid
- let url = `nc://open/${uid}@` + window.location.host + encodePath(path)
- url += '?token=' + result.data.ocs.data.token
-
- window.open(url, '_self')
- } catch (error) {
- showError(t('files', 'Failed to redirect to client'))
- }
-}
-
export const action = new FileAction({
id: 'edit-locally',
displayName: () => t('files', 'Open locally'),
@@ -99,9 +34,81 @@ export const action = new FileAction({
},
async exec(node: Node) {
- attemptOpenLocalClient(node.path)
+ await attemptOpenLocalClient(node.path)
return null
},
order: 25,
})
+
+/**
+ * Try to open the path in the Nextcloud client.
+ *
+ * If this fails a dialog is shown with 3 options:
+ * 1. Retry: If it fails no further dialog is shown.
+ * 2. Open online: The viewer is used to open the file.
+ * 3. Close the dialog and nothing happens (abort).
+ *
+ * @param path - The path to open
+ */
+async function attemptOpenLocalClient(path: string) {
+ await openLocalClient(path)
+ const result = await confirmLocalEditDialog()
+ if (result === 'local') {
+ await openLocalClient(path)
+ } else if (result === 'online') {
+ window.OCA.Viewer.open({ path })
+ }
+}
+
+/**
+ * Try to open a file in the Nextcloud client.
+ * There is no way to get notified if this action was successfull.
+ *
+ * @param path - Path to open
+ */
+async function openLocalClient(path: string): Promise<void> {
+ const link = generateOcsUrl('apps/files/api/v1') + '/openlocaleditor?format=json'
+
+ try {
+ const result = await axios.post(link, { path })
+ const uid = getCurrentUser()?.uid
+ let url = `nc://open/${uid}@` + window.location.host + encodePath(path)
+ url += '?token=' + result.data.ocs.data.token
+
+ window.open(url, '_self')
+ } catch (error) {
+ showError(t('files', 'Failed to redirect to client'))
+ }
+}
+
+/**
+ * Open the confirmation dialog.
+ */
+async function confirmLocalEditDialog(): Promise<'online'|'local'|false> {
+ let result: 'online'|'local'|false = false
+ const dialog = (new DialogBuilder())
+ .setName(t('files', 'Open file locally'))
+ .setText(t('files', 'The file should now open on your device. If it doesn\'t, please check that you have the desktop app installed.'))
+ .setButtons([
+ {
+ label: t('files', 'Retry and close'),
+ type: 'secondary',
+ callback: () => {
+ result = 'local'
+ },
+ },
+ {
+ label: t('files', 'Open online'),
+ icon: IconWeb,
+ type: 'primary',
+ callback: () => {
+ result = 'online'
+ },
+ },
+ ])
+ .build()
+
+ await dialog.show()
+ return result
+}
diff --git a/apps/files/src/actions/renameAction.spec.ts b/apps/files/src/actions/renameAction.spec.ts
index 954eca5820f..1f9c9209d41 100644
--- a/apps/files/src/actions/renameAction.spec.ts
+++ b/apps/files/src/actions/renameAction.spec.ts
@@ -3,15 +3,23 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { action } from './renameAction'
-import { File, Permission, View, FileAction } from '@nextcloud/files'
+import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
import * as eventBus from '@nextcloud/event-bus'
-import { describe, expect, test, vi } from 'vitest'
+import { describe, expect, test, vi, beforeEach } from 'vitest'
+import { useFilesStore } from '../store/files'
+import { getPinia } from '../store/index.ts'
const view = {
id: 'files',
name: 'Files',
} as View
+beforeEach(() => {
+ const root = new Folder({ owner: 'test', source: 'https://cloud.domain.com/remote.php/dav/files/admin/', id: 1, permissions: Permission.CREATE })
+ const files = useFilesStore(getPinia())
+ files.setRoot({ service: 'files', root })
+})
+
describe('Rename action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
@@ -26,7 +34,7 @@ describe('Rename action conditions tests', () => {
describe('Rename action enabled tests', () => {
test('Enabled for node with UPDATE permission', () => {
const file = new File({
- id: 1,
+ id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
@@ -39,7 +47,7 @@ describe('Rename action enabled tests', () => {
test('Disabled for node without DELETE permission', () => {
const file = new File({
- id: 1,
+ id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
@@ -54,13 +62,13 @@ describe('Rename action enabled tests', () => {
window.OCA = { Files: { Sidebar: {} } }
const file1 = new File({
- id: 1,
+ id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
owner: 'admin',
mime: 'text/plain',
})
const file2 = new File({
- id: 1,
+ id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
owner: 'admin',
mime: 'text/plain',
@@ -76,7 +84,7 @@ describe('Rename action exec tests', () => {
vi.spyOn(eventBus, 'emit')
const file = new File({
- id: 1,
+ id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
diff --git a/apps/files/src/actions/renameAction.ts b/apps/files/src/actions/renameAction.ts
index e0ea784c291..715ecb7563e 100644
--- a/apps/files/src/actions/renameAction.ts
+++ b/apps/files/src/actions/renameAction.ts
@@ -5,7 +5,10 @@
import { emit } from '@nextcloud/event-bus'
import { Permission, type Node, FileAction, View } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
-import PencilSvg from '@mdi/svg/svg/pencil.svg?raw'
+import PencilSvg from '@mdi/svg/svg/pencil-outline.svg?raw'
+import { getPinia } from '../store'
+import { useFilesStore } from '../store/files'
+import { dirname } from 'path'
export const ACTION_RENAME = 'rename'
@@ -18,12 +21,23 @@ export const action = new FileAction({
if (nodes.length === 0) {
return false
}
+
// Disable for single file shares
if (view.id === 'public-file-share') {
return false
}
- // Only enable if all nodes have the delete permission
- return nodes.every((node) => Boolean(node.permissions & Permission.DELETE))
+
+ const node = nodes[0]
+ const filesStore = useFilesStore(getPinia())
+ const parentNode = node.dirname === '/'
+ ? filesStore.getRoot(view.id)
+ : filesStore.getNode(dirname(node.source))
+ const parentPermissions = parentNode?.permissions || Permission.NONE
+
+ // Only enable if the node have the delete permission
+ // and if the parent folder allows creating files
+ return Boolean(node.permissions & Permission.DELETE)
+ && Boolean(parentPermissions & Permission.CREATE)
},
async exec(node: Node) {
diff --git a/apps/files/src/actions/viewInFolderAction.ts b/apps/files/src/actions/viewInFolderAction.ts
index eb145dc409f..b22393c1152 100644
--- a/apps/files/src/actions/viewInFolderAction.ts
+++ b/apps/files/src/actions/viewInFolderAction.ts
@@ -2,10 +2,13 @@
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { Node, FileType, Permission, View, FileAction } from '@nextcloud/files'
-import { translate as t } from '@nextcloud/l10n'
-import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw'
+import type { Node, View } from '@nextcloud/files'
+
import { isPublicShare } from '@nextcloud/sharing/public'
+import { FileAction, FileType, Permission } from '@nextcloud/files'
+import { t } from '@nextcloud/l10n'
+
+import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
export const action = new FileAction({
id: 'view-in-folder',