mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 19:58:09 +00:00
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
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:
parent
aea704fd0f
commit
27547e580c
@ -241,6 +241,10 @@ export function createModel (builder: Builder): void {
|
||||
{ _class: 1 }
|
||||
]
|
||||
})
|
||||
|
||||
builder.mixin(attachment.class.Drawing, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: attachment.component.DrawingPresenter
|
||||
})
|
||||
}
|
||||
|
||||
export default attachment
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "Náhled se nezdařil",
|
||||
"ContentType": "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": {
|
||||
"FileTooLarge": "Soubor je příliš velký"
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "Failed to preview",
|
||||
"ContentType": "Content type",
|
||||
"ContentTypeNotSupported": "Preview is not available for this content type",
|
||||
"StartDrawing": "Scribble over"
|
||||
"StartDrawing": "Scribble over",
|
||||
"DrawingHistory": "Scribble history"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "File too large"
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "Error al previsualizar",
|
||||
"ContentType": "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": {
|
||||
"FileTooLarge": "Archivo demasiado grande"
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "Échec de l'aperçu",
|
||||
"ContentType": "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": {
|
||||
"FileTooLarge": "Fichier trop volumineux"
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "Impossibile mostrare l'anteprima",
|
||||
"ContentType": "Tipo di contenuto",
|
||||
"ContentTypeNotSupported": "Anteprima non disponibile per questo tipo di contenuto",
|
||||
"StartDrawing": "Scarabocchiare sopra"
|
||||
"StartDrawing": "Scarabocchiare sopra",
|
||||
"DrawingHistory": "Scarabocchiare la storia"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "File troppo grande"
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "Falha ao pré-visualizar",
|
||||
"ContentType": "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": {
|
||||
"FileTooLarge": "Ficheiro demasiado grande"
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "Ошибка предпросмотра",
|
||||
"ContentType": "Тип контента",
|
||||
"ContentTypeNotSupported": "Предварительный просмотр недоступен для этого типа контента",
|
||||
"StartDrawing": "Сделать набросок"
|
||||
"StartDrawing": "Сделать набросок",
|
||||
"DrawingHistory": "История набросков"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Файл слишком большой"
|
||||
|
@ -34,7 +34,8 @@
|
||||
"FailedToPreview": "预览失败",
|
||||
"ContentType": "内容类型",
|
||||
"ContentTypeNotSupported": "此內容類型無法預覽",
|
||||
"StartDrawing": "随意涂鸦"
|
||||
"StartDrawing": "随意涂鸦",
|
||||
"DrawingHistory": "涂鸦的历史"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "文件太大"
|
||||
|
@ -14,8 +14,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
EditWithIcon,
|
||||
FocusHandler,
|
||||
@ -31,6 +32,7 @@
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation from '..'
|
||||
import { ObjectCreate } from '../types'
|
||||
@ -47,7 +49,7 @@
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let selectedObjects: Ref<Doc>[] = []
|
||||
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 noSearchField: boolean = false
|
||||
@ -59,7 +61,7 @@
|
||||
export let created: Doc[] = []
|
||||
export let embedded: boolean = false
|
||||
export let loading: boolean = false
|
||||
export let type: 'text' | 'object' = 'text'
|
||||
export let type: 'text' | 'object' | 'presenter' = 'text'
|
||||
|
||||
let search: string = ''
|
||||
|
||||
@ -71,6 +73,11 @@
|
||||
created.length > 0 ||
|
||||
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 => {
|
||||
if (item === undefined) {
|
||||
return
|
||||
@ -154,6 +161,19 @@
|
||||
}
|
||||
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>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -164,6 +184,7 @@
|
||||
class:full-width={width === 'full'}
|
||||
class:plainContainer={!shadows}
|
||||
class:width-40={width === 'large'}
|
||||
class:auto={width === 'auto'}
|
||||
class:embedded
|
||||
on:keydown={onKeydown}
|
||||
use:resizeObserver={() => {
|
||||
@ -229,6 +250,10 @@
|
||||
<span class="label" class:disabled={readonly || isDeselectDisabled || loading}>
|
||||
<slot name="item" item={obj} />
|
||||
</span>
|
||||
{:else if type === 'presenter'}
|
||||
{#if presenter !== undefined}
|
||||
<svelte:component this={presenter} value={obj} />
|
||||
{/if}
|
||||
{:else}
|
||||
<slot name="item" item={obj} />
|
||||
{/if}
|
||||
|
@ -14,25 +14,30 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button, IconDelete, IconEdit, resizeObserver } from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { drawing, type DrawingData, type DrawingTool } from '../drawing'
|
||||
import IconEraser from './icons/Eraser.svelte'
|
||||
|
||||
export let active = false
|
||||
export let readonly = false
|
||||
export let readonly = true
|
||||
export let imageWidth: number | undefined
|
||||
export let imageHeight: number | undefined
|
||||
export let drawingData: DrawingData
|
||||
export let saveDrawing: (data: any) => Promise<void>
|
||||
export let drawings: DrawingData[]
|
||||
export let createDrawing: (data: any) => Promise<void>
|
||||
|
||||
let drawingTool: DrawingTool = 'pen'
|
||||
let penColor = 'blue'
|
||||
const penColors = ['red', 'green', 'blue', 'white', 'black']
|
||||
|
||||
let drawingData: DrawingData | undefined
|
||||
let board: HTMLDivElement
|
||||
let toolbar: HTMLDivElement
|
||||
let toolbarInside = false
|
||||
let oldReadonly: boolean
|
||||
let oldDrawings: DrawingData[]
|
||||
let modified = false
|
||||
|
||||
$: updateToolbarPosition(readonly, board, toolbar)
|
||||
$: updateEditableState(drawings, readonly)
|
||||
|
||||
function updateToolbarPosition (readonly: boolean, board: HTMLDivElement, toolbar: HTMLDivElement): void {
|
||||
if (!readonly && board?.offsetTop !== undefined && toolbar?.clientHeight !== undefined) {
|
||||
@ -41,9 +46,47 @@
|
||||
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>
|
||||
|
||||
{#if active}
|
||||
{#if active && drawingData !== undefined}
|
||||
<div
|
||||
{...$$restProps}
|
||||
style:position="relative"
|
||||
@ -56,9 +99,14 @@
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
drawingData,
|
||||
saveDrawing,
|
||||
drawingTool,
|
||||
penColor
|
||||
penColor,
|
||||
changed: (content) => {
|
||||
modified = true
|
||||
if (drawingData !== undefined) {
|
||||
drawingData.content = content
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if !readonly}
|
||||
@ -68,6 +116,7 @@
|
||||
kind="icon"
|
||||
on:click={() => {
|
||||
drawingData = {}
|
||||
modified = true
|
||||
}}
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
|
@ -13,9 +13,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { Button, Dialog, IconEdit, tooltip } from '@hcengineering/ui'
|
||||
import { Button, Dialog, IconHistory, IconScribble, showPopup, tooltip } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
import { BlobMetadata } from '../types'
|
||||
@ -23,6 +23,7 @@
|
||||
import ActionContext from './ActionContext.svelte'
|
||||
import FilePreview from './FilePreview.svelte'
|
||||
import DownloadFileButton from './DownloadFileButton.svelte'
|
||||
import ObjectPopup from './ObjectPopup.svelte'
|
||||
import { ComponentExtensions } from '../index'
|
||||
import presentation from '../plugin'
|
||||
import FileTypeIcon from './FileTypeIcon.svelte'
|
||||
@ -31,12 +32,19 @@
|
||||
export let name: string
|
||||
export let contentType: string
|
||||
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 showIcon = true
|
||||
|
||||
let drawingLoading = false
|
||||
let createDrawing: (data: any) => Promise<any>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -45,28 +53,67 @@
|
||||
dispatch('fullsize')
|
||||
}
|
||||
if (props.drawingAvailable === true) {
|
||||
loadDrawings(props.loadDrawings)
|
||||
if (props.loadDrawings !== undefined) {
|
||||
drawingLoading = true
|
||||
props
|
||||
.loadDrawings()
|
||||
.then((result) => {
|
||||
drawingLoading = false
|
||||
props.drawings = result
|
||||
})
|
||||
.catch((error) => {
|
||||
drawingLoading = false
|
||||
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 {
|
||||
const editable = props.drawingEditable === true
|
||||
props = { ...props, drawingEditable: !editable }
|
||||
props.drawingEditable = !(props.drawingEditable === true)
|
||||
}
|
||||
|
||||
function loadDrawings (load: () => Promise<any>): void {
|
||||
if (load !== undefined) {
|
||||
drawingLoading = true
|
||||
load()
|
||||
.then((result) => {
|
||||
drawingLoading = false
|
||||
props.drawingData = result
|
||||
})
|
||||
.catch((error) => {
|
||||
drawingLoading = false
|
||||
console.error('Failed to load drawings for file', file, error)
|
||||
})
|
||||
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>
|
||||
|
||||
@ -91,13 +138,24 @@
|
||||
|
||||
<svelte:fragment slot="utils">
|
||||
{#if props.drawingAvailable === true}
|
||||
{#if props.drawings !== undefined && props.drawings.length > 0}
|
||||
<Button
|
||||
icon={IconHistory}
|
||||
kind="icon"
|
||||
disabled={drawingLoading || props.drawingEditable === true}
|
||||
showTooltip={{ label: presentation.string.DrawingHistory }}
|
||||
on:click={selectCurrentDrawing}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
icon={IconEdit}
|
||||
icon={IconScribble}
|
||||
kind="icon"
|
||||
disabled={drawingLoading}
|
||||
selected={props.drawingEditable === true}
|
||||
showTooltip={{ label: presentation.string.StartDrawing }}
|
||||
on:click={toggleDrawingEdit}
|
||||
/>
|
||||
<div class="buttons-divider" />
|
||||
{/if}
|
||||
<DownloadFileButton {name} {file} />
|
||||
<ComponentExtensions
|
||||
|
@ -43,7 +43,7 @@
|
||||
export let selectedObjects: Ref<Doc>[] = []
|
||||
export let ignoreObjects: Ref<Doc>[] = []
|
||||
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 searchMode: 'field' | 'fulltext' | 'disabled' = 'field'
|
||||
@ -55,7 +55,7 @@
|
||||
export let disallowDeselect: Ref<Doc>[] | undefined = undefined
|
||||
export let embedded: 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 = () => {
|
||||
return true
|
||||
|
@ -14,19 +14,17 @@
|
||||
//
|
||||
|
||||
export interface DrawingData {
|
||||
id?: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
export interface DrawingProps {
|
||||
readonly?: boolean
|
||||
readonly: boolean
|
||||
imageWidth?: number
|
||||
imageHeight?: number
|
||||
drawingData?: DrawingData
|
||||
saveDrawing?: (data: any) => Promise<void>
|
||||
|
||||
drawingData: DrawingData
|
||||
drawingTool?: DrawingTool
|
||||
penColor?: string
|
||||
changed?: (content: string) => void
|
||||
}
|
||||
|
||||
interface DrawCmd {
|
||||
@ -198,7 +196,6 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
draw.penColor = props.penColor ?? 'blue'
|
||||
updateCanvasCursor()
|
||||
|
||||
let modified = false
|
||||
let commands: DrawCmd[] = []
|
||||
let drawingData = props.drawingData
|
||||
parseData()
|
||||
@ -300,7 +297,7 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
points: draw.points
|
||||
}
|
||||
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 {
|
||||
clearCanvas()
|
||||
if (drawingData?.content !== undefined) {
|
||||
if (drawingData.content !== undefined && drawingData.content !== null) {
|
||||
try {
|
||||
commands = JSON.parse(drawingData.content)
|
||||
replayCommands()
|
||||
@ -356,42 +353,24 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
return {
|
||||
update (props: DrawingProps) {
|
||||
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
|
||||
if (drawingData !== undefined) {
|
||||
drawingData.id = oldId
|
||||
}
|
||||
modified = true
|
||||
parseData()
|
||||
}
|
||||
let updateCursor = false
|
||||
if (draw.tool !== props.drawingTool) {
|
||||
draw.tool = props.drawingTool ?? 'pen'
|
||||
updateCanvasCursor()
|
||||
updateCursor = true
|
||||
}
|
||||
if (draw.penColor !== props.penColor) {
|
||||
draw.penColor = props.penColor ?? 'blue'
|
||||
updateCanvasCursor()
|
||||
updateCursor = true
|
||||
}
|
||||
if (props.readonly !== readonly) {
|
||||
readonly = props.readonly ?? false
|
||||
updateCanvasCursor()
|
||||
updateCursor = true
|
||||
}
|
||||
},
|
||||
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)
|
||||
})
|
||||
}
|
||||
if (updateCursor) {
|
||||
updateCanvasCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +122,8 @@ export default plugin(presentationId, {
|
||||
FailedToPreview: '' as IntlString,
|
||||
ContentType: '' as IntlString,
|
||||
ContentTypeNotSupported: '' as IntlString,
|
||||
StartDrawing: '' as IntlString
|
||||
StartDrawing: '' as IntlString,
|
||||
DrawingHistory: '' as IntlString
|
||||
},
|
||||
extension: {
|
||||
FilePreviewExtension: '' as ComponentExtensionId,
|
||||
|
@ -133,6 +133,10 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.auto {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
&.full-width {
|
||||
flex-grow: 1;
|
||||
background: none;
|
||||
|
@ -120,7 +120,7 @@
|
||||
{#if showIcon}
|
||||
<div class="btn-icon {iconModifier}" class:buttonIconNoLabel={!shouldShowLabel}>
|
||||
<Icon
|
||||
icon={icon ?? (iconModifier === 'overdue' && !shouldIgnoreOverdue) ? DPCalendarOver : DPCalendar}
|
||||
icon={icon ?? (iconModifier === 'overdue' && !shouldIgnoreOverdue ? DPCalendarOver : DPCalendar)}
|
||||
size={'full'}
|
||||
/>
|
||||
</div>
|
||||
|
10
packages/ui/src/components/icons/History.svelte
Normal file
10
packages/ui/src/components/icons/History.svelte
Normal 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>
|
10
packages/ui/src/components/icons/Scribble.svelte
Normal file
10
packages/ui/src/components/icons/Scribble.svelte
Normal 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>
|
@ -240,6 +240,8 @@ export { default as IconFolderCollapsed } from './components/icons/FolderCollaps
|
||||
export { default as IconFolderExpanded } from './components/icons/FolderExpanded.svelte'
|
||||
export { default as IconCheckmark } from './components/icons/Checkmark.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 Panel } from './components/Panel.svelte'
|
||||
|
@ -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>
|
@ -42,6 +42,7 @@ import FileDownload from './components/icons/FileDownload.svelte'
|
||||
import IconUploadDuo from './components/icons/UploadDuo.svelte'
|
||||
import PreviewWidget from './components/PreviewWidget.svelte'
|
||||
import PreviewPopupActions from './components/PreviewPopupActions.svelte'
|
||||
import DrawingPresenter from './components/DrawingPresenter.svelte'
|
||||
|
||||
export * from './types'
|
||||
|
||||
@ -256,6 +257,7 @@ export default async (): Promise<Resources> => ({
|
||||
AttachmentPresenter,
|
||||
AttachmentGalleryPresenter,
|
||||
Attachments,
|
||||
DrawingPresenter,
|
||||
FileBrowser,
|
||||
Photos,
|
||||
PDFViewer,
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import { type BlobMetadata, type Attachment, type Drawing } from '@hcengineering/attachment'
|
||||
import core, {
|
||||
SortingOrder,
|
||||
type Blob,
|
||||
type Class,
|
||||
type TxOperations as Client,
|
||||
@ -151,28 +152,42 @@ export function showAttachmentPreviewPopup (value: WithLookup<Attachment>): Popu
|
||||
|
||||
if (value?.type?.startsWith('image/')) {
|
||||
props.drawingAvailable = true
|
||||
props.loadDrawings = async (): Promise<DrawingData | undefined> => {
|
||||
props.loadDrawings = async (): Promise<Drawing[] | undefined> => {
|
||||
const client = getClient()
|
||||
const drawing = await client.findOne(attachment.class.Drawing, { parent: value.file })
|
||||
if (drawing !== undefined) {
|
||||
return {
|
||||
id: drawing._id,
|
||||
content: drawing.content
|
||||
const drawings = await client.findAll(
|
||||
attachment.class.Drawing,
|
||||
{
|
||||
parent: value.file,
|
||||
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()
|
||||
if (data.id === undefined) {
|
||||
await client.createDoc(attachment.class.Drawing, value.space, {
|
||||
parent: value.file,
|
||||
parentClass: core.class.Blob,
|
||||
content: data.content
|
||||
})
|
||||
const newId = await client.createDoc(attachment.class.Drawing, value.space, {
|
||||
parent: value.file,
|
||||
parentClass: core.class.Blob,
|
||||
content: data.content
|
||||
})
|
||||
const newDrawing = await client.findOne(attachment.class.Drawing, { _id: newId })
|
||||
if (newDrawing !== undefined) {
|
||||
return newDrawing
|
||||
} else {
|
||||
await client.updateDoc(attachment.class.Drawing, value.space, data.id as Ref<Drawing>, {
|
||||
content: data.content
|
||||
})
|
||||
console.error('Unable to find just created drawing')
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ export default plugin(attachmentId, {
|
||||
Attachments: '' as AnyComponent,
|
||||
Photos: '' as AnyComponent,
|
||||
AttachmentsPresenter: '' as AnyComponent,
|
||||
DrawingPresenter: '' as AnyComponent,
|
||||
FileBrowser: '' as AnyComponent,
|
||||
PDFViewer: '' as AnyComponent
|
||||
},
|
||||
|
@ -24,8 +24,8 @@
|
||||
|
||||
export let drawingAvailable: boolean
|
||||
export let drawingEditable: boolean
|
||||
export let drawingData: any
|
||||
export let saveDrawing: (data: any) => Promise<void>
|
||||
export let drawings: any
|
||||
export let createDrawing: (data: any) => Promise<any>
|
||||
|
||||
$: originalWidth = metadata?.originalWidth
|
||||
$: originalHeight = metadata?.originalHeight
|
||||
@ -49,8 +49,8 @@
|
||||
<DrawingBoard
|
||||
{imageWidth}
|
||||
{imageHeight}
|
||||
{drawingData}
|
||||
{saveDrawing}
|
||||
{drawings}
|
||||
{createDrawing}
|
||||
active={drawingAvailable && !loading}
|
||||
readonly={drawingAvailable && !drawingEditable}
|
||||
class="object-contain mx-auto"
|
||||
|
@ -14,7 +14,7 @@
|
||||
// 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 { TriggerControl } from '@hcengineering/server-core'
|
||||
|
||||
@ -23,8 +23,9 @@ import type { TriggerControl } from '@hcengineering/server-core'
|
||||
*/
|
||||
export async function OnAttachmentDelete (
|
||||
txes: Tx[],
|
||||
{ removedMap, ctx, storageAdapter, workspace }: TriggerControl
|
||||
{ removedMap, ctx, storageAdapter, workspace, findAll, txFactory }: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
const toDelete: string[] = []
|
||||
for (const tx of txes) {
|
||||
const rmTx = tx as TxRemoveDoc<Attachment>
|
||||
@ -36,12 +37,18 @@ export async function OnAttachmentDelete (
|
||||
continue
|
||||
}
|
||||
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) {
|
||||
await storageAdapter.remove(ctx, workspace, toDelete)
|
||||
}
|
||||
|
||||
return []
|
||||
return result
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
|
Loading…
Reference in New Issue
Block a user