Skip to content

Commit 42aa95d

Browse files
committed
Merge branch '2026.1' into 2026.x
2 parents 3a8dc79 + ad6d25a commit 42aa95d

7 files changed

Lines changed: 200 additions & 5 deletions

File tree

assets/js/src/core/modules/app/component-registry/component-config.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ const defaultComponentConfig = {
199199
workflow: { type: ComponentType.SINGLE, name: 'element.editor.tab.workflow' },
200200
notesAndEvents: { type: ComponentType.SINGLE, name: 'element.editor.tab.notesAndEvents' },
201201
tags: { type: ComponentType.SINGLE, name: 'element.editor.tab.tags' }
202+
},
203+
workflow: {
204+
modal: {
205+
component: { type: ComponentType.SINGLE, name: 'element.editor.workflow.modal' },
206+
slots: {
207+
top: { type: ComponentType.SLOT, name: 'element.editor.workflow.modal.slots.top' },
208+
center: { type: ComponentType.SLOT, name: 'element.editor.workflow.modal.slots.center' },
209+
bottom: { type: ComponentType.SLOT, name: 'element.editor.workflow.modal.slots.bottom' }
210+
}
211+
}
202212
}
203213
}
204214
},

assets/js/src/core/modules/element/editor/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { PropertiesContainer } from './shared-tab-manager/tabs/properties/proper
1818
import { ScheduleTabContainer } from './shared-tab-manager/tabs/schedule/schedule-container'
1919
import { DependenciesTabContainer } from './shared-tab-manager/tabs/dependencies/dependencies-container'
2020
import { WorkflowTabContainer } from './shared-tab-manager/tabs/workflow/workflow-container'
21+
import { WorkflowModal } from './shared-components/workflow/modal/workflow-modal'
2122
import { NotesAndEventsTabContainer } from './shared-tab-manager/tabs/notes-and-events/notes-and-events-container'
2223
import { TagsTabContainer } from './shared-tab-manager/tabs/tags/tags-container'
2324

@@ -51,6 +52,11 @@ moduleSystem.registerModule({
5152
component: WorkflowTabContainer
5253
})
5354

55+
componentRegistry.register({
56+
name: componentConfig.element.editor.workflow.modal.component.name,
57+
component: WorkflowModal
58+
})
59+
5460
componentRegistry.register({
5561
name: componentConfig.element.editor.tab.notesAndEvents.name,
5662
component: NotesAndEventsTabContainer

assets/js/src/core/modules/element/editor/shared-components/workflow/modal/workflow-modal.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { isNull } from 'lodash'
2323
import { isNonEmptyString } from '@Pimcore/utils/type-utils'
2424
import { useWorkflowFieldRenderer } from '@Pimcore/modules/element/editor/shared-components/workflow/hooks/use-workflow-field-renderer'
2525
import { useDateConverter } from '@Pimcore/modules/app/hook/use-date-converter'
26+
import { SlotRenderer } from '@Pimcore/modules/app/component-registry/slot-renderer'
27+
import { componentConfig } from '@Pimcore/modules/app/component-registry/component-config'
2628

2729
export const WorkflowModal = (): React.JSX.Element => {
2830
const { isModalOpen, closeModal, triggeredWorkflowAction } = useWorkflow()
@@ -43,19 +45,20 @@ export const WorkflowModal = (): React.JSX.Element => {
4345
const onFinish = (values: FormValues): void => {
4446
if (triggeredWorkflowAction === null) return
4547

46-
const additionalValues: Record<string, any> = {}
48+
const { notes, ...additionalValues } = values
49+
4750
dynamicFields.forEach(field => {
48-
additionalValues[field.name] = values[field.name] ?? null
51+
additionalValues[field.name] = additionalValues[field.name] ?? null
4952
if (field.fieldType === 'date' || field.fieldType === 'datetime') {
50-
const fieldValue = values[field.name]
53+
const fieldValue = additionalValues[field.name]
5154
if (isNonEmptyString(fieldValue)) {
5255
additionalValues[field.name] = convertToTimestamp(fieldValue, true, false)
5356
}
5457
}
5558
})
5659

5760
submitWorkflowAction(triggeredWorkflowAction, {
58-
notes: values.notes,
61+
notes,
5962
additional: additionalValues
6063
})
6164
}
@@ -94,6 +97,8 @@ export const WorkflowModal = (): React.JSX.Element => {
9497
size={ 'M' }
9598
title={ <ModalTitle>{!isNull(triggeredWorkflowAction) ? t(triggeredWorkflowAction.label) : ''}</ModalTitle> }
9699
>
100+
<SlotRenderer slot={ componentConfig.element.editor.workflow.modal.slots.top.name } />
101+
97102
<Form
98103
form={ form }
99104
initialValues={ {
@@ -113,6 +118,8 @@ export const WorkflowModal = (): React.JSX.Element => {
113118
</Form.Item>
114119
))}
115120

121+
<SlotRenderer slot={ componentConfig.element.editor.workflow.modal.slots.center.name } />
122+
116123
{triggeredWorkflowAction?.notes?.commentEnabled === true && (
117124
<Form.Item
118125
label={ t('workflow-modal.notes') }
@@ -123,6 +130,8 @@ export const WorkflowModal = (): React.JSX.Element => {
123130
</Form.Item>
124131
)}
125132
</Form>
133+
134+
<SlotRenderer slot={ componentConfig.element.editor.workflow.modal.slots.bottom.name } />
126135
</Modal>
127136
)
128137
}

assets/js/src/core/modules/element/editor/shared-tab-manager/tabs/workflow/workflow-container.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ import { Header } from '@Pimcore/components/header/header'
1515
import { Content } from '@Pimcore/components/content/content'
1616
import { Space } from 'antd'
1717
import { WorkFlowProvider } from '@Pimcore/modules/element/editor/shared-components/workflow/provider/workflow-provider'
18-
import { WorkflowModal } from '@Pimcore/modules/element/editor/shared-components/workflow/modal/workflow-modal'
1918
import { useWorkflow } from '@Pimcore/modules/element/editor/shared-components/workflow/hooks/use-workflow'
19+
import { componentConfig } from '@Pimcore/modules/app/component-registry/component-config'
20+
import { useComponentRegistry } from '@Pimcore/modules/app/component-registry/use-component-registry'
2021

2122
export const WorkflowTabContainer = (): React.JSX.Element => {
2223
const { t } = useTranslation()
2324
const { workflowDetailsData, isFetchingWorkflowDetails } = useWorkflow()
25+
const componentRegistry = useComponentRegistry()
26+
const WorkflowModal = componentRegistry.get(componentConfig.element.editor.workflow.modal.component.name)
2427

2528
return (
2629
<Content

assets/js/src/sdk/modules/element/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ export * from '@Pimcore/modules/element/editor/shared-tab-manager/hooks/use-tab-
256256
export * from '@Pimcore/modules/element/editor/tab-manager/tab-manager'
257257
export * from '@Pimcore/modules/element/editor/tab-manager/interface/IElementEditorTabManager'
258258

259+
export * from '@Pimcore/modules/element/editor/shared-components/workflow/hooks/use-workflow'
260+
259261
export * from '@Pimcore/modules/element/element-helper'
260262

261263
export * from '@Pimcore/modules/element/element-selector/components/triggers/button/element-selector-button'
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
title: How to Extend the Workflow Transition Modal
3+
---
4+
5+
# How to Extend the Workflow Transition Modal
6+
7+
## Overview
8+
9+
This example adds a custom multi-select reviewers field to the
10+
workflow transition modal and persists the selected reviewers as
11+
part of the workflow note via a Symfony Workflow event subscriber.
12+
13+
Before reaching for a UI extension, check whether your use case is
14+
already covered by the
15+
[`additionalFields` option in the workflow configuration](https://docs.pimcore.com/platform/Pimcore/Workflow_Management/Workflow_Tutorial/).
16+
Static select boxes, single-user pickers, checkboxes, date pickers
17+
and similar inputs can be declared directly in the workflow YAML
18+
config — no plugin code required. Use the slot mechanism described
19+
below when you need behavior the YAML config cannot express:
20+
filtered option lists, options that depend on the current element,
21+
asynchronously fetched data, or multi-select widgets.
22+
23+
## Extension points
24+
25+
The workflow transition modal exposes three slot positions and one
26+
overridable modal component:
27+
28+
| Name | Type | Purpose |
29+
|---|---|---|
30+
| `element.editor.workflow.modal.slots.top` | Slot | Content above the form (banners, summaries). Rendered outside the form. |
31+
| `element.editor.workflow.modal.slots.center` | Slot | Form fields rendered between the YAML-declared additional fields and the notes textarea. Use this for custom `<Form.Item>` inputs. |
32+
| `element.editor.workflow.modal.slots.bottom` | Slot | Content below the form (footnotes, links). Rendered outside the form. |
33+
| `element.editor.workflow.modal` | Single | The complete modal component. Override only if you need full control over modal chrome and submission. |
34+
35+
Slot components rendered into `…slots.center` sit inside the same
36+
antd `<Form>` as the rest of the modal. Any `<Form.Item name="…">`
37+
inside the slot component automatically participates in the form
38+
context, and its value flows into the workflow submission payload
39+
under `additional.<name>` without any additional wiring.
40+
41+
## Files Overview
42+
43+
| Layer | File | Purpose |
44+
|---|---|---|
45+
| Backend | [workflow.yaml] | Example workflow definition |
46+
| Backend | [WorkflowReviewersSubscriber.php] | Reads `additional.reviewers` from the transition context and appends it to the workflow note |
47+
| Backend | [services.yaml] | Service registration |
48+
| UI | [reviewers-field.tsx] | React `<Form.Item>` rendered into the modal's center slot |
49+
| UI | [workflow-modal-extension-module.tsx] | Registers the slot component |
50+
| UI | [index.ts] | Plugin entry point |
51+
| UI | [plugins.ts] | Plugin export |
52+
53+
[workflow.yaml]: https://github.com/pimcore/studio-example-bundle/blob/main/config/pimcore/workflow.yaml
54+
[WorkflowReviewersSubscriber.php]: https://github.com/pimcore/studio-example-bundle/blob/main/src/EventSubscriber/WorkflowReviewersSubscriber.php
55+
[services.yaml]: https://github.com/pimcore/studio-example-bundle/blob/main/config/services.yaml
56+
[reviewers-field.tsx]: https://github.com/pimcore/studio-example-bundle/blob/main/assets/js/src/examples/workflow-modal-extension/components/reviewers-field.tsx
57+
[workflow-modal-extension-module.tsx]: https://github.com/pimcore/studio-example-bundle/blob/main/assets/js/src/examples/workflow-modal-extension/modules/workflow-modal-extension-module.tsx
58+
[index.ts]: https://github.com/pimcore/studio-example-bundle/blob/main/assets/js/src/examples/workflow-modal-extension/index.ts
59+
[plugins.ts]: https://github.com/pimcore/studio-example-bundle/blob/main/assets/js/src/plugins.ts
60+
61+
## Details
62+
63+
### Registering a slot component
64+
65+
A plugin registers a React component into one of the workflow modal
66+
slots via the `ComponentRegistry`:
67+
68+
```typescript
69+
componentRegistry.registerToSlot(
70+
componentConfig.element.editor.workflow.modal.slots.center.name,
71+
{
72+
name: 'reviewers',
73+
priority: 100,
74+
component: ReviewersField
75+
}
76+
)
77+
```
78+
79+
### Filtering by workflow and element context
80+
81+
A slot component renders for every workflow modal open by default.
82+
Use the existing context hooks to filter for the workflow and
83+
element you care about, and return `null` otherwise:
84+
85+
```tsx
86+
import { useWorkflow, useElementContext } from '@pimcore/studio-ui-bundle/modules/element'
87+
88+
export const ReviewersField = (): React.JSX.Element | null => {
89+
const { triggeredWorkflowAction } = useWorkflow()
90+
const { elementType } = useElementContext()
91+
92+
if (triggeredWorkflowAction?.workflowId !== 'simple_approval') return null
93+
if (triggeredWorkflowAction?.transitionId !== 'request_review') return null
94+
if (elementType !== 'data-object') return null
95+
96+
return (
97+
<Form.Item
98+
label="Reviewers"
99+
name="reviewers"
100+
rules={ [{ required: true, message: 'Please select at least one reviewer.' }] }
101+
>
102+
<Select
103+
mode="multiple"
104+
options={ reviewerOptions }
105+
/>
106+
</Form.Item>
107+
)
108+
}
109+
```
110+
111+
Filter *before* fetching data so plugins do not burn API requests
112+
on transitions they do not handle.
113+
114+
### How submitted values reach the backend
115+
116+
The modal collects every form value and posts them as part of the
117+
workflow action submission:
118+
119+
```json
120+
{
121+
"workflowOptions": {
122+
"notes": "Please review the latest changes.",
123+
"additional": {
124+
"reviewers": [1, 7, 12]
125+
}
126+
}
127+
}
128+
```
129+
130+
The `workflowOptions` array is passed verbatim to
131+
`Pimcore\Workflow\Manager::applyWithAdditionalData()`, which in turn
132+
calls `Workflow::apply($subject, $transition, $additionalData)`.
133+
That third argument becomes the Symfony Workflow transition context
134+
and is available on every workflow event:
135+
136+
```php
137+
public function onTransition(TransitionEvent $event): void
138+
{
139+
$context = $event->getContext();
140+
$reviewers = $context['additional']['reviewers'] ?? null;
141+
// …persist, notify, log, etc.
142+
}
143+
```
144+
145+
Note the nesting: slot-injected values live at
146+
`$context['additional']['<name>']`, not `$context['<name>']`.
147+
148+
### Full modal override
149+
150+
When the slot mechanism is not enough — for example, if you need to
151+
replace the modal chrome, change the submission flow, or render a
152+
wizard — override the modal component itself:
153+
154+
```typescript
155+
componentRegistry.override({
156+
name: componentConfig.element.editor.workflow.modal.component.name,
157+
component: CustomWorkflowModal
158+
})
159+
```
160+
161+
The custom modal can reuse the existing
162+
`useWorkflow`, `useSubmitWorkflow`, `useWorkflowFieldRenderer` and
163+
`useDateConverter` hooks so the standard fields and submission
164+
logic do not need to be re-implemented.

doc/04_Extending/02_Plugin_Development_Examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Each example covers a specific feature or integration pattern:
2525
- [Add a Custom Object Layout Type](./16_Custom_Object_Layout_Type.md)
2626
- [Customize Tree Icons and Tooltips](./17_Custom_Tree_Icons_and_Tooltips.md)
2727
- [Add a Custom Grid Column](./18_Custom_Grid_Column.md)
28+
- [Extend the Workflow Transition Modal](./19_Extend_Workflow_Transition_Modal.md)
2829

2930
All examples are part of the
3031
[Pimcore Studio Example Bundle](https://github.com/pimcore/studio-example-bundle/) on GitHub.

0 commit comments

Comments
 (0)