diff --git a/assets/js/src/core/app/config/services/index.ts b/assets/js/src/core/app/config/services/index.ts index cf0603fc8..381340691 100644 --- a/assets/js/src/core/app/config/services/index.ts +++ b/assets/js/src/core/app/config/services/index.ts @@ -103,6 +103,7 @@ import { DynamicTypeObjectDataDatetime } from '@Pimcore/modules/element/dynamic- import { DynamicTypeObjectDataDateRange } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-date-range' import { DynamicTypeObjectDataTime } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-time' 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 { 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' import { DynamicTypeObjectDataGeoPolygon } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-geopolygon' @@ -246,6 +247,7 @@ container.bind(serviceIds['DynamicTypes/ObjectData/Datetime']).to(DynamicTypeObj container.bind(serviceIds['DynamicTypes/ObjectData/DateRange']).to(DynamicTypeObjectDataDateRange).inSingletonScope() container.bind(serviceIds['DynamicTypes/ObjectData/Time']).to(DynamicTypeObjectDataTime).inSingletonScope() container.bind(serviceIds['DynamicTypes/ObjectData/ExternalImage']).to(DynamicTypeObjectDataExternalImage).inSingletonScope() +container.bind(serviceIds['DynamicTypes/ObjectData/Image']).to(DynamicTypeObjectDataImage).inSingletonScope() container.bind(serviceIds['DynamicTypes/ObjectData/GeoPoint']).to(DynamicTypeObjectDataGeoPoint).inSingletonScope() container.bind(serviceIds['DynamicTypes/ObjectData/GeoBounds']).to(DynamicTypeObjectDataGeoBounds).inSingletonScope() container.bind(serviceIds['DynamicTypes/ObjectData/GeoPolygon']).to(DynamicTypeObjectDataGeoPolygon).inSingletonScope() diff --git a/assets/js/src/core/app/config/services/service-ids.ts b/assets/js/src/core/app/config/services/service-ids.ts index 412266c0d..de6e46135 100644 --- a/assets/js/src/core/app/config/services/service-ids.ts +++ b/assets/js/src/core/app/config/services/service-ids.ts @@ -138,6 +138,7 @@ export const serviceIds = { 'DynamicTypes/ObjectData/DateRange': 'DynamicTypes/ObjectData/DateRange', 'DynamicTypes/ObjectData/Time': 'DynamicTypes/ObjectData/Time', 'DynamicTypes/ObjectData/ExternalImage': 'DynamicTypes/ObjectData/ExternalImage', + 'DynamicTypes/ObjectData/Image': 'DynamicTypes/ObjectData/Image', 'DynamicTypes/ObjectData/GeoPoint': 'DynamicTypes/ObjectData/GeoPoint', 'DynamicTypes/ObjectData/GeoBounds': 'DynamicTypes/ObjectData/GeoBounds', 'DynamicTypes/ObjectData/GeoPolygon': 'DynamicTypes/ObjectData/GeoPolygon', diff --git a/assets/js/src/core/components/drag-and-drop/droppable.styles.ts b/assets/js/src/core/components/drag-and-drop/droppable.styles.ts index 763cc5a5f..32ec495cc 100644 --- a/assets/js/src/core/components/drag-and-drop/droppable.styles.ts +++ b/assets/js/src/core/components/drag-and-drop/droppable.styles.ts @@ -23,7 +23,7 @@ export const useStyle = createStyles(({ token, css }) => { & .dnd--drag-valid { background: ${token.colorBgTextActive}; - border: 1px dashed ${token.colorInfoBorder}; + border: 1px dashed ${token.colorInfoBorderHover}; } & .dnd--drag-error { @@ -32,9 +32,9 @@ export const useStyle = createStyles(({ token, css }) => { } `, outline: css` - + & .dnd--drag-valid { - outline: 1px dashed ${token.colorInfoBorder}; + outline: 1px dashed ${token.colorInfoBorderHover}; } & .dnd--drag-error { diff --git a/assets/js/src/core/components/flex/flex.tsx b/assets/js/src/core/components/flex/flex.tsx index 7b957d9d1..4216f6726 100644 --- a/assets/js/src/core/components/flex/flex.tsx +++ b/assets/js/src/core/components/flex/flex.tsx @@ -11,7 +11,7 @@ * @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL */ -import React from 'react' +import React, { type LegacyRef } from 'react' import { Flex as AntFlex, type FlexProps as AntFlexProps, theme } from 'antd' import cn from 'classnames' import { isString, isNumber, isObject } from 'lodash' @@ -21,6 +21,7 @@ import { type GapType } from '@Pimcore/types/components/types' export interface FlexProps extends Omit { gap?: GapType + ref?: LegacyRef } const { useToken } = theme diff --git a/assets/js/src/core/components/image-preview/image-preview.stories.tsx b/assets/js/src/core/components/image-preview/image-preview.stories.tsx new file mode 100644 index 000000000..c30230159 --- /dev/null +++ b/assets/js/src/core/components/image-preview/image-preview.stories.tsx @@ -0,0 +1,40 @@ +/** +* 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 { type Meta } from '@storybook/react' +import { ImagePreview } from './image-preview' + +const config: Meta = { + title: 'Components/Data Display/ImagePreview', + component: ImagePreview, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: 'This component is used to display an image preview with specified width and height. The component has a max-width of 100% and the image is displayed centered within the given box. This component can also be used within a droppable component to allow drag and drop of images.' + } + } + } +} + +export default config + +export const _default = { + args: { + width: 300, + height: 300, + style: { + border: '1px dashed #d9d9d9', + } + } +} diff --git a/assets/js/src/core/components/image-preview/image-preview.styles.tsx b/assets/js/src/core/components/image-preview/image-preview.styles.tsx new file mode 100644 index 000000000..ed5080bae --- /dev/null +++ b/assets/js/src/core/components/image-preview/image-preview.styles.tsx @@ -0,0 +1,33 @@ +/** +* 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 { createStyles } from 'antd-style' + +export const useStyle = createStyles(({ token, css }) => { + return { + imagePreviewContainer: css` + max-width: 100%; + + .ant-image { + height: 100%; + width: 100%; + } + + .ant-image-img { + width: 100%; + height: 100%; + object-fit: contain; + } + ` + } +}) diff --git a/assets/js/src/core/components/image-preview/image-preview.tsx b/assets/js/src/core/components/image-preview/image-preview.tsx new file mode 100644 index 000000000..f0aad54e4 --- /dev/null +++ b/assets/js/src/core/components/image-preview/image-preview.tsx @@ -0,0 +1,55 @@ +/** +* 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, { type CSSProperties } from 'react' +import { Flex } from '@Pimcore/components/flex/flex' +import { useStyle } from './image-preview.styles' +import cn from 'classnames' +import { toCssDimension } from '@Pimcore/utils/css' +import { Image } from 'antd' +import { getPrefix } from '@Pimcore/app/api/pimcore/route' + +interface ImagePreviewProps { + src?: string + assetId?: number + className?: string + width: number | string + height: number | string + style?: CSSProperties +} + +export const ImagePreview = ({ src, assetId, width, height, className, style }: ImagePreviewProps): React.JSX.Element => { + const { styles } = useStyle() + + const imageSrc = assetId !== undefined ? `${getPrefix()}/assets/${assetId}/image/stream/preview` : src + + return ( + + + + ) +} diff --git a/assets/js/src/core/components/image-target/image-target.styles.tsx b/assets/js/src/core/components/image-target/image-target.styles.tsx index 32bbdf6b1..f0ba9d2ca 100644 --- a/assets/js/src/core/components/image-target/image-target.styles.tsx +++ b/assets/js/src/core/components/image-target/image-target.styles.tsx @@ -17,9 +17,11 @@ export const useStyle = createStyles(({ token, css }) => { return { imageTargetContainer: css` border-radius: ${token.borderRadiusLG}px; - border: 1px dashed ${token.colorBorder}; + outline: 1px dashed ${token.colorBorder}; background: ${token.controlItemBgHover}; padding: ${token.paddingSM}px; + max-width: 100%; + .image-target-title { text-align: center; diff --git a/assets/js/src/core/components/image-target/image-target.tsx b/assets/js/src/core/components/image-target/image-target.tsx index f8b69e81c..ffaab2bcf 100644 --- a/assets/js/src/core/components/image-target/image-target.tsx +++ b/assets/js/src/core/components/image-target/image-target.tsx @@ -11,30 +11,58 @@ * @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL */ -import React from 'react' +import React, { forwardRef, type MutableRefObject } from 'react' import { Flex } from '@Pimcore/components/flex/flex' import { useStyle } from './image-target.styles' import cn from 'classnames' import { toCssDimension } from '@Pimcore/utils/css' +import { Icon } from '@Pimcore/components/icon/icon' +import { useDroppable } from '@Pimcore/components/drag-and-drop/hooks/use-droppable' interface ImageTargetProps { title: string className?: string - width?: number - height?: number + width?: number | string + height?: number | string + dndIcon?: boolean + uploadIcon?: boolean } -export const ImageTarget = ({ title, width, height, className }: ImageTargetProps): React.JSX.Element => { +export const ImageTarget = forwardRef(function ImageTarget (props: ImageTargetProps, ref: MutableRefObject): React.JSX.Element { + const { getStateClasses } = useDroppable() + const { title, className, width = 200, height = 200, dndIcon, uploadIcon } = props const { styles } = useStyle() return ( - -
{ title }
-
+ + { (dndIcon === true || uploadIcon === true) && ( +
+ + +
+ )} +
{ title }
+
+ ) -} +}) diff --git a/assets/js/src/core/modules/app/theme/utils/themes/default-theme.ts b/assets/js/src/core/modules/app/theme/utils/themes/default-theme.ts index a4bc26db1..512385b2c 100644 --- a/assets/js/src/core/modules/app/theme/utils/themes/default-theme.ts +++ b/assets/js/src/core/modules/app/theme/utils/themes/default-theme.ts @@ -55,6 +55,7 @@ const defaultTheme = { colorTextTreeElement: '#404655', colorIconTree: '#404655', colorIconTreeUnpublished: 'rgba(64, 70, 85, 0.4)', + colorInfoBorderHover: '#b37feb', paddingTabs: 8, colorTextSidebarTitle: '#531dab', colorBgToolbar: '#f5f3fa', diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/external-image/external-image.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/external-image/external-image.tsx index 8ceb731b3..728e39b85 100644 --- a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/external-image/external-image.tsx +++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/external-image/external-image.tsx @@ -16,10 +16,9 @@ import { Card } from '@Pimcore/components/card/card' import { ExternalImageFooter } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/external-image/footer' -import { Image } from 'antd' import { ImageTarget } from '@Pimcore/components/image-target/image-target' -import { toCssDimension } from '@Pimcore/utils/css' import { useTranslation } from 'react-i18next' +import { ImagePreview } from '@Pimcore/components/image-preview/image-preview' export interface ExternalImageValue { url: string @@ -53,6 +52,7 @@ export const ExternalImage = (props: ExternalImageProps): React.JSX.Element => { return ( <> { > { value !== null && value.url !== '' ? ( - ) : ( )} diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/footer.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/footer.tsx new file mode 100644 index 000000000..5b7364594 --- /dev/null +++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/footer.tsx @@ -0,0 +1,41 @@ +/** +* 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 } from 'react' +import { IconButton } from '@Pimcore/components/icon-button/icon-button' + +interface ImageFooterProps { + value?: string + onChange?: (value?: string) => void + disabled?: boolean +} + +export const ImageFooter = (props: ImageFooterProps): React.JSX.Element => { + const [value, setValue] = React.useState(props.value) + + const emptyValue = (): void => { + setValue(undefined) + } + + useEffect(() => { + props.onChange?.(value) + }, [value]) + + return ( + + ) +} diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/image.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/image.tsx new file mode 100644 index 000000000..7e4b264af --- /dev/null +++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/image.tsx @@ -0,0 +1,88 @@ +/** +* 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 } from 'react' +import { Card } from '@Pimcore/components/card/card' +import { + ImageFooter +} from './footer' +import { ImageTarget } from '@Pimcore/components/image-target/image-target' +import { useTranslation } from 'react-i18next' +import { ImagePreview } from '@Pimcore/components/image-preview/image-preview' +import { Droppable } from '@Pimcore/components/drag-and-drop/droppable' +import type { DragAndDropInfo } from '@Pimcore/components/drag-and-drop/context-provider' + +export interface ImageValue { + type: 'asset' + id: number +} + +export interface ImageProps { + width: string | null + height: string | null + disabled?: boolean + value?: ImageValue | null + onChange?: (value: ImageValue | null) => void +} + +export const Image = (props: ImageProps): React.JSX.Element => { + const [value, setValue] = React.useState(props.value ?? null) + const { t } = useTranslation() + console.log(setValue) + const onChange = (value?: string): void => { + // const newUrl = value !== '' && value !== undefined ? value : null + // setValue(newUrl === null ? null : { url: newUrl }) + } + + useEffect(() => { + props.onChange?.(value) + }, [value]) + + return ( + <> + } + > + { value !== null + ? ( + + ) + : ( + true } + isValidData={ (info: DragAndDropInfo) => info.type === 'asset' && info.data.type === 'image' } + onDrop={ (info: DragAndDropInfo) => { setValue({ type: 'asset', id: info.data.id as number }) } } + variant="outline" + > + + + ) } + + + ) +} diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-image.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-image.tsx new file mode 100644 index 000000000..cde75e856 --- /dev/null +++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-image.tsx @@ -0,0 +1,32 @@ +/** +* 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 { + type AbstractObjectDataDefinition, DynamicTypeObjectDataAbstract +} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/dynamic-type-object-data-abstract' +import { + Image, type ImageProps +} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/image' + +export type ImageObjectDataDefinition = AbstractObjectDataDefinition & ImageProps + +export class DynamicTypeObjectDataImage extends DynamicTypeObjectDataAbstract { + id: string = 'image' + + getObjectDataComponent (props: ImageObjectDataDefinition): React.ReactElement { + return ( + + ) + } +} diff --git a/assets/js/src/core/modules/element/dynamic-types/index.ts b/assets/js/src/core/modules/element/dynamic-types/index.ts index 6468b93ab..616d84b4c 100644 --- a/assets/js/src/core/modules/element/dynamic-types/index.ts +++ b/assets/js/src/core/modules/element/dynamic-types/index.ts @@ -91,6 +91,7 @@ import { type DynamicTypeObjectDataDatetime } from '@Pimcore/modules/element/dyn import { type DynamicTypeObjectDataDateRange } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-date-range' import { type DynamicTypeObjectDataTime } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-time' import { type DynamicTypeObjectDataExternalImage } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-external-image' +import { type DynamicTypeObjectDataImage } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-image' import { type DynamicTypeObjectDataGeoPoint } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-geopoint' import { type DynamicTypeObjectDataGeoBounds } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-geobounds' import { type DynamicTypeObjectDataGeoPolygon } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-geopolygon' @@ -214,6 +215,7 @@ moduleSystem.registerModule({ objectDataRegistry.registerDynamicType(container.get(serviceIds['DynamicTypes/ObjectData/DateRange'])) objectDataRegistry.registerDynamicType(container.get(serviceIds['DynamicTypes/ObjectData/Time'])) objectDataRegistry.registerDynamicType(container.get(serviceIds['DynamicTypes/ObjectData/ExternalImage'])) + objectDataRegistry.registerDynamicType(container.get(serviceIds['DynamicTypes/ObjectData/Image'])) objectDataRegistry.registerDynamicType(container.get(serviceIds['DynamicTypes/ObjectData/GeoPoint'])) objectDataRegistry.registerDynamicType(container.get(serviceIds['DynamicTypes/ObjectData/GeoBounds'])) objectDataRegistry.registerDynamicType(container.get(serviceIds['DynamicTypes/ObjectData/GeoPolygon'])) diff --git a/assets/js/src/core/styles/global.styles.ts b/assets/js/src/core/styles/global.styles.ts index e94a41d6b..4561cdea2 100644 --- a/assets/js/src/core/styles/global.styles.ts +++ b/assets/js/src/core/styles/global.styles.ts @@ -631,6 +631,10 @@ export const GlobalStyles = createGlobalStyle` width: 100%; } + .max-w-full { + max-width: 100%; + } + .min-w-100 { min-width: 100px; } diff --git a/translations/studio.en.yaml b/translations/studio.en.yaml index 5f46e70da..eb3e2af06 100644 --- a/translations/studio.en.yaml +++ b/translations/studio.en.yaml @@ -333,7 +333,6 @@ info: Info no-value-set: No value set set-to-null: Empty (set to null) remove: Remove -data-object.editor.external-image.preview-placeholder: Add an external image latitude: Latitude longitude: Longitude search-address: Search address @@ -363,4 +362,6 @@ processing: Processing upload: Upload upload.assets-items-failed-message: Some items could not be uploaded. quantity-value.converted-units: Converted Units -structured-table.empty.confirm: Are you sure you want to clear all data? \ No newline at end of file +structured-table.empty.confirm: Are you sure you want to clear all data? +external-image.preview-placeholder: Add an external image +image.dnd-target: Add or drop an image directly here \ No newline at end of file