mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-7776: Get rid of blobs in UI (#6226)
This commit is contained in:
parent
fbcc59011d
commit
b5ee82cf1a
@ -13,18 +13,18 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 presentation from '../plugin'
|
||||
|
||||
import { getPreviewType, previewTypes } from '../file'
|
||||
import { BlobMetadata, FilePreviewExtension } from '../types'
|
||||
import { getFileUrl } from '../utils'
|
||||
|
||||
import { getBlobSrcFor } from '../preview'
|
||||
|
||||
export let file: Blob
|
||||
export let file: Ref<Blob>
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
export let props: Record<string, any> = {}
|
||||
export let fit: boolean = false
|
||||
@ -35,7 +35,7 @@
|
||||
$: parentHeight = ($deviceInfo.docHeight * 80) / 100
|
||||
|
||||
let previewType: FilePreviewExtension | undefined = undefined
|
||||
$: void getPreviewType(file.contentType, $previewTypes).then((res) => {
|
||||
$: void getPreviewType(contentType, $previewTypes).then((res) => {
|
||||
previewType = res
|
||||
})
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
}
|
||||
$: updateHeight(parentWidth, parentHeight, previewType, metadata)
|
||||
$: audio = previewType && Array.isArray(previewType) && previewType[0].contentType === 'audio/*'
|
||||
$: srcRef = getBlobSrcFor(file, name)
|
||||
$: srcRef = getFileUrl(file, name)
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -90,10 +90,7 @@
|
||||
<Label label={presentation.string.FailedToPreview} />
|
||||
</div>
|
||||
{:else if previewType !== undefined}
|
||||
<Component
|
||||
is={previewType.component}
|
||||
props={{ value: file, name, contentType: file.contentType, metadata, ...props, fit }}
|
||||
/>
|
||||
<Component is={previewType.component} props={{ value: file, name, contentType, metadata, ...props, fit }} />
|
||||
{:else}
|
||||
<div class="flex-col items-center flex-gap-3">
|
||||
<Label label={presentation.string.ContentTypeNotSupported} />
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { Button, Dialog, tooltip } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
@ -21,15 +21,15 @@
|
||||
import presentation from '../plugin'
|
||||
|
||||
import { BlobMetadata } from '../types'
|
||||
import { getClient } from '../utils'
|
||||
import { getBlobSrcFor } from '../preview'
|
||||
import { getClient, getFileUrl } from '../utils'
|
||||
|
||||
import ActionContext from './ActionContext.svelte'
|
||||
import FilePreview from './FilePreview.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 contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
@ -53,14 +53,7 @@
|
||||
return ext.substring(0, 4).toUpperCase()
|
||||
}
|
||||
|
||||
let blob: Blob | undefined = 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)
|
||||
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
|
||||
</script>
|
||||
|
||||
<ActionContext context={{ mode: 'browser' }} />
|
||||
@ -101,8 +94,8 @@
|
||||
{/await}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if blob !== undefined}
|
||||
<FilePreview file={blob} {name} {metadata} {props} fit />
|
||||
{#if file}
|
||||
<FilePreview {file} {contentType} {name} {metadata} {props} fit />
|
||||
{/if}
|
||||
</Dialog>
|
||||
|
||||
|
@ -17,11 +17,11 @@
|
||||
import type { Blob, Ref } from '@hcengineering/core'
|
||||
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import presentation, { getBlobSrcFor } from '..'
|
||||
import presentation, { getFileUrl } from '..'
|
||||
import ActionContext from './ActionContext.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 contentType: string | undefined
|
||||
// export let popupOptions: PopupOptions
|
||||
@ -45,7 +45,7 @@
|
||||
})
|
||||
let download: HTMLAnchorElement
|
||||
|
||||
$: srcRef = getBlobSrcFor(file, name)
|
||||
$: srcRef = file !== undefined ? getFileUrl(file, name) : undefined
|
||||
|
||||
$: isImage = contentType !== undefined && contentType.startsWith('image/')
|
||||
|
||||
|
@ -1,99 +1,36 @@
|
||||
import type { Blob, BlobLookup, Ref } from '@hcengineering/core'
|
||||
import core, { concatLink } from '@hcengineering/core'
|
||||
import type { Blob, Ref } from '@hcengineering/core'
|
||||
import { concatLink } from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { getBlobHref, getClient, getCurrentWorkspaceUrl, getFileUrl } from '.'
|
||||
import { getCurrentWorkspaceUrl, getFileUrl } from '.'
|
||||
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 {
|
||||
default?: ProviderPreviewConfig
|
||||
previewers: Record<string, ProviderPreviewConfig[]>
|
||||
previewUrl: string
|
||||
}
|
||||
|
||||
const defaultPreview = (): ProviderPreviewConfig => ({
|
||||
providerId: '',
|
||||
previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size`
|
||||
})
|
||||
const defaultPreview = (): string => `/files/${getCurrentWorkspaceUrl()}?file=:blobId&size=:size`
|
||||
|
||||
/**
|
||||
*
|
||||
* PREVIEW_CONFIG env variable format.
|
||||
* A `;` separated list of triples, providerName|previewUrl|supportedFormats.
|
||||
|
||||
- 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.
|
||||
|
||||
* previewUrl - an Url with :workspace, :blobId, :downloadFile, :size placeholders, they will be replaced in UI with an appropriate blob values.
|
||||
*/
|
||||
export function parsePreviewConfig (config?: string): PreviewConfig | undefined {
|
||||
if (config === undefined) {
|
||||
return
|
||||
}
|
||||
const result: PreviewConfig = { previewers: {} }
|
||||
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
|
||||
return { previewUrl: config }
|
||||
}
|
||||
|
||||
export function getPreviewConfig (): PreviewConfig {
|
||||
return (
|
||||
(getMetadata(presentation.metadata.PreviewConfig) as PreviewConfig) ?? {
|
||||
default: defaultPreview(),
|
||||
previewers: {
|
||||
'': [
|
||||
{
|
||||
providerId: '',
|
||||
contentTypes: ['image/gif', 'image/apng', 'image/svg'], // Disable gif and apng format preview.
|
||||
previewUrl: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
previewUrl: defaultPreview()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function getBlobRef (
|
||||
blob: Blob | undefined,
|
||||
file: Ref<Blob>,
|
||||
name?: string,
|
||||
width?: number
|
||||
@ -101,74 +38,30 @@ export async function getBlobRef (
|
||||
src: string
|
||||
srcset: string
|
||||
}> {
|
||||
let _blob = blob as BlobLookup
|
||||
if (_blob === undefined) {
|
||||
_blob = (await getClient().findOne(core.class.Blob, { _id: file })) as BlobLookup
|
||||
}
|
||||
return {
|
||||
src: _blob?.downloadUrl ?? getFileUrl(file, name),
|
||||
srcset: _blob !== undefined ? getSrcSet(_blob, width) : ''
|
||||
src: getFileUrl(file, name),
|
||||
srcset: getSrcSet(file, width)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBlobSrcSet (_blob: Blob | undefined, file: Ref<Blob>, width?: number): Promise<string> {
|
||||
if (_blob === undefined) {
|
||||
_blob = await getClient().findOne(core.class.Blob, { _id: file })
|
||||
}
|
||||
return _blob !== undefined ? getSrcSet(_blob, width) : ''
|
||||
export async function getBlobSrcSet (file: Ref<Blob>, width?: number): Promise<string> {
|
||||
return getSrcSet(file, width)
|
||||
}
|
||||
|
||||
/**
|
||||
* Select content provider based on content type.
|
||||
*/
|
||||
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: Ref<Blob>, width?: number): string {
|
||||
return blobToSrcSet(getPreviewConfig(), _blob, width)
|
||||
}
|
||||
|
||||
export function getSrcSet (_blob: Blob, width?: number): 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 {
|
||||
function blobToSrcSet (cfg: PreviewConfig, blob: Ref<Blob>, width: number | undefined): string {
|
||||
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
|
||||
if (!url.includes('://')) {
|
||||
url = concatLink(frontUrl ?? '', url)
|
||||
}
|
||||
url = url.replaceAll(':downloadFile', encodeURIComponent(downloadUrl))
|
||||
url = url.replaceAll(':blobId', encodeURIComponent(blob._id))
|
||||
url = url.replaceAll(':blobId', encodeURIComponent(blob))
|
||||
|
||||
let result = ''
|
||||
const fu = url
|
||||
@ -187,24 +80,9 @@ function blobToSrcSet (
|
||||
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.
|
||||
*/
|
||||
export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string {
|
||||
const cfg = getPreviewConfig()
|
||||
return blobToSrcSet(
|
||||
cfg.default ?? defaultPreview(),
|
||||
{
|
||||
_id: _blob
|
||||
},
|
||||
width
|
||||
)
|
||||
return blobToSrcSet(getPreviewConfig(), _blob, width)
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ import core, {
|
||||
type AnyAttribute,
|
||||
type ArrOf,
|
||||
type AttachedDoc,
|
||||
type BlobLookup,
|
||||
type Class,
|
||||
type Client,
|
||||
type Collection,
|
||||
@ -450,18 +449,6 @@ export function createQuery (dontDestroy?: boolean): LiveQuery {
|
||||
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 {
|
||||
const wsId = get(workspaceId)
|
||||
if (wsId == null) {
|
||||
|
@ -13,17 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
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 Blob, type MeasureContext, type StorageIterator, type WorkspaceId } from '@hcengineering/core'
|
||||
import { type Readable } from 'stream'
|
||||
|
||||
export type ListBlobResult = Omit<Blob, 'contentType' | 'version'>
|
||||
@ -38,11 +28,6 @@ export interface BlobStorageIterator {
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface BlobLookupResult {
|
||||
lookups: BlobLookup[]
|
||||
updates?: Map<Ref<Blob>, DocumentUpdate<BlobLookup>>
|
||||
}
|
||||
|
||||
export interface BucketInfo {
|
||||
name: string
|
||||
delete: () => Promise<void>
|
||||
@ -50,11 +35,6 @@ export interface BucketInfo {
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
close: () => Promise<void>
|
||||
@ -84,14 +64,6 @@ export interface StorageAdapter {
|
||||
offset: number,
|
||||
length?: number
|
||||
) => 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 {
|
||||
@ -189,15 +161,6 @@ export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx {
|
||||
): Promise<UploadedObjectInfo> {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
async lookup (
|
||||
ctx: MeasureContext,
|
||||
workspaceId: WorkspaceIdWithUrl,
|
||||
branding: Branding | null,
|
||||
docs: Blob[]
|
||||
): Promise<BlobLookupResult> {
|
||||
return { lookups: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export function createDummyStorageAdapter (): StorageAdapter {
|
||||
|
@ -18,7 +18,7 @@
|
||||
import {
|
||||
FilePreviewPopup,
|
||||
canPreviewFile,
|
||||
getBlobHref,
|
||||
getFileUrl,
|
||||
getPreviewAlignment,
|
||||
previewTypes
|
||||
} from '@hcengineering/presentation'
|
||||
@ -71,7 +71,8 @@
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: attachment.$lookup?.file ?? attachment.file,
|
||||
file: attachment.file,
|
||||
contentType: attachment.type,
|
||||
name: attachment.name,
|
||||
metadata: attachment.metadata
|
||||
},
|
||||
@ -130,32 +131,30 @@
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
|
||||
<a
|
||||
class="mr-1 flex-row-center gap-2 p-1"
|
||||
{href}
|
||||
download={attachment.name}
|
||||
bind:this={download}
|
||||
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{#if canPreview}
|
||||
<ActionIcon
|
||||
icon={IconOpen}
|
||||
size={'medium'}
|
||||
action={(evt) => {
|
||||
showPreview(evt)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<a
|
||||
class="mr-1 flex-row-center gap-2 p-1"
|
||||
href={getFileUrl(attachment.file, attachment.name)}
|
||||
download={attachment.name}
|
||||
bind:this={download}
|
||||
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{#if canPreview}
|
||||
<ActionIcon
|
||||
icon={FileDownload}
|
||||
icon={IconOpen}
|
||||
size={'medium'}
|
||||
action={() => {
|
||||
download.click()
|
||||
action={(evt) => {
|
||||
showPreview(evt)
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
{/await}
|
||||
{/if}
|
||||
<ActionIcon
|
||||
icon={FileDownload}
|
||||
size={'medium'}
|
||||
action={() => {
|
||||
download.click()
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
|
||||
</div>
|
||||
|
@ -48,11 +48,6 @@
|
||||
},
|
||||
(res) => {
|
||||
resAttachments = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
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 filesize from 'filesize'
|
||||
import { getType } from '../utils'
|
||||
@ -45,41 +45,27 @@
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
file: value.file,
|
||||
contentType: value.type,
|
||||
name: value.name,
|
||||
metadata: value.metadata
|
||||
},
|
||||
isImage(value.type) ? 'centered' : 'float'
|
||||
)
|
||||
}
|
||||
$: src = getFileUrl(value.file, value.name)
|
||||
</script>
|
||||
|
||||
<div class="gridCellOverlay">
|
||||
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then src}
|
||||
<div class="gridCell">
|
||||
{#if isImage(value.type)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="cellImagePreview" on:click={openAttachment}>
|
||||
<img class={'img-fit'} {src} alt={value.name} />
|
||||
</div>
|
||||
{:else}
|
||||
<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">
|
||||
<div class="gridCell">
|
||||
{#if isImage(value.type)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="cellImagePreview" on:click={openAttachment}>
|
||||
<img class={'img-fit'} {src} alt={value.name} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="cellMiscPreview">
|
||||
{#if isEmbedded(value.type)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
@ -91,24 +77,38 @@
|
||||
<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>
|
||||
{/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>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -107,7 +107,7 @@
|
||||
</script>
|
||||
|
||||
<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
|
||||
src={blobSrc.src}
|
||||
style:object-fit={getObjectFit(dimensions)}
|
||||
|
@ -43,11 +43,6 @@
|
||||
},
|
||||
(res) => {
|
||||
docs = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -83,7 +83,8 @@
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
file: value.file,
|
||||
contentType: value.type,
|
||||
name: value.name,
|
||||
metadata: value.metadata
|
||||
},
|
||||
@ -114,7 +115,7 @@
|
||||
{:else}
|
||||
<div class="flex-row-center attachment-container">
|
||||
{#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
|
||||
class="no-line"
|
||||
style:flex-shrink={0}
|
||||
|
@ -51,7 +51,7 @@
|
||||
if (listProvider !== undefined) listProvider.updateFocus(value)
|
||||
const popupInfo = showPopup(
|
||||
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'
|
||||
)
|
||||
dispatch('open', popupInfo.id)
|
||||
|
@ -121,11 +121,6 @@
|
||||
(res) => {
|
||||
originalAttachments = new Set(res.map((p) => p._id))
|
||||
attachments = toIdMap(res)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment, BlobMetadata } from '@hcengineering/attachment'
|
||||
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 {
|
||||
FileOrBlob,
|
||||
@ -25,7 +25,6 @@
|
||||
getFileMetadata,
|
||||
uploadFile
|
||||
} from '@hcengineering/presentation'
|
||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
import textEditor, { type RefAction, type TextEditorHandler } from '@hcengineering/text-editor'
|
||||
import {
|
||||
AttachIcon,
|
||||
@ -38,6 +37,7 @@
|
||||
import { AnySvelteComponent, getEventPositionElement, getPopupPositionElement, navigate } from '@hcengineering/ui'
|
||||
import { uploadFiles } from '@hcengineering/uploader'
|
||||
import view from '@hcengineering/view'
|
||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
|
||||
import AttachmentsGrid from './AttachmentsGrid.svelte'
|
||||
|
||||
@ -106,11 +106,6 @@
|
||||
},
|
||||
(res) => {
|
||||
attachments = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -127,11 +127,6 @@
|
||||
originalAttachments = new Set(res.map((p) => p._id))
|
||||
attachments = toIdMap(res)
|
||||
dispatch('attach', { action: 'saved', value: attachments.size })
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -14,9 +14,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
import { getBlobHref } from '@hcengineering/presentation'
|
||||
|
||||
import type { WithLookup } from '@hcengineering/core'
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
||||
|
||||
export let value: WithLookup<Attachment>
|
||||
@ -58,9 +58,7 @@
|
||||
</script>
|
||||
|
||||
<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={href} />
|
||||
{/await}
|
||||
<source src={getFileUrl(value.file, value.name)} />
|
||||
<track kind="captions" label={value.name} />
|
||||
<div class="container">
|
||||
<AttachmentPresenter {value} />
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
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 { AttachmentGalleryPresenter } from '..'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
@ -56,12 +56,11 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<AttachmentGalleryPresenter value={attachment}>
|
||||
<svelte:fragment slot="rowMenu">
|
||||
{@const href = getFileUrl(attachment.file, attachment.name)}
|
||||
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
|
||||
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
|
||||
<a {href} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
{/await}
|
||||
<a {href} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
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 { AttachmentPresenter } from '..'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
@ -51,16 +51,15 @@
|
||||
|
||||
<div class="flex-col">
|
||||
{#each attachments as attachment, i}
|
||||
{@const href = getFileUrl(attachment.file, attachment.name)}
|
||||
<div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}>
|
||||
<div class="item flex">
|
||||
<AttachmentPresenter value={attachment} />
|
||||
</div>
|
||||
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
|
||||
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
|
||||
<a {href} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
{/await}
|
||||
<a {href} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||
|
@ -14,11 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 { 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 fullSize = false
|
||||
@ -48,9 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<audio bind:duration bind:currentTime={time} bind:paused>
|
||||
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then href}
|
||||
<source src={href} type={value.type} />
|
||||
{/await}
|
||||
<source src={getFileUrl(value.file, value.name)} type={value.type} />
|
||||
</audio>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -92,10 +92,7 @@
|
||||
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
|
||||
{
|
||||
sort: sortModeToOptionObject(selectedSort_),
|
||||
limit: 200,
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
limit: 200
|
||||
}
|
||||
)
|
||||
isLoading = false
|
||||
|
@ -16,7 +16,7 @@
|
||||
// import { Doc } from '@hcengineering/core'
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
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 { createEventDispatcher, onMount } from 'svelte'
|
||||
import { getType } from '../utils'
|
||||
@ -40,7 +40,7 @@
|
||||
})
|
||||
let download: HTMLAnchorElement
|
||||
$: type = getType(value.type)
|
||||
$: srcRef = getBlobHref(value.$lookup?.file, value.file, value.name)
|
||||
$: srcRef = getFileUrl(value.file, value.name)
|
||||
</script>
|
||||
|
||||
<ActionContext context={{ mode: 'browser' }} />
|
||||
|
@ -17,14 +17,7 @@
|
||||
import { Photo } from '@hcengineering/attachment'
|
||||
import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core'
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import {
|
||||
FilePreviewPopup,
|
||||
createQuery,
|
||||
getBlobHref,
|
||||
getClient,
|
||||
uploadFile,
|
||||
getBlobRef
|
||||
} from '@hcengineering/presentation'
|
||||
import { FilePreviewPopup, createQuery, getBlobRef, getClient, uploadFile } from '@hcengineering/presentation'
|
||||
import { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui'
|
||||
import attachment from '../plugin'
|
||||
import UploadDuo from './icons/UploadDuo.svelte'
|
||||
@ -99,7 +92,7 @@
|
||||
if (item !== undefined) {
|
||||
showPopup(
|
||||
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'
|
||||
)
|
||||
} else {
|
||||
@ -157,7 +150,7 @@
|
||||
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} />
|
||||
{/await}
|
||||
</div>
|
||||
|
@ -17,8 +17,8 @@
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
|
||||
import { ChunterSpace } from '@hcengineering/chunter'
|
||||
import core, { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
|
||||
import { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
|
||||
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
|
||||
|
||||
export let channel: ChunterSpace | undefined
|
||||
@ -68,10 +68,7 @@
|
||||
{
|
||||
limit: ATTACHEMNTS_LIMIT,
|
||||
sort,
|
||||
total: true,
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
total: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@ -86,11 +83,9 @@
|
||||
<AttachmentPresenter value={attachment} />
|
||||
</div>
|
||||
<div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}>
|
||||
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then blobRef}
|
||||
<a href={blobRef} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
{/await}
|
||||
<a href={getFileUrl(attachment.file, attachment.name)} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showMenu(event, attachment, i)}>
|
||||
|
@ -41,11 +41,6 @@
|
||||
},
|
||||
(res) => {
|
||||
attachments = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
@ -38,7 +38,7 @@
|
||||
|
||||
$: if (empValue === undefined) {
|
||||
void getClient()
|
||||
.findOne(contact.class.Contact, { _id }, { lookup: { avatar: core.class.Blob } })
|
||||
.findOne(contact.class.Contact, { _id })
|
||||
.then((c) => {
|
||||
_contact = c
|
||||
})
|
||||
|
@ -15,11 +15,11 @@
|
||||
//
|
||||
|
||||
import {
|
||||
type Channel,
|
||||
type AvatarInfo,
|
||||
type Contact,
|
||||
getGravatarUrl,
|
||||
getName,
|
||||
type AvatarInfo,
|
||||
type Channel,
|
||||
type Contact,
|
||||
type Person,
|
||||
type PersonAccount
|
||||
} from '@hcengineering/contact'
|
||||
@ -49,6 +49,7 @@ import {
|
||||
type ColorDefinition,
|
||||
type TooltipAlignment
|
||||
} from '@hcengineering/ui'
|
||||
import { AggregationManager } from '@hcengineering/view-resources'
|
||||
import AccountArrayEditor from './components/AccountArrayEditor.svelte'
|
||||
import AccountBox from './components/AccountBox.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 ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
|
||||
import IconMembers from './components/icons/Members.svelte'
|
||||
import { AggregationManager } from '@hcengineering/view-resources'
|
||||
|
||||
import { get, writable } from 'svelte/store'
|
||||
import contact from './plugin'
|
||||
@ -408,7 +408,7 @@ export default async (): Promise<Resources> => ({
|
||||
color: getPersonColor(person, name)
|
||||
}
|
||||
}
|
||||
const blobRef = await getBlobRef(person.$lookup?.avatar, person.avatar, undefined, width)
|
||||
const blobRef = await getBlobRef(person.avatar, undefined, width)
|
||||
return {
|
||||
url: blobRef.src,
|
||||
srcSet: blobRef.srcset,
|
||||
|
@ -320,19 +320,10 @@ function fillStores (): void {
|
||||
const accountPersonQuery = createQuery(true)
|
||||
|
||||
const query = createQuery(true)
|
||||
query.query(
|
||||
contact.mixin.Employee,
|
||||
{},
|
||||
(res) => {
|
||||
employeesStore.set(res)
|
||||
employeeByIdStore.set(toIdMap(res))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
avatar: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
query.query(contact.mixin.Employee, {}, (res) => {
|
||||
employeesStore.set(res)
|
||||
employeeByIdStore.set(toIdMap(res))
|
||||
})
|
||||
|
||||
const accountQ = createQuery(true)
|
||||
accountQ.query(contact.class.PersonAccount, {}, (res) => {
|
||||
@ -345,11 +336,6 @@ function fillStores (): void {
|
||||
{ _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } },
|
||||
(res) => {
|
||||
personAccountPersonByIdStore.set(toIdMap(res))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
avatar: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { FilePreview, createQuery } from '@hcengineering/presentation'
|
||||
|
||||
@ -26,32 +26,23 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
let blob: Blob | undefined = undefined
|
||||
let blob: Ref<Blob> | undefined = undefined
|
||||
let version: WithLookup<FileVersion> | undefined = undefined
|
||||
let contentType: string | undefined
|
||||
|
||||
$: query.query(
|
||||
drive.class.FileVersion,
|
||||
{ _id: object.file },
|
||||
(res) => {
|
||||
;[version] = res
|
||||
blob = version?.$lookup?.file
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
$: query.query(drive.class.FileVersion, { _id: object.file }, (res) => {
|
||||
;[version] = res
|
||||
blob = version.file
|
||||
contentType = version.type
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', { ignoreKeys: ['parent', 'path', 'version', 'versions'] })
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if object !== undefined && version !== undefined}
|
||||
{#if blob !== undefined}
|
||||
<FilePreview file={blob} name={version.name} metadata={version.metadata} fit />
|
||||
{/if}
|
||||
{#if object !== undefined && version !== undefined && blob !== undefined && contentType !== undefined}
|
||||
<FilePreview file={blob} {contentType} name={version.name} metadata={version.metadata} fit />
|
||||
|
||||
{#if object.versions > 1}
|
||||
<div class="w-full mt-6">
|
||||
|
@ -24,7 +24,6 @@
|
||||
export let readonly: boolean = false
|
||||
|
||||
const options: FindOptions<FileVersion> = {
|
||||
lookup: { file: core.class.Blob },
|
||||
sort: { version: SortingOrder.Descending }
|
||||
}
|
||||
</script>
|
||||
|
@ -24,9 +24,5 @@
|
||||
|
||||
<Scroller>
|
||||
<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" />
|
||||
</Scroller>
|
||||
|
@ -16,7 +16,7 @@
|
||||
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, getClient } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
|
||||
import { Button, IconMoreH } from '@hcengineering/ui'
|
||||
import { showFilesUploadPopup } from '@hcengineering/uploader'
|
||||
import view from '@hcengineering/view'
|
||||
@ -108,17 +108,15 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="utils">
|
||||
{#await getBlobHref(undefined, version.file, object.name) then href}
|
||||
<a class="no-line" {href} download={object.name} bind:this={download}>
|
||||
<Button
|
||||
icon={IconDownload}
|
||||
iconProps={{ size: 'medium' }}
|
||||
kind={'icon'}
|
||||
showTooltip={{ label: drive.string.Download }}
|
||||
on:click={handleDownloadFile}
|
||||
/>
|
||||
</a>
|
||||
{/await}
|
||||
<a class="no-line" href={getFileUrl(version.file, object.name)} download={object.name} bind:this={download}>
|
||||
<Button
|
||||
icon={IconDownload}
|
||||
iconProps={{ size: 'medium' }}
|
||||
kind={'icon'}
|
||||
showTooltip={{ label: drive.string.Download }}
|
||||
on:click={handleDownloadFile}
|
||||
/>
|
||||
</a>
|
||||
<Button
|
||||
icon={IconUpload}
|
||||
iconProps={{ size: 'medium' }}
|
||||
|
@ -38,17 +38,11 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (value.$lookup?.file === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const blob = value.$lookup?.file
|
||||
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: blob._id,
|
||||
contentType: blob.contentType,
|
||||
file: value.file,
|
||||
contentType: value.type,
|
||||
name: value.name,
|
||||
metadata: value.metadata
|
||||
},
|
||||
|
@ -44,7 +44,7 @@
|
||||
{#if isFolder}
|
||||
<Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--global-no-priority-PriorityColor)'} />
|
||||
{: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
|
||||
draggable="false"
|
||||
class="img-fit"
|
||||
|
@ -16,7 +16,6 @@
|
||||
import { type Doc, type Ref, type WithLookup } from '@hcengineering/core'
|
||||
import drive, { type Drive, type File, type FileVersion, type Folder } from '@hcengineering/drive'
|
||||
import { type Resources } from '@hcengineering/platform'
|
||||
import { getBlobHref } from '@hcengineering/presentation'
|
||||
import { showPopup, type Location } from '@hcengineering/ui'
|
||||
|
||||
import CreateDrive from './components/CreateDrive.svelte'
|
||||
@ -37,8 +36,9 @@ import GridView from './components/GridView.svelte'
|
||||
import MoveResource from './components/MoveResource.svelte'
|
||||
import ResourcePresenter from './components/ResourcePresenter.svelte'
|
||||
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
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> {
|
||||
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) {
|
||||
const version = file.$lookup?.file
|
||||
if (version != null) {
|
||||
const href = await getBlobHref(undefined, version.file, version.name)
|
||||
const href = getFileUrl(version.file, version.name)
|
||||
const link = document.createElement('a')
|
||||
link.style.display = 'none'
|
||||
link.target = '_blank'
|
||||
|
@ -52,11 +52,6 @@
|
||||
},
|
||||
(res) => {
|
||||
attachments = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -36,11 +36,6 @@
|
||||
},
|
||||
(res) => {
|
||||
attachments = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -366,7 +366,7 @@
|
||||
const formattedSkills = (doc.skills.map((s) => s.toLowerCase()) ?? []).filter(
|
||||
(skill) => !namedElements.has(skill)
|
||||
)
|
||||
const refactoredSkills = []
|
||||
const refactoredSkills: any[] = []
|
||||
if (formattedSkills.length > 0) {
|
||||
const existingTags = Array.from(namedElements.keys()).filter((x) => x.length > 2)
|
||||
const regex = /\S+(?:[-+]\S+)+/g
|
||||
@ -755,6 +755,7 @@
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: object.resumeUuid,
|
||||
contentType: object.resumeType,
|
||||
name: object.resumeName
|
||||
},
|
||||
object.resumeType?.startsWith('image/') ? 'centered' : 'float'
|
||||
|
@ -120,6 +120,7 @@
|
||||
resultQuery = mergeQueries(query, e.detail)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Component
|
||||
is={viewlet.$lookup.descriptor.component}
|
||||
props={{
|
||||
|
@ -129,12 +129,14 @@ export const FileExtension = FileNode.extend<FileOptions>({
|
||||
const fileId = node.attrs['file-id'] ?? ''
|
||||
if (fileId === '') return
|
||||
const fileName = node.attrs['data-file-name'] ?? ''
|
||||
const fileType = node.attrs['data-file-type'] ?? ''
|
||||
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: fileId,
|
||||
name: fileName,
|
||||
contentType: fileType,
|
||||
fullSize: false,
|
||||
showIcon: false
|
||||
},
|
||||
|
@ -151,12 +151,14 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
|
||||
const fileId = node.attrs['file-id'] ?? node.attrs.src
|
||||
const fileName = node.attrs.alt ?? ''
|
||||
const fileType = node.attrs['data-file-type'] ?? ''
|
||||
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: fileId,
|
||||
name: fileName,
|
||||
contentType: fileType,
|
||||
fullSize: true,
|
||||
showIcon: false
|
||||
},
|
||||
@ -209,10 +211,22 @@ export async function openImage (editor: Editor): Promise<void> {
|
||||
const attributes = editor.getAttributes('image')
|
||||
const fileId = attributes['file-id'] ?? attributes.src
|
||||
const fileName = attributes.alt ?? ''
|
||||
const fileType = attributes['data-file-type'] ?? ''
|
||||
await new Promise<void>((resolve) => {
|
||||
showPopup(FilePreviewPopup, { file: fileId, name: fileName, fullSize: true, showIcon: false }, 'centered', () => {
|
||||
resolve()
|
||||
})
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: fileId,
|
||||
name: fileName,
|
||||
contentType: fileType,
|
||||
fullSize: true,
|
||||
showIcon: false
|
||||
},
|
||||
'centered',
|
||||
() => {
|
||||
resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -12,26 +12,26 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// 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 { 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 Level } from '@tiptap/extension-heading'
|
||||
import ListKeymap from '@tiptap/extension-list-keymap'
|
||||
import TableHeader from '@tiptap/extension-table-header'
|
||||
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 { HardBreakExtension } from '../components/extension/hardBreak'
|
||||
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 { 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 {
|
||||
history?: false
|
||||
@ -225,7 +225,7 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
|
||||
inline: true,
|
||||
loadingImgSrc:
|
||||
'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 ?? {},
|
||||
...this.options.image
|
||||
}
|
||||
|
@ -12,14 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import core, {
|
||||
collaborativeDocParse,
|
||||
type Blob,
|
||||
type BlobLookup,
|
||||
type CollaborativeDoc,
|
||||
type Ref
|
||||
} from '@hcengineering/core'
|
||||
import { getBlobHref, getClient } from '@hcengineering/presentation'
|
||||
import { collaborativeDocParse, type Blob, type CollaborativeDoc, type Ref } from '@hcengineering/core'
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
import { ObservableV2 as Observable } from 'lib0/observable'
|
||||
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> {
|
||||
try {
|
||||
const blob = (await getClient().findOne(core.class.Blob, { _id })) as BlobLookup
|
||||
if (blob === undefined || blob.size === 0) {
|
||||
return undefined
|
||||
}
|
||||
const href = await getBlobHref(blob, _id)
|
||||
const href = getFileUrl(_id)
|
||||
const res = await fetch(href)
|
||||
|
||||
if (res.ok) {
|
||||
|
@ -88,6 +88,7 @@
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Scroller {fade} horizontal={true}>
|
||||
<Table
|
||||
bind:this={table}
|
||||
|
@ -16,11 +16,11 @@
|
||||
import { type Blob, type Ref } from '@hcengineering/core'
|
||||
import { CircleButton, Progress } from '@hcengineering/ui'
|
||||
|
||||
import { getBlobSrcFor } from '@hcengineering/presentation'
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
import Pause from '../icons/Pause.svelte'
|
||||
import Play from '../icons/Play.svelte'
|
||||
|
||||
export let value: Blob | Ref<Blob>
|
||||
export let value: Ref<Blob>
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
export let fullSize = false
|
||||
@ -49,9 +49,7 @@
|
||||
</div>
|
||||
|
||||
<audio bind:duration bind:currentTime={time} bind:paused>
|
||||
{#await getBlobSrcFor(value, name) then src}
|
||||
<source {src} type={contentType} />
|
||||
{/await}
|
||||
<source src={getFileUrl(value, name)} type={contentType} />
|
||||
</audio>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -14,13 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Blob, type Ref } from '@hcengineering/core'
|
||||
import { type BlobMetadata } from '@hcengineering/presentation'
|
||||
import AudioPlayer from './AudioPlayer.svelte'
|
||||
|
||||
export let value: Blob | Ref<Blob>
|
||||
export let value: Ref<Blob>
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
</script>
|
||||
|
||||
<AudioPlayer {value} {name} {contentType} fullSize={true} />
|
||||
|
@ -17,12 +17,12 @@
|
||||
import { getBlobRef, type BlobMetadata } from '@hcengineering/presentation'
|
||||
import { Loading } from '@hcengineering/ui'
|
||||
|
||||
export let value: Blob | Ref<Blob>
|
||||
export let value: Ref<Blob>
|
||||
export let name: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
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%'
|
||||
$: height = metadata?.originalHeight
|
||||
? `min(${metadata.originalHeight / metadata?.pixelRatio ?? 1}px, ${fit ? '100%' : '80vh'})`
|
||||
|
@ -14,17 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 metadata: BlobMetadata | undefined
|
||||
export let fit: boolean = false
|
||||
</script>
|
||||
|
||||
{#await getBlobSrcFor(value, name) then href}
|
||||
<iframe class:fit src={href + '#view=FitH&navpanes=0'} title={name} />
|
||||
{/await}
|
||||
<iframe class:fit src={getFileUrl(value, name) + '#view=FitH&navpanes=0'} title={name} />
|
||||
|
||||
<style lang="scss">
|
||||
iframe {
|
||||
|
@ -14,25 +14,20 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 metadata: BlobMetadata | undefined
|
||||
export let fit: boolean = false
|
||||
|
||||
$: maxWidth = metadata?.originalWidth ? `min(${metadata.originalWidth}px, 100%)` : undefined
|
||||
$: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined
|
||||
|
||||
$: src = getFileUrl(value, name)
|
||||
</script>
|
||||
|
||||
{#await getBlobSrcFor(value, name) then src}
|
||||
<video
|
||||
style:max-width={fit ? '100%' : maxWidth}
|
||||
style:max-height={fit ? '100%' : maxHeight}
|
||||
controls
|
||||
preload={'auto'}
|
||||
>
|
||||
<source {src} />
|
||||
<track kind="captions" label={name} />
|
||||
</video>
|
||||
{/await}
|
||||
<video style:max-width={fit ? '100%' : maxWidth} style:max-height={fit ? '100%' : maxHeight} controls preload={'auto'}>
|
||||
<source {src} />
|
||||
<track kind="captions" label={name} />
|
||||
</video>
|
||||
|
@ -12,7 +12,6 @@ describe('aggregator tests', () => {
|
||||
ws1: WorkspaceId
|
||||
} {
|
||||
const mem1 = new MemStorageAdapter()
|
||||
mem1.contentTypes = ['application/ydoc']
|
||||
|
||||
const mem2 = new MemStorageAdapter()
|
||||
const adapters = new Map<string, StorageAdapter>()
|
||||
@ -25,19 +24,6 @@ describe('aggregator tests', () => {
|
||||
const ws1: WorkspaceId = { name: 'ws1', productId: '' }
|
||||
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 () => {
|
||||
const { mem1, aggr, ws1, testCtx } = prepare1()
|
||||
|
||||
|
@ -3,9 +3,7 @@ import core, {
|
||||
ModelDb,
|
||||
TxProcessor,
|
||||
toFindResult,
|
||||
type Branding,
|
||||
type Blob,
|
||||
type BlobLookup,
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
@ -15,22 +13,14 @@ import core, {
|
||||
type FindResult,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
import { genMinModel } from '@hcengineering/core/src/__tests__/minmodel'
|
||||
import type {
|
||||
BlobLookupResult,
|
||||
BlobStorageIterator,
|
||||
BucketInfo,
|
||||
StorageAdapter,
|
||||
UploadedObjectInfo
|
||||
} from '@hcengineering/storage'
|
||||
import type { BlobStorageIterator, BucketInfo, StorageAdapter, UploadedObjectInfo } from '@hcengineering/storage'
|
||||
import { Readable } from 'stream'
|
||||
import type { RawDBAdapter, RawDBAdapterStream } from '../adapter'
|
||||
|
||||
export class MemStorageAdapter implements StorageAdapter {
|
||||
contentTypes?: string[] | undefined
|
||||
files = new Map<string, Blob & { content: Buffer, workspace: string }>()
|
||||
|
||||
async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {}
|
||||
@ -157,15 +147,6 @@ export class MemStorageAdapter implements StorageAdapter {
|
||||
// Partial are not supported by
|
||||
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 {
|
||||
|
@ -4,19 +4,15 @@ import core, {
|
||||
toIdMap,
|
||||
withContext,
|
||||
type Blob,
|
||||
type BlobLookup,
|
||||
type Branding,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type StorageIterator,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
import { type Readable } from 'stream'
|
||||
import { type RawDBAdapter } from '../adapter'
|
||||
|
||||
import {
|
||||
type BlobLookupResult,
|
||||
type BlobStorageIterator,
|
||||
type BucketInfo,
|
||||
type ListBlobResult,
|
||||
@ -308,16 +304,6 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
|
||||
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 }
|
||||
}
|
||||
@ -367,31 +353,6 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
|
||||
await this.dbAdapter.upload<Blob>(ctx, workspaceId, DOMAIN_BLOB, [blobDoc])
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -484,8 +484,6 @@ export interface StorageConfig {
|
||||
kind: string
|
||||
endpoint: string
|
||||
port?: number
|
||||
|
||||
contentTypes?: string[]
|
||||
}
|
||||
|
||||
export interface StorageConfiguration {
|
||||
|
@ -32,7 +32,7 @@ A `;` separated list of triples, providerName|previewUrl|supportedFormats.
|
||||
- supportedFormats - a `,` separated list of file extensions.
|
||||
- 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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -21,4 +21,3 @@ export * from './queryJoin'
|
||||
export * from './spaceSecurity'
|
||||
export * from './spacePermissions'
|
||||
export * from './lookup'
|
||||
export * from './blobs'
|
||||
|
@ -16,22 +16,16 @@
|
||||
import { Client, type BucketItem, type BucketStream } from 'minio'
|
||||
|
||||
import core, {
|
||||
concatLink,
|
||||
toWorkspaceString,
|
||||
withContext,
|
||||
type Blob,
|
||||
type BlobLookup,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl,
|
||||
type Branding
|
||||
type WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import serverCore, {
|
||||
import {
|
||||
removeAllObjects,
|
||||
type BlobLookupResult,
|
||||
type BlobStorageIterator,
|
||||
type BucketInfo,
|
||||
type ListBlobResult,
|
||||
@ -62,7 +56,6 @@ export interface MinioConfig extends StorageConfig {
|
||||
export class MinioService implements StorageAdapter {
|
||||
static config = 'minio'
|
||||
client: Client
|
||||
contentTypes?: string[]
|
||||
constructor (readonly opt: MinioConfig) {
|
||||
this.client = new Client({
|
||||
endPoint: opt.endpoint,
|
||||
@ -72,26 +65,10 @@ export class MinioService implements StorageAdapter {
|
||||
port: opt.port ?? 9000,
|
||||
useSSL: opt.useSSL === 'true'
|
||||
})
|
||||
this.contentTypes = opt.contentTypes
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
@ -13,20 +13,16 @@
|
||||
// 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 { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||||
|
||||
import core, {
|
||||
toWorkspaceString,
|
||||
withContext,
|
||||
type Blob,
|
||||
type BlobLookup,
|
||||
type Branding,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
|
||||
import {
|
||||
@ -39,7 +35,7 @@ import {
|
||||
} from '@hcengineering/server-core'
|
||||
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'
|
||||
|
||||
export interface S3Config extends StorageConfig {
|
||||
@ -67,7 +63,6 @@ export class S3Service implements StorageAdapter {
|
||||
static config = 's3'
|
||||
expireTime: number
|
||||
client: S3
|
||||
contentTypes?: string[]
|
||||
constructor (readonly opt: S3Config) {
|
||||
this.client = new S3({
|
||||
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.contentTypes = opt.contentTypes
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
} from '@hcengineering/core'
|
||||
import { createElasticAdapter, createElasticBackupDataAdapter } from '@hcengineering/elastic'
|
||||
import {
|
||||
BlobLookupMiddleware,
|
||||
ConfigurationMiddleware,
|
||||
LookupMiddleware,
|
||||
ModifiedMiddleware,
|
||||
@ -58,7 +57,6 @@ export function createServerPipeline (
|
||||
): PipelineFactory {
|
||||
const middlewares: MiddlewareCreator[] = [
|
||||
LookupMiddleware.create,
|
||||
BlobLookupMiddleware.create,
|
||||
ModifiedMiddleware.create,
|
||||
PrivateMiddleware.create,
|
||||
SpaceSecurityMiddleware.create,
|
||||
|
@ -50,7 +50,7 @@ export function parseStorageEnv (storageEnv: string, storageConfig: StorageConfi
|
||||
if (st.trim().length === 0 || !st.includes('|')) {
|
||||
throw new Error('Invalid storage config:' + st)
|
||||
}
|
||||
let [kindName, url, contentTypes] = st.split('|')
|
||||
let [kindName, url] = st.split('|')
|
||||
let [kind, name] = kindName.split(',')
|
||||
if (name == null) {
|
||||
name = kind
|
||||
@ -66,8 +66,7 @@ export function parseStorageEnv (storageEnv: string, storageConfig: StorageConfi
|
||||
kind,
|
||||
name,
|
||||
endpoint: (hasProtocol ? uri.protocol + '//' : '') + uri.hostname, // Port should go away
|
||||
port: uri.port !== '' ? parseInt(uri.port) : undefined,
|
||||
contentTypes: contentTypes !== undefined ? contentTypes.split(',') : undefined
|
||||
port: uri.port !== '' ? parseInt(uri.port) : undefined
|
||||
}
|
||||
|
||||
// Add all extra parameters
|
||||
|
@ -57,19 +57,4 @@ describe('config-parse', () => {
|
||||
expect(minio.secretKey).toEqual('minio2')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user