mirror of
https://github.com/nextcloud/server.git
synced 2026-06-10 17:23:59 -04:00
Merge pull request #56743 from nextcloud/chore/files-4-0-0
This commit is contained in:
commit
52e3762045
437 changed files with 3484 additions and 1873 deletions
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Folder, View } from '@nextcloud/files'
|
||||
|
||||
import { File, FileAction, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
|
@ -26,15 +26,41 @@ describe('Inline unread comments action display name tests', () => {
|
|||
attributes: {
|
||||
'comments-unread': 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('comments-unread')
|
||||
expect(action.displayName([file], view)).toBe('')
|
||||
expect(action.title!([file], view)).toBe('1 new comment')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.inline!(file, view)).toBe(true)
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.title!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('1 new comment')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
expect(action.inline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(-140)
|
||||
})
|
||||
|
|
@ -49,10 +75,21 @@ describe('Inline unread comments action display name tests', () => {
|
|||
attributes: {
|
||||
'comments-unread': 2,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.displayName([file], view)).toBe('')
|
||||
expect(action.title!([file], view)).toBe('2 new comments')
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.title!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('2 new comments')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -64,10 +101,16 @@ describe('Inline unread comments action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
attributes: { },
|
||||
attributes: {},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Action is disabled when file does not have unread comments', () => {
|
||||
|
|
@ -80,9 +123,15 @@ describe('Inline unread comments action enabled tests', () => {
|
|||
attributes: {
|
||||
'comments-unread': 0,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Action is enabled when file has a single unread comment', () => {
|
||||
|
|
@ -95,9 +144,15 @@ describe('Inline unread comments action enabled tests', () => {
|
|||
attributes: {
|
||||
'comments-unread': 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Action is enabled when file has a two unread comments', () => {
|
||||
|
|
@ -110,9 +165,15 @@ describe('Inline unread comments action enabled tests', () => {
|
|||
attributes: {
|
||||
'comments-unread': 2,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -139,9 +200,15 @@ describe('Inline unread comments action execute tests', () => {
|
|||
attributes: {
|
||||
'comments-unread': 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.exec!(file, view, '/')
|
||||
const result = await action.exec!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(result).toBe(null)
|
||||
expect(setActiveTabMock).toBeCalledWith('comments')
|
||||
|
|
@ -173,9 +240,15 @@ describe('Inline unread comments action execute tests', () => {
|
|||
attributes: {
|
||||
'comments-unread': 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.exec!(file, view, '/')
|
||||
const result = await action.exec!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(setActiveTabMock).toBeCalledWith('comments')
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import CommentProcessingSvg from '@mdi/svg/svg/comment-processing.svg?raw'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
|
|
@ -13,9 +10,9 @@ import logger from '../logger.js'
|
|||
export const action = new FileAction({
|
||||
id: 'comments-unread',
|
||||
|
||||
title(nodes: Node[]) {
|
||||
const unread = nodes[0].attributes['comments-unread'] as number
|
||||
if (unread >= 0) {
|
||||
title({ nodes }) {
|
||||
const unread = nodes[0]?.attributes['comments-unread'] as number | undefined
|
||||
if (typeof unread === 'number' && unread >= 0) {
|
||||
return n('comments', '1 new comment', '{unread} new comments', unread, { unread })
|
||||
}
|
||||
return t('comments', 'Comment')
|
||||
|
|
@ -26,15 +23,19 @@ export const action = new FileAction({
|
|||
|
||||
iconSvgInline: () => CommentProcessingSvg,
|
||||
|
||||
enabled(nodes: Node[]) {
|
||||
const unread = nodes[0].attributes['comments-unread'] as number | undefined
|
||||
enabled({ nodes }) {
|
||||
const unread = nodes[0]?.attributes?.['comments-unread'] as number | undefined
|
||||
return typeof unread === 'number' && unread > 0
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
window.OCA.Files.Sidebar.setActiveTab('comments')
|
||||
await window.OCA.Files.Sidebar.open(node.path)
|
||||
await window.OCA.Files.Sidebar.open(nodes[0].path)
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Error while opening sidebar', { error })
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { FileAction, registerFileAction } from '@nextcloud/files'
|
||||
|
|
@ -31,20 +28,24 @@ export function registerConvertActions() {
|
|||
id: `convert-${from}-${to}`,
|
||||
displayName: () => t('files', 'Save as {displayName}', { displayName }),
|
||||
iconSvgInline: () => generateIconSvg(to),
|
||||
enabled: (nodes: Node[]) => {
|
||||
enabled: ({ nodes }) => {
|
||||
// Check that all nodes have the same mime type
|
||||
return nodes.every((node) => from === node.mime)
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
if (!nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we're here, we know that the node has a fileid
|
||||
convertFile(node.fileid as number, to)
|
||||
convertFile(nodes[0].fileid as number, to)
|
||||
|
||||
// Silently terminate, we'll handle the UI in the background
|
||||
return null
|
||||
},
|
||||
|
||||
async execBatch(nodes: Node[]) {
|
||||
async execBatch({ nodes }) {
|
||||
const fileIds = nodes.map((node) => node.fileid).filter(Boolean) as number[]
|
||||
convertFiles(fileIds, to)
|
||||
|
||||
|
|
@ -61,8 +62,8 @@ export function registerConvertActions() {
|
|||
id: ACTION_CONVERT,
|
||||
displayName: () => t('files', 'Save as …'),
|
||||
iconSvgInline: () => AutoRenewSvg,
|
||||
enabled: (nodes: Node[], view: View) => {
|
||||
return actions.some((action) => action.enabled!(nodes, view))
|
||||
enabled: (context) => {
|
||||
return actions.some((action) => action.enabled!(context))
|
||||
},
|
||||
async exec() {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ describe('Delete action conditions tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -50,6 +51,7 @@ describe('Delete action conditions tests', () => {
|
|||
'is-mount-root': true,
|
||||
'mount-type': 'shared',
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const folder = new Folder({
|
||||
|
|
@ -58,6 +60,7 @@ describe('Delete action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const folder2 = new Folder({
|
||||
|
|
@ -70,6 +73,7 @@ describe('Delete action conditions tests', () => {
|
|||
'is-mount-root': true,
|
||||
'mount-type': 'shared',
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const folder3 = new Folder({
|
||||
|
|
@ -82,23 +86,44 @@ describe('Delete action conditions tests', () => {
|
|||
'is-mount-root': true,
|
||||
'mount-type': 'external',
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('delete')
|
||||
expect(action.displayName([file], view)).toBe('Delete file')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Delete file')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(100)
|
||||
})
|
||||
|
||||
test('Default folder displayName', () => {
|
||||
expect(action.displayName([folder], view)).toBe('Delete folder')
|
||||
expect(action.displayName({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Delete folder')
|
||||
})
|
||||
|
||||
test('Default trashbin view displayName', () => {
|
||||
expect(action.displayName([file], trashbinView)).toBe('Delete permanently')
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Delete permanently')
|
||||
})
|
||||
|
||||
test('Trashbin disabled displayName', () => {
|
||||
|
|
@ -107,23 +132,58 @@ describe('Delete action conditions tests', () => {
|
|||
files: {},
|
||||
}
|
||||
})
|
||||
expect(action.displayName([file], view)).toBe('Delete permanently')
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Delete permanently')
|
||||
expect(capabilities.getCapabilities).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('Shared root node displayName', () => {
|
||||
expect(action.displayName([file2], view)).toBe('Leave this share')
|
||||
expect(action.displayName([folder2], view)).toBe('Leave this share')
|
||||
expect(action.displayName([file2, folder2], view)).toBe('Leave these shares')
|
||||
expect(action.displayName({
|
||||
nodes: [file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Leave this share')
|
||||
expect(action.displayName({
|
||||
nodes: [folder2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Leave this share')
|
||||
expect(action.displayName({
|
||||
nodes: [file2, folder2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Leave these shares')
|
||||
})
|
||||
|
||||
test('External storage root node displayName', () => {
|
||||
expect(action.displayName([folder3], view)).toBe('Disconnect storage')
|
||||
expect(action.displayName([folder3, folder3], view)).toBe('Disconnect storages')
|
||||
expect(action.displayName({
|
||||
nodes: [folder3],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Disconnect storage')
|
||||
expect(action.displayName({
|
||||
nodes: [folder3, folder3],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Disconnect storages')
|
||||
})
|
||||
|
||||
test('Shared and owned nodes displayName', () => {
|
||||
expect(action.displayName([file, file2], view)).toBe('Delete and unshare')
|
||||
expect(action.displayName({
|
||||
nodes: [file, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Delete and unshare')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -151,10 +211,16 @@ describe('Delete action enabled tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled without DELETE permissions', () => {
|
||||
|
|
@ -164,15 +230,26 @@ describe('Delete action enabled tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if not all nodes can be deleted', () => {
|
||||
|
|
@ -181,18 +258,35 @@ describe('Delete action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/test/Foo/',
|
||||
owner: 'test',
|
||||
permissions: Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
const folder2 = new Folder({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/test/Bar/',
|
||||
owner: 'test',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder1], view)).toBe(true)
|
||||
expect(action.enabled!([folder2], view)).toBe(false)
|
||||
expect(action.enabled!([folder1, folder2], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder1],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder1, folder2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if not allowed', () => {
|
||||
|
|
@ -201,7 +295,12 @@ describe('Delete action enabled tests', () => {
|
|||
})))
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -219,9 +318,15 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
expect(axios.delete).toBeCalledTimes(1)
|
||||
|
|
@ -244,6 +349,7 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -252,9 +358,15 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Not enough nodes to trigger a confirmation dialog
|
||||
expect(confirmMock).toBeCalledTimes(0)
|
||||
|
|
@ -283,6 +395,7 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -291,6 +404,7 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const file3 = new File({
|
||||
|
|
@ -299,6 +413,7 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const file4 = new File({
|
||||
|
|
@ -307,6 +422,7 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const file5 = new File({
|
||||
|
|
@ -315,9 +431,15 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2, file3, file4, file5], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2, file3, file4, file5],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Enough nodes to trigger a confirmation dialog
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
|
|
@ -361,6 +483,7 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -369,9 +492,15 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Will trigger a confirmation dialog because trashbin app is disabled
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
|
|
@ -401,9 +530,15 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(false)
|
||||
expect(axios.delete).toBeCalledTimes(1)
|
||||
|
|
@ -435,9 +570,15 @@ describe('Delete action execute tests', () => {
|
|||
owner: 'test',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
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-outline.svg?raw'
|
||||
|
|
@ -26,7 +23,7 @@ export const ACTION_DELETE = 'delete'
|
|||
export const action = new FileAction({
|
||||
id: ACTION_DELETE,
|
||||
displayName,
|
||||
iconSvgInline: (nodes: Node[]) => {
|
||||
iconSvgInline: ({ nodes }) => {
|
||||
if (canUnshareOnly(nodes)) {
|
||||
return CloseSvg
|
||||
}
|
||||
|
|
@ -38,7 +35,7 @@ export const action = new FileAction({
|
|||
return TrashCanSvg
|
||||
},
|
||||
|
||||
enabled(nodes: Node[], view: View): boolean {
|
||||
enabled({ nodes, view }) {
|
||||
if (view.id === TRASHBIN_VIEW_ID) {
|
||||
const config = loadState('files_trashbin', 'config', { allow_delete: true })
|
||||
if (config.allow_delete === false) {
|
||||
|
|
@ -51,7 +48,7 @@ export const action = new FileAction({
|
|||
.every((permission) => (permission & Permission.DELETE) !== 0)
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View) {
|
||||
async exec({ nodes, view }) {
|
||||
try {
|
||||
let confirm = true
|
||||
|
||||
|
|
@ -62,7 +59,7 @@ export const action = new FileAction({
|
|||
const isCalledFromEventListener = callStack.toLocaleLowerCase().includes('keydown')
|
||||
|
||||
if (shouldAskForConfirmation() || isCalledFromEventListener) {
|
||||
confirm = await askConfirmation([node], view)
|
||||
confirm = await askConfirmation([nodes[0]], view)
|
||||
}
|
||||
|
||||
// If the user cancels the deletion, we don't want to do anything
|
||||
|
|
@ -70,16 +67,16 @@ export const action = new FileAction({
|
|||
return null
|
||||
}
|
||||
|
||||
await deleteNode(node)
|
||||
await deleteNode(nodes[0])
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('Error while deleting a file', { error, source: node.source, node })
|
||||
logger.error('Error while deleting a file', { error, source: nodes[0].source, node: nodes[0] })
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
async execBatch(nodes: Node[], view: View): Promise<(boolean | null)[]> {
|
||||
async execBatch({ nodes, view }) {
|
||||
let confirm = true
|
||||
|
||||
if (shouldAskForConfirmation()) {
|
||||
|
|
|
|||
|
|
@ -66,10 +66,11 @@ export function isAllFolders(nodes: Node[]) {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
* @param view
|
||||
* @param root0
|
||||
* @param root0.nodes
|
||||
* @param root0.view
|
||||
*/
|
||||
export function displayName(nodes: Node[], view: View) {
|
||||
export function displayName({ nodes, view }: { nodes: Node[], view: View }) {
|
||||
/**
|
||||
* If those nodes are all the root node of a
|
||||
* share, we can only unshare them.
|
||||
|
|
@ -154,7 +155,7 @@ export async function askConfirmation(nodes: Node[], view: View) {
|
|||
t('files', 'Confirm deletion'),
|
||||
{
|
||||
type: window.OC.dialogs.YES_NO_BUTTONS,
|
||||
confirm: displayName(nodes, view),
|
||||
confirm: displayName({ nodes, view }),
|
||||
confirmClasses: 'error',
|
||||
cancel: t('files', 'Cancel'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,8 +30,18 @@ describe('Download action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('download')
|
||||
expect(action.displayName([], view)).toBe('Download')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Download')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBe(DefaultType.DEFAULT)
|
||||
expect(action.order).toBe(30)
|
||||
})
|
||||
|
|
@ -45,10 +55,16 @@ describe('Download action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled without READ permissions', () => {
|
||||
|
|
@ -58,10 +74,16 @@ describe('Download action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.NONE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if not all nodes have READ permissions', () => {
|
||||
|
|
@ -70,23 +92,45 @@ describe('Download action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const folder2 = new Folder({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.NONE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder1], view)).toBe(true)
|
||||
expect(action.enabled!([folder2], view)).toBe(false)
|
||||
expect(action.enabled!([folder1, folder2], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder1],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder1, folder2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -107,9 +151,15 @@ describe('Download action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
|
|
@ -125,9 +175,15 @@ describe('Download action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toStrictEqual([null])
|
||||
|
|
@ -144,9 +200,15 @@ describe('Download action execute tests', () => {
|
|||
mime: 'text/plain',
|
||||
displayname: 'baz.txt',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toStrictEqual([null])
|
||||
|
|
@ -161,9 +223,15 @@ describe('Download action execute tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(folder, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
|
|
@ -179,6 +247,7 @@ describe('Download action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 1,
|
||||
|
|
@ -186,9 +255,15 @@ describe('Download action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2], view, '/Dir')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toStrictEqual([null, null])
|
||||
|
|
@ -204,11 +279,17 @@ describe('Download action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
vi.spyOn(axios, 'head').mockRejectedValue(new Error('File not found'))
|
||||
|
||||
const errorSpy = vi.spyOn(dialogs, 'showError')
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toBe(null)
|
||||
expect(errorSpy).toHaveBeenCalledWith('The requested file is not available.')
|
||||
expect(link.click).not.toHaveBeenCalled()
|
||||
|
|
@ -221,6 +302,7 @@ describe('Download action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 2,
|
||||
|
|
@ -228,12 +310,18 @@ describe('Download action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
vi.spyOn(axios, 'head').mockRejectedValue(new Error('File not found'))
|
||||
vi.spyOn(eventBus, 'emit').mockImplementation(() => {})
|
||||
|
||||
const errorSpy = vi.spyOn(dialogs, 'showError')
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toStrictEqual([null, null])
|
||||
expect(errorSpy).toHaveBeenCalledWith('The requested files are not available.')
|
||||
expect(link.click).not.toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ function longestCommonPath(first: string, second: string): string {
|
|||
async function downloadNodes(nodes: Node[]) {
|
||||
let url: URL
|
||||
|
||||
if (!nodes[0]) {
|
||||
throw new Error('No nodes to download')
|
||||
}
|
||||
|
||||
if (nodes.length === 1) {
|
||||
if (nodes[0].type === FileType.File) {
|
||||
await triggerDownload(nodes[0].encodedSource, nodes[0].displayname)
|
||||
|
|
@ -125,7 +129,7 @@ export const action = new FileAction({
|
|||
displayName: () => t('files', 'Download'),
|
||||
iconSvgInline: () => ArrowDownSvg,
|
||||
|
||||
enabled(nodes: Node[], view: View) {
|
||||
enabled({ nodes, view }): boolean {
|
||||
if (nodes.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -143,25 +147,25 @@ export const action = new FileAction({
|
|||
return nodes.every(isDownloadable)
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
try {
|
||||
await downloadNodes([node])
|
||||
await downloadNodes(nodes)
|
||||
} catch (error) {
|
||||
showError(t('files', 'The requested file is not available.'))
|
||||
logger.error('The requested file is not available.', { error })
|
||||
emit('files:node:deleted', node)
|
||||
emit('files:node:deleted', nodes[0])
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
async execBatch({ nodes, view, folder }) {
|
||||
try {
|
||||
await downloadNodes(nodes)
|
||||
} catch (error) {
|
||||
showError(t('files', 'The requested files are not available.'))
|
||||
logger.error('The requested files are not available.', { error })
|
||||
// Try to reload the current directory to update the view
|
||||
const directory = getCurrentDirectory(view, dir)!
|
||||
const directory = getCurrentDirectory(view, folder.path)!
|
||||
emit('files:node:updated', directory)
|
||||
}
|
||||
return new Array(nodes.length).fill(null)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Folder, View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as eventBus from '@nextcloud/event-bus'
|
||||
|
|
@ -43,12 +43,23 @@ describe('Favorite action conditions tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('favorite')
|
||||
expect(action.displayName([file], view)).toBe('Add to favorites')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Add to favorites')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(-50)
|
||||
})
|
||||
|
|
@ -62,9 +73,15 @@ describe('Favorite action conditions tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.displayName([file], view)).toBe('Remove from favorites')
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Remove from favorites')
|
||||
})
|
||||
|
||||
test('Display name for multiple state files', () => {
|
||||
|
|
@ -77,6 +94,7 @@ describe('Favorite action conditions tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 1,
|
||||
|
|
@ -87,6 +105,7 @@ describe('Favorite action conditions tests', () => {
|
|||
attributes: {
|
||||
favorite: 0,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file3 = new File({
|
||||
id: 1,
|
||||
|
|
@ -97,12 +116,33 @@ describe('Favorite action conditions tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.displayName([file1, file2, file3], view)).toBe('Add to favorites')
|
||||
expect(action.displayName([file1, file2], view)).toBe('Add to favorites')
|
||||
expect(action.displayName([file2, file3], view)).toBe('Add to favorites')
|
||||
expect(action.displayName([file1, file3], view)).toBe('Remove from favorites')
|
||||
expect(action.displayName({
|
||||
nodes: [file1, file2, file3],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Add to favorites')
|
||||
expect(action.displayName({
|
||||
nodes: [file2, file3],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Add to favorites')
|
||||
expect(action.displayName({
|
||||
nodes: [file2, file3],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Add to favorites')
|
||||
expect(action.displayName({
|
||||
nodes: [file1, file3],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Remove from favorites')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -114,10 +154,16 @@ describe('Favorite action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for non-dav ressources', () => {
|
||||
|
|
@ -126,10 +172,16 @@ describe('Favorite action enabled tests', () => {
|
|||
source: 'https://domain.com/data/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -147,9 +199,15 @@ describe('Favorite action execute tests', () => {
|
|||
source: 'http://localhost/remote.php/dav/files/admin/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
|
||||
|
|
@ -175,9 +233,15 @@ describe('Favorite action execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
|
||||
|
|
@ -203,9 +267,15 @@ describe('Favorite action execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, favoriteView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: favoriteView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
|
||||
|
|
@ -235,7 +305,12 @@ describe('Favorite action execute tests', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, favoriteView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: favoriteView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
|
||||
|
|
@ -264,9 +339,15 @@ describe('Favorite action execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 0,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(false)
|
||||
|
||||
|
|
@ -296,9 +377,15 @@ describe('Favorite action execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(false)
|
||||
|
||||
|
|
@ -332,6 +419,7 @@ describe('Favorite action batch execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 1,
|
||||
|
|
@ -342,10 +430,16 @@ describe('Favorite action batch execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 0,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
// Mixed states triggers favorite action
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect([file1, file2].every((file) => file.attributes.favorite === 1)).toBe(true)
|
||||
|
||||
|
|
@ -367,6 +461,7 @@ describe('Favorite action batch execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 1,
|
||||
|
|
@ -377,10 +472,16 @@ describe('Favorite action batch execute tests', () => {
|
|||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
// Mixed states triggers favorite action
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect([file1, file2].every((file) => file.attributes.favorite === 0)).toBe(true)
|
||||
|
||||
|
|
|
|||
|
|
@ -73,18 +73,18 @@ export async function favoriteNode(node: Node, view: View, willFavorite: boolean
|
|||
|
||||
export const action = new FileAction({
|
||||
id: ACTION_FAVORITE,
|
||||
displayName(nodes: Node[]) {
|
||||
displayName({ nodes }) {
|
||||
return shouldFavorite(nodes)
|
||||
? t('files', 'Add to favorites')
|
||||
: t('files', 'Remove from favorites')
|
||||
},
|
||||
iconSvgInline: (nodes: Node[]) => {
|
||||
iconSvgInline: ({ nodes }) => {
|
||||
return shouldFavorite(nodes)
|
||||
? StarOutlineSvg
|
||||
: StarSvg
|
||||
},
|
||||
|
||||
enabled(nodes: Node[]) {
|
||||
enabled({ nodes }) {
|
||||
// Not enabled for public shares
|
||||
if (isPublicShare()) {
|
||||
return false
|
||||
|
|
@ -96,11 +96,11 @@ export const action = new FileAction({
|
|||
&& nodes.every((node) => node.permissions !== Permission.NONE)
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View) {
|
||||
const willFavorite = shouldFavorite([node])
|
||||
return await favoriteNode(node, view, willFavorite)
|
||||
async exec({ nodes, view }): Promise<boolean> {
|
||||
const willFavorite = shouldFavorite([nodes[0]])
|
||||
return await favoriteNode(nodes[0], view, willFavorite)
|
||||
},
|
||||
async execBatch(nodes: Node[], view: View) {
|
||||
async execBatch({ nodes, view }): Promise<boolean[]> {
|
||||
const willFavorite = shouldFavorite(nodes)
|
||||
|
||||
// Map each node to a promise that resolves with the result of exec(node)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IFilePickerButton } from '@nextcloud/dialogs'
|
||||
import type { Folder, Node, View } from '@nextcloud/files'
|
||||
import type { Folder, Node } from '@nextcloud/files'
|
||||
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
|
||||
import type { MoveCopyResult } from './moveOrCopyActionUtils.ts'
|
||||
|
||||
|
|
@ -306,7 +305,7 @@ async function openFilePickerForAction(
|
|||
export const ACTION_COPY_MOVE = 'move-copy'
|
||||
export const action = new FileAction({
|
||||
id: ACTION_COPY_MOVE,
|
||||
displayName(nodes: Node[]) {
|
||||
displayName({ nodes }) {
|
||||
switch (getActionForNodes(nodes)) {
|
||||
case MoveCopyAction.MOVE:
|
||||
return t('files', 'Move')
|
||||
|
|
@ -317,7 +316,7 @@ export const action = new FileAction({
|
|||
}
|
||||
},
|
||||
iconSvgInline: () => FolderMoveSvg,
|
||||
enabled(nodes: Node[], view: View) {
|
||||
enabled({ nodes, view }): boolean {
|
||||
// We can not copy or move in single file shares
|
||||
if (view.id === 'public-file-share') {
|
||||
return false
|
||||
|
|
@ -329,11 +328,11 @@ export const action = new FileAction({
|
|||
return nodes.length > 0 && (canMove(nodes) || canCopy(nodes))
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View, dir: string) {
|
||||
const action = getActionForNodes([node])
|
||||
async exec({ nodes, folder }) {
|
||||
const action = getActionForNodes([nodes[0]])
|
||||
let result
|
||||
try {
|
||||
result = await openFilePickerForAction(action, dir, [node])
|
||||
result = await openFilePickerForAction(action, folder.path, [nodes[0]])
|
||||
} catch (e) {
|
||||
logger.error(e as Error)
|
||||
return false
|
||||
|
|
@ -343,7 +342,7 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
try {
|
||||
await handleCopyMoveNodeTo(node, result.destination, result.action)
|
||||
await handleCopyMoveNodeTo(nodes[0], result.destination, result.action)
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error instanceof Error && !!error.message) {
|
||||
|
|
@ -355,9 +354,9 @@ export const action = new FileAction({
|
|||
}
|
||||
},
|
||||
|
||||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
async execBatch({ nodes, folder }) {
|
||||
const action = getActionForNodes(nodes)
|
||||
const result = await openFilePickerForAction(action, dir, nodes)
|
||||
const result = await openFilePickerForAction(action, folder.path, nodes)
|
||||
// Handle cancellation silently
|
||||
if (result === false) {
|
||||
return nodes.map(() => null)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
|
@ -21,12 +21,23 @@ describe('Open folder action conditions tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('open-folder')
|
||||
expect(action.displayName([folder], view)).toBe('Open folder FooBar')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Open folder FooBar')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBe(DefaultType.HIDDEN)
|
||||
expect(action.order).toBe(-100)
|
||||
})
|
||||
|
|
@ -39,10 +50,16 @@ describe('Open folder action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for non-dav ressources', () => {
|
||||
|
|
@ -51,10 +68,16 @@ describe('Open folder action enabled tests', () => {
|
|||
source: 'https://domain.com/data/FooBar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.NONE,
|
||||
root: '/',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
|
|
@ -63,16 +86,23 @@ describe('Open folder action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const folder2 = new Folder({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder1, folder2], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder1, folder2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for files', () => {
|
||||
|
|
@ -81,10 +111,16 @@ describe('Open folder action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without READ permissions', () => {
|
||||
|
|
@ -93,17 +129,22 @@ describe('Open folder action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.NONE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Open folder action execute tests', () => {
|
||||
test('Open folder', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const folder = new Folder({
|
||||
|
|
@ -111,9 +152,22 @@ describe('Open folder action execute tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(folder, view, '/')
|
||||
const root = new Folder({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
|
|
@ -122,17 +176,21 @@ describe('Open folder action execute tests', () => {
|
|||
|
||||
test('Open folder fails without node', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const exec = await action.exec(null as unknown as Node, view, '/')
|
||||
const exec = await action.exec({
|
||||
// @ts-expect-error We want to test without node
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toBe(false)
|
||||
expect(goToRouteMock).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
test('Open folder fails without Folder', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -140,9 +198,22 @@ describe('Open folder action execute tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const root = new Folder({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toBe(false)
|
||||
expect(goToRouteMock).toBeCalledTimes(0)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,30 +2,31 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
||||
import { DefaultType, FileAction, FileType, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'open-folder',
|
||||
displayName(files: Node[]) {
|
||||
displayName({ nodes }) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return t('files', 'Open folder')
|
||||
}
|
||||
|
||||
// Only works on single node
|
||||
const displayName = files[0].displayname
|
||||
const displayName = nodes[0].displayname
|
||||
return t('files', 'Open folder {displayName}', { displayName })
|
||||
},
|
||||
iconSvgInline: () => FolderSvg,
|
||||
|
||||
enabled(nodes: Node[]) {
|
||||
enabled({ nodes }) {
|
||||
// Only works on single node
|
||||
if (nodes.length !== 1) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
const node = nodes[0]
|
||||
|
||||
if (!node.isDavRessource) {
|
||||
if (!node.isDavResource) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +34,8 @@ export const action = new FileAction({
|
|||
&& (node.permissions & Permission.READ) !== 0
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View) {
|
||||
async exec({ nodes, view }) {
|
||||
const node = nodes[0]
|
||||
if (!node || node.type !== FileType.Folder) {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,18 @@ describe('Open in files action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('open-in-files')
|
||||
expect(action.displayName([], recentView)).toBe('Open in Files')
|
||||
expect(action.iconSvgInline([], recentView)).toBe('')
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view: recentView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Open in Files')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view: recentView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.default).toBe(DefaultType.HIDDEN)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline).toBeUndefined()
|
||||
|
|
@ -34,12 +44,22 @@ describe('Open in files action conditions tests', () => {
|
|||
describe('Open in files action enabled tests', () => {
|
||||
test('Enabled with on valid view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], recentView)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view: recentView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -57,7 +77,12 @@ describe('Open in files action execute tests', () => {
|
|||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
|
|
@ -77,7 +102,12 @@ describe('Open in files action execute tests', () => {
|
|||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, FileAction, FileType } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { VIEW_ID as SEARCH_VIEW_ID } from '../views/search.ts'
|
||||
|
|
@ -14,19 +11,23 @@ export const action = new FileAction({
|
|||
displayName: () => t('files', 'Open in Files'),
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled(nodes, view) {
|
||||
enabled({ view }) {
|
||||
return view.id === 'recent' || view.id === SEARCH_VIEW_ID
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
let dir = node.dirname
|
||||
if (node.type === FileType.Folder) {
|
||||
dir = dir + '/' + node.basename
|
||||
async exec({ nodes }) {
|
||||
if (!nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
let dir = nodes[0].dirname
|
||||
if (nodes[0].type === FileType.Folder) {
|
||||
dir = dir + '/' + nodes[0].basename
|
||||
}
|
||||
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files', fileid: String(node.fileid) },
|
||||
{ view: 'files', fileid: String(nodes[0].fileid) },
|
||||
{ dir, openfile: 'true' },
|
||||
)
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Folder, View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as nextcloudDialogs from '@nextcloud/dialogs'
|
||||
|
|
@ -30,8 +30,18 @@ describe('Open locally action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('edit-locally')
|
||||
expect(action.displayName([], view)).toBe('Open locally')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as any,
|
||||
contents: [],
|
||||
})).toBe('Open locally')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as any,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(25)
|
||||
})
|
||||
|
|
@ -45,10 +55,16 @@ describe('Open locally action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as any,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for non-dav resources', () => {
|
||||
|
|
@ -57,10 +73,16 @@ describe('Open locally action enabled tests', () => {
|
|||
source: 'https://domain.com/data/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as any,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
|
|
@ -70,6 +92,7 @@ describe('Open locally action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 1,
|
||||
|
|
@ -77,10 +100,16 @@ describe('Open locally action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file1, file2], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as any,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for files', () => {
|
||||
|
|
@ -89,10 +118,16 @@ describe('Open locally action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as any,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without UPDATE permissions', () => {
|
||||
|
|
@ -102,10 +137,16 @@ describe('Open locally action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as any,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -130,9 +171,15 @@ describe('Open locally action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.UPDATE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(spyShowDialog).toBeCalled()
|
||||
|
||||
|
|
@ -154,9 +201,15 @@ describe('Open locally action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.UPDATE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(spyShowDialog).toBeCalled()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw'
|
||||
import IconWeb from '@mdi/svg/svg/web.svg?raw'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
|
@ -24,9 +21,9 @@ export const action = new FileAction({
|
|||
iconSvgInline: () => LaptopSvg,
|
||||
|
||||
// Only works on single files
|
||||
enabled(nodes: Node[]) {
|
||||
enabled({ nodes }) {
|
||||
// Only works on single node
|
||||
if (nodes.length !== 1) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -38,8 +35,8 @@ export const action = new FileAction({
|
|||
return isSyncable(nodes[0])
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
await attemptOpenLocalClient(node.path)
|
||||
async exec({ nodes }) {
|
||||
await attemptOpenLocalClient(nodes[0].path)
|
||||
return null
|
||||
},
|
||||
|
||||
|
|
@ -68,7 +65,7 @@ async function attemptOpenLocalClient(path: string) {
|
|||
|
||||
/**
|
||||
* Try to open a file in the Nextcloud client.
|
||||
* There is no way to get notified if this action was successfull.
|
||||
* There is no way to get notified if this action was successful.
|
||||
*
|
||||
* @param path - Path to open
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ const view = {
|
|||
} 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 root = new Folder({
|
||||
owner: 'test',
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
|
||||
id: 1,
|
||||
permissions: Permission.CREATE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const files = useFilesStore(getPinia())
|
||||
files.setRoot({ service: 'files', root })
|
||||
})
|
||||
|
|
@ -27,8 +33,18 @@ describe('Rename action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('rename')
|
||||
expect(action.displayName([], view)).toBe('Rename')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Rename')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(10)
|
||||
})
|
||||
|
|
@ -42,10 +58,16 @@ describe('Rename action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.UPDATE | Permission.DELETE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for node without DELETE permission', () => {
|
||||
|
|
@ -55,10 +77,16 @@ describe('Rename action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
|
|
@ -70,16 +98,23 @@ describe('Rename action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file1, file2], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -92,9 +127,15 @@ describe('Rename action exec tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import PencilSvg from '@mdi/svg/svg/pencil-outline.svg?raw'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
|
|
@ -21,8 +17,8 @@ export const action = new FileAction({
|
|||
displayName: () => t('files', 'Rename'),
|
||||
iconSvgInline: () => PencilSvg,
|
||||
|
||||
enabled: (nodes: Node[], view: View) => {
|
||||
if (nodes.length === 0) {
|
||||
enabled: ({ nodes, view }) => {
|
||||
if (nodes.length === 0 || !nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -44,9 +40,9 @@ export const action = new FileAction({
|
|||
&& Boolean(parentPermissions & Permission.CREATE)
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
// Renaming is a built-in feature of the files app
|
||||
emit('files:node:rename', node)
|
||||
emit('files:node:rename', nodes[0])
|
||||
return null
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,18 @@ describe('Open sidebar action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('details')
|
||||
expect(action.displayName([], view)).toBe('Details')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Details')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(-50)
|
||||
})
|
||||
|
|
@ -37,10 +47,16 @@ describe('Open sidebar action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled without permissions', () => {
|
||||
|
|
@ -53,10 +69,16 @@ describe('Open sidebar action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.NONE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
|
|
@ -68,16 +90,23 @@ describe('Open sidebar action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file1, file2], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if no Sidebar', () => {
|
||||
|
|
@ -89,10 +118,16 @@ describe('Open sidebar action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for non-dav ressources', () => {
|
||||
|
|
@ -104,10 +139,16 @@ describe('Open sidebar action enabled tests', () => {
|
|||
source: 'https://domain.com/documents/admin/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/documents/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -126,9 +167,22 @@ describe('Open sidebar action exec tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const folder = new Folder({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(openMock).toBeCalledWith('/foobar.txt')
|
||||
|
|
@ -155,9 +209,22 @@ describe('Open sidebar action exec tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar',
|
||||
owner: 'admin',
|
||||
mime: 'httpd/unix-directory',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const folder = new Folder({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(openMock).toBeCalledWith('/foobar')
|
||||
|
|
@ -184,9 +251,15 @@ describe('Open sidebar action exec tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toBe(false)
|
||||
expect(openMock).toBeCalledTimes(1)
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import InformationSvg from '@mdi/svg/svg/information-outline.svg?raw'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
|
@ -18,7 +16,7 @@ export const action = new FileAction({
|
|||
iconSvgInline: () => InformationSvg,
|
||||
|
||||
// Sidebar currently supports user folder only, /files/USER
|
||||
enabled: (nodes: Node[]) => {
|
||||
enabled: ({ nodes }) => {
|
||||
if (isPublicShare()) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -40,7 +38,8 @@ export const action = new FileAction({
|
|||
return (nodes[0].root?.startsWith('/files/') && nodes[0].permissions !== Permission.NONE) ?? false
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View, dir: string) {
|
||||
async exec({ nodes, view, folder }) {
|
||||
const node = nodes[0]
|
||||
try {
|
||||
// If the sidebar is already open for the current file, do nothing
|
||||
if (window.OCA.Files?.Sidebar?.file === node.path) {
|
||||
|
|
@ -57,7 +56,7 @@ export const action = new FileAction({
|
|||
window.OCP?.Files?.Router?.goToRoute(
|
||||
null,
|
||||
{ view: view.id, fileid: String(node.fileid) },
|
||||
{ ...window.OCP.Files.Router.query, dir, opendetails: 'true' },
|
||||
{ ...window.OCP.Files.Router.query, dir: folder.path, opendetails: 'true' },
|
||||
true,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
|
@ -22,8 +22,18 @@ describe('View in folder action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('view-in-folder')
|
||||
expect(action.displayName([], view)).toBe('View in folder')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('View in folder')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(80)
|
||||
expect(action.enabled).toBeDefined()
|
||||
|
|
@ -38,10 +48,16 @@ describe('View in folder action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for files', () => {
|
||||
|
|
@ -51,10 +67,16 @@ describe('View in folder action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], viewFiles)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view: viewFiles,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without permissions', () => {
|
||||
|
|
@ -64,10 +86,16 @@ describe('View in folder action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.NONE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for non-dav ressources', () => {
|
||||
|
|
@ -76,10 +104,16 @@ describe('View in folder action enabled tests', () => {
|
|||
source: 'https://domain.com/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
|
|
@ -88,16 +122,23 @@ describe('View in folder action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file1, file2], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for folders', () => {
|
||||
|
|
@ -106,10 +147,16 @@ describe('View in folder action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for files outside the user root folder', () => {
|
||||
|
|
@ -118,10 +165,16 @@ describe('View in folder action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/trashbin/admin/trash/image.jpg.d1731053878',
|
||||
owner: 'admin',
|
||||
permissions: Permission.READ,
|
||||
root: '/trashbin/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -136,9 +189,15 @@ describe('View in folder action execute tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.READ,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
|
|
@ -158,7 +217,12 @@ describe('View in folder action execute tests', () => {
|
|||
permissions: Permission.READ,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
|
|
@ -169,7 +233,13 @@ describe('View in folder action execute tests', () => {
|
|||
const goToRouteMock = vi.fn()
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const exec = await action.exec(null as unknown as Node, view, '/')
|
||||
const exec = await action.exec({
|
||||
// @ts-expect-error We want to test without node
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toBe(false)
|
||||
expect(goToRouteMock).toBeCalledTimes(0)
|
||||
})
|
||||
|
|
@ -182,9 +252,15 @@ describe('View in folder action execute tests', () => {
|
|||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(folder, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(exec).toBe(false)
|
||||
expect(goToRouteMock).toBeCalledTimes(0)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
|
||||
import { FileAction, FileType, Permission } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
|
@ -16,7 +14,7 @@ export const action = new FileAction({
|
|||
},
|
||||
iconSvgInline: () => FolderMoveSvg,
|
||||
|
||||
enabled(nodes: Node[], view: View) {
|
||||
enabled({ nodes, view }) {
|
||||
// Not enabled for public shares
|
||||
if (isPublicShare()) {
|
||||
return false
|
||||
|
|
@ -28,13 +26,12 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
// Only works on single node
|
||||
if (nodes.length !== 1) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
const node = nodes[0]
|
||||
|
||||
if (!node.isDavRessource) {
|
||||
if (!node.isDavResource) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -50,15 +47,15 @@ export const action = new FileAction({
|
|||
return node.type === FileType.File
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
if (!node || node.type !== FileType.File) {
|
||||
async exec({ nodes }) {
|
||||
if (!nodes[0] || nodes[0].type !== FileType.File) {
|
||||
return false
|
||||
}
|
||||
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
{ view: 'files', fileid: String(node.fileid) },
|
||||
{ dir: node.dirname },
|
||||
{ view: 'files', fileid: String(nodes[0].fileid) },
|
||||
{ dir: nodes[0].dirname },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { FileAction, Folder, Node, View } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
type RenderFunction = typeof FileAction.prototype.renderInline
|
||||
|
||||
/**
|
||||
* This component is used to render custom
|
||||
* elements provided by an API. Vue doesn't allow
|
||||
|
|
@ -17,17 +22,22 @@ export default {
|
|||
name: 'CustomElementRender',
|
||||
props: {
|
||||
source: {
|
||||
type: Object,
|
||||
type: Object as PropType<Node>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
currentView: {
|
||||
type: Object,
|
||||
activeView: {
|
||||
type: Object as PropType<View>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
activeFolder: {
|
||||
type: Object as PropType<Folder>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
render: {
|
||||
type: Function,
|
||||
type: Function as PropType<RenderFunction>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -48,7 +58,13 @@ export default {
|
|||
|
||||
methods: {
|
||||
async updateRootElement() {
|
||||
const element = await this.render(this.source, this.currentView)
|
||||
const element = await this.render!({
|
||||
nodes: [this.source],
|
||||
view: this.activeView,
|
||||
folder: this.activeFolder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
if (element) {
|
||||
this.$el.replaceChildren(element)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -92,12 +92,13 @@
|
|||
<td
|
||||
v-for="column in columns"
|
||||
:key="column.id"
|
||||
:class="`files-list__row-${currentView.id}-${column.id}`"
|
||||
:class="`files-list__row-${activeView.id}-${column.id}`"
|
||||
class="files-list__row-column-custom"
|
||||
:data-cy-files-list-row-column-custom="column.id"
|
||||
@click="openDetailsIfAvailable">
|
||||
<CustomElementRender
|
||||
:current-view="currentView"
|
||||
:active-folder="activeFolder"
|
||||
:active-view="activeView"
|
||||
:render="column.render"
|
||||
:source="source" />
|
||||
</td>
|
||||
|
|
@ -116,9 +117,9 @@ import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
|
|||
import FileEntryName from './FileEntry/FileEntryName.vue'
|
||||
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
|
||||
import { useFileListWidth } from '../composables/useFileListWidth.ts'
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import { useRouteParameters } from '../composables/useRouteParameters.ts'
|
||||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
import { useActiveStore } from '../store/active.ts'
|
||||
import { useDragAndDropStore } from '../store/dragging.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useRenamingStore } from '../store/renaming.ts'
|
||||
|
|
@ -160,24 +161,27 @@ export default defineComponent({
|
|||
const renamingStore = useRenamingStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
const filesListWidth = useFileListWidth()
|
||||
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
|
||||
const { currentView } = useNavigation(true)
|
||||
const {
|
||||
directory: currentDir,
|
||||
fileId: currentFileId,
|
||||
fileId: currentRouteFileId,
|
||||
} = useRouteParameters()
|
||||
|
||||
const {
|
||||
activeFolder,
|
||||
activeNode,
|
||||
activeView,
|
||||
} = useActiveStore()
|
||||
|
||||
return {
|
||||
actionsMenuStore,
|
||||
activeFolder,
|
||||
activeNode,
|
||||
activeView,
|
||||
currentRouteFileId,
|
||||
draggingStore,
|
||||
filesListWidth,
|
||||
filesStore,
|
||||
renamingStore,
|
||||
selectionStore,
|
||||
|
||||
currentDir,
|
||||
currentFileId,
|
||||
currentView,
|
||||
filesListWidth,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -208,7 +212,7 @@ export default defineComponent({
|
|||
if (this.filesListWidth < 512 || this.compact) {
|
||||
return []
|
||||
}
|
||||
return this.currentView.columns || []
|
||||
return this.activeView.columns || []
|
||||
},
|
||||
|
||||
mime() {
|
||||
|
|
@ -281,7 +285,12 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.defaultFileAction?.exec(this.source, this.currentView, this.currentDir)
|
||||
this.defaultFileAction?.exec({
|
||||
nodes: [this.source],
|
||||
folder: this.activeFolder!,
|
||||
contents: this.nodes,
|
||||
view: this.activeView!,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
v-for="action in enabledRenderActions"
|
||||
:key="action.id"
|
||||
:class="'files-list__row-action-' + action.id"
|
||||
:current-view="currentView"
|
||||
:active-folder="activeStore.activeFolder"
|
||||
:active-view="activeStore.activeView"
|
||||
:render="action.renderInline"
|
||||
:source="source"
|
||||
class="files-list__row-action--inline" />
|
||||
|
|
@ -43,15 +44,15 @@
|
|||
:close-after-click="!isValidMenu(action)"
|
||||
:data-cy-files-list-row-action="action.id"
|
||||
:is-menu="isValidMenu(action)"
|
||||
:aria-label="action.title?.([source], currentView)"
|
||||
:title="action.title?.([source], currentView)"
|
||||
:aria-label="action.title?.(actionContext)"
|
||||
:title="action.title?.(actionContext)"
|
||||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="isLoadingAction(action)" />
|
||||
<NcIconSvgWrapper
|
||||
v-else
|
||||
class="files-list__row-action-icon"
|
||||
:svg="action.iconSvgInline([source], currentView)" />
|
||||
:svg="action.iconSvgInline(actionContext)" />
|
||||
</template>
|
||||
{{ actionDisplayName(action) }}
|
||||
</NcActionButton>
|
||||
|
|
@ -72,15 +73,15 @@
|
|||
:close-after-click="!isValidMenu(action)"
|
||||
:data-cy-files-list-row-action="action.id"
|
||||
:is-menu="isValidMenu(action)"
|
||||
:aria-label="action.title?.([source], currentView)"
|
||||
:title="action.title?.([source], currentView)"
|
||||
:aria-label="action.title?.(actionContext)"
|
||||
:title="action.title?.(actionContext)"
|
||||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="isLoadingAction(action)" />
|
||||
<NcIconSvgWrapper
|
||||
v-else
|
||||
class="files-list__row-action-icon"
|
||||
:svg="action.iconSvgInline([source], currentView)" />
|
||||
:svg="action.iconSvgInline(actionContext)" />
|
||||
</template>
|
||||
{{ actionDisplayName(action) }}
|
||||
</NcActionButton>
|
||||
|
|
@ -105,12 +106,12 @@
|
|||
class="files-list__row-action--submenu"
|
||||
close-after-click
|
||||
:data-cy-files-list-row-action="action.id"
|
||||
:aria-label="action.title?.([source], currentView)"
|
||||
:title="action.title?.([source], currentView)"
|
||||
:aria-label="action.title?.(actionContext)"
|
||||
:title="action.title?.(actionContext)"
|
||||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="isLoadingAction(action)" />
|
||||
<NcIconSvgWrapper v-else :svg="action.iconSvgInline([source], currentView)" />
|
||||
<NcIconSvgWrapper v-else :svg="action.iconSvgInline(actionContext)" />
|
||||
</template>
|
||||
{{ actionDisplayName(action) }}
|
||||
</NcActionButton>
|
||||
|
|
@ -120,7 +121,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { FileAction, Node } from '@nextcloud/files'
|
||||
import type { ActionContextSingle, FileAction, Node } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { DefaultType, NodeStatus } from '@nextcloud/files'
|
||||
|
|
@ -135,8 +136,6 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
|||
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
|
||||
import CustomElementRender from '../CustomElementRender.vue'
|
||||
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
|
||||
import { useNavigation } from '../../composables/useNavigation.ts'
|
||||
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
|
||||
import logger from '../../logger.ts'
|
||||
import actionsMixins from '../../mixins/actionsMixin.ts'
|
||||
import { useActiveStore } from '../../store/active.ts'
|
||||
|
|
@ -175,17 +174,12 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup() {
|
||||
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
|
||||
const { currentView } = useNavigation(true)
|
||||
const { directory: currentDir } = useRouteParameters()
|
||||
|
||||
// The file list is guaranteed to be shown with active view - thus we can set the `loaded` flag
|
||||
const activeStore = useActiveStore()
|
||||
const filesListWidth = useFileListWidth()
|
||||
const enabledFileActions = inject<FileAction[]>('enabledFileActions', [])
|
||||
return {
|
||||
activeStore,
|
||||
currentDir,
|
||||
currentView,
|
||||
enabledFileActions,
|
||||
filesListWidth,
|
||||
t,
|
||||
|
|
@ -194,13 +188,22 @@ export default defineComponent({
|
|||
|
||||
computed: {
|
||||
isActive() {
|
||||
return this.activeStore?.activeNode?.source === this.source.source
|
||||
return this.activeStore.activeNode?.source === this.source.source
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return this.source.status === NodeStatus.LOADING
|
||||
},
|
||||
|
||||
actionContext(): ActionContextSingle {
|
||||
return {
|
||||
nodes: [this.source],
|
||||
view: this.activeStore.activeView!,
|
||||
folder: this.activeStore.activeFolder!,
|
||||
contents: [],
|
||||
}
|
||||
},
|
||||
|
||||
// Enabled action that are displayed inline
|
||||
enabledInlineActions() {
|
||||
if (this.filesListWidth < 768 || this.gridMode) {
|
||||
|
|
@ -208,7 +211,7 @@ export default defineComponent({
|
|||
}
|
||||
return this.enabledFileActions.filter((action) => {
|
||||
try {
|
||||
return action?.inline?.(this.source, this.currentView)
|
||||
return action?.inline?.(this.actionContext) === true
|
||||
} catch (error) {
|
||||
logger.error('Error while checking if action is inline', { action, error })
|
||||
return false
|
||||
|
|
@ -302,12 +305,12 @@ export default defineComponent({
|
|||
if ((this.gridMode || (this.filesListWidth < 768 && action.inline)) && typeof action.title === 'function') {
|
||||
// if an inline action is rendered in the menu for
|
||||
// lack of space we use the title first if defined
|
||||
const title = action.title([this.source], this.currentView)
|
||||
const title = action.title(this.actionContext)
|
||||
if (title) {
|
||||
return title
|
||||
}
|
||||
}
|
||||
return action.displayName([this.source], this.currentView)
|
||||
return action.displayName(this.actionContext)
|
||||
} catch (error) {
|
||||
logger.error('Error while getting action display name', { action, error })
|
||||
// Not ideal, but better than nothing
|
||||
|
|
@ -319,7 +322,7 @@ export default defineComponent({
|
|||
if (!this.isActive) {
|
||||
return false
|
||||
}
|
||||
return this.activeStore?.activeAction?.id === action.id
|
||||
return this.activeStore.activeAction?.id === action.id
|
||||
},
|
||||
|
||||
async onActionClick(action) {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,8 @@ import { translate as t } from '@nextcloud/l10n'
|
|||
import { defineComponent, inject } from 'vue'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
|
||||
import { useNavigation } from '../../composables/useNavigation.ts'
|
||||
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
|
||||
import logger from '../../logger.ts'
|
||||
import { useActiveStore } from '../../store/active.ts'
|
||||
import { useRenamingStore } from '../../store/renaming.ts'
|
||||
import { useUserConfigStore } from '../../store/userconfig.ts'
|
||||
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
|
||||
|
|
@ -97,20 +96,18 @@ export default defineComponent({
|
|||
|
||||
setup() {
|
||||
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
|
||||
const { currentView } = useNavigation(true)
|
||||
const { directory } = useRouteParameters()
|
||||
const filesListWidth = useFileListWidth()
|
||||
const renamingStore = useRenamingStore()
|
||||
const userConfigStore = useUserConfigStore()
|
||||
const { activeFolder, activeView } = useActiveStore()
|
||||
|
||||
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
|
||||
|
||||
return {
|
||||
currentView,
|
||||
activeFolder,
|
||||
activeView,
|
||||
defaultFileAction,
|
||||
directory,
|
||||
filesListWidth,
|
||||
|
||||
renamingStore,
|
||||
userConfigStore,
|
||||
}
|
||||
|
|
@ -154,7 +151,12 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (this.defaultFileAction) {
|
||||
const displayName = this.defaultFileAction.displayName([this.source], this.currentView)
|
||||
const displayName = this.defaultFileAction.displayName({
|
||||
nodes: [this.source],
|
||||
view: this.activeView,
|
||||
folder: this.activeFolder,
|
||||
contents: [],
|
||||
})
|
||||
return {
|
||||
is: 'button',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@
|
|||
<!-- Actions -->
|
||||
<FileEntryActions
|
||||
ref="actions"
|
||||
:opened.sync="openedMenu"
|
||||
:class="`files-list__row-actions-${uniqueId}`"
|
||||
:grid-mode="true"
|
||||
:opened.sync="openedMenu"
|
||||
:source="source" />
|
||||
</tr>
|
||||
</template>
|
||||
|
|
@ -79,9 +79,10 @@ import FileEntryActions from './FileEntry/FileEntryActions.vue'
|
|||
import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
|
||||
import FileEntryName from './FileEntry/FileEntryName.vue'
|
||||
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import { useFileListWidth } from '../composables/useFileListWidth.ts'
|
||||
import { useRouteParameters } from '../composables/useRouteParameters.ts'
|
||||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
import { useActiveStore } from '../store/active.ts'
|
||||
import { useDragAndDropStore } from '../store/dragging.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useRenamingStore } from '../store/renaming.ts'
|
||||
|
|
@ -105,29 +106,35 @@ export default defineComponent({
|
|||
|
||||
inheritAttrs: false,
|
||||
|
||||
// keep in sync with FileEntry.vue
|
||||
setup() {
|
||||
const actionsMenuStore = useActionsMenuStore()
|
||||
const draggingStore = useDragAndDropStore()
|
||||
const filesStore = useFilesStore()
|
||||
const renamingStore = useRenamingStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
// The file list is guaranteed to be only shown with active view - thus we can set the `loaded` flag
|
||||
const { currentView } = useNavigation(true)
|
||||
const filesListWidth = useFileListWidth()
|
||||
const {
|
||||
directory: currentDir,
|
||||
fileId: currentFileId,
|
||||
fileId: currentRouteFileId,
|
||||
} = useRouteParameters()
|
||||
|
||||
const {
|
||||
activeFolder,
|
||||
activeNode,
|
||||
activeView,
|
||||
} = useActiveStore()
|
||||
|
||||
return {
|
||||
actionsMenuStore,
|
||||
activeFolder,
|
||||
activeNode,
|
||||
activeView,
|
||||
currentRouteFileId,
|
||||
draggingStore,
|
||||
filesListWidth,
|
||||
filesStore,
|
||||
renamingStore,
|
||||
selectionStore,
|
||||
|
||||
currentDir,
|
||||
currentFileId,
|
||||
currentView,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { FileAction } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
import type { FileSource } from '../types.ts'
|
||||
|
||||
|
|
@ -122,7 +123,9 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
isActive() {
|
||||
return String(this.fileid) === String(this.currentFileId)
|
||||
// Not using activeNode here because we want to
|
||||
// be reactive to the url change directly
|
||||
return String(this.fileid) === String(this.currentRouteFileId)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -235,7 +238,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return actions
|
||||
.filter((action) => {
|
||||
.filter((action: FileAction) => {
|
||||
if (!action.enabled) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -243,7 +246,12 @@ export default defineComponent({
|
|||
// In case something goes wrong, since we don't want to break
|
||||
// the entire list, we filter out actions that throw an error.
|
||||
try {
|
||||
return action.enabled([this.source], this.currentView)
|
||||
return action.enabled({
|
||||
nodes: [this.source],
|
||||
view: this.activeView,
|
||||
folder: this.activeFolder!,
|
||||
contents: this.nodes,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error while checking action', { action, error })
|
||||
return false
|
||||
|
|
@ -253,7 +261,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
defaultFileAction() {
|
||||
return this.enabledFileActions.find((action) => action.default !== undefined)
|
||||
return this.enabledFileActions.find((action: FileAction) => action.default !== undefined)
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -378,14 +386,29 @@ export default defineComponent({
|
|||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// Execute the first default action if any
|
||||
this.defaultFileAction.exec(this.source, this.currentView, this.currentDir)
|
||||
this.defaultFileAction.exec({
|
||||
nodes: [this.source],
|
||||
folder: this.activeFolder!,
|
||||
contents: this.nodes,
|
||||
view: this.activeView!,
|
||||
})
|
||||
},
|
||||
|
||||
openDetailsIfAvailable(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (sidebarAction?.enabled?.([this.source], this.currentView)) {
|
||||
sidebarAction.exec(this.source, this.currentView, this.currentDir)
|
||||
if (sidebarAction?.enabled?.({
|
||||
nodes: [this.source],
|
||||
folder: this.activeFolder!,
|
||||
contents: this.nodes,
|
||||
view: this.activeView!,
|
||||
})) {
|
||||
sidebarAction.exec({
|
||||
nodes: [this.source],
|
||||
folder: this.activeFolder!,
|
||||
contents: this.nodes,
|
||||
view: this.activeView!,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -468,7 +491,7 @@ export default defineComponent({
|
|||
const fileTree = await dataTransferToFileTree(items)
|
||||
|
||||
// We might not have the target directory fetched yet
|
||||
const contents = await this.currentView?.getContents(this.source.path)
|
||||
const contents = await this.activeView?.getContents(this.source.path)
|
||||
const folder = contents?.folder
|
||||
if (!folder) {
|
||||
showError(this.t('files', 'Target folder does not exist any more'))
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@
|
|||
:close-after-click="!isValidMenu(action)"
|
||||
:data-cy-files-list-selection-action="action.id"
|
||||
:is-menu="isValidMenu(action)"
|
||||
:aria-label="action.displayName(nodes, currentView) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */"
|
||||
:title="action.title?.(nodes, currentView)"
|
||||
:aria-label="action.displayName(actionContext) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */"
|
||||
:title="action.title?.(actionContext)"
|
||||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="loading === action.id" :size="18" />
|
||||
<NcIconSvgWrapper v-else :svg="action.iconSvgInline(nodes, currentView)" />
|
||||
<NcIconSvgWrapper v-else :svg="action.iconSvgInline(actionContext)" />
|
||||
</template>
|
||||
{{ action.displayName(nodes, currentView) }}
|
||||
{{ action.displayName(actionContext) }}
|
||||
</NcActionButton>
|
||||
|
||||
<!-- Submenu actions list-->
|
||||
|
|
@ -55,14 +55,14 @@
|
|||
class="files-list__row-actions-batch--submenu"
|
||||
close-after-click
|
||||
:data-cy-files-list-selection-action="action.id"
|
||||
:aria-label="action.displayName(nodes, currentView) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */"
|
||||
:title="action.title?.(nodes, currentView)"
|
||||
:aria-label="action.displayName(actionContext) + ' ' + t('files', '(selected)') /** TRANSLATORS: Selected like 'selected files and folders' */"
|
||||
:title="action.title?.(actionContext)"
|
||||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="loading === action.id" :size="18" />
|
||||
<NcIconSvgWrapper v-else :svg="action.iconSvgInline(nodes, currentView)" />
|
||||
<NcIconSvgWrapper v-else :svg="action.iconSvgInline(actionContext)" />
|
||||
</template>
|
||||
{{ action.displayName(nodes, currentView) }}
|
||||
{{ action.displayName(actionContext) }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcActions>
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { FileAction, Node, View } from '@nextcloud/files'
|
||||
import type { ActionContext, FileAction, Node, View } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
import type { FileSource } from '../types.ts'
|
||||
|
||||
|
|
@ -84,10 +84,10 @@ import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
|||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
|
||||
import { useFileListWidth } from '../composables/useFileListWidth.ts'
|
||||
import { useRouteParameters } from '../composables/useRouteParameters.ts'
|
||||
import logger from '../logger.ts'
|
||||
import actionsMixins from '../mixins/actionsMixin.ts'
|
||||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
import { useActiveStore } from '../store/active.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
|
||||
|
|
@ -120,19 +120,18 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup() {
|
||||
const { activeFolder } = useActiveStore()
|
||||
const actionsMenuStore = useActionsMenuStore()
|
||||
const filesStore = useFilesStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
const fileListWidth = useFileListWidth()
|
||||
const { directory } = useRouteParameters()
|
||||
|
||||
const boundariesElement = document.getElementById('app-content-vue')
|
||||
|
||||
return {
|
||||
directory,
|
||||
fileListWidth,
|
||||
|
||||
actionsMenuStore,
|
||||
activeFolder,
|
||||
fileListWidth,
|
||||
filesStore,
|
||||
selectionStore,
|
||||
|
||||
|
|
@ -157,7 +156,7 @@ export default defineComponent({
|
|||
// but children actions always need to have it
|
||||
.filter((action) => action.execBatch || !action.parent)
|
||||
// We filter out actions that are not enabled for the current selection
|
||||
.filter((action) => !action.enabled || action.enabled(this.nodes, this.currentView))
|
||||
.filter((action) => !action.enabled || action.enabled(this.actionContext))
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
},
|
||||
|
||||
|
|
@ -227,6 +226,15 @@ export default defineComponent({
|
|||
return [...this.enabledInlineActions, ...menuActions]
|
||||
},
|
||||
|
||||
actionContext(): ActionContext {
|
||||
return {
|
||||
nodes: this.nodes,
|
||||
view: this.currentView,
|
||||
folder: this.activeFolder!,
|
||||
contents: this.nodes,
|
||||
}
|
||||
},
|
||||
|
||||
nodes() {
|
||||
return this.selectedNodes
|
||||
.map((source) => this.getNode(source))
|
||||
|
|
@ -294,7 +302,7 @@ export default defineComponent({
|
|||
})
|
||||
|
||||
// Dispatch action execution
|
||||
const results = await action.execBatch(this.nodes, this.currentView, this.directory)
|
||||
const results = await action.execBatch(this.actionContext)
|
||||
|
||||
// Check if all actions returned null
|
||||
if (!results.some((result) => result !== null)) {
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ export default defineComponent({
|
|||
subscribe('files:sidebar:closed', this.onSidebarClosed)
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
|
||||
mainContent.removeEventListener('dragover', this.onDragOver)
|
||||
unsubscribe('files:sidebar:closed', this.onSidebarClosed)
|
||||
|
|
@ -311,9 +311,19 @@ export default defineComponent({
|
|||
// Open the sidebar for the given URL fileid
|
||||
// iif we just loaded the app.
|
||||
const node = this.nodes.find((n) => n.fileid === fileId) as NcNode
|
||||
if (node && sidebarAction?.enabled?.([node], this.currentView)) {
|
||||
if (node && sidebarAction?.enabled?.({
|
||||
nodes: [node],
|
||||
folder: this.currentFolder,
|
||||
view: this.currentView,
|
||||
contents: this.nodes,
|
||||
})) {
|
||||
logger.debug('Opening sidebar on file ' + node.path, { node })
|
||||
sidebarAction.exec(node, this.currentView, this.currentFolder.path)
|
||||
sidebarAction.exec({
|
||||
nodes: [node],
|
||||
folder: this.currentFolder,
|
||||
view: this.currentView,
|
||||
contents: this.nodes,
|
||||
})
|
||||
return
|
||||
}
|
||||
logger.warn(`Failed to open sidebar on file ${fileId}, file isn't cached yet !`, { fileId, node })
|
||||
|
|
@ -382,7 +392,12 @@ export default defineComponent({
|
|||
// Get only default actions (visible and hidden)
|
||||
.filter((action) => !!action?.default)
|
||||
// Find actions that are either always enabled or enabled for the current node
|
||||
.filter((action) => !action.enabled || action.enabled([node], this.currentView))
|
||||
.filter((action) => (!action.enabled || action.enabled({
|
||||
nodes: [node],
|
||||
view: this.currentView,
|
||||
folder: this.currentFolder,
|
||||
contents: this.nodes,
|
||||
})))
|
||||
.filter((action) => action.id !== 'download')
|
||||
// Sort enabled default actions by order
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
|
@ -393,7 +408,12 @@ export default defineComponent({
|
|||
// So if there is an enabled default action, so execute it
|
||||
if (defaultAction) {
|
||||
logger.debug('Opening file ' + node.path, { node })
|
||||
return await defaultAction.exec(node, this.currentView, this.currentFolder.path)
|
||||
return await defaultAction.exec({
|
||||
nodes: [node],
|
||||
view: this.currentView,
|
||||
folder: this.currentFolder,
|
||||
contents: this.nodes,
|
||||
})
|
||||
}
|
||||
}
|
||||
// The file is either a folder or has no default action other than downloading
|
||||
|
|
@ -449,6 +469,7 @@ export default defineComponent({
|
|||
|
||||
if (nextNode && nextNode?.fileid) {
|
||||
this.setActiveNode(nextNode)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -464,6 +485,7 @@ export default defineComponent({
|
|||
|
||||
if (nextNode && nextNode?.fileid) {
|
||||
this.setActiveNode(nextNode)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -81,18 +81,27 @@ describe('HotKeysService testing', () => {
|
|||
file = new File({
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
root: '/files/admin',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const root = new Folder({ owner: 'test', source: 'https://cloud.domain.com/remote.php/dav/files/admin/', id: 1, permissions: Permission.CREATE })
|
||||
const root = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
|
||||
root: '/files/admin',
|
||||
owner: 'admin',
|
||||
permissions: Permission.CREATE,
|
||||
})
|
||||
|
||||
const files = useFilesStore(getPinia())
|
||||
files.setRoot({ service: 'files', root })
|
||||
|
||||
// Setting the view first as it reset the active node
|
||||
activeStore.activeView = view
|
||||
activeStore.activeNode = file
|
||||
activeStore.activeFolder = root
|
||||
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: { async open() {}, setActiveTab: () => {} } } }
|
||||
|
|
@ -148,7 +157,7 @@ describe('HotKeysService testing', () => {
|
|||
})
|
||||
|
||||
it('Pressing Delete should delete the file', async () => {
|
||||
// @ts-expect-error unit testing
|
||||
// @ts-expect-error unit testing - private method access
|
||||
vi.spyOn(deleteAction._action, 'exec').mockResolvedValue(() => true)
|
||||
|
||||
dispatchEvent({ key: 'Delete', code: 'Delete' })
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export function useNavigation<T extends boolean>(_loaded?: T) {
|
|||
type MaybeView = T extends true ? View : (View | null)
|
||||
const navigation = getNavigation()
|
||||
const views: ShallowRef<View[]> = shallowRef(navigation.views)
|
||||
|
||||
/** @deprecated use activeStore.activeView instead */
|
||||
const currentView: ShallowRef<MaybeView> = shallowRef(navigation.active as MaybeView)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addNewFileMenuEntry, registerDavProperty, registerFileAction } from '@nextcloud/files'
|
||||
import { addNewFileMenuEntry, registerFileAction } from '@nextcloud/files'
|
||||
import { registerDavProperty } from '@nextcloud/files/dav'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import { registerConvertActions } from './actions/convertAction.ts'
|
||||
import { action as deleteAction } from './actions/deleteAction.ts'
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import type { Upload } from '@nextcloud/upload'
|
|||
import type { RootDirectory } from './DropServiceUtils.ts'
|
||||
|
||||
import { showError, showInfo, showSuccess, showWarning } from '@nextcloud/dialogs'
|
||||
import { davRootPath, NodeStatus } from '@nextcloud/files'
|
||||
import { NodeStatus } from '@nextcloud/files'
|
||||
import { getRootPath } from '@nextcloud/files/dav'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { joinPaths } from '@nextcloud/paths'
|
||||
import { getUploader, hasConflict } from '@nextcloud/upload'
|
||||
|
|
@ -125,7 +126,7 @@ export async function onDropExternalFiles(root: RootDirectory, destination: Fold
|
|||
// If the file is a directory, we need to create it first
|
||||
// then browse its tree and upload its contents.
|
||||
if (file instanceof Directory) {
|
||||
const absolutePath = joinPaths(davRootPath, destination.path, relativePath)
|
||||
const absolutePath = joinPaths(getRootPath(), destination.path, relativePath)
|
||||
try {
|
||||
logger.debug('Processing directory', { relativePath })
|
||||
await createDirectoryIfNotExists(absolutePath)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { FileStat, ResponseDataDetailed } from 'webdav'
|
|||
|
||||
import { showInfo, showWarning } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { davGetClient, davGetDefaultPropfind, davResultToNode } from '@nextcloud/files'
|
||||
import { getClient, getDefaultPropfind, resultToNode } from '@nextcloud/files/dav'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { openConflictPicker } from '@nextcloud/upload'
|
||||
import logger from '../logger.ts'
|
||||
|
|
@ -135,13 +135,13 @@ function readDirectory(directory: FileSystemDirectoryEntry): Promise<FileSystemE
|
|||
* @param absolutePath
|
||||
*/
|
||||
export async function createDirectoryIfNotExists(absolutePath: string) {
|
||||
const davClient = davGetClient()
|
||||
const davClient = getClient()
|
||||
const dirExists = await davClient.exists(absolutePath)
|
||||
if (!dirExists) {
|
||||
logger.debug('Directory does not exist, creating it', { absolutePath })
|
||||
await davClient.createDirectory(absolutePath, { recursive: true })
|
||||
const stat = await davClient.stat(absolutePath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
|
||||
emit('files:node:created', davResultToNode(stat.data))
|
||||
const stat = await davClient.stat(absolutePath, { details: true, data: getDefaultPropfind() }) as ResponseDataDetailed<FileStat>
|
||||
emit('files:node:created', resultToNode(stat.data))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
import type { ContentsWithRoot } from '@nextcloud/files'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { davRemoteURL, davRootPath, Folder, getFavoriteNodes, Permission } from '@nextcloud/files'
|
||||
import { Folder, Permission } from '@nextcloud/files'
|
||||
import { getFavoriteNodes, getRemoteURL, getRootPath } from '@nextcloud/files/dav'
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
import { getContents as filesContents } from './Files.ts'
|
||||
import { client } from './WebdavClient.ts'
|
||||
|
|
@ -32,8 +33,8 @@ export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
|
|||
contents,
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: `${davRemoteURL}${davRootPath}`,
|
||||
root: davRootPath,
|
||||
source: `${getRemoteURL()}${getRootPath()}`,
|
||||
root: getRootPath(),
|
||||
owner: getCurrentUser()?.uid || null,
|
||||
permissions: Permission.READ,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { ContentsWithRoot, File, Folder, Node } from '@nextcloud/files'
|
||||
import type { ContentsWithRoot, File, Folder } from '@nextcloud/files'
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
||||
import { resultToNode as davResultToNode, defaultRootPath, getDefaultPropfind } from '@nextcloud/files/dav'
|
||||
import { getDefaultPropfind, getRootPath, resultToNode } from '@nextcloud/files/dav'
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
import { join } from 'path'
|
||||
import logger from '../logger.ts'
|
||||
|
|
@ -14,12 +14,6 @@ import { getPinia } from '../store/index.ts'
|
|||
import { useSearchStore } from '../store/search.ts'
|
||||
import { client } from './WebdavClient.ts'
|
||||
import { searchNodes } from './WebDavSearch.ts'
|
||||
/**
|
||||
* Slim wrapper over `@nextcloud/files` `davResultToNode` to allow using the function with `Array.map`
|
||||
*
|
||||
* @param stat The result returned by the webdav library
|
||||
*/
|
||||
export const resultToNode = (stat: FileStat): Node => davResultToNode(stat)
|
||||
|
||||
/**
|
||||
* Get contents implementation for the files view.
|
||||
|
|
@ -49,7 +43,7 @@ export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
|
|||
* @param path - The path to get the contents
|
||||
*/
|
||||
export function defaultGetContents(path: string): CancelablePromise<ContentsWithRoot> {
|
||||
path = join(defaultRootPath, path)
|
||||
path = join(getRootPath(), path)
|
||||
const controller = new AbortController()
|
||||
const propfindPayload = getDefaultPropfind()
|
||||
|
||||
|
|
@ -66,7 +60,7 @@ export function defaultGetContents(path: string): CancelablePromise<ContentsWith
|
|||
|
||||
const root = contentsResponse.data[0]
|
||||
const contents = contentsResponse.data.slice(1)
|
||||
if (root.filename !== path && `${root.filename}/` !== path) {
|
||||
if (root?.filename !== path && `${root?.filename}/` !== path) {
|
||||
logger.debug(`Exepected "${path}" but got filename "${root.filename}" instead.`)
|
||||
throw new Error('Root node does not match requested path')
|
||||
}
|
||||
|
|
@ -99,7 +93,7 @@ async function getLocalSearch(path: string, query: string, signal: AbortSignal):
|
|||
const filesStore = useFilesStore(getPinia())
|
||||
let folder = filesStore.getDirectoryByPath('files', path)
|
||||
if (!folder) {
|
||||
const rootPath = join(defaultRootPath, path)
|
||||
const rootPath = join(getRootPath(), path)
|
||||
const stat = await client.stat(rootPath, { details: true }) as ResponseDataDetailed<FileStat>
|
||||
folder = resultToNode(stat.data) as Folder
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { CancelablePromise } from 'cancelable-promise'
|
|||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { davRemoteURL } from '@nextcloud/files'
|
||||
import { getRemoteURL } from '@nextcloud/files/dav'
|
||||
import { getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
|
||||
import { dirname, encodePath, joinPaths } from '@nextcloud/paths'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
|
|
@ -34,7 +34,7 @@ export interface TreeNode {
|
|||
|
||||
export const folderTreeId = 'folders'
|
||||
|
||||
export const sourceRoot = `${davRemoteURL}/files/${getCurrentUser()?.uid}`
|
||||
export const sourceRoot = `${getRemoteURL()}/files/${getCurrentUser()?.uid}`
|
||||
|
||||
const collator = Intl.Collator(
|
||||
[getLanguage(), getCanonicalLocale()],
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { registerDavProperty } from '@nextcloud/files'
|
||||
import { registerDavProperty } from '@nextcloud/files/dav'
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { ContentsWithRoot, Node } from '@nextcloud/files'
|
||||
import type { FileStat, ResponseDataDetailed, SearchResult } from 'webdav'
|
||||
import type { ResponseDataDetailed, SearchResult } from 'webdav'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { davGetRecentSearch, davRemoteURL, davResultToNode, davRootPath, Folder, Permission } from '@nextcloud/files'
|
||||
import { getBaseUrl } from '@nextcloud/router'
|
||||
import { Folder, Permission } from '@nextcloud/files'
|
||||
import { getRecentSearch, getRemoteURL, getRootPath, resultToNode } from '@nextcloud/files/dav'
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
import { useUserConfigStore } from '../store/userconfig.ts'
|
||||
|
|
@ -15,14 +15,6 @@ import { client } from './WebdavClient.ts'
|
|||
|
||||
const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 14))
|
||||
|
||||
/**
|
||||
* Helper to map a WebDAV result to a Nextcloud node
|
||||
* The search endpoint already includes the dav remote URL so we must not include it in the source
|
||||
*
|
||||
* @param stat the WebDAV result
|
||||
*/
|
||||
const resultToNode = (stat: FileStat) => davResultToNode(stat, davRootPath, getBaseUrl())
|
||||
|
||||
/**
|
||||
* Get recently changed nodes
|
||||
*
|
||||
|
|
@ -48,18 +40,22 @@ export function getContents(path = '/'): CancelablePromise<ContentsWithRoot> {
|
|||
const contentsResponse = await client.search('/', {
|
||||
signal: controller.signal,
|
||||
details: true,
|
||||
data: davGetRecentSearch(lastTwoWeeksTimestamp),
|
||||
data: getRecentSearch(lastTwoWeeksTimestamp),
|
||||
}) as ResponseDataDetailed<SearchResult>
|
||||
|
||||
const contents = contentsResponse.data.results
|
||||
.map(resultToNode)
|
||||
.map((stat) => {
|
||||
// The search endpoint already includes the dav remote URL so we must not include it in the source
|
||||
stat.filename = stat.filename.replace('/remote.php/dav', '')
|
||||
return resultToNode(stat)
|
||||
})
|
||||
.filter(filterHidden)
|
||||
|
||||
return {
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: `${davRemoteURL}${davRootPath}`,
|
||||
root: davRootPath,
|
||||
source: `${getRemoteURL()}${getRootPath()}`,
|
||||
root: getRootPath(),
|
||||
owner: getCurrentUser()?.uid || null,
|
||||
permissions: Permission.READ,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ vi.mock('./WebDavSearch.ts', () => ({ searchNodes }))
|
|||
vi.mock('@nextcloud/auth')
|
||||
|
||||
describe('Search service', () => {
|
||||
const fakeFolder = new Folder({ owner: 'owner', source: 'https://cloud.example.com/remote.php/dav/files/owner/folder', root: '/files/owner' })
|
||||
const fakeFolder = new Folder({
|
||||
owner: 'owner',
|
||||
source: 'https://cloud.example.com/remote.php/dav/files/owner/folder',
|
||||
root: '/files/owner',
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
window.OCP ??= {}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { ContentsWithRoot } from '@nextcloud/files'
|
|||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { Folder, Permission } from '@nextcloud/files'
|
||||
import { defaultRemoteURL } from '@nextcloud/files/dav'
|
||||
import { defaultRemoteURL, getRootPath } from '@nextcloud/files/dav'
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
import logger from '../logger.ts'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
|
|
@ -30,9 +30,10 @@ export function getContents(): CancelablePromise<ContentsWithRoot> {
|
|||
contents,
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: `${defaultRemoteURL}#search`,
|
||||
source: `${defaultRemoteURL}${getRootPath()}}#search`,
|
||||
owner: getCurrentUser()!.uid,
|
||||
permissions: Permission.READ,
|
||||
root: getRootPath(),
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ describe('Path store', () => {
|
|||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
root = new Folder({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/', id: 1 })
|
||||
root = new Folder({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/',
|
||||
id: 1,
|
||||
root: '/files/test',
|
||||
})
|
||||
files = useFilesStore()
|
||||
files.setRoot({ service: 'files', root })
|
||||
|
||||
|
|
@ -30,7 +35,12 @@ describe('Path store', () => {
|
|||
expect(store.paths).toEqual({})
|
||||
|
||||
// create the folder
|
||||
const node = new Folder({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/folder', id: 2 })
|
||||
const node = new Folder({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/folder',
|
||||
id: 2,
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node)
|
||||
|
||||
// see that the path is added
|
||||
|
|
@ -45,7 +55,13 @@ describe('Path store', () => {
|
|||
expect(store.paths).toEqual({})
|
||||
|
||||
// create the file
|
||||
const node = new File({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/file.txt', id: 2, mime: 'text/plain' })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/file.txt',
|
||||
id: 2,
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node)
|
||||
|
||||
// see that there are still no paths
|
||||
|
|
@ -60,7 +76,13 @@ describe('Path store', () => {
|
|||
expect(store.paths).toEqual({})
|
||||
|
||||
// create the file
|
||||
const node1 = new File({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/file.txt', id: 2, mime: 'text/plain' })
|
||||
const node1 = new File({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/file.txt',
|
||||
id: 2,
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node1)
|
||||
|
||||
// see that there are still no paths
|
||||
|
|
@ -70,7 +92,13 @@ describe('Path store', () => {
|
|||
expect(root._children).toEqual([node1.source])
|
||||
|
||||
// create the same named file again
|
||||
const node2 = new File({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/file.txt', id: 2, mime: 'text/plain' })
|
||||
const node2 = new File({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/file.txt',
|
||||
id: 2,
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node2)
|
||||
|
||||
// see that there are still no paths and the children are not duplicated
|
||||
|
|
@ -83,7 +111,12 @@ describe('Path store', () => {
|
|||
expect(store.paths).toEqual({})
|
||||
|
||||
// create the file
|
||||
const node1 = new Folder({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/folder', id: 2 })
|
||||
const node1 = new Folder({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/folder',
|
||||
id: 2,
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node1)
|
||||
|
||||
// see the path is added
|
||||
|
|
@ -93,7 +126,12 @@ describe('Path store', () => {
|
|||
expect(root._children).toEqual([node1.source])
|
||||
|
||||
// create the same named file again
|
||||
const node2 = new Folder({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/folder', id: 2 })
|
||||
const node2 = new Folder({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/folder',
|
||||
id: 2,
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node2)
|
||||
|
||||
// see that there is still only one paths and the children are not duplicated
|
||||
|
|
@ -102,7 +140,12 @@ describe('Path store', () => {
|
|||
})
|
||||
|
||||
test('Folder is deleted', () => {
|
||||
const node = new Folder({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/folder', id: 2 })
|
||||
const node = new Folder({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/folder',
|
||||
id: 2,
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node)
|
||||
// see that the path is added and the children are set-up
|
||||
expect(store.paths).toEqual({ files: { [node.path]: node.source } })
|
||||
|
|
@ -116,7 +159,13 @@ describe('Path store', () => {
|
|||
})
|
||||
|
||||
test('File is deleted', () => {
|
||||
const node = new File({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/file.txt', id: 2, mime: 'text/plain' })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/file.txt',
|
||||
id: 2,
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node)
|
||||
// see that the children are set-up
|
||||
expect(root._children).toEqual([node.source])
|
||||
|
|
@ -127,7 +176,12 @@ describe('Path store', () => {
|
|||
})
|
||||
|
||||
test('Folder is moved', () => {
|
||||
const node = new Folder({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/folder', id: 2 })
|
||||
const node = new Folder({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/folder',
|
||||
id: 2,
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node)
|
||||
// see that the path is added and the children are set-up
|
||||
expect(store.paths).toEqual({ files: { [node.path]: node.source } })
|
||||
|
|
@ -141,13 +195,21 @@ describe('Path store', () => {
|
|||
|
||||
emit('files:node:moved', { node: renamedNode, oldSource: node.source })
|
||||
// See the path is updated
|
||||
expect(store.paths).toEqual({ files: { [renamedNode.path]: renamedNode.source } })
|
||||
expect(store.paths).toEqual({
|
||||
files: { [renamedNode.path]: renamedNode.source },
|
||||
})
|
||||
// See the child is updated
|
||||
expect(root._children).toEqual([renamedNode.source])
|
||||
})
|
||||
|
||||
test('File is moved', () => {
|
||||
const node = new File({ owner: 'test', source: 'http://example.com/remote.php/dav/files/test/file.txt', id: 2, mime: 'text/plain' })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'http://example.com/remote.php/dav/files/test/file.txt',
|
||||
id: 2,
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
emit('files:node:created', node)
|
||||
// see that the children are set-up
|
||||
expect(root._children).toEqual([node.source])
|
||||
|
|
|
|||
|
|
@ -112,7 +112,12 @@ export function usePathsStore(...args) {
|
|||
}
|
||||
|
||||
// Dummy simple clone of the renamed node from a previous state
|
||||
const oldNode = new File({ source: oldSource, owner: node.owner, mime: node.mime })
|
||||
const oldNode = new File({
|
||||
source: oldSource,
|
||||
owner: node.owner,
|
||||
mime: node.mime,
|
||||
root: node.root,
|
||||
})
|
||||
|
||||
this.deleteNodeFromParentChildren(oldNode)
|
||||
this.addNodeToParentChildren(node)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { FileAction } from '@nextcloud/files'
|
||||
import type { ActionContextSingle, FileAction } from '@nextcloud/files'
|
||||
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { NodeStatus } from '@nextcloud/files'
|
||||
|
|
@ -10,7 +10,6 @@ import { t } from '@nextcloud/l10n'
|
|||
import Vue from 'vue'
|
||||
import logger from '../logger.ts'
|
||||
import { useActiveStore } from '../store/active.ts'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
|
||||
/**
|
||||
* Execute an action on the current active node
|
||||
|
|
@ -18,13 +17,13 @@ import { getPinia } from '../store/index.ts'
|
|||
* @param action The action to execute
|
||||
*/
|
||||
export async function executeAction(action: FileAction) {
|
||||
const activeStore = useActiveStore(getPinia())
|
||||
const currentDir = (window?.OCP?.Files?.Router?.query?.dir || '/') as string
|
||||
const activeStore = useActiveStore()
|
||||
const currentFolder = activeStore.activeFolder
|
||||
const currentNode = activeStore.activeNode
|
||||
const currentView = activeStore.activeView
|
||||
|
||||
if (!currentNode || !currentView) {
|
||||
logger.error('No active node or view', { node: currentNode, view: currentView })
|
||||
if (!currentFolder || !currentNode || !currentView) {
|
||||
logger.error('No active folder, node or view', { folder: currentFolder, node: currentNode, view: currentView })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -33,14 +32,23 @@ export async function executeAction(action: FileAction) {
|
|||
return
|
||||
}
|
||||
|
||||
if (!action.enabled!([currentNode], currentView)) {
|
||||
// @ts-expect-error _children is private
|
||||
const contents = currentFolder?._children || []
|
||||
const context = {
|
||||
nodes: [currentNode],
|
||||
view: currentView,
|
||||
folder: currentFolder,
|
||||
contents,
|
||||
} as ActionContextSingle
|
||||
|
||||
if (!action.enabled!(context)) {
|
||||
logger.debug('Action is not not available for the current context', { action, node: currentNode, view: currentView })
|
||||
return
|
||||
}
|
||||
|
||||
let displayName = action.id
|
||||
try {
|
||||
displayName = action.displayName([currentNode], currentView)
|
||||
displayName = action.displayName(context)
|
||||
} catch (error) {
|
||||
logger.error('Error while getting action display name', { action, error })
|
||||
}
|
||||
|
|
@ -50,7 +58,7 @@ export async function executeAction(action: FileAction) {
|
|||
Vue.set(currentNode, 'status', NodeStatus.LOADING)
|
||||
activeStore.activeAction = action
|
||||
|
||||
const success = await action.exec(currentNode, currentView, currentDir)
|
||||
const success = await action.exec(context)
|
||||
|
||||
// If the action returns null, we stay silent
|
||||
if (success === null || success === undefined) {
|
||||
|
|
|
|||
|
|
@ -499,11 +499,11 @@ export default defineComponent({
|
|||
if (action.enabled === undefined) {
|
||||
return true
|
||||
}
|
||||
return action.enabled(
|
||||
this.currentView!,
|
||||
this.dirContents,
|
||||
this.currentFolder as Folder,
|
||||
)
|
||||
return action.enabled({
|
||||
view: this.currentView!,
|
||||
folder: this.currentFolder!,
|
||||
contents: this.dirContents,
|
||||
})
|
||||
})
|
||||
.toSorted((a, b) => a.order - b.order)
|
||||
return enabledActions
|
||||
|
|
@ -793,7 +793,12 @@ export default defineComponent({
|
|||
if (window?.OCA?.Files?.Sidebar?.setActiveTab) {
|
||||
window.OCA.Files.Sidebar.setActiveTab('sharing')
|
||||
}
|
||||
sidebarAction.exec(this.currentFolder, this.currentView!, this.currentFolder.path)
|
||||
sidebarAction.exec({
|
||||
nodes: [this.source],
|
||||
view: this.currentView,
|
||||
folder: this.currentFolder,
|
||||
contents: this.dirContents,
|
||||
})
|
||||
},
|
||||
|
||||
toggleGridView() {
|
||||
|
|
@ -823,7 +828,12 @@ export default defineComponent({
|
|||
|
||||
const displayName = this.actionDisplayName(action)
|
||||
try {
|
||||
const success = await action.exec(this.source, this.dirContents, this.currentDir)
|
||||
const success = await action.exec({
|
||||
nodes: [this.source],
|
||||
view: this.currentView,
|
||||
folder: this.currentFolder,
|
||||
contents: this.dirContents,
|
||||
})
|
||||
// If the action returns null, we stay silent
|
||||
if (success === null || success === undefined) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -112,7 +112,8 @@ import axios from '@nextcloud/axios'
|
|||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { davRemoteURL, davRootPath, File, Folder, formatFileSize } from '@nextcloud/files'
|
||||
import { File, Folder, formatFileSize } from '@nextcloud/files'
|
||||
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
|
||||
import { encodePath } from '@nextcloud/paths'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { ShareType } from '@nextcloud/sharing'
|
||||
|
|
@ -210,7 +211,7 @@ export default defineComponent({
|
|||
* @return {string}
|
||||
*/
|
||||
davPath() {
|
||||
return `${davRemoteURL}${davRootPath}${encodePath(this.file)}`
|
||||
return `${getRemoteURL()}${getRootPath()}${encodePath(this.file)}`
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -342,7 +343,7 @@ export default defineComponent({
|
|||
this.handleWindowResize()
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
unsubscribe('file:node:deleted', this.onNodeDeleted)
|
||||
window.removeEventListener('resize', this.handleWindowResize)
|
||||
},
|
||||
|
|
@ -451,9 +452,10 @@ export default defineComponent({
|
|||
const isDir = this.fileInfo.type === 'dir'
|
||||
const Node = isDir ? Folder : File
|
||||
const node = new Node({
|
||||
fileid: this.fileInfo.id,
|
||||
source: `${davRemoteURL}${davRootPath}${this.file}`,
|
||||
root: davRootPath,
|
||||
id: this.fileInfo.id,
|
||||
source: `${getRemoteURL()}${getRootPath()}${this.file}`,
|
||||
root: getRootPath(),
|
||||
owner: null,
|
||||
mime: isDir ? undefined : this.fileInfo.mimetype,
|
||||
attributes: {
|
||||
favorite: 1,
|
||||
|
|
|
|||
|
|
@ -157,10 +157,16 @@ describe('Dynamic update of favorite folders', () => {
|
|||
id: 1,
|
||||
source: 'http://nextcloud.local/remote.php/dav/files/admin/Foo/Bar',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
// Exec the action
|
||||
await action.exec(folder, favoritesView, '/')
|
||||
await action.exec({
|
||||
nodes: [folder],
|
||||
view: favoritesView,
|
||||
folder: {} as CFolder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(eventBus.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
|
||||
|
|
@ -202,7 +208,12 @@ describe('Dynamic update of favorite folders', () => {
|
|||
eventBus.subscribe('files:favorites:removed', fo)
|
||||
|
||||
// Exec the action
|
||||
await action.exec(folder, favoritesView, '/')
|
||||
await action.exec({
|
||||
nodes: [folder],
|
||||
view: favoritesView,
|
||||
folder: {} as CFolder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(eventBus.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', folder)
|
||||
|
|
@ -238,10 +249,16 @@ describe('Dynamic update of favorite folders', () => {
|
|||
id: 1,
|
||||
source: 'http://nextcloud.local/remote.php/dav/files/admin/Foo/Bar',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
// Exec the action
|
||||
await action.exec(folder, favoritesView, '/')
|
||||
await action.exec({
|
||||
nodes: [folder],
|
||||
view: favoritesView,
|
||||
folder: {} as CFolder,
|
||||
contents: [],
|
||||
})
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:favorites:added', folder)
|
||||
|
||||
// Create a folder with the same id but renamed
|
||||
|
|
@ -249,6 +266,7 @@ describe('Dynamic update of favorite folders', () => {
|
|||
id: 1,
|
||||
source: 'http://nextcloud.local/remote.php/dav/files/admin/Foo/Bar.renamed',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
// Exec the rename action
|
||||
|
|
|
|||
|
|
@ -38,11 +38,26 @@ describe('Enter credentials action conditions tests', () => {
|
|||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('credentials-external-storage')
|
||||
expect(action.displayName([storage], externalStorageView)).toBe('Enter missing credentials')
|
||||
expect(action.iconSvgInline([storage], externalStorageView)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
view: externalStorageView,
|
||||
nodes: [storage],
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Enter missing credentials')
|
||||
expect(action.iconSvgInline({
|
||||
view: externalStorageView,
|
||||
nodes: [storage],
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBe(DefaultType.DEFAULT)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline!(storage, externalStorageView)).toBe(true)
|
||||
expect(action.inline!({
|
||||
view: externalStorageView,
|
||||
nodes: [storage],
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -119,31 +134,61 @@ describe('Enter credentials action enabled tests', () => {
|
|||
|
||||
test('Disabled with on success storage', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([storage], externalStorageView)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [storage],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for multiple nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([storage, storage], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [storage, storage],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Enabled for missing user auth storage', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([userProvidedStorage], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [userProvidedStorage],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Enabled for missing global user auth storage', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([globalAuthUserStorage], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [globalAuthUserStorage],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for missing config', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([missingConfig], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [missingConfig],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for normal nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([notAStorage], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [notAStorage],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ export const action = new FileAction({
|
|||
displayName: () => t('files', 'Enter missing credentials'),
|
||||
iconSvgInline: () => LoginSvg,
|
||||
|
||||
enabled: (nodes: Node[]) => {
|
||||
enabled: ({ nodes }) => {
|
||||
// Only works on single node
|
||||
if (nodes.length !== 1) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ export const action = new FileAction({
|
|||
return false
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
const { login, password } = await new Promise<CredentialResponse>((resolve) => spawnDialog(
|
||||
defineAsyncComponent(() => import('../views/CredentialsDialog.vue')),
|
||||
{},
|
||||
|
|
@ -96,7 +96,7 @@ export const action = new FileAction({
|
|||
|
||||
if (login && password) {
|
||||
try {
|
||||
await setCredentials(node, login, password)
|
||||
await setCredentials(nodes[0], login, password)
|
||||
showSuccess(t('files_external', 'Credentials successfully set'))
|
||||
} catch (error) {
|
||||
showError(t('files_external', 'Error while setting credentials: {error}', {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
*/
|
||||
|
||||
import type { AxiosError } from '@nextcloud/axios'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw'
|
||||
|
|
@ -23,7 +22,7 @@ export const action = new FileAction({
|
|||
displayName: () => '',
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled: (nodes: Node[]) => {
|
||||
enabled: ({ nodes }) => {
|
||||
return nodes.every((node) => isNodeExternalStorage(node) === true)
|
||||
},
|
||||
exec: async () => null,
|
||||
|
|
@ -34,7 +33,12 @@ export const action = new FileAction({
|
|||
*
|
||||
* @param node The node to render inline
|
||||
*/
|
||||
async renderInline(node: Node) {
|
||||
async renderInline({ nodes }) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return null
|
||||
}
|
||||
|
||||
const node = nodes[0]
|
||||
const span = document.createElement('span')
|
||||
span.className = 'files-list__row-status'
|
||||
span.innerHTML = t('files_external', 'Checking storage …')
|
||||
|
|
|
|||
|
|
@ -38,8 +38,18 @@ describe('Open in files action conditions tests', () => {
|
|||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('open-in-files-external-storage')
|
||||
expect(action.displayName([storage], externalStorageView)).toBe('Open in Files')
|
||||
expect(action.iconSvgInline([storage], externalStorageView)).toBe('')
|
||||
expect(action.displayName({
|
||||
nodes: [storage],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Open in Files')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [storage],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.default).toBe(DefaultType.HIDDEN)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline).toBeUndefined()
|
||||
|
|
@ -58,19 +68,34 @@ describe('Open in files action conditions tests', () => {
|
|||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
expect(action.displayName([failingStorage], externalStorageView)).toBe('Examine this faulty external storage configuration')
|
||||
expect(action.displayName({
|
||||
nodes: [failingStorage],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Examine this faulty external storage configuration')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Open in files action enabled tests', () => {
|
||||
test('Enabled with on valid view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], externalStorageView)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -92,7 +117,12 @@ describe('Open in files action execute tests', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const exec = await action.exec(storage, externalStorageView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [storage],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
|
|
@ -116,7 +146,12 @@ describe('Open in files action execute tests', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const exec = await action.exec(storage, externalStorageView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [storage],
|
||||
view: externalStorageView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
|
@ -13,7 +12,7 @@ import { STORAGE_STATUS } from '../utils/credentialsUtils.ts'
|
|||
|
||||
export const action = new FileAction({
|
||||
id: 'open-in-files-external-storage',
|
||||
displayName: (nodes: Node[]) => {
|
||||
displayName: ({ nodes }) => {
|
||||
const config = nodes?.[0]?.attributes?.config as StorageConfig || { status: STORAGE_STATUS.INDETERMINATE }
|
||||
if (config.status !== STORAGE_STATUS.SUCCESS) {
|
||||
return t('files_external', 'Examine this faulty external storage configuration')
|
||||
|
|
@ -22,10 +21,10 @@ export const action = new FileAction({
|
|||
},
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled: (nodes: Node[], view) => view.id === 'extstoragemounts',
|
||||
enabled: ({ view }) => view.id === 'extstoragemounts',
|
||||
|
||||
async exec(node: Node) {
|
||||
const config = node.attributes.config as StorageConfig
|
||||
async exec({ nodes }) {
|
||||
const config = nodes[0]?.attributes?.config as StorageConfig
|
||||
if (config?.status !== STORAGE_STATUS.SUCCESS) {
|
||||
window.OC.dialogs.confirm(
|
||||
t('files_external', 'There was an error with this external storage. Do you want to review this mount point config in the settings page?'),
|
||||
|
|
@ -45,7 +44,7 @@ export const action = new FileAction({
|
|||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files' },
|
||||
{ dir: node.path },
|
||||
{ dir: nodes[0].path },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ describe('Is node an external storage', () => {
|
|||
scope: 'personal',
|
||||
backend: 'SFTP',
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(true)
|
||||
})
|
||||
|
|
@ -29,6 +30,7 @@ describe('Is node an external storage', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
expect(isNodeExternalStorage(file)).toBe(false)
|
||||
})
|
||||
|
|
@ -42,6 +44,7 @@ describe('Is node an external storage', () => {
|
|||
attributes: {
|
||||
scope: 'personal',
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(false)
|
||||
})
|
||||
|
|
@ -55,6 +58,7 @@ describe('Is node an external storage', () => {
|
|||
attributes: {
|
||||
backend: 'SFTP',
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(false)
|
||||
})
|
||||
|
|
@ -69,6 +73,7 @@ describe('Is node an external storage', () => {
|
|||
scope: 'null',
|
||||
backend: 'SFTP',
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ import { Folder } from '@nextcloud/files'
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { action } from './clearReminderAction.ts'
|
||||
|
||||
const view = {} as unknown as View
|
||||
|
||||
const root = new Folder({
|
||||
owner: 'user',
|
||||
source: 'https://example.com/remote.php/dav/files/user/',
|
||||
root: '/files/user',
|
||||
})
|
||||
|
||||
describe('clearReminderAction', () => {
|
||||
const folder = new Folder({
|
||||
owner: 'user',
|
||||
|
|
@ -16,25 +24,46 @@ describe('clearReminderAction', () => {
|
|||
attributes: {
|
||||
'reminder-due-date': '2024-12-25T10:00:00Z',
|
||||
},
|
||||
root: '/files/user',
|
||||
})
|
||||
|
||||
beforeEach(() => vi.resetAllMocks())
|
||||
|
||||
it('should be enabled for one node with due date', () => {
|
||||
expect(action.enabled!([folder], {} as unknown as View)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('should be disabled with more than one node', () => {
|
||||
expect(action.enabled!([folder, folder], {} as unknown as View)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder, folder],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('should be disabled if no due date', () => {
|
||||
const node = folder.clone()
|
||||
delete node.attributes['reminder-due-date']
|
||||
expect(action.enabled!([node], {} as unknown as View)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [node],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('should have title based on due date', () => {
|
||||
expect(action.title!([folder], {} as unknown as View)).toMatchInlineSnapshot('"Clear reminder – Wednesday, December 25, 2024 at 10:00 AM"')
|
||||
expect(action.title!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toMatchInlineSnapshot('"Clear reminder – Wednesday, December 25, 2024 at 10:00 AM"')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode } from '@nextcloud/files'
|
||||
|
||||
import AlarmOffSvg from '@mdi/svg/svg/alarm-off.svg?raw'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
|
|
@ -17,7 +14,7 @@ export const action = new FileAction({
|
|||
|
||||
displayName: () => t('files_reminders', 'Clear reminder'),
|
||||
|
||||
title: (nodes: INode[]) => {
|
||||
title: ({ nodes }) => {
|
||||
const node = nodes.at(0)!
|
||||
const dueDate = new Date(node.attributes['reminder-due-date'])
|
||||
return `${t('files_reminders', 'Clear reminder')} – ${getVerboseDateString(dueDate)}`
|
||||
|
|
@ -25,7 +22,7 @@ export const action = new FileAction({
|
|||
|
||||
iconSvgInline: () => AlarmOffSvg,
|
||||
|
||||
enabled: (nodes: INode[]) => {
|
||||
enabled: ({ nodes }) => {
|
||||
// Only allow on a single node
|
||||
if (nodes.length !== 1) {
|
||||
return false
|
||||
|
|
@ -35,7 +32,8 @@ export const action = new FileAction({
|
|||
return Boolean(dueDate)
|
||||
},
|
||||
|
||||
async exec(node: INode) {
|
||||
async exec({ nodes }) {
|
||||
const node = nodes.at(0)!
|
||||
if (node.fileid) {
|
||||
try {
|
||||
await clearReminder(node.fileid)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ import { Folder } from '@nextcloud/files'
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { action } from './reminderStatusAction.ts'
|
||||
|
||||
const view = {} as unknown as View
|
||||
|
||||
const root = new Folder({
|
||||
owner: 'user',
|
||||
source: 'https://example.com/remote.php/dav/files/user/',
|
||||
root: '/files/user',
|
||||
})
|
||||
|
||||
describe('reminderStatusAction', () => {
|
||||
const folder = new Folder({
|
||||
owner: 'user',
|
||||
|
|
@ -16,25 +24,46 @@ describe('reminderStatusAction', () => {
|
|||
attributes: {
|
||||
'reminder-due-date': '2024-12-25T10:00:00Z',
|
||||
},
|
||||
root: '/files/user',
|
||||
})
|
||||
|
||||
beforeEach(() => vi.resetAllMocks())
|
||||
|
||||
it('should be enabled for one node with due date', () => {
|
||||
expect(action.enabled!([folder], {} as unknown as View)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('should be disabled with more than one node', () => {
|
||||
expect(action.enabled!([folder, folder], {} as unknown as View)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder, folder],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('should be disabled if no due date', () => {
|
||||
const node = folder.clone()
|
||||
delete node.attributes['reminder-due-date']
|
||||
expect(action.enabled!([node], {} as unknown as View)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [node],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('should have title based on due date', () => {
|
||||
expect(action.title!([folder], {} as unknown as View)).toMatchInlineSnapshot('"Reminder set – Wednesday, December 25, 2024 at 10:00 AM"')
|
||||
expect(action.title!({
|
||||
nodes: [folder],
|
||||
view,
|
||||
folder: root,
|
||||
contents: [],
|
||||
})).toMatchInlineSnapshot('"Reminder set – Wednesday, December 25, 2024 at 10:00 AM"')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode } from '@nextcloud/files'
|
||||
|
||||
import AlarmSvg from '@mdi/svg/svg/alarm.svg?raw'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
|
@ -18,7 +15,7 @@ export const action = new FileAction({
|
|||
|
||||
displayName: () => '',
|
||||
|
||||
title: (nodes: INode[]) => {
|
||||
title: ({ nodes }) => {
|
||||
const node = nodes.at(0)!
|
||||
const dueDate = new Date(node.attributes['reminder-due-date'])
|
||||
return `${t('files_reminders', 'Reminder set')} – ${getVerboseDateString(dueDate)}`
|
||||
|
|
@ -26,17 +23,19 @@ export const action = new FileAction({
|
|||
|
||||
iconSvgInline: () => AlarmSvg,
|
||||
|
||||
enabled: (nodes: INode[]) => {
|
||||
enabled: ({ nodes }) => {
|
||||
// Only allow on a single node
|
||||
if (nodes.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const node = nodes.at(0)!
|
||||
const dueDate = node.attributes['reminder-due-date']
|
||||
return Boolean(dueDate)
|
||||
},
|
||||
|
||||
async exec(node: INode) {
|
||||
async exec({ nodes }) {
|
||||
const node = nodes.at(0)!
|
||||
await pickCustomDate(node)
|
||||
return null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode, View } from '@nextcloud/files'
|
||||
|
||||
import CalendarClockSvg from '@mdi/svg/svg/calendar-clock.svg?raw'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
|
@ -17,7 +14,7 @@ export const action = new FileAction({
|
|||
title: () => t('files_reminders', 'Reminder at custom date & time'),
|
||||
iconSvgInline: () => CalendarClockSvg,
|
||||
|
||||
enabled: (nodes: INode[], view: View) => {
|
||||
enabled: ({ nodes, view }) => {
|
||||
if (view.id === 'trashbin') {
|
||||
return false
|
||||
}
|
||||
|
|
@ -32,8 +29,9 @@ export const action = new FileAction({
|
|||
|
||||
parent: SET_REMINDER_MENU_ID,
|
||||
|
||||
async exec(file: INode) {
|
||||
pickCustomDate(file)
|
||||
async exec({ nodes }) {
|
||||
const node = nodes.at(0)!
|
||||
pickCustomDate(node)
|
||||
return null
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode, View } from '@nextcloud/files'
|
||||
|
||||
import AlarmSvg from '@mdi/svg/svg/alarm.svg?raw'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
|
@ -16,7 +13,7 @@ export const action = new FileAction({
|
|||
displayName: () => t('files_reminders', 'Set reminder'),
|
||||
iconSvgInline: () => AlarmSvg,
|
||||
|
||||
enabled: (nodes: INode[], view: View) => {
|
||||
enabled: ({ nodes, view }) => {
|
||||
if (view.id === 'trashbin') {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode, View } from '@nextcloud/files'
|
||||
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
|
|
@ -72,7 +69,7 @@ function generateFileAction(option: ReminderOption): FileAction | null {
|
|||
// Empty svg to hide the icon
|
||||
iconSvgInline: () => '<svg></svg>',
|
||||
|
||||
enabled: (nodes: INode[], view: View) => {
|
||||
enabled: ({ nodes, view }) => {
|
||||
if (view.id === 'trashbin') {
|
||||
return false
|
||||
}
|
||||
|
|
@ -87,8 +84,9 @@ function generateFileAction(option: ReminderOption): FileAction | null {
|
|||
|
||||
parent: SET_REMINDER_MENU_ID,
|
||||
|
||||
async exec(node: INode) {
|
||||
async exec({ nodes }) {
|
||||
// Can't really happen, but just in case™
|
||||
const node = nodes.at(0)!
|
||||
if (!node.fileid) {
|
||||
logger.error('Failed to set reminder, missing file id')
|
||||
showError(t('files_reminders', 'Failed to set reminder'))
|
||||
|
|
|
|||
20
apps/files_reminders/src/init.ts
Normal file
20
apps/files_reminders/src/init.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { registerFileAction } from '@nextcloud/files'
|
||||
import { registerDavProperty } from '@nextcloud/files/dav'
|
||||
import { action as clearAction } from './files_actions/clearReminderAction.ts'
|
||||
import { action as statusAction } from './files_actions/reminderStatusAction.ts'
|
||||
import { action as customAction } from './files_actions/setReminderCustomAction.ts'
|
||||
import { action as menuAction } from './files_actions/setReminderMenuAction.ts'
|
||||
import { actions as suggestionActions } from './files_actions/setReminderSuggestionActions.ts'
|
||||
|
||||
registerDavProperty('nc:reminder-due-date', { nc: 'http://nextcloud.org/ns' })
|
||||
|
||||
registerFileAction(statusAction)
|
||||
registerFileAction(clearAction)
|
||||
registerFileAction(menuAction)
|
||||
registerFileAction(customAction)
|
||||
suggestionActions.forEach((action) => registerFileAction(action))
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { View } from '@nextcloud/files'
|
||||
import type { Folder, View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as eventBus from '@nextcloud/event-bus'
|
||||
|
|
@ -38,16 +38,32 @@ describe('Accept share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('accept-share')
|
||||
expect(action.displayName([file], pendingShareView)).toBe('Accept share')
|
||||
expect(action.iconSvgInline([file], pendingShareView)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Accept share')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(1)
|
||||
expect(action.inline).toBeDefined()
|
||||
expect(action.inline!(file, pendingShareView)).toBe(true)
|
||||
expect(action.inline!({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Default values for multiple files', () => {
|
||||
|
|
@ -57,6 +73,7 @@ describe('Accept share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 2,
|
||||
|
|
@ -64,9 +81,15 @@ describe('Accept share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.displayName([file1, file2], pendingShareView)).toBe('Accept shares')
|
||||
expect(action.displayName({
|
||||
nodes: [file1, file2],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Accept shares')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -78,20 +101,36 @@ describe('Accept share action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], pendingShareView)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], pendingShareView)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -114,9 +153,15 @@ describe('Accept share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, pendingShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
expect(axios.post).toBeCalledTimes(1)
|
||||
|
|
@ -141,9 +186,15 @@ describe('Accept share action execute tests', () => {
|
|||
remote: 3,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, pendingShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
expect(axios.post).toBeCalledTimes(1)
|
||||
|
|
@ -167,6 +218,7 @@ describe('Accept share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -179,9 +231,15 @@ describe('Accept share action execute tests', () => {
|
|||
id: 456,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2], pendingShareView, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect(axios.post).toBeCalledTimes(2)
|
||||
|
|
@ -208,9 +266,15 @@ describe('Accept share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, pendingShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(false)
|
||||
expect(axios.post).toBeCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import CheckSvg from '@mdi/svg/svg/check.svg?raw'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
|
|
@ -14,13 +12,14 @@ import { pendingSharesViewId } from '../files_views/shares.ts'
|
|||
|
||||
export const action = new FileAction({
|
||||
id: 'accept-share',
|
||||
displayName: (nodes: Node[]) => n('files_sharing', 'Accept share', 'Accept shares', nodes.length),
|
||||
displayName: ({ nodes }) => n('files_sharing', 'Accept share', 'Accept shares', nodes.length),
|
||||
iconSvgInline: () => CheckSvg,
|
||||
|
||||
enabled: (nodes, view) => nodes.length > 0 && view.id === pendingSharesViewId,
|
||||
enabled: ({ nodes, view }) => nodes.length > 0 && view.id === pendingSharesViewId,
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
try {
|
||||
const node = nodes[0]
|
||||
const isRemote = !!node.attributes.remote
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/{shareBase}/pending/{id}', {
|
||||
shareBase: isRemote ? 'remote_shares' : 'shares',
|
||||
|
|
@ -36,8 +35,13 @@ export const action = new FileAction({
|
|||
return false
|
||||
}
|
||||
},
|
||||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
return Promise.all(nodes.map((node) => this.exec(node, view, dir)))
|
||||
async execBatch({ nodes, view, folder, contents }) {
|
||||
return Promise.all(nodes.map((node) => this.exec({
|
||||
nodes: [node],
|
||||
view,
|
||||
folder,
|
||||
contents,
|
||||
})))
|
||||
},
|
||||
|
||||
order: 1,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Folder, View } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, File, FileAction, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
|
@ -33,8 +33,18 @@ describe('Open in files action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('files_sharing:open-in-files')
|
||||
expect(action.displayName([], validViews[0])).toBe('Open in Files')
|
||||
expect(action.iconSvgInline([], validViews[0])).toBe('')
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view: validViews[0]!,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Open in Files')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view: validViews[0]!,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.default).toBe(DefaultType.HIDDEN)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline).toBeUndefined()
|
||||
|
|
@ -45,14 +55,24 @@ describe('Open in files action enabled tests', () => {
|
|||
test('Enabled with on valid view', () => {
|
||||
validViews.forEach((view) => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
invalidViews.forEach((view) => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -71,7 +91,12 @@ describe('Open in files action execute tests', () => {
|
|||
permissions: Permission.READ,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, FileAction, FileType, registerFileAction } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { sharedWithOthersViewId, sharedWithYouViewId, sharesViewId, sharingByLinksViewId } from '../files_views/shares.ts'
|
||||
|
|
@ -14,7 +11,7 @@ export const action = new FileAction({
|
|||
displayName: () => t('files_sharing', 'Open in Files'),
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled: (nodes, view) => [
|
||||
enabled: ({ view }) => [
|
||||
sharesViewId,
|
||||
sharedWithYouViewId,
|
||||
sharedWithOthersViewId,
|
||||
|
|
@ -23,18 +20,18 @@ export const action = new FileAction({
|
|||
// accessible in the files app.
|
||||
].includes(view.id),
|
||||
|
||||
async exec(node: Node) {
|
||||
const isFolder = node.type === FileType.Folder
|
||||
async exec({ nodes }) {
|
||||
const isFolder = nodes[0].type === FileType.Folder
|
||||
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{
|
||||
view: 'files',
|
||||
fileid: String(node.fileid),
|
||||
fileid: String(nodes[0].fileid),
|
||||
},
|
||||
{
|
||||
// If this node is a folder open the folder in files
|
||||
dir: isFolder ? node.path : node.dirname,
|
||||
dir: isFolder ? nodes[0].path : nodes[0].dirname,
|
||||
// otherwise if this is a file, we should open it
|
||||
openfile: isFolder ? undefined : 'true',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,16 +39,32 @@ describe('Reject share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('reject-share')
|
||||
expect(action.displayName([file], pendingShareView)).toBe('Reject share')
|
||||
expect(action.iconSvgInline([file], pendingShareView)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Reject share')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(2)
|
||||
expect(action.inline).toBeDefined()
|
||||
expect(action.inline!(file, pendingShareView)).toBe(true)
|
||||
expect(action.inline!({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Default values for multiple files', () => {
|
||||
|
|
@ -58,6 +74,7 @@ describe('Reject share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 2,
|
||||
|
|
@ -65,9 +82,15 @@ describe('Reject share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.displayName([file1, file2], pendingShareView)).toBe('Reject shares')
|
||||
expect(action.displayName({
|
||||
nodes: [file1, file2],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Reject shares')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -79,20 +102,36 @@ describe('Reject share action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], pendingShareView)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], pendingShareView)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled if some nodes are remote group shares', () => {
|
||||
|
|
@ -104,6 +143,7 @@ describe('Reject share action enabled tests', () => {
|
|||
attributes: {
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
const folder2 = new Folder({
|
||||
id: 2,
|
||||
|
|
@ -114,12 +154,28 @@ describe('Reject share action enabled tests', () => {
|
|||
remote_id: 1,
|
||||
share_type: ShareType.RemoteGroup,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([folder1], pendingShareView)).toBe(true)
|
||||
expect(action.enabled!([folder2], pendingShareView)).toBe(false)
|
||||
expect(action.enabled!([folder1, folder2], pendingShareView)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder1],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder2],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [folder1, folder2],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -142,9 +198,15 @@ describe('Reject share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, pendingShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
expect(axios.delete).toBeCalledTimes(1)
|
||||
|
|
@ -169,9 +231,15 @@ describe('Reject share action execute tests', () => {
|
|||
remote: 3,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, pendingShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
expect(axios.delete).toBeCalledTimes(1)
|
||||
|
|
@ -195,6 +263,7 @@ describe('Reject share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -207,9 +276,15 @@ describe('Reject share action execute tests', () => {
|
|||
id: 456,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2], pendingShareView, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect(axios.delete).toBeCalledTimes(2)
|
||||
|
|
@ -236,9 +311,15 @@ describe('Reject share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, pendingShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: pendingShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(false)
|
||||
expect(axios.delete).toBeCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
|
|
@ -15,10 +13,10 @@ import { pendingSharesViewId } from '../files_views/shares.ts'
|
|||
|
||||
export const action = new FileAction({
|
||||
id: 'reject-share',
|
||||
displayName: (nodes: Node[]) => n('files_sharing', 'Reject share', 'Reject shares', nodes.length),
|
||||
displayName: ({ nodes }) => n('files_sharing', 'Reject share', 'Reject shares', nodes.length),
|
||||
iconSvgInline: () => CloseSvg,
|
||||
|
||||
enabled: (nodes, view) => {
|
||||
enabled: ({ nodes, view }) => {
|
||||
if (view.id !== pendingSharesViewId) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -37,8 +35,9 @@ export const action = new FileAction({
|
|||
return true
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
try {
|
||||
const node = nodes[0]
|
||||
const isRemote = !!node.attributes.remote
|
||||
const shareBase = isRemote ? 'remote_shares' : 'shares'
|
||||
const id = node.attributes.id
|
||||
|
|
@ -64,8 +63,8 @@ export const action = new FileAction({
|
|||
return false
|
||||
}
|
||||
},
|
||||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
return Promise.all(nodes.map((node) => this.exec(node, view, dir)))
|
||||
async execBatch({ nodes, view, folder, contents }) {
|
||||
return Promise.all(nodes.map((node) => this.exec({ nodes: [node], view, folder, contents })))
|
||||
},
|
||||
|
||||
order: 2,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Folder, View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as eventBus from '@nextcloud/event-bus'
|
||||
|
|
@ -39,16 +39,32 @@ describe('Restore share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('restore-share')
|
||||
expect(action.displayName([file], deletedShareView)).toBe('Restore share')
|
||||
expect(action.iconSvgInline([file], deletedShareView)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
view: deletedShareView,
|
||||
nodes: [file],
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Restore share')
|
||||
expect(action.iconSvgInline({
|
||||
view: deletedShareView,
|
||||
nodes: [file],
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(1)
|
||||
expect(action.inline).toBeDefined()
|
||||
expect(action.inline!(file, deletedShareView)).toBe(true)
|
||||
expect(action.inline!({
|
||||
view: deletedShareView,
|
||||
nodes: [file],
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Default values for multiple files', () => {
|
||||
|
|
@ -58,6 +74,7 @@ describe('Restore share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
const file2 = new File({
|
||||
id: 2,
|
||||
|
|
@ -65,9 +82,15 @@ describe('Restore share action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.displayName([file1, file2], deletedShareView)).toBe('Restore shares')
|
||||
expect(action.displayName({
|
||||
view: deletedShareView,
|
||||
nodes: [file1, file2],
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Restore shares')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -79,20 +102,36 @@ describe('Restore share action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], deletedShareView)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view: deletedShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], deletedShareView)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view: deletedShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -115,9 +154,15 @@ describe('Restore share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, deletedShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: deletedShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(true)
|
||||
expect(axios.post).toBeCalledTimes(1)
|
||||
|
|
@ -141,6 +186,7 @@ describe('Restore share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -153,9 +199,15 @@ describe('Restore share action execute tests', () => {
|
|||
id: 456,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.execBatch!([file1, file2], deletedShareView, '/')
|
||||
const exec = await action.execBatch!({
|
||||
nodes: [file1, file2],
|
||||
view: deletedShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect(axios.post).toBeCalledTimes(2)
|
||||
|
|
@ -181,9 +233,15 @@ describe('Restore share action execute tests', () => {
|
|||
id: 123,
|
||||
share_type: ShareType.User,
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, deletedShareView, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view: deletedShareView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
expect(exec).toBe(false)
|
||||
expect(axios.post).toBeCalledTimes(1)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import ArrowULeftTopSvg from '@mdi/svg/svg/arrow-u-left-top.svg?raw'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
|
|
@ -14,14 +12,15 @@ import { deletedSharesViewId } from '../files_views/shares.ts'
|
|||
|
||||
export const action = new FileAction({
|
||||
id: 'restore-share',
|
||||
displayName: (nodes: Node[]) => n('files_sharing', 'Restore share', 'Restore shares', nodes.length),
|
||||
displayName: ({ nodes }) => n('files_sharing', 'Restore share', 'Restore shares', nodes.length),
|
||||
|
||||
iconSvgInline: () => ArrowULeftTopSvg,
|
||||
|
||||
enabled: (nodes, view) => nodes.length > 0 && view.id === deletedSharesViewId,
|
||||
enabled: ({ nodes, view }) => nodes.length > 0 && view.id === deletedSharesViewId,
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
try {
|
||||
const node = nodes[0]
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/deletedshares/{id}', {
|
||||
id: node.attributes.id,
|
||||
})
|
||||
|
|
@ -35,8 +34,8 @@ export const action = new FileAction({
|
|||
return false
|
||||
}
|
||||
},
|
||||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
return Promise.all(nodes.map((node) => this.exec(node, view, dir)))
|
||||
async execBatch({ nodes, view, folder, contents }) {
|
||||
return Promise.all(nodes.map((node) => this.exec({ nodes: [node], view, folder, contents })))
|
||||
},
|
||||
|
||||
order: 1,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import AccountGroupSvg from '@mdi/svg/svg/account-group-outline.svg?raw'
|
||||
import AccountPlusSvg from '@mdi/svg/svg/account-plus-outline.svg?raw'
|
||||
|
|
@ -31,8 +31,8 @@ function isExternal(node: Node) {
|
|||
export const ACTION_SHARING_STATUS = 'sharing-status'
|
||||
export const action = new FileAction({
|
||||
id: ACTION_SHARING_STATUS,
|
||||
displayName(nodes: Node[]) {
|
||||
const node = nodes[0]
|
||||
displayName({ nodes }) {
|
||||
const node = nodes[0]!
|
||||
const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[]
|
||||
|
||||
if (shareTypes.length > 0
|
||||
|
|
@ -43,9 +43,8 @@ export const action = new FileAction({
|
|||
return ''
|
||||
},
|
||||
|
||||
title(nodes: Node[]) {
|
||||
const node = nodes[0]
|
||||
|
||||
title({ nodes }) {
|
||||
const node = nodes[0]!
|
||||
if (node.owner && (node.owner !== getCurrentUser()?.uid || isExternal(node))) {
|
||||
const ownerDisplayName = node?.attributes?.['owner-display-name']
|
||||
return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName })
|
||||
|
|
@ -63,7 +62,7 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
const sharee = [sharees].flat()[0] // the property is sometimes weirdly normalized, so we need to compensate
|
||||
switch (sharee.type) {
|
||||
switch (sharee?.type) {
|
||||
case ShareType.User:
|
||||
return t('files_sharing', 'Shared with {user}', { user: sharee['display-name'] })
|
||||
case ShareType.Group:
|
||||
|
|
@ -73,8 +72,8 @@ export const action = new FileAction({
|
|||
}
|
||||
},
|
||||
|
||||
iconSvgInline(nodes: Node[]) {
|
||||
const node = nodes[0]
|
||||
iconSvgInline({ nodes }) {
|
||||
const node = nodes[0]!
|
||||
const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[]
|
||||
|
||||
// Mixed share types
|
||||
|
|
@ -106,7 +105,7 @@ export const action = new FileAction({
|
|||
return AccountPlusSvg
|
||||
},
|
||||
|
||||
enabled(nodes: Node[]) {
|
||||
enabled({ nodes }) {
|
||||
if (nodes.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -116,7 +115,7 @@ export const action = new FileAction({
|
|||
return false
|
||||
}
|
||||
|
||||
const node = nodes[0]
|
||||
const node = nodes[0]!
|
||||
const shareTypes = node.attributes?.['share-types']
|
||||
const isMixed = Array.isArray(shareTypes) && shareTypes.length > 0
|
||||
|
||||
|
|
@ -137,11 +136,12 @@ export const action = new FileAction({
|
|||
&& (node.permissions & Permission.READ) !== 0
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View, dir: string) {
|
||||
async exec({ nodes, view, folder, contents }) {
|
||||
// You need read permissions to see the sidebar
|
||||
const node = nodes[0]
|
||||
if ((node.permissions & Permission.READ) !== 0) {
|
||||
window.OCA?.Files?.Sidebar?.setActiveTab?.('sharing')
|
||||
sidebarAction.exec(node, view, dir)
|
||||
sidebarAction.exec({ nodes, view, folder, contents })
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
||||
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
|
||||
import { davGetDefaultPropfind, davRemoteURL, davResultToNode, davRootPath, Folder, getNavigation, Permission, View } from '@nextcloud/files'
|
||||
import { Folder, getNavigation, Permission, View } from '@nextcloud/files'
|
||||
import { getDefaultPropfind, getRemoteURL, getRootPath, resultToNode } from '@nextcloud/files/dav'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
import { client } from '../../../files/src/services/WebdavClient.ts'
|
||||
|
|
@ -30,9 +31,9 @@ export default () => {
|
|||
onCancel(() => abort.abort())
|
||||
try {
|
||||
const node = await client.stat(
|
||||
davRootPath,
|
||||
getRootPath(),
|
||||
{
|
||||
data: davGetDefaultPropfind(),
|
||||
data: getDefaultPropfind(),
|
||||
details: true,
|
||||
signal: abort.signal,
|
||||
},
|
||||
|
|
@ -40,12 +41,12 @@ export default () => {
|
|||
|
||||
resolve({
|
||||
// We only have one file as the content
|
||||
contents: [davResultToNode(node.data)],
|
||||
contents: [resultToNode(node.data)],
|
||||
// Fake a readonly folder as root
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: `${davRemoteURL}${davRootPath}`,
|
||||
root: davRootPath,
|
||||
source: `${getRemoteURL()}${getRootPath()}`,
|
||||
root: getRootPath(),
|
||||
owner: null,
|
||||
permissions: Permission.READ,
|
||||
attributes: {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import type { ShareAttribute } from '../sharing.d.ts'
|
|||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { davRemoteURL, davRootPath, File, Folder, Permission } from '@nextcloud/files'
|
||||
import { File, Folder, Permission } from '@nextcloud/files'
|
||||
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import logger from './logger.ts'
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ async function ocsEntryToNode(ocsEntry: any): Promise<Folder | File | null> {
|
|||
|
||||
// Generate path and strip double slashes
|
||||
const path = ocsEntry.path || ocsEntry.file_target || ocsEntry.name
|
||||
const source = `${davRemoteURL}${davRootPath}/${path.replace(/^\/+/, '')}`
|
||||
const source = `${getRemoteURL()}${getRootPath()}/${path.replace(/^\/+/, '')}`
|
||||
|
||||
let mtime = ocsEntry.item_mtime ? new Date((ocsEntry.item_mtime) * 1000) : undefined
|
||||
// Prefer share time if more recent than item mtime
|
||||
|
|
@ -94,7 +95,7 @@ async function ocsEntryToNode(ocsEntry: any): Promise<Folder | File | null> {
|
|||
mtime,
|
||||
size: ocsEntry?.item_size,
|
||||
permissions: ocsEntry?.item_permissions || ocsEntry?.permissions,
|
||||
root: davRootPath,
|
||||
root: getRootPath(),
|
||||
attributes: {
|
||||
...ocsEntry,
|
||||
'has-preview': hasPreview,
|
||||
|
|
@ -272,8 +273,9 @@ export async function getContents(sharedWithYou = true, sharedWithOthers = true,
|
|||
return {
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: `${davRemoteURL}${davRootPath}`,
|
||||
source: `${getRemoteURL()}${getRootPath()}`,
|
||||
owner: getCurrentUser()?.uid || null,
|
||||
root: getRootPath(),
|
||||
}),
|
||||
contents,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const PERMISSION_ALL = 31
|
|||
const axiosMock = vi.hoisted(() => ({
|
||||
request: vi.fn(),
|
||||
}))
|
||||
vi.mock('@nextcloud/axios', async (origial) => ({ ...(await origial()), default: axiosMock }))
|
||||
vi.mock('@nextcloud/axios', async (original) => ({ ...(await original()), default: axiosMock }))
|
||||
vi.mock('@nextcloud/auth')
|
||||
|
||||
const errorSpy = vi.spyOn(window.console, 'error').mockImplementation(() => {})
|
||||
|
|
@ -40,19 +40,34 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' })
|
||||
|
||||
expect(restoreAction.inline).toBeTypeOf('function')
|
||||
expect(restoreAction.inline!(node, trashbinView)).toBe(true)
|
||||
expect(restoreAction.inline!({
|
||||
nodes: [node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('has the display name set', () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' })
|
||||
|
||||
expect(restoreAction.displayName([node], trashbinView)).toBe('Restore')
|
||||
expect(restoreAction.displayName({
|
||||
nodes: [node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Restore')
|
||||
})
|
||||
|
||||
it('has an icon set', () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' })
|
||||
|
||||
const icon = restoreAction.iconSvgInline([node], trashbinView)
|
||||
const icon = restoreAction.iconSvgInline({
|
||||
nodes: [node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(icon).toBeTypeOf('string')
|
||||
expect(isSvg(icon)).toBe(true)
|
||||
})
|
||||
|
|
@ -63,7 +78,12 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
]
|
||||
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!(nodes, trashbinView)).toBe(true)
|
||||
expect(restoreAction.enabled!({
|
||||
nodes,
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('is not enabled when permissions are missing', () => {
|
||||
|
|
@ -72,12 +92,22 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
]
|
||||
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!(nodes, trashbinView)).toBe(false)
|
||||
expect(restoreAction.enabled!({
|
||||
nodes,
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('is not enabled when no nodes are selected', () => {
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!([], trashbinView)).toBe(false)
|
||||
expect(restoreAction.enabled!({
|
||||
nodes: [],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('is not enabled for other views', () => {
|
||||
|
|
@ -95,7 +125,12 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
})
|
||||
|
||||
expect(restoreAction.enabled).toBeTypeOf('function')
|
||||
expect(restoreAction.enabled!(nodes, otherView)).toBe(false)
|
||||
expect(restoreAction.enabled!({
|
||||
nodes,
|
||||
view: otherView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
describe('execute', () => {
|
||||
|
|
@ -106,7 +141,12 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
it('send restore request', async () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
|
||||
|
||||
expect(await restoreAction.exec(node, trashbinView, '/')).toBe(true)
|
||||
expect(await restoreAction.exec({
|
||||
nodes: [node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
expect(axiosMock.request).toBeCalled()
|
||||
expect(axiosMock.request.mock.calls[0]![0].method).toBe('MOVE')
|
||||
expect(axiosMock.request.mock.calls[0]![0].url).toBe(node.encodedSource)
|
||||
|
|
@ -118,7 +158,12 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
|
||||
const emitSpy = vi.spyOn(ncEventBus, 'emit')
|
||||
|
||||
expect(await restoreAction.exec(node, trashbinView, '/')).toBe(true)
|
||||
expect(await restoreAction.exec({
|
||||
nodes: [node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
expect(axiosMock.request).toBeCalled()
|
||||
expect(emitSpy).toBeCalled()
|
||||
expect(emitSpy).toBeCalledWith('files:node:deleted', node)
|
||||
|
|
@ -132,7 +177,12 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
})
|
||||
const emitSpy = vi.spyOn(ncEventBus, 'emit')
|
||||
|
||||
expect(await restoreAction.exec(node, trashbinView, '/')).toBe(false)
|
||||
expect(await restoreAction.exec({
|
||||
nodes: [node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
expect(axiosMock.request).toBeCalled()
|
||||
expect(emitSpy).not.toBeCalled()
|
||||
expect(errorSpy).toBeCalled()
|
||||
|
|
@ -141,7 +191,12 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
it('batch: only returns success if all requests worked', async () => {
|
||||
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
|
||||
|
||||
expect(await restoreAction.execBatch!([node, node], trashbinView, '/')).toStrictEqual([true, true])
|
||||
expect(await restoreAction.execBatch!({
|
||||
nodes: [node, node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toStrictEqual([true, true])
|
||||
expect(axiosMock.request).toBeCalledTimes(2)
|
||||
})
|
||||
|
||||
|
|
@ -151,7 +206,12 @@ describe('files_trashbin: file actions - restore action', () => {
|
|||
axiosMock.request.mockImplementationOnce(() => {
|
||||
throw new Error()
|
||||
})
|
||||
expect(await restoreAction.execBatch!([node, node], trashbinView, '/')).toStrictEqual([false, true])
|
||||
expect(await restoreAction.execBatch!({
|
||||
nodes: [node, node],
|
||||
view: trashbinView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toStrictEqual([false, true])
|
||||
expect(axiosMock.request).toBeCalledTimes(2)
|
||||
expect(errorSpy).toBeCalled()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import svgHistory from '@mdi/svg/svg/history.svg?raw'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios, { isAxiosError } from '@nextcloud/axios'
|
||||
|
|
@ -26,7 +23,7 @@ export const restoreAction = new FileAction({
|
|||
|
||||
iconSvgInline: () => svgHistory,
|
||||
|
||||
enabled(nodes: Node[], view) {
|
||||
enabled({ nodes, view }) {
|
||||
// Only available in the trashbin view
|
||||
if (view.id !== TRASHBIN_VIEW_ID) {
|
||||
return false
|
||||
|
|
@ -39,7 +36,8 @@ export const restoreAction = new FileAction({
|
|||
.every((permission) => Boolean(permission & Permission.READ))
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
async exec({ nodes }) {
|
||||
const node = nodes[0]
|
||||
try {
|
||||
const destination = generateRemoteUrl(encodePath(`dav/trashbin/${getCurrentUser()!.uid}/restore/${node.basename}`))
|
||||
await axios.request({
|
||||
|
|
@ -63,8 +61,8 @@ export const restoreAction = new FileAction({
|
|||
}
|
||||
},
|
||||
|
||||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
return Promise.all(nodes.map((node) => this.exec(node, view, dir)))
|
||||
async execBatch({ nodes, view, folder, contents }) {
|
||||
return Promise.all(nodes.map((node) => this.exec({ nodes: [node], view, folder, contents })))
|
||||
},
|
||||
|
||||
order: 1,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import * as ncDialogs from '@nextcloud/dialogs'
|
||||
import * as ncEventBus from '@nextcloud/event-bus'
|
||||
import { Folder } from '@nextcloud/files'
|
||||
|
|
@ -12,9 +14,11 @@ import * as api from '../services/api.ts'
|
|||
import { emptyTrashAction } from './emptyTrashAction.ts'
|
||||
|
||||
const loadState = vi.hoisted(() => vi.fn((app, key, fallback) => {
|
||||
if (fallback) {
|
||||
if (fallback !== undefined) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
console.error('Unexpected loadState call without fallback', { app, key })
|
||||
throw new Error()
|
||||
}))
|
||||
|
||||
|
|
@ -30,7 +34,7 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
})
|
||||
|
||||
it('has display name set', () => {
|
||||
expect(emptyTrashAction.displayName(trashbinView)).toBe('Empty deleted files')
|
||||
expect(emptyTrashAction.displayName({ view: trashbinView })).toBe('Empty deleted files')
|
||||
})
|
||||
|
||||
it('has order set', () => {
|
||||
|
|
@ -41,13 +45,17 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
it('is enabled on trashbin view', () => {
|
||||
loadState.mockImplementation(() => ({ allow_delete: true }))
|
||||
|
||||
const root = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const nodes = [
|
||||
const folder = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const contents = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' }),
|
||||
]
|
||||
|
||||
expect(emptyTrashAction.enabled).toBeTypeOf('function')
|
||||
expect(emptyTrashAction.enabled!(trashbinView, nodes, root)).toBe(true)
|
||||
expect(emptyTrashAction.enabled!({
|
||||
view: trashbinView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(true)
|
||||
expect(loadState).toHaveBeenCalled()
|
||||
expect(loadState).toHaveBeenCalledWith('files_trashbin', 'config')
|
||||
})
|
||||
|
|
@ -55,10 +63,10 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
it('is not enabled on another view enabled', () => {
|
||||
loadState.mockImplementation(() => ({ allow_delete: true }))
|
||||
|
||||
const root = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const nodes = [
|
||||
const folder = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const contents = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' }),
|
||||
]
|
||||
] as Node[]
|
||||
|
||||
const otherView = new Proxy(trashbinView, {
|
||||
get(target, p) {
|
||||
|
|
@ -70,37 +78,49 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
})
|
||||
|
||||
expect(emptyTrashAction.enabled).toBeTypeOf('function')
|
||||
expect(emptyTrashAction.enabled!(otherView, nodes, root)).toBe(false)
|
||||
expect(emptyTrashAction.enabled!({
|
||||
view: otherView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('is not enabled when deletion is forbidden', () => {
|
||||
loadState.mockImplementation(() => ({ allow_delete: false }))
|
||||
|
||||
const root = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const nodes = [
|
||||
const folder = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const contents = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' }),
|
||||
]
|
||||
|
||||
expect(emptyTrashAction.enabled).toBeTypeOf('function')
|
||||
expect(emptyTrashAction.enabled!(trashbinView, nodes, root)).toBe(false)
|
||||
expect(emptyTrashAction.enabled!({
|
||||
view: trashbinView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(false)
|
||||
expect(loadState).toHaveBeenCalled()
|
||||
expect(loadState).toHaveBeenCalledWith('files_trashbin', 'config')
|
||||
})
|
||||
|
||||
it('is not enabled when not in trashbin root', () => {
|
||||
loadState.mockImplementation(() => ({ allow_delete: true }))
|
||||
const root = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/other-folder', root: '/trashbin/test/' })
|
||||
const nodes = [
|
||||
const folder = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/other-folder', root: '/trashbin/test/' })
|
||||
const contents = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' }),
|
||||
]
|
||||
|
||||
expect(emptyTrashAction.enabled).toBeTypeOf('function')
|
||||
expect(emptyTrashAction.enabled!(trashbinView, nodes, root)).toBe(false)
|
||||
expect(emptyTrashAction.enabled!({
|
||||
view: trashbinView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
describe('execute', () => {
|
||||
const root = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const nodes = [
|
||||
const folder = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/', root: '/trashbin/test/' })
|
||||
const contents = [
|
||||
new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/' }),
|
||||
]
|
||||
|
||||
|
|
@ -129,7 +149,11 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
const apiSpy = vi.spyOn(api, 'emptyTrash')
|
||||
|
||||
dialogBuilder.build.mockImplementationOnce(() => ({ show: async () => false }))
|
||||
expect(await emptyTrashAction.exec(trashbinView, nodes, root)).toBe(null)
|
||||
expect(await emptyTrashAction.exec({
|
||||
view: trashbinView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(null)
|
||||
expect(apiSpy).not.toBeCalled()
|
||||
})
|
||||
|
||||
|
|
@ -143,7 +167,11 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
await cancel.callback()
|
||||
},
|
||||
}))
|
||||
expect(await emptyTrashAction.exec(trashbinView, nodes, root)).toBe(null)
|
||||
expect(await emptyTrashAction.exec({
|
||||
view: trashbinView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(null)
|
||||
expect(apiSpy).not.toBeCalled()
|
||||
})
|
||||
|
||||
|
|
@ -159,10 +187,14 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
await cancel.callback()
|
||||
},
|
||||
}))
|
||||
expect(await emptyTrashAction.exec(trashbinView, nodes, root)).toBe(null)
|
||||
expect(await emptyTrashAction.exec({
|
||||
view: trashbinView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(null)
|
||||
expect(apiSpy).toBeCalled()
|
||||
expect(dialogSpy).not.toBeCalled()
|
||||
expect(eventBusSpy).toBeCalledWith('files:node:deleted', nodes[0])
|
||||
expect(eventBusSpy).toBeCalledWith('files:node:deleted', contents[0])
|
||||
})
|
||||
|
||||
it('will not emit files deleted event if API request failed', async () => {
|
||||
|
|
@ -177,7 +209,11 @@ describe('files_trashbin: file list actions - empty trashbin', () => {
|
|||
await cancel.callback()
|
||||
},
|
||||
}))
|
||||
expect(await emptyTrashAction.exec(trashbinView, nodes, root)).toBe(null)
|
||||
expect(await emptyTrashAction.exec({
|
||||
view: trashbinView,
|
||||
folder,
|
||||
contents,
|
||||
})).toBe(null)
|
||||
expect(apiSpy).toBeCalled()
|
||||
expect(dialogSpy).not.toBeCalled()
|
||||
expect(eventBusSpy).not.toBeCalled()
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Folder, Node, View } from '@nextcloud/files'
|
||||
|
||||
import { getDialogBuilder } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileListAction } from '@nextcloud/files'
|
||||
|
|
@ -23,7 +20,7 @@ export const emptyTrashAction = new FileListAction({
|
|||
displayName: () => t('files_trashbin', 'Empty deleted files'),
|
||||
order: 0,
|
||||
|
||||
enabled(view: View, nodes: Node[], folder: Folder) {
|
||||
enabled({ view, folder, contents }) {
|
||||
if (view.id !== TRASHBIN_VIEW_ID) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -33,10 +30,10 @@ export const emptyTrashAction = new FileListAction({
|
|||
return false
|
||||
}
|
||||
|
||||
return nodes.length > 0 && folder.path === '/'
|
||||
return contents.length > 0 && folder.path === '/'
|
||||
},
|
||||
|
||||
async exec(view: View, nodes: Node[]): Promise<null> {
|
||||
async exec({ contents }): Promise<null> {
|
||||
const askConfirmation = new Promise<boolean>((resolve) => {
|
||||
const dialog = getDialogBuilder(t('files_trashbin', 'Confirm permanent deletion'))
|
||||
.setSeverity('warning')
|
||||
|
|
@ -63,7 +60,7 @@ export const emptyTrashAction = new FileListAction({
|
|||
const result = await askConfirmation
|
||||
if (result === true) {
|
||||
if (await emptyTrash()) {
|
||||
nodes.forEach((node) => emit('files:node:deleted', node))
|
||||
contents.forEach((node) => emit('files:node:deleted', node))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,20 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes by original location', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'z-folder/a.txt' } })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'folder/b.txt' } })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-original-location': 'z-folder/a.txt' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-original-location': 'folder/b.txt' },
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(originalLocation.sort).toBeTypeOf('function')
|
||||
expect(originalLocation.sort!(nodeA, nodeB)).toBeGreaterThan(0)
|
||||
|
|
@ -35,7 +47,13 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('renders a node with original location', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'folder/a.txt' } })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-original-location': 'folder/a.txt' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = originalLocation.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('folder')
|
||||
|
|
@ -43,7 +61,12 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('renders a node when original location is missing', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = originalLocation.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('Unknown')
|
||||
|
|
@ -51,7 +74,13 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('renders a node when original location is the root', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'a.txt' } })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-original-location': 'a.txt' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = originalLocation.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('All files')
|
||||
|
|
@ -69,8 +98,20 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes by deleted time', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': 1741684522 } })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': 1741684422 } })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deletion-time': 1741684522 },
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deletion-time': 1741684422 },
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(deleted.sort).toBeTypeOf('function')
|
||||
expect(deleted.sort!(nodeA, nodeB)).toBeLessThan(0)
|
||||
|
|
@ -78,8 +119,20 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes by deleted time and falls back to mtime', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': 1741684522 } })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', mtime: new Date(1741684422000) })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deletion-time': 1741684522 },
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
mtime: new Date(1741684422000),
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(deleted.sort).toBeTypeOf('function')
|
||||
expect(deleted.sort!(nodeA, nodeB)).toBeLessThan(0)
|
||||
|
|
@ -87,8 +140,19 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes even if no deletion date is provided', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', mtime: new Date(1741684422000) })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
mtime: new Date(1741684422000),
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(deleted.sort).toBeTypeOf('function')
|
||||
expect(deleted.sort!(nodeA, nodeB)).toBeGreaterThan(0)
|
||||
|
|
@ -105,7 +169,15 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('renders a node with deletion date', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': (Date.now() / 1000) - 120 } })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: {
|
||||
'trashbin-deletion-time': Date.now() / 1000 - 120,
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = deleted.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('2 minutes ago')
|
||||
|
|
@ -113,7 +185,13 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('renders a node when deletion date is missing and falls back to mtime', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', mtime: new Date(Date.now() - 60000) })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
mtime: new Date(Date.now() - 60000),
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = deleted.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('1 minute ago')
|
||||
|
|
@ -121,7 +199,12 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('renders a node when deletion date is missing', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = deleted.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('A long time ago')
|
||||
|
|
@ -138,8 +221,20 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes by user-id of deleting user', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'zzz' } })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'aaa' } })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-id': 'zzz' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-id': 'aaa' },
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(deletedBy.sort).toBeTypeOf('function')
|
||||
expect(deletedBy.sort!(nodeA, nodeB)).toBeGreaterThan(0)
|
||||
|
|
@ -147,8 +242,20 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes by display name of deleting user', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'zzz' } })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'aaa' } })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-display-name': 'zzz' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-display-name': 'aaa' },
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(deletedBy.sort).toBeTypeOf('function')
|
||||
expect(deletedBy.sort!(nodeA, nodeB)).toBeGreaterThan(0)
|
||||
|
|
@ -156,8 +263,26 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes by display name of deleting user before user id', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': '000', 'trashbin-deleted-by-id': 'zzz' } })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'aaa', 'trashbin-deleted-by-id': '999' } })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: {
|
||||
'trashbin-deleted-by-display-name': '000',
|
||||
'trashbin-deleted-by-id': 'zzz',
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: {
|
||||
'trashbin-deleted-by-display-name': 'aaa',
|
||||
'trashbin-deleted-by-id': '999',
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(deletedBy.sort).toBeTypeOf('function')
|
||||
expect(deletedBy.sort!(nodeA, nodeB)).toBeLessThan(0)
|
||||
|
|
@ -165,9 +290,26 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('correctly sorts nodes even when one is missing', () => {
|
||||
const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'aaa' } })
|
||||
const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'zzz' } })
|
||||
const nodeC = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain' })
|
||||
const nodeA = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-id': 'aaa' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeB = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-id': 'zzz' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const nodeC = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/b.txt',
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
|
||||
expect(deletedBy.sort).toBeTypeOf('function')
|
||||
// aaa is less then "Unknown"
|
||||
|
|
@ -177,21 +319,41 @@ describe('files_trashbin: file list columns', () => {
|
|||
})
|
||||
|
||||
it('renders a node with deleting user', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'user-id' } })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-id': 'user-id' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = deletedBy.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent.trim()).toBe('user-id')
|
||||
})
|
||||
|
||||
it('renders a node with deleting user display name', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'user-name', 'trashbin-deleted-by-id': 'user-id' } })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: {
|
||||
'trashbin-deleted-by-display-name': 'user-name',
|
||||
'trashbin-deleted-by-id': 'user-id',
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = deletedBy.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent.trim()).toBe('user-name')
|
||||
})
|
||||
|
||||
it('renders a node even when information is missing', () => {
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = deletedBy.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('Unknown')
|
||||
|
|
@ -204,7 +366,13 @@ describe('files_trashbin: file list columns', () => {
|
|||
isAdmin: false,
|
||||
}))
|
||||
|
||||
const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'user-id' } })
|
||||
const node = new File({
|
||||
owner: 'test',
|
||||
source: 'https://example.com/remote.php/dav/files/test/a.txt',
|
||||
mime: 'text/plain',
|
||||
attributes: { 'trashbin-deleted-by-id': 'user-id' },
|
||||
root: '/files/test',
|
||||
})
|
||||
const el: HTMLElement = deletedBy.render(node, trashbinView)
|
||||
expect(el).toBeInstanceOf(HTMLElement)
|
||||
expect(el.textContent).toBe('You')
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@
|
|||
<NcTextField
|
||||
:id="inputIdWithDefault"
|
||||
ref="email"
|
||||
class="email__input"
|
||||
v-model="emailAddress"
|
||||
class="email__input"
|
||||
autocapitalize="none"
|
||||
autocomplete="email"
|
||||
:error="hasError || !!helperText"
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@
|
|||
:class="{ 'row__cell--obfuscated': hasObfuscated }">
|
||||
<template v-if="editing && settings.canChangePassword && user.backendCapabilities.setPassword">
|
||||
<NcTextField
|
||||
v-model="editedPassword"
|
||||
class="user-row-text-field"
|
||||
data-cy-user-list-input-password
|
||||
v-model="editedPassword"
|
||||
:data-loading="loading.password || undefined"
|
||||
:trailing-button-label="t('settings', 'Submit')"
|
||||
:class="{ 'icon-loading-small': loading.password }"
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@
|
|||
<div class="system-tag-form__group">
|
||||
<label for="system-tags-input">{{ t('systemtags', 'Search for a tag to edit') }}</label>
|
||||
<NcSelectTags
|
||||
v-model="selectedTag"
|
||||
:model-value="selectedTag"
|
||||
input-id="system-tags-input"
|
||||
:placeholder="t('systemtags', 'Collaborative tags …')"
|
||||
:fetch-tags="false"
|
||||
:options="tags"
|
||||
:multiple="false"
|
||||
passthru>
|
||||
label-outside
|
||||
@update:model-value="onSelectTag">
|
||||
<template #no-options>
|
||||
{{ t('systemtags', 'No tags to select') }}
|
||||
</template>
|
||||
|
|
@ -49,7 +50,8 @@
|
|||
:options="tagLevelOptions"
|
||||
:reduce="level => level.id"
|
||||
:clearable="false"
|
||||
:disabled="loading" />
|
||||
:disabled="loading"
|
||||
label-outside />
|
||||
</div>
|
||||
|
||||
<div class="system-tag-form__row">
|
||||
|
|
@ -85,11 +87,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { Tag, TagWithId } from '../types.js'
|
||||
|
||||
import { showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import Vue, { type PropType } from 'vue'
|
||||
import { defineComponent } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
|
|
@ -135,10 +138,10 @@ function getTagLevel(userVisible: boolean, userAssignable: boolean): TagLevel {
|
|||
[[true, false].join(',')]: TagLevel.Restricted,
|
||||
[[false, false].join(',')]: TagLevel.Invisible,
|
||||
}
|
||||
return matchLevel[[userVisible, userAssignable].join(',')]
|
||||
return matchLevel[[userVisible, userAssignable].join(',')]!
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
name: 'SystemTagForm',
|
||||
|
||||
components: {
|
||||
|
|
@ -156,6 +159,12 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
|
||||
emits: [
|
||||
'tag:created',
|
||||
'tag:updated',
|
||||
'tag:deleted',
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
|
@ -230,6 +239,11 @@ export default Vue.extend({
|
|||
methods: {
|
||||
t,
|
||||
|
||||
onSelectTag(tagId: number | null) {
|
||||
const tag = this.tags.find((search) => search.id === tagId) || null
|
||||
this.selectedTag = tag
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
if (this.isCreating) {
|
||||
await this.create()
|
||||
|
|
|
|||
|
|
@ -18,8 +18,18 @@ describe('Manage tags action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('systemtags:bulk')
|
||||
expect(action.displayName([], view)).toBe('Manage tags')
|
||||
expect(action.iconSvgInline([], view)).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Manage tags')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toMatch(/<svg.+<\/svg>/)
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.order).toBe(undefined)
|
||||
expect(action.enabled).toBeDefined()
|
||||
|
|
@ -34,6 +44,7 @@ describe('Manage tags action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.NONE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const file2 = new File({
|
||||
|
|
@ -42,12 +53,28 @@ describe('Manage tags action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.UPDATE,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file1, file2], view)).toBe(false)
|
||||
expect(action.enabled!([file1], view)).toBe(false)
|
||||
expect(action.enabled!([file2], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file1, file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file1],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file2],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for non-dav ressources', () => {
|
||||
|
|
@ -57,10 +84,16 @@ describe('Manage tags action enabled tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Enabled for files outside the user root folder', () => {
|
||||
|
|
@ -69,9 +102,15 @@ describe('Manage tags action enabled tests', () => {
|
|||
source: 'https://cloud.domain.com/remote.php/dav/trashbin/admin/trash/image.jpg.d1731053878',
|
||||
owner: 'admin',
|
||||
permissions: Permission.ALL,
|
||||
root: '/trashbin/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ import { defineAsyncComponent } from 'vue'
|
|||
* Spawn a dialog to add or remove tags from multiple nodes.
|
||||
*
|
||||
* @param nodes Nodes to modify tags for
|
||||
* @param nodes.nodes
|
||||
*/
|
||||
async function execBatch(nodes: Node[]): Promise<(null | boolean)[]> {
|
||||
async function execBatch({ nodes }: { nodes: Node[] }): Promise<(null | boolean)[]> {
|
||||
const response = await new Promise<null | boolean>((resolve) => {
|
||||
spawnDialog(defineAsyncComponent(() => import('../components/SystemTagPicker.vue')), {
|
||||
nodes,
|
||||
|
|
@ -34,7 +35,7 @@ export const action = new FileAction({
|
|||
iconSvgInline: () => TagMultipleSvg,
|
||||
|
||||
// If the app is disabled, the action is not available anyway
|
||||
enabled(nodes) {
|
||||
enabled({ nodes }) {
|
||||
if (isPublicShare()) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -52,8 +53,8 @@ export const action = new FileAction({
|
|||
return !nodes.some((node) => (node.permissions & Permission.UPDATE) === 0)
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
return execBatch([node])[0]
|
||||
async exec({ nodes }) {
|
||||
return execBatch({ nodes })[0]
|
||||
},
|
||||
|
||||
execBatch,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Folder, View } from '@nextcloud/files'
|
||||
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { File, FileAction, Permission } from '@nextcloud/files'
|
||||
|
|
@ -25,17 +25,33 @@ describe('Inline system tags action conditions tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('system-tags')
|
||||
expect(action.displayName([file], view)).toBe('')
|
||||
expect(action.iconSvgInline([], view)).toBe('')
|
||||
expect(action.displayName({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.default).toBeUndefined()
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.order).toBe(0)
|
||||
// Always enabled
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Enabled with valid system tags', () => {
|
||||
|
|
@ -50,9 +66,15 @@ describe('Inline system tags action conditions tests', () => {
|
|||
'system-tag': 'Confidential',
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
expect(action.enabled!([file], view)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -70,9 +92,15 @@ describe('Inline system tags action render tests', () => {
|
|||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.renderInline!(file, view)
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(result!.outerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"></ul>"')
|
||||
})
|
||||
|
|
@ -89,9 +117,15 @@ describe('Inline system tags action render tests', () => {
|
|||
'system-tag': 'Confidential',
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.renderInline!(file, view)
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(result!.outerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential">Confidential</li></ul>"')
|
||||
})
|
||||
|
|
@ -108,9 +142,15 @@ describe('Inline system tags action render tests', () => {
|
|||
'system-tag': ['Important', 'Confidential'],
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.renderInline!(file, view)
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(result!.outerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Important">Important</li><li class="files-list__system-tag" data-systemtag-name="Confidential">Confidential</li></ul>"')
|
||||
})
|
||||
|
|
@ -132,9 +172,15 @@ describe('Inline system tags action render tests', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.renderInline!(file, view)
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(result!.outerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Important">Important</li><li class="files-list__system-tag files-list__system-tag--more" data-systemtag-name="+3" title="Confidential, Secret, Classified" aria-hidden="true" role="presentation">+3</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Confidential">Confidential</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Secret">Secret</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Classified">Classified</li></ul>"')
|
||||
})
|
||||
|
|
@ -156,9 +202,15 @@ describe('Inline system tags action render tests', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.renderInline!(file, view) as HTMLElement
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
}) as HTMLElement
|
||||
document.body.appendChild(result)
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(document.body.innerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Important">Important</li><li class="files-list__system-tag files-list__system-tag--more" data-systemtag-name="+3" title="Confidential, Secret, Classified" aria-hidden="true" role="presentation">+3</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Confidential">Confidential</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Secret">Secret</li><li class="files-list__system-tag hidden-visually" data-systemtag-name="Classified">Classified</li></ul>"')
|
||||
|
|
@ -212,9 +264,15 @@ describe('Inline system tags action colors', () => {
|
|||
'system-tag': 'Confidential',
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.renderInline!(file, view)
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(result!.outerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential" style="--systemtag-color: #000000;" data-systemtag-color="true">Confidential</li></ul>"')
|
||||
})
|
||||
|
|
@ -231,11 +289,17 @@ describe('Inline system tags action colors', () => {
|
|||
'system-tag': 'Confidential',
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
document.body.setAttribute('data-themes', 'theme-dark')
|
||||
|
||||
const result = await action.renderInline!(file, view)
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(result!.outerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential" style="--systemtag-color: #646464;" data-systemtag-color="true">Confidential</li></ul>"')
|
||||
|
||||
|
|
@ -254,9 +318,15 @@ describe('Inline system tags action colors', () => {
|
|||
'system-tag': 'Confidential',
|
||||
},
|
||||
},
|
||||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const result = await action.renderInline!(file, view) as HTMLElement
|
||||
const result = await action.renderInline!({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
}) as HTMLElement
|
||||
document.body.appendChild(result)
|
||||
expect(result).toBeInstanceOf(HTMLElement)
|
||||
expect(document.body.innerHTML).toMatchInlineSnapshot('"<ul class="files-list__system-tags" aria-label="Assigned collaborative tags" data-systemtags-fileid="1"><li class="files-list__system-tag" data-systemtag-name="Confidential" style="--systemtag-color: #000000;" data-systemtag-color="true">Confidential</li></ul>"')
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const action = new FileAction({
|
|||
displayName: () => '',
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled(nodes: Node[]) {
|
||||
enabled({ nodes }) {
|
||||
// Only show the action on single nodes
|
||||
if (nodes.length !== 1) {
|
||||
return false
|
||||
|
|
@ -36,7 +36,12 @@ export const action = new FileAction({
|
|||
},
|
||||
|
||||
exec: async () => null,
|
||||
renderInline,
|
||||
renderInline: ({ nodes }) => {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
return renderInline(nodes[0])
|
||||
},
|
||||
|
||||
order: 0,
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,18 @@ describe('Open in files action conditions tests', () => {
|
|||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('systemtags:open-in-files')
|
||||
expect(action.displayName([], systemTagsView)).toBe('Open in Files')
|
||||
expect(action.iconSvgInline([], systemTagsView)).toBe('')
|
||||
expect(action.displayName({
|
||||
nodes: [],
|
||||
view: systemTagsView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('Open in Files')
|
||||
expect(action.iconSvgInline({
|
||||
nodes: [],
|
||||
view: systemTagsView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe('')
|
||||
expect(action.default).toBe(DefaultType.HIDDEN)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline).toBeUndefined()
|
||||
|
|
@ -56,34 +66,58 @@ describe('Open in files action conditions tests', () => {
|
|||
describe('Open in files action enabled tests', () => {
|
||||
test('Enabled with on valid view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([validNode], systemTagsView)).toBe(true)
|
||||
expect(action.enabled!({
|
||||
nodes: [validNode],
|
||||
view: systemTagsView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([validNode], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [validNode],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled without nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [],
|
||||
view: systemTagsView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled with too many nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([validNode, validNode], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [validNode, validNode],
|
||||
view: systemTagsView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled with when node is a tag', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([validTag], view)).toBe(false)
|
||||
expect(action.enabled!({
|
||||
nodes: [validTag],
|
||||
view: systemTagsView,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Open in files action execute tests', () => {
|
||||
test('Open in files', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -95,7 +129,12 @@ describe('Open in files action execute tests', () => {
|
|||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
|
|
@ -105,7 +144,6 @@ describe('Open in files action execute tests', () => {
|
|||
|
||||
test('Open in files with folder', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new Folder({
|
||||
|
|
@ -116,7 +154,12 @@ describe('Open in files action execute tests', () => {
|
|||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
const exec = await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder: {} as Folder,
|
||||
contents: [],
|
||||
})
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, FileAction, FileType } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { systemTagsViewId } from '../files_views/systemtagsView.ts'
|
||||
|
|
@ -14,13 +12,13 @@ export const action = new FileAction({
|
|||
displayName: () => t('systemtags', 'Open in Files'),
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled(nodes, view) {
|
||||
enabled({ nodes, view }) {
|
||||
// Only for the system tags view
|
||||
if (view.id !== systemTagsViewId) {
|
||||
return false
|
||||
}
|
||||
// Only for single nodes
|
||||
if (nodes.length !== 1) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
return false
|
||||
}
|
||||
// Do not open tags (keep the default action) and only open folders
|
||||
|
|
@ -28,15 +26,19 @@ export const action = new FileAction({
|
|||
&& nodes[0].type === FileType.Folder
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
let dir = node.dirname
|
||||
if (node.type === FileType.Folder) {
|
||||
dir = node.path
|
||||
async exec({ nodes }) {
|
||||
if (!nodes[0] || nodes.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
let dir = nodes[0].dirname
|
||||
if (nodes[0].type === FileType.Folder) {
|
||||
dir = nodes[0].path
|
||||
}
|
||||
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files', fileid: String(node.fileid) },
|
||||
{ view: 'files', fileid: String(nodes[0].fileid) },
|
||||
{ dir, openfile: 'true' },
|
||||
)
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { registerDavProperty, registerFileAction } from '@nextcloud/files'
|
||||
import { registerFileAction } from '@nextcloud/files'
|
||||
import { registerDavProperty } from '@nextcloud/files/dav'
|
||||
import { action as bulkSystemTagsAction } from './files_actions/bulkSystemTagsAction.ts'
|
||||
import { action as inlineSystemTagsAction } from './files_actions/inlineSystemTagsAction.ts'
|
||||
import { action as openInFilesAction } from './files_actions/openInFilesAction.ts'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { File, Permission } from '@nextcloud/files'
|
||||
import { File, Folder, Permission } from '@nextcloud/files'
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
|
@ -29,16 +29,26 @@ describe('HotKeysService testing', () => {
|
|||
beforeEach(() => {
|
||||
// Make sure the file is reset before each test
|
||||
file = new File({
|
||||
id: 1,
|
||||
id: 2,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
root: '/files/admin',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const root = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
|
||||
root: '/files/admin',
|
||||
owner: 'admin',
|
||||
permissions: Permission.CREATE,
|
||||
})
|
||||
|
||||
// Setting the view first as it reset the active node
|
||||
activeStore.activeView = view
|
||||
activeStore.activeNode = file
|
||||
activeStore.activeFolder = root
|
||||
})
|
||||
|
||||
it('Pressing t should open the tag management dialog', () => {
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import type { FileStat, ResponseDataDetailed } from 'webdav'
|
|||
import type { TagWithId } from '../types.ts'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { davGetClient, davRemoteURL, davResultToNode, davRootPath, Folder, getDavNameSpaces, getDavProperties, Permission } from '@nextcloud/files'
|
||||
import { Folder, Permission } from '@nextcloud/files'
|
||||
import { getClient, getDavNameSpaces, getDavProperties, getRemoteURL, getRootPath, resultToNode } from '@nextcloud/files/dav'
|
||||
import { fetchTags } from './api.ts'
|
||||
|
||||
const rootPath = '/systemtags'
|
||||
|
||||
const client = davGetClient()
|
||||
const resultToNode = (node: FileStat) => davResultToNode(node)
|
||||
const client = getClient()
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -38,7 +38,7 @@ function formatReportPayload(tagId: number) {
|
|||
function tagToNode(tag: TagWithId): Folder {
|
||||
return new Folder({
|
||||
id: tag.id,
|
||||
source: `${davRemoteURL}${rootPath}/${tag.id}`,
|
||||
source: `${getRemoteURL()}${rootPath}/${tag.id}`,
|
||||
owner: String(getCurrentUser()?.uid ?? 'anonymous'),
|
||||
root: rootPath,
|
||||
displayname: tag.displayName,
|
||||
|
|
@ -62,7 +62,7 @@ export async function getContents(path = '/'): Promise<ContentsWithRoot> {
|
|||
return {
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: `${davRemoteURL}${rootPath}`,
|
||||
source: `${getRemoteURL()}${rootPath}`,
|
||||
owner: getCurrentUser()?.uid as string,
|
||||
root: rootPath,
|
||||
permissions: Permission.NONE,
|
||||
|
|
@ -79,7 +79,7 @@ export async function getContents(path = '/'): Promise<ContentsWithRoot> {
|
|||
}
|
||||
|
||||
const folder = tagToNode(tag)
|
||||
const contentsResponse = await client.getDirectoryContents(davRootPath, {
|
||||
const contentsResponse = await client.getDirectoryContents(getRootPath(), {
|
||||
details: true,
|
||||
// Only filter favorites if we're at the root
|
||||
data: formatReportPayload(tagId),
|
||||
|
|
@ -91,6 +91,6 @@ export async function getContents(path = '/'): Promise<ContentsWithRoot> {
|
|||
|
||||
return {
|
||||
folder,
|
||||
contents: contentsResponse.data.map(resultToNode),
|
||||
contents: contentsResponse.data.map((stat) => resultToNode(stat)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ describe('systemtags - utils', () => {
|
|||
'system-tag': 'tag',
|
||||
},
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
expect(getNodeSystemTags(node)).toStrictEqual(['tag'])
|
||||
})
|
||||
|
|
@ -113,6 +114,7 @@ describe('systemtags - utils', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
expect(getNodeSystemTags(node)).toStrictEqual(['tag', 'my-tag'])
|
||||
})
|
||||
|
|
@ -129,6 +131,7 @@ describe('systemtags - utils', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
expect(getNodeSystemTags(node)).toStrictEqual(['tag'])
|
||||
})
|
||||
|
|
@ -151,6 +154,7 @@ describe('systemtags - utils', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
expect(getNodeSystemTags(node)).toStrictEqual(['tag', 'my-tag'])
|
||||
})
|
||||
|
|
@ -170,6 +174,7 @@ describe('systemtags - utils', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
root: '/files/test',
|
||||
})
|
||||
expect(getNodeSystemTags(node)).toStrictEqual(['tag', 'my-tag'])
|
||||
})
|
||||
|
|
|
|||
185
build/frontend-legacy/package-lock.json
generated
185
build/frontend-legacy/package-lock.json
generated
|
|
@ -18,7 +18,7 @@
|
|||
"@nextcloud/capabilities": "^1.2.1",
|
||||
"@nextcloud/dialogs": "^7.1.0",
|
||||
"@nextcloud/event-bus": "^3.3.3",
|
||||
"@nextcloud/files": "^3.12.0",
|
||||
"@nextcloud/files": "^4.0.0-beta.4",
|
||||
"@nextcloud/initial-state": "^3.0.0",
|
||||
"@nextcloud/l10n": "^3.4.1",
|
||||
"@nextcloud/logger": "^3.0.3",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"@nextcloud/router": "^3.1.0",
|
||||
"@nextcloud/sharing": "^0.3.0",
|
||||
"@nextcloud/upload": "^1.11.0",
|
||||
"@nextcloud/vue": "^8.34.0",
|
||||
"@nextcloud/vue": "^8.35.0",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@vue/web-component-wrapper": "^1.3.0",
|
||||
"@vueuse/components": "^11.3.0",
|
||||
|
|
@ -2896,6 +2896,50 @@
|
|||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/dialogs/node_modules/@nextcloud/files": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.0.tgz",
|
||||
"integrity": "sha512-LVZklgooZzBj2jkbPRZO4jnnvW5+RvOn7wN5weyOZltF6i2wVMbg1Y/Czl2pi/UNMjUm5ENqc0j7FgxMBo8bwA==",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@nextcloud/auth": "^2.5.1",
|
||||
"@nextcloud/capabilities": "^1.2.0",
|
||||
"@nextcloud/l10n": "^3.3.0",
|
||||
"@nextcloud/logger": "^3.0.2",
|
||||
"@nextcloud/paths": "^2.2.1",
|
||||
"@nextcloud/router": "^3.0.1",
|
||||
"@nextcloud/sharing": "^0.2.4",
|
||||
"cancelable-promise": "^4.3.1",
|
||||
"is-svg": "^6.0.0",
|
||||
"typescript-event-target": "^1.1.1",
|
||||
"webdav": "^5.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/dialogs/node_modules/@nextcloud/files/node_modules/@nextcloud/initial-state": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz",
|
||||
"integrity": "sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": "^20.0.0",
|
||||
"npm": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/dialogs/node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.2.5.tgz",
|
||||
"integrity": "sha512-B3K5Dq9b5dexDA5n3AAuCF69Huwhrpw0J72fsVXV4KpPdImjhVPlExAv5o70AoXa+OqN4Rwn6gqJw+3ED892zg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@nextcloud/initial-state": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/dialogs/node_modules/@nextcloud/vue": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.0.1.tgz",
|
||||
|
|
@ -3206,20 +3250,20 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@nextcloud/files": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.0.tgz",
|
||||
"integrity": "sha512-LVZklgooZzBj2jkbPRZO4jnnvW5+RvOn7wN5weyOZltF6i2wVMbg1Y/Czl2pi/UNMjUm5ENqc0j7FgxMBo8bwA==",
|
||||
"version": "4.0.0-beta.4",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-4.0.0-beta.4.tgz",
|
||||
"integrity": "sha512-2MwEZw0JXyNPOmj6PcEUT/9TbmEzT+C5c9zMC6hwJMjjLoO19Nf1VaxmHqyzgfNy1jzst8Eq106ZsBExH2rncw==",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@nextcloud/auth": "^2.5.1",
|
||||
"@nextcloud/capabilities": "^1.2.0",
|
||||
"@nextcloud/l10n": "^3.3.0",
|
||||
"@nextcloud/auth": "^2.5.3",
|
||||
"@nextcloud/capabilities": "^1.2.1",
|
||||
"@nextcloud/l10n": "^3.4.1",
|
||||
"@nextcloud/logger": "^3.0.2",
|
||||
"@nextcloud/paths": "^2.2.1",
|
||||
"@nextcloud/router": "^3.0.1",
|
||||
"@nextcloud/sharing": "^0.2.4",
|
||||
"@nextcloud/paths": "^2.3.0",
|
||||
"@nextcloud/router": "^3.1.0",
|
||||
"@nextcloud/sharing": "^0.3.0",
|
||||
"cancelable-promise": "^4.3.1",
|
||||
"is-svg": "^6.0.0",
|
||||
"is-svg": "^6.1.0",
|
||||
"typescript-event-target": "^1.1.1",
|
||||
"webdav": "^5.8.0"
|
||||
},
|
||||
|
|
@ -3227,28 +3271,6 @@
|
|||
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/files/node_modules/@nextcloud/initial-state": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz",
|
||||
"integrity": "sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": "^20.0.0",
|
||||
"npm": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/files/node_modules/@nextcloud/sharing": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.2.5.tgz",
|
||||
"integrity": "sha512-B3K5Dq9b5dexDA5n3AAuCF69Huwhrpw0J72fsVXV4KpPdImjhVPlExAv5o70AoXa+OqN4Rwn6gqJw+3ED892zg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@nextcloud/initial-state": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/initial-state": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz",
|
||||
|
|
@ -3679,6 +3701,53 @@
|
|||
"@nextcloud/files": "^3.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/sharing/node_modules/@nextcloud/files": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.0.tgz",
|
||||
"integrity": "sha512-LVZklgooZzBj2jkbPRZO4jnnvW5+RvOn7wN5weyOZltF6i2wVMbg1Y/Czl2pi/UNMjUm5ENqc0j7FgxMBo8bwA==",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@nextcloud/auth": "^2.5.1",
|
||||
"@nextcloud/capabilities": "^1.2.0",
|
||||
"@nextcloud/l10n": "^3.3.0",
|
||||
"@nextcloud/logger": "^3.0.2",
|
||||
"@nextcloud/paths": "^2.2.1",
|
||||
"@nextcloud/router": "^3.0.1",
|
||||
"@nextcloud/sharing": "^0.2.4",
|
||||
"cancelable-promise": "^4.3.1",
|
||||
"is-svg": "^6.0.0",
|
||||
"typescript-event-target": "^1.1.1",
|
||||
"webdav": "^5.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/sharing/node_modules/@nextcloud/sharing": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.2.5.tgz",
|
||||
"integrity": "sha512-B3K5Dq9b5dexDA5n3AAuCF69Huwhrpw0J72fsVXV4KpPdImjhVPlExAv5o70AoXa+OqN4Rwn6gqJw+3ED892zg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@nextcloud/initial-state": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/sharing/node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz",
|
||||
"integrity": "sha512-cDW98L5KGGgpS8pzd+05304/p80cyu8U2xSDQGa+kGPTpUFmCbv2qnO5WrwwGTauyjYijCal2bmw82VddSH+Pg==",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^20.0.0",
|
||||
"npm": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/timezones": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/timezones/-/timezones-0.2.0.tgz",
|
||||
|
|
@ -3735,9 +3804,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@nextcloud/upload/node_modules/@nextcloud/dialogs": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.4.0.tgz",
|
||||
"integrity": "sha512-xtmmj7vGocEmImDJrnD4WfSIbzWH0qaSNP9+OnrX4E61IB1MjoQ/+NszRzf+M/vT1k9X+o0Tjgnzf06QKOichA==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-6.4.1.tgz",
|
||||
"integrity": "sha512-Pr9pnZ21bwvvM43sc9tTLo3ETM6kIffmdILwvyfbTdCXkPcOsgggE2FI1c5iZUbtd8hrvAYMic4oZxhqA2izSA==",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.4.47",
|
||||
|
|
@ -3808,6 +3877,28 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/upload/node_modules/@nextcloud/files": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.0.tgz",
|
||||
"integrity": "sha512-LVZklgooZzBj2jkbPRZO4jnnvW5+RvOn7wN5weyOZltF6i2wVMbg1Y/Czl2pi/UNMjUm5ENqc0j7FgxMBo8bwA==",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@nextcloud/auth": "^2.5.1",
|
||||
"@nextcloud/capabilities": "^1.2.0",
|
||||
"@nextcloud/l10n": "^3.3.0",
|
||||
"@nextcloud/logger": "^3.0.2",
|
||||
"@nextcloud/paths": "^2.2.1",
|
||||
"@nextcloud/router": "^3.0.1",
|
||||
"@nextcloud/sharing": "^0.2.4",
|
||||
"cancelable-promise": "^4.3.1",
|
||||
"is-svg": "^6.0.0",
|
||||
"typescript-event-target": "^1.1.1",
|
||||
"webdav": "^5.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/upload/node_modules/@nextcloud/initial-state": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.2.0.tgz",
|
||||
|
|
@ -3862,9 +3953,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@nextcloud/vue": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-8.34.0.tgz",
|
||||
"integrity": "sha512-zUmInTvT4NgbRjWJZbw8nA+h4EqitYKfoCTj3h3Xr930sQZcczQatPtSo5Sps8RAh+JJz3iiAqAawYqS9jvBdA==",
|
||||
"version": "8.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-8.35.0.tgz",
|
||||
"integrity": "sha512-qPm0aaPbnt7n694WQ97T+EMQTxCa3+RPKDzsBVD6vb01N4uGYwjvrEEOLVmBMlEWqkFy+ks3tpeOjkDPOoJbNA==",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4",
|
||||
|
|
@ -3872,12 +3963,12 @@
|
|||
"@nextcloud/auth": "^2.5.3",
|
||||
"@nextcloud/axios": "^2.5.2",
|
||||
"@nextcloud/browser-storage": "^0.5.0",
|
||||
"@nextcloud/capabilities": "^1.2.0",
|
||||
"@nextcloud/event-bus": "^3.3.2",
|
||||
"@nextcloud/capabilities": "^1.2.1",
|
||||
"@nextcloud/event-bus": "^3.3.3",
|
||||
"@nextcloud/initial-state": "^2.2.0",
|
||||
"@nextcloud/l10n": "^3.4.0",
|
||||
"@nextcloud/l10n": "^3.4.1",
|
||||
"@nextcloud/logger": "^3.0.2",
|
||||
"@nextcloud/router": "^3.0.1",
|
||||
"@nextcloud/router": "^3.1.0",
|
||||
"@nextcloud/sharing": "^0.3.0",
|
||||
"@nextcloud/timezones": "^0.2.0",
|
||||
"@nextcloud/vue-select": "^3.26.0",
|
||||
|
|
@ -11291,9 +11382,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/js-beautify/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
"@nextcloud/capabilities": "^1.2.1",
|
||||
"@nextcloud/dialogs": "^7.1.0",
|
||||
"@nextcloud/event-bus": "^3.3.3",
|
||||
"@nextcloud/files": "^3.12.0",
|
||||
"@nextcloud/files": "^4.0.0-beta.4",
|
||||
"@nextcloud/initial-state": "^3.0.0",
|
||||
"@nextcloud/l10n": "^3.4.1",
|
||||
"@nextcloud/logger": "^3.0.3",
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"@nextcloud/router": "^3.1.0",
|
||||
"@nextcloud/sharing": "^0.3.0",
|
||||
"@nextcloud/upload": "^1.11.0",
|
||||
"@nextcloud/vue": "^8.34.0",
|
||||
"@nextcloud/vue": "^8.35.0",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@vue/web-component-wrapper": "^1.3.0",
|
||||
"@vueuse/components": "^11.3.0",
|
||||
|
|
|
|||
|
|
@ -60,8 +60,6 @@ describe('core: LoginForm', () => {
|
|||
},
|
||||
})
|
||||
|
||||
page.debug()
|
||||
|
||||
const input: HTMLInputElement = page.getByRole('textbox', { name: /Account name or email/ })
|
||||
expect(input.id).toBe('user')
|
||||
expect(input.name).toBe('user')
|
||||
|
|
|
|||
|
|
@ -12,25 +12,35 @@ const updatedTagName = 'bar'
|
|||
|
||||
describe('Create system tags', () => {
|
||||
before(() => {
|
||||
// delete any existing tags
|
||||
cy.runOccCommand('tag:list --output=json').then((output) => {
|
||||
Object.keys(JSON.parse(output.stdout)).forEach((id) => {
|
||||
cy.runOccCommand(`tag:delete ${id}`)
|
||||
})
|
||||
})
|
||||
|
||||
// login as admin and go to admin settings
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/admin')
|
||||
})
|
||||
|
||||
it('Can create a tag', () => {
|
||||
cy.intercept('POST', '/remote.php/dav/systemtags').as('createTag')
|
||||
cy.get('input#system-tag-name').should('exist').and('have.value', '')
|
||||
cy.get('input#system-tag-name').type(tagName)
|
||||
cy.get('input#system-tag-name').should('have.value', tagName)
|
||||
// submit the form
|
||||
cy.get('input#system-tag-name').type('{enter}')
|
||||
|
||||
// wait for the tag to be created
|
||||
cy.wait('@createTag').its('response.statusCode').should('eq', 201)
|
||||
|
||||
// see that the created tag is in the list
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then((id) => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', tagName).should('exist')
|
||||
// ensure only one tag exists
|
||||
cy.get('li').should('have.length', 1)
|
||||
})
|
||||
cy.get(`ul#${id} li span[title="${tagName}"]`)
|
||||
.should('exist')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -42,12 +52,9 @@ describe('Update system tags', { testIsolation: false }, () => {
|
|||
})
|
||||
|
||||
it('select the tag', () => {
|
||||
// select the tag to edit
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then((id) => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', tagName).should('exist').click()
|
||||
})
|
||||
cy.get(`ul#${id} li span[title="${tagName}"]`).should('exist').click()
|
||||
})
|
||||
// see that the tag name matches the selected tag
|
||||
cy.get('input#system-tag-name').should('exist').and('have.value', tagName)
|
||||
|
|
@ -57,28 +64,27 @@ describe('Update system tags', { testIsolation: false }, () => {
|
|||
})
|
||||
|
||||
it('update the tag name and level', () => {
|
||||
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*').as('updateTag')
|
||||
cy.get('input#system-tag-name').clear()
|
||||
cy.get('input#system-tag-name').type(updatedTagName)
|
||||
cy.get('input#system-tag-name').should('have.value', updatedTagName)
|
||||
// select the new tag level
|
||||
cy.get('input#system-tag-level').focus()
|
||||
cy.get('input#system-tag-level').invoke('attr', 'aria-controls').then((id) => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', 'Invisible').should('exist').click()
|
||||
})
|
||||
cy.get(`ul#${id} li span[title="Invisible"]`).should('exist').click()
|
||||
})
|
||||
// submit the form
|
||||
cy.get('input#system-tag-name').type('{enter}')
|
||||
// wait for the tag to be updated
|
||||
cy.wait('@updateTag').its('response.statusCode').should('eq', 207)
|
||||
})
|
||||
|
||||
it('see the tag was successfully updated', () => {
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then((id) => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', `${updatedTagName} (invisible)`).should('exist')
|
||||
// ensure only one tag exists
|
||||
cy.get('li').should('have.length', 1)
|
||||
})
|
||||
cy.get(`ul#${id} li span[title="${updatedTagName} (invisible)"]`)
|
||||
.should('exist')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -93,9 +99,7 @@ describe('Delete system tags', { testIsolation: false }, () => {
|
|||
// select the tag to edit
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then((id) => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', `${updatedTagName} (invisible)`).should('exist').click()
|
||||
})
|
||||
cy.get(`ul#${id} li span[title="${updatedTagName} (invisible)"]`).should('exist').click()
|
||||
})
|
||||
// see that the tag name matches the selected tag
|
||||
cy.get('input#system-tag-name').should('exist').and('have.value', updatedTagName)
|
||||
|
|
@ -105,17 +109,18 @@ describe('Delete system tags', { testIsolation: false }, () => {
|
|||
})
|
||||
|
||||
it('can delete the tag', () => {
|
||||
cy.intercept('DELETE', '/remote.php/dav/systemtags/*').as('deleteTag')
|
||||
cy.get('.system-tag-form__row').within(() => {
|
||||
cy.contains('button', 'Delete').should('be.enabled').click()
|
||||
})
|
||||
// wait for the tag to be deleted
|
||||
cy.wait('@deleteTag').its('response.statusCode').should('eq', 204)
|
||||
})
|
||||
|
||||
it('see that the deleted tag is not present', () => {
|
||||
cy.get('input#system-tags-input').focus()
|
||||
cy.get('input#system-tags-input').invoke('attr', 'aria-controls').then((id) => {
|
||||
cy.get(`ul#${id}`).within(() => {
|
||||
cy.contains('li', updatedTagName).should('not.exist')
|
||||
})
|
||||
cy.get(`ul#${id} li span[title="${updatedTagName}"]`).should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
2
dist/1078-1078.js
vendored
2
dist/1078-1078.js
vendored
File diff suppressed because one or more lines are too long
1
dist/1078-1078.js.map.license
vendored
1
dist/1078-1078.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
1078-1078.js.license
|
||||
14
dist/1082-1082.js.license
vendored
14
dist/1082-1082.js.license
vendored
|
|
@ -12,6 +12,7 @@ SPDX-FileCopyrightText: debounce developers
|
|||
SPDX-FileCopyrightText: Tobias Koppers @sokra
|
||||
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
|
||||
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
|
||||
SPDX-FileCopyrightText: Perry Mitchell <perry@perrymitchell.net>
|
||||
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-FileCopyrightText: Joyent
|
||||
SPDX-FileCopyrightText: Jonas Schade <derzade@gmail.com>
|
||||
|
|
@ -49,11 +50,8 @@ This file is generated from multiple sources. Included packages:
|
|||
- @nextcloud/event-bus
|
||||
- version: 3.3.3
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/initial-state
|
||||
- version: 2.2.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/files
|
||||
- version: 3.12.0
|
||||
- version: 4.0.0-beta.4
|
||||
- license: AGPL-3.0-or-later
|
||||
- @nextcloud/initial-state
|
||||
- version: 3.0.0
|
||||
|
|
@ -70,8 +68,11 @@ This file is generated from multiple sources. Included packages:
|
|||
- @nextcloud/router
|
||||
- version: 3.1.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/sharing
|
||||
- version: 0.3.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/vue
|
||||
- version: 8.34.0
|
||||
- version: 8.35.0
|
||||
- license: AGPL-3.0-or-later
|
||||
- @vue/devtools-api
|
||||
- version: 6.6.4
|
||||
|
|
@ -139,6 +140,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- vue
|
||||
- version: 2.7.16
|
||||
- license: MIT
|
||||
- webdav
|
||||
- version: 5.8.0
|
||||
- license: MIT
|
||||
- nextcloud
|
||||
- version: 1.0.0
|
||||
- license: AGPL-3.0-or-later
|
||||
|
|
|
|||
2
dist/1543-1543.js.license
vendored
2
dist/1543-1543.js.license
vendored
|
|
@ -179,7 +179,7 @@ This file is generated from multiple sources. Included packages:
|
|||
- version: 0.3.0
|
||||
- license: GPL-3.0-or-later
|
||||
- @nextcloud/vue
|
||||
- version: 8.34.0
|
||||
- version: 8.35.0
|
||||
- license: AGPL-3.0-or-later
|
||||
- @ungap/structured-clone
|
||||
- version: 1.3.0
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue