--- /dev/null
+import { basename } from 'node:path'
+import mime from 'mime'
+
+class FileSystemEntry {
+
+ private _isFile: boolean
+ private _fullPath: string
+
+ constructor(isFile: boolean, fullPath: string) {
+ this._isFile = isFile
+ this._fullPath = fullPath
+ }
+
+ get isFile() {
+ return !!this._isFile
+ }
+
+ get isDirectory() {
+ return !this.isFile
+ }
+
+ get name() {
+ return basename(this._fullPath)
+ }
+
+}
+
+export class FileSystemFileEntry extends FileSystemEntry {
+
+ private _contents: string
+ private _lastModified: number
+
+ constructor(fullPath: string, contents: string, lastModified = Date.now()) {
+ super(true, fullPath)
+ this._contents = contents
+ this._lastModified = lastModified
+ }
+
+ file(success: (file: File) => void) {
+ const lastModified = this._lastModified
+ // Faking the mime by using the file extension
+ const type = mime.getType(this.name) || ''
+ success(new File([this._contents], this.name, { lastModified, type }))
+ }
+
+}
+
+export class FileSystemDirectoryEntry extends FileSystemEntry {
+
+ private _entries: FileSystemEntry[]
+
+ constructor(fullPath: string, entries: FileSystemEntry[]) {
+ super(false, fullPath)
+ this._entries = entries || []
+ }
+
+ createReader() {
+ let read = false
+ return {
+ readEntries: (success: (entries: FileSystemEntry[]) => void) => {
+ if (read) {
+ return success([])
+ }
+ read = true
+ success(this._entries)
+ },
+ }
+ }
+
+}
+
+/**
+ * This mocks the File API's File class
+ * It will allow us to test the Filesystem API as well as the
+ * File API in the same test suite.
+ */
+export class DataTransferItem {
+
+ private _type: string
+ private _entry: FileSystemEntry
+
+ getAsEntry?: () => FileSystemEntry
+
+ constructor(type = '', entry: FileSystemEntry, isFileSystemAPIAvailable = true) {
+ this._type = type
+ this._entry = entry
+
+ // Only when the Files API is available we are
+ // able to get the entry
+ if (isFileSystemAPIAvailable) {
+ this.getAsEntry = () => this._entry
+ }
+ }
+
+ get kind() {
+ return 'file'
+ }
+
+ get type() {
+ return this._type
+ }
+
+ getAsFile(): File|null {
+ if (this._entry.isFile && this._entry instanceof FileSystemFileEntry) {
+ let file: File | null = null
+ this._entry.file((f) => {
+ file = f
+ })
+ return file
+ }
+
+ // The browser will return an empty File object if the entry is a directory
+ return new File([], this._entry.name, { type: '' })
+ }
+
+}
+
+export const fileSystemEntryToDataTransferItem = (entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem => {
+ return new DataTransferItem(
+ entry.isFile ? 'text/plain' : 'httpd/unix-directory',
+ entry,
+ isFileSystemAPIAvailable,
+ )
+}
// If another button is pressed, cancel it. This
// allows cancelling the drag with the right click.
- if (event.button !== 0) {
+ if (event.button) {
return
}
// Scroll to last successful upload in current directory if terminated
const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED
&& !upload.file.webkitRelativePath.includes('/')
- && upload.response?.headers?.['oc-fileid'])
+ && upload.response?.headers?.['oc-fileid']
+ // Only use the last ID if it's in the current folder
+ && upload.source.replace(folder.source, '').split('/').length === 2)
if (lastUpload !== undefined) {
+ logger.debug('Scrolling to last upload in current folder', { lastUpload })
this.$router.push({
...this.$route,
params: {
// If another button is pressed, cancel it. This
// allows cancelling the drag with the right click.
- if (!this.canDrop || event.button !== 0) {
+ if (!this.canDrop || event.button) {
return
}
*/
import type { Upload } from '@nextcloud/upload'
-import type { FileStat, ResponseDataDetailed } from 'webdav'
+import type { RootDirectory } from './DropServiceUtils'
-import { emit } from '@nextcloud/event-bus'
-import { Folder, Node, NodeStatus, davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
-import { getUploader, hasConflict, openConflictPicker } from '@nextcloud/upload'
+import { Folder, Node, NodeStatus, davRootPath } from '@nextcloud/files'
+import { getUploader, hasConflict } from '@nextcloud/upload'
import { join } from 'path'
import { joinPaths } from '@nextcloud/paths'
import { showError, showInfo, showSuccess, showWarning } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import Vue from 'vue'
+import { Directory, traverseTree, resolveConflict, createDirectoryIfNotExists } from './DropServiceUtils'
import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction'
import { MoveCopyAction } from '../actions/moveOrCopyActionUtils'
import logger from '../logger.js'
-/**
- * This represents a Directory in the file tree
- * We extend the File class to better handling uploading
- * and stay as close as possible as the Filesystem API.
- * This also allow us to hijack the size or lastModified
- * properties to compute them dynamically.
- */
-class Directory extends File {
-
- /* eslint-disable no-use-before-define */
- _contents: (Directory|File)[]
-
- constructor(name, contents: (Directory|File)[] = []) {
- super([], name, { type: 'httpd/unix-directory' })
- this._contents = contents
- }
-
- set contents(contents: (Directory|File)[]) {
- this._contents = contents
- }
-
- get contents(): (Directory|File)[] {
- return this._contents
- }
-
- get size() {
- return this._computeDirectorySize(this)
- }
-
- get lastModified() {
- if (this._contents.length === 0) {
- return Date.now()
- }
- return this._computeDirectoryMtime(this)
- }
-
- /**
- * Get the last modification time of a file tree
- * This is not perfect, but will get us a pretty good approximation
- * @param directory the directory to traverse
- */
- _computeDirectoryMtime(directory: Directory): number {
- return directory.contents.reduce((acc, file) => {
- return file.lastModified > acc
- // If the file is a directory, the lastModified will
- // also return the results of its _computeDirectoryMtime method
- // Fancy recursion, huh?
- ? file.lastModified
- : acc
- }, 0)
- }
-
- /**
- * Get the size of a file tree
- * @param directory the directory to traverse
- */
- _computeDirectorySize(directory: Directory): number {
- return directory.contents.reduce((acc: number, entry: Directory|File) => {
- // If the file is a directory, the size will
- // also return the results of its _computeDirectorySize method
- // Fancy recursion, huh?
- return acc + entry.size
- }, 0)
- }
-
-}
-
-type RootDirectory = Directory & {
- name: 'root'
-}
-
-/**
- * Traverse a file tree using the Filesystem API
- * @param entry the entry to traverse
- */
-const traverseTree = async (entry: FileSystemEntry): Promise<Directory|File> => {
- // Handle file
- if (entry.isFile) {
- return new Promise<File>((resolve, reject) => {
- (entry as FileSystemFileEntry).file(resolve, reject)
- })
- }
-
- // Handle directory
- logger.debug('Handling recursive file tree', { entry: entry.name })
- const directory = entry as FileSystemDirectoryEntry
- const entries = await readDirectory(directory)
- const contents = (await Promise.all(entries.map(traverseTree))).flat()
- return new Directory(directory.name, contents)
-}
-
-/**
- * Read a directory using Filesystem API
- * @param directory the directory to read
- */
-const readDirectory = (directory: FileSystemDirectoryEntry): Promise<FileSystemEntry[]> => {
- const dirReader = directory.createReader()
-
- return new Promise<FileSystemEntry[]>((resolve, reject) => {
- const entries = [] as FileSystemEntry[]
- const getEntries = () => {
- dirReader.readEntries((results) => {
- if (results.length) {
- entries.push(...results)
- getEntries()
- } else {
- resolve(entries)
- }
- }, (error) => {
- reject(error)
- })
- }
-
- getEntries()
- })
-}
-
-const createDirectoryIfNotExists = async (absolutePath: string) => {
- const davClient = davGetClient()
- 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 resolveConflict = async <T extends ((Directory|File)|Node)>(files: Array<T>, destination: Folder, contents: Node[]): Promise<T[]> => {
- try {
- // List all conflicting files
- const conflicts = files.filter((file: File|Node) => {
- return contents.find((node: Node) => node.basename === (file instanceof File ? file.name : file.basename))
- }).filter(Boolean) as (File|Node)[]
-
- // List of incoming files that are NOT in conflict
- const uploads = files.filter((file: File|Node) => {
- return !conflicts.includes(file)
- })
-
- // Let the user choose what to do with the conflicting files
- const { selected, renamed } = await openConflictPicker(destination.path, conflicts, contents)
-
- logger.debug('Conflict resolution', { uploads, selected, renamed })
-
- // If the user selected nothing, we cancel the upload
- if (selected.length === 0 && renamed.length === 0) {
- // User skipped
- showInfo(t('files', 'Conflicts resolution skipped'))
- logger.info('User skipped the conflict resolution')
- return []
- }
-
- // Update the list of files to upload
- return [...uploads, ...selected, ...renamed] as (typeof files)
- } catch (error) {
- console.error(error)
- // User cancelled
- showError(t('files', 'Upload cancelled'))
- logger.error('User cancelled the upload')
- }
-
- return []
-}
-
/**
* This function converts a list of DataTransferItems to a file tree.
* It uses the Filesystem API if available, otherwise it falls back to the File API.
}).map((item) => {
// MDN recommends to try both, as it might be renamed in the future
return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined })?.getAsEntry?.()
- ?? item.webkitGetAsEntry()
+ ?? item?.webkitGetAsEntry?.()
?? item
}) as (FileSystemEntry | DataTransferItem)[]
// we therefore cannot upload directories recursively.
if (file.type === 'httpd/unix-directory' || !file.type) {
if (!warned) {
- showWarning(t('files', 'Your browser does not support the Filesystem API. Directories will not be uploaded.'))
+ logger.warn('Browser does not support Filesystem API. Directories will not be uploaded')
+ showWarning(t('files', 'Your browser does not support the Filesystem API. Directories will not be uploaded'))
warned = true
}
continue
--- /dev/null
+import { describe, it, expect } from '@jest/globals'
+
+import { FileSystemDirectoryEntry, FileSystemFileEntry, fileSystemEntryToDataTransferItem, DataTransferItem as DataTransferItemMock } from '../../../../__tests__/FileSystemAPIUtils'
+import { join } from 'node:path'
+import { Directory, traverseTree } from './DropServiceUtils'
+import { dataTransferToFileTree } from './DropService'
+import logger from '../logger'
+
+const dataTree = {
+ 'file0.txt': ['Hello, world!', 1234567890],
+ dir1: {
+ 'file1.txt': ['Hello, world!', 4567891230],
+ 'file2.txt': ['Hello, world!', 7891234560],
+ },
+ dir2: {
+ 'file3.txt': ['Hello, world!', 1234567890],
+ },
+}
+
+// This is mocking a file tree using the FileSystem API
+const buildFileSystemDirectoryEntry = (path: string, tree: any): FileSystemDirectoryEntry => {
+ const entries = Object.entries(tree).map(([name, contents]) => {
+ const fullPath = join(path, name)
+ if (Array.isArray(contents)) {
+ return new FileSystemFileEntry(fullPath, contents[0], contents[1])
+ } else {
+ return buildFileSystemDirectoryEntry(fullPath, contents)
+ }
+ })
+ return new FileSystemDirectoryEntry(path, entries)
+}
+
+const buildDataTransferItemArray = (path: string, tree: any, isFileSystemAPIAvailable = true): DataTransferItemMock[] => {
+ return Object.entries(tree).map(([name, contents]) => {
+ const fullPath = join(path, name)
+ if (Array.isArray(contents)) {
+ const entry = new FileSystemFileEntry(fullPath, contents[0], contents[1])
+ return fileSystemEntryToDataTransferItem(entry, isFileSystemAPIAvailable)
+ }
+
+ const entry = buildFileSystemDirectoryEntry(fullPath, contents)
+ return fileSystemEntryToDataTransferItem(entry, isFileSystemAPIAvailable)
+ })
+}
+
+describe('Filesystem API traverseTree', () => {
+ it('Should traverse a file tree from root', async () => {
+ // Fake a FileSystemEntry tree
+ const root = buildFileSystemDirectoryEntry('root', dataTree)
+ const tree = await traverseTree(root as unknown as FileSystemEntry) as Directory
+
+ expect(tree.name).toBe('root')
+ expect(tree).toBeInstanceOf(Directory)
+ expect(tree.contents).toHaveLength(3)
+ expect(tree.size).toBe(13 * 4) // 13 bytes from 'Hello, world!'
+ })
+
+ it('Should traverse a file tree from a subdirectory', async () => {
+ // Fake a FileSystemEntry tree
+ const dir2 = buildFileSystemDirectoryEntry('dir2', dataTree.dir2)
+ const tree = await traverseTree(dir2 as unknown as FileSystemEntry) as Directory
+
+ expect(tree.name).toBe('dir2')
+ expect(tree).toBeInstanceOf(Directory)
+ expect(tree.contents).toHaveLength(1)
+ expect(tree.contents[0].name).toBe('file3.txt')
+ expect(tree.size).toBe(13) // 13 bytes from 'Hello, world!'
+ })
+
+ it('Should properly compute the last modified', async () => {
+ // Fake a FileSystemEntry tree
+ const root = buildFileSystemDirectoryEntry('root', dataTree)
+ const rootTree = await traverseTree(root as unknown as FileSystemEntry) as Directory
+
+ expect(rootTree.lastModified).toBe(7891234560)
+
+ // Fake a FileSystemEntry tree
+ const dir2 = buildFileSystemDirectoryEntry('root', dataTree.dir2)
+ const dir2Tree = await traverseTree(dir2 as unknown as FileSystemEntry) as Directory
+ expect(dir2Tree.lastModified).toBe(1234567890)
+ })
+})
+
+describe('DropService dataTransferToFileTree', () => {
+
+ beforeAll(() => {
+ // DataTransferItem doesn't exists in jsdom, let's mock
+ // a dumb one so we can check the instanceof
+ // @ts-expect-error jsdom doesn't have DataTransferItem
+ window.DataTransferItem = DataTransferItemMock
+ })
+
+ afterAll(() => {
+ // @ts-expect-error jsdom doesn't have DataTransferItem
+ delete window.DataTransferItem
+ })
+
+ it('Should return a RootDirectory with Filesystem API', async () => {
+ jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
+ jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn())
+
+ const dataTransferItems = buildDataTransferItemArray('root', dataTree)
+ const fileTree = await dataTransferToFileTree(dataTransferItems as unknown as DataTransferItem[])
+
+ expect(fileTree.name).toBe('root')
+ expect(fileTree).toBeInstanceOf(Directory)
+ expect(fileTree.contents).toHaveLength(3)
+
+ // The file tree should be recursive when using the Filesystem API
+ expect(fileTree.contents[1]).toBeInstanceOf(Directory)
+ expect((fileTree.contents[1] as Directory).contents).toHaveLength(2)
+ expect(fileTree.contents[2]).toBeInstanceOf(Directory)
+ expect((fileTree.contents[2] as Directory).contents).toHaveLength(1)
+
+ expect(logger.error).not.toBeCalled()
+ expect(logger.warn).not.toBeCalled()
+ })
+
+ it('Should return a RootDirectory with legacy File API ignoring recursive directories', async () => {
+ jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
+ jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn())
+
+ const dataTransferItems = buildDataTransferItemArray('root', dataTree, false)
+
+ const fileTree = await dataTransferToFileTree(dataTransferItems as unknown as DataTransferItem[])
+
+ expect(fileTree.name).toBe('root')
+ expect(fileTree).toBeInstanceOf(Directory)
+ expect(fileTree.contents).toHaveLength(1)
+
+ // The file tree should be recursive when using the Filesystem API
+ expect(fileTree.contents[0]).not.toBeInstanceOf(Directory)
+ expect((fileTree.contents[0].name)).toBe('file0.txt')
+
+ expect(logger.error).not.toBeCalled()
+ expect(logger.warn).toHaveBeenNthCalledWith(1, 'Could not get FilesystemEntry of item, falling back to file')
+ expect(logger.warn).toHaveBeenNthCalledWith(2, 'Could not get FilesystemEntry of item, falling back to file')
+ expect(logger.warn).toHaveBeenNthCalledWith(3, 'Browser does not support Filesystem API. Directories will not be uploaded')
+ expect(logger.warn).toHaveBeenNthCalledWith(4, 'Could not get FilesystemEntry of item, falling back to file')
+ expect(logger.warn).toHaveBeenCalledTimes(4)
+ })
+})
--- /dev/null
+/**
+ * @copyright Copyright (c) 2024 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+import type { FileStat, ResponseDataDetailed } from 'webdav'
+
+import { emit } from '@nextcloud/event-bus'
+import { Folder, Node, davGetClient, davGetDefaultPropfind, davResultToNode } from '@nextcloud/files'
+import { openConflictPicker } from '@nextcloud/upload'
+import { showError, showInfo } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+
+import logger from '../logger.js'
+
+/**
+ * This represents a Directory in the file tree
+ * We extend the File class to better handling uploading
+ * and stay as close as possible as the Filesystem API.
+ * This also allow us to hijack the size or lastModified
+ * properties to compute them dynamically.
+ */
+export class Directory extends File {
+
+ /* eslint-disable no-use-before-define */
+ _contents: (Directory|File)[]
+
+ constructor(name, contents: (Directory|File)[] = []) {
+ super([], name, { type: 'httpd/unix-directory' })
+ this._contents = contents
+ }
+
+ set contents(contents: (Directory|File)[]) {
+ this._contents = contents
+ }
+
+ get contents(): (Directory|File)[] {
+ return this._contents
+ }
+
+ get size() {
+ return this._computeDirectorySize(this)
+ }
+
+ get lastModified() {
+ if (this._contents.length === 0) {
+ return Date.now()
+ }
+ return this._computeDirectoryMtime(this)
+ }
+
+ /**
+ * Get the last modification time of a file tree
+ * This is not perfect, but will get us a pretty good approximation
+ * @param directory the directory to traverse
+ */
+ _computeDirectoryMtime(directory: Directory): number {
+ return directory.contents.reduce((acc, file) => {
+ return file.lastModified > acc
+ // If the file is a directory, the lastModified will
+ // also return the results of its _computeDirectoryMtime method
+ // Fancy recursion, huh?
+ ? file.lastModified
+ : acc
+ }, 0)
+ }
+
+ /**
+ * Get the size of a file tree
+ * @param directory the directory to traverse
+ */
+ _computeDirectorySize(directory: Directory): number {
+ return directory.contents.reduce((acc: number, entry: Directory|File) => {
+ // If the file is a directory, the size will
+ // also return the results of its _computeDirectorySize method
+ // Fancy recursion, huh?
+ return acc + entry.size
+ }, 0)
+ }
+
+}
+
+export type RootDirectory = Directory & {
+ name: 'root'
+}
+
+/**
+ * Traverse a file tree using the Filesystem API
+ * @param entry the entry to traverse
+ */
+export const traverseTree = async (entry: FileSystemEntry): Promise<Directory|File> => {
+ // Handle file
+ if (entry.isFile) {
+ return new Promise<File>((resolve, reject) => {
+ (entry as FileSystemFileEntry).file(resolve, reject)
+ })
+ }
+
+ // Handle directory
+ logger.debug('Handling recursive file tree', { entry: entry.name })
+ const directory = entry as FileSystemDirectoryEntry
+ const entries = await readDirectory(directory)
+ const contents = (await Promise.all(entries.map(traverseTree))).flat()
+ return new Directory(directory.name, contents)
+}
+
+/**
+ * Read a directory using Filesystem API
+ * @param directory the directory to read
+ */
+const readDirectory = (directory: FileSystemDirectoryEntry): Promise<FileSystemEntry[]> => {
+ const dirReader = directory.createReader()
+
+ return new Promise<FileSystemEntry[]>((resolve, reject) => {
+ const entries = [] as FileSystemEntry[]
+ const getEntries = () => {
+ dirReader.readEntries((results) => {
+ if (results.length) {
+ entries.push(...results)
+ getEntries()
+ } else {
+ resolve(entries)
+ }
+ }, (error) => {
+ reject(error)
+ })
+ }
+
+ getEntries()
+ })
+}
+
+export const createDirectoryIfNotExists = async (absolutePath: string) => {
+ const davClient = davGetClient()
+ 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))
+ }
+}
+
+export const resolveConflict = async <T extends ((Directory|File)|Node)>(files: Array<T>, destination: Folder, contents: Node[]): Promise<T[]> => {
+ try {
+ // List all conflicting files
+ const conflicts = files.filter((file: File|Node) => {
+ return contents.find((node: Node) => node.basename === (file instanceof File ? file.name : file.basename))
+ }).filter(Boolean) as (File|Node)[]
+
+ // List of incoming files that are NOT in conflict
+ const uploads = files.filter((file: File|Node) => {
+ return !conflicts.includes(file)
+ })
+
+ // Let the user choose what to do with the conflicting files
+ const { selected, renamed } = await openConflictPicker(destination.path, conflicts, contents)
+
+ logger.debug('Conflict resolution', { uploads, selected, renamed })
+
+ // If the user selected nothing, we cancel the upload
+ if (selected.length === 0 && renamed.length === 0) {
+ // User skipped
+ showInfo(t('files', 'Conflicts resolution skipped'))
+ logger.info('User skipped the conflict resolution')
+ return []
+ }
+
+ // Update the list of files to upload
+ return [...uploads, ...selected, ...renamed] as (typeof files)
+ } catch (error) {
+ console.error(error)
+ // User cancelled
+ showError(t('files', 'Upload cancelled'))
+ logger.error('User cancelled the upload')
+ }
+
+ return []
+}
+++ /dev/null
-import { basename } from 'node:path'
-
-class FileSystemEntry {
-
- private _isFile: boolean
- private _fullPath: string
-
- constructor(isFile: boolean, fullPath: string) {
- this._isFile = isFile
- this._fullPath = fullPath
- }
-
- get isFile() {
- return !!this._isFile
- }
-
- get isDirectory() {
- return !this.isFile
- }
-
- get name() {
- return basename(this._fullPath)
- }
-
-}
-
-export class FileSystemFileEntry extends FileSystemEntry {
-
- private _contents: string
-
- constructor(fullPath: string, contents: string) {
- super(true, fullPath)
- this._contents = contents
- }
-
- file(success: (file: File) => void) {
- success(new File([this._contents], this.name))
- }
-
-}
-
-export class FileSystemDirectoryEntry extends FileSystemEntry {
-
- private _entries: FileSystemEntry[]
-
- constructor(fullPath: string, entries: FileSystemEntry[]) {
- super(false, fullPath)
- this._entries = entries || []
- }
-
- createReader() {
- return {
- readEntries: (success: (entries: FileSystemEntry[]) => void) => {
- success(this._entries)
- },
- }
- }
-
-}
import { getRowForFile } from './FilesUtils.ts'
-describe('files: Drag and Drop legacy', { testIsolation: true }, () => {
+describe('files: Drag and Drop', { testIsolation: true }, () => {
beforeEach(() => {
cy.createRandomUser().then((user) => {
cy.login(user)
'@buttercup/fetch',
'@juliushaertl',
'@mdi/svg',
+ '@nextcloud/upload',
'@nextcloud/vue',
'ansi-regex',
'camelcase',
'char-regex',
'hot-patcher',
'is-svg',
+ 'mime',
+ 'p-cancelable',
+ 'p-limit',
+ 'p-queue',
+ 'p-timeout',
'splitpanes',
'string-length',
'strip-ansi',
'tributejs',
'vue-material-design-icons',
'webdav',
+ 'yocto-queue',
]
const config: Config = {
"@nextcloud/paths": "^2.1.0",
"@nextcloud/router": "^3.0.0",
"@nextcloud/sharing": "^0.1.0",
- "@nextcloud/upload": "^1.0.5",
+ "@nextcloud/upload": "^1.1.0",
"@nextcloud/vue": "^8.11.1",
"@skjnldsv/sanitize-svg": "^1.0.2",
"@vueuse/components": "^10.7.2",
"karma-jasmine-sinon": "^1.0.4",
"karma-spec-reporter": "^0.0.36",
"karma-viewport": "^1.0.9",
+ "mime": "^4.0.1",
"puppeteer": "^22.5.0",
"raw-loader": "^4.0.2",
"regextras": "^0.8.0",