From 741769a85f318b4878dda420683b83689e977146 Mon Sep 17 00:00:00 2001 From: Alexander Batenburg Date: Mon, 28 Sep 2026 09:25:03 +0200 Subject: [PATCH 1/3] [Studio] Offer advanced relation fields for the Many-to-Many Relation data target The class attributes were only keyed by transformation result type and always requested without loadAdvancedRelations, so advancedManyToMany(Object)Relation fields never showed up in the target field select, whichever data target was chosen. Attributes for the manyToManyRelation data target are now kept under a separate map key and requested with loadAdvancedRelations=true. Co-Authored-By: Claude Opus 5.5 --- .../hooks/use-auto-recalculate-type.ts | 6 +-- .../step-target/useClassAttributes.ts | 4 +- .../hooks/use-mapping-step-loader.tsx | 49 +++++++++---------- .../hooks/use-mapping-step-loader.types.ts | 2 +- .../mapping-item/mapping-item-with-filter.tsx | 1 + .../mapping-item/mapping-item.tsx | 5 +- .../utils/compute-autofill-suggestions.ts | 8 +-- .../js/src/modules/data-importer/types.ts | 48 ++++++++++++++++-- 8 files changed, 83 insertions(+), 40 deletions(-) diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/hooks/use-auto-recalculate-type.ts b/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/hooks/use-auto-recalculate-type.ts index 4bfb0238b..c2b3985b7 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/hooks/use-auto-recalculate-type.ts +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/hooks/use-auto-recalculate-type.ts @@ -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' @@ -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 === '' } diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/step-target/useClassAttributes.ts b/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/step-target/useClassAttributes.ts index ad8e31b75..531fd83cc 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/step-target/useClassAttributes.ts +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/advanced-mapping-modal/step-target/useClassAttributes.ts @@ -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 }))), diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx index bf07bb748..17acb6339 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx @@ -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' @@ -99,13 +99,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( @@ -255,29 +257,31 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us const backendConfig = (configData?.configuration ?? {}) as BackendConfiguration const items: MappingConfigItem[] = (backendConfig.mappingConfig) ?? [] - const uniqueTypes = new Set() + const uniqueMapKeys = new Set() if (effectiveClassId !== undefined && effectiveClassId !== '') { - uniqueTypes.add(undefined) + uniqueMapKeys.add(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) => uniqueTypes.add(trt)) - items.forEach((item) => { uniqueTypes.add(item.transformationResultType) }) + SUGGESTION_TRANSFORMATION_RESULT_TYPES.forEach((trt) => uniqueMapKeys.add(resolveAttrMapKey(trt))) + items.forEach((item) => { + uniqueMapKeys.add(resolveAttrMapKey(item.transformationResultType, item.dataTarget?.type)) + }) } - const typesArray = Array.from(uniqueTypes) + const mapKeysArray = Array.from(uniqueMapKeys) 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 @@ -287,13 +291,10 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us const attrResults = await Promise.all(attrPromises) - if (typesArray.length > 0) { + if (mapKeysArray.length > 0) { const newEntries: Record = {} 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, @@ -388,8 +389,7 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us const items = getMappingConfig() const missingTypes = new Set() items.forEach((item) => { - const trt = item.transformationResultType - const mapKey = resolveAttrMapKey(trt) + const mapKey = resolveAttrMapKey(item.transformationResultType, item.dataTarget?.type) if (attributesMap[mapKey] === undefined) { missingTypes.add(mapKey) } @@ -399,11 +399,10 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us 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 }) ) @@ -421,7 +420,7 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us return { ...prev, ...next } }) }) - }, [mappingTrtList, initialLoadDone, classId]) + }, [mappingAttrMapKeyList, initialLoadDone, classId]) return { columnHeaderOptions, @@ -431,7 +430,7 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us attributesMap, setAttributesMap, classId, - mappingTrtList, + mappingAttrMapKeyList, getMappingConfig } } diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.types.ts b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.types.ts index bd407303f..7d736bc8a 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.types.ts +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.types.ts @@ -25,7 +25,7 @@ export interface UseMappingStepLoaderResult { attributesMap: Record setAttributesMap: React.Dispatch>> classId: string | undefined - mappingTrtList: string[] | undefined + mappingAttrMapKeyList: string[] | undefined getMappingConfig: () => MappingConfigItem[] } diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item-with-filter.tsx b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item-with-filter.tsx index b0a6cb7db..45b042bb3 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item-with-filter.tsx +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item-with-filter.tsx @@ -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 } diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item.tsx b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item.tsx index 5fb50c1b2..89eec4e78 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item.tsx +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/mapping-item/mapping-item.tsx @@ -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 @@ -63,6 +64,7 @@ const MappingItemComponent = ({ itemLabel, dataSourceIndex, transformationResultType, + dataTargetType, selectedFieldName, language, attributesMap @@ -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. @@ -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 diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/utils/compute-autofill-suggestions.ts b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/utils/compute-autofill-suggestions.ts index 6bcfcb626..5b33a3ea7 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/utils/compute-autofill-suggestions.ts +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/utils/compute-autofill-suggestions.ts @@ -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 @@ -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): PoolEntry[] { const attrsByKey = new Map() 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] ?? [])) { diff --git a/assets/studio/js/src/modules/data-importer/types.ts b/assets/studio/js/src/modules/data-importer/types.ts index 85c078a9a..17cfdca09 100644 --- a/assets/studio/js/src/modules/data-importer/types.ts +++ b/assets/studio/js/src/modules/data-importer/types.ts @@ -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 = ['manyToManyRelation'] +const ADVANCED_RELATION_RESULT_TYPES = ['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.includes(dataTargetType ?? '') && + ADVANCED_RELATION_RESULT_TYPES.includes(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 { From 31c20bd9d8db81927db0ccd03acdb99aa6ab5669 Mon Sep 17 00:00:00 2001 From: Alexander Batenburg Date: Mon, 28 Sep 2026 10:01:59 +0200 Subject: [PATCH 2/3] [Studio] Use Sets for the advanced relation type lookups Co-Authored-By: Claude Opus 5.5 --- assets/studio/js/src/modules/data-importer/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/studio/js/src/modules/data-importer/types.ts b/assets/studio/js/src/modules/data-importer/types.ts index 17cfdca09..417ab6557 100644 --- a/assets/studio/js/src/modules/data-importer/types.ts +++ b/assets/studio/js/src/modules/data-importer/types.ts @@ -105,8 +105,8 @@ export const DEFAULT_ATTR_MAP_KEY = '__default__' // 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 = ['manyToManyRelation'] -const ADVANCED_RELATION_RESULT_TYPES = ['dataObjectArray', 'assetArray'] +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 { @@ -119,8 +119,8 @@ export function resolveAttrMapKey (transformationResultType: string | undefined, } if ( - ADVANCED_RELATION_DATA_TARGET_TYPES.includes(dataTargetType ?? '') && - ADVANCED_RELATION_RESULT_TYPES.includes(transformationResultType) + ADVANCED_RELATION_DATA_TARGET_TYPES.has(dataTargetType ?? '') && + ADVANCED_RELATION_RESULT_TYPES.has(transformationResultType) ) { return ADVANCED_RELATIONS_ATTR_MAP_KEY_PREFIX + transformationResultType } From e69c7688ad24066d3a0889597ffe6d2ba4bb5775 Mon Sep 17 00:00:00 2001 From: Alexander Batenburg Date: Mon, 28 Sep 2026 10:01:59 +0200 Subject: [PATCH 3/3] [Studio] Drop loaded class attributes when the class changes The class can be changed in the resolver step without saving. The mapping step loader only fetched the attribute map keys that were missing, so entries loaded for the previous class, e.g. its advanced relation fields, stayed in use. The loader now tracks which class the map belongs to and reloads every needed key when it changes, discarding results of a superseded class. Co-Authored-By: Claude Opus 5.5 --- .../hooks/use-mapping-step-loader.tsx | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx index 17acb6339..fd72d0969 100644 --- a/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx +++ b/assets/studio/js/src/modules/data-importer/components/tabs/steps/mapping-step/hooks/use-mapping-step-loader.tsx @@ -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([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() @@ -49,6 +62,9 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us const [sourceRows, setSourceRows] = useState([]) const [hasPreviewError, setHasPreviewError] = useState(false) const [attributesMap, setAttributesMap] = useState>({}) + // 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(undefined) const [headersRequest, setHeadersRequest] = useState<{ name: string bundleDataImporterCopyPreviewParameters: { @@ -257,19 +273,9 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us const backendConfig = (configData?.configuration ?? {}) as BackendConfiguration const items: MappingConfigItem[] = (backendConfig.mappingConfig) ?? [] - const uniqueMapKeys = new Set() - if (effectiveClassId !== undefined && effectiveClassId !== '') { - uniqueMapKeys.add(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) => uniqueMapKeys.add(resolveAttrMapKey(trt))) - items.forEach((item) => { - uniqueMapKeys.add(resolveAttrMapKey(item.transformationResultType, item.dataTarget?.type)) - }) - } - - const mapKeysArray = Array.from(uniqueMapKeys) + const mapKeysArray = (effectiveClassId !== undefined && effectiveClassId !== '') + ? collectAttrMapKeys(items) + : [] if (debugEnabled) { console.debug('[DI][Loader] class attributes load start', { cycleId, @@ -291,6 +297,9 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us const attrResults = await Promise.all(attrPromises) + const classChanged = attributesMapClassIdRef.current !== effectiveClassId + attributesMapClassIdRef.current = effectiveClassId + if (mapKeysArray.length > 0) { const newEntries: Record = {} attrResults.forEach((result, i) => { @@ -300,6 +309,7 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us // 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]) { @@ -387,17 +397,22 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us if (effectiveClassId === undefined || effectiveClassId === '' || !initialLoadDone) return const items = getMappingConfig() - const missingTypes = new Set() - items.forEach((item) => { - const mapKey = resolveAttrMapKey(item.transformationResultType, item.dataTarget?.type) - 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) => { return await dispatch( api.endpoints.bundleDataImporterDataTypeLoadClassAttributes.initiate({ @@ -409,6 +424,9 @@ export function useMappingStepLoader (configName: string, isActive: boolean): Us }) void Promise.all(promises).then((results) => { + // the class changed again while loading + if (attributesMapClassIdRef.current !== effectiveClassId) return + setAttributesMap((prev) => { const next: Record = {} results.forEach((result, i) => {