Skip to content
Draft
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import { useEffect, useMemo, useRef } from 'react'
import { type MappingConfigItem, type ClassAttribute, resolveAttrMapKey, DEFAULT_ATTR_MAP_KEY } from '../../../../../types'
import { type MappingConfigItem, type ClassAttribute, resolveAttrMapKey, parseAttrMapKey, DEFAULT_ATTR_MAP_KEY } from '../../../../../types'
import { useBundleDataImporterDataTypeLoadClassAttributesQuery } from '../../../../../data-importer-api-slice.gen'
import { parseClassAttribute } from '../../mapping-step/hooks/use-mapping-step-loader.types'

Expand Down Expand Up @@ -75,12 +75,12 @@ export function useAutoRecalculateType ({
})
}, [pipelineKey, dataSourceKey, open, configName, localItemRef, setCalculateTypeRequest])

const currentAttrMapKey = resolveAttrMapKey(localItem.transformationResultType)
const currentAttrMapKey = resolveAttrMapKey(localItem.transformationResultType, localItem.dataTarget?.type)
const needsAttrFetch = currentAttrMapKey !== DEFAULT_ATTR_MAP_KEY && attributesMap[currentAttrMapKey] === undefined
const { data: extraAttrData, isFetching: isFetchingExtraAttributes } = useBundleDataImporterDataTypeLoadClassAttributesQuery(
{
classId: classId ?? '',
transformationResultType: localItem.transformationResultType,
...parseAttrMapKey(currentAttrMapKey),
systemWrite: true
},
{ skip: !needsAttrFetch || classId === undefined || classId === '' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export function useClassAttributes ({
}: StepTargetProps): { classFieldOptions: Array<{ value: string, label: string }>, isLocalized: boolean } {
const { isFetchingAttributes, calculateTypeError } = useResultPreviewContext()
const attributes = useMemo(
() => attributesMap[resolveAttrMapKey(transformationResultType)] ?? [],
[attributesMap, transformationResultType]
() => attributesMap[resolveAttrMapKey(transformationResultType, dataTarget?.type)] ?? [],
[attributesMap, transformationResultType, dataTarget?.type]
)
const classFieldOptions = useMemo(
() => ((calculateTypeError ?? '') !== '' ? [] : attributes.map((a) => ({ value: a.key, label: a.title }))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { api, useBundleDataImporterConfigGetQuery } from '../../../../../data-im
import { useBundleDataImporterConfigLoadColumnHeadersQuery, useBundleDataImporterConfigLoadPreviewQuery } from '../../../../../data-importer-api-slice.gen'
import { transformFormToBackend, type BackendConfiguration } from '../../../../../utils/transformers'
import { normalizeDataRow } from '../../../../../utils/normalize-data-row'
import { type DataImporterFormValues, type MappingConfigItem, type ClassAttribute, resolveAttrMapKey, DEFAULT_ATTR_MAP_KEY } from '../../../../../types'
import { type DataImporterFormValues, type MappingConfigItem, type ClassAttribute, resolveAttrMapKey, parseAttrMapKey, DEFAULT_ATTR_MAP_KEY } from '../../../../../types'
import { type SourceRow } from '../sources-panel/sources-panel'
import { parseClassAttribute, type ColumnHeaderEntry, type UseMappingStepLoaderResult } from './use-mapping-step-loader.types'

Expand All @@ -39,6 +39,19 @@ export const SUGGESTION_TRANSFORMATION_RESULT_TYPES: string[] = [
'dataObjectArray'
]

// every attributes map key the mapping step needs for the given mapping items
function collectAttrMapKeys (items: MappingConfigItem[]): string[] {
const mapKeys = new Set<string>([DEFAULT_ATTR_MAP_KEY])
// the default request only returns DEFAULT+NUMERIC typed attributes — without
// the other result types the autofill suggestions would miss e.g. select,
// relation or asset fields on configs that have no mapping rows yet (#622)
SUGGESTION_TRANSFORMATION_RESULT_TYPES.forEach((trt) => mapKeys.add(resolveAttrMapKey(trt)))
items.forEach((item) => {
mapKeys.add(resolveAttrMapKey(item.transformationResultType, item.dataTarget?.type))
})
return Array.from(mapKeys)
}

export function useMappingStepLoader (configName: string, isActive: boolean): UseMappingStepLoaderResult {
const form = Form.useFormInstance()
const dispatch = useAppDispatch()
Expand All @@ -49,6 +62,9 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us
const [sourceRows, setSourceRows] = useState<SourceRow[]>([])
const [hasPreviewError, setHasPreviewError] = useState(false)
const [attributesMap, setAttributesMap] = useState<Record<string, ClassAttribute[]>>({})
// The class the attributesMap entries belong to. The class can be changed in the resolver
// step without saving, so entries loaded for another class must never be reused.
const attributesMapClassIdRef = useRef<string | undefined>(undefined)
const [headersRequest, setHeadersRequest] = useState<{
name: string
bundleDataImporterCopyPreviewParameters: {
Expand Down Expand Up @@ -99,13 +115,15 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us
// Serialize to a string so useWatch only triggers a re-render when the TRT
// list actually changes, not on every unrelated form field update (returning a
// new array reference on every call would always be referentially unequal).
const mappingTrtListJson = Form.useWatch(
const mappingAttrMapKeyListJson = Form.useWatch(
(values: { mappingConfig?: MappingConfigItem[] }) =>
JSON.stringify((values.mappingConfig ?? []).map((item) => item.transformationResultType ?? ''))
JSON.stringify((values.mappingConfig ?? []).map((item) =>
resolveAttrMapKey(item.transformationResultType, item.dataTarget?.type)
))
) as string | undefined
const mappingTrtList = useMemo(
() => (mappingTrtListJson !== undefined ? JSON.parse(mappingTrtListJson) as string[] : undefined),
[mappingTrtListJson]
const mappingAttrMapKeyList = useMemo(
() => (mappingAttrMapKeyListJson !== undefined ? JSON.parse(mappingAttrMapKeyListJson) as string[] : undefined),
[mappingAttrMapKeyListJson]
)

const getMappingConfig = useCallback(
Expand Down Expand Up @@ -255,29 +273,21 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us
const backendConfig = (configData?.configuration ?? {}) as BackendConfiguration
const items: MappingConfigItem[] = (backendConfig.mappingConfig) ?? []

const uniqueTypes = new Set<string | undefined>()
if (effectiveClassId !== undefined && effectiveClassId !== '') {
uniqueTypes.add(undefined)
// the default request only returns DEFAULT+NUMERIC typed attributes — without
// the other result types the autofill suggestions would miss e.g. select,
// relation or asset fields on configs that have no mapping rows yet (#622)
SUGGESTION_TRANSFORMATION_RESULT_TYPES.forEach((trt) => uniqueTypes.add(trt))
items.forEach((item) => { uniqueTypes.add(item.transformationResultType) })
}

const typesArray = Array.from(uniqueTypes)
const mapKeysArray = (effectiveClassId !== undefined && effectiveClassId !== '')
? collectAttrMapKeys(items)
: []
if (debugEnabled) {
console.debug('[DI][Loader] class attributes load start', {
cycleId,
effectiveClassId,
types: typesArray.map((t) => t ?? DEFAULT_ATTR_MAP_KEY)
types: mapKeysArray
})
}
const attrPromises = typesArray.map(async (trt) =>
const attrPromises = mapKeysArray.map(async (mapKey) =>
await dispatch(
api.endpoints.bundleDataImporterDataTypeLoadClassAttributes.initiate({
classId: effectiveClassId!,
transformationResultType: trt,
...parseAttrMapKey(mapKey),
systemWrite: true
}, {
forceRefetch: requestChanged
Expand All @@ -287,18 +297,19 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us

const attrResults = await Promise.all(attrPromises)

if (typesArray.length > 0) {
const classChanged = attributesMapClassIdRef.current !== effectiveClassId
attributesMapClassIdRef.current = effectiveClassId

if (mapKeysArray.length > 0) {
const newEntries: Record<string, ClassAttribute[]> = {}
attrResults.forEach((result, i) => {
const trt = typesArray[i]
const mapKey = (trt === undefined || trt === '' || trt === 'default') ? DEFAULT_ATTR_MAP_KEY : trt
const attrs = (result.data?.attributes ?? []).map(parseClassAttribute)
newEntries[mapKey] = attrs
newEntries[mapKeysArray[i]] = (result.data?.attributes ?? []).map(parseClassAttribute)
})
// Merge new entries — only update the map object when content actually
// changed so consumers with referential equality checks (React.memo,
// useMemo) don't re-render when their own TRT's attrs are unchanged.
setAttributesMap((prev) => {
if (classChanged) return newEntries
let changed = false
for (const key of Object.keys(newEntries)) {
if (prev[key] !== newEntries[key]) {
Expand Down Expand Up @@ -386,30 +397,36 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us
if (effectiveClassId === undefined || effectiveClassId === '' || !initialLoadDone) return

const items = getMappingConfig()
const missingTypes = new Set<string>()
items.forEach((item) => {
const trt = item.transformationResultType
const mapKey = resolveAttrMapKey(trt)
if (attributesMap[mapKey] === undefined) {
missingTypes.add(mapKey)
}
})
let missingArray: string[]
if (attributesMapClassIdRef.current !== effectiveClassId) {
// the class was changed (unsaved) in the resolver step: drop the other class's entries
attributesMapClassIdRef.current = effectiveClassId
setAttributesMap({})
missingArray = collectAttrMapKeys(items)
} else {
missingArray = Array.from(new Set(
items
.map((item) => resolveAttrMapKey(item.transformationResultType, item.dataTarget?.type))
.filter((mapKey) => attributesMap[mapKey] === undefined)
))
}

if (missingTypes.size === 0) return
if (missingArray.length === 0) return

const missingArray = Array.from(missingTypes)
const promises = missingArray.map(async (mapKey) => {
const trt = mapKey === DEFAULT_ATTR_MAP_KEY ? undefined : mapKey
return await dispatch(
api.endpoints.bundleDataImporterDataTypeLoadClassAttributes.initiate({
classId: effectiveClassId,
transformationResultType: trt,
...parseAttrMapKey(mapKey),
systemWrite: true
})
)
})

void Promise.all(promises).then((results) => {
// the class changed again while loading
if (attributesMapClassIdRef.current !== effectiveClassId) return

setAttributesMap((prev) => {
const next: Record<string, ClassAttribute[]> = {}
results.forEach((result, i) => {
Expand All @@ -421,7 +438,7 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us
return { ...prev, ...next }
})
})
}, [mappingTrtList, initialLoadDone, classId])
}, [mappingAttrMapKeyList, initialLoadDone, classId])

return {
columnHeaderOptions,
Expand All @@ -431,7 +448,7 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us
attributesMap,
setAttributesMap,
classId,
mappingTrtList,
mappingAttrMapKeyList,
getMappingConfig
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface UseMappingStepLoaderResult {
attributesMap: Record<string, ClassAttribute[]>
setAttributesMap: React.Dispatch<React.SetStateAction<Record<string, ClassAttribute[]>>>
classId: string | undefined
mappingTrtList: string[] | undefined
mappingAttrMapKeyList: string[] | undefined
getMappingConfig: () => MappingConfigItem[]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const MappingItemWithFilter = React.memo(({
columnHeaderOptions={ columnHeaderOptions }
configName={ configName }
dataSourceIndex={ dataSourceIndex }
dataTargetType={ item.dataTarget?.type }
expanded={ expanded }
fieldIndex={ fieldIndex }
itemLabel={ item.label }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface MappingItemProps {
itemLabel: string | undefined
dataSourceIndex: string[] | undefined
transformationResultType: string | undefined
dataTargetType: string | undefined
selectedFieldName: string | undefined
language: string | undefined
attributesMap: Record<string, ClassAttribute[]>
Expand All @@ -63,6 +64,7 @@ const MappingItemComponent = ({
itemLabel,
dataSourceIndex,
transformationResultType,
dataTargetType,
selectedFieldName,
language,
attributesMap
Expand Down Expand Up @@ -123,7 +125,7 @@ const MappingItemComponent = ({
}
}, [expanded, columnHeaderOptions, form, getCurrentIndexByMappingId])

const attrMapKey = resolveAttrMapKey(transformationResultType)
const attrMapKey = resolveAttrMapKey(transformationResultType, dataTargetType)
const attributes: ClassAttribute[] = attributesMap[attrMapKey] ?? []

// Only compute props needed by MappingItemContent when the panel is expanded.
Expand Down Expand Up @@ -275,6 +277,7 @@ function areMappingItemPropsEqual (prev: MappingItemProps, next: MappingItemProp
prev.itemLabel === next.itemLabel &&
dataSourceIndexEqual &&
prev.transformationResultType === next.transformationResultType &&
prev.dataTargetType === next.dataTargetType &&
prev.selectedFieldName === next.selectedFieldName &&
prev.language === next.language &&
prev.attributesMap === next.attributesMap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { uuid } from '@pimcore/studio-ui-bundle/utils'
import type { ClassAttribute, MappingConfigItem } from '../../../../../types'
import type { SourceRow } from '../sources-panel/sources-panel'
import { DEFAULT_ATTR_MAP_KEY } from '../../../../../types'
import { DEFAULT_ATTR_MAP_KEY, isAdvancedRelationsAttrMapKey } from '../../../../../types'

export interface MappingSuggestion {
id: string
Expand Down Expand Up @@ -124,12 +124,14 @@ interface PoolEntry {
}

// the default map key wins when an attribute is offered by several result types,
// so simple fields keep the default transformation result type
// so simple fields keep the default transformation result type. Advanced relation
// keys are skipped: their key is not a transformation result type and autofill only
// creates direct data targets.
function flattenAttributes (attributesMap: Record<string, ClassAttribute[]>): PoolEntry[] {
const attrsByKey = new Map<string, PoolEntry>()
const keyOrder = [
DEFAULT_ATTR_MAP_KEY,
...Object.keys(attributesMap).filter((k) => k !== DEFAULT_ATTR_MAP_KEY)
...Object.keys(attributesMap).filter((k) => k !== DEFAULT_ATTR_MAP_KEY && !isAdvancedRelationsAttrMapKey(k))
]
for (const mapKey of keyOrder) {
for (const attr of (attributesMap[mapKey] ?? [])) {
Expand Down
48 changes: 43 additions & 5 deletions assets/studio/js/src/modules/data-importer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,52 @@ export interface ClassAttribute {

export const DEFAULT_ATTR_MAP_KEY = '__default__'

export function resolveAttrMapKey (transformationResultType: string | undefined): string {
return (
// Data targets that write into advanced relation fields. For these the backend only offers
// advancedManyToMany(Object)Relation fields when the attributes are requested with
// loadAdvancedRelations, so their attributes are kept under a separate map key.
const ADVANCED_RELATION_DATA_TARGET_TYPES = new Set(['manyToManyRelation'])
const ADVANCED_RELATION_RESULT_TYPES = new Set(['dataObjectArray', 'assetArray'])
const ADVANCED_RELATIONS_ATTR_MAP_KEY_PREFIX = 'advancedRelations:'

export function resolveAttrMapKey (transformationResultType: string | undefined, dataTargetType?: string): string {
if (
transformationResultType === undefined ||
transformationResultType === '' ||
transformationResultType === 'default'
)
? DEFAULT_ATTR_MAP_KEY
: transformationResultType
) {
return DEFAULT_ATTR_MAP_KEY
}

if (
ADVANCED_RELATION_DATA_TARGET_TYPES.has(dataTargetType ?? '') &&
ADVANCED_RELATION_RESULT_TYPES.has(transformationResultType)
) {
return ADVANCED_RELATIONS_ATTR_MAP_KEY_PREFIX + transformationResultType
}

return transformationResultType
}

export function isAdvancedRelationsAttrMapKey (mapKey: string): boolean {
return mapKey.startsWith(ADVANCED_RELATIONS_ATTR_MAP_KEY_PREFIX)
}

/**
* Turns an attributes map key back into the parameters of the class-attributes request.
*/
export function parseAttrMapKey (mapKey: string): { transformationResultType?: string, loadAdvancedRelations?: boolean } {
if (mapKey === DEFAULT_ATTR_MAP_KEY) {
return {}
}

if (isAdvancedRelationsAttrMapKey(mapKey)) {
return {
transformationResultType: mapKey.slice(ADVANCED_RELATIONS_ATTR_MAP_KEY_PREFIX.length),
loadAdvancedRelations: true
}
}

return { transformationResultType: mapKey }
}

export interface MappingConfigItem {
Expand Down
Loading