Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
14 changes: 14 additions & 0 deletions assets/js/src/core/app/config/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ import { DynamicTypeWidgetTypeRegistry } from '@Pimcore/modules/widget-editor/dy
import { WidgetRegistry } from '@Pimcore/modules/widget-manager/services/widget-registry'
import { DynamicTypeFieldFilterClassificationStore } from '@Pimcore/modules/element/dynamic-types/definitions/field-filters/types/classification-store/dynamic-type-field-filter-classification-store'
import { DynamicTypeBatchEditClassificationStore } from '@Pimcore/modules/element/dynamic-types/definitions/batch-edits/types/classification-store/dynamic-type-batch-edit-classification-store'
import { DocumentUrlProcessorRegistry } from '@Pimcore/modules/document/services/processors/document-url-processor-registry'
import { DocumentSaveDataProcessorRegistry } from '@Pimcore/modules/document/services/processors/document-save-data-processor-registry'
import { DataObjectSaveDataProcessorRegistry } from '@Pimcore/modules/data-object/services/processors/data-object-save-data-processor-registry'
import { AssetSaveDataProcessorRegistry } from '@Pimcore/modules/asset/services/processors/asset-save-data-processor-registry'

// Component registry
container.bind(serviceIds['App/ComponentRegistry/ComponentRegistry']).to(ComponentRegistry).inSingletonScope()
Expand Down Expand Up @@ -283,12 +287,18 @@ container.bind(serviceIds['Asset/Editor/AudioTabManager']).to(AudioTabManager).i
container.bind(serviceIds['Asset/Editor/ArchiveTabManager']).to(ArchiveTabManager).inSingletonScope()
container.bind(serviceIds['Asset/Editor/UnknownTabManager']).to(UnknownTabManager).inSingletonScope()

// Asset Processor Registries
container.bind(serviceIds['Asset/ProcessorRegistry/SaveDataProcessor']).to(AssetSaveDataProcessorRegistry).inSingletonScope()

// Data Objects
container.bind(serviceIds['DataObject/Editor/TypeRegistry']).to(TypeRegistry).inSingletonScope()
container.bind(serviceIds['DataObject/Editor/ObjectTabManager']).to(ObjectTabManager).inSingletonScope()
container.bind(serviceIds['DataObject/Editor/VariantTabManager']).to(VariantTabManager).inSingletonScope()
container.bind(serviceIds['DataObject/Editor/FolderTabManager']).to(FolderTabManager).inSingletonScope()

// Data Object Processor Registries
container.bind(serviceIds['DataObject/ProcessorRegistry/SaveDataProcessor']).to(DataObjectSaveDataProcessorRegistry).inSingletonScope()

// Documents
container.bind(serviceIds['Document/Editor/TypeRegistry']).to(TypeRegistry).inSingletonScope()
container.bind(serviceIds['Document/Editor/PageTabManager']).to(PageTabManager).inSingletonScope()
Expand All @@ -301,6 +311,10 @@ container.bind(serviceIds['Document/Editor/SnippetTabManager']).to(SnippetTabMan
// Document Services
container.bind(serviceIds['Document/RequiredFieldsValidationService']).to(DocumentRequiredFieldsValidationServiceImpl).inSingletonScope()

// Document Processor Registries
container.bind(serviceIds['Document/ProcessorRegistry/UrlProcessor']).to(DocumentUrlProcessorRegistry).inSingletonScope()
container.bind(serviceIds['Document/ProcessorRegistry/SaveDataProcessor']).to(DocumentSaveDataProcessorRegistry).inSingletonScope()

// Document Sidebar Managers
container.bind(serviceIds['Document/Editor/Sidebar/PageSidebarManager']).to(DocumentSidebarManager).inSingletonScope()
container.bind(serviceIds['Document/Editor/Sidebar/SnippetSidebarManager']).to(DocumentSidebarManager).inSingletonScope()
Expand Down
8 changes: 7 additions & 1 deletion assets/js/src/core/app/config/services/service-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,5 +334,11 @@ export const serviceIds = {
'App/ContextMenuRegistry/ContextMenuRegistry': 'App/ContextMenuRegistry/ContextMenuRegistry',

// Document required fields validation service
'Document/RequiredFieldsValidationService': 'Document/RequiredFieldsValidationService'
'Document/RequiredFieldsValidationService': 'Document/RequiredFieldsValidationService',

// Processor registries
'Document/ProcessorRegistry/UrlProcessor': 'Document/ProcessorRegistry/UrlProcessor',
'Document/ProcessorRegistry/SaveDataProcessor': 'Document/ProcessorRegistry/SaveDataProcessor',
'DataObject/ProcessorRegistry/SaveDataProcessor': 'DataObject/ProcessorRegistry/SaveDataProcessor',
'Asset/ProcessorRegistry/SaveDataProcessor': 'Asset/ProcessorRegistry/SaveDataProcessor'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

/**
* Abstract Processor Registry
*
* Base class for managing collections of processors that transform or modify context objects.
* Processors are executed in priority order (highest to lowest) and can be registered/unregistered dynamically.
*
* Each processor receives a context object and can modify it through its execute method.
* This pattern allows for extensible, plugin-based functionality where external code can
* register processors to extend core behavior.
*
* @template TContext - The type of context object that processors will receive
*/
export interface Processor<TContext> {
readonly id: string
readonly priority: number
execute: (context: TContext) => void
}

export abstract class AbstractProcessorRegistry<TContext> {
protected processors: Array<Processor<TContext>> = []

registerProcessor (processor: Processor<TContext>): void {
this.processors = this.processors.filter(p => p.id !== processor.id)

this.processors.push(processor)
this.processors.sort((a, b) => b.priority - a.priority)
}

unregisterProcessor (id: string): void {
this.processors = this.processors.filter(p => p.id !== id)
}

executeProcessors (context: TContext): void {
for (const processor of this.processors) {
try {
processor.execute(context)
} catch (error) {
console.warn(`Processor ${processor.id} failed:`, error)
}
}
}

getRegisteredProcessors (): ReadonlyArray<Processor<TContext>> {
return [...this.processors]
}

hasProcessor (id: string): boolean {
return this.processors.some(p => p.id === id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

/**
* Abstract base class for contexts that need to manipulate data objects
* Provides generic field manipulation methods that work with any object type
*/
export abstract class AbstractDataContext<TData extends Record<string, any>> {
constructor (protected data: TData) {}

/**
* Add or update a field in the data
*/
setField<K extends keyof TData>(
key: K,
value: TData[K]
): void {
this.data[key] = value
}

/**
* Get a field from the data
*/
getField<K extends keyof TData>(
key: K
): TData[K] {
return this.data[key]
}

/**
* Check if a field exists in the data
*/
hasField<K extends keyof TData>(key: K): boolean {
return key in this.data
}

/**
* Get the entire data object
*/
getData (): TData {
return this.data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

/**
* Abstract Processor Registry
*
* Base class for managing collections of processors that transform or modify context objects.
* Processors are executed in priority order (highest to lowest) and can be registered/unregistered dynamically.
*
* Each processor receives a context object and can modify it through its execute method.
* This pattern allows for extensible, plugin-based functionality where external code can
* register processors to extend core behavior.
*
* @template TContext - The type of context object that processors will receive
*/
export interface Processor<TContext> {
readonly id: string
readonly priority: number
execute: (context: TContext) => void
}

export abstract class AbstractProcessorRegistry<TContext> {
protected processors: Array<Processor<TContext>> = []

registerProcessor (processor: Processor<TContext>): void {
this.processors = this.processors.filter(p => p.id !== processor.id)

this.processors.push(processor)
this.processors.sort((a, b) => b.priority - a.priority)
}

unregisterProcessor (id: string): void {
this.processors = this.processors.filter(p => p.id !== id)
}

executeProcessors (context: TContext): void {
for (const processor of this.processors) {
try {
processor.execute(context)
} catch (error) {
console.warn(`Processor ${processor.id} failed:`, error)
}
}
}

getRegisteredProcessors (): ReadonlyArray<Processor<TContext>> {
return [...this.processors]
}

hasProcessor (id: string): boolean {
return this.processors.some(p => p.id === id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@Pimcore/components/button/button'
import { useAssetDraft } from '../../../hooks/use-asset-draft'
import { type AssetUpdateByIdApiArg, useAssetUpdateByIdMutation } from '../../../asset-api-slice-enhanced'
import { useAssetUpdateByIdMutation } from '../../../asset-api-slice-enhanced'
import { useMessage } from '@Pimcore/components/message/useMessage'
import {
type DataProperty as DataPropertyApi
Expand All @@ -29,6 +29,13 @@ import { useElementContext } from '@Pimcore/modules/element/hooks/use-element-co
import { checkElementPermission } from '@Pimcore/modules/element/permissions/permission-helper'
import { isNil } from 'lodash'
import trackError, { ApiError } from '@Pimcore/modules/app/error-handler'
import { container } from '@Pimcore/app/depency-injection'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import {
type AssetSaveDataProcessorRegistry,
AssetSaveDataContext,
type AssetSaveUpdateData
} from '@Pimcore/modules/asset/services/processors/asset-save-data-processor-registry'

export const EditorToolbarSaveButton = (): React.JSX.Element => {
const { t } = useTranslation()
Expand Down Expand Up @@ -85,7 +92,7 @@ export const EditorToolbarSaveButton = (): React.JSX.Element => {
function onSaveClick (): void {
if (asset?.changes === undefined) return

const update: AssetUpdateByIdApiArg['body']['data'] = {}
const update: AssetSaveUpdateData = {}

if (asset.changes.properties) {
const propertyUpdate = properties?.map((property: DataProperty): DataPropertyApi => {
Expand Down Expand Up @@ -138,6 +145,18 @@ export const EditorToolbarSaveButton = (): React.JSX.Element => {
update.data = textData
}

// Apply save data processors
try {
const saveDataProcessorRegistry = container.get<AssetSaveDataProcessorRegistry>(
serviceIds['Asset/ProcessorRegistry/SaveDataProcessor']
)

const context = new AssetSaveDataContext(id, update)
saveDataProcessorRegistry.executeProcessors(context)
} catch (error) {
console.warn(`Save data processors failed for asset ${id}:`, error)
}

const saveAssetPromise = saveAsset({
id,
body: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

import { injectable } from 'inversify'
import { AbstractProcessorRegistry, type Processor } from '@Pimcore/modules/app/processor-registry/abstract-processor-registry'
import { AbstractDataContext } from '@Pimcore/modules/app/processor-registry/abstract-data-context'
import type { AssetUpdateByIdApiArg } from '../../asset-api-slice.gen'

export type AssetSaveUpdateData = AssetUpdateByIdApiArg['body']['data']

/**
* Context object passed to asset save data processors
*/
export class AssetSaveDataContext extends AbstractDataContext<AssetSaveUpdateData> {
constructor (
public readonly assetId: number,
public updateData: AssetSaveUpdateData
) {
super(updateData)
}
}

/**
* Processor for modifying asset save data before it's sent to the API.
* Allows adding, transforming, or enriching data based on custom logic.
*/
export interface AssetSaveDataProcessor extends Processor<AssetSaveDataContext> {}

@injectable()
export class AssetSaveDataProcessorRegistry extends AbstractProcessorRegistry<AssetSaveDataContext> {}
22 changes: 20 additions & 2 deletions assets/js/src/core/modules/data-object/actions/save/use-save.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import { useContext, useEffect } from 'react'
import { DataObjectContext } from '@Pimcore/modules/data-object/data-object-provider'
import { useDataObjectDraft } from '@Pimcore/modules/data-object/hooks/use-data-object-draft'
import type { DataObjectUpdateByIdApiArg } from '@Pimcore/modules/data-object/data-object-api-slice.gen'
import type { DataProperty } from '@Pimcore/modules/element/draft/hooks/use-properties'
import type {
DataProperty as DataPropertyApi
Expand All @@ -25,6 +24,13 @@ import { type FetchBaseQueryError } from '@reduxjs/toolkit/query'
import { type SerializedError } from '@reduxjs/toolkit'
import { useAppDispatch } from '@sdk/app'
import { setNodePublished } from '@Pimcore/components/element-tree/element-tree-slice'
import { container } from '@Pimcore/app/depency-injection'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import {
type DataObjectSaveDataProcessorRegistry,
DataObjectSaveDataContext,
type DataObjectSaveUpdateData
} from '@Pimcore/modules/data-object/services/processors/data-object-save-data-processor-registry'

export enum SaveTaskType {
Version = 'version',
Expand Down Expand Up @@ -84,7 +90,7 @@ export const useSave = (useDraftData: boolean = true): UseSaveHookReturn => {

setRunningTask(task)

const updatedData: DataObjectUpdateByIdApiArg['body']['data'] = {}
const updatedData: DataObjectSaveUpdateData = {}
if (dataObject.changes.properties) {
const propertyUpdate = properties?.map((property: DataProperty): DataPropertyApi => {
const { rowId, ...propertyApi } = property
Expand Down Expand Up @@ -112,6 +118,18 @@ export const useSave = (useDraftData: boolean = true): UseSaveHookReturn => {

updatedData.useDraftData = useDraftData

// Apply save data processors
try {
const saveDataProcessorRegistry = container.get<DataObjectSaveDataProcessorRegistry>(
serviceIds['DataObject/ProcessorRegistry/SaveDataProcessor']
)

const context = new DataObjectSaveDataContext(id, task, updatedData)
saveDataProcessorRegistry.executeProcessors(context)
} catch (error) {
console.warn(`Save data processors failed for data object ${id}:`, error)
}

await saveDataObject({
id,
body: {
Expand Down
Loading