mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +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.
|
// 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} />
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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/')
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -48,11 +48,6 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
resAttachments = res
|
resAttachments = res
|
||||||
},
|
|
||||||
{
|
|
||||||
lookup: {
|
|
||||||
file: core.class.Blob
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -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">
|
||||||
|
@ -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)}
|
||||||
|
@ -43,11 +43,6 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
docs = res
|
docs = res
|
||||||
},
|
|
||||||
{
|
|
||||||
lookup: {
|
|
||||||
file: core.class.Blob
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
|
@ -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)}>
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
|
@ -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' }} />
|
||||||
|
@ -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>
|
||||||
|
@ -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)}>
|
||||||
|
@ -41,11 +41,6 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
attachments = res
|
attachments = res
|
||||||
},
|
|
||||||
{
|
|
||||||
lookup: {
|
|
||||||
file: core.class.Blob
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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' }}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
|
@ -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'
|
||||||
|
@ -52,11 +52,6 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
attachments = res
|
attachments = res
|
||||||
},
|
|
||||||
{
|
|
||||||
lookup: {
|
|
||||||
file: core.class.Blob
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,11 +36,6 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
attachments = res
|
attachments = res
|
||||||
},
|
|
||||||
{
|
|
||||||
lookup: {
|
|
||||||
file: core.class.Blob
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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={{
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -88,6 +88,7 @@
|
|||||||
mode: 'browser'
|
mode: 'browser'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Scroller {fade} horizontal={true}>
|
<Scroller {fade} horizontal={true}>
|
||||||
<Table
|
<Table
|
||||||
bind:this={table}
|
bind:this={table}
|
||||||
|
@ -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">
|
||||||
|
@ -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} />
|
||||||
|
@ -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'})`
|
||||||
|
@ -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 {
|
||||||
|
@ -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}
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 './spaceSecurity'
|
||||||
export * from './spacePermissions'
|
export * from './spacePermissions'
|
||||||
export * from './lookup'
|
export * from './lookup'
|
||||||
export * from './blobs'
|
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user