}
+ indicator={ <>
+ {icon}
+ > }
{ ...props }
/>
diff --git a/assets/js/src/core/modules/asset/actions/clear-thumbnails/use-clear-thumbnails.tsx b/assets/js/src/core/modules/asset/actions/clear-thumbnails/use-clear-thumbnails.tsx
new file mode 100644
index 000000000..229b5d0ca
--- /dev/null
+++ b/assets/js/src/core/modules/asset/actions/clear-thumbnails/use-clear-thumbnails.tsx
@@ -0,0 +1,77 @@
+/**
+* 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 ItemType } from '@Pimcore/components/dropdown/dropdown'
+import { type Asset, useAssetImageClearThumbnailMutation } from '@Pimcore/modules/asset/asset-api-slice.gen'
+import { Icon } from '@Pimcore/components/icon/icon'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { checkElementPermission } from '@Pimcore/modules/element/permissions/permission-helper'
+
+export interface UseClearThumbnailsHookReturn {
+ clearImageThumbnailContextMenuItem: (node: Asset, onFinish?: () => void) => ItemType
+ clearVideoThumbnailContextMenuItem: (node: Asset, onFinish?: () => void) => ItemType
+ clearPdfThumbnailContextMenuItem: (node: Asset, onFinish?: () => void) => ItemType
+}
+
+export const useClearThumbnails = (): UseClearThumbnailsHookReturn => {
+ const { t } = useTranslation()
+ const [clearThumbnail] = useAssetImageClearThumbnailMutation()
+
+ const handleClearThumbnails = async (node: Asset, onFinish?: () => void): Promise
=> {
+ const clearThumbnailTask = clearThumbnail({ id: node.id })
+
+ try {
+ await clearThumbnailTask
+ onFinish?.()
+ } catch (error) {
+ console.error('Error clearing thumbnails (id: ' + node.id + ')', error)
+ }
+ }
+
+ const clearImageThumbnailContextMenuItem = (node: Asset, onFinish?: () => void): ItemType => {
+ return {
+ label: t('asset.tree.context-menu.clear-thumbnails'),
+ key: 'clear-image-thumbnails',
+ icon: ,
+ hidden: node.type !== 'image' || !checkElementPermission(node.permissions!, 'publish'),
+ onClick: async () => { await handleClearThumbnails(node, onFinish) }
+ }
+ }
+
+ const clearVideoThumbnailContextMenuItem = (node: Asset, onFinish?: () => void): ItemType => {
+ return {
+ label: t('asset.tree.context-menu.clear-thumbnails'),
+ key: 'clear-video-thumbnails',
+ icon: ,
+ hidden: node.type !== 'video' || !checkElementPermission(node.permissions!, 'publish'),
+ onClick: async () => { await handleClearThumbnails(node, onFinish) }
+ }
+ }
+
+ const clearPdfThumbnailContextMenuItem = (node: Asset, onFinish?: () => void): ItemType => {
+ return {
+ label: t('asset.tree.context-menu.clear-thumbnails'),
+ key: 'clear-pdf-thumbnails',
+ icon: ,
+ hidden: node.mimeType !== 'application/pdf' || !checkElementPermission(node.permissions!, 'publish'),
+ onClick: async () => { await handleClearThumbnails(node, onFinish) }
+ }
+ }
+
+ return {
+ clearImageThumbnailContextMenuItem,
+ clearVideoThumbnailContextMenuItem,
+ clearPdfThumbnailContextMenuItem
+ }
+}
diff --git a/assets/js/src/core/modules/asset/actions/zip-download/use-zip-download.tsx b/assets/js/src/core/modules/asset/actions/zip-download/use-zip-download.tsx
index 2bcf0c00f..1651d091e 100644
--- a/assets/js/src/core/modules/asset/actions/zip-download/use-zip-download.tsx
+++ b/assets/js/src/core/modules/asset/actions/zip-download/use-zip-download.tsx
@@ -21,6 +21,7 @@ import type { ItemType } from '@Pimcore/components/dropdown/dropdown'
import { Icon } from '@Pimcore/components/icon/icon'
import React from 'react'
import { checkElementPermission } from '@Pimcore/modules/element/permissions/permission-helper'
+import { type Element, getElementKey } from '@Pimcore/modules/element/element-helper'
export interface ICreateZipDownloadProps {
jobTitle: string
@@ -44,7 +45,8 @@ export interface UseZipDownloadHookProps {
export interface UseZipDownloadHookReturn {
createZipDownload: CreateFolderZipDownload | CreateAssetListZipDownload
- createZipDownloadContextMenuItem: (node: TreeNodeProps) => ItemType
+ createZipDownloadContextMenuItem: (node: Element, onFinish?: () => void) => ItemType
+ createZipDownloadTreeContextMenuItem: (node: TreeNodeProps) => ItemType
}
export const useZipDownload = (props: UseZipDownloadHookProps): UseZipDownloadHookReturn => {
@@ -79,7 +81,22 @@ export const useZipDownload = (props: UseZipDownloadHookProps): UseZipDownloadHo
}))
}
- const createZipDownloadContextMenuItem = (node: TreeNodeProps): ItemType => {
+ const createZipDownloadContextMenuItem = (node: Element, onFinish?: () => void): ItemType => {
+ return {
+ label: t('asset.tree.context-menu.download-as-zip'),
+ key: 'download-as-zip',
+ icon: ,
+ hidden: node.type !== 'folder' || !checkElementPermission(node.permissions!, 'view'),
+ onClick: () => {
+ createZipDownload({
+ jobTitle: getElementKey(node, 'asset'),
+ requestData: { body: { folders: [node.id] } }
+ })
+ }
+ }
+ }
+
+ const createZipDownloadTreeContextMenuItem = (node: TreeNodeProps): ItemType => {
return {
label: t('asset.tree.context-menu.download-as-zip'),
key: 'download-as-zip',
@@ -97,12 +114,14 @@ export const useZipDownload = (props: UseZipDownloadHookProps): UseZipDownloadHo
if (props.type === 'folder') {
return {
createZipDownload: createZipDownload as CreateFolderZipDownload,
+ createZipDownloadTreeContextMenuItem,
createZipDownloadContextMenuItem
}
}
return {
createZipDownload: createZipDownload as CreateAssetListZipDownload,
+ createZipDownloadTreeContextMenuItem,
createZipDownloadContextMenuItem
}
}
diff --git a/assets/js/src/core/modules/asset/editor/title/title-container.tsx b/assets/js/src/core/modules/asset/editor/title/title-container.tsx
index 61fe2a6c1..a5441d627 100644
--- a/assets/js/src/core/modules/asset/editor/title/title-container.tsx
+++ b/assets/js/src/core/modules/asset/editor/title/title-container.tsx
@@ -15,14 +15,21 @@ import React from 'react'
import { TabTitleContainer, type TabTitleContainerProps } from '@Pimcore/modules/widget-manager/title/tab-title-container'
import { useAssetDraft } from '../../hooks/use-asset-draft'
import { useTranslation } from 'react-i18next'
+import { useAssetGetByIdQuery } from '@Pimcore/modules/asset/asset-api-slice.gen'
export const TitleContainer = (props: TabTitleContainerProps): React.JSX.Element => {
const { node } = props
const { asset } = useAssetDraft(node.getConfig().id as number)
+ const { data } = useAssetGetByIdQuery({ id: node.getConfig().id })
const { t } = useTranslation()
- if (asset?.parentId === 0) {
- node.getName = () => t('home')
+ const nodeName = node.getName()
+ node.getName = () => {
+ if (asset?.parentId === 0) {
+ return t('home')
+ }
+
+ return data?.filename ?? nodeName
}
return (
diff --git a/assets/js/src/core/modules/asset/editor/toolbar/context-menu/context-menu.tsx b/assets/js/src/core/modules/asset/editor/toolbar/context-menu/context-menu.tsx
index 123bae594..1d7924aa3 100644
--- a/assets/js/src/core/modules/asset/editor/toolbar/context-menu/context-menu.tsx
+++ b/assets/js/src/core/modules/asset/editor/toolbar/context-menu/context-menu.tsx
@@ -15,19 +15,45 @@ import { Popconfirm } from 'antd'
import { IconButton } from '@Pimcore/components/icon-button/icon-button'
import ButtonGroup from 'antd/es/button/button-group'
import React, { useContext, useState } from 'react'
-import { api } from '@Pimcore/modules/asset/asset-api-slice-enhanced'
-import { invalidatingTags } from '@Pimcore/app/api/pimcore/tags'
-import { useAppDispatch } from '@Pimcore/app/store'
+import { type Asset } from '@Pimcore/modules/asset/asset-api-slice-enhanced'
import { useTranslation } from 'react-i18next'
import { AssetContext } from '@Pimcore/modules/asset/asset-provider'
import { useAssetDraft } from '@Pimcore/modules/asset/hooks/use-asset-draft'
+import { useRename } from '@Pimcore/modules/element/actions/rename/use-rename'
+import { Dropdown, type DropdownMenuProps } from '@Pimcore/components/dropdown/dropdown'
+import { useDelete } from '@Pimcore/modules/element/actions/delete/use-delete'
+import { useDownload } from '@Pimcore/modules/asset/actions/download/use-download'
+import { DropdownButton } from '@Pimcore/components/dropdown-button/dropdown-button'
+import { useZipDownload } from '@Pimcore/modules/asset/actions/zip-download/use-zip-download'
+import { useClearThumbnails } from '@Pimcore/modules/asset/actions/clear-thumbnails/use-clear-thumbnails'
+import { useElementRefresh } from '@Pimcore/modules/element/actions/refresh-element/use-element-refresh'
export const EditorToolbarContextMenu = (): React.JSX.Element => {
const { t } = useTranslation()
- const dispatch = useAppDispatch()
const { id } = useContext(AssetContext)
- const { asset, removeAssetFromState } = useAssetDraft(id)
+ const { asset } = useAssetDraft(id)
const [popConfirmOpen, setPopConfirmOpen] = useState(false)
+ const { renameContextMenuItem } = useRename('asset')
+ const { deleteContextMenuItem } = useDelete('asset')
+ const { downloadContextMenuItem } = useDownload()
+ const { createZipDownloadContextMenuItem } = useZipDownload({ type: 'folder' })
+ const { refreshElement } = useElementRefresh(asset!.id, 'asset')
+ const {
+ clearImageThumbnailContextMenuItem,
+ clearVideoThumbnailContextMenuItem,
+ clearPdfThumbnailContextMenuItem
+ } = useClearThumbnails()
+
+ const items: DropdownMenuProps['items'] = [
+ renameContextMenuItem(asset as Asset, () => { refreshElement() }),
+ deleteContextMenuItem(asset as Asset),
+ downloadContextMenuItem(asset as Asset),
+ createZipDownloadContextMenuItem(asset as Asset),
+ clearImageThumbnailContextMenuItem(asset as Asset),
+ clearVideoThumbnailContextMenuItem(asset as Asset),
+ clearPdfThumbnailContextMenuItem(asset as Asset)
+ ]
+ const visibleItems = items.filter(item => (item !== null && 'hidden' in item) ? item?.hidden === false : false)
return (
@@ -44,6 +70,14 @@ export const EditorToolbarContextMenu = (): React.JSX.Element => {
{t('toolbar.reload')}
+
+ {visibleItems.length > 0 && (
+
+
+ {t('toolbar.more')}
+
+
+ )}
)
@@ -56,21 +90,16 @@ export const EditorToolbarContextMenu = (): React.JSX.Element => {
if (Object.keys(asset?.changes ?? {}).length > 0) {
setPopConfirmOpen(true)
} else {
- refreshAsset()
+ refreshElement()
}
}
function onConfirm (): void {
setPopConfirmOpen(false)
- refreshAsset()
+ refreshElement()
}
function onCancel (): void {
setPopConfirmOpen(false)
}
-
- function refreshAsset (): void {
- removeAssetFromState()
- dispatch(api.util.invalidateTags(invalidatingTags.ASSET_DETAIL_ID(id)))
- }
}
diff --git a/assets/js/src/core/modules/asset/editor/toolbar/toolbar.tsx b/assets/js/src/core/modules/asset/editor/toolbar/toolbar.tsx
index 6c1724e8e..85e58813a 100644
--- a/assets/js/src/core/modules/asset/editor/toolbar/toolbar.tsx
+++ b/assets/js/src/core/modules/asset/editor/toolbar/toolbar.tsx
@@ -117,6 +117,21 @@ export const Toolbar = (): React.JSX.Element => {
if (asset.changes.customMetadata) {
update.metadata = customMetadata?.map((metadata: CustomMetadata): CustomMetadataApi => {
const { rowId, ...metadataApi } = metadata
+
+ if (metadataApi.type.startsWith('metadata.')) {
+ metadataApi.type = metadataApi.type.replace('metadata.', '')
+ }
+
+ if (metadataApi.data === null) {
+ if (metadataApi.type === 'input' || metadataApi.type === 'textarea') {
+ metadataApi.data = ''
+ }
+
+ if (metadataApi.type === 'checkbox') {
+ metadataApi.data = false
+ }
+ }
+
return metadataApi
})
}
diff --git a/assets/js/src/core/modules/asset/hooks/use-asset-draft.ts b/assets/js/src/core/modules/asset/hooks/use-asset-draft.ts
index 3b88de548..69e12eecb 100644
--- a/assets/js/src/core/modules/asset/hooks/use-asset-draft.ts
+++ b/assets/js/src/core/modules/asset/hooks/use-asset-draft.ts
@@ -59,7 +59,7 @@ import { useInjection } from '@Pimcore/app/depency-injection'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import { initialTabsStateValue, useTabsDraft, type UseTabsDraftReturn } from '@Pimcore/modules/element/draft/hooks/use-tabs'
-interface UseAssetDraftReturn extends
+export interface UseAssetDraftReturn extends
UseCustomMetadataDraftReturn,
UsePropertiesDraftReturn,
UseSchedulesDraftReturn,
diff --git a/assets/js/src/core/modules/asset/tree/context-menu/context-menu.tsx b/assets/js/src/core/modules/asset/tree/context-menu/context-menu.tsx
index 8e5f10242..8c399e57c 100644
--- a/assets/js/src/core/modules/asset/tree/context-menu/context-menu.tsx
+++ b/assets/js/src/core/modules/asset/tree/context-menu/context-menu.tsx
@@ -39,7 +39,7 @@ export const AssetTreeContextMenu = (props: TreeContextMenuProps): React.JSX.Ele
const uploadZipRef = React.useRef(null)
const uploadContext = React.useContext(UploadContext)!
- const { createZipDownloadContextMenuItem } = useZipDownload({ type: 'folder' })
+ const { createZipDownloadTreeContextMenuItem } = useZipDownload({ type: 'folder' })
const { addFolderTreeContextMenuItem } = useAddFolder('asset')
const { renameTreeContextMenuItem } = useRename('asset')
const { deleteTreeContextMenuItem } = useDelete('asset')
@@ -92,7 +92,7 @@ export const AssetTreeContextMenu = (props: TreeContextMenuProps): React.JSX.Ele
cutTreeContextMenuItem(props.node),
pasteCutContextMenuItem(parseInt(props.node.id)),
deleteTreeContextMenuItem(props.node),
- createZipDownloadContextMenuItem(props.node),
+ createZipDownloadTreeContextMenuItem(props.node),
uploadNewVersionTreeContextMenuItem(props.node),
downloadTreeContextMenuItem(props.node),
{
diff --git a/assets/js/src/core/modules/asset/tree/node/with-action-states.tsx b/assets/js/src/core/modules/asset/tree/node/with-action-states.tsx
new file mode 100644
index 000000000..dc257d682
--- /dev/null
+++ b/assets/js/src/core/modules/asset/tree/node/with-action-states.tsx
@@ -0,0 +1,34 @@
+/**
+* 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 TreeNodeProps } from '@Pimcore/components/element-tree/node/tree-node'
+import React, { type ElementType, type ReactElement } from 'react'
+import { useAssetPatchByIdMutation } from '../../asset-api-slice-enhanced'
+import { useElementDeleteMutation } from '@Pimcore/modules/element/element-api-slice.gen'
+
+export const withActionStates = (Component: ElementType): ElementType => {
+ const ActionStates = (props: TreeNodeProps): ReactElement => {
+ const [, { isLoading }] = useAssetPatchByIdMutation({ fixedCacheKey: `ASSET_ACTION_RENAME_ID_${props.id}` })
+ const [, { isLoading: isDeleteLoading }] = useElementDeleteMutation({ fixedCacheKey: `ASSET_ACTION_DELETE_ID_${props.id}` })
+
+ return (
+
+ )
+ }
+
+ return ActionStates
+}
diff --git a/assets/js/src/core/modules/asset/tree/tree-container.tsx b/assets/js/src/core/modules/asset/tree/tree-container.tsx
index e31d1b572..c40294b2e 100644
--- a/assets/js/src/core/modules/asset/tree/tree-container.tsx
+++ b/assets/js/src/core/modules/asset/tree/tree-container.tsx
@@ -26,6 +26,7 @@ import { transformApiDataToNodes } from './utils/transform-api-data-to-node'
import { Skeleton } from '@Pimcore/components/element-tree/skeleton/skeleton'
import { useTranslation } from 'react-i18next'
import { Box } from '@Pimcore/components/box/box'
+import { withActionStates } from './node/with-action-states'
export interface TreeContainerProps {
id: number
@@ -95,7 +96,7 @@ const TreeContainer = ({ id = 1 }: TreeContainerProps): React.JSX.Element => {
nodeId={ id }
onSelect={ onSelect }
renderFilter={ SearchContainer }
- renderNode={ withDraggable(TreeNode) }
+ renderNode={ withActionStates(withDraggable(TreeNode)) }
renderNodeContent={ defaultProps.renderNodeContent }
renderPager={ PagerContainer }
rootNode={ rootNode }
diff --git a/assets/js/src/core/modules/data-object/editor/toolbar/context-menu/context-menu.tsx b/assets/js/src/core/modules/data-object/editor/toolbar/context-menu/context-menu.tsx
index 083b05412..260bcba78 100644
--- a/assets/js/src/core/modules/data-object/editor/toolbar/context-menu/context-menu.tsx
+++ b/assets/js/src/core/modules/data-object/editor/toolbar/context-menu/context-menu.tsx
@@ -15,19 +15,17 @@ import { Popconfirm } from 'antd'
import { IconButton } from '@Pimcore/components/icon-button/icon-button'
import ButtonGroup from 'antd/es/button/button-group'
import React, { useContext, useState } from 'react'
-import { api } from '@Pimcore/modules/data-object/data-object-api-slice-enhanced'
-import { invalidatingTags } from '@Pimcore/app/api/pimcore/tags'
-import { useAppDispatch } from '@Pimcore/app/store'
import { useTranslation } from 'react-i18next'
import { useDataObjectDraft } from '@Pimcore/modules/data-object/hooks/use-data-object-draft'
import { DataObjectContext } from '@Pimcore/modules/data-object/data-object-provider'
+import { useElementRefresh } from '@Pimcore/modules/element/actions/refresh-element/use-element-refresh'
export const EditorToolbarContextMenu = (): React.JSX.Element => {
const { t } = useTranslation()
- const dispatch = useAppDispatch()
const { id } = useContext(DataObjectContext)
- const { dataObject, removeDataObjectFromState } = useDataObjectDraft(id)
+ const { dataObject } = useDataObjectDraft(id)
const [popConfirmOpen, setPopConfirmOpen] = useState(false)
+ const { refreshElement } = useElementRefresh(id, 'data-object')
return (
@@ -56,21 +54,16 @@ export const EditorToolbarContextMenu = (): React.JSX.Element => {
if (Object.keys(dataObject?.changes ?? {}).length > 0) {
setPopConfirmOpen(true)
} else {
- refreshDataObject()
+ refreshElement()
}
}
function onConfirm (): void {
setPopConfirmOpen(false)
- refreshDataObject()
+ refreshElement()
}
function onCancel (): void {
setPopConfirmOpen(false)
}
-
- function refreshDataObject (): void {
- removeDataObjectFromState()
- dispatch(api.util.invalidateTags(invalidatingTags.DATA_OBJECT_DETAIL_ID(id)))
- }
}
diff --git a/assets/js/src/core/modules/data-object/editor/types/object/tab-manager/tabs/edit/components/data-component.tsx b/assets/js/src/core/modules/data-object/editor/types/object/tab-manager/tabs/edit/components/data-component.tsx
index c83a9bbfc..0e17f1b4a 100644
--- a/assets/js/src/core/modules/data-object/editor/types/object/tab-manager/tabs/edit/components/data-component.tsx
+++ b/assets/js/src/core/modules/data-object/editor/types/object/tab-manager/tabs/edit/components/data-component.tsx
@@ -22,6 +22,7 @@ import { useFormList } from '../providers/form-list-provider/use-form-list'
import { useLocalizedFields } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/localized-fields/provider/localized-fields-provider/use-localized-fields'
import { useLanguageSelection } from '@Pimcore/modules/data-object/editor/toolbar/language-selection/provider/use-language-selection'
import { Text } from '@Pimcore/components/text/text'
+import ErrorBoundary from '@Pimcore/modules/app/error-boundary/error-boundary'
export interface DataComponentProps extends ObjectComponentProps {
datatype: 'data'
@@ -82,18 +83,20 @@ export const DataComponent = (props: DataComponentProps): React.JSX.Element => {
if (!objectDataType.isCollectionType) {
return (
-
- {objectDataType.getObjectDataComponent(_props)}
-
+
+
+ {objectDataType.getObjectDataComponent(_props)}
+
+
)
}
return (
- <>
+
{objectDataType.getObjectDataComponent(_props)}
- >
+
)
}
diff --git a/assets/js/src/core/modules/data-object/hooks/use-data-object-draft.ts b/assets/js/src/core/modules/data-object/hooks/use-data-object-draft.ts
index 8c9279ffe..7b107e1cc 100644
--- a/assets/js/src/core/modules/data-object/hooks/use-data-object-draft.ts
+++ b/assets/js/src/core/modules/data-object/hooks/use-data-object-draft.ts
@@ -42,7 +42,7 @@ import { useInjection } from '@Pimcore/app/depency-injection'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import { initialTabsStateValue, useTabsDraft, type UseTabsDraftReturn } from '@Pimcore/modules/element/draft/hooks/use-tabs'
-interface UseDataObjectDraftReturn extends
+export interface UseDataObjectDraftReturn extends
UsePropertiesDraftReturn,
UseSchedulesDraftReturn,
UseTabsDraftReturn,
diff --git a/assets/js/src/core/modules/data-object/tree/context-menu/context-menu.tsx b/assets/js/src/core/modules/data-object/tree/context-menu/context-menu.tsx
index de564d1a3..f38ed5676 100644
--- a/assets/js/src/core/modules/data-object/tree/context-menu/context-menu.tsx
+++ b/assets/js/src/core/modules/data-object/tree/context-menu/context-menu.tsx
@@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next'
import { type TreeNodeProps } from '@Pimcore/components/element-tree/node/tree-node'
import { Dropdown, type DropdownMenuProps } from '@Pimcore/components/dropdown/dropdown'
import { Icon } from '@Pimcore/components/icon/icon'
-import { useZipDownload } from '@Pimcore/modules/asset/actions/zip-download/use-zip-download'
import { useAddFolder } from '@Pimcore/modules/element/actions/add-folder/use-add-folder'
import { useRename } from '@Pimcore/modules/element/actions/rename/use-rename'
import { useDelete } from '@Pimcore/modules/element/actions/delete/use-delete'
@@ -32,7 +31,6 @@ export interface DataObjectTreeContextMenuProps {
export const DataObjectTreeContextMenu = (props: DataObjectTreeContextMenuProps): React.JSX.Element => {
const { t } = useTranslation()
- const { createZipDownloadContextMenuItem } = useZipDownload({ type: 'folder' })
const { addFolderTreeContextMenuItem } = useAddFolder('data-object')
const { renameTreeContextMenuItem } = useRename('data-object')
const { deleteTreeContextMenuItem } = useDelete('data-object')
@@ -48,7 +46,6 @@ export const DataObjectTreeContextMenu = (props: DataObjectTreeContextMenuProps)
cutTreeContextMenuItem(props.node),
pasteCutContextMenuItem(parseInt(props.node.id)),
deleteTreeContextMenuItem(props.node),
- createZipDownloadContextMenuItem(props.node),
{
label: t('element.tree.context-menu.advanced'),
diff --git a/assets/js/src/core/modules/element/actions/delete/use-delete.tsx b/assets/js/src/core/modules/element/actions/delete/use-delete.tsx
index 4380aed14..2757bff93 100644
--- a/assets/js/src/core/modules/element/actions/delete/use-delete.tsx
+++ b/assets/js/src/core/modules/element/actions/delete/use-delete.tsx
@@ -45,7 +45,7 @@ export const useDelete = (elementType: ElementType): UseDeleteHookReturn => {
const { refreshTree } = useRefreshTree(elementType)
const { refreshGrid } = useRefreshGrid(elementType)
const { getElementById } = useElementApi(elementType)
- const [elementDelete] = useElementDeleteMutation()
+ const [elementDelete] = useElementDeleteMutation({ fixedCacheKey: `${elementType.toUpperCase()}_ACTION_DELETE_ID_XY` })
const deleteElement = (id: number, label: string, parentId?: number, onFinish?: () => void): void => {
modal.confirm({
diff --git a/assets/js/src/core/modules/element/actions/refresh-element/use-element-refresh.tsx b/assets/js/src/core/modules/element/actions/refresh-element/use-element-refresh.tsx
new file mode 100644
index 000000000..022a6f745
--- /dev/null
+++ b/assets/js/src/core/modules/element/actions/refresh-element/use-element-refresh.tsx
@@ -0,0 +1,52 @@
+/**
+* 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 ElementType } from '../../../../../../types/element-type.d'
+import { useAppDispatch } from '@Pimcore/app/store'
+import { type UseAssetDraftReturn } from '@Pimcore/modules/asset/hooks/use-asset-draft'
+import { type UseDataObjectDraftReturn } from '@Pimcore/modules/data-object/hooks/use-data-object-draft'
+import { api as assetApi } from '@Pimcore/modules/asset/asset-api-slice-enhanced'
+import { api as dataObjectApi } from '@Pimcore/modules/data-object/data-object-api-slice-enhanced'
+import { invalidatingTags } from '@Pimcore/app/api/pimcore/tags'
+import { useElementDraft } from '@Pimcore/modules/element/hooks/use-element-draft'
+
+interface UseElementRefreshHookReturn {
+ refreshElement: () => void
+}
+
+export const useElementRefresh = (id: number, elementType: ElementType): UseElementRefreshHookReturn => {
+ const dispatch = useAppDispatch()
+ const draft = useElementDraft(id, elementType)
+
+ const refreshElement = (): void => {
+ if (elementType === 'asset') {
+ (draft as any as UseAssetDraftReturn).removeAssetFromState()
+ dispatch(
+ assetApi.util.invalidateTags(
+ invalidatingTags.ASSET_DETAIL_ID(id)
+ )
+ )
+ } else if (elementType === 'data-object') {
+ (draft as any as UseDataObjectDraftReturn).removeDataObjectFromState()
+ dispatch(
+ dataObjectApi.util.invalidateTags(
+ invalidatingTags.DATA_OBJECT_DETAIL_ID(id)
+ )
+ )
+ }
+ }
+
+ return {
+ refreshElement
+ }
+}
diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-preview/image-preview.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-preview/image-preview.tsx
index 8af8da410..232ae70cc 100644
--- a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-preview/image-preview.tsx
+++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/image-preview/image-preview.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, { useState } from 'react'
import { Droppable } from '@Pimcore/components/drag-and-drop/droppable'
import { ImagePreview } from '@Pimcore/components/image-preview/image-preview'
import { Icon } from '@Pimcore/components/icon/icon'
@@ -20,6 +20,23 @@ import { useAssetHelper } from '@Pimcore/modules/asset/hooks/use-asset-helper'
import { type DragAndDropInfo } from '@Pimcore/components/drag-and-drop/context-provider'
import type { ImageGalleryValueItem } from '../../image-gallery'
import type { UniqueIdentifier } from '@dnd-kit/core'
+import {
+ fromIHotspots,
+ toIHotspots
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/utils/hotspot-converter'
+import {
+ type Hotspot, type Marker
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/types/hotspot-types'
+import { type IHotspot } from '@Pimcore/components/hotspot-image/hotspot-image'
+import {
+ type HotspotMarkersModalContainerRef
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/hotspot-markers-modal-container'
+import { useMessage } from '@Pimcore/components/message/useMessage'
+import {
+ type ImageValue
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/image'
+import _ from 'lodash'
+import { useFormModal } from '@Pimcore/components/modal/form-modal/hooks/use-form-modal'
interface ImageGalleryImagePreviewProps {
item: ImageGalleryValueItem
@@ -27,11 +44,63 @@ interface ImageGalleryImagePreviewProps {
value: ImageGalleryValueItem[]
setValue: React.Dispatch>
disabled?: boolean
+ onHotspotsChange?: (hotspots: Hotspot[], marker: Marker[]) => void
+ hotspotMarkersModalContainer: React.RefObject
}
-export const ImageGalleryImagePreview = ({ item, index, value, setValue, disabled }: ImageGalleryImagePreviewProps): React.JSX.Element => {
+export const ImageGalleryImagePreview = ({ item, index, value, setValue, disabled, onHotspotsChange, hotspotMarkersModalContainer }: ImageGalleryImagePreviewProps): React.JSX.Element => {
const { t } = useTranslation()
const { openAsset } = useAssetHelper()
+ const [markerModalOpen, setMarkerModalOpen] = useState(false)
+ const messageApi = useMessage()
+ const { confirm } = useFormModal()
+
+ const hotspots = toIHotspots(item.hotspots ?? [], item.marker ?? [])
+
+ const hideMarkerModal = (): void => {
+ setMarkerModalOpen(false)
+ }
+
+ const onModalHotspotsChange = (iHotspots: IHotspot[]): void => {
+ const { hotspots, marker } = fromIHotspots(iHotspots)
+ onHotspotsChange?.(hotspots, marker)
+ }
+
+ if (hotspotMarkersModalContainer.current !== null) {
+ const hotspotMarkersModalProps = {
+ hotspots,
+ imageId: item.image!.id,
+ open: markerModalOpen,
+ onClose: hideMarkerModal,
+ onChange: onModalHotspotsChange
+ }
+ hotspotMarkersModalContainer.current?.setModal(index, hotspotMarkersModalProps)
+ }
+
+ const clearValueData = async (): Promise => {
+ setValue(value.map((v, i) => i === index ? { ...v, hotspots: [], marker: [] } : v))
+ await messageApi.success(t('hotspots.data-cleared'))
+ }
+
+ const hasHotspotData = (index: number): boolean => {
+ return !_.isEmpty(value[index].hotspots) || !_.isEmpty(value[index].marker)
+ }
+
+ const hasValueData = (index: number): boolean => {
+ return hasHotspotData(index)
+ }
+
+ const setImage = (index: number, image: ImageValue, replaceValueData: boolean): void => {
+ const newValue = [...value]
+
+ if (replaceValueData) {
+ newValue[index] = { image }
+ } else {
+ newValue[index] = { ...newValue[index], image }
+ }
+
+ setValue(newValue)
+ }
return (
{
- const newValue = [...value]
- newValue[index] = { image: { type: 'asset', id: info.data.id as number } }
- setValue(newValue)
+ const newImage: ImageValue = { type: 'asset', id: info.data.id as number }
+ if (hasValueData(index)) {
+ confirm({
+ title: t('hotspots.clear-data'),
+ content: t('hotspots.clear-data.dnd-message'),
+ okText: t('yes'),
+ cancelText: t('no'),
+ onOk: () => {
+ setImage(index, newImage, true)
+ },
+ onCancel: () => {
+ setImage(index, newImage, false)
+ }
+ })
+ } else {
+ setImage(index, newImage, true)
+ }
} }
onSort={ (info: DragAndDropInfo, dragId: UniqueIdentifier, dropId: UniqueIdentifier) => {
const newValue = [...value]
@@ -93,6 +176,21 @@ export const ImageGalleryImagePreview = ({ item, index, value, setValue, disable
setValue(newValue)
}
},
+ {
+ label: t('hotspots.edit'),
+ key: 'hotspots-edit',
+ icon: ,
+ onClick: async () => {
+ setMarkerModalOpen(true)
+ }
+ },
+ {
+ disabled: !hasValueData(index),
+ label: t('hotspots.clear-data'),
+ key: 'hotspots-edit',
+ icon: ,
+ onClick: clearValueData
+ },
{
label: t('element.open'),
key: 'open',
@@ -104,9 +202,18 @@ export const ImageGalleryImagePreview = ({ item, index, value, setValue, disable
}
})
}
+ },
+ {
+ label: t('empty'),
+ key: 'open',
+ icon: ,
+ onClick: async () => {
+ setValue(value.map((v, i) => i === index ? { image: null, hotspots: null, marker: null } : v))
+ }
}
] }
height={ 100 }
+ onHotspotsDataButtonClick={ hasHotspotData(index) ? () => { setMarkerModalOpen(true) } : undefined }
style={ { backgroundColor: '#fff' } }
width={ 200 }
/>
diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/sortable-item/sortable-item.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/sortable-item/sortable-item.tsx
index 0fbd18c48..3a223fa45 100644
--- a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/sortable-item/sortable-item.tsx
+++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/components/sortable-item/sortable-item.tsx
@@ -24,6 +24,13 @@ import {
type ImageGalleryValue,
type ImageGalleryValueItem
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/image-gallery'
+import type {
+ Hotspot,
+ Marker
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/types/hotspot-types'
+import {
+ type HotspotMarkersModalContainerRef
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/hotspot-markers-modal-container'
export interface ImageGallerySortableItemProps {
id: string
@@ -32,9 +39,10 @@ export interface ImageGallerySortableItemProps {
value: ImageGalleryValue
setValue: React.Dispatch>
disabled?: boolean
+ hotspotMarkersModalContainer: React.RefObject
}
-export const ImageGallerySortableItem = ({ id, index, item, value, setValue, disabled }: ImageGallerySortableItemProps): React.JSX.Element => {
+export const ImageGallerySortableItem = ({ id, index, item, value, setValue, disabled, hotspotMarkersModalContainer }: ImageGallerySortableItemProps): React.JSX.Element => {
const sortable = useSortable({
id,
transition: {
@@ -49,6 +57,12 @@ export const ImageGallerySortableItem = ({ id, index, item, value, setValue, dis
transition
}
+ const onHotspotsChange = (hotspots: Hotspot[], marker: Marker[]): void => {
+ const newValue = value.map((v, i) => i === index ? { ...v, hotspots, marker } : v)
+
+ setValue(newValue)
+ }
+
return (
diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/image-gallery.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/image-gallery.tsx
index a4d56fee8..87420d2d1 100644
--- a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/image-gallery.tsx
+++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/image-gallery/image-gallery.tsx
@@ -11,7 +11,7 @@
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import {
type ImageValue
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/image/image'
@@ -32,6 +32,13 @@ import {
SortableContext
} from '@dnd-kit/sortable'
import { uuid } from '@Pimcore/utils/uuid'
+import {
+ type Hotspot, type Marker
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/types/hotspot-types'
+import {
+ HotspotMarkersModalContainer,
+ type HotspotMarkersModalContainerRef
+} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/hotspot-image/hotspot-markers-modal-container'
export interface ImageGalleryProps {
value?: ImageGalleryValue | null
@@ -43,11 +50,14 @@ export type ImageGalleryValue = ImageGalleryValueItem[]
export interface ImageGalleryValueItem {
image: ImageValue | null
+ hotspots?: Hotspot[] | null
+ marker?: Marker[] | null
}
export const ImageGallery = (props: ImageGalleryProps): React.JSX.Element => {
const [value, setValue] = useState(props.value ?? [])
const { t } = useTranslation()
+ const hotspotMarkersModalContainerRef = useRef(null)
useEffect(() => {
if (props.onChange !== undefined) {
@@ -80,6 +90,7 @@ export const ImageGallery = (props: ImageGalleryProps): React.JSX.Element => {
{ value.map((item, index) => (
{
/>
) }
+
)
}
diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/grid.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/grid.tsx
index 5554ae4c6..7152a5b52 100644
--- a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/grid.tsx
+++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/grid.tsx
@@ -30,12 +30,16 @@ import { Box } from '@Pimcore/components/box/box'
import { useFormModal } from '@Pimcore/components/modal/form-modal/hooks/use-form-modal'
import cn from 'classnames'
import { useDownload } from '@Pimcore/modules/asset/actions/download/use-download'
+import { toCssDimension } from '@Pimcore/utils/css'
+import { Content } from '@Pimcore/components/content/content'
interface ManyToManyRelationGridProps {
value?: ManyToManyRelationValue | null
deleteItem: (id: number, type: string) => void
assetInlineDownloadAllowed: boolean
disabled?: boolean
+ width: number | string | null
+ height: number | string | null
}
export const ManyToManyRelationGrid = forwardRef(function ManyToManyRelationGrid (props: ManyToManyRelationGridProps, ref: MutableRefObject): React.JSX.Element {
@@ -170,12 +174,25 @@ export const ManyToManyRelationGrid = forwardRef(function ManyToManyRelationGrid
className={ cn(...getStateClasses()) }
ref={ ref }
>
-
+
+
+
+
+
)
})
diff --git a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/many-to-many-relation.tsx b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/many-to-many-relation.tsx
index 13e8160be..d879e45cc 100644
--- a/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/many-to-many-relation.tsx
+++ b/assets/js/src/core/modules/element/dynamic-types/defintinitions/objects/data-related/components/many-to-many-relation/many-to-many-relation.tsx
@@ -31,6 +31,8 @@ import {
dndIsValidData,
type IRelationAllowedTypesDataComponent
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/helpers/relations/allowed-types'
+import { toCssDimension } from '@Pimcore/utils/css'
+import { Content } from '@Pimcore/components/content/content'
export interface ManyToManyRelationClassDefinitionProps {
assetUploadPath?: string | null
@@ -69,19 +71,27 @@ export const ManyToManyRelation = (props: ManyToManyRelationProps): React.JSX.El
assetInlineDownloadAllowed={ props.assetInlineDownloadAllowed ?? false }
deleteItem={ deleteItem }
disabled={ props.disabled }
+ height={ props.height }
value={ displayedValue }
+ width={ props.width }
/>
-