refactor(files): adjust for files library interfaces

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-02-05 15:28:33 +01:00
parent a023b5b2d5
commit aac91a8df9
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
31 changed files with 395 additions and 379 deletions

View file

@ -1,10 +1,13 @@
/**
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction } from '@nextcloud/files'
import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw'
import { getCapabilities } from '@nextcloud/capabilities'
import { FileAction, registerFileAction } from '@nextcloud/files'
import { registerFileAction } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { convertFile, convertFiles } from './convertUtils.ts'
@ -18,47 +21,45 @@ type ConversionsProvider = {
export const ACTION_CONVERT = 'convert'
/**
*
* Registers the convert actions based on the capabilities provided by the server.
*/
export function registerConvertActions() {
// Generate sub actions
const convertProviders = getCapabilities()?.files?.file_conversions as ConversionsProvider[] ?? []
const actions = convertProviders.map(({ to, from, displayName }) => {
return new FileAction({
id: `convert-${from}-${to}`,
displayName: () => t('files', 'Save as {displayName}', { displayName }),
iconSvgInline: () => generateIconSvg(to),
enabled: ({ nodes }) => {
// Check that all nodes have the same mime type
return nodes.every((node) => from === node.mime)
},
const actions = convertProviders.map(({ to, from, displayName }) => ({
id: `convert-${from}-${to}`,
displayName: () => t('files', 'Save as {displayName}', { displayName }),
iconSvgInline: () => generateIconSvg(to),
enabled: ({ nodes }) => {
// Check that all nodes have the same mime type
return nodes.every((node) => from === node.mime)
},
async exec({ nodes }) {
if (!nodes[0]) {
return false
}
async exec({ nodes }) {
if (!nodes[0]) {
return false
}
// If we're here, we know that the node has a fileid
convertFile(nodes[0].fileid as number, to)
// If we're here, we know that the node has a fileid
convertFile(nodes[0].fileid as number, to)
// Silently terminate, we'll handle the UI in the background
return null
},
// Silently terminate, we'll handle the UI in the background
return null
},
async execBatch({ nodes }) {
const fileIds = nodes.map((node) => node.fileid).filter(Boolean) as number[]
convertFiles(fileIds, to)
async execBatch({ nodes }) {
const fileIds = nodes.map((node) => node.fileid).filter(Boolean) as number[]
convertFiles(fileIds, to)
// Silently terminate, we'll handle the UI in the background
return Array(nodes.length).fill(null)
},
// Silently terminate, we'll handle the UI in the background
return Array(nodes.length).fill(null)
},
parent: ACTION_CONVERT,
})
})
parent: ACTION_CONVERT,
} satisfies IFileAction))
// Register main action
registerFileAction(new FileAction({
registerFileAction({
id: ACTION_CONVERT,
displayName: () => t('files', 'Save as …'),
iconSvgInline: () => AutoRenewSvg,
@ -69,15 +70,16 @@ export function registerConvertActions() {
return null
},
order: 25,
}))
} satisfies IFileAction)
// Register sub actions
actions.forEach(registerFileAction)
}
/**
* Generates an SVG icon for a given mime type by using the server's mime icon endpoint.
*
* @param mime
* @param mime - The mime type to generate the icon for
*/
export function generateIconSvg(mime: string) {
// Generate icon based on mime type

View file

@ -1,13 +1,14 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import type { IView } from '@nextcloud/files'
import axios from '@nextcloud/axios'
import * as capabilities from '@nextcloud/capabilities'
import * as eventBus from '@nextcloud/event-bus'
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
import { File, Folder, Permission } from '@nextcloud/files'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import logger from '../logger.ts'
import { action } from './deleteAction.ts'
@ -20,12 +21,12 @@ vi.mock('@nextcloud/capabilities')
const view = {
id: 'files',
name: 'Files',
} as View
} as IView
const trashbinView = {
id: 'trashbin',
name: 'Trashbin',
} as View
} as IView
describe('Delete action conditions tests', () => {
beforeEach(() => {
@ -90,7 +91,6 @@ describe('Delete action conditions tests', () => {
})
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('delete')
expect(action.displayName({
nodes: [file],

View file

@ -1,11 +1,14 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction } 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'
import { FileAction, Permission } from '@nextcloud/files'
import { Permission } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import PQueue from 'p-queue'
@ -20,7 +23,7 @@ const queue = new PQueue({ concurrency: 5 })
export const ACTION_DELETE = 'delete'
export const action = new FileAction({
export const action: IFileAction = {
id: ACTION_DELETE,
displayName,
iconSvgInline: ({ nodes }) => {
@ -117,4 +120,4 @@ export const action = new FileAction({
description: t('files', 'Delete'),
key: 'Delete',
},
})
}

View file

@ -1,14 +1,14 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import type { IView } from '@nextcloud/files'
import axios from '@nextcloud/axios'
import * as dialogs from '@nextcloud/dialogs'
import * as eventBus from '@nextcloud/event-bus'
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
import { DefaultType, File, Folder, Permission } from '@nextcloud/files'
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
import { action } from './downloadAction.ts'
@ -19,7 +19,7 @@ vi.mock('@nextcloud/event-bus')
const view = {
id: 'files',
name: 'Files',
} as View
} as IView
// Mock webroot variable
beforeAll(() => {
@ -28,7 +28,6 @@ beforeAll(() => {
describe('Download action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('download')
expect(action.displayName({
nodes: [],

View file

@ -1,15 +1,15 @@
/**
/*!
* 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 { IFileAction, INode, IView } from '@nextcloud/files'
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { DefaultType, FileAction, FileType } from '@nextcloud/files'
import { DefaultType, FileType } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import logger from '../logger.ts'
import { useFilesStore } from '../store/files.ts'
@ -17,112 +17,7 @@ import { getPinia } from '../store/index.ts'
import { usePathsStore } from '../store/paths.ts'
import { isDownloadable } from '../utils/permissions.ts'
/**
* Trigger downloading a file.
*
* @param url The url of the asset to download
* @param name Optionally the recommended name of the download (browsers might ignore it)
*/
async function triggerDownload(url: string, name?: string) {
// try to see if the resource is still available
await axios.head(url)
const hiddenElement = document.createElement('a')
hiddenElement.download = name ?? ''
hiddenElement.href = url
hiddenElement.click()
}
/**
* Find the longest common path prefix of both input paths
*
* @param first The first path
* @param second The second path
*/
function longestCommonPath(first: string, second: string): string {
const firstSegments = first.split('/').filter(Boolean)
const secondSegments = second.split('/').filter(Boolean)
let base = ''
for (const [index, segment] of firstSegments.entries()) {
if (index >= second.length) {
break
}
if (segment !== secondSegments[index]) {
break
}
const sep = base === '' ? '' : '/'
base = `${base}${sep}${segment}`
}
return base
}
/**
* Download the given nodes.
*
* If only one node is given, it will be downloaded directly.
* If multiple nodes are given, they will be zipped and downloaded.
*
* @param nodes The node(s) to download
*/
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)
return
} else {
url = new URL(nodes[0].encodedSource)
url.searchParams.append('accept', 'zip')
}
} else {
url = new URL(nodes[0].encodedSource)
let base = url.pathname
for (const node of nodes.slice(1)) {
base = longestCommonPath(base, (new URL(node.encodedSource).pathname))
}
url.pathname = base
// The URL contains the path encoded so we need to decode as the query.append will re-encode it
const filenames = nodes.map((node) => decodeURIComponent(node.encodedSource.slice(url.href.length + 1)))
url.searchParams.append('accept', 'zip')
url.searchParams.append('files', JSON.stringify(filenames))
}
if (url.pathname.at(-1) !== '/') {
url.pathname = `${url.pathname}/`
}
await triggerDownload(url.href)
}
/**
* Get the current directory node for the given view and path.
* TODO: ideally the folder would directly be passed as exec params
*
* @param view The current view
* @param directory The directory path
* @return The current directory node or null if not found
*/
function getCurrentDirectory(view: View, directory: string): Node | null {
const filesStore = useFilesStore(getPinia())
const pathsStore = usePathsStore(getPinia())
if (!view?.id) {
return null
}
if (directory === '/') {
return filesStore.getRoot(view.id) || null
}
const fileId = pathsStore.getPath(view.id, directory)!
return filesStore.getNode(fileId) || null
}
export const action = new FileAction({
export const action: IFileAction = {
id: 'download',
default: DefaultType.DEFAULT,
@ -172,4 +67,109 @@ export const action = new FileAction({
},
order: 30,
})
}
/**
* Trigger downloading a file.
*
* @param url The url of the asset to download
* @param name Optionally the recommended name of the download (browsers might ignore it)
*/
async function triggerDownload(url: string, name?: string) {
// try to see if the resource is still available
await axios.head(url)
const hiddenElement = document.createElement('a')
hiddenElement.download = name ?? ''
hiddenElement.href = url
hiddenElement.click()
}
/**
* Find the longest common path prefix of both input paths
*
* @param first The first path
* @param second The second path
*/
function longestCommonPath(first: string, second: string): string {
const firstSegments = first.split('/').filter(Boolean)
const secondSegments = second.split('/').filter(Boolean)
let base = ''
for (const [index, segment] of firstSegments.entries()) {
if (index >= second.length) {
break
}
if (segment !== secondSegments[index]) {
break
}
const sep = base === '' ? '' : '/'
base = `${base}${sep}${segment}`
}
return base
}
/**
* Download the given nodes.
*
* If only one node is given, it will be downloaded directly.
* If multiple nodes are given, they will be zipped and downloaded.
*
* @param nodes The node(s) to download
*/
async function downloadNodes(nodes: INode[]) {
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)
return
} else {
url = new URL(nodes[0].encodedSource)
url.searchParams.append('accept', 'zip')
}
} else {
url = new URL(nodes[0].encodedSource)
let base = url.pathname
for (const node of nodes.slice(1)) {
base = longestCommonPath(base, (new URL(node.encodedSource).pathname))
}
url.pathname = base
// The URL contains the path encoded so we need to decode as the query.append will re-encode it
const filenames = nodes.map((node) => decodeURIComponent(node.encodedSource.slice(url.href.length + 1)))
url.searchParams.append('accept', 'zip')
url.searchParams.append('files', JSON.stringify(filenames))
}
if (url.pathname.at(-1) !== '/') {
url.pathname = `${url.pathname}/`
}
await triggerDownload(url.href)
}
/**
* Get the current directory node for the given view and path.
* TODO: ideally the folder would directly be passed as exec params
*
* @param view The current view
* @param directory The directory path
* @return The current directory node or null if not found
*/
function getCurrentDirectory(view: IView, directory: string): INode | null {
const filesStore = useFilesStore(getPinia())
const pathsStore = usePathsStore(getPinia())
if (!view?.id) {
return null
}
if (directory === '/') {
return filesStore.getRoot(view.id) || null
}
const fileId = pathsStore.getPath(view.id, directory)!
return filesStore.getNode(fileId) || null
}

View file

@ -1,13 +1,13 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Folder, View } from '@nextcloud/files'
import type { IFolder, IView } from '@nextcloud/files'
import axios from '@nextcloud/axios'
import * as eventBus from '@nextcloud/event-bus'
import { File, FileAction, Permission } from '@nextcloud/files'
import { File, Permission } from '@nextcloud/files'
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
import logger from '../logger.ts'
import { action } from './favoriteAction.ts'
@ -19,12 +19,12 @@ vi.mock('@nextcloud/axios')
const view = {
id: 'files',
name: 'Files',
} as View
} as IView
const favoriteView = {
id: 'favorites',
name: 'Favorites',
} as View
} as IView
// Mock webroot variable
beforeAll(() => {
@ -46,18 +46,17 @@ describe('Favorite action conditions tests', () => {
root: '/files/admin',
})
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('favorite')
expect(action.displayName({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe('Add to favorites')
expect(action.iconSvgInline({
nodes: [],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toMatch(/<svg.+<\/svg>/)
expect(action.default).toBeUndefined()
@ -79,7 +78,7 @@ describe('Favorite action conditions tests', () => {
expect(action.displayName({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe('Remove from favorites')
})
@ -122,25 +121,25 @@ describe('Favorite action conditions tests', () => {
expect(action.displayName({
nodes: [file1, file2, file3],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe('Add to favorites')
expect(action.displayName({
nodes: [file2, file3],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe('Add to favorites')
expect(action.displayName({
nodes: [file2, file3],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe('Add to favorites')
expect(action.displayName({
nodes: [file1, file3],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe('Remove from favorites')
})
@ -161,7 +160,7 @@ describe('Favorite action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe(true)
})
@ -179,7 +178,7 @@ describe('Favorite action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})).toBe(false)
})
@ -205,7 +204,7 @@ describe('Favorite action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
@ -239,7 +238,7 @@ describe('Favorite action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
@ -273,7 +272,7 @@ describe('Favorite action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view: favoriteView,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
@ -308,7 +307,7 @@ describe('Favorite action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view: favoriteView,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
@ -345,7 +344,7 @@ describe('Favorite action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
@ -383,7 +382,7 @@ describe('Favorite action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
@ -437,7 +436,7 @@ describe('Favorite action batch execute tests', () => {
const exec = await action.execBatch!({
nodes: [file1, file2],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
expect(exec).toStrictEqual([true, true])
@ -479,7 +478,7 @@ describe('Favorite action batch execute tests', () => {
const exec = await action.execBatch!({
nodes: [file1, file2],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
expect(exec).toStrictEqual([true, true])

View file

@ -3,13 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { INode, IView } from '@nextcloud/files'
import type { IFileAction, INode, IView } from '@nextcloud/files'
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
import StarSvg from '@mdi/svg/svg/star.svg?raw'
import axios from '@nextcloud/axios'
import { emit } from '@nextcloud/event-bus'
import { FileAction, Permission } from '@nextcloud/files'
import { Permission } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { encodePath } from '@nextcloud/paths'
import { generateUrl } from '@nextcloud/router'
@ -18,63 +18,11 @@ import PQueue from 'p-queue'
import Vue from 'vue'
import logger from '../logger.ts'
export const ACTION_FAVORITE = 'favorite'
const queue = new PQueue({ concurrency: 5 })
/**
* If any of the nodes is not favorited, we display the favorite action.
*
* @param nodes - The nodes to check
*/
function shouldFavorite(nodes: INode[]): boolean {
return nodes.some((node) => node.attributes.favorite !== 1)
}
export const ACTION_FAVORITE = 'favorite'
/**
* Favorite or unfavorite a node
*
* @param node - The node to favorite/unfavorite
* @param view - The current view
* @param willFavorite - Whether to favorite or unfavorite the node
*/
export async function favoriteNode(node: INode, view: IView, willFavorite: boolean): Promise<boolean> {
try {
// TODO: migrate to webdav tags plugin
const url = generateUrl('/apps/files/api/v1/files') + encodePath(node.path)
await axios.post(url, {
tags: willFavorite
? [window.OC.TAG_FAVORITE]
: [],
})
// Let's delete if we are in the favourites view
// AND if it is removed from the user favorites
// AND it's in the root of the favorites view
if (view.id === 'favorites' && !willFavorite && node.dirname === '/') {
emit('files:node:deleted', node)
}
// Update the node webdav attribute
Vue.set(node.attributes, 'favorite', willFavorite ? 1 : 0)
emit('files:node:updated', node)
// Dispatch event to whoever is interested
if (willFavorite) {
emit('files:favorites:added', node)
} else {
emit('files:favorites:removed', node)
}
return true
} catch (error) {
const action = willFavorite ? 'adding a file to favourites' : 'removing a file from favourites'
logger.error('Error while ' + action, { error, source: node.source, node })
return false
}
}
export const action = new FileAction({
export const action: IFileAction = {
id: ACTION_FAVORITE,
displayName({ nodes }) {
return shouldFavorite(nodes)
@ -132,4 +80,56 @@ export const action = new FileAction({
description: t('files', 'Add or remove favorite'),
key: 'S',
},
})
}
/**
* Favorite or unfavorite a node
*
* @param node - The node to favorite/unfavorite
* @param view - The current view
* @param willFavorite - Whether to favorite or unfavorite the node
*/
export async function favoriteNode(node: INode, view: IView, willFavorite: boolean): Promise<boolean> {
try {
// TODO: migrate to webdav tags plugin
const url = generateUrl('/apps/files/api/v1/files') + encodePath(node.path)
await axios.post(url, {
tags: willFavorite
? [window.OC.TAG_FAVORITE]
: [],
})
// Let's delete if we are in the favourites view
// AND if it is removed from the user favorites
// AND it's in the root of the favorites view
if (view.id === 'favorites' && !willFavorite && node.dirname === '/') {
emit('files:node:deleted', node)
}
// Update the node webdav attribute
Vue.set(node.attributes, 'favorite', willFavorite ? 1 : 0)
emit('files:node:updated', node)
// Dispatch event to whoever is interested
if (willFavorite) {
emit('files:favorites:added', node)
} else {
emit('files:favorites:removed', node)
}
return true
} catch (error) {
const action = willFavorite ? 'adding a file to favourites' : 'removing a file from favourites'
logger.error('Error while ' + action, { error, source: node.source, node })
return false
}
}
/**
* If any of the nodes is not favored, we display the favorite action.
*
* @param nodes - The nodes to check
*/
function shouldFavorite(nodes: INode[]): boolean {
return nodes.some((node) => node.attributes.favorite !== 1)
}

View file

@ -4,7 +4,7 @@
*/
import type { IFilePickerButton } from '@nextcloud/dialogs'
import type { IFolder, INode } from '@nextcloud/files'
import type { IFileAction, IFolder, INode } from '@nextcloud/files'
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
import type { MoveCopyResult } from './moveOrCopyActionUtils.ts'
@ -13,7 +13,7 @@ import CopyIconSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
import { isAxiosError } from '@nextcloud/axios'
import { FilePickerClosed, getFilePickerBuilder, openConflictPicker, showError, showLoading } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { FileAction, FileType, getUniqueName, NodeStatus, Permission } from '@nextcloud/files'
import { FileType, getUniqueName, NodeStatus, Permission } from '@nextcloud/files'
import { defaultRootPath, getClient, getDefaultPropfind, resultToNode } from '@nextcloud/files/dav'
import { t } from '@nextcloud/l10n'
import { getConflicts } from '@nextcloud/upload'
@ -31,7 +31,7 @@ export class HintException extends Error {}
export const ACTION_COPY_MOVE = 'move-copy'
export const action = new FileAction({
export const action: IFileAction = {
id: ACTION_COPY_MOVE,
order: 15,
displayName({ nodes }) {
@ -84,7 +84,7 @@ export const action = new FileAction({
return nodes.map(() => false)
}
},
})
}
/**
* Handle the copy/move of a node to a destination
@ -248,11 +248,11 @@ function getActionForNodes(nodes: INode[]): MoveCopyAction {
function createLoadingNotification(mode: MoveCopyAction, sources: string[], destination: string): () => void {
const text = mode === MoveCopyAction.MOVE
? (sources.length === 1
? t('files', 'Moving "{source}" to "{destination}" …', { source: sources[0], destination })
? t('files', 'Moving "{source}" to "{destination}" …', { source: sources[0]!, destination })
: t('files', 'Moving {count} files to "{destination}" …', { count: sources.length, destination })
)
: (sources.length === 1
? t('files', 'Copying "{source}" to "{destination}" …', { source: sources[0], destination })
? t('files', 'Copying "{source}" to "{destination}" …', { source: sources[0]!, destination })
: t('files', 'Copying {count} files to "{destination}" …', { count: sources.length, destination })
)
@ -277,7 +277,7 @@ async function openFilePickerForAction(
const fileIDs = nodes.map((node) => node.fileid).filter(Boolean)
const filePicker = getFilePickerBuilder(t('files', 'Choose destination'))
.allowDirectories(true)
.setFilter((n: INode) => {
.setFilter((n) => {
// We don't want to show the current nodes in the file picker
return !fileIDs.includes(n.fileid)
})
@ -288,7 +288,7 @@ async function openFilePickerForAction(
.setMimeTypeFilter([])
.setMultiSelect(false)
.startAt(dir)
.setButtonFactory((selection: INode[], path: string) => {
.setButtonFactory((selection, path) => {
const buttons: IFilePickerButton[] = []
const target = basename(path)
@ -300,9 +300,9 @@ async function openFilePickerForAction(
label: target ? t('files', 'Copy to {target}', { target }, { escape: false, sanitize: false }) : t('files', 'Copy'),
variant: 'primary',
icon: CopyIconSvg,
async callback(destination: INode[]) {
async callback(destination) {
resolve({
destination: destination[0] as IFolder,
destination: destination[0] as unknown as IFolder,
action: MoveCopyAction.COPY,
} as MoveCopyResult)
},
@ -330,9 +330,9 @@ async function openFilePickerForAction(
label: target ? t('files', 'Move to {target}', { target }, undefined, { escape: false, sanitize: false }) : t('files', 'Move'),
variant: action === MoveCopyAction.MOVE ? 'primary' : 'secondary',
icon: FolderMoveSvg,
async callback(destination: INode[]) {
async callback(destination) {
resolve({
destination: destination[0] as IFolder,
destination: destination[0] as unknown as IFolder,
action: MoveCopyAction.MOVE,
} as MoveCopyResult)
},

View file

@ -1,18 +1,18 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import type { IView } from '@nextcloud/files'
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
import { DefaultType, File, Folder, Permission } from '@nextcloud/files'
import { describe, expect, test, vi } from 'vitest'
import { action } from './openFolderAction.ts'
const view = {
id: 'files',
name: 'Files',
} as View
} as IView
describe('Open folder action conditions tests', () => {
test('Default values', () => {
@ -24,7 +24,6 @@ describe('Open folder action conditions tests', () => {
root: '/files/admin',
})
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('open-folder')
expect(action.displayName({
nodes: [folder],

View file

@ -1,12 +1,15 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
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({
import type { IFileAction } from '@nextcloud/files'
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
import { DefaultType, FileType, Permission } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
export const action: IFileAction = {
id: 'open-folder',
displayName({ nodes }) {
if (nodes.length !== 1 || !nodes[0]) {
@ -51,4 +54,4 @@ export const action = new FileAction({
// Main action if enabled, meaning folders only
default: DefaultType.HIDDEN,
order: -100,
})
}

View file

@ -1,27 +1,26 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import type { IView } from '@nextcloud/files'
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
import { DefaultType, File, Folder, Permission } from '@nextcloud/files'
import { describe, expect, test, vi } from 'vitest'
import { action } from './openInFilesAction.ts'
const view = {
id: 'files',
name: 'Files',
} as View
} as IView
const recentView = {
id: 'recent',
name: 'Recent',
} as View
} as IView
describe('Open in files action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('open-in-files')
expect(action.displayName({
nodes: [],

View file

@ -1,12 +1,15 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { DefaultType, FileAction, FileType } from '@nextcloud/files'
import type { IFileAction } from '@nextcloud/files'
import { DefaultType, FileType } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { VIEW_ID as SEARCH_VIEW_ID } from '../views/search.ts'
export const action = new FileAction({
export const action: IFileAction = {
id: 'open-in-files',
displayName: () => t('files', 'Open in Files'),
iconSvgInline: () => '',
@ -36,4 +39,4 @@ export const action = new FileAction({
// Before openFolderAction
order: -1000,
default: DefaultType.HIDDEN,
})
}

View file

@ -1,13 +1,13 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Folder, View } from '@nextcloud/files'
import type { IFolder, IView } from '@nextcloud/files'
import axios from '@nextcloud/axios'
import * as nextcloudDialogs from '@nextcloud/dialogs'
import { File, FileAction, Permission } from '@nextcloud/files'
import { File, Permission } from '@nextcloud/files'
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
import { action } from './openLocallyAction.ts'
@ -17,7 +17,7 @@ vi.mock('@nextcloud/axios')
const view = {
id: 'files',
name: 'Files',
} as View
} as IView
// Mock web root variable
beforeAll(() => {
@ -28,18 +28,17 @@ beforeAll(() => {
describe('Open locally action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('edit-locally')
expect(action.displayName({
nodes: [],
view,
folder: {} as any,
folder: {} as IFolder,
contents: [],
})).toBe('Open locally')
expect(action.iconSvgInline({
nodes: [],
view,
folder: {} as any,
folder: {} as IFolder,
contents: [],
})).toMatch(/<svg.+<\/svg>/)
expect(action.default).toBeUndefined()
@ -62,7 +61,7 @@ describe('Open locally action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as any,
folder: {} as IFolder,
contents: [],
})).toBe(true)
})
@ -80,7 +79,7 @@ describe('Open locally action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as any,
folder: {} as IFolder,
contents: [],
})).toBe(false)
})
@ -107,7 +106,7 @@ describe('Open locally action enabled tests', () => {
expect(action.enabled!({
nodes: [file1, file2],
view,
folder: {} as any,
folder: {} as IFolder,
contents: [],
})).toBe(false)
})
@ -125,7 +124,7 @@ describe('Open locally action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as any,
folder: {} as IFolder,
contents: [],
})).toBe(false)
})
@ -144,7 +143,7 @@ describe('Open locally action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as any,
folder: {} as IFolder,
contents: [],
})).toBe(false)
})
@ -177,7 +176,7 @@ describe('Open locally action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})
@ -207,7 +206,7 @@ describe('Open locally action execute tests', () => {
const exec = await action.exec({
nodes: [file],
view,
folder: {} as Folder,
folder: {} as IFolder,
contents: [],
})

View file

@ -1,13 +1,15 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction } 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'
import axios from '@nextcloud/axios'
import { DialogBuilder, showError } from '@nextcloud/dialogs'
import { FileAction } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { encodePath } from '@nextcloud/paths'
import { generateOcsUrl } from '@nextcloud/router'
@ -15,7 +17,7 @@ import { isPublicShare } from '@nextcloud/sharing/public'
import logger from '../logger.ts'
import { isSyncable } from '../utils/permissions.ts'
export const action = new FileAction({
export const action: IFileAction = {
id: 'edit-locally',
displayName: () => t('files', 'Open locally'),
iconSvgInline: () => LaptopSvg,
@ -32,7 +34,7 @@ export const action = new FileAction({
return false
}
return isSyncable(nodes[0])
return isSyncable(nodes[0]!)
},
async exec({ nodes }) {
@ -41,7 +43,7 @@ export const action = new FileAction({
},
order: 25,
})
}
/**
* Try to open the path in the Nextcloud client.

View file

@ -3,10 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import type { IView } from '@nextcloud/files'
import * as eventBus from '@nextcloud/event-bus'
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
import { File, Folder, Permission } from '@nextcloud/files'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { useFilesStore } from '../store/files.ts'
import { getPinia } from '../store/index.ts'
@ -15,7 +15,7 @@ import { action } from './renameAction.ts'
const view = {
id: 'files',
name: 'Files',
} as View
} as IView
beforeEach(() => {
const root = new Folder({
@ -31,7 +31,6 @@ beforeEach(() => {
describe('Rename action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('rename')
expect(action.displayName({
nodes: [],

View file

@ -1,10 +1,13 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction } 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'
import { Permission } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { dirname } from 'path'
import { useFilesStore } from '../store/files.ts'
@ -12,7 +15,7 @@ import { getPinia } from '../store/index.ts'
export const ACTION_RENAME = 'rename'
export const action = new FileAction({
export const action: IFileAction = {
id: ACTION_RENAME,
displayName: () => t('files', 'Rename'),
iconSvgInline: () => PencilSvg,
@ -52,4 +55,4 @@ export const action = new FileAction({
description: t('files', 'Rename'),
key: 'F2',
},
})
}

View file

@ -1,11 +1,11 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IView } from '@nextcloud/files'
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
import { File, Folder, Permission } from '@nextcloud/files'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import logger from '../logger.ts'
import { action } from './sidebarAction.ts'
@ -32,7 +32,6 @@ beforeEach(() => {
describe('Open sidebar action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('details')
expect(action.displayName({
nodes: [],

View file

@ -3,15 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction } from '@nextcloud/files'
import InformationSvg from '@mdi/svg/svg/information-outline.svg?raw'
import { FileAction, getSidebar, Permission } from '@nextcloud/files'
import { getSidebar, Permission } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { isPublicShare } from '@nextcloud/sharing/public'
import logger from '../logger.ts'
export const ACTION_DETAILS = 'details'
export const action = new FileAction({
export const action: IFileAction = {
id: ACTION_DETAILS,
displayName: () => t('files', 'Details'),
iconSvgInline: () => InformationSvg,
@ -59,4 +61,4 @@ export const action = new FileAction({
key: 'D',
description: t('files', 'Open the details sidebar'),
},
})
}

View file

@ -1,26 +1,26 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { View } from '@nextcloud/files'
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
import type { IView } from '@nextcloud/files'
import { File, Folder, Permission } from '@nextcloud/files'
import { describe, expect, test, vi } from 'vitest'
import { action } from './viewInFolderAction.ts'
const view = {
id: 'trashbin',
name: 'Trashbin',
} as View
} as IView
const viewFiles = {
id: 'files',
name: 'Files',
} as View
} as IView
describe('View in folder action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('view-in-folder')
expect(action.displayName({
nodes: [],

View file

@ -1,13 +1,16 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { IFileAction } from '@nextcloud/files'
import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
import { FileAction, FileType, Permission } from '@nextcloud/files'
import { FileType, Permission } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { isPublicShare } from '@nextcloud/sharing/public'
export const action = new FileAction({
export const action: IFileAction = {
id: 'view-in-folder',
displayName() {
return t('files', 'View in folder')
@ -61,4 +64,4 @@ export const action = new FileAction({
},
order: 80,
})
}

View file

@ -7,10 +7,10 @@
</template>
<script lang="ts">
import type { FileAction, Folder, Node, View } from '@nextcloud/files'
import type { IFileAction, IFolder, INode, IView } from '@nextcloud/files'
import type { PropType } from 'vue'
type RenderFunction = typeof FileAction.prototype.renderInline
type RenderFunction = IFileAction['renderInline']
/**
* This component is used to render custom
@ -22,17 +22,17 @@ export default {
name: 'CustomElementRender',
props: {
source: {
type: Object as PropType<Node>,
type: Object as PropType<INode>,
required: true,
},
activeView: {
type: Object as PropType<View>,
type: Object as PropType<IView>,
required: true,
},
activeFolder: {
type: Object as PropType<Folder>,
type: Object as PropType<IFolder>,
required: true,
},

View file

@ -121,7 +121,7 @@
</template>
<script lang="ts">
import type { ActionContextSingle, FileAction, Node } from '@nextcloud/files'
import type { ActionContextSingle, IFileAction, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import { DefaultType, NodeStatus } from '@nextcloud/files'
@ -179,7 +179,7 @@ export default defineComponent({
// The file list is guaranteed to be shown with active view - thus we can set the `loaded` flag
const activeStore = useActiveStore()
const { isNarrow } = useFileListWidth()
const enabledFileActions = inject<FileAction[]>('enabledFileActions', [])
const enabledFileActions = inject<IFileAction[]>('enabledFileActions', [])
return {
activeStore,
enabledFileActions,
@ -302,7 +302,7 @@ export default defineComponent({
},
methods: {
actionDisplayName(action: FileAction) {
actionDisplayName(action: IFileAction) {
try {
if ((this.gridMode || (this.isNarrow && action.inline)) && typeof action.title === 'function') {
// if an inline action is rendered in the menu for
@ -320,7 +320,7 @@ export default defineComponent({
}
},
isLoadingAction(action: FileAction) {
isLoadingAction(action: IFileAction) {
if (!this.isActive) {
return false
}

View file

@ -39,7 +39,7 @@
</template>
<script lang="ts">
import type { FileAction, Node, TFileType } from '@nextcloud/files'
import type { IFileAction, Node, TFileType } from '@nextcloud/files'
import type { PropType } from 'vue'
import { showError } from '@nextcloud/dialogs'
@ -92,7 +92,7 @@ export default defineComponent({
const userConfigStore = useUserConfigStore()
const { activeFolder, activeView } = useActiveStore()
const defaultFileAction = inject<FileAction | undefined>('defaultFileAction')
const defaultFileAction = inject<IFileAction | undefined>('defaultFileAction')
return {
activeFolder,

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { FileAction } from '@nextcloud/files'
import type { IFileAction } from '@nextcloud/files'
import type { PropType } from 'vue'
import type { FileSource } from '../types.ts'
@ -234,7 +234,7 @@ export default defineComponent({
}
return actions
.filter((action: FileAction) => {
.filter((action: IFileAction) => {
if (!action.enabled) {
return true
}
@ -257,7 +257,7 @@ export default defineComponent({
},
defaultFileAction() {
return this.enabledFileActions.find((action: FileAction) => action.default !== undefined)
return this.enabledFileActions.find((action: IFileAction) => action.default !== undefined)
},
},

View file

@ -70,7 +70,7 @@
</template>
<script lang="ts">
import type { ActionContext, FileAction, Node, View } from '@nextcloud/files'
import type { ActionContext, IFileAction, Node, View } from '@nextcloud/files'
import type { PropType } from 'vue'
import type { FileSource } from '../types.ts'
@ -158,7 +158,7 @@ export default defineComponent({
},
computed: {
enabledFileActions(): FileAction[] {
enabledFileActions(): IFileAction[] {
return actions
// We don't handle renderInline actions in this component
.filter((action) => !action.renderInline)
@ -178,7 +178,7 @@ export default defineComponent({
* This means that they are not within a menu, nor
* being the parent of submenu actions.
*/
enabledInlineActions(): FileAction[] {
enabledInlineActions(): IFileAction[] {
return this.enabledFileActions
// Remove all actions that are not top-level actions
.filter((action) => action.parent === undefined)
@ -194,7 +194,7 @@ export default defineComponent({
* Return the rest of enabled actions that are not
* rendered inlined.
*/
enabledMenuActions(): FileAction[] {
enabledMenuActions(): IFileAction[] {
// If we're in a submenu, only render the inline
// actions before the filtered submenu
if (this.openedSubmenu) {

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { FileAction } from '@nextcloud/files'
import type { IFileAction } from '@nextcloud/files'
import { defineComponent } from 'vue'
@ -11,23 +11,23 @@ export default defineComponent({
data() {
return {
openedSubmenu: null as FileAction | null,
openedSubmenu: null as IFileAction | null,
}
},
computed: {
enabledSubmenuActions(): Record<string, FileAction[]> {
return (this.enabledFileActions as FileAction[])
enabledSubmenuActions(): Record<string, IFileAction[]> {
return (this.enabledFileActions as IFileAction[])
.reduce((record, action) => {
if (action.parent !== undefined) {
if (!record[action.parent]) {
record[action.parent] = []
}
record[action.parent].push(action)
record[action.parent]!.push(action)
}
return record
}, {} as Record<string, FileAction[]>)
}, {} as Record<string, IFileAction[]>)
},
},
@ -38,11 +38,11 @@ export default defineComponent({
*
* @param action The action to check
*/
isValidMenu(action: FileAction): boolean {
isValidMenu(action: IFileAction): boolean {
return this.enabledSubmenuActions[action.id]?.length > 0
},
async onBackToMenuClick(action: FileAction | null) {
async onBackToMenuClick(action: IFileAction | null) {
if (!action) {
return
}

View file

@ -3,19 +3,19 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { FileAction, IFolder, INode, IView } from '@nextcloud/files'
import type { IFileAction, IFolder, INode, IView } from '@nextcloud/files'
import { subscribe } from '@nextcloud/event-bus'
import { getNavigation } from '@nextcloud/files'
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
import { ref, shallowRef, watch } from 'vue'
import logger from '../logger.ts'
export const useActiveStore = defineStore('active', () => {
/**
* The currently active action
*/
const activeAction = ref<FileAction>()
const activeAction = shallowRef<IFileAction>()
/**
* The currently active folder
@ -30,7 +30,7 @@ export const useActiveStore = defineStore('active', () => {
/**
* The current active view
*/
const activeView = ref<IView>()
const activeView = shallowRef<IView>()
// Set the active node on the router params
watch(activeNode, () => {

View file

@ -1,8 +1,9 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { FileAction, Folder, Node, View } from '@nextcloud/files'
import type { IFileAction, IFolder, INode, IView } from '@nextcloud/files'
import type { Upload } from '@nextcloud/upload'
// Global definitions
@ -12,11 +13,11 @@ export type ViewId = string
// Files store
export type FilesStore = {
[source: FileSource]: Node
[source: FileSource]: INode
}
export type RootsStore = {
[service: Service]: Folder
[service: Service]: IFolder
}
export type FilesState = {
@ -25,7 +26,7 @@ export type FilesState = {
}
export interface RootOptions {
root: Folder
root: IFolder
service: Service
}
@ -95,7 +96,7 @@ export interface ViewConfigStore {
// Renaming store
export interface RenamingStore {
renamingNode?: Node
renamingNode?: INode
newName: string
}
@ -111,10 +112,10 @@ export interface DragAndDropStore {
// Active node store
export interface ActiveStore {
activeAction: FileAction | null
activeFolder: Folder | null
activeNode: Node | null
activeView: View | null
activeAction: IFileAction | null
activeFolder: IFolder | null
activeNode: INode | null
activeView: IView | null
}
/**

View file

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { ActionContextSingle, FileAction } from '@nextcloud/files'
import type { ActionContextSingle, IFileAction } from '@nextcloud/files'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { NodeStatus } from '@nextcloud/files'
@ -16,7 +16,7 @@ import { useActiveStore } from '../store/active.ts'
*
* @param action The action to execute
*/
export async function executeAction(action: FileAction) {
export async function executeAction(action: IFileAction) {
const activeStore = useActiveStore()
const currentFolder = activeStore.activeFolder
const currentNode = activeStore.activeNode

View file

@ -2,7 +2,8 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { ShareAttribute } from '../../../files_sharing/src/sharing.ts'
import { Permission } from '@nextcloud/files'
@ -13,7 +14,7 @@ import { Permission } from '@nextcloud/files'
* @param node The node to check
* @return True if downloadable, false otherwise
*/
export function isDownloadable(node: Node): boolean {
export function isDownloadable(node: INode): boolean {
if ((node.permissions & Permission.READ) === 0) {
return false
}
@ -43,7 +44,7 @@ export function isDownloadable(node: Node): boolean {
* @param node The node to check
* @return True if syncable, false otherwise
*/
export function isSyncable(node: Node): boolean {
export function isSyncable(node: INode): boolean {
if (!node.isDavResource) {
return false
}

View file

@ -4,8 +4,8 @@
*/
import type { User } from '@nextcloud/e2e-test-server/cypress'
import type { IFileAction } from '@nextcloud/files'
import { FileAction } from '@nextcloud/files'
import { getActionButtonForFileId, getActionEntryForFileId, getRowForFile, getSelectionActionButton, getSelectionActionEntry, selectRowForFile } from './FilesUtils.ts'
const ACTION_DELETE = 'delete'
@ -14,7 +14,7 @@ const ACTION_DETAILS = 'details'
declare global {
interface Window {
_nc_fileactions: FileAction[]
_nc_fileactions: IFileAction[]
}
}
@ -58,28 +58,28 @@ describe('Files: Actions', { testIsolation: true }, () => {
})
it('Show some nested actions', () => {
const parent = new FileAction({
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
})
}
const child1 = new FileAction({
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
})
}
const child2 = new FileAction({
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
})
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
@ -147,30 +147,30 @@ describe('Files: Actions', { testIsolation: true }, () => {
})
it('Show some nested actions for a selection', () => {
const parent = new FileAction({
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
})
}
const child1 = new FileAction({
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
execBatch: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
})
}
const child2 = new FileAction({
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
execBatch: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
})
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
@ -220,28 +220,28 @@ describe('Files: Actions', { testIsolation: true }, () => {
})
it('Do not show parent if nested action has no batch support', () => {
const parent = new FileAction({
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
})
}
const child1 = new FileAction({
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
})
}
const child2 = new FileAction({
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
})
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here