Skip to content

Commit

Permalink
Add image advanced data object data type (#890)
Browse files Browse the repository at this point in the history
  • Loading branch information
markus-moser authored Jan 17, 2025
1 parent cae9a7a commit 9d8e830
Show file tree
Hide file tree
Showing 39 changed files with 1,540 additions and 1,158 deletions.
2 changes: 2 additions & 0 deletions assets/js/src/core/app/config/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import { DynamicTypeObjectDataTime } from '@Pimcore/modules/element/dynamic-type
import { DynamicTypeObjectDataExternalImage } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-external-image'
import { DynamicTypeObjectDataImage } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-image'
import { DynamicTypeObjectDataVideo } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-video'
import { DynamicTypeObjectDataHotspotImage } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-hotspotimage'
import { DynamicTypeObjectDataImageGallery } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-image-gallery'
import { DynamicTypeObjectDataGeoPoint } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-geopoint'
import { DynamicTypeObjectDataGeoBounds } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-geobounds'
Expand Down Expand Up @@ -262,6 +263,7 @@ container.bind(serviceIds['DynamicTypes/ObjectData/Time']).to(DynamicTypeObjectD
container.bind(serviceIds['DynamicTypes/ObjectData/ExternalImage']).to(DynamicTypeObjectDataExternalImage).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/Image']).to(DynamicTypeObjectDataImage).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/Video']).to(DynamicTypeObjectDataVideo).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/HotspotImage']).to(DynamicTypeObjectDataHotspotImage).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/ImageGallery']).to(DynamicTypeObjectDataImageGallery).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/GeoPoint']).to(DynamicTypeObjectDataGeoPoint).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/GeoBounds']).to(DynamicTypeObjectDataGeoBounds).inSingletonScope()
Expand Down
1 change: 1 addition & 0 deletions assets/js/src/core/app/config/services/service-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const serviceIds = {
'DynamicTypes/ObjectData/ExternalImage': 'DynamicTypes/ObjectData/ExternalImage',
'DynamicTypes/ObjectData/Image': 'DynamicTypes/ObjectData/Image',
'DynamicTypes/ObjectData/Video': 'DynamicTypes/ObjectData/Video',
'DynamicTypes/ObjectData/HotspotImage': 'DynamicTypes/ObjectData/HotspotImage',
'DynamicTypes/ObjectData/ImageGallery': 'DynamicTypes/ObjectData/ImageGallery',
'DynamicTypes/ObjectData/GeoPoint': 'DynamicTypes/ObjectData/GeoPoint',
'DynamicTypes/ObjectData/GeoBounds': 'DynamicTypes/ObjectData/GeoBounds',
Expand Down
1 change: 0 additions & 1 deletion assets/js/src/core/components/hotspot-image/utils/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export const dragItem = (
marginLeft: number,
marginTop: number
): IHotspot[] => {
console.log('margins', marginLeft, marginTop)
const hotspot = convertHotspotToPixel(hotspots[hotspotIndex], containerBounds)
const newX = Math.min(containerBounds.width - hotspot.width, Math.max(0, evt.clientX - containerBounds.left - dragStart.x)) - marginLeft
const newY = Math.min(containerBounds.height - hotspot.height, Math.max(0, evt.clientY - containerBounds.top - dragStart.y)) - marginTop
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - Pimcore Open Core License (POCL)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/

import React from 'react'
import { IconButton } from '@Pimcore/components/icon-button/icon-button'
import { type HotspotImageValue } from './hotspot-image'
import _ from 'lodash'
import { Tooltip } from 'antd'
import { useTranslation } from 'react-i18next'
import { ButtonGroup } from '@Pimcore/components/button-group/button-group'
import { useAssetHelper } from '@Pimcore/modules/asset/hooks/use-asset-helper'
import { Dropdown } from '@Pimcore/components/dropdown/dropdown'
import { Icon } from '@Pimcore/components/icon/icon'

interface HotspotImageFooterProps {
emptyValue?: () => void
disabled?: boolean
value?: HotspotImageValue | null
setValue: (value: HotspotImageValue | null) => void
setCropModalOpen: (open: boolean) => void
setMarkerModalOpen: (open: boolean) => void
}

export const HotspotImageFooter = (props: HotspotImageFooterProps): React.JSX.Element => {
const { t } = useTranslation()
const { openAsset } = useAssetHelper()

const clearValueData = (): void => {
props.setValue({
...props.value!,
hotspots: [],
marker: [],
crop: null
})
}
const hasValueData = (): boolean => {
return !_.isEmpty(props.value?.hotspots) || !_.isEmpty(props.value?.marker) || !_.isEmpty(props.value?.crop)
}

return (
<ButtonGroup
items={ [
<Tooltip
key="empty"
title={ t('empty') }
>
<IconButton
disabled={ _.isEmpty(props.value) || props.disabled }
icon={ { value: 'trash' } }
onClick={ props.emptyValue }
/>
</Tooltip>,
<Tooltip
key="open"
title={ t('open') }
>
<IconButton
disabled={ _.isEmpty(props.value) }
icon={ { value: 'open-folder' } }
onClick={ () => {
if (typeof props.value?.image?.id === 'number') {
openAsset({ config: { id: props.value.image.id } })
}
} }
/>
</Tooltip>,
<Dropdown
key="more"
menu={ {
items: [
{
label: t('crop'),
key: 'crop',
icon: <Icon value={ 'crop' } />,
onClick: async () => {
props.setCropModalOpen(true)
}
},
{
label: t('hotspots.edit'),
key: 'hotspots-edit',
icon: <Icon value={ 'new-marker' } />,
onClick: async () => {
props.setMarkerModalOpen(true)
}
},
{
disabled: !hasValueData(),
label: t('hotspots.clear-data'),
key: 'hotspots-edit',
icon: <Icon value={ 'remove-marker' } />,
onClick: clearValueData
}
]
} }
placement='topLeft'
trigger={ ['click'] }
>
<IconButton
icon={ { value: 'more' } }
onClick={ (e) => { e.stopPropagation() } }
size="small"
/>
</Dropdown>
] }
noSpacing
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - Pimcore Open Core License (POCL)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/

import React, { useEffect, useState } from 'react'
import { Card } from '@Pimcore/components/card/card'
import {
HotspotImageFooter
} from './footer'
import { AssetTarget } from '@Pimcore/components/asset-target/asset-target'
import { useTranslation } from 'react-i18next'
import { Droppable } from '@Pimcore/components/drag-and-drop/droppable'
import type { DragAndDropInfo } from '@Pimcore/components/drag-and-drop/context-provider'
import type {
ImageValue
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/image'
import type {
Hotspot,
Marker
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/types/hotspot-types'
import type {
CropSettings
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/types/crop-types'
import {
HotspotImagePreview
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/hotspot-image/image-preview'

export interface HotspotImageValue {
image: ImageValue | null
hotspots?: Hotspot[] | null
marker?: Marker[] | null
crop?: CropSettings | null
}

export interface HotspotImageProps {
width: string | number | null
height: string | number | null
disabled?: boolean
value?: HotspotImageValue | null
onChange?: (value: HotspotImageValue | null) => void
}

export const HotspotImage = (props: HotspotImageProps): React.JSX.Element => {
const [value, setValue] = React.useState<HotspotImageValue | null>(props.value ?? null)
const [markerModalOpen, setMarkerModalOpen] = useState(false)
const [cropModalOpen, setCropModalOpen] = useState(false)

const { t } = useTranslation()
const emptyValue = (): void => {
setValue(null)
}

useEffect(() => {
props.onChange?.(value)
}, [value])

const width = props.width === null || props.width === '' ? 300 : props.width
const height = props.height === null || props.width === '' ? 150 : props.height

return (
<Card
className="max-w-full"
fitContent
footer={ <HotspotImageFooter
disabled={ props.disabled }
emptyValue={ emptyValue }
key="image-footer"
setCropModalOpen={ setCropModalOpen }
setMarkerModalOpen={ setMarkerModalOpen }
setValue={ setValue }
value={ value }
/> }
>
<Droppable
isValidContext={ (info: DragAndDropInfo) => props.disabled !== true }
isValidData={ (info: DragAndDropInfo) => info.type === 'asset' && info.data.type === 'image' }
onDrop={ (info: DragAndDropInfo) => { setValue({ image: { type: 'asset', id: info.data.id as number } }) } }
variant="outline"
>
{ // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
value !== null && value?.image !== null
? (
<HotspotImagePreview
assetId={ value.image.id }
cropModalOpen={ cropModalOpen }
height={ height }
markerModalOpen={ markerModalOpen }
onChange={ props.onChange }
setCropModalOpen={ setCropModalOpen }
setMarkerModalOpen={ setMarkerModalOpen }
value={ value }
width={ width }
/>
)
: (
<AssetTarget
dndIcon={ props.disabled !== true }
height={ height }
title={ t(props.disabled !== true ? 'image.dnd-target' : 'empty') }
uploadIcon={ props.disabled !== true }
width={ width }
/>
) }
</Droppable>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - Pimcore Open Core License (POCL)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/

import React, { useState, forwardRef, type MutableRefObject, useEffect } from 'react'
import { ImagePreview } from '@Pimcore/components/image-preview/image-preview'
import { HotspotMarkersModal } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/hotspot-markers-modal'
import { fromIHotspots, toIHotspots } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/utils/hotspot-converter'
import type { IHotspot } from '@Pimcore/components/hotspot-image/hotspot-image'
import type { HotspotImageValue } from './hotspot-image'
import _ from 'lodash'
import {
CropModal
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/crop-modal'
import type {
CropSettings
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/types/crop-types'

interface HotspotImagePreviewProps {
assetId: number
height: number | string
width: number | string
value: HotspotImageValue
onChange?: (value: HotspotImageValue) => void
cropModalOpen: boolean
markerModalOpen: boolean
setCropModalOpen: (open: boolean) => void
setMarkerModalOpen: (open: boolean) => void
}

export const HotspotImagePreview = forwardRef(function HotspotImagePreview (
{ assetId, height, width, value: initialValue, onChange, cropModalOpen, setCropModalOpen, markerModalOpen, setMarkerModalOpen }: HotspotImagePreviewProps,
ref: MutableRefObject<HTMLDivElement>
): React.JSX.Element {
const [value, setValue] = useState<HotspotImageValue>(initialValue)

useEffect(() => {
setValue(initialValue)
}, [initialValue])

const handleHotspotsChange = (iHotspots: IHotspot[]): void => {
const { hotspots, marker } = fromIHotspots(iHotspots)
const newValue: HotspotImageValue = { ...value, hotspots, marker }
setValue(newValue)
onChange?.(newValue)
}

const hasHotspotData = (): boolean => {
return !_.isEmpty(value.hotspots) || !_.isEmpty(value.marker)
}

const hideMarkerModal = (): void => {
setMarkerModalOpen(false)
}

const hideCropModal = (): void => {
setCropModalOpen(false)
}

const onCropChange = (crop: CropSettings | null): void => {
const newValue = { ...value, crop }
setValue(newValue)
onChange?.(newValue)
}

return (
<div ref={ ref }>
<ImagePreview
assetId={ assetId }
height={ height }
onHotspotsDataButtonClick={ hasHotspotData() ? () => { setMarkerModalOpen(true) } : undefined }
width={ width }
/>

{ cropModalOpen && (
<CropModal
crop={ value.crop }
imageId={ value.image!.id }
onChange={ onCropChange }
onClose={ hideCropModal }
open={ cropModalOpen }
/>
) }

{ markerModalOpen && (
<HotspotMarkersModal
hotspots={ toIHotspots(value.hotspots ?? [], value.marker ?? []) }
imageId={ assetId }
onChange={ handleHotspotsChange }
onClose={ hideMarkerModal }
open={ markerModalOpen }
/>
) }
</div>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export const CropModal = (props: CropModalProps): React.JSX.Element => {
}

const onUpdate = (item: IHotspot): void => {
console.log('onupdate', item, hotspotToCrop(item))
setCrop(hotspotToCrop(item))
}

Expand All @@ -72,8 +71,8 @@ export const CropModal = (props: CropModalProps): React.JSX.Element => {
props.onClose?.()
}

const thumbnailSrc = `${getPrefix()}/assets/${props.imageId}/image/stream/custom?width=${width}&height=${height}&mimeType=JPEG&resizeMode=none&contain=true`
console.log('currentcrop', cropToHotspot(crop))
const thumbnailSrc = `${getPrefix()}/assets/${props.imageId}/image/stream/custom?width=${width}&height=${height}&mimeType=PNG&resizeMode=none&contain=true`

return (
<Modal
afterOpenChange={ afterOpenChange }
Expand Down
Loading

0 comments on commit 9d8e830

Please sign in to comment.