Enhanced drawing overlay for screenshots (#7231)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
This commit is contained in:
Chunosov 2024-11-26 13:45:49 +07:00 committed by GitHub
parent aea704fd0f
commit 27547e580c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 284 additions and 96 deletions

View File

@ -241,6 +241,10 @@ export function createModel (builder: Builder): void {
{ _class: 1 } { _class: 1 }
] ]
}) })
builder.mixin(attachment.class.Drawing, core.class.Class, view.mixin.ObjectPresenter, {
presenter: attachment.component.DrawingPresenter
})
} }
export default attachment export default attachment

View File

@ -34,7 +34,8 @@
"FailedToPreview": "Náhled se nezdařil", "FailedToPreview": "Náhled se nezdařil",
"ContentType": "Typ obsahu", "ContentType": "Typ obsahu",
"ContentTypeNotSupported": "Náhled není dostupný pro tento typ obsahu", "ContentTypeNotSupported": "Náhled není dostupný pro tento typ obsahu",
"StartDrawing": "Načmárejte" "StartDrawing": "Načmárejte",
"DrawingHistory": "Čmárání historie"
}, },
"status": { "status": {
"FileTooLarge": "Soubor je příliš velký" "FileTooLarge": "Soubor je příliš velký"

View File

@ -34,7 +34,8 @@
"FailedToPreview": "Failed to preview", "FailedToPreview": "Failed to preview",
"ContentType": "Content type", "ContentType": "Content type",
"ContentTypeNotSupported": "Preview is not available for this content type", "ContentTypeNotSupported": "Preview is not available for this content type",
"StartDrawing": "Scribble over" "StartDrawing": "Scribble over",
"DrawingHistory": "Scribble history"
}, },
"status": { "status": {
"FileTooLarge": "File too large" "FileTooLarge": "File too large"

View File

@ -34,7 +34,8 @@
"FailedToPreview": "Error al previsualizar", "FailedToPreview": "Error al previsualizar",
"ContentType": "Tipo de contenido", "ContentType": "Tipo de contenido",
"ContentTypeNotSupported": "La vista previa no está disponible para este tipo de contenido", "ContentTypeNotSupported": "La vista previa no está disponible para este tipo de contenido",
"StartDrawing": "Garabatear encima" "StartDrawing": "Garabatear encima",
"DrawingHistory": "Historia de garabatos"
}, },
"status": { "status": {
"FileTooLarge": "Archivo demasiado grande" "FileTooLarge": "Archivo demasiado grande"

View File

@ -34,7 +34,8 @@
"FailedToPreview": "Échec de l'aperçu", "FailedToPreview": "Échec de l'aperçu",
"ContentType": "Type de contenu", "ContentType": "Type de contenu",
"ContentTypeNotSupported": "L'aperçu n'est pas disponible pour ce type de contenu", "ContentTypeNotSupported": "L'aperçu n'est pas disponible pour ce type de contenu",
"StartDrawing": "Gribouiller dessus" "StartDrawing": "Gribouiller dessus",
"DrawingHistory": "Histoire de gribouillage"
}, },
"status": { "status": {
"FileTooLarge": "Fichier trop volumineux" "FileTooLarge": "Fichier trop volumineux"

View File

@ -34,7 +34,8 @@
"FailedToPreview": "Impossibile mostrare l'anteprima", "FailedToPreview": "Impossibile mostrare l'anteprima",
"ContentType": "Tipo di contenuto", "ContentType": "Tipo di contenuto",
"ContentTypeNotSupported": "Anteprima non disponibile per questo tipo di contenuto", "ContentTypeNotSupported": "Anteprima non disponibile per questo tipo di contenuto",
"StartDrawing": "Scarabocchiare sopra" "StartDrawing": "Scarabocchiare sopra",
"DrawingHistory": "Scarabocchiare la storia"
}, },
"status": { "status": {
"FileTooLarge": "File troppo grande" "FileTooLarge": "File troppo grande"

View File

@ -34,7 +34,8 @@
"FailedToPreview": "Falha ao pré-visualizar", "FailedToPreview": "Falha ao pré-visualizar",
"ContentType": "Tipo de conteúdo", "ContentType": "Tipo de conteúdo",
"ContentTypeNotSupported": "A visualização não está disponível para este tipo de conteúdo", "ContentTypeNotSupported": "A visualização não está disponível para este tipo de conteúdo",
"StartDrawing": "Scarabocchiare sopra" "StartDrawing": "Scarabocchiare sopra",
"DrawingHistory": "História de rabiscos"
}, },
"status": { "status": {
"FileTooLarge": "Ficheiro demasiado grande" "FileTooLarge": "Ficheiro demasiado grande"

View File

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

View File

@ -34,7 +34,8 @@
"FailedToPreview": "预览失败", "FailedToPreview": "预览失败",
"ContentType": "内容类型", "ContentType": "内容类型",
"ContentTypeNotSupported": "此內容類型無法預覽", "ContentTypeNotSupported": "此內容類型無法預覽",
"StartDrawing": "随意涂鸦" "StartDrawing": "随意涂鸦",
"DrawingHistory": "涂鸦的历史"
}, },
"status": { "status": {
"FileTooLarge": "文件太大" "FileTooLarge": "文件太大"

View File

@ -14,8 +14,9 @@
--> -->
<script lang="ts"> <script lang="ts">
import { getObjectValue, type Class, type Doc, type Ref } from '@hcengineering/core' import { getObjectValue, type Class, type Doc, type Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import { getResource, type IntlString } from '@hcengineering/platform'
import { import {
AnySvelteComponent,
Button, Button,
EditWithIcon, EditWithIcon,
FocusHandler, FocusHandler,
@ -31,6 +32,7 @@
showPopup, showPopup,
tooltip tooltip
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import presentation from '..' import presentation from '..'
import { ObjectCreate } from '../types' import { ObjectCreate } from '../types'
@ -47,7 +49,7 @@
export let placeholder: IntlString = presentation.string.Search export let placeholder: IntlString = presentation.string.Search
export let selectedObjects: Ref<Doc>[] = [] export let selectedObjects: Ref<Doc>[] = []
export let shadows: boolean = true export let shadows: boolean = true
export let width: 'medium' | 'large' | 'full' = 'medium' export let width: 'medium' | 'large' | 'full' | 'auto' = 'medium'
export let size: 'small' | 'medium' | 'large' = 'large' export let size: 'small' | 'medium' | 'large' = 'large'
export let noSearchField: boolean = false export let noSearchField: boolean = false
@ -59,7 +61,7 @@
export let created: Doc[] = [] export let created: Doc[] = []
export let embedded: boolean = false export let embedded: boolean = false
export let loading: boolean = false export let loading: boolean = false
export let type: 'text' | 'object' = 'text' export let type: 'text' | 'object' | 'presenter' = 'text'
let search: string = '' let search: string = ''
@ -71,6 +73,11 @@
created.length > 0 || created.length > 0 ||
objects.map((it) => getObjectValue(groupBy, it)).filter((it, index, arr) => arr.indexOf(it) === index).length > 1 objects.map((it) => getObjectValue(groupBy, it)).filter((it, index, arr) => arr.indexOf(it) === index).length > 1
let presenter: AnySvelteComponent | undefined = undefined
$: if (type === 'presenter') {
findObjectPresenter(_class)
}
const checkSelected = (item?: Doc): void => { const checkSelected = (item?: Doc): void => {
if (item === undefined) { if (item === undefined) {
return return
@ -154,6 +161,19 @@
} }
return getObjectValue(groupBy, toAny(doc)) return getObjectValue(groupBy, toAny(doc))
} }
function findObjectPresenter (_class: Ref<Class<Doc>>): void {
const presenterMixin = client.getHierarchy().classHierarchyMixin(_class, view.mixin.ObjectPresenter)
if (presenterMixin?.presenter !== undefined) {
getResource(presenterMixin.presenter)
.then((result) => {
presenter = result
})
.catch((err) => {
console.error('Failed to find presenter for class ' + _class, err)
})
}
}
</script> </script>
<FocusHandler {manager} /> <FocusHandler {manager} />
@ -164,6 +184,7 @@
class:full-width={width === 'full'} class:full-width={width === 'full'}
class:plainContainer={!shadows} class:plainContainer={!shadows}
class:width-40={width === 'large'} class:width-40={width === 'large'}
class:auto={width === 'auto'}
class:embedded class:embedded
on:keydown={onKeydown} on:keydown={onKeydown}
use:resizeObserver={() => { use:resizeObserver={() => {
@ -229,6 +250,10 @@
<span class="label" class:disabled={readonly || isDeselectDisabled || loading}> <span class="label" class:disabled={readonly || isDeselectDisabled || loading}>
<slot name="item" item={obj} /> <slot name="item" item={obj} />
</span> </span>
{:else if type === 'presenter'}
{#if presenter !== undefined}
<svelte:component this={presenter} value={obj} />
{/if}
{:else} {:else}
<slot name="item" item={obj} /> <slot name="item" item={obj} />
{/if} {/if}

View File

@ -14,25 +14,30 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Button, IconDelete, IconEdit, resizeObserver } from '@hcengineering/ui' import { Button, IconDelete, IconEdit, resizeObserver } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { drawing, type DrawingData, type DrawingTool } from '../drawing' import { drawing, type DrawingData, type DrawingTool } from '../drawing'
import IconEraser from './icons/Eraser.svelte' import IconEraser from './icons/Eraser.svelte'
export let active = false export let active = false
export let readonly = false export let readonly = true
export let imageWidth: number | undefined export let imageWidth: number | undefined
export let imageHeight: number | undefined export let imageHeight: number | undefined
export let drawingData: DrawingData export let drawings: DrawingData[]
export let saveDrawing: (data: any) => Promise<void> export let createDrawing: (data: any) => Promise<void>
let drawingTool: DrawingTool = 'pen' let drawingTool: DrawingTool = 'pen'
let penColor = 'blue' let penColor = 'blue'
const penColors = ['red', 'green', 'blue', 'white', 'black'] const penColors = ['red', 'green', 'blue', 'white', 'black']
let drawingData: DrawingData | undefined
let board: HTMLDivElement let board: HTMLDivElement
let toolbar: HTMLDivElement let toolbar: HTMLDivElement
let toolbarInside = false let toolbarInside = false
let oldReadonly: boolean
let oldDrawings: DrawingData[]
let modified = false
$: updateToolbarPosition(readonly, board, toolbar) $: updateToolbarPosition(readonly, board, toolbar)
$: updateEditableState(drawings, readonly)
function updateToolbarPosition (readonly: boolean, board: HTMLDivElement, toolbar: HTMLDivElement): void { function updateToolbarPosition (readonly: boolean, board: HTMLDivElement, toolbar: HTMLDivElement): void {
if (!readonly && board?.offsetTop !== undefined && toolbar?.clientHeight !== undefined) { if (!readonly && board?.offsetTop !== undefined && toolbar?.clientHeight !== undefined) {
@ -41,9 +46,47 @@
toolbarInside = board.offsetTop <= toolbar.clientHeight * 3 toolbarInside = board.offsetTop <= toolbar.clientHeight * 3
} }
} }
function updateEditableState (drawings: DrawingData[], readonly: boolean): void {
if (readonly !== oldReadonly || drawings !== oldDrawings) {
if (drawings !== undefined) {
if (readonly) {
if (modified && drawingData !== undefined) {
createDrawing(drawingData).catch((error) => {
console.error('Failed to save drawing', error)
})
}
drawingData = drawings[0]
modified = false
} else {
if (drawingData === undefined) {
drawingData = {}
} else {
// Edit current content as a new drawing
drawingData = {
content: drawingData.content
}
}
modified = false
}
} else {
drawingData = undefined
}
oldDrawings = drawings
oldReadonly = readonly
}
}
onDestroy(() => {
if (modified && drawingData !== undefined) {
createDrawing(drawingData).catch((error) => {
console.error('Failed to save drawing', error)
})
}
})
</script> </script>
{#if active} {#if active && drawingData !== undefined}
<div <div
{...$$restProps} {...$$restProps}
style:position="relative" style:position="relative"
@ -56,9 +99,14 @@
imageWidth, imageWidth,
imageHeight, imageHeight,
drawingData, drawingData,
saveDrawing,
drawingTool, drawingTool,
penColor penColor,
changed: (content) => {
modified = true
if (drawingData !== undefined) {
drawingData.content = content
}
}
}} }}
> >
{#if !readonly} {#if !readonly}
@ -68,6 +116,7 @@
kind="icon" kind="icon"
on:click={() => { on:click={() => {
drawingData = {} drawingData = {}
modified = true
}} }}
/> />
<div class="divider buttons-divider" /> <div class="divider buttons-divider" />

View File

@ -13,9 +13,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core' import { SortingOrder, type Blob, type Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform' import { getEmbeddedLabel } from '@hcengineering/platform'
import { Button, Dialog, IconEdit, tooltip } from '@hcengineering/ui' import { Button, Dialog, IconHistory, IconScribble, showPopup, tooltip } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { BlobMetadata } from '../types' import { BlobMetadata } from '../types'
@ -23,6 +23,7 @@
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import FilePreview from './FilePreview.svelte' import FilePreview from './FilePreview.svelte'
import DownloadFileButton from './DownloadFileButton.svelte' import DownloadFileButton from './DownloadFileButton.svelte'
import ObjectPopup from './ObjectPopup.svelte'
import { ComponentExtensions } from '../index' import { ComponentExtensions } from '../index'
import presentation from '../plugin' import presentation from '../plugin'
import FileTypeIcon from './FileTypeIcon.svelte' import FileTypeIcon from './FileTypeIcon.svelte'
@ -31,12 +32,19 @@
export let name: string export let name: string
export let contentType: string export let contentType: string
export let metadata: BlobMetadata | undefined export let metadata: BlobMetadata | undefined
export let props: Record<string, any> = {} export let props: Record<string, any> & {
drawings?: any[]
drawingAvailable?: boolean
drawingEditable?: boolean
loadDrawings?: () => Promise<any>
createDrawing?: (data: any) => Promise<any>
} = {}
export let fullSize = false export let fullSize = false
export let showIcon = true export let showIcon = true
let drawingLoading = false let drawingLoading = false
let createDrawing: (data: any) => Promise<any>
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -45,28 +53,67 @@
dispatch('fullsize') dispatch('fullsize')
} }
if (props.drawingAvailable === true) { if (props.drawingAvailable === true) {
loadDrawings(props.loadDrawings) if (props.loadDrawings !== undefined) {
}
})
function toggleDrawingEdit (): void {
const editable = props.drawingEditable === true
props = { ...props, drawingEditable: !editable }
}
function loadDrawings (load: () => Promise<any>): void {
if (load !== undefined) {
drawingLoading = true drawingLoading = true
load() props
.loadDrawings()
.then((result) => { .then((result) => {
drawingLoading = false drawingLoading = false
props.drawingData = result props.drawings = result
}) })
.catch((error) => { .catch((error) => {
drawingLoading = false drawingLoading = false
console.error('Failed to load drawings for file', file, error) console.error('Failed to load drawings for file', file, error)
}) })
} }
if (props.createDrawing !== undefined) {
createDrawing = props.createDrawing
props.createDrawing = async (data: any): Promise<any> => {
const newDrawing = await createDrawing(data)
if (props.drawings !== undefined) {
props.drawings = [newDrawing, ...props.drawings]
} else {
props.drawings = [newDrawing]
}
return newDrawing
}
}
}
})
function toggleDrawingEdit (): void {
props.drawingEditable = !(props.drawingEditable === true)
}
function selectCurrentDrawing (ev: MouseEvent): void {
if (props.drawings === undefined || props.drawings.length === 0) {
// no current means no history
return
}
showPopup(
ObjectPopup,
{
_class: props.drawings[0]._class,
selected: props.drawings[0]._id,
docQuery: {
parent: props.drawings[0].parent
},
options: {
sort: {
createdOn: SortingOrder.Descending
}
},
searchMode: 'disabled',
type: 'presenter',
width: 'auto'
},
ev.target as HTMLElement,
async (result) => {
if (result !== undefined) {
props.drawings = [result]
}
}
)
} }
</script> </script>
@ -91,13 +138,24 @@
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
{#if props.drawingAvailable === true} {#if props.drawingAvailable === true}
{#if props.drawings !== undefined && props.drawings.length > 0}
<Button <Button
icon={IconEdit} icon={IconHistory}
kind="icon"
disabled={drawingLoading || props.drawingEditable === true}
showTooltip={{ label: presentation.string.DrawingHistory }}
on:click={selectCurrentDrawing}
/>
{/if}
<Button
icon={IconScribble}
kind="icon" kind="icon"
disabled={drawingLoading} disabled={drawingLoading}
selected={props.drawingEditable === true}
showTooltip={{ label: presentation.string.StartDrawing }} showTooltip={{ label: presentation.string.StartDrawing }}
on:click={toggleDrawingEdit} on:click={toggleDrawingEdit}
/> />
<div class="buttons-divider" />
{/if} {/if}
<DownloadFileButton {name} {file} /> <DownloadFileButton {name} {file} />
<ComponentExtensions <ComponentExtensions

View File

@ -43,7 +43,7 @@
export let selectedObjects: Ref<Doc>[] = [] export let selectedObjects: Ref<Doc>[] = []
export let ignoreObjects: Ref<Doc>[] = [] export let ignoreObjects: Ref<Doc>[] = []
export let shadows: boolean = true export let shadows: boolean = true
export let width: 'medium' | 'large' | 'full' = 'medium' export let width: 'medium' | 'large' | 'full' | 'auto' = 'medium'
export let size: 'small' | 'medium' | 'large' = 'large' export let size: 'small' | 'medium' | 'large' = 'large'
export let searchMode: 'field' | 'fulltext' | 'disabled' = 'field' export let searchMode: 'field' | 'fulltext' | 'disabled' = 'field'
@ -55,7 +55,7 @@
export let disallowDeselect: Ref<Doc>[] | undefined = undefined export let disallowDeselect: Ref<Doc>[] | undefined = undefined
export let embedded: boolean = false export let embedded: boolean = false
export let loading: boolean = false export let loading: boolean = false
export let type: 'text' | 'object' = 'text' export let type: 'text' | 'object' | 'presenter' = 'text'
export let filter: (it: Doc) => boolean = () => { export let filter: (it: Doc) => boolean = () => {
return true return true

View File

@ -14,19 +14,17 @@
// //
export interface DrawingData { export interface DrawingData {
id?: string
content?: string content?: string
} }
export interface DrawingProps { export interface DrawingProps {
readonly?: boolean readonly: boolean
imageWidth?: number imageWidth?: number
imageHeight?: number imageHeight?: number
drawingData?: DrawingData drawingData: DrawingData
saveDrawing?: (data: any) => Promise<void>
drawingTool?: DrawingTool drawingTool?: DrawingTool
penColor?: string penColor?: string
changed?: (content: string) => void
} }
interface DrawCmd { interface DrawCmd {
@ -198,7 +196,6 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
draw.penColor = props.penColor ?? 'blue' draw.penColor = props.penColor ?? 'blue'
updateCanvasCursor() updateCanvasCursor()
let modified = false
let commands: DrawCmd[] = [] let commands: DrawCmd[] = []
let drawingData = props.drawingData let drawingData = props.drawingData
parseData() parseData()
@ -300,7 +297,7 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
points: draw.points points: draw.points
} }
commands.push(cmd) commands.push(cmd)
modified = true props.changed?.(JSON.stringify(commands))
} }
} }
@ -340,7 +337,7 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
function parseData (): void { function parseData (): void {
clearCanvas() clearCanvas()
if (drawingData?.content !== undefined) { if (drawingData.content !== undefined && drawingData.content !== null) {
try { try {
commands = JSON.parse(drawingData.content) commands = JSON.parse(drawingData.content)
replayCommands() replayCommands()
@ -356,43 +353,25 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
return { return {
update (props: DrawingProps) { update (props: DrawingProps) {
if (drawingData !== props.drawingData) { if (drawingData !== props.drawingData) {
// Currently it expectes only the empty data on update
// which means we pressed the "Clear canvas" button
// We don't support yet creation of multiple drawings for the same image
// so preserve the id to continue editing the previous drawing
const oldId = drawingData?.id
drawingData = props.drawingData drawingData = props.drawingData
if (drawingData !== undefined) {
drawingData.id = oldId
}
modified = true
parseData() parseData()
} }
let updateCursor = false
if (draw.tool !== props.drawingTool) { if (draw.tool !== props.drawingTool) {
draw.tool = props.drawingTool ?? 'pen' draw.tool = props.drawingTool ?? 'pen'
updateCanvasCursor() updateCursor = true
} }
if (draw.penColor !== props.penColor) { if (draw.penColor !== props.penColor) {
draw.penColor = props.penColor ?? 'blue' draw.penColor = props.penColor ?? 'blue'
updateCanvasCursor() updateCursor = true
} }
if (props.readonly !== readonly) { if (props.readonly !== readonly) {
readonly = props.readonly ?? false readonly = props.readonly ?? false
updateCursor = true
}
if (updateCursor) {
updateCanvasCursor() updateCanvasCursor()
} }
},
destroy () {
if (props.saveDrawing === undefined) {
console.log('Save drawing method is not provided')
} else {
if (modified && (commands.length > 0 || drawingData?.id !== undefined)) {
const data: DrawingData = drawingData ?? {}
data.content = JSON.stringify(commands)
props.saveDrawing(data).catch((error) => {
console.error('Failed to save drawing', error)
})
}
}
} }
} }
} }

View File

@ -122,7 +122,8 @@ export default plugin(presentationId, {
FailedToPreview: '' as IntlString, FailedToPreview: '' as IntlString,
ContentType: '' as IntlString, ContentType: '' as IntlString,
ContentTypeNotSupported: '' as IntlString, ContentTypeNotSupported: '' as IntlString,
StartDrawing: '' as IntlString StartDrawing: '' as IntlString,
DrawingHistory: '' as IntlString
}, },
extension: { extension: {
FilePreviewExtension: '' as ComponentExtensionId, FilePreviewExtension: '' as ComponentExtensionId,

View File

@ -133,6 +133,10 @@
box-shadow: none; box-shadow: none;
} }
&.auto {
max-width: unset;
}
&.full-width { &.full-width {
flex-grow: 1; flex-grow: 1;
background: none; background: none;

View File

@ -120,7 +120,7 @@
{#if showIcon} {#if showIcon}
<div class="btn-icon {iconModifier}" class:buttonIconNoLabel={!shouldShowLabel}> <div class="btn-icon {iconModifier}" class:buttonIconNoLabel={!shouldShowLabel}>
<Icon <Icon
icon={icon ?? (iconModifier === 'overdue' && !shouldIgnoreOverdue) ? DPCalendarOver : DPCalendar} icon={icon ?? (iconModifier === 'overdue' && !shouldIgnoreOverdue ? DPCalendarOver : DPCalendar)}
size={'full'} size={'full'}
/> />
</div> </div>

View File

@ -0,0 +1,10 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.5 8H12v5l4.28 2.54.72-1.21-3.5-2.08V8M13 3a9 9 0 00-9 9H1l3.96 4.03L9 12H6a7 7 0 017-7 7 7 0 017 7 7 7 0 01-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.896 8.896 0 0013 21a9 9 0 009-9 9 9 0 00-9-9"
/>
</svg>

View File

@ -0,0 +1,10 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.06 2.334c-1.077.463-2.426 1.515-3.113 2.89l-.894-.448c.813-1.625 2.364-2.823 3.612-3.36.316-.136.626-.236.912-.287.278-.05.571-.061.833.017a.923.923 0 01.645.619c.091.3.028.623-.09.92-.284.708-.897 1.514-1.538 2.302-.196.241-.396.482-.596.723-.478.574-.953 1.147-1.365 1.71-.593.811-.972 1.501-1.04 2.034-.032.247.007.434.103.588.1.16.293.338.668.498 1.104.474 2.543.426 3.98.028.05-.527.17-1.101.342-1.705.536-1.876 1.757-3.141 2.93-3.581.583-.219 1.223-.254 1.743.053.542.32.808.924.808 1.665 0 .48-.196.947-.483 1.367-.29.424-.692.834-1.164 1.213-.86.69-1.99 1.308-3.195 1.73.02.326.086.581.186.77.123.234.31.394.603.476.313.088.77.092 1.418-.062.643-.154 1.44-.456 2.411-.941l.448.894c-1.013.507-1.885.842-2.627 1.02-.738.175-1.382.202-1.92.052a1.917 1.917 0 01-1.218-.972 2.711 2.711 0 01-.279-.946c-1.483.37-3.064.421-4.377-.142-.5-.214-.885-.505-1.123-.888-.242-.389-.3-.819-.246-1.244.103-.81.63-1.683 1.225-2.497.43-.59.939-1.201 1.425-1.786.195-.235.386-.465.567-.688.656-.805 1.168-1.499 1.385-2.042a.832.832 0 00.06-.216 1.008 1.008 0 00-.343.015 3.27 3.27 0 00-.693.221zm3.172 7.88c.952-.375 1.825-.876 2.495-1.414.419-.336.745-.676.964-.996C11.91 7.48 12 7.209 12 7c0-.509-.171-.718-.317-.804-.168-.099-.465-.134-.882.022-.827.31-1.856 1.295-2.32 2.92a9.81 9.81 0 00-.249 1.077z"
/>
</svg>

View File

@ -240,6 +240,8 @@ export { default as IconFolderCollapsed } from './components/icons/FolderCollaps
export { default as IconFolderExpanded } from './components/icons/FolderExpanded.svelte' export { default as IconFolderExpanded } from './components/icons/FolderExpanded.svelte'
export { default as IconCheckmark } from './components/icons/Checkmark.svelte' export { default as IconCheckmark } from './components/icons/Checkmark.svelte'
export { default as IconToDetails } from './components/icons/ToDetails.svelte' export { default as IconToDetails } from './components/icons/ToDetails.svelte'
export { default as IconHistory } from './components/icons/History.svelte'
export { default as IconScribble } from './components/icons/Scribble.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte' export { default as PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.svelte' export { default as Panel } from './components/Panel.svelte'

View File

@ -0,0 +1,13 @@
<script lang="ts">
import { type Drawing } from '@hcengineering/attachment'
import core, { DateRangeMode } from '@hcengineering/core'
import { DatePresenter, IconScribble } from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
export let value: Drawing
</script>
<div class="flex-presenter flex-gap-3">
<DatePresenter value={value.createdOn} mode={DateRangeMode.DATETIME} kind="list" icon={IconScribble} />
<ObjectPresenter objectId={value.createdBy} _class={core.class.Account} shouldShowName={false} />
</div>

View File

@ -42,6 +42,7 @@ import FileDownload from './components/icons/FileDownload.svelte'
import IconUploadDuo from './components/icons/UploadDuo.svelte' import IconUploadDuo from './components/icons/UploadDuo.svelte'
import PreviewWidget from './components/PreviewWidget.svelte' import PreviewWidget from './components/PreviewWidget.svelte'
import PreviewPopupActions from './components/PreviewPopupActions.svelte' import PreviewPopupActions from './components/PreviewPopupActions.svelte'
import DrawingPresenter from './components/DrawingPresenter.svelte'
export * from './types' export * from './types'
@ -256,6 +257,7 @@ export default async (): Promise<Resources> => ({
AttachmentPresenter, AttachmentPresenter,
AttachmentGalleryPresenter, AttachmentGalleryPresenter,
Attachments, Attachments,
DrawingPresenter,
FileBrowser, FileBrowser,
Photos, Photos,
PDFViewer, PDFViewer,

View File

@ -16,6 +16,7 @@
import { type BlobMetadata, type Attachment, type Drawing } from '@hcengineering/attachment' import { type BlobMetadata, type Attachment, type Drawing } from '@hcengineering/attachment'
import core, { import core, {
SortingOrder,
type Blob, type Blob,
type Class, type Class,
type TxOperations as Client, type TxOperations as Client,
@ -151,28 +152,42 @@ export function showAttachmentPreviewPopup (value: WithLookup<Attachment>): Popu
if (value?.type?.startsWith('image/')) { if (value?.type?.startsWith('image/')) {
props.drawingAvailable = true props.drawingAvailable = true
props.loadDrawings = async (): Promise<DrawingData | undefined> => { props.loadDrawings = async (): Promise<Drawing[] | undefined> => {
const client = getClient() const client = getClient()
const drawing = await client.findOne(attachment.class.Drawing, { parent: value.file }) const drawings = await client.findAll(
if (drawing !== undefined) { attachment.class.Drawing,
return { {
id: drawing._id, parent: value.file,
content: drawing.content space: value.space
},
{
sort: {
createdOn: SortingOrder.Descending
},
limit: 1
}
)
const result = []
if (drawings !== undefined) {
for (const drawing of drawings) {
result.push(drawing)
} }
} }
return result
} }
props.saveDrawing = async (data: DrawingData): Promise<void> => { props.createDrawing = async (data: DrawingData): Promise<DrawingData> => {
const client = getClient() const client = getClient()
if (data.id === undefined) { const newId = await client.createDoc(attachment.class.Drawing, value.space, {
await client.createDoc(attachment.class.Drawing, value.space, {
parent: value.file, parent: value.file,
parentClass: core.class.Blob, parentClass: core.class.Blob,
content: data.content content: data.content
}) })
const newDrawing = await client.findOne(attachment.class.Drawing, { _id: newId })
if (newDrawing !== undefined) {
return newDrawing
} else { } else {
await client.updateDoc(attachment.class.Drawing, value.space, data.id as Ref<Drawing>, { console.error('Unable to find just created drawing')
content: data.content return data
})
} }
} }
} }

View File

@ -81,6 +81,7 @@ export default plugin(attachmentId, {
Attachments: '' as AnyComponent, Attachments: '' as AnyComponent,
Photos: '' as AnyComponent, Photos: '' as AnyComponent,
AttachmentsPresenter: '' as AnyComponent, AttachmentsPresenter: '' as AnyComponent,
DrawingPresenter: '' as AnyComponent,
FileBrowser: '' as AnyComponent, FileBrowser: '' as AnyComponent,
PDFViewer: '' as AnyComponent PDFViewer: '' as AnyComponent
}, },

View File

@ -24,8 +24,8 @@
export let drawingAvailable: boolean export let drawingAvailable: boolean
export let drawingEditable: boolean export let drawingEditable: boolean
export let drawingData: any export let drawings: any
export let saveDrawing: (data: any) => Promise<void> export let createDrawing: (data: any) => Promise<any>
$: originalWidth = metadata?.originalWidth $: originalWidth = metadata?.originalWidth
$: originalHeight = metadata?.originalHeight $: originalHeight = metadata?.originalHeight
@ -49,8 +49,8 @@
<DrawingBoard <DrawingBoard
{imageWidth} {imageWidth}
{imageHeight} {imageHeight}
{drawingData} {drawings}
{saveDrawing} {createDrawing}
active={drawingAvailable && !loading} active={drawingAvailable && !loading}
readonly={drawingAvailable && !drawingEditable} readonly={drawingAvailable && !drawingEditable}
class="object-contain mx-auto" class="object-contain mx-auto"

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import type { Attachment } from '@hcengineering/attachment' import attachment, { type Attachment } from '@hcengineering/attachment'
import type { Tx, TxRemoveDoc } from '@hcengineering/core' import type { Tx, TxRemoveDoc } from '@hcengineering/core'
import type { TriggerControl } from '@hcengineering/server-core' import type { TriggerControl } from '@hcengineering/server-core'
@ -23,8 +23,9 @@ import type { TriggerControl } from '@hcengineering/server-core'
*/ */
export async function OnAttachmentDelete ( export async function OnAttachmentDelete (
txes: Tx[], txes: Tx[],
{ removedMap, ctx, storageAdapter, workspace }: TriggerControl { removedMap, ctx, storageAdapter, workspace, findAll, txFactory }: TriggerControl
): Promise<Tx[]> { ): Promise<Tx[]> {
const result: Tx[] = []
const toDelete: string[] = [] const toDelete: string[] = []
for (const tx of txes) { for (const tx of txes) {
const rmTx = tx as TxRemoveDoc<Attachment> const rmTx = tx as TxRemoveDoc<Attachment>
@ -36,12 +37,18 @@ export async function OnAttachmentDelete (
continue continue
} }
toDelete.push(attach.file) toDelete.push(attach.file)
const drawings = await findAll(ctx, attachment.class.Drawing, { parent: attach.file })
for (const drawing of drawings) {
const removeTx = txFactory.createTxRemoveDoc(drawing._class, drawing.space, drawing._id)
result.push(removeTx)
}
} }
if (toDelete.length > 0) { if (toDelete.length > 0) {
await storageAdapter.remove(ctx, workspace, toDelete) await storageAdapter.remove(ctx, workspace, toDelete)
} }
return [] return result
} }
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type