UBERF-7776: Get rid of blobs in UI (#6226)

This commit is contained in:
Andrey Sobolev 2024-08-02 12:08:24 +07:00 committed by GitHub
parent fbcc59011d
commit b5ee82cf1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 240 additions and 803 deletions

View File

@ -13,18 +13,18 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { type Blob } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { Button, Component, Label, resizeObserver, deviceOptionsStore as deviceInfo } from '@hcengineering/ui' import { Button, Component, Label, resizeObserver, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import presentation from '../plugin' import presentation from '../plugin'
import { getPreviewType, previewTypes } from '../file' import { getPreviewType, previewTypes } from '../file'
import { BlobMetadata, FilePreviewExtension } from '../types' import { BlobMetadata, FilePreviewExtension } from '../types'
import { getFileUrl } from '../utils'
import { getBlobSrcFor } from '../preview' export let file: Ref<Blob>
export let file: Blob
export let name: string export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined export let metadata: BlobMetadata | undefined
export let props: Record<string, any> = {} export let props: Record<string, any> = {}
export let fit: boolean = false export let fit: boolean = false
@ -35,7 +35,7 @@
$: parentHeight = ($deviceInfo.docHeight * 80) / 100 $: parentHeight = ($deviceInfo.docHeight * 80) / 100
let previewType: FilePreviewExtension | undefined = undefined let previewType: FilePreviewExtension | undefined = undefined
$: void getPreviewType(file.contentType, $previewTypes).then((res) => { $: void getPreviewType(contentType, $previewTypes).then((res) => {
previewType = res previewType = res
}) })
@ -75,7 +75,7 @@
} }
$: updateHeight(parentWidth, parentHeight, previewType, metadata) $: updateHeight(parentWidth, parentHeight, previewType, metadata)
$: audio = previewType && Array.isArray(previewType) && previewType[0].contentType === 'audio/*' $: audio = previewType && Array.isArray(previewType) && previewType[0].contentType === 'audio/*'
$: srcRef = getBlobSrcFor(file, name) $: srcRef = getFileUrl(file, name)
</script> </script>
<div <div
@ -90,10 +90,7 @@
<Label label={presentation.string.FailedToPreview} /> <Label label={presentation.string.FailedToPreview} />
</div> </div>
{:else if previewType !== undefined} {:else if previewType !== undefined}
<Component <Component is={previewType.component} props={{ value: file, name, contentType, metadata, ...props, fit }} />
is={previewType.component}
props={{ value: file, name, contentType: file.contentType, metadata, ...props, fit }}
/>
{:else} {:else}
<div class="flex-col items-center flex-gap-3"> <div class="flex-col items-center flex-gap-3">
<Label label={presentation.string.ContentTypeNotSupported} /> <Label label={presentation.string.ContentTypeNotSupported} />

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform' import { getEmbeddedLabel } from '@hcengineering/platform'
import { Button, Dialog, tooltip } from '@hcengineering/ui' import { Button, Dialog, tooltip } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
@ -21,15 +21,15 @@
import presentation from '../plugin' import presentation from '../plugin'
import { BlobMetadata } from '../types' import { BlobMetadata } from '../types'
import { getClient } from '../utils' import { getClient, getFileUrl } from '../utils'
import { getBlobSrcFor } from '../preview'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import FilePreview from './FilePreview.svelte' import FilePreview from './FilePreview.svelte'
import Download from './icons/Download.svelte' import Download from './icons/Download.svelte'
export let file: Blob | Ref<Blob> | undefined export let file: Ref<Blob> | undefined
export let name: string export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined export let metadata: BlobMetadata | undefined
export let props: Record<string, any> = {} export let props: Record<string, any> = {}
@ -53,14 +53,7 @@
return ext.substring(0, 4).toUpperCase() return ext.substring(0, 4).toUpperCase()
} }
let blob: Blob | undefined = undefined $: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
$: void fetchBlob(file)
async function fetchBlob (file: Blob | Ref<Blob> | undefined): Promise<void> {
blob = typeof file === 'string' ? await client.findOne(core.class.Blob, { _id: file }) : file
}
$: srcRef = getBlobSrcFor(blob, name)
</script> </script>
<ActionContext context={{ mode: 'browser' }} /> <ActionContext context={{ mode: 'browser' }} />
@ -101,8 +94,8 @@
{/await} {/await}
</svelte:fragment> </svelte:fragment>
{#if blob !== undefined} {#if file}
<FilePreview file={blob} {name} {metadata} {props} fit /> <FilePreview {file} {contentType} {name} {metadata} {props} fit />
{/if} {/if}
</Dialog> </Dialog>

View File

@ -17,11 +17,11 @@
import type { Blob, Ref } from '@hcengineering/core' import type { Blob, Ref } from '@hcengineering/core'
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui' import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import presentation, { getBlobSrcFor } from '..' import presentation, { getFileUrl } from '..'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import Download from './icons/Download.svelte' import Download from './icons/Download.svelte'
export let file: Blob | Ref<Blob> | undefined export let file: Ref<Blob> | undefined
export let name: string export let name: string
export let contentType: string | undefined export let contentType: string | undefined
// export let popupOptions: PopupOptions // export let popupOptions: PopupOptions
@ -45,7 +45,7 @@
}) })
let download: HTMLAnchorElement let download: HTMLAnchorElement
$: srcRef = getBlobSrcFor(file, name) $: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
$: isImage = contentType !== undefined && contentType.startsWith('image/') $: isImage = contentType !== undefined && contentType.startsWith('image/')

View File

@ -1,99 +1,36 @@
import type { Blob, BlobLookup, Ref } from '@hcengineering/core' import type { Blob, Ref } from '@hcengineering/core'
import core, { concatLink } from '@hcengineering/core' import { concatLink } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { getBlobHref, getClient, getCurrentWorkspaceUrl, getFileUrl } from '.' import { getCurrentWorkspaceUrl, getFileUrl } from '.'
import presentation from './plugin' import presentation from './plugin'
export interface ProviderPreviewConfig {
// Identifier of provider
// If set to '' could be applied to any provider, for example to exclude some 'image/gif' etc from being processing with providers.
providerId: string
// Preview url
// If '' preview is disabled for config.
previewUrl: string
// Content type markers, will check by containts, if passed, only allow to be used with matched content types.
contentTypes?: string[]
}
export interface PreviewConfig { export interface PreviewConfig {
default?: ProviderPreviewConfig previewUrl: string
previewers: Record<string, ProviderPreviewConfig[]>
} }
const defaultPreview = (): ProviderPreviewConfig => ({ const defaultPreview = (): string => `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size`
providerId: '',
previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size`
})
/** /**
* *
* PREVIEW_CONFIG env variable format. * PREVIEW_CONFIG env variable format.
* A `;` separated list of triples, providerName|previewUrl|supportedFormats. * previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
- providerName - a provider name should be same as in Storage configuration.
It coult be empty and it will match by content types.
- previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
- supportedFormats - a `,` separated list of file extensions.
- contentTypes - a ',' separated list of content type patterns.
*/ */
export function parsePreviewConfig (config?: string): PreviewConfig | undefined { export function parsePreviewConfig (config?: string): PreviewConfig | undefined {
if (config === undefined) { if (config === undefined) {
return return
} }
const result: PreviewConfig = { previewers: {} } return { previewUrl: config }
const nolineData = config
.split('\n')
.map((it) => it.trim())
.join(';')
const configs = nolineData.split(';')
for (const c of configs) {
if (c === '') {
continue // Skip empty lines
}
const [provider, url, contentTypes] = c.split('|').map((it) => it.trim())
const p: ProviderPreviewConfig = {
providerId: provider,
previewUrl: url,
// Allow preview only for images by default
contentTypes:
contentTypes !== undefined
? contentTypes
.split(',')
.map((it) => it.trim())
.filter((it) => it !== '')
: ['image/']
}
if (provider === '*') {
result.default = p
} else {
result.previewers[provider] = [...(result.previewers[provider] ?? []), p]
}
}
return result
} }
export function getPreviewConfig (): PreviewConfig { export function getPreviewConfig (): PreviewConfig {
return ( return (
(getMetadata(presentation.metadata.PreviewConfig) as PreviewConfig) ?? { (getMetadata(presentation.metadata.PreviewConfig) as PreviewConfig) ?? {
default: defaultPreview(), previewUrl: defaultPreview()
previewers: {
'': [
{
providerId: '',
contentTypes: ['image/gif', 'image/apng', 'image/svg'], // Disable gif and apng format preview.
previewUrl: ''
}
]
}
} }
) )
} }
export async function getBlobRef ( export async function getBlobRef (
blob: Blob | undefined,
file: Ref<Blob>, file: Ref<Blob>,
name?: string, name?: string,
width?: number width?: number
@ -101,74 +38,30 @@ export async function getBlobRef (
src: string src: string
srcset: string srcset: string
}> { }> {
let _blob = blob as BlobLookup
if (_blob === undefined) {
_blob = (await getClient().findOne(core.class.Blob, { _id: file })) as BlobLookup
}
return { return {
src: _blob?.downloadUrl ?? getFileUrl(file, name), src: getFileUrl(file, name),
srcset: _blob !== undefined ? getSrcSet(_blob, width) : '' srcset: getSrcSet(file, width)
} }
} }
export async function getBlobSrcSet (_blob: Blob | undefined, file: Ref<Blob>, width?: number): Promise<string> { export async function getBlobSrcSet (file: Ref<Blob>, width?: number): Promise<string> {
if (_blob === undefined) { return getSrcSet(file, width)
_blob = await getClient().findOne(core.class.Blob, { _id: file })
}
return _blob !== undefined ? getSrcSet(_blob, width) : ''
} }
/** export function getSrcSet (_blob: Ref<Blob>, width?: number): string {
* Select content provider based on content type. return blobToSrcSet(getPreviewConfig(), _blob, width)
*/
export function selectProvider (
blob: Blob,
providers: Array<ProviderPreviewConfig | undefined>
): ProviderPreviewConfig | undefined {
const isMatched = (it: ProviderPreviewConfig): boolean =>
it.contentTypes === undefined || it.contentTypes.some((e) => blob.contentType === e || blob.contentType.includes(e))
let candidate: ProviderPreviewConfig | undefined
for (const p of providers) {
if (p !== undefined && isMatched(p)) {
if (p.previewUrl === '') {
// we found one disable config line, so return it.
return p
}
candidate = p
}
}
return candidate
} }
export function getSrcSet (_blob: Blob, width?: number): string { function blobToSrcSet (cfg: PreviewConfig, blob: Ref<Blob>, width: number | undefined): string {
const blob = _blob as BlobLookup
const c = getPreviewConfig()
// Select providers from
const cfg = selectProvider(blob, [...(c.previewers[_blob.provider] ?? []), ...(c.previewers[''] ?? []), c.default])
if (cfg === undefined || cfg.previewUrl === '') {
return '' // No previewer is available for blob
}
return blobToSrcSet(cfg, blob, width)
}
function blobToSrcSet (
cfg: ProviderPreviewConfig,
blob: { _id: Ref<Blob>, downloadUrl?: string },
width: number | undefined
): string {
let url = cfg.previewUrl.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceUrl())) let url = cfg.previewUrl.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceUrl()))
const downloadUrl = blob.downloadUrl ?? getFileUrl(blob._id) const downloadUrl = getFileUrl(blob)
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
if (!url.includes('://')) { if (!url.includes('://')) {
url = concatLink(frontUrl ?? '', url) url = concatLink(frontUrl ?? '', url)
} }
url = url.replaceAll(':downloadFile', encodeURIComponent(downloadUrl)) url = url.replaceAll(':downloadFile', encodeURIComponent(downloadUrl))
url = url.replaceAll(':blobId', encodeURIComponent(blob._id)) url = url.replaceAll(':blobId', encodeURIComponent(blob))
let result = '' let result = ''
const fu = url const fu = url
@ -187,24 +80,9 @@ function blobToSrcSet (
return result return result
} }
export async function getBlobSrcFor (blob: Blob | Ref<Blob> | undefined, name?: string): Promise<string> {
return blob === undefined
? ''
: typeof blob === 'string'
? await getBlobHref(undefined, blob, name)
: await getBlobHref(blob, blob._id)
}
/*** /***
* @deprecated, please use Blob direct operations. * @deprecated, please use Blob direct operations.
*/ */
export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string { export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string {
const cfg = getPreviewConfig() return blobToSrcSet(getPreviewConfig(), _blob, width)
return blobToSrcSet(
cfg.default ?? defaultPreview(),
{
_id: _blob
},
width
)
} }

View File

@ -24,7 +24,6 @@ import core, {
type AnyAttribute, type AnyAttribute,
type ArrOf, type ArrOf,
type AttachedDoc, type AttachedDoc,
type BlobLookup,
type Class, type Class,
type Client, type Client,
type Collection, type Collection,
@ -450,18 +449,6 @@ export function createQuery (dontDestroy?: boolean): LiveQuery {
return new LiveQuery(dontDestroy) return new LiveQuery(dontDestroy)
} }
export async function getBlobHref (
_blob: PlatformBlob | undefined,
file: Ref<PlatformBlob>,
filename?: string
): Promise<string> {
let blob = _blob as BlobLookup
if (blob?.downloadUrl === undefined) {
blob = (await getClient().findOne(core.class.Blob, { _id: file })) as BlobLookup
}
return blob?.downloadUrl ?? getFileUrl(file, filename)
}
export function getCurrentWorkspaceUrl (): string { export function getCurrentWorkspaceUrl (): string {
const wsId = get(workspaceId) const wsId = get(workspaceId)
if (wsId == null) { if (wsId == null) {

View File

@ -13,17 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { import { type Blob, type MeasureContext, type StorageIterator, type WorkspaceId } from '@hcengineering/core'
type Blob,
type Branding,
type DocumentUpdate,
type MeasureContext,
type Ref,
type StorageIterator,
type WorkspaceId,
type WorkspaceIdWithUrl
} from '@hcengineering/core'
import type { BlobLookup } from '@hcengineering/core/src/classes'
import { type Readable } from 'stream' import { type Readable } from 'stream'
export type ListBlobResult = Omit<Blob, 'contentType' | 'version'> export type ListBlobResult = Omit<Blob, 'contentType' | 'version'>
@ -38,11 +28,6 @@ export interface BlobStorageIterator {
close: () => Promise<void> close: () => Promise<void>
} }
export interface BlobLookupResult {
lookups: BlobLookup[]
updates?: Map<Ref<Blob>, DocumentUpdate<BlobLookup>>
}
export interface BucketInfo { export interface BucketInfo {
name: string name: string
delete: () => Promise<void> delete: () => Promise<void>
@ -50,11 +35,6 @@ export interface BucketInfo {
} }
export interface StorageAdapter { export interface StorageAdapter {
// If specified will limit a blobs available to put into selected provider.
// A set of content type patterns supported by this storage provider.
// If not defined, will be suited for any other content types.
contentTypes?: string[]
initialize: (ctx: MeasureContext, workspaceId: WorkspaceId) => Promise<void> initialize: (ctx: MeasureContext, workspaceId: WorkspaceId) => Promise<void>
close: () => Promise<void> close: () => Promise<void>
@ -84,14 +64,6 @@ export interface StorageAdapter {
offset: number, offset: number,
length?: number length?: number
) => Promise<Readable> ) => Promise<Readable>
// Lookup will extend Blob with lookup information.
lookup: (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
) => Promise<BlobLookupResult>
} }
export interface StorageAdapterEx extends StorageAdapter { export interface StorageAdapterEx extends StorageAdapter {
@ -189,15 +161,6 @@ export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx {
): Promise<UploadedObjectInfo> { ): Promise<UploadedObjectInfo> {
throw new Error('not implemented') throw new Error('not implemented')
} }
async lookup (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
): Promise<BlobLookupResult> {
return { lookups: [] }
}
} }
export function createDummyStorageAdapter (): StorageAdapter { export function createDummyStorageAdapter (): StorageAdapter {

View File

@ -18,7 +18,7 @@
import { import {
FilePreviewPopup, FilePreviewPopup,
canPreviewFile, canPreviewFile,
getBlobHref, getFileUrl,
getPreviewAlignment, getPreviewAlignment,
previewTypes previewTypes
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
@ -71,7 +71,8 @@
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: attachment.$lookup?.file ?? attachment.file, file: attachment.file,
contentType: attachment.type,
name: attachment.name, name: attachment.name,
metadata: attachment.metadata metadata: attachment.metadata
}, },
@ -130,32 +131,30 @@
</script> </script>
<div class="flex"> <div class="flex">
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href} <a
<a class="mr-1 flex-row-center gap-2 p-1"
class="mr-1 flex-row-center gap-2 p-1" href={getFileUrl(attachment.file, attachment.name)}
{href} download={attachment.name}
download={attachment.name} bind:this={download}
bind:this={download} use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }} on:click|stopPropagation
on:click|stopPropagation >
> {#if canPreview}
{#if canPreview}
<ActionIcon
icon={IconOpen}
size={'medium'}
action={(evt) => {
showPreview(evt)
}}
/>
{/if}
<ActionIcon <ActionIcon
icon={FileDownload} icon={IconOpen}
size={'medium'} size={'medium'}
action={() => { action={(evt) => {
download.click() showPreview(evt)
}} }}
/> />
</a> {/if}
{/await} <ActionIcon
icon={FileDownload}
size={'medium'}
action={() => {
download.click()
}}
/>
</a>
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} /> <ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
</div> </div>

View File

@ -48,11 +48,6 @@
}, },
(res) => { (res) => {
resAttachments = res resAttachments = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )
} else { } else {

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core' import type { WithLookup } from '@hcengineering/core'
import { FilePreviewPopup, getBlobHref } from '@hcengineering/presentation' import { FilePreviewPopup, getFileUrl } from '@hcengineering/presentation'
import { closeTooltip, showPopup } from '@hcengineering/ui' import { closeTooltip, showPopup } from '@hcengineering/ui'
import filesize from 'filesize' import filesize from 'filesize'
import { getType } from '../utils' import { getType } from '../utils'
@ -45,41 +45,27 @@
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: value.$lookup?.file ?? value.file, file: value.file,
contentType: value.type,
name: value.name, name: value.name,
metadata: value.metadata metadata: value.metadata
}, },
isImage(value.type) ? 'centered' : 'float' isImage(value.type) ? 'centered' : 'float'
) )
} }
$: src = getFileUrl(value.file, value.name)
</script> </script>
<div class="gridCellOverlay"> <div class="gridCellOverlay">
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then src} <div class="gridCell">
<div class="gridCell"> {#if isImage(value.type)}
{#if isImage(value.type)} <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <div class="cellImagePreview" on:click={openAttachment}>
<div class="cellImagePreview" on:click={openAttachment}> <img class={'img-fit'} {src} alt={value.name} />
<img class={'img-fit'} {src} alt={value.name} /> </div>
</div> {:else}
{:else} <div class="cellMiscPreview">
<div class="cellMiscPreview">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-center extensionIcon" on:click={openAttachment}>
{extensionIconLabel(value.name)}
</div>
{:else}
<a class="no-line" href={src} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
</div>
{/if}
<div class="cellInfo">
{#if isEmbedded(value.type)} {#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@ -91,24 +77,38 @@
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div> <div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a> </a>
{/if} {/if}
<div class="eCellInfoData">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eCellInfoFilename" on:click={openAttachment}>
{trimFilename(value.name)}
</div>
{:else}
<div class="eCellInfoFilename">
<a href={src} download={value.name}>{trimFilename(value.name)}</a>
</div>
{/if}
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
</div>
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
</div> </div>
{/if}
<div class="cellInfo">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-center extensionIcon" on:click={openAttachment}>
{extensionIconLabel(value.name)}
</div>
{:else}
<a class="no-line" href={src} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
<div class="eCellInfoData">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eCellInfoFilename" on:click={openAttachment}>
{trimFilename(value.name)}
</div>
{:else}
<div class="eCellInfoFilename">
<a href={src} download={value.name}>{trimFilename(value.name)}</a>
</div>
{/if}
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
</div>
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
</div> </div>
{/await} </div>
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@ -107,7 +107,7 @@
</script> </script>
<div class="container" style="width:{toStyle(dimensions.width)}; height:{toStyle(dimensions.height)}"> <div class="container" style="width:{toStyle(dimensions.width)}; height:{toStyle(dimensions.height)}">
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth(urlSize)) then blobSrc} {#await getBlobRef(value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
<img <img
src={blobSrc.src} src={blobSrc.src}
style:object-fit={getObjectFit(dimensions)} style:object-fit={getObjectFit(dimensions)}

View File

@ -43,11 +43,6 @@
}, },
(res) => { (res) => {
docs = res docs = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )

View File

@ -83,7 +83,8 @@
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: value.$lookup?.file ?? value.file, file: value.file,
contentType: value.type,
name: value.name, name: value.name,
metadata: value.metadata metadata: value.metadata
}, },
@ -114,7 +115,7 @@
{:else} {:else}
<div class="flex-row-center attachment-container"> <div class="flex-row-center attachment-container">
{#if value} {#if value}
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth('large')) then valueRef} {#await getBlobRef(value.file, value.name, sizeToWidth('large')) then valueRef}
<a <a
class="no-line" class="no-line"
style:flex-shrink={0} style:flex-shrink={0}

View File

@ -51,7 +51,7 @@
if (listProvider !== undefined) listProvider.updateFocus(value) if (listProvider !== undefined) listProvider.updateFocus(value)
const popupInfo = showPopup( const popupInfo = showPopup(
FilePreviewPopup, FilePreviewPopup,
{ file: value.$lookup?.file ?? value.file, name: value.name }, { file: value.file, name: value.name, contentType: value.type },
value.type.startsWith('image/') ? 'centered' : 'float' value.type.startsWith('image/') ? 'centered' : 'float'
) )
dispatch('open', popupInfo.id) dispatch('open', popupInfo.id)

View File

@ -121,11 +121,6 @@
(res) => { (res) => {
originalAttachments = new Set(res.map((p) => p._id)) originalAttachments = new Set(res.map((p) => p._id))
attachments = toIdMap(res) attachments = toIdMap(res)
},
{
lookup: {
file: core.class.Blob
}
} }
) )
} else { } else {

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import attachment, { Attachment, BlobMetadata } from '@hcengineering/attachment' import attachment, { Attachment, BlobMetadata } from '@hcengineering/attachment'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import core, { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core' import { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core'
import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform' import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { import {
FileOrBlob, FileOrBlob,
@ -25,7 +25,6 @@
getFileMetadata, getFileMetadata,
uploadFile uploadFile
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
import textEditor, { type RefAction, type TextEditorHandler } from '@hcengineering/text-editor' import textEditor, { type RefAction, type TextEditorHandler } from '@hcengineering/text-editor'
import { import {
AttachIcon, AttachIcon,
@ -38,6 +37,7 @@
import { AnySvelteComponent, getEventPositionElement, getPopupPositionElement, navigate } from '@hcengineering/ui' import { AnySvelteComponent, getEventPositionElement, getPopupPositionElement, navigate } from '@hcengineering/ui'
import { uploadFiles } from '@hcengineering/uploader' import { uploadFiles } from '@hcengineering/uploader'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
import AttachmentsGrid from './AttachmentsGrid.svelte' import AttachmentsGrid from './AttachmentsGrid.svelte'
@ -106,11 +106,6 @@
}, },
(res) => { (res) => {
attachments = res attachments = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )

View File

@ -127,11 +127,6 @@
originalAttachments = new Set(res.map((p) => p._id)) originalAttachments = new Set(res.map((p) => p._id))
attachments = toIdMap(res) attachments = toIdMap(res)
dispatch('attach', { action: 'saved', value: attachments.size }) dispatch('attach', { action: 'saved', value: attachments.size })
},
{
lookup: {
file: core.class.Blob
}
} }
) )
} }

View File

@ -14,9 +14,9 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import { getBlobHref } from '@hcengineering/presentation'
import type { WithLookup } from '@hcengineering/core' import type { WithLookup } from '@hcengineering/core'
import { getFileUrl } from '@hcengineering/presentation'
import AttachmentPresenter from './AttachmentPresenter.svelte' import AttachmentPresenter from './AttachmentPresenter.svelte'
export let value: WithLookup<Attachment> export let value: WithLookup<Attachment>
@ -58,9 +58,7 @@
</script> </script>
<video controls width={dimensions.width} height={dimensions.height} preload={preload ? 'auto' : 'none'}> <video controls width={dimensions.width} height={dimensions.height} preload={preload ? 'auto' : 'none'}>
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then href} <source src={getFileUrl(value.file, value.name)} />
<source src={href} />
{/await}
<track kind="captions" label={value.name} /> <track kind="captions" label={value.name} />
<div class="container"> <div class="container">
<AttachmentPresenter {value} /> <AttachmentPresenter {value} />

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core' import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { getBlobHref, getClient } from '@hcengineering/presentation' import { getClient, getFileUrl } from '@hcengineering/presentation'
import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui' import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
import { AttachmentGalleryPresenter } from '..' import { AttachmentGalleryPresenter } from '..'
import FileDownload from './icons/FileDownload.svelte' import FileDownload from './icons/FileDownload.svelte'
@ -56,12 +56,11 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<AttachmentGalleryPresenter value={attachment}> <AttachmentGalleryPresenter value={attachment}>
<svelte:fragment slot="rowMenu"> <svelte:fragment slot="rowMenu">
{@const href = getFileUrl(attachment.file, attachment.name)}
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}> <div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href} <a {href} download={attachment.name}>
<a {href} download={attachment.name}> <Icon icon={FileDownload} size={'small'} />
<Icon icon={FileDownload} size={'small'} /> </a>
</a>
{/await}
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}> <div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
<IconMoreV size={'small'} /> <IconMoreV size={'small'} />
</div> </div>

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core' import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { getBlobHref, getClient } from '@hcengineering/presentation' import { getClient, getFileUrl } from '@hcengineering/presentation'
import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui' import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
import { AttachmentPresenter } from '..' import { AttachmentPresenter } from '..'
import FileDownload from './icons/FileDownload.svelte' import FileDownload from './icons/FileDownload.svelte'
@ -51,16 +51,15 @@
<div class="flex-col"> <div class="flex-col">
{#each attachments as attachment, i} {#each attachments as attachment, i}
{@const href = getFileUrl(attachment.file, attachment.name)}
<div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}> <div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}>
<div class="item flex"> <div class="item flex">
<AttachmentPresenter value={attachment} /> <AttachmentPresenter value={attachment} />
</div> </div>
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}> <div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href} <a {href} download={attachment.name}>
<a {href} download={attachment.name}> <Icon icon={FileDownload} size={'small'} />
<Icon icon={FileDownload} size={'small'} /> </a>
</a>
{/await}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}> <div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>

View File

@ -14,11 +14,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import { getBlobHref, getFileUrl } from '@hcengineering/presentation'
import { CircleButton, Progress } from '@hcengineering/ui'
import Play from './icons/Play.svelte'
import Pause from './icons/Pause.svelte'
import type { WithLookup } from '@hcengineering/core' import type { WithLookup } from '@hcengineering/core'
import { getFileUrl } from '@hcengineering/presentation'
import { CircleButton, Progress } from '@hcengineering/ui'
import Pause from './icons/Pause.svelte'
import Play from './icons/Play.svelte'
export let value: WithLookup<Attachment> export let value: WithLookup<Attachment>
export let fullSize = false export let fullSize = false
@ -48,9 +48,7 @@
</div> </div>
</div> </div>
<audio bind:duration bind:currentTime={time} bind:paused> <audio bind:duration bind:currentTime={time} bind:paused>
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then href} <source src={getFileUrl(value.file, value.name)} type={value.type} />
<source src={href} type={value.type} />
{/await}
</audio> </audio>
<style lang="scss"> <style lang="scss">

View File

@ -92,10 +92,7 @@
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery }, { ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
{ {
sort: sortModeToOptionObject(selectedSort_), sort: sortModeToOptionObject(selectedSort_),
limit: 200, limit: 200
lookup: {
file: core.class.Blob
}
} }
) )
isLoading = false isLoading = false

View File

@ -16,7 +16,7 @@
// import { Doc } from '@hcengineering/core' // import { Doc } from '@hcengineering/core'
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core' import type { WithLookup } from '@hcengineering/core'
import presentation, { ActionContext, IconDownload, getBlobHref, getBlobRef } from '@hcengineering/presentation' import presentation, { ActionContext, getFileUrl, IconDownload } from '@hcengineering/presentation'
import { Button, Dialog } from '@hcengineering/ui' import { Button, Dialog } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { getType } from '../utils' import { getType } from '../utils'
@ -40,7 +40,7 @@
}) })
let download: HTMLAnchorElement let download: HTMLAnchorElement
$: type = getType(value.type) $: type = getType(value.type)
$: srcRef = getBlobHref(value.$lookup?.file, value.file, value.name) $: srcRef = getFileUrl(value.file, value.name)
</script> </script>
<ActionContext context={{ mode: 'browser' }} /> <ActionContext context={{ mode: 'browser' }} />

View File

@ -17,14 +17,7 @@
import { Photo } from '@hcengineering/attachment' import { Photo } from '@hcengineering/attachment'
import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core' import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core'
import { setPlatformStatus, unknownError } from '@hcengineering/platform' import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { import { FilePreviewPopup, createQuery, getBlobRef, getClient, uploadFile } from '@hcengineering/presentation'
FilePreviewPopup,
createQuery,
getBlobHref,
getClient,
uploadFile,
getBlobRef
} from '@hcengineering/presentation'
import { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui' import { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui'
import attachment from '../plugin' import attachment from '../plugin'
import UploadDuo from './icons/UploadDuo.svelte' import UploadDuo from './icons/UploadDuo.svelte'
@ -99,7 +92,7 @@
if (item !== undefined) { if (item !== undefined) {
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ file: item.$lookup?.file ?? item.file, name: item.name }, { file: item.file, name: item.name, contentType: item.type },
item.type.startsWith('image/') ? 'centered' : 'float' item.type.startsWith('image/') ? 'centered' : 'float'
) )
} else { } else {
@ -157,7 +150,7 @@
click(ev, image) click(ev, image)
}} }}
> >
{#await getBlobRef(image.$lookup?.file, image.file, image.name) then blobRef} {#await getBlobRef(image.file, image.name) then blobRef}
<img src={blobRef.src} srcset={blobRef.srcset} alt={image.name} /> <img src={blobRef.src} srcset={blobRef.srcset} alt={image.name} />
{/await} {/await}
</div> </div>

View File

@ -17,8 +17,8 @@
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources' import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
import { ChunterSpace } from '@hcengineering/chunter' import { ChunterSpace } from '@hcengineering/chunter'
import core, { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core' import { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation' import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui' import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
export let channel: ChunterSpace | undefined export let channel: ChunterSpace | undefined
@ -68,10 +68,7 @@
{ {
limit: ATTACHEMNTS_LIMIT, limit: ATTACHEMNTS_LIMIT,
sort, sort,
total: true, total: true
lookup: {
file: core.class.Blob
}
} }
) )
</script> </script>
@ -86,11 +83,9 @@
<AttachmentPresenter value={attachment} /> <AttachmentPresenter value={attachment} />
</div> </div>
<div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}> <div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then blobRef} <a href={getFileUrl(attachment.file, attachment.name)} download={attachment.name}>
<a href={blobRef} download={attachment.name}> <Icon icon={FileDownload} size={'small'} />
<Icon icon={FileDownload} size={'small'} /> </a>
</a>
{/await}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showMenu(event, attachment, i)}> <div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showMenu(event, attachment, i)}>

View File

@ -41,11 +41,6 @@
}, },
(res) => { (res) => {
attachments = res attachments = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )
} else { } else {

View File

@ -38,7 +38,7 @@
$: if (empValue === undefined) { $: if (empValue === undefined) {
void getClient() void getClient()
.findOne(contact.class.Contact, { _id }, { lookup: { avatar: core.class.Blob } }) .findOne(contact.class.Contact, { _id })
.then((c) => { .then((c) => {
_contact = c _contact = c
}) })

View File

@ -15,11 +15,11 @@
// //
import { import {
type Channel,
type AvatarInfo,
type Contact,
getGravatarUrl, getGravatarUrl,
getName, getName,
type AvatarInfo,
type Channel,
type Contact,
type Person, type Person,
type PersonAccount type PersonAccount
} from '@hcengineering/contact' } from '@hcengineering/contact'
@ -49,6 +49,7 @@ import {
type ColorDefinition, type ColorDefinition,
type TooltipAlignment type TooltipAlignment
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { AggregationManager } from '@hcengineering/view-resources'
import AccountArrayEditor from './components/AccountArrayEditor.svelte' import AccountArrayEditor from './components/AccountArrayEditor.svelte'
import AccountBox from './components/AccountBox.svelte' import AccountBox from './components/AccountBox.svelte'
import AssigneeBox from './components/AssigneeBox.svelte' import AssigneeBox from './components/AssigneeBox.svelte'
@ -122,7 +123,6 @@ import NameChangedActivityMessage from './components/activity/NameChangedActivit
import IconAddMember from './components/icons/AddMember.svelte' import IconAddMember from './components/icons/AddMember.svelte'
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte' import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
import IconMembers from './components/icons/Members.svelte' import IconMembers from './components/icons/Members.svelte'
import { AggregationManager } from '@hcengineering/view-resources'
import { get, writable } from 'svelte/store' import { get, writable } from 'svelte/store'
import contact from './plugin' import contact from './plugin'
@ -408,7 +408,7 @@ export default async (): Promise<Resources> => ({
color: getPersonColor(person, name) color: getPersonColor(person, name)
} }
} }
const blobRef = await getBlobRef(person.$lookup?.avatar, person.avatar, undefined, width) const blobRef = await getBlobRef(person.avatar, undefined, width)
return { return {
url: blobRef.src, url: blobRef.src,
srcSet: blobRef.srcset, srcSet: blobRef.srcset,

View File

@ -320,19 +320,10 @@ function fillStores (): void {
const accountPersonQuery = createQuery(true) const accountPersonQuery = createQuery(true)
const query = createQuery(true) const query = createQuery(true)
query.query( query.query(contact.mixin.Employee, {}, (res) => {
contact.mixin.Employee, employeesStore.set(res)
{}, employeeByIdStore.set(toIdMap(res))
(res) => { })
employeesStore.set(res)
employeeByIdStore.set(toIdMap(res))
},
{
lookup: {
avatar: core.class.Blob
}
}
)
const accountQ = createQuery(true) const accountQ = createQuery(true)
accountQ.query(contact.class.PersonAccount, {}, (res) => { accountQ.query(contact.class.PersonAccount, {}, (res) => {
@ -345,11 +336,6 @@ function fillStores (): void {
{ _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } }, { _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } },
(res) => { (res) => {
personAccountPersonByIdStore.set(toIdMap(res)) personAccountPersonByIdStore.set(toIdMap(res))
},
{
lookup: {
avatar: core.class.Blob
}
} }
) )
}) })

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { type Blob, type WithLookup } from '@hcengineering/core' import { type Blob, type Ref, type WithLookup } from '@hcengineering/core'
import drive, { type File, type FileVersion } from '@hcengineering/drive' import drive, { type File, type FileVersion } from '@hcengineering/drive'
import { FilePreview, createQuery } from '@hcengineering/presentation' import { FilePreview, createQuery } from '@hcengineering/presentation'
@ -26,32 +26,23 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const query = createQuery() const query = createQuery()
let blob: Blob | undefined = undefined let blob: Ref<Blob> | undefined = undefined
let version: WithLookup<FileVersion> | undefined = undefined let version: WithLookup<FileVersion> | undefined = undefined
let contentType: string | undefined
$: query.query( $: query.query(drive.class.FileVersion, { _id: object.file }, (res) => {
drive.class.FileVersion, ;[version] = res
{ _id: object.file }, blob = version.file
(res) => { contentType = version.type
;[version] = res })
blob = version?.$lookup?.file
},
{
lookup: {
file: core.class.Blob
}
}
)
onMount(() => { onMount(() => {
dispatch('open', { ignoreKeys: ['parent', 'path', 'version', 'versions'] }) dispatch('open', { ignoreKeys: ['parent', 'path', 'version', 'versions'] })
}) })
</script> </script>
{#if object !== undefined && version !== undefined} {#if object !== undefined && version !== undefined && blob !== undefined && contentType !== undefined}
{#if blob !== undefined} <FilePreview file={blob} {contentType} name={version.name} metadata={version.metadata} fit />
<FilePreview file={blob} name={version.name} metadata={version.metadata} fit />
{/if}
{#if object.versions > 1} {#if object.versions > 1}
<div class="w-full mt-6"> <div class="w-full mt-6">

View File

@ -24,7 +24,6 @@
export let readonly: boolean = false export let readonly: boolean = false
const options: FindOptions<FileVersion> = { const options: FindOptions<FileVersion> = {
lookup: { file: core.class.Blob },
sort: { version: SortingOrder.Descending } sort: { version: SortingOrder.Descending }
} }
</script> </script>

View File

@ -24,9 +24,5 @@
<Scroller> <Scroller>
<DocAttributeBar {object} {readonly} ignoreKeys={[]} /> <DocAttributeBar {object} {readonly} ignoreKeys={[]} />
{#if object.$lookup?.file}
<DocAttributeBar object={object.$lookup.file} {readonly} ignoreKeys={['name', 'file', 'version', 'version']} />
{/if}
<div class="space-divider bottom" /> <div class="space-divider bottom" />
</Scroller> </Scroller>

View File

@ -16,7 +16,7 @@
import { type Ref, type WithLookup } from '@hcengineering/core' import { type Ref, type WithLookup } from '@hcengineering/core'
import { createFileVersion, type File as DriveFile, type FileVersion } from '@hcengineering/drive' import { createFileVersion, type File as DriveFile, type FileVersion } from '@hcengineering/drive'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation' import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
import { Button, IconMoreH } from '@hcengineering/ui' import { Button, IconMoreH } from '@hcengineering/ui'
import { showFilesUploadPopup } from '@hcengineering/uploader' import { showFilesUploadPopup } from '@hcengineering/uploader'
import view from '@hcengineering/view' import view from '@hcengineering/view'
@ -108,17 +108,15 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
{#await getBlobHref(undefined, version.file, object.name) then href} <a class="no-line" href={getFileUrl(version.file, object.name)} download={object.name} bind:this={download}>
<a class="no-line" {href} download={object.name} bind:this={download}> <Button
<Button icon={IconDownload}
icon={IconDownload} iconProps={{ size: 'medium' }}
iconProps={{ size: 'medium' }} kind={'icon'}
kind={'icon'} showTooltip={{ label: drive.string.Download }}
showTooltip={{ label: drive.string.Download }} on:click={handleDownloadFile}
on:click={handleDownloadFile} />
/> </a>
</a>
{/await}
<Button <Button
icon={IconUpload} icon={IconUpload}
iconProps={{ size: 'medium' }} iconProps={{ size: 'medium' }}

View File

@ -38,17 +38,11 @@
return return
} }
if (value.$lookup?.file === undefined) {
return
}
const blob = value.$lookup?.file
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: blob._id, file: value.file,
contentType: blob.contentType, contentType: value.type,
name: value.name, name: value.name,
metadata: value.metadata metadata: value.metadata
}, },

View File

@ -44,7 +44,7 @@
{#if isFolder} {#if isFolder}
<Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--global-no-priority-PriorityColor)'} /> <Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--global-no-priority-PriorityColor)'} />
{:else if previewRef != null && isImage && !isError} {:else if previewRef != null && isImage && !isError}
{#await getBlobRef(undefined, previewRef, object.name, sizeToWidth(size)) then blobSrc} {#await getBlobRef(previewRef, object.name, sizeToWidth(size)) then blobSrc}
<img <img
draggable="false" draggable="false"
class="img-fit" class="img-fit"

View File

@ -16,7 +16,6 @@
import { type Doc, type Ref, type WithLookup } from '@hcengineering/core' import { type Doc, type Ref, type WithLookup } from '@hcengineering/core'
import drive, { type Drive, type File, type FileVersion, type Folder } from '@hcengineering/drive' import drive, { type Drive, type File, type FileVersion, type Folder } from '@hcengineering/drive'
import { type Resources } from '@hcengineering/platform' import { type Resources } from '@hcengineering/platform'
import { getBlobHref } from '@hcengineering/presentation'
import { showPopup, type Location } from '@hcengineering/ui' import { showPopup, type Location } from '@hcengineering/ui'
import CreateDrive from './components/CreateDrive.svelte' import CreateDrive from './components/CreateDrive.svelte'
@ -37,8 +36,9 @@ import GridView from './components/GridView.svelte'
import MoveResource from './components/MoveResource.svelte' import MoveResource from './components/MoveResource.svelte'
import ResourcePresenter from './components/ResourcePresenter.svelte' import ResourcePresenter from './components/ResourcePresenter.svelte'
import { getFileUrl } from '@hcengineering/presentation'
import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation' import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation'
import { showCreateFolderPopup, showRenameResourcePopup, restoreFileVersion } from './utils' import { restoreFileVersion, showCreateFolderPopup, showRenameResourcePopup } from './utils'
async function CreateRootFolder (doc: Drive): Promise<void> { async function CreateRootFolder (doc: Drive): Promise<void> {
await showCreateFolderPopup(doc._id, drive.ids.Root) await showCreateFolderPopup(doc._id, drive.ids.Root)
@ -57,7 +57,7 @@ async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): P
for (const file of files) { for (const file of files) {
const version = file.$lookup?.file const version = file.$lookup?.file
if (version != null) { if (version != null) {
const href = await getBlobHref(undefined, version.file, version.name) const href = getFileUrl(version.file, version.name)
const link = document.createElement('a') const link = document.createElement('a')
link.style.display = 'none' link.style.display = 'none'
link.target = '_blank' link.target = '_blank'

View File

@ -52,11 +52,6 @@
}, },
(res) => { (res) => {
attachments = res attachments = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )

View File

@ -36,11 +36,6 @@
}, },
(res) => { (res) => {
attachments = res attachments = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )

View File

@ -366,7 +366,7 @@
const formattedSkills = (doc.skills.map((s) => s.toLowerCase()) ?? []).filter( const formattedSkills = (doc.skills.map((s) => s.toLowerCase()) ?? []).filter(
(skill) => !namedElements.has(skill) (skill) => !namedElements.has(skill)
) )
const refactoredSkills = [] const refactoredSkills: any[] = []
if (formattedSkills.length > 0) { if (formattedSkills.length > 0) {
const existingTags = Array.from(namedElements.keys()).filter((x) => x.length > 2) const existingTags = Array.from(namedElements.keys()).filter((x) => x.length > 2)
const regex = /\S+(?:[-+]\S+)+/g const regex = /\S+(?:[-+]\S+)+/g
@ -755,6 +755,7 @@
FilePreviewPopup, FilePreviewPopup,
{ {
file: object.resumeUuid, file: object.resumeUuid,
contentType: object.resumeType,
name: object.resumeName name: object.resumeName
}, },
object.resumeType?.startsWith('image/') ? 'centered' : 'float' object.resumeType?.startsWith('image/') ? 'centered' : 'float'

View File

@ -120,6 +120,7 @@
resultQuery = mergeQueries(query, e.detail) resultQuery = mergeQueries(query, e.detail)
}} }}
/> />
<Component <Component
is={viewlet.$lookup.descriptor.component} is={viewlet.$lookup.descriptor.component}
props={{ props={{

View File

@ -129,12 +129,14 @@ export const FileExtension = FileNode.extend<FileOptions>({
const fileId = node.attrs['file-id'] ?? '' const fileId = node.attrs['file-id'] ?? ''
if (fileId === '') return if (fileId === '') return
const fileName = node.attrs['data-file-name'] ?? '' const fileName = node.attrs['data-file-name'] ?? ''
const fileType = node.attrs['data-file-type'] ?? ''
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: fileId, file: fileId,
name: fileName, name: fileName,
contentType: fileType,
fullSize: false, fullSize: false,
showIcon: false showIcon: false
}, },

View File

@ -151,12 +151,14 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
const fileId = node.attrs['file-id'] ?? node.attrs.src const fileId = node.attrs['file-id'] ?? node.attrs.src
const fileName = node.attrs.alt ?? '' const fileName = node.attrs.alt ?? ''
const fileType = node.attrs['data-file-type'] ?? ''
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: fileId, file: fileId,
name: fileName, name: fileName,
contentType: fileType,
fullSize: true, fullSize: true,
showIcon: false showIcon: false
}, },
@ -209,10 +211,22 @@ export async function openImage (editor: Editor): Promise<void> {
const attributes = editor.getAttributes('image') const attributes = editor.getAttributes('image')
const fileId = attributes['file-id'] ?? attributes.src const fileId = attributes['file-id'] ?? attributes.src
const fileName = attributes.alt ?? '' const fileName = attributes.alt ?? ''
const fileType = attributes['data-file-type'] ?? ''
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
showPopup(FilePreviewPopup, { file: fileId, name: fileName, fullSize: true, showIcon: false }, 'centered', () => { showPopup(
resolve() FilePreviewPopup,
}) {
file: fileId,
name: fileName,
contentType: fileType,
fullSize: true,
showIcon: false
},
'centered',
() => {
resolve()
}
)
}) })
} }

View File

@ -12,26 +12,26 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { type Class, type Space, type Doc, type Ref } from '@hcengineering/core' import { type Class, type Doc, type Ref, type Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { getBlobRef, getClient } from '@hcengineering/presentation'
import { CodeBlockExtension, codeBlockOptions, CodeExtension, codeOptions } from '@hcengineering/text'
import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor'
import { type AnyExtension, type Editor, Extension } from '@tiptap/core' import { type AnyExtension, type Editor, Extension } from '@tiptap/core'
import { type Level } from '@tiptap/extension-heading' import { type Level } from '@tiptap/extension-heading'
import ListKeymap from '@tiptap/extension-list-keymap' import ListKeymap from '@tiptap/extension-list-keymap'
import TableHeader from '@tiptap/extension-table-header' import TableHeader from '@tiptap/extension-table-header'
import 'prosemirror-codemark/dist/codemark.css' import 'prosemirror-codemark/dist/codemark.css'
import { getBlobRef, getClient } from '@hcengineering/presentation'
import { CodeBlockExtension, codeBlockOptions, CodeExtension, codeOptions } from '@hcengineering/text'
import textEditor, { type ActionContext, type ExtensionCreator, type TextEditorMode } from '@hcengineering/text-editor'
import { DefaultKit, type DefaultKitOptions } from './default-kit'
import { HardBreakExtension } from '../components/extension/hardBreak'
import { FileExtension, type FileOptions } from '../components/extension/fileExt' import { FileExtension, type FileOptions } from '../components/extension/fileExt'
import { HardBreakExtension } from '../components/extension/hardBreak'
import { ImageExtension, type ImageOptions } from '../components/extension/imageExt' import { ImageExtension, type ImageOptions } from '../components/extension/imageExt'
import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { Table, TableCell, TableRow } from '../components/extension/table'
import { SubmitExtension, type SubmitOptions } from '../components/extension/submit'
import { ParagraphExtension } from '../components/extension/paragraph'
import { InlineToolbarExtension } from '../components/extension/inlineToolbar' import { InlineToolbarExtension } from '../components/extension/inlineToolbar'
import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { ParagraphExtension } from '../components/extension/paragraph'
import { SubmitExtension, type SubmitOptions } from '../components/extension/submit'
import { Table, TableCell, TableRow } from '../components/extension/table'
import { DefaultKit, type DefaultKitOptions } from './default-kit'
export interface EditorKitOptions extends DefaultKitOptions { export interface EditorKitOptions extends DefaultKitOptions {
history?: false history?: false
@ -225,7 +225,7 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
inline: true, inline: true,
loadingImgSrc: loadingImgSrc:
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im0gNCAxIGMgLTEuNjQ0NTMxIDAgLTMgMS4zNTU0NjkgLTMgMyB2IDEgaCAxIHYgLTEgYyAwIC0xLjEwOTM3NSAwLjg5MDYyNSAtMiAyIC0yIGggMSB2IC0xIHogbSAyIDAgdiAxIGggNCB2IC0xIHogbSA1IDAgdiAxIGggMSBjIDEuMTA5Mzc1IDAgMiAwLjg5MDYyNSAyIDIgdiAxIGggMSB2IC0xIGMgMCAtMS42NDQ1MzEgLTEuMzU1NDY5IC0zIC0zIC0zIHogbSAtNSA0IGMgLTAuNTUwNzgxIDAgLTEgMC40NDkyMTkgLTEgMSBzIDAuNDQ5MjE5IDEgMSAxIHMgMSAtMC40NDkyMTkgMSAtMSBzIC0wLjQ0OTIxOSAtMSAtMSAtMSB6IG0gLTUgMSB2IDQgaCAxIHYgLTQgeiBtIDEzIDAgdiA0IGggMSB2IC00IHogbSAtNC41IDIgbCAtMiAyIGwgLTEuNSAtMSBsIC0yIDIgdiAwLjUgYyAwIDAuNSAwLjUgMC41IDAuNSAwLjUgaCA3IHMgMC40NzI2NTYgLTAuMDM1MTU2IDAuNSAtMC41IHYgLTEgeiBtIC04LjUgMyB2IDEgYyAwIDEuNjQ0NTMxIDEuMzU1NDY5IDMgMyAzIGggMSB2IC0xIGggLTEgYyAtMS4xMDkzNzUgMCAtMiAtMC44OTA2MjUgLTIgLTIgdiAtMSB6IG0gMTMgMCB2IDEgYyAwIDEuMTA5Mzc1IC0wLjg5MDYyNSAyIC0yIDIgaCAtMSB2IDEgaCAxIGMgMS42NDQ1MzEgMCAzIC0xLjM1NTQ2OSAzIC0zIHYgLTEgeiBtIC04IDMgdiAxIGggNCB2IC0xIHogbSAwIDAiIGZpbGw9IiMyZTM0MzQiIGZpbGwtb3BhY2l0eT0iMC4zNDkwMiIvPg0KPC9zdmc+DQo=', 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgdmlld0JveD0iMCAwIDE2IDE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im0gNCAxIGMgLTEuNjQ0NTMxIDAgLTMgMS4zNTU0NjkgLTMgMyB2IDEgaCAxIHYgLTEgYyAwIC0xLjEwOTM3NSAwLjg5MDYyNSAtMiAyIC0yIGggMSB2IC0xIHogbSAyIDAgdiAxIGggNCB2IC0xIHogbSA1IDAgdiAxIGggMSBjIDEuMTA5Mzc1IDAgMiAwLjg5MDYyNSAyIDIgdiAxIGggMSB2IC0xIGMgMCAtMS42NDQ1MzEgLTEuMzU1NDY5IC0zIC0zIC0zIHogbSAtNSA0IGMgLTAuNTUwNzgxIDAgLTEgMC40NDkyMTkgLTEgMSBzIDAuNDQ5MjE5IDEgMSAxIHMgMSAtMC40NDkyMTkgMSAtMSBzIC0wLjQ0OTIxOSAtMSAtMSAtMSB6IG0gLTUgMSB2IDQgaCAxIHYgLTQgeiBtIDEzIDAgdiA0IGggMSB2IC00IHogbSAtNC41IDIgbCAtMiAyIGwgLTEuNSAtMSBsIC0yIDIgdiAwLjUgYyAwIDAuNSAwLjUgMC41IDAuNSAwLjUgaCA3IHMgMC40NzI2NTYgLTAuMDM1MTU2IDAuNSAtMC41IHYgLTEgeiBtIC04LjUgMyB2IDEgYyAwIDEuNjQ0NTMxIDEuMzU1NDY5IDMgMyAzIGggMSB2IC0xIGggLTEgYyAtMS4xMDkzNzUgMCAtMiAtMC44OTA2MjUgLTIgLTIgdiAtMSB6IG0gMTMgMCB2IDEgYyAwIDEuMTA5Mzc1IC0wLjg5MDYyNSAyIC0yIDIgaCAtMSB2IDEgaCAxIGMgMS42NDQ1MzEgMCAzIC0xLjM1NTQ2OSAzIC0zIHYgLTEgeiBtIC04IDMgdiAxIGggNCB2IC0xIHogbSAwIDAiIGZpbGw9IiMyZTM0MzQiIGZpbGwtb3BhY2l0eT0iMC4zNDkwMiIvPg0KPC9zdmc+DQo=',
getBlobRef: async (file, name, size) => await getBlobRef(undefined, file, name, size), getBlobRef: async (file, name, size) => await getBlobRef(file, name, size),
HTMLAttributes: this.options.image?.HTMLAttributes ?? {}, HTMLAttributes: this.options.image?.HTMLAttributes ?? {},
...this.options.image ...this.options.image
} }

View File

@ -12,14 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import core, { import { collaborativeDocParse, type Blob, type CollaborativeDoc, type Ref } from '@hcengineering/core'
collaborativeDocParse, import { getFileUrl } from '@hcengineering/presentation'
type Blob,
type BlobLookup,
type CollaborativeDoc,
type Ref
} from '@hcengineering/core'
import { getBlobHref, getClient } from '@hcengineering/presentation'
import { ObservableV2 as Observable } from 'lib0/observable' import { ObservableV2 as Observable } from 'lib0/observable'
import { applyUpdate, type Doc as YDoc } from 'yjs' import { applyUpdate, type Doc as YDoc } from 'yjs'
@ -39,11 +33,7 @@ async function fetchContent (blob: Ref<Blob>, doc: YDoc): Promise<boolean> {
async function fetchBlobContent (_id: Ref<Blob>): Promise<Uint8Array | undefined> { async function fetchBlobContent (_id: Ref<Blob>): Promise<Uint8Array | undefined> {
try { try {
const blob = (await getClient().findOne(core.class.Blob, { _id })) as BlobLookup const href = getFileUrl(_id)
if (blob === undefined || blob.size === 0) {
return undefined
}
const href = await getBlobHref(blob, _id)
const res = await fetch(href) const res = await fetch(href)
if (res.ok) { if (res.ok) {

View File

@ -88,6 +88,7 @@
mode: 'browser' mode: 'browser'
}} }}
/> />
<Scroller {fade} horizontal={true}> <Scroller {fade} horizontal={true}>
<Table <Table
bind:this={table} bind:this={table}

View File

@ -16,11 +16,11 @@
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { CircleButton, Progress } from '@hcengineering/ui' import { CircleButton, Progress } from '@hcengineering/ui'
import { getBlobSrcFor } from '@hcengineering/presentation' import { getFileUrl } from '@hcengineering/presentation'
import Pause from '../icons/Pause.svelte' import Pause from '../icons/Pause.svelte'
import Play from '../icons/Play.svelte' import Play from '../icons/Play.svelte'
export let value: Blob | Ref<Blob> export let value: Ref<Blob>
export let name: string export let name: string
export let contentType: string export let contentType: string
export let fullSize = false export let fullSize = false
@ -49,9 +49,7 @@
</div> </div>
<audio bind:duration bind:currentTime={time} bind:paused> <audio bind:duration bind:currentTime={time} bind:paused>
{#await getBlobSrcFor(value, name) then src} <source src={getFileUrl(value, name)} type={contentType} />
<source {src} type={contentType} />
{/await}
</audio> </audio>
<style lang="scss"> <style lang="scss">

View File

@ -14,13 +14,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { type BlobMetadata } from '@hcengineering/presentation'
import AudioPlayer from './AudioPlayer.svelte' import AudioPlayer from './AudioPlayer.svelte'
export let value: Blob | Ref<Blob> export let value: Ref<Blob>
export let name: string export let name: string
export let contentType: string export let contentType: string
export let metadata: BlobMetadata | undefined
</script> </script>
<AudioPlayer {value} {name} {contentType} fullSize={true} /> <AudioPlayer {value} {name} {contentType} fullSize={true} />

View File

@ -17,12 +17,12 @@
import { getBlobRef, type BlobMetadata } from '@hcengineering/presentation' import { getBlobRef, type BlobMetadata } from '@hcengineering/presentation'
import { Loading } from '@hcengineering/ui' import { Loading } from '@hcengineering/ui'
export let value: Blob | Ref<Blob> export let value: Ref<Blob>
export let name: string export let name: string
export let metadata: BlobMetadata | undefined export let metadata: BlobMetadata | undefined
export let fit: boolean = false export let fit: boolean = false
$: p = typeof value === 'string' ? getBlobRef(undefined, value, name) : getBlobRef(value, value._id) $: p = getBlobRef(value, name)
$: width = metadata?.originalWidth ? `min(${metadata.originalWidth / metadata?.pixelRatio ?? 1}px, 100%)` : '100%' $: width = metadata?.originalWidth ? `min(${metadata.originalWidth / metadata?.pixelRatio ?? 1}px, 100%)` : '100%'
$: height = metadata?.originalHeight $: height = metadata?.originalHeight
? `min(${metadata.originalHeight / metadata?.pixelRatio ?? 1}px, ${fit ? '100%' : '80vh'})` ? `min(${metadata.originalHeight / metadata?.pixelRatio ?? 1}px, ${fit ? '100%' : '80vh'})`

View File

@ -14,17 +14,14 @@
--> -->
<script lang="ts"> <script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { getBlobSrcFor, type BlobMetadata } from '@hcengineering/presentation' import { getFileUrl } from '@hcengineering/presentation'
export let value: Blob | Ref<Blob> export let value: Ref<Blob>
export let name: string export let name: string
export let metadata: BlobMetadata | undefined
export let fit: boolean = false export let fit: boolean = false
</script> </script>
{#await getBlobSrcFor(value, name) then href} <iframe class:fit src={getFileUrl(value, name) + '#view=FitH&navpanes=0'} title={name} />
<iframe class:fit src={href + '#view=FitH&navpanes=0'} title={name} />
{/await}
<style lang="scss"> <style lang="scss">
iframe { iframe {

View File

@ -14,25 +14,20 @@
--> -->
<script lang="ts"> <script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { getBlobSrcFor, type BlobMetadata } from '@hcengineering/presentation' import { getFileUrl, type BlobMetadata } from '@hcengineering/presentation'
export let value: Blob | Ref<Blob> export let value: Ref<Blob>
export let name: string export let name: string
export let metadata: BlobMetadata | undefined export let metadata: BlobMetadata | undefined
export let fit: boolean = false export let fit: boolean = false
$: maxWidth = metadata?.originalWidth ? `min(${metadata.originalWidth}px, 100%)` : undefined $: maxWidth = metadata?.originalWidth ? `min(${metadata.originalWidth}px, 100%)` : undefined
$: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined $: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined
$: src = getFileUrl(value, name)
</script> </script>
{#await getBlobSrcFor(value, name) then src} <video style:max-width={fit ? '100%' : maxWidth} style:max-height={fit ? '100%' : maxHeight} controls preload={'auto'}>
<video <source {src} />
style:max-width={fit ? '100%' : maxWidth} <track kind="captions" label={name} />
style:max-height={fit ? '100%' : maxHeight} </video>
controls
preload={'auto'}
>
<source {src} />
<track kind="captions" label={name} />
</video>
{/await}

View File

@ -12,7 +12,6 @@ describe('aggregator tests', () => {
ws1: WorkspaceId ws1: WorkspaceId
} { } {
const mem1 = new MemStorageAdapter() const mem1 = new MemStorageAdapter()
mem1.contentTypes = ['application/ydoc']
const mem2 = new MemStorageAdapter() const mem2 = new MemStorageAdapter()
const adapters = new Map<string, StorageAdapter>() const adapters = new Map<string, StorageAdapter>()
@ -25,19 +24,6 @@ describe('aggregator tests', () => {
const ws1: WorkspaceId = { name: 'ws1', productId: '' } const ws1: WorkspaceId = { name: 'ws1', productId: '' }
return { mem1, mem2, aggr, ws1, testCtx } return { mem1, mem2, aggr, ws1, testCtx }
} }
it('choose a proper storage', async () => {
const { aggr, ws1, testCtx } = prepare1()
// Test default provider
await aggr.put(testCtx, ws1, 'test', 'data', 'text/plain')
const stat = await aggr.stat(testCtx, ws1, 'test')
expect(stat?.provider).toEqual('mem2')
// Test content typed provider
await aggr.put(testCtx, ws1, 'test2', 'data', 'application/ydoc')
const stat2 = await aggr.stat(testCtx, ws1, 'test2')
expect(stat2?.provider).toEqual('mem1')
})
it('reuse existing storage', async () => { it('reuse existing storage', async () => {
const { mem1, aggr, ws1, testCtx } = prepare1() const { mem1, aggr, ws1, testCtx } = prepare1()

View File

@ -3,9 +3,7 @@ import core, {
ModelDb, ModelDb,
TxProcessor, TxProcessor,
toFindResult, toFindResult,
type Branding,
type Blob, type Blob,
type BlobLookup,
type Class, type Class,
type Doc, type Doc,
type DocumentQuery, type DocumentQuery,
@ -15,22 +13,14 @@ import core, {
type FindResult, type FindResult,
type MeasureContext, type MeasureContext,
type Ref, type Ref,
type WorkspaceId, type WorkspaceId
type WorkspaceIdWithUrl
} from '@hcengineering/core' } from '@hcengineering/core'
import { genMinModel } from '@hcengineering/core/src/__tests__/minmodel' import { genMinModel } from '@hcengineering/core/src/__tests__/minmodel'
import type { import type { BlobStorageIterator, BucketInfo, StorageAdapter, UploadedObjectInfo } from '@hcengineering/storage'
BlobLookupResult,
BlobStorageIterator,
BucketInfo,
StorageAdapter,
UploadedObjectInfo
} from '@hcengineering/storage'
import { Readable } from 'stream' import { Readable } from 'stream'
import type { RawDBAdapter, RawDBAdapterStream } from '../adapter' import type { RawDBAdapter, RawDBAdapterStream } from '../adapter'
export class MemStorageAdapter implements StorageAdapter { export class MemStorageAdapter implements StorageAdapter {
contentTypes?: string[] | undefined
files = new Map<string, Blob & { content: Buffer, workspace: string }>() files = new Map<string, Blob & { content: Buffer, workspace: string }>()
async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {} async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {}
@ -157,15 +147,6 @@ export class MemStorageAdapter implements StorageAdapter {
// Partial are not supported by // Partial are not supported by
throw new Error('NoSuchKey') throw new Error('NoSuchKey')
} }
async lookup (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
): Promise<BlobLookupResult> {
return { lookups: docs as unknown as BlobLookup[] }
}
} }
export class MemRawDBAdapter implements RawDBAdapter { export class MemRawDBAdapter implements RawDBAdapter {

View File

@ -4,19 +4,15 @@ import core, {
toIdMap, toIdMap,
withContext, withContext,
type Blob, type Blob,
type BlobLookup,
type Branding,
type MeasureContext, type MeasureContext,
type Ref, type Ref,
type StorageIterator, type StorageIterator,
type WorkspaceId, type WorkspaceId
type WorkspaceIdWithUrl
} from '@hcengineering/core' } from '@hcengineering/core'
import { type Readable } from 'stream' import { type Readable } from 'stream'
import { type RawDBAdapter } from '../adapter' import { type RawDBAdapter } from '../adapter'
import { import {
type BlobLookupResult,
type BlobStorageIterator, type BlobStorageIterator,
type BucketInfo, type BucketInfo,
type ListBlobResult, type ListBlobResult,
@ -308,16 +304,6 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
provider: forceProvider provider: forceProvider
} }
} }
// try select provider based on content type matching.
for (const [provider, adapter] of this.adapters.entries()) {
if (adapter.contentTypes === undefined) {
continue
}
if (adapter.contentTypes.some((it) => contentType.includes(it))) {
// we have matched content type for adapter.
return { adapter, provider }
}
}
return { adapter: this.adapters.get(this.defaultAdapter) as StorageAdapter, provider: this.defaultAdapter } return { adapter: this.adapters.get(this.defaultAdapter) as StorageAdapter, provider: this.defaultAdapter }
} }
@ -367,31 +353,6 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
await this.dbAdapter.upload<Blob>(ctx, workspaceId, DOMAIN_BLOB, [blobDoc]) await this.dbAdapter.upload<Blob>(ctx, workspaceId, DOMAIN_BLOB, [blobDoc])
return result return result
} }
@withContext('aggregator-lookup', {})
async lookup (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
): Promise<BlobLookupResult> {
const result: BlobLookup[] = []
const byProvider = groupByArray(docs, (it) => it.provider)
for (const [k, v] of byProvider.entries()) {
const provider = this.adapters.get(k)
if (provider?.lookup !== undefined) {
const upd = await provider.lookup(ctx, workspaceId, branding, v)
if (upd.updates !== undefined) {
await this.dbAdapter.update(ctx, workspaceId, DOMAIN_BLOB, upd.updates)
}
result.push(...upd.lookups)
}
}
// Check if we need to perform diff update for blobs
return { lookups: result }
}
} }
/** /**

View File

@ -484,8 +484,6 @@ export interface StorageConfig {
kind: string kind: string
endpoint: string endpoint: string
port?: number port?: number
contentTypes?: string[]
} }
export interface StorageConfiguration { export interface StorageConfiguration {

View File

@ -32,7 +32,7 @@ A `;` separated list of triples, providerName|previewUrl|supportedFormats.
- supportedFormats - a `,` separated list of file extensions. - supportedFormats - a `,` separated list of file extensions.
- contentTypes - a ',' separated list of content type patterns. - contentTypes - a ',' separated list of content type patterns.
PREVIEW_CONFIG=*|https://front.hc.engineering/files/:workspace/api/preview/?width=:size&image=:downloadFile PREVIEW_CONFIG=https://front.hc.engineering/files/:workspace/api/preview/?width=:size&image=:downloadFile
## Variables ## Variables

View File

@ -1,120 +0,0 @@
//
// Copyright © 2022 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.
//
import core, {
Class,
Doc,
DocumentQuery,
FindOptions,
FindResult,
MeasureContext,
Ref,
Tx,
toFindResult,
toIdMap,
type Blob,
type BlobLookup,
type WorkspaceIdWithUrl,
type Branding
} from '@hcengineering/core'
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
import { BaseMiddleware } from './base'
/**
* @public
*/
export class BlobLookupMiddleware extends BaseMiddleware implements Middleware {
private constructor (storage: ServerStorage, next?: Middleware) {
super(storage, next)
}
static async create (ctx: MeasureContext, storage: ServerStorage, next?: Middleware): Promise<BlobLookupMiddleware> {
return new BlobLookupMiddleware(storage, next)
}
async tx (ctx: SessionContext, tx: Tx): Promise<TxMiddlewareResult> {
return await this.provideTx(ctx, tx)
}
async fetchBlobInfo (
ctx: MeasureContext,
workspace: WorkspaceIdWithUrl,
branding: Branding | null,
toUpdate: [Doc, Blob, string][]
): Promise<void> {
if (this.storage.storageAdapter.lookup !== undefined) {
const docsToUpdate = toUpdate.map((it) => it[1])
const updatedBlobs = toIdMap<Blob>(
(await this.storage.storageAdapter.lookup(ctx, workspace, branding, docsToUpdate)).lookups
)
for (const [doc, blob, key] of toUpdate) {
const ublob = updatedBlobs.get(blob._id)
if (ublob !== undefined) {
;(doc as any).$lookup[key] = ublob
}
}
}
}
override async findAll<T extends Doc>(
ctx: SessionContext,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<FindResult<T>> {
const result = await this.provideFindAll(ctx, _class, query, options)
// Fill lookup map to make more compact representation
if (_class === core.class.Blob) {
// Bulk update of info
const updatedBlobs = toIdMap<Blob>(
(await this.storage.storageAdapter.lookup(ctx.ctx, ctx.workspace, ctx.branding, result as unknown as Blob[]))
.lookups
)
const res: T[] = []
for (const d of result) {
res.push((updatedBlobs.get(d._id as unknown as Ref<BlobLookup>) ?? d) as T)
}
return toFindResult(res, result.total, result.lookupMap)
}
if (options?.lookup !== undefined && this.storage.storageAdapter.lookup !== undefined) {
// Check if $lookups has core.class.Blob as object, and we need to enhance them
let toUpdate: [Doc, Blob, string][] = []
for (const d of result) {
if (d.$lookup !== undefined) {
for (const [k, v] of Object.entries(d.$lookup)) {
if (v !== undefined && !Array.isArray(v) && v._class === core.class.Blob) {
toUpdate.push([d, v, k])
}
}
}
if (toUpdate.length > 50) {
// Bulk update of info
await this.fetchBlobInfo(ctx.ctx, ctx.workspace, ctx.branding, toUpdate)
toUpdate = []
}
}
if (toUpdate.length > 0) {
// Bulk update of info
await this.fetchBlobInfo(ctx.ctx, ctx.workspace, ctx.branding, toUpdate)
toUpdate = []
}
}
return result
}
}

View File

@ -21,4 +21,3 @@ export * from './queryJoin'
export * from './spaceSecurity' export * from './spaceSecurity'
export * from './spacePermissions' export * from './spacePermissions'
export * from './lookup' export * from './lookup'
export * from './blobs'

View File

@ -16,22 +16,16 @@
import { Client, type BucketItem, type BucketStream } from 'minio' import { Client, type BucketItem, type BucketStream } from 'minio'
import core, { import core, {
concatLink,
toWorkspaceString, toWorkspaceString,
withContext, withContext,
type Blob, type Blob,
type BlobLookup,
type MeasureContext, type MeasureContext,
type Ref, type Ref,
type WorkspaceId, type WorkspaceId
type WorkspaceIdWithUrl,
type Branding
} from '@hcengineering/core' } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform' import {
import serverCore, {
removeAllObjects, removeAllObjects,
type BlobLookupResult,
type BlobStorageIterator, type BlobStorageIterator,
type BucketInfo, type BucketInfo,
type ListBlobResult, type ListBlobResult,
@ -62,7 +56,6 @@ export interface MinioConfig extends StorageConfig {
export class MinioService implements StorageAdapter { export class MinioService implements StorageAdapter {
static config = 'minio' static config = 'minio'
client: Client client: Client
contentTypes?: string[]
constructor (readonly opt: MinioConfig) { constructor (readonly opt: MinioConfig) {
this.client = new Client({ this.client = new Client({
endPoint: opt.endpoint, endPoint: opt.endpoint,
@ -72,26 +65,10 @@ export class MinioService implements StorageAdapter {
port: opt.port ?? 9000, port: opt.port ?? 9000,
useSSL: opt.useSSL === 'true' useSSL: opt.useSSL === 'true'
}) })
this.contentTypes = opt.contentTypes
} }
async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {} async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {}
async lookup (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
): Promise<BlobLookupResult> {
const frontUrl = branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
for (const d of docs) {
// Let's add current from URI for previews.
const bl = d as BlobLookup
bl.downloadUrl = concatLink(frontUrl, `/files/${workspaceId.workspaceUrl}?file=${d._id}`)
}
return { lookups: docs as BlobLookup[] }
}
/** /**
* @public * @public
*/ */

View File

@ -13,20 +13,16 @@
// limitations under the License. // limitations under the License.
// //
import { CopyObjectCommand, GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3' import { CopyObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage' import { Upload } from '@aws-sdk/lib-storage'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import core, { import core, {
toWorkspaceString, toWorkspaceString,
withContext, withContext,
type Blob, type Blob,
type BlobLookup,
type Branding,
type MeasureContext, type MeasureContext,
type Ref, type Ref,
type WorkspaceId, type WorkspaceId
type WorkspaceIdWithUrl
} from '@hcengineering/core' } from '@hcengineering/core'
import { import {
@ -39,7 +35,7 @@ import {
} from '@hcengineering/server-core' } from '@hcengineering/server-core'
import { Readable } from 'stream' import { Readable } from 'stream'
import { removeAllObjects, type BlobLookupResult, type BucketInfo } from '@hcengineering/storage' import { removeAllObjects, type BucketInfo } from '@hcengineering/storage'
import type { ReadableStream } from 'stream/web' import type { ReadableStream } from 'stream/web'
export interface S3Config extends StorageConfig { export interface S3Config extends StorageConfig {
@ -67,7 +63,6 @@ export class S3Service implements StorageAdapter {
static config = 's3' static config = 's3'
expireTime: number expireTime: number
client: S3 client: S3
contentTypes?: string[]
constructor (readonly opt: S3Config) { constructor (readonly opt: S3Config) {
this.client = new S3({ this.client = new S3({
endpoint: opt.endpoint, endpoint: opt.endpoint,
@ -79,50 +74,10 @@ export class S3Service implements StorageAdapter {
}) })
this.expireTime = parseInt(this.opt.expireTime ?? '168') * 3600 // use 7 * 24 - hours as default value for expireF this.expireTime = parseInt(this.opt.expireTime ?? '168') * 3600 // use 7 * 24 - hours as default value for expireF
this.contentTypes = opt.contentTypes
} }
async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {} async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {}
async lookup (
ctx: MeasureContext,
workspaceId: WorkspaceIdWithUrl,
branding: Branding | null,
docs: Blob[]
): Promise<BlobLookupResult> {
const result: BlobLookupResult = {
lookups: [],
updates: new Map()
}
const now = Date.now()
for (const d of docs) {
// Let's add current from URI for previews.
const bl = d as BlobLookup
const command = new GetObjectCommand({
Bucket: this.getBucketId(workspaceId),
Key: this.getDocumentKey(workspaceId, d.storageId),
ResponseCacheControl: 'max-age=9d'
})
if (
(bl.downloadUrl === undefined || (bl.downloadUrlExpire ?? 0) < now) &&
(this.opt.allowPresign ?? 'true') === 'true'
) {
bl.downloadUrl = await getSignedUrl(this.client, command, {
expiresIn: this.expireTime
})
bl.downloadUrlExpire = now + this.expireTime * 1000
result.updates?.set(bl._id, {
downloadUrl: bl.downloadUrl,
downloadUrlExpire: bl.downloadUrlExpire
})
}
result.lookups.push(bl)
}
// this.client.presignedUrl(httpMethod, bucketName, objectName, callback)
return result
}
/** /**
* @public * @public
*/ */

View File

@ -10,7 +10,6 @@ import {
} from '@hcengineering/core' } from '@hcengineering/core'
import { createElasticAdapter, createElasticBackupDataAdapter } from '@hcengineering/elastic' import { createElasticAdapter, createElasticBackupDataAdapter } from '@hcengineering/elastic'
import { import {
BlobLookupMiddleware,
ConfigurationMiddleware, ConfigurationMiddleware,
LookupMiddleware, LookupMiddleware,
ModifiedMiddleware, ModifiedMiddleware,
@ -58,7 +57,6 @@ export function createServerPipeline (
): PipelineFactory { ): PipelineFactory {
const middlewares: MiddlewareCreator[] = [ const middlewares: MiddlewareCreator[] = [
LookupMiddleware.create, LookupMiddleware.create,
BlobLookupMiddleware.create,
ModifiedMiddleware.create, ModifiedMiddleware.create,
PrivateMiddleware.create, PrivateMiddleware.create,
SpaceSecurityMiddleware.create, SpaceSecurityMiddleware.create,

View File

@ -50,7 +50,7 @@ export function parseStorageEnv (storageEnv: string, storageConfig: StorageConfi
if (st.trim().length === 0 || !st.includes('|')) { if (st.trim().length === 0 || !st.includes('|')) {
throw new Error('Invalid storage config:' + st) throw new Error('Invalid storage config:' + st)
} }
let [kindName, url, contentTypes] = st.split('|') let [kindName, url] = st.split('|')
let [kind, name] = kindName.split(',') let [kind, name] = kindName.split(',')
if (name == null) { if (name == null) {
name = kind name = kind
@ -66,8 +66,7 @@ export function parseStorageEnv (storageEnv: string, storageConfig: StorageConfi
kind, kind,
name, name,
endpoint: (hasProtocol ? uri.protocol + '//' : '') + uri.hostname, // Port should go away endpoint: (hasProtocol ? uri.protocol + '//' : '') + uri.hostname, // Port should go away
port: uri.port !== '' ? parseInt(uri.port) : undefined, port: uri.port !== '' ? parseInt(uri.port) : undefined
contentTypes: contentTypes !== undefined ? contentTypes.split(',') : undefined
} }
// Add all extra parameters // Add all extra parameters

View File

@ -57,19 +57,4 @@ describe('config-parse', () => {
expect(minio.secretKey).toEqual('minio2') expect(minio.secretKey).toEqual('minio2')
expect((minio as any).downloadUrl).toEqual('https://front.hc.engineering') expect((minio as any).downloadUrl).toEqual('https://front.hc.engineering')
}) })
it('test-decode unexpected symbols', async () => {
const cfg: StorageConfiguration = { default: '', storages: [] }
parseStorageEnv(
'minio|localhost:9000?accessKey=%F0%9F%91%85%F0%9F%91%BB%20-%20%D0%AD%D0%A2%D0%9E%20%20%20%20%D1%82%D0%B0%D0%BA%D0%BE%D0%B9%20%D0%BF%D0%B0%D1%80%D0%BE%D0%BB%D1%8C%0A%D0%90%20%D1%82%D0%BE&secretKey=minio2&downloadUrl=https%3A%2F%2Ffront.hc.engineering|image/jpeg,image/gif',
cfg
)
expect(cfg.default).toEqual('minio')
const minio = cfg.storages[0] as MinioConfig
expect(minio.endpoint).toEqual('localhost')
expect(minio.port).toEqual(9000)
expect(minio.accessKey).toEqual('👅👻 - ЭТО такой пароль\nА то')
expect(minio.secretKey).toEqual('minio2')
expect(minio.contentTypes).toEqual(['image/jpeg', 'image/gif'])
expect((minio as any).downloadUrl).toEqual('https://front.hc.engineering')
})
}) })