mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-7354 File edit page (#5884)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
parent
55a030d383
commit
caf3fedd9c
@ -63,8 +63,8 @@ import {
|
||||
Prop,
|
||||
ReadOnly,
|
||||
TypeBoolean,
|
||||
TypeFileSize,
|
||||
TypeIntlString,
|
||||
TypeNumber,
|
||||
TypeRecord,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
@ -155,7 +155,7 @@ export class TBlob extends TDoc implements Blob {
|
||||
@ReadOnly()
|
||||
version!: string
|
||||
|
||||
@Prop(TypeNumber(), core.string.BlobSize)
|
||||
@Prop(TypeFileSize(), core.string.BlobSize)
|
||||
@ReadOnly()
|
||||
size!: number
|
||||
}
|
||||
@ -228,6 +228,10 @@ export class TTypeIntlString extends TType {}
|
||||
@Model(core.class.TypeNumber, core.class.Type)
|
||||
export class TTypeNumber extends TType {}
|
||||
|
||||
@UX(core.string.BlobSize)
|
||||
@Model(core.class.TypeFileSize, core.class.Type)
|
||||
export class TTypeFileSize extends TType {}
|
||||
|
||||
@UX(core.string.Markup)
|
||||
@Model(core.class.TypeMarkup, core.class.Type)
|
||||
export class TTypeMarkup extends TType {}
|
||||
|
@ -66,6 +66,7 @@ import {
|
||||
TTypeCollaborativeDocVersion,
|
||||
TTypeCollaborativeMarkup,
|
||||
TTypeDate,
|
||||
TTypeFileSize,
|
||||
TTypeHyperlink,
|
||||
TTypeIntlString,
|
||||
TTypeMarkup,
|
||||
@ -147,6 +148,7 @@ export function createModel (builder: Builder): void {
|
||||
TArrOf,
|
||||
TRefTo,
|
||||
TTypeDate,
|
||||
TTypeFileSize,
|
||||
TTypeTimestamp,
|
||||
TTypeNumber,
|
||||
TTypeBoolean,
|
||||
|
@ -32,9 +32,12 @@
|
||||
"@hcengineering/model": "^0.6.11",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/model-preference": "^0.6.0",
|
||||
"@hcengineering/model-print": "^0.6.0",
|
||||
"@hcengineering/model-tracker": "^0.6.0",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/model-workbench": "^0.6.1",
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/drive": "^0.6.0",
|
||||
"@hcengineering/drive-resources": "^0.6.0",
|
||||
|
@ -13,19 +13,19 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import core, {
|
||||
type Blob,
|
||||
type Domain,
|
||||
type FindOptions,
|
||||
type Role,
|
||||
type RolesAssignment,
|
||||
type Type,
|
||||
Account,
|
||||
AccountRole,
|
||||
IndexKind,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
DOMAIN_MODEL
|
||||
SortingOrder
|
||||
} from '@hcengineering/core'
|
||||
import { type Drive, type File, type Folder, type Resource, driveId } from '@hcengineering/drive'
|
||||
import {
|
||||
@ -39,11 +39,13 @@ import {
|
||||
TypeRecord,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
UX
|
||||
UX,
|
||||
Collection
|
||||
} from '@hcengineering/model'
|
||||
import { TDoc, TType, TTypedSpace } from '@hcengineering/model-core'
|
||||
import { TDoc, TTypedSpace } from '@hcengineering/model-core'
|
||||
import print from '@hcengineering/model-print'
|
||||
import tracker from '@hcengineering/model-tracker'
|
||||
import view, { type Viewlet, classPresenter, createAction } from '@hcengineering/model-view'
|
||||
import view, { type Viewlet, createAction } from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
|
||||
@ -54,14 +56,6 @@ export { drive as default }
|
||||
|
||||
export const DOMAIN_DRIVE = 'drive' as Domain
|
||||
|
||||
/** @public */
|
||||
export function TypeFilesize (): Type<number> {
|
||||
return { _class: drive.class.TypeFileSize, label: drive.string.Size }
|
||||
}
|
||||
|
||||
@Model(drive.class.TypeFileSize, core.class.Type, DOMAIN_MODEL)
|
||||
export class TTypeFileSize extends TType {}
|
||||
|
||||
@Model(drive.class.Drive, core.class.TypedSpace)
|
||||
@UX(drive.string.Drive)
|
||||
export class TDrive extends TTypedSpace implements Drive {}
|
||||
@ -98,6 +92,9 @@ export class TResource extends TDoc implements Resource {
|
||||
@Prop(TypeRef(drive.class.Resource), drive.string.Path)
|
||||
@ReadOnly()
|
||||
path!: Ref<Resource>[]
|
||||
|
||||
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
|
||||
comments?: number
|
||||
}
|
||||
|
||||
@Model(drive.class.Folder, drive.class.Resource, DOMAIN_DRIVE)
|
||||
@ -125,7 +122,6 @@ export class TFile extends TResource implements File {
|
||||
|
||||
@Prop(TypeRecord(), drive.string.Metadata)
|
||||
@ReadOnly()
|
||||
@Hidden()
|
||||
metadata?: Record<string, any>
|
||||
|
||||
@Prop(TypeRef(drive.class.Folder), drive.string.Parent)
|
||||
@ -207,7 +203,7 @@ function defineDrive (builder: Builder): void {
|
||||
// Actions
|
||||
|
||||
builder.mixin(drive.class.Drive, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [tracker.action.EditRelatedTargets, tracker.action.NewRelatedIssue]
|
||||
actions: [tracker.action.EditRelatedTargets, print.action.Print, tracker.action.NewRelatedIssue]
|
||||
})
|
||||
|
||||
createAction(
|
||||
@ -249,9 +245,7 @@ function defineDrive (builder: Builder): void {
|
||||
}
|
||||
|
||||
function defineResource (builder: Builder): void {
|
||||
builder.createModel(TTypeFileSize, TResource)
|
||||
|
||||
classPresenter(builder, drive.class.TypeFileSize, drive.component.FileSizePresenter)
|
||||
builder.createModel(TResource)
|
||||
|
||||
builder.mixin(drive.class.Resource, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: drive.component.ResourcePresenter
|
||||
@ -270,15 +264,9 @@ function defineResource (builder: Builder): void {
|
||||
label: drive.string.Name,
|
||||
sortingKey: 'name'
|
||||
},
|
||||
{
|
||||
key: '$lookup.file.size',
|
||||
presenter: drive.component.FileSizePresenter,
|
||||
label: drive.string.Size,
|
||||
sortingKey: '$lookup.file.size'
|
||||
},
|
||||
{
|
||||
key: '$lookup.file.modifiedOn'
|
||||
},
|
||||
'$lookup.file.size',
|
||||
'comments',
|
||||
'$lookup.file.modifiedOn',
|
||||
'createdBy'
|
||||
],
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||
@ -332,16 +320,8 @@ function defineResource (builder: Builder): void {
|
||||
label: drive.string.Name,
|
||||
sortingKey: 'name'
|
||||
},
|
||||
{
|
||||
key: '$lookup.file.size',
|
||||
presenter: drive.component.FileSizePresenter,
|
||||
label: drive.string.Size,
|
||||
sortingKey: '$lookup.file.size'
|
||||
},
|
||||
{
|
||||
key: '$lookup.file.modifiedOn',
|
||||
label: core.string.ModifiedDate
|
||||
},
|
||||
'$lookup.file.size',
|
||||
'$lookup.file.modifiedOn',
|
||||
'createdBy'
|
||||
],
|
||||
configOptions: {
|
||||
@ -388,6 +368,7 @@ function defineFolder (builder: Builder): void {
|
||||
actions: [
|
||||
view.action.Open,
|
||||
view.action.OpenInNewTab,
|
||||
print.action.Print,
|
||||
tracker.action.EditRelatedTargets,
|
||||
tracker.action.NewRelatedIssue
|
||||
]
|
||||
@ -438,12 +419,34 @@ function defineFile (builder: Builder): void {
|
||||
presenter: drive.component.FilePresenter
|
||||
})
|
||||
|
||||
builder.mixin(drive.class.File, core.class.Class, view.mixin.ObjectEditor, {
|
||||
editor: drive.component.EditFile
|
||||
})
|
||||
|
||||
builder.mixin(drive.class.File, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: drive.component.FilePanel
|
||||
})
|
||||
|
||||
builder.mixin(drive.class.File, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: drive.function.FileLinkProvider
|
||||
})
|
||||
|
||||
// Activity
|
||||
|
||||
builder.mixin(drive.class.File, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: drive.class.File,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
// Actions
|
||||
|
||||
builder.mixin(drive.class.File, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [
|
||||
view.action.Open,
|
||||
view.action.OpenInNewTab,
|
||||
print.action.Print,
|
||||
tracker.action.EditRelatedTargets,
|
||||
tracker.action.NewRelatedIssue
|
||||
]
|
||||
|
@ -36,7 +36,9 @@ export default mergeIds(driveId, drive, {
|
||||
DriveSpacePresenter: '' as AnyComponent,
|
||||
DrivePanel: '' as AnyComponent,
|
||||
DrivePresenter: '' as AnyComponent,
|
||||
EditFile: '' as AnyComponent,
|
||||
EditFolder: '' as AnyComponent,
|
||||
FilePanel: '' as AnyComponent,
|
||||
FolderPanel: '' as AnyComponent,
|
||||
FolderPresenter: '' as AnyComponent,
|
||||
FilePresenter: '' as AnyComponent,
|
||||
@ -46,6 +48,7 @@ export default mergeIds(driveId, drive, {
|
||||
function: {
|
||||
DriveLinkProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
FolderLinkProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
FileLinkProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
CanRenameFile: '' as Resource<ViewActionAvailabilityFunction>,
|
||||
CanRenameFolder: '' as Resource<ViewActionAvailabilityFunction>
|
||||
},
|
||||
@ -78,10 +81,7 @@ export default mergeIds(driveId, drive, {
|
||||
Grid: '' as IntlString,
|
||||
Name: '' as IntlString,
|
||||
Description: '' as IntlString,
|
||||
Size: '' as IntlString,
|
||||
Type: '' as IntlString,
|
||||
Metadata: '' as IntlString,
|
||||
LastModified: '' as IntlString,
|
||||
Parent: '' as IntlString,
|
||||
Path: '' as IntlString,
|
||||
Drives: '' as IntlString,
|
||||
|
@ -486,6 +486,7 @@ export function createModel (builder: Builder): void {
|
||||
view.component.MarkupEditorPopup,
|
||||
view.component.MarkupDiffPresenter
|
||||
)
|
||||
classPresenter(builder, core.class.TypeFileSize, view.component.FileSizePresenter, view.component.FileSizePresenter)
|
||||
|
||||
builder.mixin(core.class.TypeMarkup, core.class.Class, view.mixin.InlineAttributEditor, {
|
||||
editor: view.component.HTMLEditor
|
||||
|
@ -54,6 +54,7 @@ export default mergeIds(viewId, view, {
|
||||
HyperlinkEditor: '' as AnyComponent,
|
||||
HyperlinkEditorPopup: '' as AnyComponent,
|
||||
IntlStringPresenter: '' as AnyComponent,
|
||||
FileSizePresenter: '' as AnyComponent,
|
||||
NumberEditor: '' as AnyComponent,
|
||||
NumberPresenter: '' as AnyComponent,
|
||||
MarkupDiffPresenter: '' as AnyComponent,
|
||||
|
@ -61,6 +61,7 @@
|
||||
"ArchiveSpaceDescription": "Grants users ability to archive the space",
|
||||
"AutoJoin": "Auto join",
|
||||
"AutoJoinDescr": "Automatically join new employees to this space",
|
||||
"BlobSize": "Size"
|
||||
"BlobSize": "Size",
|
||||
"BlobContentType": "Content type"
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@
|
||||
"ArchiveSpaceDescription": "Concede a los usuarios la capacidad de archivar el espacio",
|
||||
"AutoJoin": "Auto unirse",
|
||||
"AutoJoinDescr": "Unirse automáticamente a los nuevos empleados a este espacio",
|
||||
"BlobSize": "Tamaño"
|
||||
"BlobSize": "Tamaño",
|
||||
"BlobContentType": "Tipo de contenido"
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,7 @@
|
||||
"ArchiveSpaceDescription": "Accorde aux utilisateurs la capacité d'archiver l'espace",
|
||||
"AutoJoin": "Rejoindre automatiquement",
|
||||
"AutoJoinDescr": "Ajouter automatiquement les nouveaux employés à cet espace",
|
||||
"BlobSize": "Taille"
|
||||
"BlobSize": "Taille",
|
||||
"BlobContentType": "Type de contenu"
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@
|
||||
"ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço",
|
||||
"AutoJoin": "Auto adesão",
|
||||
"AutoJoinDescr": "Adesão automática de novos funcionários a este espaço",
|
||||
"BlobSize": "Tamanho"
|
||||
"BlobSize": "Tamanho",
|
||||
"BlobContentType": "Tipo de conteúdo"
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,7 @@
|
||||
"ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство",
|
||||
"AutoJoin": "Автоприсоединение",
|
||||
"AutoJoinDescr": "Автоматически присоединять новых сотрудников к этому пространству",
|
||||
"BlobSize": "Размер"
|
||||
"BlobSize": "Размер",
|
||||
"BlobContentType": "Тип контента"
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,7 @@
|
||||
"ArchiveSpaceDescription": "授予用户归档空间的权限",
|
||||
"AutoJoin": "自动加入",
|
||||
"AutoJoinDescr": "自动将新员工加入此空间",
|
||||
"BlobSize": "大小"
|
||||
"BlobSize": "大小",
|
||||
"BlobContentType": "內容類型"
|
||||
}
|
||||
}
|
||||
|
@ -115,6 +115,7 @@ export default plugin(coreId, {
|
||||
TypeIntlString: '' as Ref<Class<Type<IntlString>>>,
|
||||
TypeHyperlink: '' as Ref<Class<Type<Hyperlink>>>,
|
||||
TypeNumber: '' as Ref<Class<Type<number>>>,
|
||||
TypeFileSize: '' as Ref<Class<Type<number>>>,
|
||||
TypeMarkup: '' as Ref<Class<Type<string>>>,
|
||||
TypeRank: '' as Ref<Class<Type<Rank>>>,
|
||||
TypeRecord: '' as Ref<Class<Type<Record<any, any>>>>,
|
||||
@ -201,6 +202,7 @@ export default plugin(coreId, {
|
||||
Array: '' as IntlString,
|
||||
Name: '' as IntlString,
|
||||
Enum: '' as IntlString,
|
||||
Size: '' as IntlString,
|
||||
Description: '' as IntlString,
|
||||
ShortDescription: '' as IntlString,
|
||||
Descriptor: '' as IntlString,
|
||||
|
@ -468,6 +468,13 @@ export function TypeEnum (of: Ref<Enum>): EnumOf {
|
||||
return { _class: core.class.EnumOf, label: core.string.Enum, of }
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function TypeFileSize (): Type<number> {
|
||||
return { _class: core.class.TypeFileSize, label: core.string.Size }
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -33,7 +33,7 @@
|
||||
"Next": "Next",
|
||||
"FailedToPreview": "Failed to preview",
|
||||
"ContentType": "Content type",
|
||||
"ContentTypeNotSupported": "Content type not supported"
|
||||
"ContentTypeNotSupported": "Preview is not available for this content type"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "File too large"
|
||||
|
@ -33,7 +33,7 @@
|
||||
"Next": "Siguiente",
|
||||
"FailedToPreview": "Error al previsualizar",
|
||||
"ContentType": "Tipo de contenido",
|
||||
"ContentTypeNotSupported": "Tipo de contenido no admitido"
|
||||
"ContentTypeNotSupported": "La vista previa no está disponible para este tipo de contenido"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Archivo demasiado grande"
|
||||
|
@ -33,7 +33,7 @@
|
||||
"Next": "Suivant",
|
||||
"FailedToPreview": "Échec de l'aperçu",
|
||||
"ContentType": "Type de contenu",
|
||||
"ContentTypeNotSupported": "Type de contenu non supporté"
|
||||
"ContentTypeNotSupported": "L'aperçu n'est pas disponible pour ce type de contenu"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Fichier trop volumineux"
|
||||
|
@ -33,7 +33,7 @@
|
||||
"Next": "Seguinte",
|
||||
"FailedToPreview": "Falha ao pré-visualizar",
|
||||
"ContentType": "Tipo de conteúdo",
|
||||
"ContentTypeNotSupported": "Tipo de conteúdo não suportado"
|
||||
"ContentTypeNotSupported": "A visualização não está disponível para este tipo de conteúdo"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Ficheiro demasiado grande"
|
||||
|
@ -33,7 +33,7 @@
|
||||
"Next": "Далее",
|
||||
"FailedToPreview": "Ошибка предпросмотра",
|
||||
"ContentType": "Тип контента",
|
||||
"ContentTypeNotSupported": "Тип контента не поддерживается"
|
||||
"ContentTypeNotSupported": "Предварительный просмотр недоступен для этого типа контента"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Файл слишком большой"
|
||||
|
@ -33,7 +33,7 @@
|
||||
"Next": "下一步",
|
||||
"FailedToPreview": "预览失败",
|
||||
"ContentType": "内容类型",
|
||||
"ContentTypeNotSupported": "内容类型不支持"
|
||||
"ContentTypeNotSupported": "此內容類型無法預覽"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "文件太大"
|
||||
|
83
packages/presentation/src/components/FilePreview.svelte
Normal file
83
packages/presentation/src/components/FilePreview.svelte
Normal file
@ -0,0 +1,83 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Blob } from '@hcengineering/core'
|
||||
import { Button, Component, Label } from '@hcengineering/ui'
|
||||
|
||||
import presentation from '../plugin'
|
||||
|
||||
import { getPreviewType, previewTypes } from '../file'
|
||||
import { BlobMetadata, FilePreviewExtension } from '../types'
|
||||
|
||||
import { getBlobSrcFor } from '../preview'
|
||||
|
||||
export let file: Blob
|
||||
export let name: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
let download: HTMLAnchorElement
|
||||
|
||||
let previewType: FilePreviewExtension | undefined = undefined
|
||||
$: void getPreviewType(file.contentType, $previewTypes).then((res) => {
|
||||
previewType = res
|
||||
})
|
||||
$: srcRef = getBlobSrcFor(file, name)
|
||||
</script>
|
||||
|
||||
{#await srcRef then src}
|
||||
{#if src === ''}
|
||||
<div class="centered">
|
||||
<Label label={presentation.string.FailedToPreview} />
|
||||
</div>
|
||||
{:else if previewType !== undefined}
|
||||
<div class="content flex-col flex-grow items-center">
|
||||
<Component
|
||||
is={previewType.component}
|
||||
props={{ value: file, name, contentType: file.contentType, metadata, ...props }}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="centered flex-col flex-gap-3">
|
||||
<Label label={presentation.string.ContentTypeNotSupported} />
|
||||
<a class="no-line" href={src} download={name} bind:this={download}>
|
||||
<Button
|
||||
label={presentation.string.Download}
|
||||
kind={'primary'}
|
||||
on:click={() => {
|
||||
download.click()
|
||||
}}
|
||||
showTooltip={{ label: presentation.string.Download }}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
border: none;
|
||||
}
|
||||
.centered {
|
||||
flex-grow: 1;
|
||||
width: 100;
|
||||
height: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
@ -13,30 +13,34 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Blob, type Ref } from '@hcengineering/core'
|
||||
import { Button, Component, Dialog, Label } from '@hcengineering/ui'
|
||||
import core, { type Blob, type Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { Button, Dialog, tooltip } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
import presentation from '../plugin'
|
||||
|
||||
import { getPreviewType, previewTypes } from '../file'
|
||||
import { BlobMetadata, FilePreviewExtension } from '../types'
|
||||
|
||||
import { BlobMetadata } from '../types'
|
||||
import { getClient } from '../utils'
|
||||
import { getBlobSrcFor } from '../preview'
|
||||
|
||||
import ActionContext from './ActionContext.svelte'
|
||||
import FilePreview from './FilePreview.svelte'
|
||||
import Download from './icons/Download.svelte'
|
||||
|
||||
export let file: Blob | Ref<Blob> | undefined
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
export let props: Record<string, any> = {}
|
||||
|
||||
export let fullSize = false
|
||||
export let showIcon = true
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let download: HTMLAnchorElement
|
||||
|
||||
onMount(() => {
|
||||
if (fullSize) {
|
||||
dispatch('fullsize')
|
||||
@ -49,17 +53,14 @@
|
||||
return ext.substring(0, 4).toUpperCase()
|
||||
}
|
||||
|
||||
let previewType: FilePreviewExtension | undefined = undefined
|
||||
$: if (file !== undefined) {
|
||||
void getPreviewType(contentType, $previewTypes).then((res) => {
|
||||
previewType = res
|
||||
})
|
||||
} else {
|
||||
previewType = undefined
|
||||
}
|
||||
let download: HTMLAnchorElement
|
||||
let blob: Blob | undefined = undefined
|
||||
$: void fetchBlob(file)
|
||||
|
||||
$: srcRef = getBlobSrcFor(file, name)
|
||||
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>
|
||||
|
||||
<ActionContext context={{ mode: 'browser' }} />
|
||||
@ -79,7 +80,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="wrapped-title">{name}</span>
|
||||
<span class="wrapped-title" use:tooltip={{ label: getEmbeddedLabel(name) }}>{name}</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@ -100,29 +101,9 @@
|
||||
{/await}
|
||||
</svelte:fragment>
|
||||
|
||||
{#await srcRef then src}
|
||||
{#if src === ''}
|
||||
<div class="centered">
|
||||
<Label label={presentation.string.FailedToPreview} />
|
||||
</div>
|
||||
{:else if previewType !== undefined}
|
||||
<div class="content flex-col flex-grow">
|
||||
<Component is={previewType.component} props={{ value: file, name, contentType, metadata, ...props }} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="centered flex-col flex-gap-3">
|
||||
<Label label={presentation.string.ContentTypeNotSupported} />
|
||||
<Button
|
||||
label={presentation.string.Download}
|
||||
kind={'primary'}
|
||||
on:click={() => {
|
||||
download.click()
|
||||
}}
|
||||
showTooltip={{ label: presentation.string.Download }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{#if blob !== undefined}
|
||||
<FilePreview file={blob} {name} {metadata} {props} />
|
||||
{/if}
|
||||
</Dialog>
|
||||
|
||||
<style lang="scss">
|
||||
@ -139,17 +120,4 @@
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
border: none;
|
||||
}
|
||||
.centered {
|
||||
flex-grow: 1;
|
||||
width: 100;
|
||||
height: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
@ -39,6 +39,7 @@ export { default as IndexedDocumentPreview } from './components/IndexedDocumentP
|
||||
export { default as IndexedDocumentCompare } from './components/IndexedDocumentCompare.svelte'
|
||||
export { default as NavLink } from './components/NavLink.svelte'
|
||||
export { default as IconDownload } from './components/icons/Download.svelte'
|
||||
export { default as FilePreview } from './components/FilePreview.svelte'
|
||||
export { default as FilePreviewPopup } from './components/FilePreviewPopup.svelte'
|
||||
export { default as IconForward } from './components/icons/Forward.svelte'
|
||||
export { default as Breadcrumbs } from './components/breadcrumbs/Breadcrumbs.svelte'
|
||||
|
@ -41,14 +41,9 @@
|
||||
const attributes = textEditor.getAttributes('image')
|
||||
const fileId = attributes['file-id'] ?? attributes.src
|
||||
const fileName = attributes.alt ?? ''
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{ file: fileId, name: fileName, contentType: 'image/*', fullSize: true, showIcon: false },
|
||||
'centered',
|
||||
() => {
|
||||
dispatch('focus')
|
||||
}
|
||||
)
|
||||
showPopup(FilePreviewPopup, { file: fileId, name: fileName, fullSize: true, showIcon: false }, 'centered', () => {
|
||||
dispatch('focus')
|
||||
})
|
||||
}
|
||||
|
||||
function openOriginalImage (): void {
|
||||
|
@ -122,14 +122,12 @@ export const FileExtension = FileNode.extend<FileOptions>({
|
||||
const fileId = node.attrs['file-id'] ?? ''
|
||||
if (fileId === '') return
|
||||
const fileName = node.attrs['data-file-name'] ?? ''
|
||||
const fileType: string = node.attrs['data-file-type'] ?? ''
|
||||
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: fileId,
|
||||
name: fileName,
|
||||
contentType: fileType,
|
||||
fullSize: false,
|
||||
showIcon: false
|
||||
},
|
||||
|
@ -149,7 +149,6 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
{
|
||||
file: fileId,
|
||||
name: fileName,
|
||||
contentType: 'image/*',
|
||||
fullSize: true,
|
||||
showIcon: false
|
||||
},
|
||||
|
@ -763,6 +763,8 @@ input.search {
|
||||
.square-4 { width: 1rem; height: 1rem; }
|
||||
.square-36 { width: 2.25rem; height: 2.25rem; }
|
||||
|
||||
.object-contain { object-fit: contain; }
|
||||
|
||||
/* --------- */
|
||||
.svg-xx-small {
|
||||
width: .5rem;
|
||||
|
@ -315,6 +315,7 @@
|
||||
min-height: 0;
|
||||
|
||||
.title {
|
||||
min-width: 0;
|
||||
padding: .125rem .375rem;
|
||||
font-size: .8125rem;
|
||||
color: var(--theme-content-color);
|
||||
|
@ -16,7 +16,7 @@
|
||||
<script lang="ts">
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { resizeObserver, Button, Label, IconClose, IconScale, IconScaleFull } from '..'
|
||||
import { Button, Label, IconClose, IconScale, IconScaleFull, resizeObserver, tooltip } from '..'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let isFullSize: boolean = false
|
||||
@ -39,11 +39,11 @@
|
||||
<div class="flex-row-center gap-1-5">
|
||||
<Button icon={IconClose} kind={'ghost'} size={'medium'} on:click={() => dispatch('close')} />
|
||||
{#if label}
|
||||
<span class="title"><Label {label} /></span>
|
||||
<span class="title" use:tooltip={{ label }}><Label {label} /></span>
|
||||
{/if}
|
||||
{#if $$slots.title}<slot name="title" />{/if}
|
||||
</div>
|
||||
<div class="flex-row-center gap-1-5">
|
||||
<div class="flex-row-center flex-no-shrink gap-1-5">
|
||||
{#if $$slots.utils}
|
||||
<slot name="utils" />
|
||||
{/if}
|
||||
|
@ -16,6 +16,7 @@
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import { getPlatformColor } from '../colors'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { deviceOptionsStore } from '..'
|
||||
|
||||
export let value: number
|
||||
export let min: number = 0
|
||||
@ -37,8 +38,8 @@
|
||||
|
||||
function calcValue (e: MouseEvent): void {
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
let pos = x / rect.width
|
||||
const x = e.clientX - rect.left - $deviceOptionsStore.fontSize / 2
|
||||
let pos = x / (rect.width - $deviceOptionsStore.fontSize)
|
||||
if (pos > 100) pos = 100
|
||||
if (pos < 0) pos = 0
|
||||
value = (max - min) * pos + min
|
||||
@ -66,9 +67,10 @@
|
||||
<div class="container" class:editable on:click={click} on:mousemove={move} on:mouseleave={save} on:mouseup={save}>
|
||||
<div
|
||||
class="bar"
|
||||
style="background-color: {color !== undefined
|
||||
style:width={`calc(calc(100% - 1rem) * ${position} / 100 + .5rem)`}
|
||||
style:background-color={color !== undefined
|
||||
? getPlatformColor(color, $themeStore.dark)
|
||||
: 'var(--theme-toggle-on-bg-color)'}; width: calc(100% * {position} / 100 + 0.5rem);"
|
||||
: 'var(--theme-toggle-on-bg-color)'}
|
||||
/>
|
||||
{#if editable}
|
||||
<div
|
||||
@ -76,7 +78,7 @@
|
||||
on:mousedown={() => {
|
||||
drag = true
|
||||
}}
|
||||
style="left: calc(100% * {position} / 100 - 0.5rem);"
|
||||
style:left={`calc(calc(100% - 1rem) * ${position} / 100)`}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@ -94,7 +96,7 @@
|
||||
cursor: pointer;
|
||||
|
||||
.bar {
|
||||
border-radius: 0.5rem;
|
||||
border-radius: 0.5rem 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.control {
|
||||
@ -103,8 +105,11 @@
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--caption-color);
|
||||
background-color: var(--primary-button-color);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
box-shadow:
|
||||
inset -0.125rem -0.125rem 0.175rem rgba(0, 0, 0, 0.1),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,21 @@ export function floorFractionDigits (n: number | string, amount: number): number
|
||||
return Number(Number(n).toFixed(amount))
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function humanReadableFileSize (size: number, base: 2 | 10 = 10, fractionDigits: number = 2): string {
|
||||
const units =
|
||||
base === 10
|
||||
? ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
const kb = base === 10 ? 1000 : 1024
|
||||
|
||||
const pow = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(kb))
|
||||
const val = (1.0 * size) / Math.pow(kb, pow)
|
||||
return `${val.toFixed(2)} ${units[pow]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -57,6 +57,7 @@ const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [
|
||||
core.class.EnumOf,
|
||||
core.class.TypeNumber,
|
||||
core.class.TypeDate,
|
||||
core.class.TypeFileSize,
|
||||
core.class.TypeMarkup,
|
||||
core.class.TypeCollaborativeMarkup,
|
||||
core.class.TypeHyperlink
|
||||
|
@ -73,7 +73,6 @@
|
||||
{
|
||||
file: attachment.$lookup?.file ?? attachment.file,
|
||||
name: attachment.name,
|
||||
contentType: attachment.type ?? '',
|
||||
metadata: attachment.metadata
|
||||
},
|
||||
getPreviewAlignment(attachment.type ?? '')
|
||||
|
@ -47,7 +47,6 @@
|
||||
{
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
name: value.name,
|
||||
contentType: value.type,
|
||||
metadata: value.metadata
|
||||
},
|
||||
isImage(value.type) ? 'centered' : 'float'
|
||||
|
@ -85,7 +85,6 @@
|
||||
{
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
name: value.name,
|
||||
contentType: value.type,
|
||||
metadata: value.metadata
|
||||
},
|
||||
getPreviewAlignment(value.type)
|
||||
|
@ -51,7 +51,7 @@
|
||||
if (listProvider !== undefined) listProvider.updateFocus(value)
|
||||
const popupInfo = showPopup(
|
||||
FilePreviewPopup,
|
||||
{ file: value.$lookup?.file ?? value.file, name: value.name, contentType: value.type },
|
||||
{ file: value.$lookup?.file ?? value.file, name: value.name },
|
||||
value.type.startsWith('image/') ? 'centered' : 'float'
|
||||
)
|
||||
dispatch('open', popupInfo.id)
|
||||
|
@ -99,7 +99,7 @@
|
||||
if (item !== undefined) {
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{ file: item.$lookup?.file ?? item.file, name: item.name, contentType: item.type },
|
||||
{ file: item.$lookup?.file ?? item.file, name: item.name },
|
||||
item.type.startsWith('image/') ? 'centered' : 'float'
|
||||
)
|
||||
} else {
|
||||
|
@ -21,6 +21,7 @@
|
||||
core.class.TypeBoolean,
|
||||
core.class.TypeDate,
|
||||
core.class.TypeNumber,
|
||||
core.class.TypeFileSize,
|
||||
core.class.EnumOf,
|
||||
core.class.Collection,
|
||||
core.class.ArrOf,
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import core, { Account, DocumentQuery, Ref, matchQuery } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { ButtonKind, ButtonSize, IconSize } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
import { personAccountByIdStore } from '../utils'
|
||||
@ -28,6 +28,9 @@
|
||||
export let docQuery: DocumentQuery<Account> = {}
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let avatarSize: IconSize = 'card'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let readonly = false
|
||||
|
||||
const client = getClient()
|
||||
@ -63,6 +66,9 @@
|
||||
showNavigate={false}
|
||||
{kind}
|
||||
{size}
|
||||
{avatarSize}
|
||||
{justify}
|
||||
{width}
|
||||
{label}
|
||||
{readonly}
|
||||
value={selectedEmp}
|
||||
|
@ -8,9 +8,6 @@
|
||||
"Resource": "Resource",
|
||||
"Name": "Name",
|
||||
"Description": "Description",
|
||||
"Size": "Size",
|
||||
"Type": "Type",
|
||||
"LastModified": "Last modified",
|
||||
"Parent": "Parent",
|
||||
"Path": "Path",
|
||||
"Download": "Download",
|
||||
|
@ -8,9 +8,6 @@
|
||||
"Resource": "Recurso",
|
||||
"Name": "Nombre",
|
||||
"Description": "Descripción",
|
||||
"Size": "Tamaño",
|
||||
"Type": "Tipo",
|
||||
"LastModified": "Última modificación",
|
||||
"Parent": "Padre",
|
||||
"Path": "Ruta",
|
||||
"Download": "Descargar",
|
||||
|
@ -8,9 +8,6 @@
|
||||
"Resource": "Ressource",
|
||||
"Name": "Nom",
|
||||
"Description": "Description",
|
||||
"Size": "Taille",
|
||||
"Type": "Type",
|
||||
"LastModified": "Dernière modification",
|
||||
"Parent": "Parent",
|
||||
"Path": "Chemin",
|
||||
"Download": "Télécharger",
|
||||
|
@ -8,9 +8,6 @@
|
||||
"Resource": "Recurso",
|
||||
"Name": "Nome",
|
||||
"Description": "Descrição",
|
||||
"Size": "Tamanho",
|
||||
"Type": "Tipo",
|
||||
"LastModified": "Última modificação",
|
||||
"Parent": "Pai",
|
||||
"Path": "Caminho",
|
||||
"Download": "Descarregar",
|
||||
|
@ -8,9 +8,6 @@
|
||||
"Resource": "Ресурс",
|
||||
"Name": "Название",
|
||||
"Description": "Описание",
|
||||
"Size": "Размер",
|
||||
"Type": "Тип",
|
||||
"LastModified": "Последнее изменение",
|
||||
"Parent": "Родительская папка",
|
||||
"Path": "Путь",
|
||||
"Download": "Скачать",
|
||||
|
@ -8,9 +8,6 @@
|
||||
"Resource": "资源",
|
||||
"Name": "名称",
|
||||
"Description": "描述",
|
||||
"Size": "大小",
|
||||
"Type": "类型",
|
||||
"LastModified": "最后修改",
|
||||
"Parent": "父级",
|
||||
"Path": "路径",
|
||||
"Download": "下载",
|
||||
|
@ -64,6 +64,12 @@
|
||||
</Scroller>
|
||||
</svelte:fragment>
|
||||
|
||||
<FolderBrowser space={object._id} parent={drive.ids.Root} />
|
||||
<FolderBrowser
|
||||
space={object._id}
|
||||
parent={drive.ids.Root}
|
||||
on:contextmenu={(evt) => {
|
||||
showMenu(evt, { object })
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
42
plugins/drive-resources/src/components/EditFile.svelte
Normal file
42
plugins/drive-resources/src/components/EditFile.svelte
Normal file
@ -0,0 +1,42 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { type Blob } from '@hcengineering/core'
|
||||
import drive, { type File } from '@hcengineering/drive'
|
||||
import { FilePreview, createQuery } from '@hcengineering/presentation'
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
export let object: File
|
||||
export let readonly: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
let blob: Blob | undefined = undefined
|
||||
$: query.query(core.class.Blob, { _id: object.file }, (res) => {
|
||||
;[blob] = res
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', { ignoreKeys: ['file', 'preview', 'parent', 'path', 'metadata'] })
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if object !== undefined}
|
||||
{#if blob !== undefined}
|
||||
<FilePreview file={blob} name={object.name} metadata={object.metadata} />
|
||||
{/if}
|
||||
{/if}
|
36
plugins/drive-resources/src/components/FileAside.svelte
Normal file
36
plugins/drive-resources/src/components/FileAside.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { type File } from '@hcengineering/drive'
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
import { DocAttributeBar } from '@hcengineering/view-resources'
|
||||
|
||||
export let object: WithLookup<File>
|
||||
export let readonly: boolean = false
|
||||
</script>
|
||||
|
||||
<Scroller>
|
||||
<DocAttributeBar {object} {readonly} ignoreKeys={[]} />
|
||||
|
||||
{#if object.$lookup?.file}
|
||||
<DocAttributeBar
|
||||
object={object.$lookup.file}
|
||||
{readonly}
|
||||
ignoreKeys={['provider', 'storageId', 'etag', 'version']}
|
||||
/>
|
||||
{/if}
|
||||
<div class="space-divider bottom" />
|
||||
</Scroller>
|
37
plugins/drive-resources/src/components/FileHeader.svelte
Normal file
37
plugins/drive-resources/src/components/FileHeader.svelte
Normal file
@ -0,0 +1,37 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Doc } from '@hcengineering/core'
|
||||
import { type File } from '@hcengineering/drive'
|
||||
import { DocsNavigator } from '@hcengineering/view-resources'
|
||||
|
||||
import FilePresenter from './FilePresenter.svelte'
|
||||
import { resolveParents } from '../utils'
|
||||
|
||||
export let object: File
|
||||
|
||||
let parents: Doc[] = []
|
||||
$: void updateParents(object)
|
||||
|
||||
async function updateParents (object: File): Promise<void> {
|
||||
parents = await resolveParents(object)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiHSpacer x2" />
|
||||
<DocsNavigator elements={parents} />
|
||||
<div class="title">
|
||||
<FilePresenter value={object} shouldShowAvatar={false} disabled noUnderline />
|
||||
</div>
|
106
plugins/drive-resources/src/components/FilePanel.svelte
Normal file
106
plugins/drive-resources/src/components/FilePanel.svelte
Normal file
@ -0,0 +1,106 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { WithLookup, type Ref } from '@hcengineering/core'
|
||||
import { type File } from '@hcengineering/drive'
|
||||
import { Panel } from '@hcengineering/panel'
|
||||
import presentation, { IconDownload, createQuery, getBlobHref } from '@hcengineering/presentation'
|
||||
import { Button, IconMoreH } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
|
||||
import EditFile from './EditFile.svelte'
|
||||
import FileAside from './FileAside.svelte'
|
||||
import FileHeader from './FileHeader.svelte'
|
||||
|
||||
import drive from '../plugin'
|
||||
|
||||
export let _id: Ref<File>
|
||||
export let readonly: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let kind: 'default' | 'modern' = 'default'
|
||||
|
||||
export function canClose (): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
let object: WithLookup<File> | undefined = undefined
|
||||
let download: HTMLAnchorElement
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(
|
||||
drive.class.File,
|
||||
{ _id },
|
||||
(res) => {
|
||||
;[object] = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<Panel
|
||||
{object}
|
||||
{embedded}
|
||||
{kind}
|
||||
allowClose={!embedded}
|
||||
isHeader={false}
|
||||
useMaxWidth={false}
|
||||
on:open
|
||||
on:close
|
||||
on:update
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<FileHeader {object} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="utils">
|
||||
{#await getBlobHref(object.$lookup?.file, object.file, object.name) then href}
|
||||
<a class="no-line" {href} download={object.name} bind:this={download}>
|
||||
<Button
|
||||
icon={IconDownload}
|
||||
iconProps={{ size: 'medium' }}
|
||||
kind={'icon'}
|
||||
showTooltip={{ label: presentation.string.Download }}
|
||||
on:click={() => {
|
||||
download.click()
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
{/await}
|
||||
<Button
|
||||
icon={IconMoreH}
|
||||
iconProps={{ size: 'medium' }}
|
||||
kind={'icon'}
|
||||
showTooltip={{ label: view.string.MoreActions }}
|
||||
on:click={(ev) => {
|
||||
showMenu(ev, { object })
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<FileAside {object} {readonly} />
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex-col flex-grow flex-no-shrink step-tb-6">
|
||||
<EditFile {object} {readonly} />
|
||||
</div>
|
||||
</Panel>
|
||||
{/if}
|
@ -18,8 +18,7 @@
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { File } from '@hcengineering/drive'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { FilePreviewPopup } from '@hcengineering/presentation'
|
||||
import { Icon, showPopup, tooltip } from '@hcengineering/ui'
|
||||
import { Icon, tooltip } from '@hcengineering/ui'
|
||||
import { ObjectPresenterType } from '@hcengineering/view'
|
||||
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
|
||||
|
||||
@ -34,36 +33,13 @@
|
||||
export let type: ObjectPresenterType = 'link'
|
||||
|
||||
$: icon = getFileTypeIcon(value.$lookup?.file?.contentType ?? '')
|
||||
|
||||
function handleClick (): void {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (value.$lookup?.file === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const blob = value.$lookup?.file
|
||||
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
contentType: blob.contentType,
|
||||
name: value.name,
|
||||
metadata: value.metadata
|
||||
},
|
||||
'float'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if inline}
|
||||
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
|
||||
{:else if type === 'link'}
|
||||
<DocNavLink object={value} onClick={handleClick} {disabled} {accent} {noUnderline}>
|
||||
<DocNavLink object={value} {disabled} {accent} {noUnderline}>
|
||||
<div class="flex-presenter" use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
|
||||
{#if shouldShowAvatar}
|
||||
<div class="icon">
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import filesize from 'filesize'
|
||||
import { humanReadableFileSize } from '@hcengineering/ui'
|
||||
|
||||
export let value: number | undefined
|
||||
export let accent: boolean = false
|
||||
@ -21,6 +21,6 @@
|
||||
|
||||
{#if value}
|
||||
<span class="overflow-label select-text" class:fs-bold={accent}>
|
||||
{value !== undefined ? filesize(value) : '-'}
|
||||
{value !== undefined ? humanReadableFileSize(value) : '-'}
|
||||
</span>
|
||||
{/if}
|
||||
|
@ -93,7 +93,7 @@
|
||||
|
||||
{#if viewlet !== undefined && viewOptions}
|
||||
<FilterBar {_class} {space} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
|
||||
<div class="popupPanel rowContent">
|
||||
<div class="popupPanel rowContent" on:contextmenu>
|
||||
{#if viewlet}
|
||||
<Scroller horizontal={true}>
|
||||
<ViewletContentView {_class} {viewlet} query={resultQuery} {space} {viewOptions} />
|
||||
|
@ -13,38 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { toIdMap, type Doc, type Ref } from '@hcengineering/core'
|
||||
import drive, { type Folder } from '@hcengineering/drive'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type Doc } from '@hcengineering/core'
|
||||
import { type Folder } from '@hcengineering/drive'
|
||||
import { DocsNavigator } from '@hcengineering/view-resources'
|
||||
|
||||
import FolderPresenter from './FolderPresenter.svelte'
|
||||
import { resolveParents } from '../utils'
|
||||
|
||||
export let object: Folder
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let parents: Doc[] = []
|
||||
$: void updateParents(object.path)
|
||||
$: void updateParents(object)
|
||||
|
||||
async function updateParents (path: Ref<Folder>[]): Promise<void> {
|
||||
const docs: Array<Doc> = []
|
||||
|
||||
const folders = await client.findAll(drive.class.Folder, { _id: { $in: path } })
|
||||
const byId = toIdMap(folders)
|
||||
for (const p of path) {
|
||||
const parent = byId.get(p)
|
||||
if (parent !== undefined) {
|
||||
docs.push(parent)
|
||||
}
|
||||
}
|
||||
|
||||
const root = await client.findOne(drive.class.Drive, { _id: object.space })
|
||||
if (root !== undefined) {
|
||||
docs.push(root)
|
||||
}
|
||||
|
||||
parents = docs.reverse()
|
||||
async function updateParents (object: Folder): Promise<void> {
|
||||
parents = await resolveParents(object)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -62,6 +62,13 @@
|
||||
</Scroller>
|
||||
</svelte:fragment>
|
||||
|
||||
<FolderBrowser space={object.space} parent={object._id} {readonly} />
|
||||
<FolderBrowser
|
||||
space={object.space}
|
||||
parent={object._id}
|
||||
{readonly}
|
||||
on:contextmenu={(evt) => {
|
||||
showMenu(evt, { object })
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
@ -80,16 +80,16 @@
|
||||
|
||||
<div class="flex-between flex-gap-2 h-4">
|
||||
<div class="flex-row-center flex-gap-2 font-regular-12">
|
||||
<span class="flex-no-shrink">
|
||||
<ObjectPresenter
|
||||
_class={core.class.Account}
|
||||
objectId={object.createdBy}
|
||||
noUnderline
|
||||
props={{ avatarSize: 'tiny' }}
|
||||
/>
|
||||
</span>
|
||||
<ObjectPresenter
|
||||
_class={core.class.Account}
|
||||
objectId={object.createdBy}
|
||||
noUnderline
|
||||
props={{ avatarSize: 'tiny' }}
|
||||
/>
|
||||
<span>•</span>
|
||||
<TimestampPresenter value={object.$lookup?.file?.modifiedOn ?? object.createdOn ?? object.modifiedOn} />
|
||||
<span class="flex-no-shrink">
|
||||
<TimestampPresenter value={object.$lookup?.file?.modifiedOn ?? object.createdOn ?? object.modifiedOn} />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-no-shrink font-regular-12">
|
||||
<FileSizePresenter value={object.$lookup?.file?.size} />
|
||||
|
@ -24,7 +24,9 @@ import DrivePanel from './components/DrivePanel.svelte'
|
||||
import DrivePresenter from './components/DrivePresenter.svelte'
|
||||
import DriveSpaceHeader from './components/DriveSpaceHeader.svelte'
|
||||
import DriveSpacePresenter from './components/DriveSpacePresenter.svelte'
|
||||
import EditFile from './components/EditFile.svelte'
|
||||
import EditFolder from './components/EditFolder.svelte'
|
||||
import FilePanel from './components/FilePanel.svelte'
|
||||
import FilePresenter from './components/FilePresenter.svelte'
|
||||
import FileSizePresenter from './components/FileSizePresenter.svelte'
|
||||
import FolderPanel from './components/FolderPanel.svelte'
|
||||
@ -32,7 +34,7 @@ import FolderPresenter from './components/FolderPresenter.svelte'
|
||||
import GridView from './components/GridView.svelte'
|
||||
import ResourcePresenter from './components/ResourcePresenter.svelte'
|
||||
|
||||
import { getDriveLink, getFolderLink, resolveLocation } from './navigation'
|
||||
import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation'
|
||||
import { createFolder, renameResource } from './utils'
|
||||
|
||||
async function CreateRootFolder (doc: Drive): Promise<void> {
|
||||
@ -68,6 +70,10 @@ async function FolderLinkProvider (doc: Doc): Promise<Location> {
|
||||
return getFolderLink(doc._id as Ref<Folder>)
|
||||
}
|
||||
|
||||
async function FileLinkProvider (doc: Doc): Promise<Location> {
|
||||
return getFileLink(doc._id as Ref<File>)
|
||||
}
|
||||
|
||||
async function RenameFile (doc: File | File[]): Promise<void> {
|
||||
if (!Array.isArray(doc)) {
|
||||
await renameResource(doc)
|
||||
@ -95,7 +101,9 @@ export default async (): Promise<Resources> => ({
|
||||
DriveSpaceHeader,
|
||||
DriveSpacePresenter,
|
||||
DrivePresenter,
|
||||
EditFile,
|
||||
EditFolder,
|
||||
FilePanel,
|
||||
FilePresenter,
|
||||
FileSizePresenter,
|
||||
FolderPanel,
|
||||
@ -113,6 +121,7 @@ export default async (): Promise<Resources> => ({
|
||||
},
|
||||
function: {
|
||||
DriveLinkProvider,
|
||||
FileLinkProvider,
|
||||
FolderLinkProvider,
|
||||
CanRenameFile,
|
||||
CanRenameFolder
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import type { Doc, Ref } from '@hcengineering/core'
|
||||
import drive, { driveId, type Drive, type Folder } from '@hcengineering/drive'
|
||||
import drive, { type File, type Drive, type Folder, driveId } from '@hcengineering/drive'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
|
||||
import view, { type ObjectPanel } from '@hcengineering/view'
|
||||
@ -55,11 +55,28 @@ export function getFolderLink (_id: Ref<Folder>): Location {
|
||||
return loc
|
||||
}
|
||||
|
||||
export function getFileLink (_id: Ref<File>): Location {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path.length = 2
|
||||
loc.fragment = undefined
|
||||
loc.query = undefined
|
||||
loc.path[2] = driveId
|
||||
loc.path[3] = 'file'
|
||||
loc.path[4] = _id
|
||||
|
||||
return loc
|
||||
}
|
||||
|
||||
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
|
||||
if (loc.path[2] !== driveId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (loc.path[3] === 'file' && loc.path[4] !== undefined) {
|
||||
const fileId = loc.path[4] as Ref<File>
|
||||
return await generateFileLocation(loc, fileId)
|
||||
}
|
||||
|
||||
if (loc.path[3] === 'folder' && loc.path[4] !== undefined) {
|
||||
const folderId = loc.path[4] as Ref<Folder>
|
||||
return await generateFolderLocation(loc, folderId)
|
||||
@ -98,6 +115,31 @@ export async function generateFolderLocation (loc: Location, id: Ref<Folder>): P
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateFileLocation (loc: Location, id: Ref<File>): Promise<ResolvedLocation | undefined> {
|
||||
const client = getClient()
|
||||
|
||||
const doc = await client.findOne(drive.class.File, { _id: id })
|
||||
if (doc === undefined) {
|
||||
accessDeniedStore.set(true)
|
||||
console.error(`Could not find file ${id}.`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const appComponent = loc.path[0] ?? ''
|
||||
const workspace = loc.path[1] ?? ''
|
||||
|
||||
return {
|
||||
loc: {
|
||||
path: [appComponent, workspace, driveId, doc.space],
|
||||
fragment: getPanelFragment(doc)
|
||||
},
|
||||
defaultLocation: {
|
||||
path: [appComponent, workspace, driveId],
|
||||
fragment: getPanelFragment(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateDriveLocation (loc: Location, id: Ref<Drive>): Promise<ResolvedLocation | undefined> {
|
||||
const client = getClient()
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { toIdMap, type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import drive, { type Drive, type Folder, type Resource } from '@hcengineering/drive'
|
||||
import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation'
|
||||
@ -110,3 +110,26 @@ export function getFileTypeIcon (contentType: string): Asset | AnySvelteComponen
|
||||
const type = contentType.split('/', 1)[0]
|
||||
return fileTypesMap[type] ?? fileTypesMap[contentType] ?? drive.icon.File
|
||||
}
|
||||
|
||||
export async function resolveParents (object: Resource): Promise<Doc[]> {
|
||||
const client = getClient()
|
||||
|
||||
const parents: Doc[] = []
|
||||
|
||||
const path = object.path
|
||||
const folders = await client.findAll(drive.class.Resource, { _id: { $in: path } })
|
||||
const byId = toIdMap(folders)
|
||||
for (const p of path) {
|
||||
const parent = byId.get(p)
|
||||
if (parent !== undefined) {
|
||||
parents.push(parent)
|
||||
}
|
||||
}
|
||||
|
||||
const root = await client.findOne(drive.class.Drive, { _id: object.space })
|
||||
if (root !== undefined) {
|
||||
parents.push(root)
|
||||
}
|
||||
|
||||
return parents.reverse()
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ export interface Resource extends Doc<Drive> {
|
||||
|
||||
parent: Ref<Resource>
|
||||
path: Ref<Resource>[]
|
||||
|
||||
comments?: number
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -754,7 +754,6 @@
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: object.resumeUuid,
|
||||
contentType: object.resumeType ?? 'application/pdf',
|
||||
name: object.resumeName
|
||||
},
|
||||
object.resumeType?.startsWith('image/') ? 'centered' : 'float'
|
||||
|
@ -0,0 +1,35 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button, ButtonSize, humanReadableFileSize } from '@hcengineering/ui'
|
||||
|
||||
export let value: number | undefined
|
||||
export let kind: 'no-border' | 'link' | 'list' = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = 'fit-content'
|
||||
|
||||
$: strValue = value != null ? humanReadableFileSize(value) : '-'
|
||||
</script>
|
||||
|
||||
{#if kind === 'link'}
|
||||
<Button {kind} {size} {justify} {width}>
|
||||
<svelte:fragment slot="content">
|
||||
{strValue}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{:else}
|
||||
<span class="caption-color overflow-label">{strValue}</span>
|
||||
{/if}
|
@ -38,7 +38,7 @@
|
||||
|
||||
<div class="container flex-between" class:fullSize>
|
||||
<CircleButton size="x-large" on:click={handleClick} {icon} />
|
||||
<div class="w-full ml-4">
|
||||
<div class="w-full ml-3 mr-2">
|
||||
<Progress
|
||||
value={time}
|
||||
max={Number.isFinite(duration) ? duration : 100}
|
||||
|
@ -24,14 +24,3 @@
|
||||
</script>
|
||||
|
||||
<AudioPlayer {value} {name} {contentType} fullSize={true} />
|
||||
|
||||
<style lang="scss">
|
||||
.img-fit {
|
||||
margin: 0 auto;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
@ -18,23 +18,20 @@
|
||||
|
||||
export let value: Blob | Ref<Blob>
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
|
||||
$: p = typeof value === 'string' ? getBlobRef(undefined, value, name) : getBlobRef(value, value._id)
|
||||
$: maxWidth = metadata?.originalWidth ? `min(${metadata.originalWidth}px, 100%)` : undefined
|
||||
$: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined
|
||||
</script>
|
||||
|
||||
{#await p then blobRef}
|
||||
<img class="w-full h-full img-fit" src={blobRef.src} srcset={blobRef.srcset} alt={name} />
|
||||
<img
|
||||
class="object-contain"
|
||||
style:max-width={maxWidth}
|
||||
style:max-height={maxHeight}
|
||||
src={blobRef.src}
|
||||
srcset={blobRef.srcset}
|
||||
alt={name}
|
||||
/>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.img-fit {
|
||||
margin: 0 auto;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
@ -18,16 +18,18 @@
|
||||
|
||||
export let value: Blob | Ref<Blob>
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
</script>
|
||||
|
||||
{#await getBlobSrcFor(value, name) then href}
|
||||
<iframe src={href + '#view=FitH&navpanes=0'} class="w-full h-full" title={name} />
|
||||
<iframe src={href + '#view=FitH&navpanes=0'} title={name} />
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
min-height: 20rem;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
@ -18,13 +18,15 @@
|
||||
|
||||
export let value: Blob | Ref<Blob>
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
|
||||
$: maxWidth = metadata?.originalWidth ? `min(${metadata.originalWidth}px, 100%)` : undefined
|
||||
$: maxHeight = metadata?.originalHeight ? `min(${metadata.originalHeight}px, 80vh)` : undefined
|
||||
</script>
|
||||
|
||||
{#await getBlobSrcFor(value, name) then blobRef}
|
||||
<video controls preload={'auto'}>
|
||||
<source src={blobRef} />
|
||||
{#await getBlobSrcFor(value, name) then src}
|
||||
<video style:max-width={maxWidth} style:max-height={maxHeight} controls preload={'auto'}>
|
||||
<source {src} />
|
||||
<track kind="captions" label={name} />
|
||||
</video>
|
||||
{/await}
|
||||
|
@ -40,6 +40,7 @@ import EditDoc from './components/EditDoc.svelte'
|
||||
import EnumArrayEditor from './components/EnumArrayEditor.svelte'
|
||||
import EnumEditor from './components/EnumEditor.svelte'
|
||||
import EnumPresenter from './components/EnumPresenter.svelte'
|
||||
import FileSizePresenter from './components/FileSizePresenter.svelte'
|
||||
import HTMLEditor from './components/HTMLEditor.svelte'
|
||||
import HTMLPresenter from './components/HTMLPresenter.svelte'
|
||||
import HyperlinkEditor from './components/HyperlinkEditor.svelte'
|
||||
@ -285,6 +286,7 @@ export default async (): Promise<Resources> => ({
|
||||
SpaceTypeSelector,
|
||||
EnumArrayEditor,
|
||||
EnumPresenter,
|
||||
FileSizePresenter,
|
||||
StatusPresenter,
|
||||
StatusRefPresenter,
|
||||
DateFilterPresenter,
|
||||
|
Loading…
Reference in New Issue
Block a user