Files for cards (#8217)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2025-03-13 11:47:56 +05:00 committed by GitHub
parent 7fafd3d772
commit af2f78a141
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 569 additions and 240 deletions

View File

@ -24,13 +24,15 @@ import {
import chunter from '@hcengineering/chunter'
import core, {
AccountRole,
type Blobs,
DOMAIN_MODEL,
IndexKind,
SortingOrder,
type CollectionSize,
type MarkupBlobRef,
type Rank,
type Ref
type Ref,
ClassifierKind,
type MarkupBlobRef
} from '@hcengineering/core'
import {
Collection,
@ -75,6 +77,8 @@ export class TCard extends TDoc implements Card {
@Prop(TypeCollaborativeDoc(), card.string.Content)
content!: MarkupBlobRef
blobs!: Blobs
@Prop(TypeRef(card.class.Card), card.string.Parent)
parent?: Ref<Card> | null
@ -103,6 +107,62 @@ export * from './migration'
export function createModel (builder: Builder): void {
builder.createModel(TMasterTag, TTag, TCard, MasterTagEditorSection)
builder.createDoc(
card.class.MasterTag,
core.space.Model,
{
label: attachment.string.File,
extends: card.class.Card,
icon: card.icon.File,
kind: ClassifierKind.CLASS
},
card.types.File
)
builder.mixin(card.types.File, card.class.MasterTag, setting.mixin.Editable, {
value: true
})
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: card.types.File,
descriptor: view.viewlet.Table,
configOptions: {
hiddenKeys: ['content', 'title']
},
config: [
'',
'_class',
{ key: '', presenter: view.component.RolePresenter, label: card.string.Tags, props: { fullSize: true } },
'modifiedOn'
]
})
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: card.types.File,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['_class', 'createdBy', 'modifiedBy'],
orderBy: [
['modifiedOn', SortingOrder.Descending],
['rank', SortingOrder.Ascending]
],
other: []
},
configOptions: {
hiddenKeys: ['content', 'title']
},
config: [
{ key: '', props: { showParent: true } },
'_class',
{ key: '', presenter: view.component.RolePresenter, label: card.string.Tags, props: { fullSize: true } },
{ key: '', displayProps: { grow: true } },
{
key: 'modifiedOn',
displayProps: { fixed: 'right', dividerBefore: true }
}
]
})
builder.createDoc(
workbench.class.Application,
core.space.Model,

View File

@ -13,7 +13,15 @@
// limitations under the License.
//
import { DOMAIN_MODEL, type Tx, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
import {
DOMAIN_MODEL,
type Tx,
type Blob,
type Class,
type Doc,
type Ref,
type BlobMetadata
} from '@hcengineering/core'
import { Mixin, Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
@ -23,7 +31,6 @@ import {
type PresentationMiddlewareFactory
} from '@hcengineering/presentation/src/pipeline'
import {
type BlobMetadata,
type ComponentPointExtension,
type CreateExtensionKind,
type DocAttributeRule,

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { type Blob, type Doc, type Ref } from '@hcengineering/core'
import { type Blob, type BlobMetadata, type Doc, type Ref } from '@hcengineering/core'
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import {
@ -23,7 +23,7 @@ import {
viewId,
type ViewOptionsAction
} from '@hcengineering/view'
import { type BlobMetadata, type FileOrBlob, type FilePreviewExtension } from '@hcengineering/presentation/src/types'
import { type FileOrBlob, type FilePreviewExtension } from '@hcengineering/presentation/src/types'
import { type PresentationMiddlewareFactory } from '@hcengineering/presentation/src/pipeline'
import view from '@hcengineering/view-resources/src/plugin'

View File

@ -601,6 +601,11 @@ export interface Sequence extends Doc {
sequence: number
}
/**
* @public
*/
export type BlobMetadata = Record<string, any>
/**
* @public
*
@ -622,6 +627,18 @@ export interface Blob extends Doc {
size: number
}
export interface BlobType {
file: Ref<Blob>
type: string
name: string
metadata?: BlobMetadata
}
export type Blobs = Record<string, BlobType>
/**
* For every blob will automatically add a lookup.
*

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { BlobMetadata, type Blob, type Ref } from '@hcengineering/core'
import {
Button,
Component,
@ -28,7 +28,7 @@
import { getFileUrl } from '../file'
import { getPreviewType, previewTypes } from '../filetypes'
import { imageSizeToRatio } from '../image'
import { BlobMetadata, FilePreviewExtension } from '../types'
import { FilePreviewExtension } from '../types'
export let file: Ref<Blob>
export let name: string

View File

@ -14,13 +14,11 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { SortingOrder, type Blob, type Ref } from '@hcengineering/core'
import { BlobMetadata, SortingOrder, type Blob, type Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Button, Dialog, IconHistory, IconScribble, showPopup, tooltip } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import { BlobMetadata } from '../types'
import ActionContext from './ActionContext.svelte'
import FilePreview from './FilePreview.svelte'
import DownloadFileButton from './DownloadFileButton.svelte'

View File

@ -18,7 +18,7 @@
import { getBlobRef } from '../preview'
export let blob: Ref<Blob>
export let alt: string = ''
export let alt: string | undefined = undefined
export let fit: string = 'contain'
export let width: number
export let height: number

View File

@ -14,13 +14,13 @@
//
import { Analytics } from '@hcengineering/analytics'
import { type Blob, type Ref } from '@hcengineering/core'
import { type BlobMetadata, type Blob, type Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { type PopupAlignment } from '@hcengineering/ui'
import { writable } from 'svelte/store'
import plugin from './plugin'
import type { BlobMetadata, FileOrBlob, FilePreviewExtension } from './types'
import type { FileOrBlob, FilePreviewExtension } from './types'
import { createQuery } from './utils'
/**

View File

@ -10,7 +10,8 @@ import {
type Ref,
type RelatedDocument,
type Space,
type TxOperations
type TxOperations,
type BlobMetadata
} from '@hcengineering/core'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import { type AnyComponent, type AnySvelteComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'
@ -184,11 +185,6 @@ export interface DocRules extends Doc {
*/
export type FileOrBlob = File | Blob
/**
* @public
*/
export type BlobMetadata = Record<string, any>
/**
* @public
*/

View File

@ -21,7 +21,7 @@
export let src: string
export let hlsSrc: string
export let hlsThumbnail = ''
export let name = ''
export let name: string | undefined = undefined
export let preload = true
let video: HTMLVideoElement | null = null
@ -147,7 +147,7 @@
</script>
<video bind:this={video} width="100%" height="100%" class="plyr" preload={preload ? 'auto' : 'none'} controls>
<track kind="captions" label={name} />
<track kind="captions" label={name ?? ''} />
</video>
<style lang="scss">

View File

@ -17,7 +17,7 @@
export let src: string | undefined
export let srcset: string | undefined = undefined
export let alt: string = ''
export let alt: string | undefined = undefined
export let width: number | string
export let height: number | string
export let fit: string = 'contain'
@ -55,7 +55,7 @@
<img
{src}
{srcset}
{alt}
alt={alt ?? ''}
{width}
{height}
style:object-fit={fit}

View File

@ -14,14 +14,14 @@
-->
<script lang="ts">
export let src: string
export let name: string = ''
export let name: string | undefined = undefined
export let preload = true
let video: HTMLVideoElement
</script>
<video bind:this={video} {src} width="100%" height="100%" preload={preload ? 'auto' : 'none'} controls>
<track kind="captions" label={name} />
<track kind="captions" label={name ?? ''} />
</video>
<style lang="scss">

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { type Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import type { BlobType, WithLookup } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import presentation, { canPreviewFile, getFileUrl, previewTypes } from '@hcengineering/presentation'
import { IconMoreH, Menu, Action as UIAction, showPopup, tooltip } from '@hcengineering/ui'
@ -24,9 +24,9 @@
import AttachmentAction from './AttachmentAction.svelte'
import FileDownload from './icons/FileDownload.svelte'
import attachmentPlugin from '../plugin'
import { openAttachmentInSidebar, showAttachmentPreviewPopup } from '../utils'
import { isAttachment, openAttachmentInSidebar, showAttachmentPreviewPopup } from '../utils'
export let attachment: WithLookup<Attachment>
export let attachment: WithLookup<Attachment> | BlobType
export let isSaved = false
export let removable = false
@ -87,22 +87,28 @@
await openAttachmentInSidebar(attachment)
}
})
actions.push({
label: saveAttachmentAction.label,
icon: saveAttachmentAction.icon,
action: async (props: any, evt: Event) => {
const impl = await getResource(saveAttachmentAction.action)
await impl(attachment, evt)
}
})
if (removable) {
if (isAttachment(attachment)) {
actions.push({
label: attachmentPlugin.string.DeleteFile,
label: saveAttachmentAction.label,
icon: saveAttachmentAction.icon,
action: async (props: any, evt: Event) => {
const impl = await getResource(attachmentPlugin.actionImpl.DeleteAttachment)
await impl(attachment, evt)
if (isAttachment(attachment)) {
const impl = await getResource(saveAttachmentAction.action)
await impl(attachment, evt)
}
}
})
if (removable) {
actions.push({
label: attachmentPlugin.string.DeleteFile,
action: async (props: any, evt: Event) => {
if (isAttachment(attachment)) {
const impl = await getResource(attachmentPlugin.actionImpl.DeleteAttachment)
await impl(attachment, evt)
}
}
})
}
}
showPopup(
Menu,

View File

@ -14,14 +14,14 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import type { BlobType, WithLookup } from '@hcengineering/core'
import { Image } from '@hcengineering/presentation'
import { Loading } from '@hcengineering/ui'
import BrokenImage from './icons/BrokenImage.svelte'
import { AttachmentImageSize } from '../types'
export let value: WithLookup<Attachment>
export let value: WithLookup<Attachment> | BlobType
export let size: AttachmentImageSize = 'auto'
interface Dimensions {
@ -41,7 +41,7 @@
$: dimensions = getDimensions(value, size)
function getDimensions (value: Attachment, size: AttachmentImageSize): Dimensions {
function getDimensions (value: Attachment | BlobType, size: AttachmentImageSize): Dimensions {
if (size === 'auto' || size == null) {
return {
width: 300,

View File

@ -14,8 +14,9 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { BlobType } from '@hcengineering/core'
export let value: Attachment | undefined
export let value: Attachment | BlobType | undefined
</script>
{#if value}

View File

@ -16,7 +16,7 @@
<script lang="ts">
import contact, { PermissionsStore } from '@hcengineering/contact'
import type { Attachment } from '@hcengineering/attachment'
import core, { type WithLookup } from '@hcengineering/core'
import core, { BlobType, type WithLookup } from '@hcengineering/core'
import presentation, {
canPreviewFile,
getBlobRef,
@ -31,16 +31,16 @@
import { createEventDispatcher, onMount } from 'svelte'
import { getResource } from '@hcengineering/platform'
import { Readable } from 'svelte/store'
import { getType, openAttachmentInSidebar, showAttachmentPreviewPopup } from '../utils'
import { getType, isAttachment, openAttachmentInSidebar, showAttachmentPreviewPopup } from '../utils'
import AttachmentName from './AttachmentName.svelte'
export let value: WithLookup<Attachment> | undefined
export let value: WithLookup<Attachment> | BlobType | undefined
export let removable: boolean = false
export let showPreview = false
export let preview = false
const dispatch = createEventDispatcher()
let permissionsStore: Readable<PermissionsStore>
let permissionsStore: Readable<PermissionsStore> | undefined = undefined
onMount(async () => {
permissionsStore = await getResource(contact.store.Permissions)
@ -51,13 +51,22 @@
const trimFilename = (fname: string): string =>
fname.length > maxLength ? fname.substr(0, (maxLength - 1) / 2) + '...' + fname.substr(-(maxLength - 1) / 2) : fname
$: canRemove =
removable &&
value !== undefined &&
value.readonly !== true &&
permissionsStore != null &&
($permissionsStore.whitelist.has(value.space) ||
!$permissionsStore.ps[value.space]?.has(core.permission.ForbidDeleteObject))
$: canRemove = isRemovable(removable, value, $permissionsStore)
function isRemovable (
removable: boolean,
value: Attachment | BlobType | undefined,
permissionsStore: PermissionsStore | undefined
): boolean {
if (value === undefined || !removable) return false
if (!isAttachment(value)) return true
if (permissionsStore === undefined) return false
return (
value.readonly !== true &&
(permissionsStore.whitelist.has(value.space) ||
!permissionsStore.ps[value.space]?.has(core.permission.ForbidDeleteObject))
)
}
function iconLabel (name: string): string {
const parts = `${name}`.split('.')
@ -201,7 +210,7 @@
</a>
</div>
<div class="info-content flex-row-center">
{filesize(value.size, { spacer: '' })}
{#if isAttachment(value)}{filesize(value.size, { spacer: '' })}{/if}
<span class="actions inline-flex clear-mins ml-1 gap-1">
<span></span>
<a class="no-line colorInherit" href={valueRef.src} download={value.name} bind:this={download}>

View File

@ -17,16 +17,16 @@
import { Attachment } from '@hcengineering/attachment'
import { ListSelectionProvider } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { WithLookup } from '@hcengineering/core'
import { BlobType, WithLookup } from '@hcengineering/core'
import { AttachmentImageSize } from '../types'
import { getType, showAttachmentPreviewPopup } from '../utils'
import { getType, isAttachment, showAttachmentPreviewPopup } from '../utils'
import AttachmentActions from './AttachmentActions.svelte'
import AttachmentImagePreview from './AttachmentImagePreview.svelte'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import AttachmentVideoPreview from './AttachmentVideoPreview.svelte'
import AudioPlayer from './AudioPlayer.svelte'
export let value: WithLookup<Attachment>
export let value: WithLookup<Attachment> | BlobType
export let isSaved: boolean = false
export let listProvider: ListSelectionProvider | undefined = undefined
export let imageSize: AttachmentImageSize = 'auto'
@ -44,7 +44,7 @@
<div
class="content flex-center buttonContainer cursor-pointer"
on:click={() => {
if (listProvider !== undefined) listProvider.updateFocus(value)
if (listProvider !== undefined && isAttachment(value)) listProvider.updateFocus(value)
const popupInfo = showAttachmentPreviewPopup(value)
dispatch('open', popupInfo.id)
}}

View File

@ -13,9 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import attachment, { Attachment, BlobMetadata, AttachmentsEvents } from '@hcengineering/attachment'
import attachment, { Attachment, AttachmentsEvents } from '@hcengineering/attachment'
import contact from '@hcengineering/contact'
import { Doc, PersonId, Ref, generateId, type Blob } from '@hcengineering/core'
import { BlobMetadata, Doc, PersonId, Ref, generateId, type Blob } from '@hcengineering/core'
import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
FileOrBlob,

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Attachment, BlobMetadata } from '@hcengineering/attachment'
import { Attachment } from '@hcengineering/attachment'
import {
Class,
Doc,
@ -24,7 +24,8 @@
toIdMap,
type Blob,
TxOperations,
PersonId
PersonId,
BlobMetadata
} from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {

View File

@ -14,11 +14,11 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import type { BlobType, WithLookup } from '@hcengineering/core'
import { getFileUrl, getVideoMeta } from '@hcengineering/presentation'
import { HlsVideo, Video } from '@hcengineering/ui'
export let value: WithLookup<Attachment>
export let value: WithLookup<Attachment> | BlobType
export let preload = true
const maxSizeRem = 20
@ -27,7 +27,7 @@
$: dimensions = getDimensions(value)
function getDimensions (value: Attachment): { width: number, height: number } {
function getDimensions (value: Attachment | BlobType): { width: number, height: number } {
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
if (!value.metadata) {

View File

@ -14,13 +14,13 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import type { BlobType, WithLookup } from '@hcengineering/core'
import { getFileUrl } from '@hcengineering/presentation'
import { CircleButton, Progress } from '@hcengineering/ui'
import Pause from './icons/Pause.svelte'
import Play from './icons/Play.svelte'
export let value: WithLookup<Attachment>
export let value: WithLookup<Attachment> | BlobType
export let fullSize = false
let time = 0

View File

@ -13,8 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { Blob, Ref } from '@hcengineering/core'
import { BlobMetadata } from '@hcengineering/presentation'
import type { BlobMetadata, Blob, Ref } from '@hcengineering/core'
import { Button, closePopup, closeTooltip, IconToDetails } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'

View File

@ -22,7 +22,6 @@ import { type ViewAction } from '@hcengineering/view'
export default mergeIds(attachmentId, attachment, {
string: {
NoAttachments: '' as IntlString,
UploadDropFilesHere: '' as IntlString,
Photos: '' as IntlString,
FileBrowserFileCounter: '' as IntlString,
FileBrowserListView: '' as IntlString,

View File

@ -14,8 +14,9 @@
// limitations under the License.
//
import { type BlobMetadata, type Attachment, type Drawing } from '@hcengineering/attachment'
import { type Attachment, type Drawing } from '@hcengineering/attachment'
import core, {
type BlobMetadata,
SortingOrder,
type Blob,
type Class,
@ -24,7 +25,8 @@ import core, {
type Doc,
type Ref,
type Space,
type WithLookup
type WithLookup,
type BlobType
} from '@hcengineering/core'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
@ -116,7 +118,7 @@ export function getType (
return 'other'
}
export async function openAttachmentInSidebar (value: Attachment): Promise<void> {
export async function openAttachmentInSidebar (value: Attachment | BlobType): Promise<void> {
closeTooltip()
await openFilePreviewInSidebar(value.file, value.name, value.type, value.metadata)
}
@ -151,10 +153,14 @@ export async function openFilePreviewInSidebar (
await createFn(widget, tab, true)
}
export function showAttachmentPreviewPopup (value: WithLookup<Attachment>): PopupResult {
export function isAttachment (value: Attachment | BlobType): value is WithLookup<Attachment> {
return (value as Attachment)._id !== undefined
}
export function showAttachmentPreviewPopup (value: WithLookup<Attachment> | BlobType): PopupResult {
const props: Record<string, any> = {}
if (value?.type?.startsWith('image/')) {
if (value?.type?.startsWith('image/') && isAttachment(value)) {
props.drawingAvailable = true
props.loadDrawings = async (): Promise<Drawing[] | undefined> => {
const client = getClient()

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import type { AttachedDoc, Blob, Class, Doc, Ref } from '@hcengineering/core'
import type { AttachedDoc, Blob, BlobMetadata, Class, Doc, Ref } from '@hcengineering/core'
import type { Asset, Plugin } from '@hcengineering/platform'
import { IntlString, plugin, Resource } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
@ -23,11 +23,6 @@ import { Widget } from '@hcengineering/workbench'
export * from './analytics'
/**
* @public
*/
export type BlobMetadata = Record<string, any>
/**
* @public
*/
@ -109,6 +104,7 @@ export default plugin(attachmentId, {
PreviewWidget: '' as Ref<Widget>
},
string: {
UploadDropFilesHere: '' as IntlString,
Files: '' as IntlString,
NoFiles: '' as IntlString,
NoParticipants: '' as IntlString,

View File

@ -37,4 +37,7 @@
<path d="M3 8C3 8.55229 2.55228 9 2 9C1.44772 9 1 8.55229 1 8C1 7.44772 1.44772 7 2 7C2.55228 7 3 7.44772 3 8Z" />
<path d="M2 13.5C2.55228 13.5 3 13.0523 3 12.5C3 11.9477 2.55228 11.5 2 11.5C1.44772 11.5 1 11.9477 1 12.5C1 13.0523 1.44772 13.5 2 13.5Z" />
</symbol>
<symbol id="file" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6Z" fill="currentColor"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -20,5 +20,6 @@ loadMetadata(card.icon, {
MasterTag: `${icons}#masterTag`,
Tags: `${icons}#tags`,
Tag: `${icons}#tag`,
Card: `${icons}#card`
Card: `${icons}#card`,
File: `${icons}#file`
})

View File

@ -47,6 +47,7 @@
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/setting-resources": "^0.6.0",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/uploader": "^0.6.0",
"@hcengineering/text": "^0.6.5",
"@hcengineering/panel": "^0.6.23",
"@hcengineering/rank": "^0.6.4",

View File

@ -1,3 +1,17 @@
<!--
// Copyright © 2025 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 { Card, CardEvents } from '@hcengineering/card'
import core, { Data, Doc, fillDefaults, MarkupBlobRef, SortingOrder, WithLookup } from '@hcengineering/core'
@ -102,6 +116,7 @@
title,
rank: makeRank(lastOne?.rank, undefined),
content: '' as MarkupBlobRef,
blobs: {},
parentInfo: [
...(object.parentInfo ?? []),
{

View File

@ -0,0 +1,46 @@
<!--
// Copyright © 2025 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 card, { Card } from '@hcengineering/card'
import { getClient } from '@hcengineering/presentation'
import Description from './Description.svelte'
import FilePreview from '@hcengineering/presentation/src/components/FilePreview.svelte'
import FilePlaceholder from './FilePlaceholder.svelte'
export let doc: Card
export let readonly: boolean = false
export let content: HTMLElement
const client = getClient()
const hierarchy = client.getHierarchy()
$: hasDescription = !hierarchy.isDerived(doc._class, card.types.File)
</script>
{#if hasDescription}
<Description {doc} {readonly} bind:content />
{:else if Object.keys(doc.blobs ?? {}).length === 0 && !readonly}
<FilePlaceholder {doc} />
{/if}
{#each Object.values(doc.blobs ?? {}) as blob}
<FilePreview
file={blob.file}
contentType={blob.type}
name={blob.name}
metadata={blob.metadata}
fit={blob.type !== 'application/pdf'}
/>
{/each}

View File

@ -14,12 +14,10 @@
-->
<script lang="ts">
import { MasterTag, Tag } from '@hcengineering/card'
import core, { Class, ClassifierKind, Data, Doc, Ref, generateId } from '@hcengineering/core'
import core, { Class, ClassifierKind, Data, Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Card, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { EditBox, Icon, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import card from '../plugin'
@ -40,8 +38,7 @@
icon: isMasterTag ? card.icon.MasterTag : card.icon.Tag
}
const id = generateId<Class<MasterTag>>()
await client.createDoc(_class, core.space.Model, data, id)
await client.createDoc(_class, core.space.Model, data)
dispatch('close')
}

View File

@ -0,0 +1,132 @@
<!--
// Copyright © 2025 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 attachment from '@hcengineering/attachment'
import { Card } from '@hcengineering/card'
import { Blob, Ref } from '@hcengineering/core'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Heading } from '@hcengineering/text-editor'
import { TableOfContents } from '@hcengineering/text-editor-resources'
import { navigate } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import ContentEditor from './ContentEditor.svelte'
export let doc: Card
export let readonly: boolean = false
export let content: HTMLElement
const client = getClient()
let editor: ContentEditor
async function createEmbedding (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
if (doc === undefined) {
return undefined
}
try {
const uploadFile = await getResource(attachment.helper.UploadFile)
const uuid = await uploadFile(file)
// const attachmentId: Ref<Attachment> = generateId()
// await client.addCollection(
// attachment.class.Embedding,
// doc.space,
// doc._id,
// doc._class,
// 'embeddings',
// {
// file: uuid,
// name: file.name,
// type: file.type,
// size: file.size,
// lastModified: file.lastModified
// },
// attachmentId
// )
return { file: uuid, type: file.type }
} catch (err: any) {
await setPlatformStatus(unknownError(err))
}
}
let headings: Heading[] = []
</script>
<div class="content select-text mt-4">
<div class="toc-container">
<div class="toc">
<TableOfContents
items={headings}
on:select={(evt) => {
const heading = evt.detail
const element = window.document.getElementById(heading.id)
element?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}}
/>
</div>
</div>
{#key doc._id}
<ContentEditor
focusIndex={30}
object={doc}
{readonly}
boundary={content}
overflow={'none'}
editorAttributes={{ style: 'padding: 0 2em 2em; margin: 0 -2em; min-height: 30vh' }}
attachFile={async (file) => {
return await createEmbedding(file)
}}
on:headings={(evt) => {
headings = evt.detail
}}
on:open-document={async (event) => {
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
if (doc != null) {
const location = await getObjectLinkFragment(client.getHierarchy(), doc, {}, view.component.EditDoc)
navigate(location)
}
}}
bind:this={editor}
/>
{/key}
</div>
<style lang="scss">
.toc-container {
position: absolute;
pointer-events: none;
inset: 0;
z-index: 1;
}
.toc {
width: 1rem;
pointer-events: all;
margin-left: -3rem;
position: sticky;
top: 0;
}
.content {
position: relative;
color: var(--content-color);
line-height: 150%;
}
</style>

View File

@ -16,31 +16,22 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import attachment, { Attachment } from '@hcengineering/attachment'
import { Card, CardEvents } from '@hcengineering/card'
import { Doc, Mixin, Ref, WithLookup, generateId, type Blob } from '@hcengineering/core'
import { Doc, Mixin, Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Heading } from '@hcengineering/text-editor'
import { TableOfContents } from '@hcengineering/text-editor-resources'
import { Button, EditBox, FocusHandler, IconMoreH, createFocusManager, navigate } from '@hcengineering/ui'
import { Button, EditBox, FocusHandler, IconMoreH, createFocusManager } from '@hcengineering/ui'
import view from '@hcengineering/view'
import {
ParentsNavigator,
RelationsEditor,
getDocMixins,
getObjectLinkFragment,
showMenu
} from '@hcengineering/view-resources'
import { ParentsNavigator, RelationsEditor, getDocMixins, showMenu } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import card from '../plugin'
import CardAttributeEditor from './CardAttributeEditor.svelte'
import CardPresenter from './CardPresenter.svelte'
import ContentEditor from './ContentEditor.svelte'
import TagsEditor from './TagsEditor.svelte'
import Childs from './Childs.svelte'
import Content from './Content.svelte'
import TagsEditor from './TagsEditor.svelte'
export let _id: Ref<Card>
export let readonly: boolean = false
@ -62,16 +53,11 @@
let title = ''
let innerWidth: number
let headings: Heading[] = []
let loadedDocumentContent = false
const notificationClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
$: read(_id)
function read (_id: Ref<Doc>): void {
if (lastId !== _id) {
loadedDocumentContent = false
const prev = lastId
lastId = _id
void notificationClient.then((client) => client.readDoc(prev))
@ -82,38 +68,6 @@
void notificationClient.then((client) => client.readDoc(_id))
})
async function createEmbedding (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
if (doc === undefined) {
return undefined
}
try {
const uploadFile = await getResource(attachment.helper.UploadFile)
const uuid = await uploadFile(file)
const attachmentId: Ref<Attachment> = generateId()
await client.addCollection(
attachment.class.Embedding,
doc.space,
doc._id,
doc._class,
'embeddings',
{
file: uuid,
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified
},
attachmentId
)
return { file: uuid, type: file.type }
} catch (err: any) {
await setPlatformStatus(unknownError(err))
}
}
$: _id !== undefined &&
query.query(card.class.Card, { _id }, async (result) => {
;[doc] = result
@ -145,17 +99,10 @@
localStorage.setItem('document.useMaxWidth', useMaxWidth.toString())
}
let sideContentSpace = 0
function updateSizeContentSpace (width: number): void {
sideContentSpace = width
}
onMount(() => {
Analytics.handleEvent(CardEvents.CardOpened, { id: _id })
})
let editor: ContentEditor
let content: HTMLElement
const manager = createFocusManager()
@ -169,14 +116,12 @@
{#if doc !== undefined}
<Panel
withoutActivity={!loadedDocumentContent}
object={doc}
allowClose={!embedded}
isAside={false}
isHeader={false}
isSub={false}
bind:useMaxWidth
{sideContentSpace}
printHeader={false}
{embedded}
adaptive={'default'}
@ -193,66 +138,14 @@
<div class="container">
<div class="title flex-row-center">
<EditBox
focusIndex={1}
bind:value={title}
placeholder={card.string.Card}
on:blur={(evt) => saveTitle(evt)}
on:keydown={(evt) => {
if (evt.key === 'Enter' || evt.key === 'ArrowDown') {
editor.focus('start')
}
}}
/>
<EditBox focusIndex={1} bind:value={title} placeholder={card.string.Card} on:blur={(evt) => saveTitle(evt)} />
</div>
<TagsEditor {doc} />
<CardAttributeEditor value={doc} {mixins} {readonly} ignoreKeys={['title', 'content', 'parent']} />
<div class="content select-text mt-4">
<div class="toc-container">
<div class="toc">
<TableOfContents
items={headings}
on:select={(evt) => {
const heading = evt.detail
const element = window.document.getElementById(heading.id)
element?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}}
/>
</div>
</div>
{#key doc._id}
<ContentEditor
focusIndex={30}
object={doc}
{readonly}
boundary={content}
overflow={'none'}
editorAttributes={{ style: 'padding: 0 2em 2em; margin: 0 -2em; min-height: 30vh' }}
requestSideSpace={updateSizeContentSpace}
attachFile={async (file) => {
return await createEmbedding(file)
}}
on:headings={(evt) => {
headings = evt.detail
}}
on:open-document={async (event) => {
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
if (doc != null) {
const location = await getObjectLinkFragment(client.getHierarchy(), doc, {}, view.component.EditDoc)
navigate(location)
}
}}
on:loaded={() => {
loadedDocumentContent = true
}}
bind:this={editor}
/>
{/key}
</div>
<Content {doc} {readonly} bind:content />
</div>
<Childs object={doc} {readonly} />
@ -281,27 +174,6 @@
width: 100%;
margin: auto;
}
.toc-container {
position: absolute;
pointer-events: none;
inset: 0;
z-index: 1;
}
.toc {
width: 1rem;
pointer-events: all;
margin-left: -3rem;
position: sticky;
top: 0;
}
.content {
position: relative;
color: var(--content-color);
line-height: 150%;
}
.title {
font-size: 2.25rem;
margin-top: 1.75rem;

View File

@ -0,0 +1,114 @@
<!--
// Copyright © 2025 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 attachment from '@hcengineering/attachment'
import { Card } from '@hcengineering/card'
import { getClient, getFileMetadata } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { FileUploadCallbackParams, uploadFiles } from '@hcengineering/uploader'
import UploadDuo from './icons/UploadDuo.svelte'
export let doc: Card
const client = getClient()
let inputFile: HTMLInputElement
let dragover = false
async function onFileUploaded ({ uuid, name, file, type }: FileUploadCallbackParams): Promise<void> {
const metadata = await getFileMetadata(file, uuid)
const blobs = doc.blobs ?? {}
blobs[uuid] = {
name,
type,
metadata,
file: uuid
}
await client.update(doc, {
blobs
})
}
async function fileSelected (): Promise<void> {
const list = inputFile.files
if (list === null || list.length === 0) return
const options = {
onFileUploaded,
showProgress: {
target: { objectId: doc._id, objectClass: doc._class }
}
}
await uploadFiles(list, options)
inputFile.value = ''
}
async function fileDrop (e: DragEvent): Promise<void> {
dragover = false
e.preventDefault()
e.stopPropagation()
const list = e.dataTransfer?.files
if (list === undefined || list.length === 0) return
const options = {
onFileUploaded,
showProgress: {
target: { objectId: doc._id, objectClass: doc._class }
}
}
await uploadFiles(list, options)
}
</script>
<input
bind:this={inputFile}
disabled={inputFile == null}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected}
/>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:dragover={(e) => {
dragover = true
e.preventDefault()
}}
on:dragleave={() => {
dragover = false
}}
on:drop={fileDrop}
>
<div class="antiSection-empty attachments flex-col mt-3" class:solid={dragover}>
<div class="flex-center caption-color">
<UploadDuo size={'large'} />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="over-underline text-sm caption-color"
style:pointer-events={dragover ? 'none' : 'all'}
on:click={() => {
inputFile.click()
}}
>
<Label label={attachment.string.UploadDropFilesHere} />
</div>
</div>
</div>

View File

@ -36,7 +36,7 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const label = hierarchy.getClass(value._class).label
$: label = hierarchy.getClass(value._class).label
let isCollapsed = false

View File

@ -57,7 +57,8 @@
title,
rank: makeRank(lastOne?.rank, undefined),
content: '' as MarkupBlobRef,
parentInfo: []
parentInfo: [],
blobs: {}
}
const filledData = fillDefaults(hierarchy, data, _class)

View File

@ -0,0 +1,31 @@
<!--
// Copyright © 2025 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">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="var(--duotone-color)"
d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0-0.1-0.1 c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1 C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h6h6 c2,0,3.6-1.6,3.6-3.6S20,6.4,18,6.4z"
/>
<g {fill}>
<path
d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0 c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1-0.1c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3 C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1c0,0,0,0,0,0C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0 c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h0.6l1.2-1.2H6c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4h0.1 c0.2,0,0.4,0,0.6,0c0.2,0,0.4-0.1,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.1-0.2,0.2-0.3C7.8,6.5,7.9,6.4,8,6.2l0,0 c0.7-1.5,2.2-2.6,4-2.6s3.3,1.1,4,2.6l0,0c0.1,0.2,0.1,0.3,0.2,0.4c0,0.1,0.1,0.2,0.2,0.3c0.1,0.2,0.2,0.3,0.4,0.4 c0.2,0.1,0.4,0.2,0.6,0.2c0.2,0,0.4,0,0.6,0H18c1.3,0,2.4,1.1,2.4,2.4c0,1.3-1.1,2.4-2.4,2.4h-1.8l1.2,1.2H18c2,0,3.6-1.6,3.6-3.6 S20,6.4,18,6.4z"
/>
<path d="M12,11.2l-4.4,4.4l0.8,0.8l3-3V21c0,0.3,0.3,0.6,0.6,0.6s0.6-0.3,0.6-0.6v-7.6l3,3l0.8-0.8L12,11.2z" />
</g>
</svg>

View File

@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Class, Mixin, Doc, Ref, MarkupBlobRef, Rank, Domain } from '@hcengineering/core'
import { Blobs, Class, Doc, Domain, MarkupBlobRef, Mixin, Rank, Ref } from '@hcengineering/core'
import { Asset, IntlString, plugin, Plugin } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
@ -22,10 +22,10 @@ export interface MasterTag extends Class<Card> {}
export interface Tag extends MasterTag, Mixin<Card> {}
export interface Card extends Doc {
attachments?: number
_class: Ref<MasterTag>
title: string
content: MarkupBlobRef
blobs: Blobs
children?: number
parentInfo: ParentInfo[]
parent?: Ref<Card> | null
@ -62,12 +62,16 @@ const cardPlugin = plugin(cardId, {
Tag: '' as Ref<Class<Tag>>,
MasterTagEditorSection: '' as Ref<Class<MasterTagEditorSection>>
},
types: {
File: '' as Ref<MasterTag>
},
icon: {
MasterTags: '' as Asset,
MasterTag: '' as Asset,
Tag: '' as Asset,
Tags: '' as Asset,
Card: '' as Asset
Card: '' as Asset,
File: '' as Asset
},
string: {
MasterTag: '' as IntlString,

View File

@ -4,9 +4,9 @@
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { type Blob, type BlobMetadata, type Ref } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation, { BlobMetadata, getFileUrl } from '@hcengineering/presentation'
import presentation, { getFileUrl } from '@hcengineering/presentation'
import { EmbeddedPDF, Spinner, themeStore } from '@hcengineering/ui'
import { convertToHTML } from '@hcengineering/print'

View File

@ -54,6 +54,7 @@ async function uploadRecording (recordingName: string, onUploaded: FileUploadCal
uuid: getBlobUrl(recordingName) as Ref<Blob>,
name: 'Recording-' + now(),
file: { ...new Blob(), type: 'video/x-mpegURL' },
type: 'video/x-mpegURL',
path: undefined,
metadata: undefined,
navigateOnUpload: true

View File

@ -94,7 +94,7 @@ export async function uploadFiles (files: File[] | FileList, options: FileUpload
const { relativePath } = data
const uuid = data.name
void limiter.add(async () => {
await uploadFile(data, { name: files[i].name, uuid, relativePath }, upload, options)
await uploadFile(data, { name: files[i].name, uuid, type: data.type, relativePath }, upload, options)
})
}
@ -156,6 +156,7 @@ export async function uploadFile (
try {
void callbackLimiter.exec(async () => {
void onFileUploaded({
type: metadata.type,
uuid,
name: metadata.name,
file,

View File

@ -70,6 +70,7 @@ export interface FileUploadPopupOptions {
export interface FileUploadCallbackParams {
uuid: Ref<PlatformBlob>
name: string
type: string
file: FileWithPath | Blob
path: string | undefined
metadata: Record<string, any> | undefined

View File

@ -13,8 +13,8 @@
// limitations under the License.
//
import { type Blob, type Ref } from '@hcengineering/core'
import { type BlobMetadata, getImageSize } from '@hcengineering/presentation'
import { type Blob, type BlobMetadata, type Ref } from '@hcengineering/core'
import { getImageSize } from '@hcengineering/presentation'
export async function blobImageMetadata (file: File, blob: Ref<Blob>): Promise<BlobMetadata | undefined> {
if (file.size === 0) {

View File

@ -13,8 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { DrawingBoard, getBlobRef, imageSizeToRatio, type BlobMetadata } from '@hcengineering/presentation'
import { type Blob, type Ref, type BlobMetadata } from '@hcengineering/core'
import { DrawingBoard, getBlobRef, imageSizeToRatio } from '@hcengineering/presentation'
import { Loading } from '@hcengineering/ui'
export let value: Ref<Blob>

View File

@ -13,8 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { getFileUrl, getVideoMeta, type BlobMetadata } from '@hcengineering/presentation'
import { type Blob, type BlobMetadata, type Ref } from '@hcengineering/core'
import { getFileUrl, getVideoMeta } from '@hcengineering/presentation'
import { HlsVideo, Video } from '@hcengineering/ui'
export let value: Ref<Blob>

View File

@ -187,6 +187,20 @@ async function OnCardRemove (ctx: TxRemoveDoc<Card>[], control: TriggerControl):
for (const card of cards) {
res.push(control.txFactory.createTxRemoveDoc(card._class, card.space, card._id))
}
const toDelete: string[] = []
for (const key in removedCard.blobs ?? {}) {
const val = removedCard.blobs[key]
if (val === undefined) continue
const toDelete: string[] = []
toDelete.push(val.file)
}
if (toDelete.length > 0) {
await control.storageAdapter.remove(control.ctx, control.workspace, toDelete)
}
if (removedCard.parent != null) {
res.push(
control.txFactory.createTxUpdateDoc(card.class.Card, core.space.Workspace, removedCard.parent, {