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:
Alexander Onnikov 2024-06-21 16:36:01 +07:00 committed by GitHub
parent 55a030d383
commit caf3fedd9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 629 additions and 252 deletions

View File

@ -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 {}

View File

@ -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,

View File

@ -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",

View File

@ -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
]

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -61,6 +61,7 @@
"ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство",
"AutoJoin": "Автоприсоединение",
"AutoJoinDescr": "Автоматически присоединять новых сотрудников к этому пространству",
"BlobSize": "Размер"
"BlobSize": "Размер",
"BlobContentType": "Тип контента"
}
}

View File

@ -61,6 +61,7 @@
"ArchiveSpaceDescription": "授予用户归档空间的权限",
"AutoJoin": "自动加入",
"AutoJoinDescr": "自动将新员工加入此空间",
"BlobSize": "大小"
"BlobSize": "大小",
"BlobContentType": "內容類型"
}
}

View File

@ -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,

View File

@ -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
*/

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -33,7 +33,7 @@
"Next": "Далее",
"FailedToPreview": "Ошибка предпросмотра",
"ContentType": "Тип контента",
"ContentTypeNotSupported": "Тип контента не поддерживается"
"ContentTypeNotSupported": "Предварительный просмотр недоступен для этого типа контента"
},
"status": {
"FileTooLarge": "Файл слишком большой"

View File

@ -33,7 +33,7 @@
"Next": "下一步",
"FailedToPreview": "预览失败",
"ContentType": "内容类型",
"ContentTypeNotSupported": "内容类型不支持"
"ContentTypeNotSupported": "此內容類型無法預覽"
},
"status": {
"FileTooLarge": "文件太大"

View 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>

View File

@ -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>

View File

@ -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'

View File

@ -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 {

View File

@ -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
},

View File

@ -149,7 +149,6 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
{
file: fileId,
name: fileName,
contentType: 'image/*',
fullSize: true,
showIcon: false
},

View File

@ -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;

View File

@ -315,6 +315,7 @@
min-height: 0;
.title {
min-width: 0;
padding: .125rem .375rem;
font-size: .8125rem;
color: var(--theme-content-color);

View File

@ -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}

View File

@ -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);
}
}

View File

@ -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
*/

View File

@ -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

View File

@ -73,7 +73,6 @@
{
file: attachment.$lookup?.file ?? attachment.file,
name: attachment.name,
contentType: attachment.type ?? '',
metadata: attachment.metadata
},
getPreviewAlignment(attachment.type ?? '')

View File

@ -47,7 +47,6 @@
{
file: value.$lookup?.file ?? value.file,
name: value.name,
contentType: value.type,
metadata: value.metadata
},
isImage(value.type) ? 'centered' : 'float'

View File

@ -85,7 +85,6 @@
{
file: value.$lookup?.file ?? value.file,
name: value.name,
contentType: value.type,
metadata: value.metadata
},
getPreviewAlignment(value.type)

View File

@ -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)

View File

@ -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 {

View File

@ -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,

View File

@ -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}

View File

@ -8,9 +8,6 @@
"Resource": "Resource",
"Name": "Name",
"Description": "Description",
"Size": "Size",
"Type": "Type",
"LastModified": "Last modified",
"Parent": "Parent",
"Path": "Path",
"Download": "Download",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -8,9 +8,6 @@
"Resource": "Ресурс",
"Name": "Название",
"Description": "Описание",
"Size": "Размер",
"Type": "Тип",
"LastModified": "Последнее изменение",
"Parent": "Родительская папка",
"Path": "Путь",
"Download": "Скачать",

View File

@ -8,9 +8,6 @@
"Resource": "资源",
"Name": "名称",
"Description": "描述",
"Size": "大小",
"Type": "类型",
"LastModified": "最后修改",
"Parent": "父级",
"Path": "路径",
"Download": "下载",

View File

@ -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}

View 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}

View 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>

View 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>

View 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}

View File

@ -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">

View File

@ -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}

View File

@ -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} />

View File

@ -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>

View File

@ -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}

View File

@ -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} />

View File

@ -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

View File

@ -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()

View File

@ -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()
}

View File

@ -29,6 +29,8 @@ export interface Resource extends Doc<Drive> {
parent: Ref<Resource>
path: Ref<Resource>[]
comments?: number
}
/** @public */

View File

@ -754,7 +754,6 @@
FilePreviewPopup,
{
file: object.resumeUuid,
contentType: object.resumeType ?? 'application/pdf',
name: object.resumeName
},
object.resumeType?.startsWith('image/') ? 'centered' : 'float'

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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,