diff --git a/assets/js/src/core/app/config/services/index.ts b/assets/js/src/core/app/config/services/index.ts index 39f11273c..7bbda47b8 100644 --- a/assets/js/src/core/app/config/services/index.ts +++ b/assets/js/src/core/app/config/services/index.ts @@ -107,6 +107,7 @@ import { DynamicTypeObjectDataDateRange } from '@Pimcore/modules/element/dynamic 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 { DynamicTypeObjectDataVideo } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-video' 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' @@ -255,6 +256,7 @@ container.bind(serviceIds['DynamicTypes/ObjectData/DateRange']).to(DynamicTypeOb 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/Video']).to(DynamicTypeObjectDataVideo).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() 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 070016c23..8d020aafe 100644 --- a/assets/js/src/core/app/config/services/service-ids.ts +++ b/assets/js/src/core/app/config/services/service-ids.ts @@ -142,6 +142,7 @@ export const serviceIds = { 'DynamicTypes/ObjectData/Time': 'DynamicTypes/ObjectData/Time', 'DynamicTypes/ObjectData/ExternalImage': 'DynamicTypes/ObjectData/ExternalImage', 'DynamicTypes/ObjectData/Image': 'DynamicTypes/ObjectData/Image', + 'DynamicTypes/ObjectData/Video': 'DynamicTypes/ObjectData/Video', 'DynamicTypes/ObjectData/ImageGallery': 'DynamicTypes/ObjectData/ImageGallery', 'DynamicTypes/ObjectData/GeoPoint': 'DynamicTypes/ObjectData/GeoPoint', 'DynamicTypes/ObjectData/GeoBounds': 'DynamicTypes/ObjectData/GeoBounds', diff --git a/assets/js/src/core/components/image-target/image-target.stories.tsx b/assets/js/src/core/components/asset-target/asset-target.stories.tsx similarity index 85% rename from assets/js/src/core/components/image-target/image-target.stories.tsx rename to assets/js/src/core/components/asset-target/asset-target.stories.tsx index d551a377f..374c9ef72 100644 --- a/assets/js/src/core/components/image-target/image-target.stories.tsx +++ b/assets/js/src/core/components/asset-target/asset-target.stories.tsx @@ -12,11 +12,11 @@ */ import { type Meta } from '@storybook/react' -import { ImageTarget } from './image-target' +import { AssetTarget } from './asset-target' const config: Meta = { - title: 'Components/Data Display/ImageTarget', - component: ImageTarget, + title: 'Components/Data Display/AssetTarget', + component: AssetTarget, tags: ['autodocs'] } diff --git a/assets/js/src/core/components/image-target/image-target.styles.tsx b/assets/js/src/core/components/asset-target/asset-target.styles.tsx similarity index 96% rename from assets/js/src/core/components/image-target/image-target.styles.tsx rename to assets/js/src/core/components/asset-target/asset-target.styles.tsx index abf6ee40d..1ff7d4b4c 100644 --- a/assets/js/src/core/components/image-target/image-target.styles.tsx +++ b/assets/js/src/core/components/asset-target/asset-target.styles.tsx @@ -15,7 +15,7 @@ import { createStyles } from 'antd-style' export const useStyle = createStyles(({ token, css }) => { return { - imageTargetContainer: css` + assetTargetContainer: css` border-radius: ${token.borderRadiusLG}px; outline: 1px dashed ${token.colorBorder}; background: ${token.controlItemBgHover}; diff --git a/assets/js/src/core/components/image-target/image-target.tsx b/assets/js/src/core/components/asset-target/asset-target.tsx similarity index 90% rename from assets/js/src/core/components/image-target/image-target.tsx rename to assets/js/src/core/components/asset-target/asset-target.tsx index c45c0d6e0..328c78795 100644 --- a/assets/js/src/core/components/image-target/image-target.tsx +++ b/assets/js/src/core/components/asset-target/asset-target.tsx @@ -13,7 +13,7 @@ import React, { forwardRef, type MutableRefObject } from 'react' import { Flex } from '@Pimcore/components/flex/flex' -import { useStyle } from './image-target.styles' +import { useStyle } from './asset-target.styles' import cn from 'classnames' import { toCssDimension } from '@Pimcore/utils/css' import { Icon } from '@Pimcore/components/icon/icon' @@ -22,7 +22,7 @@ import { IconButton } from '@Pimcore/components/icon-button/icon-button' import { Tooltip } from 'antd' import { useTranslation } from 'react-i18next' -interface ImageTargetProps { +interface AssetTargetProps { onRemove?: (event: React.MouseEvent) => void title: string className?: string @@ -32,14 +32,14 @@ interface ImageTargetProps { uploadIcon?: boolean } -export const ImageTarget = forwardRef(function ImageTarget ({ title, className, width = 200, height = 200, dndIcon, uploadIcon, onRemove }: ImageTargetProps, ref: MutableRefObject): React.JSX.Element { +export const AssetTarget = forwardRef(function AssetTarget ({ title, className, width = 200, height = 200, dndIcon, uploadIcon, onRemove }: AssetTargetProps, ref: MutableRefObject): React.JSX.Element { const { getStateClasses } = useDroppable() const { styles } = useStyle() const { t } = useTranslation() return (
diff --git a/assets/js/src/core/components/image-preview/image-preview.tsx b/assets/js/src/core/components/image-preview/image-preview.tsx index 0ee53ebb7..7ed785226 100644 --- a/assets/js/src/core/components/image-preview/image-preview.tsx +++ b/assets/js/src/core/components/image-preview/image-preview.tsx @@ -26,6 +26,7 @@ import { ImagePreviewDropdown } from '@Pimcore/components/image-preview/componen interface ImagePreviewProps { src?: string assetId?: number + assetType?: 'image' | 'video' className?: string width: number | string height: number | string @@ -34,12 +35,37 @@ interface ImagePreviewProps { dropdownItems?: DropdownProps['menu']['items'] } -export const ImagePreview = forwardRef(function ImagePreview ({ src, assetId, width, height, className, style, dropdownItems, bordered = false }: ImagePreviewProps, ref: MutableRefObject): React.JSX.Element { +export const ImagePreview = forwardRef(function ImagePreview ({ src, assetId, assetType, width, height, className, style, dropdownItems, bordered = false }: ImagePreviewProps, ref: MutableRefObject): React.JSX.Element { const [key, setKey] = React.useState(0) + const [thumbnailDimensions, setThumbnailDimensions] = React.useState({ width: 0, height: 0 }) const { getStateClasses } = useDroppable() const { styles } = useStyle() + const wrapperRef = React.useRef(null) - const imageSrc = assetId !== undefined ? `${getPrefix()}/assets/${assetId}/image/stream/preview` : src + const getAssetPreviewUrl = (): string | undefined => { + const { width, height } = thumbnailDimensions + + if (width === 0 || height === 0) { + return undefined + } + + if (assetType === 'video') { + return `${getPrefix()}/assets/${assetId}/video/stream/image-thumbnail?width=${width}&height=${height}&frame=true&aspectRatio=true` + } + + return `${getPrefix()}/assets/${assetId}/image/stream/custom?width=${width}&height=${height}&mimeType=JPEG&resizeMode=none&frame=true` + } + + const imageSrc = assetId !== undefined ? getAssetPreviewUrl() : src + + useEffect(() => { + if (wrapperRef?.current !== null && wrapperRef?.current !== undefined) { + setThumbnailDimensions({ + width: wrapperRef.current.offsetWidth, + height: wrapperRef.current.offsetHeight + }) + } + }, [wrapperRef, width, height]) useEffect(() => { setKey(key + 1) @@ -56,25 +82,29 @@ export const ImagePreview = forwardRef(function ImagePreview ({ src, assetId, wi ) return ( -
- +
+
+ { imageSrc !== undefined && ( + + ) } - + +
) }) diff --git a/assets/js/src/core/components/modal/modal.styles.tsx b/assets/js/src/core/components/modal/modal.styles.tsx index 40b85aa8c..0495b3d28 100644 --- a/assets/js/src/core/components/modal/modal.styles.tsx +++ b/assets/js/src/core/components/modal/modal.styles.tsx @@ -16,9 +16,6 @@ import { createStyles } from 'antd-style' export const useStyle = createStyles(({ token, css }) => { return { modal: css` - &.ant-modal .ant-modal-footer > .ant-btn + .ant-btn { - margin-inline-start: 0; - } .ant-modal-content { width: 100%; diff --git a/assets/js/src/core/components/modal/window-modal/window-modal.styles.tsx b/assets/js/src/core/components/modal/window-modal/window-modal.styles.tsx new file mode 100644 index 000000000..ded219e4b --- /dev/null +++ b/assets/js/src/core/components/modal/window-modal/window-modal.styles.tsx @@ -0,0 +1,25 @@ +/** +* 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 { + modal: css` + .ant-modal-content { + outline: 1px solid ${token.colorBorderContainer}; + box-shadow: ${token.boxShadowSecondary} !important; + } + ` + } +}, { hashPriority: 'low' }) diff --git a/assets/js/src/core/components/modal/window-modal/window-modal.tsx b/assets/js/src/core/components/modal/window-modal/window-modal.tsx new file mode 100644 index 000000000..6761f1cb4 --- /dev/null +++ b/assets/js/src/core/components/modal/window-modal/window-modal.tsx @@ -0,0 +1,91 @@ +/** +* 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, useRef } from 'react' +import { type IModalProps, Modal } from '@Pimcore/components/modal/modal' +import type { DraggableData, DraggableEvent } from 'react-draggable' +import Draggable from 'react-draggable' +import { useStyle } from './window-modal.styles' +import cn from 'classnames' +import { Icon } from '@Pimcore/components/icon/icon' +import { Flex } from '@Pimcore/components/flex/flex' + +export interface IWindowModalProps extends Omit { + +} + +export const WindowModal = (props: IWindowModalProps): React.JSX.Element => { + const { styles } = useStyle() + + const [disabled, setDisabled] = useState(true) + const [bounds, setBounds] = useState({ left: 0, top: 0, bottom: 0, right: 0 }) + const draggleRef = useRef(null!) + + const onStart = (_event: DraggableEvent, uiData: DraggableData): void => { + const { clientWidth, clientHeight } = window.document.documentElement + const targetRect = draggleRef.current?.getBoundingClientRect() + if (targetRect === undefined) { + return + } + setBounds({ + left: -targetRect.left + uiData.x, + right: clientWidth - (targetRect.right - uiData.x), + top: -targetRect.top + uiData.y, + bottom: clientHeight - (targetRect.bottom - uiData.y) + }) + } + + return ( + ( + { onStart(event, uiData) } } + > +
{modal}
+
+ ) } + title={ +
{} } + onFocus={ () => {} } + onMouseOut={ () => { + setDisabled(true) + } } + onMouseOver={ () => { + if (disabled) { + setDisabled(false) + } + } } + // fix eslintjsx-a11y/mouse-events-have-key-events + // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/mouse-events-have-key-events.md + style={ { width: '100%', cursor: 'move', flex: 1 } } + // end + > + + {props.title ?? } + +
+ } + wrapStyle={ { pointerEvents: 'none' } } + > + {props.children} +
+ ) +} 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 728e39b85..5ab42497e 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,7 +16,7 @@ import { Card } from '@Pimcore/components/card/card' import { ExternalImageFooter } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/external-image/footer' -import { ImageTarget } from '@Pimcore/components/image-target/image-target' +import { AssetTarget } from '@Pimcore/components/asset-target/asset-target' import { useTranslation } from 'react-i18next' import { ImagePreview } from '@Pimcore/components/image-preview/image-preview' @@ -71,7 +71,7 @@ export const ExternalImage = (props: ExternalImageProps): React.JSX.Element => { /> ) : ( - void + onOpenElement?: () => void + allowedElementTypes?: ElementType[] + allowedElementSubTypes?: string[] +} + +export const Href = (props: HrefProps): React.JSX.Element => { + const [value, setValue] = React.useState(props.value ?? null) + const { openElement } = useElementHelper() + + useEffect(() => { + props.onChange?.(value) + }, [value]) + + const clickOpenElement = (): void => { + if (value !== null) { + openElement(value).catch(() => {}) + props.onOpenElement?.() + } + } + + const allowedElementTypes = props.allowedElementTypes ?? allElementTypes + const allowedElementSubTypes = props.allowedElementSubTypes ?? [] + + return ( + +
+ props.disabled !== true && allElementTypes.includes(info.type) } + isValidData={ (info: DragAndDropInfo) => allowedElementTypes.includes(info.type) && (allowedElementSubTypes.length === 0 || allowedElementSubTypes.includes(info.data.type as string)) } + onDrop={ (info: DragAndDropInfo) => { + setValue({ + type: info.type as ElementType, + id: info.data.id as number, + fullPath: `${info.data.path}${info.data.filename ?? info.data.key}` + }) + } } + > + + +
+ + +
+ ) +} diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/href/path-target.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/href/path-target.tsx new file mode 100644 index 000000000..3b77f44a7 --- /dev/null +++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/href/path-target.tsx @@ -0,0 +1,54 @@ +/** +* 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, { forwardRef, type MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import { Input } from '@Pimcore/components/input/input' +import { + type HrefValue +} from './href' +import { useDroppable } from '@Pimcore/components/drag-and-drop/hooks/use-droppable' +import cn from 'classnames' + +export interface PathTargetProps { + value: HrefValue | null + disabled?: boolean +} + +export const PathTarget = forwardRef(function PathTarget ( + props: PathTargetProps, + ref: MutableRefObject +): React.JSX.Element { + const { t } = useTranslation() + const { getStateClasses } = useDroppable() + + const displayText = props.value === null + ? undefined + : props.value.fullPath ?? props.value.id + + return ( +
+ +
+ ) +}) diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-target/image-target.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-target/image-target.tsx index d21d2d64a..f86f903b5 100644 --- a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-target/image-target.tsx +++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-target/image-target.tsx @@ -13,7 +13,7 @@ import React from 'react' import { Droppable } from '@Pimcore/components/drag-and-drop/droppable' -import { ImageTarget } from '@Pimcore/components/image-target/image-target' +import { AssetTarget } from '@Pimcore/components/asset-target/asset-target' import type { DragAndDropInfo } from '@Pimcore/components/drag-and-drop/context-provider' import type { ImageGalleryValueItem } from '../../image-gallery' import { useTranslation } from 'react-i18next' @@ -39,7 +39,7 @@ export const ImageGalleryImageTarget = ({ index, value, setValue, disabled }: Im } } variant="outline" > - { const { openAsset } = useAssetHelper() return ( - - - , - - { - if (typeof props.value?.id === 'number') { - openAsset({ config: { id: props.value.id } }) - } - } } - /> - - ] } + + + , + + { + if (typeof props.value?.id === 'number') { + openAsset({ config: { id: props.value.id } }) + } + } } + /> + + ] } + noSpacing /> ) } 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 index f8323dc77..00a27e1b9 100644 --- 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 @@ -16,7 +16,7 @@ import { Card } from '@Pimcore/components/card/card' import { ImageFooter } from './footer' -import { ImageTarget } from '@Pimcore/components/image-target/image-target' +import { AssetTarget } from '@Pimcore/components/asset-target/asset-target' import { useTranslation } from 'react-i18next' import { ImagePreview } from '@Pimcore/components/image-preview/image-preview' import { Droppable } from '@Pimcore/components/drag-and-drop/droppable' @@ -74,7 +74,7 @@ export const Image = (props: ImageProps): React.JSX.Element => { /> ) : ( - void + disabled?: boolean + value?: VideoValue | null + onSave?: (value: VideoValue) => void + allowedVideoTypes?: VideoType[] +} + +export const VideoFooter = (props: VideoFooterProps): React.JSX.Element => { + const { t } = useTranslation() + + const [isModalVisible, setIsModalVisible] = useState(false) + const firstType = props.allowedVideoTypes?.[0] ?? 'asset' + const [value, setValue] = useState(props.value ?? { type: firstType, data: null }) + const [form] = Form.useForm() + + useEffect(() => { + form.setFieldsValue({ + type: value.type, + data: value.data + }) + if (value.type === 'asset') { + form.setFieldsValue({ + title: value.title, + description: value.description, + poster: value.poster + }) + } + }, [value]) + + useEffect(() => { + setValue(props.value ?? { type: firstType, data: null }) + }, [props.value]) + + const showModal = (): void => { + setIsModalVisible(true) + } + + const handleOk = (): void => { + const sanitizedValue = sanitizeVideoIds(value) + setValue(sanitizedValue) + props.onSave?.(sanitizedValue) + setIsModalVisible(false) + } + + const handleCancel = (): void => { + setIsModalVisible(false) + } + + const sanitizeVideoIds = (videoValue: VideoValue): VideoValue => { + let { type, data } = videoValue + + if (type === 'asset') { + return videoValue + } + + if (typeof data === 'string' && data !== '') { + const videoId = parseVideoIdFromUrl(data, type) + data = videoId ?? data + } + + return { + type, + data: data as string + } + } + + const handleValuesChange = (changedValues: any, allValues: VideoValue): void => { + setValue(allValues) + } + + const getVideoTypeOptions = (): Array<{ value: VideoType, label: string }> => { + const allowedVideoTypes = props.allowedVideoTypes ?? ['asset', 'youtube', 'vimeo', 'dailymotion'] + return allowedVideoTypes.map(type => { + return { + value: type, + label: t(`video.type.${type}`) + } + }) + } + + return ( + <> + + + , + + + + ] } + noSpacing + /> + : undefined } + okText={ t('save') } + onCancel={ handleCancel } + onOk={ handleOk } + open={ isModalVisible } + size="M" + title={ t('video.settings') } + > +
+ + + + )} + + { value.type === 'asset' && ( + <> + + { setIsModalVisible(false) } } + /> + + + + + +