UBERF-7605 Use uppy for file upload in more places (#6091)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-07-19 13:27:05 +07:00 committed by GitHub
parent 702f30ee2a
commit c34b3a7ad2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 314 additions and 292 deletions

View File

@ -47,6 +47,7 @@
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/text": "^0.6.5",
"@hcengineering/uploader": "^0.6.0",
"svelte": "^4.2.12",
"@hcengineering/client": "^0.6.18",
"@hcengineering/collaborator-client": "^0.6.4",

View File

@ -15,16 +15,16 @@
import extract from 'png-chunks-extract'
export async function getImageSize (
file: File,
src: string
): Promise<{ width: number, height: number, pixelRatio: number }> {
export async function getImageSize (file: Blob): Promise<{ width: number, height: number, pixelRatio: number }> {
const size = isPng(file) ? await getPngImageSize(file) : undefined
const promise = new Promise<{ width: number, height: number, pixelRatio: number }>((resolve, reject) => {
const img = new Image()
const src = URL.createObjectURL(file)
img.onload = () => {
URL.revokeObjectURL(src)
resolve({
width: size?.width ?? img.naturalWidth,
height: size?.height ?? img.naturalHeight,
@ -39,11 +39,11 @@ export async function getImageSize (
return await promise
}
function isPng (file: File): boolean {
function isPng (file: Blob): boolean {
return file.type === 'image/png'
}
async function getPngImageSize (file: File): Promise<{ width: number, height: number, pixelRatio: number } | undefined> {
async function getPngImageSize (file: Blob): Promise<{ width: number, height: number, pixelRatio: number } | undefined> {
if (!isPng(file)) {
return undefined
}

View File

@ -51,6 +51,7 @@
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/text-editor-resources": "^0.6.0",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/uploader": "^0.6.0",
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"filesize": "^8.0.3",

View File

@ -13,11 +13,18 @@
// limitations under the License.
-->
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import attachment, { Attachment, BlobMetadata } from '@hcengineering/attachment'
import contact from '@hcengineering/contact'
import core, { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core'
import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { KeyedAttribute, createQuery, getClient, uploadFile } from '@hcengineering/presentation'
import {
FileOrBlob,
KeyedAttribute,
createQuery,
getClient,
getFileMetadata,
uploadFile
} from '@hcengineering/presentation'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
import textEditor, { type RefAction, type TextEditorHandler } from '@hcengineering/text-editor'
import {
@ -29,6 +36,7 @@
getModelRefActions
} from '@hcengineering/text-editor-resources'
import { AnySvelteComponent, getEventPositionElement, getPopupPositionElement, navigate } from '@hcengineering/ui'
import { uploadFiles } from '@hcengineering/uploader'
import view from '@hcengineering/view'
import AttachmentsGrid from './AttachmentsGrid.svelte'
@ -124,23 +132,60 @@
async function fileSelected (): Promise<void> {
if (readonly) return
progress = true
const list = inputFile.files
if (list === null || list.length === 0) return
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
await createAttachment(file)
progress = true
await uploadFiles(
list,
{ objectId: object._id, objectClass: object._class },
{},
async (uuid, name, file, metadata) => {
await createAttachment(uuid, name, file, metadata)
}
}
)
inputFile.value = ''
progress = false
}
async function createAttachment (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
async function attachFiles (files: File[] | FileList): Promise<void> {
progress = true
if (files.length > 0) {
await uploadFiles(
files,
{ objectId: object._id, objectClass: object._class },
{},
async (uuid, name, file, metadata) => {
await createAttachment(uuid, name, file, metadata)
}
)
}
progress = false
}
async function attachFile (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
try {
const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid)
await createAttachment(uuid, file.name, file, metadata)
return { file: uuid, type: file.type }
} catch (err: any) {
await setPlatformStatus(unknownError(err))
}
}
async function createAttachment (
uuid: Ref<Blob>,
name: string,
file: FileOrBlob,
metadata: BlobMetadata | undefined
): Promise<void> {
try {
const _id: Ref<Attachment> = generateId()
const attachmentDoc: Attachment = {
_id,
_class: attachment.class.Attachment,
@ -150,11 +195,12 @@
space: object.space,
attachedTo: object._id,
attachedToClass: object._class,
name: file.name,
name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
metadata,
lastModified: file instanceof File ? file.lastModified : Date.now()
}
await client.addCollection(
@ -166,7 +212,6 @@
attachmentDoc,
attachmentDoc._id
)
return { file: uuid, type: file.type }
} catch (err: any) {
await setPlatformStatus(unknownError(err))
}
@ -195,31 +240,34 @@
return
}
progress = true
const items = evt.clipboardData?.items ?? []
const files = []
for (const index in items) {
const item = items[index]
if (item.kind === 'file') {
const blob = item.getAsFile()
if (blob !== null) {
await createAttachment(blob)
files.push(blob)
}
}
}
if (files.length > 0) {
await attachFiles(files)
}
progress = false
}
export async function fileDrop (e: DragEvent): Promise<void> {
if (readonly) return
progress = true
const list = e.dataTransfer?.files
if (list !== undefined && list.length !== 0) {
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
await createAttachment(file)
}
}
await attachFiles(list)
}
progress = false
}
async function removeAttachment (attachment: Attachment): Promise<void> {
@ -274,9 +322,7 @@
{boundary}
{refActions}
{readonly}
attachFile={async (file) => {
return await createAttachment(file)
}}
{attachFile}
on:open-document={async (event) => {
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
if (doc != null) {

View File

@ -409,7 +409,7 @@
return await createAttachment(file)
}}
/>
{#if (attachments.size > 0 && enableAttachments) || progress}
{#if attachments.size > 0 && enableAttachments}
<AttachmentsGrid
attachments={Array.from(attachments.values())}
{progress}

View File

@ -15,15 +15,18 @@
-->
<script lang="ts">
import { Attachment } from '@hcengineering/attachment'
import { Class, Data, Doc, DocumentQuery, Ref, Space } from '@hcengineering/core'
import { Blob, Class, Data, Doc, DocumentQuery, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Icon, Label, resizeObserver, Scroller, Spinner, Button, IconAdd } from '@hcengineering/ui'
import view, { BuildModelKey } from '@hcengineering/view'
import { Table } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import attachment from '../plugin'
import { createAttachments } from '../utils'
import { uploadFiles } from '@hcengineering/uploader'
import { createEventDispatcher } from 'svelte'
import attachment from '../plugin'
import { createAttachment } from '../utils'
import AttachmentDroppable from './AttachmentDroppable.svelte'
import IconAttachments from './icons/Attachments.svelte'
import UploadDuo from './icons/UploadDuo.svelte'
@ -49,19 +52,23 @@
const client = getClient()
const dispatch = createEventDispatcher()
async function fileSelected () {
async function fileSelected (): Promise<void> {
const list = inputFile.files
if (list === null || list.length === 0) return
loading++
try {
await createAttachments(
client,
list,
{ objectClass: object?._class ?? _class, objectId, space },
attachmentClass,
attachmentClassOptions
)
await uploadFiles(list, { objectId, objectClass: object?._class ?? _class }, {}, async (uuid, name, file) => {
await createAttachment(
client,
uuid,
name,
file,
{ objectClass: object?._class ?? _class, objectId, space },
attachmentClass,
attachmentClassOptions
)
})
} finally {
loading--
}
@ -71,11 +78,11 @@
dispatch('attached')
}
function openFile () {
function openFile (): void {
inputFile.click()
}
function updateContent (evt: CustomEvent) {
function updateContent (evt: CustomEvent): void {
attachments = evt.detail.length
dispatch('attachments', evt.detail)
}

View File

@ -15,9 +15,17 @@
//
import { type Attachment } from '@hcengineering/attachment'
import { type Class, type TxOperations as Client, type Data, type Doc, type Ref, type Space } from '@hcengineering/core'
import {
type Blob,
type Class,
type TxOperations as Client,
type Data,
type Doc,
type Ref,
type Space
} from '@hcengineering/core'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getFileMetadata, uploadFile } from '@hcengineering/presentation'
import { type FileOrBlob, getFileMetadata, uploadFile } from '@hcengineering/presentation'
import attachment from './plugin'
@ -28,23 +36,12 @@ export async function createAttachments (
attachmentClass: Ref<Class<Attachment>> = attachment.class.Attachment,
extraData: Partial<Data<Attachment>> = {}
): Promise<void> {
const { objectClass, objectId, space } = attachTo
try {
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid)
await client.addCollection(attachmentClass, space, objectId, objectClass, 'attachments', {
...extraData,
name: file.name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified,
metadata
})
await createAttachment(client, uuid, file.name, file, attachTo, attachmentClass, extraData)
}
}
} catch (err: any) {
@ -52,6 +49,33 @@ export async function createAttachments (
}
}
export async function createAttachment (
client: Client,
uuid: Ref<Blob>,
name: string,
file: FileOrBlob,
attachTo: { objectClass: Ref<Class<Doc>>, space: Ref<Space>, objectId: Ref<Doc> },
attachmentClass: Ref<Class<Attachment>> = attachment.class.Attachment,
extraData: Partial<Data<Attachment>> = {}
): Promise<void> {
const { objectClass, objectId, space } = attachTo
try {
const metadata = await getFileMetadata(file, uuid)
await client.addCollection(attachmentClass, space, objectId, objectClass, 'attachments', {
...extraData,
name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file instanceof File ? file.lastModified : Date.now(),
metadata
})
} catch (err: any) {
await setPlatformStatus(unknownError(err))
}
}
export function getType (type: string): 'image' | 'text' | 'json' | 'video' | 'audio' | 'pdf' | 'other' {
if (type.startsWith('image/')) {
return 'image'

View File

@ -13,10 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import { AccountRole, Blob, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { createFile, type Drive } from '@hcengineering/drive'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { createQuery, FileOrBlob, getClient, getFileMetadata } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, ButtonWithDropdown, IconAdd, IconDropdown, Loading, SelectPopupValueType } from '@hcengineering/ui'
import { showFilesUploadPopup } from '@hcengineering/uploader'
@ -71,11 +71,10 @@
parent !== drive.ids.Root
? { objectId: parent, objectClass: drive.class.Folder }
: { objectId: space, objectClass: drive.class.Drive }
await showFilesUploadPopup(target, {}, async (uuid: string, name: string, file: FileOrBlob) => {
await showFilesUploadPopup(target, {}, async (uuid, name, file, metadata) => {
try {
const metadata = await getFileMetadata(file, uuid as Ref<Blob>)
const data = {
file: uuid as Ref<Blob>,
file: uuid,
size: file.size,
type: file.type,
lastModified: file instanceof File ? file.lastModified : Date.now(),

View File

@ -13,10 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { type Ref } from '@hcengineering/core'
import drive, { createFile, type Drive, type Folder } from '@hcengineering/drive'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { FileOrBlob, getClient, getFileMetadata } from '@hcengineering/presentation'
import { getClient } from '@hcengineering/presentation'
import { uploadFiles } from '@hcengineering/uploader'
export let space: Ref<Drive>
@ -72,11 +72,10 @@
parent !== drive.ids.Root
? { objectId: parent, objectClass: drive.class.Folder }
: { objectId: space, objectClass: drive.class.Drive }
await uploadFiles(list, target, {}, async (uuid: string, name: string, file: FileOrBlob) => {
await uploadFiles(list, target, {}, async (uuid, name, file, metadata) => {
try {
const metadata = await getFileMetadata(file, uuid as Ref<Blob>)
const data = {
file: uuid as Ref<Blob>,
file: uuid,
size: file.size,
type: file.type,
lastModified: file instanceof File ? file.lastModified : Date.now(),

View File

@ -1,46 +0,0 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let multiple: boolean = false
export function upload (): void {
inputFile.click()
}
const dispatch = createEventDispatcher()
let inputFile: HTMLInputElement
async function fileSelected (): Promise<void> {
const files = inputFile.files
if (files === null || files.length === 0) {
return
}
dispatch('selected', files)
}
</script>
<input
bind:this={inputFile}
{multiple}
id="file"
name="file"
type="file"
style="display: none"
disabled={inputFile == null}
on:change={fileSelected}
/>

View File

@ -13,11 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { WithLookup, type Ref } from '@hcengineering/core'
import { type File, type FileVersion } from '@hcengineering/drive'
import { type Ref, type WithLookup } from '@hcengineering/core'
import { createFileVersion, type File as DriveFile, type FileVersion } from '@hcengineering/drive'
import { Panel } from '@hcengineering/panel'
import { createQuery, getBlobHref } from '@hcengineering/presentation'
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
import { Button, IconMoreH } from '@hcengineering/ui'
import { showFilesUploadPopup } from '@hcengineering/uploader'
import view from '@hcengineering/view'
import { showMenu } from '@hcengineering/view-resources'
@ -28,9 +29,8 @@
import IconUpload from './icons/FileUpload.svelte'
import drive from '../plugin'
import { replaceOneFile } from '../utils'
export let _id: Ref<File>
export let _id: Ref<DriveFile>
export let readonly: boolean = false
export let embedded: boolean = false
export let kind: 'default' | 'modern' = 'default'
@ -39,12 +39,13 @@
return false
}
let object: WithLookup<File> | undefined = undefined
let object: WithLookup<DriveFile> | undefined = undefined
let version: FileVersion | undefined = undefined
let download: HTMLAnchorElement
let upload: HTMLInputElement
const client = getClient()
const query = createQuery()
$: query.query(
drive.class.File,
{ _id },
@ -66,17 +67,27 @@
}
function handleUploadFile (): void {
if (object != null && upload != null) {
upload.click()
}
}
if (object != null) {
void showFilesUploadPopup(
{ objectId: object._id, objectClass: object._class },
{
maxNumberOfFiles: 1,
hideProgress: true
},
async (uuid, name, file, metadata) => {
const data = {
file: uuid,
name,
size: file.size,
type: file.type,
lastModified: file instanceof File ? file.lastModified : Date.now(),
metadata
}
async function handleFileSelected (): Promise<void> {
const files = upload.files
if (object != null && files !== null && files.length > 0) {
await replaceOneFile(object._id, files[0])
await createFileVersion(client, _id, data)
}
)
}
upload.value = ''
}
</script>
@ -92,16 +103,6 @@
on:close
on:update
>
<input
bind:this={upload}
id="file"
name="file"
type="file"
style="display: none"
disabled={upload == null}
on:change={handleFileSelected}
/>
<svelte:fragment slot="title">
<FileHeader {object} />
</svelte:fragment>

View File

@ -14,10 +14,10 @@
//
import { type Class, type Doc, type Ref, toIdMap } from '@hcengineering/core'
import type { Drive, File as DriveFile, FileVersion, Folder, Resource } from '@hcengineering/drive'
import drive, { createFile, createFileVersion } from '@hcengineering/drive'
import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation'
import type { Drive, FileVersion, Folder, Resource } from '@hcengineering/drive'
import drive from '@hcengineering/drive'
import { type Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { type AnySvelteComponent, showPopup } from '@hcengineering/ui'
import { openDoc } from '@hcengineering/view-resources'
@ -63,47 +63,6 @@ export async function editDrive (drive: Drive): Promise<void> {
showPopup(CreateDrive, { drive })
}
export async function uploadFiles (list: FileList, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
await uploadOneFile(file, space, parent)
}
}
}
export async function uploadOneFile (file: File, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
const client = getClient()
try {
const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid)
const { name, size, type, lastModified } = file
const data = { file: uuid, name, size, type, lastModified, metadata }
await createFile(client, space, parent, data)
} catch (e) {
void setPlatformStatus(unknownError(e))
}
}
export async function replaceOneFile (existing: Ref<DriveFile>, file: File): Promise<void> {
const client = getClient()
try {
const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid)
const { name, size, type, lastModified } = file
const data = { file: uuid, name, size, type, lastModified, metadata }
await createFileVersion(client, existing, data)
} catch (e) {
void setPlatformStatus(unknownError(e))
}
}
export async function renameResource (resource: Resource): Promise<void> {
showPopup(RenamePopup, { value: resource.name, format: 'text' }, undefined, async (res) => {
if (res != null && res !== resource.name) {

View File

@ -349,7 +349,7 @@
return
}
const size = await getImageSize(file, getFileUrl(attached.file))
const size = await getImageSize(file)
editor.commands.insertContent(
{

View File

@ -250,7 +250,7 @@
return
}
const size = await getImageSize(file, getFileUrl(attached.file))
const size = await getImageSize(file)
editor.editorHandler.insertContent(
{

View File

@ -150,7 +150,7 @@ async function handleImageUpload (
try {
const url = getFileUrl(attached.file)
const size = await getImageSize(file, url)
const size = await getImageSize(file)
const node = view.state.schema.nodes.image.create({
'file-id': attached.file,
src: url,

View File

@ -17,6 +17,9 @@
import { type Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import ScreenCapture from '@uppy/screen-capture'
import Webcam from '@uppy/webcam'
import { onMount, onDestroy, createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
@ -34,6 +37,8 @@
onMount(() => {
uppy.on('upload', handleUpload)
uppy.use(ScreenCapture)
uppy.use(Webcam)
uppy.use(Dashboard, {
id: 'huly:Dashboard',
target: container,

View File

@ -17,8 +17,9 @@
import {
Button,
IconClose,
ProgressCircle,
Label,
ProgressCircle,
Scroller,
humanReadableFileSize as filesize,
tooltip
} from '@hcengineering/ui'
@ -110,98 +111,110 @@
<div class="antiPopup upload-popup">
<div class="upload-popup__header flex-row-center flex-gap-1">
<Label label={uploader.string.UploadingTo} params={{ files: files.length }} />
<ObjectPresenter
objectId={upload.target.objectId}
_class={upload.target.objectClass}
shouldShowAvatar={false}
accent
noUnderline
/>
<div class="label overflow-label">
<Label label={uploader.string.UploadingTo} params={{ files: files.length }} />
</div>
<div class="flex flex-grow overflow-label">
<ObjectPresenter
objectId={upload.target.objectId}
_class={upload.target.objectClass}
shouldShowAvatar={false}
accent
noUnderline
/>
</div>
</div>
<div class="flex-col flex-gap-4">
{#each files as file}
{#if file.progress}
{@const error = getFileError(file)}
{@const percentage = getFilePercentage(file)}
<Scroller>
<div class="upload-popup__content flex-col flex-no-shrink flex-gap-4">
{#each files as file}
{#if file.progress}
{@const error = getFileError(file)}
{@const percentage = getFilePercentage(file)}
<div class="upload-file-row flex-row-center justify-start flex-gap-4">
<div class="upload-file-row__status w-4">
{#if error}
<IconError size={'small'} fill={'var(--negative-button-default)'} />
{:else if file.progress.uploadComplete}
<IconCompleted size={'small'} fill={'var(--positive-button-default)'} />
{:else}
<ProgressCircle value={percentage} size={'small'} primary />
{/if}
</div>
<div class="upload-file-row__content flex-col flex-gap-1">
<div class="label overflow-label" use:tooltip={{ label: getEmbeddedLabel(file.name) }}>{file.name}</div>
<div class="flex-row-center flex-gap-2 text-sm">
<div class="upload-file-row flex-row-center justify-start flex-gap-4">
<div class="upload-file-row__status w-4">
{#if error}
<Label label={uploader.status.Error} />
<span class="label overflow-label" use:tooltip={{ label: getEmbeddedLabel(error) }}>{error}</span>
{:else if file.progress.uploadStarted != null}
{#if file.progress.uploadComplete}
<Label label={uploader.status.Completed} />
<IconError size={'small'} fill={'var(--negative-button-default)'} />
{:else if file.progress.uploadComplete}
<IconCompleted size={'small'} fill={'var(--positive-button-default)'} />
{:else}
<ProgressCircle value={percentage} size={'small'} primary />
{/if}
</div>
<div class="upload-file-row__content flex-col flex-gap-1">
<div class="label overflow-label" use:tooltip={{ label: getEmbeddedLabel(file.name) }}>{file.name}</div>
<div class="flex-row-center flex-gap-2 text-sm">
{#if error}
<Label label={uploader.status.Error} />
<span class="label overflow-label" use:tooltip={{ label: getEmbeddedLabel(error) }}>{error}</span>
{:else if file.progress.uploadStarted != null}
{#if file.progress.uploadComplete}
<Label label={uploader.status.Completed} />
{#if file.progress.bytesTotal}
<span>{filesize(file.progress.bytesTotal)}</span>
{/if}
{:else}
<Label label={uploader.status.Uploading} />
{#if file.progress.bytesTotal}
<span>{filesize(file.progress.bytesUploaded)} / {filesize(file.progress.bytesTotal)}</span>
{:else}
<span>{filesize(file.progress.bytesUploaded)}}</span>
{/if}
{/if}
{:else}
<Label label={uploader.status.Waiting} />
{#if file.progress.bytesTotal}
<span>{filesize(file.progress.bytesTotal)}</span>
{/if}
{:else}
<Label label={uploader.status.Uploading} />
{#if file.progress.bytesTotal}
<span>{filesize(file.progress.bytesUploaded)} / {filesize(file.progress.bytesTotal)}</span>
{:else}
<span>{filesize(file.progress.bytesUploaded)}}</span>
{/if}
{/if}
{:else}
<Label label={uploader.status.Waiting} />
{#if file.progress.bytesTotal}
<span>{filesize(file.progress.bytesTotal)}</span>
{/if}
</div>
</div>
<div class="upload-file-row__tools flex-row-center">
{#if error}
<Button
kind={'icon'}
icon={IconRetry}
iconProps={{ size: 'small' }}
showTooltip={{ label: uploader.string.Retry }}
on:click={() => {
handleRetryFile(file)
}}
/>
{/if}
{#if !file.progress.uploadComplete && individualCancellation}
<Button
kind={'icon'}
icon={IconClose}
iconProps={{ size: 'small' }}
showTooltip={{ label: uploader.string.Cancel }}
on:click={() => {
handleCancelFile(file)
}}
/>
{/if}
</div>
</div>
<div class="upload-file-row__tools flex-row-center">
{#if error}
<Button
kind={'icon'}
icon={IconRetry}
iconProps={{ size: 'small' }}
showTooltip={{ label: uploader.string.Retry }}
on:click={() => {
handleRetryFile(file)
}}
/>
{/if}
{#if !file.progress.uploadComplete && individualCancellation}
<Button
kind={'icon'}
icon={IconClose}
iconProps={{ size: 'small' }}
showTooltip={{ label: uploader.string.Cancel }}
on:click={() => {
handleCancelFile(file)
}}
/>
{/if}
</div>
</div>
{/if}
{/each}
</div>
{/if}
{/each}
</div>
</Scroller>
</div>
<style lang="scss">
.upload-popup {
padding: var(--spacing-2);
max-height: 30rem;
.upload-popup__header {
padding-bottom: 1rem;
}
.upload-popup__content {
margin: 0.5rem;
margin-right: 0.625rem;
}
}
.upload-file-row {

View File

@ -13,15 +13,13 @@
// limitations under the License.
//
import { generateId } from '@hcengineering/core'
import { type Blob, type Ref, generateId } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import presentation, { getFileMetadata } from '@hcengineering/presentation'
import { getCurrentLanguage } from '@hcengineering/theme'
import type { FileUploadCallback, FileUploadOptions } from '@hcengineering/uploader'
import Uppy, { type IndexedObject, type UppyOptions } from '@uppy/core'
import ScreenCapture from '@uppy/screen-capture'
import Webcam from '@uppy/webcam'
import XHR from '@uppy/xhr-upload'
import En from '@uppy/locales/lib/en_US'
@ -60,31 +58,38 @@ export type UppyBody = Body & {
/** @public */
export function getUppy (options: FileUploadOptions, onFileUploaded?: FileUploadCallback): Uppy<UppyMeta, UppyBody> {
const id = generateId()
const locale = getUppyLocale(getCurrentLanguage())
const uppy = new Uppy<UppyMeta, UppyBody>({ id, locale, ...options })
.use(ScreenCapture)
.use(Webcam)
.use(XHR, {
endpoint: getMetadata(presentation.metadata.UploadURL) ?? '',
method: 'POST',
headers: {
Authorization: 'Bearer ' + (getMetadata(presentation.metadata.Token) as string)
},
getResponseData: (body: string): UppyBody => {
return {
uuid: body
}
const uppyOptions: Partial<UppyOptions> = {
id: generateId(),
locale: getUppyLocale(getCurrentLanguage()),
allowMultipleUploadBatches: false,
restrictions: {
maxFileSize: options.maxFileSize,
maxNumberOfFiles: options.maxNumberOfFiles,
allowedFileTypes: options.allowedFileTypes
}
}
const uppy = new Uppy<UppyMeta, UppyBody>(uppyOptions).use(XHR, {
endpoint: getMetadata(presentation.metadata.UploadURL) ?? '',
method: 'POST',
headers: {
Authorization: 'Bearer ' + (getMetadata(presentation.metadata.Token) as string)
},
getResponseData: (body: string): UppyBody => {
return {
uuid: body
}
})
}
})
if (onFileUploaded != null) {
uppy.addPostProcessor(async (fileIds: string[]) => {
for (const fileId of fileIds) {
const file = uppy.getFile(fileId)
const uuid = file?.response?.body?.uuid
const uuid = file?.response?.body?.uuid as Ref<Blob>
if (uuid !== undefined) {
await onFileUploaded(uuid, file.name, file.data)
const metadata = await getFileMetadata(file.data, uuid)
await onFileUploaded(uuid, file.name, file.data, metadata)
}
}
})

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import type { Class, Doc, Ref } from '@hcengineering/core'
import type { Blob as PlatformBlob, Class, Doc, Ref } from '@hcengineering/core'
/** @public */
export type UploadFilesPopupFn = (
@ -40,9 +40,14 @@ export interface FileUploadTarget {
export interface FileUploadOptions {
maxFileSize?: number
maxNumberOfFiles?: number
allowedFileTypes?: string
allowedFileTypes?: string[] | null
hideProgress?: boolean
}
/** @public */
export type FileUploadCallback = (uuid: string, name: string, file: File | Blob) => Promise<void>
export type FileUploadCallback = (
uuid: Ref<PlatformBlob>,
name: string,
file: File | Blob,
metadata: Record<string, any> | undefined
) => Promise<void>

View File

@ -14,10 +14,10 @@
//
import { type Blob, type Ref } from '@hcengineering/core'
import { type BlobMetadata, getFileUrl, getImageSize } from '@hcengineering/presentation'
import { type BlobMetadata, getImageSize } from '@hcengineering/presentation'
export async function blobImageMetadata (file: File, blob: Ref<Blob>): Promise<BlobMetadata | undefined> {
const size = await getImageSize(file, getFileUrl(blob, 'full'))
const size = await getImageSize(file)
return {
originalHeight: size.height,
@ -27,7 +27,7 @@ export async function blobImageMetadata (file: File, blob: Ref<Blob>): Promise<B
}
export async function blobVideoMetadata (file: File, blob: Ref<Blob>): Promise<BlobMetadata | undefined> {
const size = await getVideoSize(blob)
const size = await getVideoSize(file)
if (size === undefined) {
return undefined
@ -39,19 +39,22 @@ export async function blobVideoMetadata (file: File, blob: Ref<Blob>): Promise<B
}
}
async function getVideoSize (uuid: Ref<Blob>): Promise<{ width: number, height: number } | undefined> {
async function getVideoSize (file: File): Promise<{ width: number, height: number } | undefined> {
const promise = new Promise<{ width: number, height: number }>((resolve, reject) => {
const element = document.createElement('video')
const src = URL.createObjectURL(file)
element.onloadedmetadata = () => {
const height = element.videoHeight
const width = element.videoWidth
URL.revokeObjectURL(src)
resolve({ height, width })
}
element.onerror = reject
element.src = getFileUrl(uuid)
element.src = src
})
return await promise