diff --git a/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte b/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte
index 3e7b95f000..b66a691fed 100644
--- a/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte
+++ b/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte
@@ -142,7 +142,7 @@
list,
{ objectId: object._id, objectClass: object._class },
{},
- async (uuid, name, file, metadata) => {
+ async (uuid, name, file, path, metadata) => {
await createAttachment(uuid, name, file, metadata)
}
)
@@ -158,7 +158,7 @@
files,
{ objectId: object._id, objectClass: object._class },
{},
- async (uuid, name, file, metadata) => {
+ async (uuid, name, file, path, metadata) => {
await createAttachment(uuid, name, file, metadata)
}
)
diff --git a/plugins/drive-resources/src/components/CreateFolder.svelte b/plugins/drive-resources/src/components/CreateFolder.svelte
index 7ca2bbceb9..afd672707d 100644
--- a/plugins/drive-resources/src/components/CreateFolder.svelte
+++ b/plugins/drive-resources/src/components/CreateFolder.svelte
@@ -15,13 +15,14 @@
//
-->
diff --git a/plugins/drive-resources/src/components/FilePanel.svelte b/plugins/drive-resources/src/components/FilePanel.svelte
index b34d7a3478..7ae9e4cf53 100644
--- a/plugins/drive-resources/src/components/FilePanel.svelte
+++ b/plugins/drive-resources/src/components/FilePanel.svelte
@@ -74,7 +74,7 @@
maxNumberOfFiles: 1,
hideProgress: true
},
- async (uuid, name, file, metadata) => {
+ async (uuid, name, file, path, metadata) => {
const data = {
file: uuid,
name,
diff --git a/plugins/drive-resources/src/index.ts b/plugins/drive-resources/src/index.ts
index d71d619a8c..7ba89894da 100644
--- a/plugins/drive-resources/src/index.ts
+++ b/plugins/drive-resources/src/index.ts
@@ -38,14 +38,14 @@ import MoveResource from './components/MoveResource.svelte'
import ResourcePresenter from './components/ResourcePresenter.svelte'
import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation'
-import { createFolder, renameResource, restoreFileVersion } from './utils'
+import { showCreateFolderPopup, showRenameResourcePopup, restoreFileVersion } from './utils'
async function CreateRootFolder (doc: Drive): Promise {
- await createFolder(doc._id, drive.ids.Root)
+ await showCreateFolderPopup(doc._id, drive.ids.Root)
}
async function CreateChildFolder (doc: Folder): Promise {
- await createFolder(doc.space, doc._id)
+ await showCreateFolderPopup(doc.space, doc._id)
}
async function EditDrive (drive: Drive): Promise {
@@ -82,13 +82,13 @@ async function FileLinkProvider (doc: Doc): Promise {
async function RenameFile (doc: File | File[]): Promise {
if (!Array.isArray(doc)) {
- await renameResource(doc)
+ await showRenameResourcePopup(doc)
}
}
async function RenameFolder (doc: Folder | Folder[]): Promise {
if (!Array.isArray(doc)) {
- await renameResource(doc)
+ await showRenameResourcePopup(doc)
}
}
diff --git a/plugins/drive-resources/src/utils.ts b/plugins/drive-resources/src/utils.ts
index f5ded4b7dc..51121f5da0 100644
--- a/plugins/drive-resources/src/utils.ts
+++ b/plugins/drive-resources/src/utils.ts
@@ -14,11 +14,12 @@
//
import { type Class, type Doc, type Ref, toIdMap } from '@hcengineering/core'
-import type { Drive, FileVersion, Folder, Resource } from '@hcengineering/drive'
-import drive from '@hcengineering/drive'
-import { type Asset } from '@hcengineering/platform'
+import { type Drive, type FileVersion, type Folder, type Resource, createFolder } from '@hcengineering/drive'
+import drive, { createFile } from '@hcengineering/drive'
+import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { type AnySvelteComponent, showPopup } from '@hcengineering/ui'
+import { uploadFiles } from '@hcengineering/uploader'
import { openDoc } from '@hcengineering/view-resources'
import CreateDrive from './components/CreateDrive.svelte'
@@ -43,7 +44,11 @@ export function formatFileVersion (version: number): string {
return `v${version}`
}
-export async function createFolder (space: Ref | undefined, parent: Ref, open = false): Promise {
+export async function showCreateFolderPopup (
+ space: Ref | undefined,
+ parent: Ref,
+ open = false
+): Promise {
showPopup(CreateFolder, { space, parent }, 'top', async (id) => {
if (open && id !== undefined && id !== null) {
await navigateToDoc(id, drive.class.Folder)
@@ -51,7 +56,7 @@ export async function createFolder (space: Ref | undefined, parent: Ref {
+export async function showCreateDrivePopup (open = false): Promise {
showPopup(CreateDrive, {}, 'top', async (id) => {
if (open && id !== undefined && id !== null) {
await navigateToDoc(id, drive.class.Folder)
@@ -59,11 +64,11 @@ export async function createDrive (open = false): Promise {
})
}
-export async function editDrive (drive: Drive): Promise {
+export async function showEditDrivePopup (drive: Drive): Promise {
showPopup(CreateDrive, { drive })
}
-export async function renameResource (resource: Resource): Promise {
+export async function showRenameResourcePopup (resource: Resource): Promise {
showPopup(RenamePopup, { value: resource.name, format: 'text' }, undefined, async (res) => {
if (res != null && res !== resource.name) {
const client = getClient()
@@ -148,3 +153,63 @@ export async function resolveParents (object: Resource): Promise {
return parents.reverse()
}
+
+export async function uploadFilesToDrive (files: DataTransfer, space: Ref, parent: Ref): Promise {
+ const client = getClient()
+
+ const query = parent !== drive.ids.Root ? { space, path: parent } : { space }
+ const folders = await client.findAll(drive.class.Folder, query)
+ const foldersByName = new Map(folders.map((folder) => [folder.name, folder]))
+
+ const findParent = async (path: string | undefined): Promise[> => {
+ if (path == null || path.length === 0) {
+ return parent
+ }
+
+ const segments = path.split('/').filter((p) => p.length > 0)
+ if (segments.length <= 1) {
+ return parent
+ }
+
+ let current = parent
+ while (segments.length > 1) {
+ const name = segments.shift()
+ if (name !== undefined) {
+ let folder = foldersByName.get(name)
+ if (folder !== undefined) {
+ current = folder._id
+ } else {
+ current = await createFolder(client, space, { name, parent: current })
+ folder = await client.findOne(drive.class.Folder, { _id: current })
+ if (folder !== undefined) {
+ foldersByName.set(folder.name, folder)
+ }
+ }
+ }
+ }
+ return current
+ }
+
+ const target =
+ parent !== drive.ids.Root
+ ? { objectId: parent, objectClass: drive.class.Folder }
+ : { objectId: space, objectClass: drive.class.Drive }
+
+ await uploadFiles(files, target, {}, async (uuid, name, file, path, metadata) => {
+ const folder = await findParent(path)
+ try {
+ const data = {
+ file: uuid,
+ size: file.size,
+ type: file.type,
+ lastModified: file instanceof File ? file.lastModified : Date.now(),
+ name,
+ metadata
+ }
+
+ await createFile(client, space, folder, data)
+ } catch (err) {
+ void setPlatformStatus(unknownError(err))
+ }
+ })
+}
diff --git a/plugins/drive/src/utils.ts b/plugins/drive/src/utils.ts
index 170107c69c..7c7c698fd5 100644
--- a/plugins/drive/src/utils.ts
+++ b/plugins/drive/src/utils.ts
@@ -13,11 +13,30 @@
// limitations under the License.
//
-import { type AttachedData, type Ref, type TxOperations, generateId } from '@hcengineering/core'
+import { type AttachedData, type Data, type Ref, type TxOperations, generateId } from '@hcengineering/core'
import drive from './plugin'
import type { Drive, File, FileVersion, Folder } from './types'
+/** @public */
+export async function createFolder (
+ client: TxOperations,
+ space: Ref,
+ data: Omit, 'path'>
+): Promise][> {
+ let path: Array][> = []
+
+ if (data.parent !== drive.ids.Root) {
+ const parent = await client.findOne(drive.class.Folder, { _id: data.parent })
+ if (parent === undefined) {
+ throw new Error('parent not found')
+ }
+ path = [parent._id, ...parent.path]
+ }
+
+ return await client.createDoc(drive.class.Folder, space, { ...data, path })
+}
+
/** @public */
export async function createFile (
client: TxOperations,
diff --git a/plugins/uploader-resources/src/uppy.ts b/plugins/uploader-resources/src/uppy.ts
index d91f7500c4..89ba19475e 100644
--- a/plugins/uploader-resources/src/uppy.ts
+++ b/plugins/uploader-resources/src/uppy.ts
@@ -49,7 +49,9 @@ type Meta = IndexedObject
type Body = IndexedObject
/** @public */
-export type UppyMeta = Meta
+export type UppyMeta = Meta & {
+ relativePath?: string
+}
/** @public */
export type UppyBody = Body & {
@@ -89,7 +91,7 @@ export function getUppy (options: FileUploadOptions, onFileUploaded?: FileUpload
const uuid = file?.response?.body?.uuid as Ref
if (uuid !== undefined) {
const metadata = await getFileMetadata(file.data, uuid)
- await onFileUploaded(uuid, file.name, file.data, metadata)
+ await onFileUploaded(uuid, file.name, file.data, file.meta.relativePath, metadata)
}
}
})
diff --git a/plugins/uploader-resources/src/utils.ts b/plugins/uploader-resources/src/utils.ts
index 9d077fbf24..6b36e0f8da 100644
--- a/plugins/uploader-resources/src/utils.ts
+++ b/plugins/uploader-resources/src/utils.ts
@@ -14,7 +14,13 @@
//
import { showPopup } from '@hcengineering/ui'
-import type { FileUploadCallback, FileUploadOptions, FileUploadTarget } from '@hcengineering/uploader'
+import {
+ type FileUploadCallback,
+ type FileUploadOptions,
+ type FileUploadTarget,
+ getDataTransferFiles,
+ toFileWithPath
+} from '@hcengineering/uploader'
import FileUploadPopup from './components/FileUploadPopup.svelte'
@@ -38,19 +44,21 @@ export async function showFilesUploadPopup (
/** @public */
export async function uploadFiles (
- files: File[] | FileList,
+ files: File[] | FileList | DataTransfer,
target: FileUploadTarget,
options: FileUploadOptions,
onFileUploaded: FileUploadCallback
): Promise {
- if (files.length === 0) return
+ const items =
+ files instanceof DataTransfer ? await getDataTransferFiles(files) : Array.from(files, (p) => toFileWithPath(p))
+
+ if (items.length === 0) return
const uppy = getUppy(options, onFileUploaded)
- for (let index = 0; index < files.length; index++) {
- const data = files[index]
- const { name, type } = data
- uppy.addFile({ name, type, data })
+ for (const data of items) {
+ const { name, type, relativePath } = data
+ uppy.addFile({ name, type, data, meta: { relativePath } })
}
if (options.hideProgress !== true) {
diff --git a/plugins/uploader/src/types.ts b/plugins/uploader/src/types.ts
index 42f8f3e5f5..5bc29fae22 100644
--- a/plugins/uploader/src/types.ts
+++ b/plugins/uploader/src/types.ts
@@ -15,6 +15,11 @@
import type { Blob as PlatformBlob, Class, Doc, Ref } from '@hcengineering/core'
+/** @public */
+export interface FileWithPath extends File {
+ relativePath?: string
+}
+
/** @public */
export type UploadFilesPopupFn = (
target: FileUploadTarget,
@@ -24,7 +29,7 @@ export type UploadFilesPopupFn = (
/** @public */
export type UploadFilesFn = (
- files: File[] | FileList,
+ files: File[] | FileList | DataTransfer,
target: FileUploadTarget,
options: FileUploadOptions,
onFileUploaded: FileUploadCallback
@@ -48,6 +53,7 @@ export interface FileUploadOptions {
export type FileUploadCallback = (
uuid: Ref,
name: string,
- file: File | Blob,
+ file: FileWithPath | Blob,
+ path: string | undefined,
metadata: Record | undefined
) => Promise
diff --git a/plugins/uploader/src/utils.ts b/plugins/uploader/src/utils.ts
index eacd189f66..70af2a1189 100644
--- a/plugins/uploader/src/utils.ts
+++ b/plugins/uploader/src/utils.ts
@@ -16,7 +16,7 @@
import { getResource } from '@hcengineering/platform'
import uploader from './plugin'
-import type { FileUploadCallback, FileUploadOptions, FileUploadTarget } from './types'
+import type { FileUploadCallback, FileUploadOptions, FileUploadTarget, FileWithPath } from './types'
/** @public */
export async function showFilesUploadPopup (
@@ -30,7 +30,7 @@ export async function showFilesUploadPopup (
/** @public */
export async function uploadFiles (
- files: File[] | FileList,
+ files: File[] | FileList | DataTransfer,
target: FileUploadTarget,
options: FileUploadOptions,
onFileUploaded: FileUploadCallback
@@ -38,3 +38,88 @@ export async function uploadFiles (
const fn = await getResource(uploader.function.UploadFiles)
await fn(files, target, options, onFileUploaded)
}
+
+/** @public */
+export async function getDataTransferFiles (dataTransfer: DataTransfer): Promise {
+ try {
+ const accumulator = []
+ const entries = Array.from(dataTransfer.items, getAsEntry)
+ for (const entry of entries) {
+ if (entry != null) {
+ const files = await fromEntry(entry)
+ if (Array.isArray(files)) {
+ accumulator.push(...files)
+ } else {
+ accumulator.push(files)
+ }
+ }
+ }
+ return accumulator
+ } catch {
+ return Array.from(dataTransfer.files, (file) => toFileWithPath(file))
+ }
+}
+
+/** @public */
+export function toFileWithPath (file: File, path?: string): FileWithPath {
+ const { webkitRelativePath } = file
+ Object.defineProperty(file, 'relativePath', {
+ value:
+ typeof path === 'string'
+ ? path
+ : typeof webkitRelativePath === 'string' && webkitRelativePath.length > 0
+ ? webkitRelativePath
+ : file.name,
+ writable: false,
+ configurable: false,
+ enumerable: true
+ })
+ return file
+}
+
+function getAsEntry (item: DataTransferItem): FileSystemEntry | null | undefined {
+ // https://developer.mozilla.org/docs/Web/API/DataTransferItem/webkitGetAsEntry
+ return (item as any).getAsEntry === 'function' ? (item as any).getAsEntry() : item.webkitGetAsEntry()
+}
+
+async function fromEntry (entry: FileSystemEntry): Promise {
+ return entry.isDirectory
+ ? await fromDirEntry(entry as FileSystemDirectoryEntry)
+ : await fromFileEntry(entry as FileSystemFileEntry)
+}
+
+async function fromFileEntry (entry: FileSystemFileEntry): Promise {
+ return await new Promise((resolve, reject) => {
+ entry.file((file) => {
+ resolve(toFileWithPath(file, entry.fullPath))
+ }, reject)
+ })
+}
+
+async function fromDirEntry (entry: FileSystemDirectoryEntry): Promise {
+ const reader = entry.createReader()
+
+ return await new Promise((resolve, reject) => {
+ const promises: Promise[] = []
+
+ function readEntries (): void {
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ reader.readEntries(async (entries: FileSystemEntry[]) => {
+ if (entries.length === 0) {
+ try {
+ const files = await Promise.all(promises)
+ resolve(files.flat())
+ } catch (err) {
+ reject(err)
+ }
+ } else {
+ // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries
+ promises.push(...entries.map(fromEntry))
+ readEntries()
+ }
+ }, reject)
+ }
+
+ readEntries()
+ })
+}
]