Skip to content

Commit b63f556

Browse files
Merge branch '2026.1' into 2026.x
2 parents 0fe42a6 + 420bf57 commit b63f556

794 files changed

Lines changed: 2555 additions & 30473 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* This source file is available under the terms of the
3+
* Pimcore Open Core License (POCL)
4+
* Full copyright and license information is available in
5+
* LICENSE.md which is distributed with this source code.
6+
*
7+
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
8+
* @license Pimcore Open Core License (POCL)
9+
*/
10+
11+
import { container } from '@Pimcore/app/depency-injection'
12+
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
13+
import { type DynamicTypeIconSetRegistry } from '@Pimcore/components/icon-selector/dynamic-types/registry/dynamic-type-icon-set-registry'
14+
import { type ElementIcon } from '@Pimcore/modules/asset/asset-api-slice.gen'
15+
16+
const isElementIcon = (value: unknown): value is ElementIcon => {
17+
return (
18+
typeof value === 'object' &&
19+
value !== null &&
20+
'type' in value &&
21+
'value' in value
22+
)
23+
}
24+
25+
const isLibraryIcon = (name: string): boolean => {
26+
const registry = container.get<DynamicTypeIconSetRegistry>(serviceIds['DynamicTypes/IconSetRegistry'])
27+
28+
return registry.getDynamicTypes().some((iconSet) =>
29+
iconSet.getIcons().some((icon) => icon.value === name)
30+
)
31+
}
32+
33+
export const toElementIcon = (input: ElementIcon | string | null | undefined): ElementIcon | undefined => {
34+
if (input === null || input === undefined || input === '') {
35+
return undefined
36+
}
37+
38+
if (isElementIcon(input)) {
39+
return input
40+
}
41+
42+
return {
43+
type: isLibraryIcon(input) ? 'name' : 'path',
44+
value: input
45+
}
46+
}
47+
48+
export const toIconString = (input: ElementIcon | string | null | undefined): string => {
49+
if (input === null || input === undefined) {
50+
return ''
51+
}
52+
53+
if (typeof input === 'string') {
54+
return input
55+
}
56+
57+
return input.value
58+
}

assets/js/src/core/components/panel/panel.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010

1111
import React, { type ReactNode } from 'react'
1212
import { isNil } from 'lodash'
13-
import { Space } from '@Pimcore/components/space/space'
13+
import { Flex } from '@Pimcore/components/flex/flex'
1414
import { Box, type BoxProps } from '@Pimcore/components/box/box'
1515
import { BaseView } from '@Pimcore/components/base-view/base-view'
1616
import { TooltipIcon } from '@Pimcore/components/tooltip-icon/tooltip-icon'
17+
import { Icon, type ElementIcon } from '@Pimcore/components/icon/icon'
1718
import { type CollapseProps } from 'antd'
1819
import { ItemSpacer } from '@Pimcore/components/form/layouts/item-spacer/item-spacer'
1920

2021
export interface PanelProps {
2122
title?: ReactNode
2223
tooltip?: ReactNode
24+
icon?: ElementIcon | null
2325
border?: boolean
2426
collapsible?: boolean
2527
collapsed?: boolean
@@ -43,6 +45,7 @@ export const Panel = ({
4345
collapsible,
4446
title,
4547
tooltip,
48+
icon,
4649
theme = 'card-with-highlight',
4750
extra,
4851
extraPosition,
@@ -55,15 +58,28 @@ export const Panel = ({
5558
return undefined
5659
}
5760

58-
if (isNil(tooltip)) {
61+
const iconNode = !isNil(icon)
62+
? (
63+
<Icon
64+
type={ icon.type }
65+
value={ icon.value }
66+
/>
67+
)
68+
: null
69+
70+
if (isNil(tooltip) && iconNode === null) {
5971
return title
6072
}
6173

6274
return (
63-
<Space size='extra-small'>
75+
<Flex
76+
align='center'
77+
gap='extra-small'
78+
>
79+
{iconNode}
6480
{title}
65-
<TooltipIcon tooltip={ tooltip } />
66-
</Space>
81+
{!isNil(tooltip) && <TooltipIcon tooltip={ tooltip } />}
82+
</Flex>
6783
)
6884
}
6985

assets/js/src/core/components/tabpanel/tabpanel.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { isNonEmptyString } from '@sdk/utils'
1717

1818
export interface TabpanelItem {
1919
key?: string
20-
label?: string
20+
label?: React.ReactNode
2121
title?: string
2222
closable?: boolean
2323
children?: React.ReactNode
@@ -64,7 +64,12 @@ export const Tabpanel = ({
6464
const { t } = useTranslation()
6565
const tabItems: ITabsProps['items'] = items.map((item, index) => {
6666
const labelOrTitle = item.label ?? item.title
67-
const label = isNonEmptyString(labelOrTitle) ? t(labelOrTitle) : `Tab ${index + 1}`
67+
let label: React.ReactNode
68+
if (typeof labelOrTitle === 'string') {
69+
label = isNonEmptyString(labelOrTitle) ? t(labelOrTitle) : `Tab ${index + 1}`
70+
} else {
71+
label = labelOrTitle ?? `Tab ${index + 1}`
72+
}
6873

6974
return {
7075
...item,

assets/js/src/core/components/users-roles-dropdown/users-roles-dropdown.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const UsersRolesDropdown = ({ userList, initialSharedUsers, roleList, ini
8787
label: renderLabel({ labelName: item?.username, iconName: 'user' }),
8888
searchValue: item?.username
8989
}))
90+
?.sort((a, b) => (a.searchValue ?? '').localeCompare(b.searchValue ?? ''))
9091

9192
return renderSelect({
9293
options,
@@ -101,7 +102,7 @@ export const UsersRolesDropdown = ({ userList, initialSharedUsers, roleList, ini
101102
value: item.id,
102103
label: renderLabel({ labelName: item?.name, iconName: 'shield' }),
103104
searchValue: item?.name
104-
}))
105+
}))?.sort((a, b) => (a.searchValue ?? '').localeCompare(b.searchValue ?? ''))
105106

106107
return renderSelect({
107108
options,

assets/js/src/core/modules/class-definition/class-definition-slice-enhanced.ts

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,31 @@
88
* @license Pimcore Open Core License (POCL)
99
*/
1010

11+
import { type OverrideResultType } from '@reduxjs/toolkit/query'
1112
import { invalidatingTags, providingTags, tagNames } from '@Pimcore/app/api/pimcore/tags'
12-
import { api as baseApi } from './class-definition-slice.gen'
13+
import { type ElementIcon } from '@Pimcore/components/icon/icon'
14+
import { denormalizeLayoutTreeIcons, normalizeLayoutTreeIcons } from '@Pimcore/utils/normalize-icon'
15+
import { api as baseApi, type ConfigLayoutDefinition, type CustomLayouts, type Layout } from './class-definition-slice.gen'
16+
17+
// Backend returns the root layout's icon as `string` for field-collection and
18+
// object-brick layouts. The codegen for ConfigLayoutDefinition reflects that
19+
// (`icon: string | null`), so we both convert it at runtime and override the
20+
// result type. For the other layout endpoints (classDefinitionGetLayoutById,
21+
// classCustomLayoutGet, classObjectBrickCustomLayoutGet) the codegen claims
22+
// `ElementIcon | null` already, but the same string-runtime mismatch can
23+
// occur, so we normalize defensively without any type override.
24+
export type NormalizedConfigLayoutDefinition =
25+
Omit<ConfigLayoutDefinition, 'icon'> & { icon: ElementIcon | null }
26+
27+
const normalizeLayout = (raw: Layout): Layout => normalizeLayoutTreeIcons(raw)
28+
29+
const normalizeCustomLayouts = (raw: CustomLayouts): CustomLayouts => ({
30+
...raw,
31+
layoutDefinition: raw.layoutDefinition !== null ? normalizeLayout(raw.layoutDefinition) : null
32+
})
1333

1434
/* eslint-disable max-lines */
15-
const api = baseApi.enhanceEndpoints({
35+
const apiWithTags = baseApi.enhanceEndpoints({
1636
addTagTypes: [tagNames.DATA_OBJECT, tagNames.DATA_OBJECT_DETAIL, tagNames.CLASS_DEFINITION, tagNames.CLASS_DEFINITION_DETAIL, tagNames.CLASS_DEFINITION_COLLECTION, tagNames.CUSTOM_LAYOUT, tagNames.CUSTOM_LAYOUT_DETAIL, tagNames.CUSTOM_LAYOUT_COLLECTION, tagNames.FIELD_COLLECTION, tagNames.FIELD_COLLECTION_DETAIL, tagNames.FIELD_COLLECTION_COLLECTION, tagNames.OBJECT_BRICK, tagNames.OBJECT_BRICK_DETAIL, tagNames.OBJECT_BRICK_COLLECTION, tagNames.OBJECT_BRICK_CUSTOM_LAYOUT, tagNames.OBJECT_BRICK_CUSTOM_LAYOUT_DETAIL, tagNames.OBJECT_BRICK_CUSTOM_LAYOUT_COLLECTION, tagNames.SELECT_OPTION_DETAIL, tagNames.SELECT_OPTION_COLLECTION],
1737
endpoints: {
1838
classDefinitionCollection: {
@@ -22,7 +42,8 @@ const api = baseApi.enhanceEndpoints({
2242
providesTags: (result, error, args) => providingTags.CLASS_DEFINITION_DETAIL(args.id)
2343
},
2444
classDefinitionGetLayoutById: {
25-
providesTags: (result, error, args) => providingTags.CLASS_DEFINITION_DETAIL(args.id)
45+
providesTags: (result, error, args) => providingTags.CLASS_DEFINITION_DETAIL(args.id),
46+
transformResponse: normalizeLayout
2647
},
2748
classCustomLayoutCollection: {
2849
providesTags: () => providingTags.CUSTOM_LAYOUT_COLLECTION()
@@ -33,7 +54,7 @@ const api = baseApi.enhanceEndpoints({
3354
try {
3455
const { data } = await queryFulfilled
3556
dispatch(
36-
api.util.updateQueryData('classDefinitionGetById', { id: args.id }, (draft) => {
57+
apiWithTags.util.updateQueryData('classDefinitionGetById', { id: args.id }, (draft) => {
3758
Object.assign(draft, data)
3859
})
3960
)
@@ -61,15 +82,17 @@ const api = baseApi.enhanceEndpoints({
6182
]
6283
},
6384
classCustomLayoutGet: {
64-
providesTags: (result, error, args) => providingTags.CUSTOM_LAYOUT_DETAIL(args.customLayoutId)
85+
providesTags: (result, error, args) => providingTags.CUSTOM_LAYOUT_DETAIL(args.customLayoutId),
86+
transformResponse: normalizeCustomLayouts
6587
},
6688
classCustomLayoutUpdate: {
6789
invalidatesTags: () => [],
90+
transformResponse: normalizeCustomLayouts,
6891
async onQueryStarted (args, { dispatch, queryFulfilled }) {
6992
try {
7093
const { data } = await queryFulfilled
7194
dispatch(
72-
api.util.updateQueryData('classCustomLayoutGet', { customLayoutId: args.customLayoutId }, (draft) => {
95+
apiWithTags.util.updateQueryData('classCustomLayoutGet', { customLayoutId: args.customLayoutId }, (draft) => {
7396
Object.assign(draft, data)
7497
})
7598
)
@@ -120,7 +143,7 @@ const api = baseApi.enhanceEndpoints({
120143
try {
121144
const { data } = await queryFulfilled
122145
dispatch(
123-
api.util.updateQueryData('classFieldCollectionGetByKey', { key: args.key }, (draft) => {
146+
apiWithTags.util.updateQueryData('classFieldCollectionGetByKey', { key: args.key }, (draft) => {
124147
Object.assign(draft, data)
125148
})
126149
)
@@ -171,7 +194,7 @@ const api = baseApi.enhanceEndpoints({
171194
try {
172195
const { data } = await queryFulfilled
173196
dispatch(
174-
api.util.updateQueryData('classObjectBrickGetByKey', { key: args.key }, (draft) => {
197+
apiWithTags.util.updateQueryData('classObjectBrickGetByKey', { key: args.key }, (draft) => {
175198
Object.assign(draft, data)
176199
})
177200
)
@@ -196,15 +219,17 @@ const api = baseApi.enhanceEndpoints({
196219
]
197220
},
198221
classObjectBrickCustomLayoutGet: {
199-
providesTags: (result, error, args) => providingTags.OBJECT_BRICK_CUSTOM_LAYOUT_DETAIL(args.key, args.customLayoutId)
222+
providesTags: (result, error, args) => providingTags.OBJECT_BRICK_CUSTOM_LAYOUT_DETAIL(args.key, args.customLayoutId),
223+
transformResponse: normalizeCustomLayouts
200224
},
201225
classObjectBrickCustomLayoutUpdate: {
202226
invalidatesTags: () => [],
227+
transformResponse: normalizeCustomLayouts,
203228
async onQueryStarted (args, { dispatch, queryFulfilled }) {
204229
try {
205230
const { data } = await queryFulfilled
206231
dispatch(
207-
api.util.updateQueryData('classObjectBrickCustomLayoutGet', { key: args.key, customLayoutId: args.customLayoutId }, (draft) => {
232+
apiWithTags.util.updateQueryData('classObjectBrickCustomLayoutGet', { key: args.key, customLayoutId: args.customLayoutId }, (draft) => {
208233
Object.assign(draft, data)
209234
})
210235
)
@@ -237,7 +262,7 @@ const api = baseApi.enhanceEndpoints({
237262
try {
238263
const { data } = await queryFulfilled
239264
dispatch(
240-
api.util.updateQueryData('classSelectOptionGet', { id: args.id }, (draft) => {
265+
apiWithTags.util.updateQueryData('classSelectOptionGet', { id: args.id }, (draft) => {
241266
Object.assign(draft, data)
242267
})
243268
)
@@ -278,6 +303,69 @@ const api = baseApi.enhanceEndpoints({
278303
}
279304
})
280305

306+
// Retype the two layout-by-key queries so callers see the normalized icon,
307+
// and wire transformResponse to do the runtime conversion. Both happen in the
308+
// same enhanceEndpoints call so transformResponse's return type matches the
309+
// overridden result type.
310+
const apiWithReadTransforms = apiWithTags.enhanceEndpoints<never, {
311+
classFieldCollectionGetLayoutByKey: OverrideResultType<
312+
typeof apiWithTags.endpoints.classFieldCollectionGetLayoutByKey.Types.QueryDefinition,
313+
NormalizedConfigLayoutDefinition
314+
>
315+
classObjectBrickGetLayoutByKey: OverrideResultType<
316+
typeof apiWithTags.endpoints.classObjectBrickGetLayoutByKey.Types.QueryDefinition,
317+
NormalizedConfigLayoutDefinition
318+
>
319+
}>({
320+
endpoints: {
321+
classFieldCollectionGetLayoutByKey: {
322+
transformResponse: (raw: ConfigLayoutDefinition): NormalizedConfigLayoutDefinition =>
323+
normalizeLayoutTreeIcons(raw) as unknown as NormalizedConfigLayoutDefinition
324+
},
325+
classObjectBrickGetLayoutByKey: {
326+
transformResponse: (raw: ConfigLayoutDefinition): NormalizedConfigLayoutDefinition =>
327+
normalizeLayoutTreeIcons(raw) as unknown as NormalizedConfigLayoutDefinition
328+
}
329+
}
330+
})
331+
332+
const denormalizeConfigurationOnQuery = (endpoint: { query?: (arg: any) => any }): void => {
333+
const originalQuery = endpoint.query
334+
if (originalQuery === undefined) {
335+
return
336+
}
337+
338+
endpoint.query = (queryArg: any) => {
339+
const baseResult = originalQuery(queryArg)
340+
if (baseResult === null || typeof baseResult !== 'object' || !('body' in baseResult)) {
341+
return baseResult
342+
}
343+
344+
const body = baseResult.body as { configuration?: unknown }
345+
if (body.configuration === undefined) {
346+
return baseResult
347+
}
348+
349+
return {
350+
...baseResult,
351+
body: {
352+
...body,
353+
configuration: denormalizeLayoutTreeIcons(body.configuration)
354+
}
355+
}
356+
}
357+
}
358+
359+
const api = apiWithReadTransforms.enhanceEndpoints({
360+
endpoints: {
361+
classDefinitionUpdate: denormalizeConfigurationOnQuery,
362+
classCustomLayoutUpdate: denormalizeConfigurationOnQuery,
363+
classFieldCollectionUpdate: denormalizeConfigurationOnQuery,
364+
classObjectBrickUpdate: denormalizeConfigurationOnQuery,
365+
classObjectBrickCustomLayoutUpdate: denormalizeConfigurationOnQuery
366+
}
367+
})
368+
281369
export type * from './class-definition-slice.gen'
282370

283371
export const {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* This source file is available under the terms of the
3+
* Pimcore Open Core License (POCL)
4+
* Full copyright and license information is available in
5+
* LICENSE.md which is distributed with this source code.
6+
*
7+
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
8+
* @license Pimcore Open Core License (POCL)
9+
*/
10+
11+
import { useMemo } from 'react'
12+
import { useClassDefinitionGetByIdQuery } from '@Pimcore/modules/class-definition/class-definition-slice-enhanced'
13+
import { toElementIcon } from '@Pimcore/components/icon-selector/utils/element-icon'
14+
import { type AnyQueryHook } from 'types/react-query'
15+
16+
type QueryArg = Parameters<typeof useClassDefinitionGetByIdQuery>[0]
17+
18+
export const useClassDefinitionGetById = ((arg: QueryArg) => {
19+
const result = useClassDefinitionGetByIdQuery(arg)
20+
21+
const data = useMemo(() => {
22+
if (result.data === undefined) {
23+
return result.data
24+
}
25+
26+
return {
27+
...result.data,
28+
icon: toElementIcon(result.data.icon as Parameters<typeof toElementIcon>[0])
29+
}
30+
}, [result.data])
31+
32+
return { ...result, data }
33+
}) as AnyQueryHook

0 commit comments

Comments
 (0)