Skip to content

Commit c0b607d

Browse files
committed
feat(DataMapper): wrap collection choice mapping with for-each (#2815)
- Detect collection choice wrapper + collection target and wrap choose/when/otherwise inside a for-each - Add tests for collection choice scenarios
1 parent 30f1e35 commit c0b607d

2 files changed

Lines changed: 185 additions & 4 deletions

File tree

packages/ui/src/services/visualization/mapping-action.service.choice.test.ts

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import {
44
DocumentDefinitionType,
55
DocumentType,
66
} from '../../models/datamapper/document';
7-
import { ChooseItem, FieldItem, MappingTree, OtherwiseItem, ValueSelector } from '../../models/datamapper/mapping';
7+
import {
8+
ChooseItem,
9+
FieldItem,
10+
ForEachItem,
11+
MappingTree,
12+
OtherwiseItem,
13+
ValueSelector,
14+
} from '../../models/datamapper/mapping';
815
import {
916
ChoiceFieldNodeData,
1017
DocumentNodeData,
@@ -53,6 +60,38 @@ describe('MappingActionService / choice field mappings', () => {
5360
} as unknown as typeof baseField;
5461
}
5562

63+
function createMockCollectionChoiceField(members: { name: string }[], selectedMemberIndex?: number) {
64+
const baseField = sourceDoc.fields[0];
65+
const memberFields = members.map((m) => ({
66+
...baseField,
67+
name: m.name,
68+
displayName: m.name,
69+
fields: [],
70+
isChoice: false,
71+
maxOccurs: 1,
72+
}));
73+
return {
74+
...baseField,
75+
name: '__choice__',
76+
displayName: 'choice',
77+
isChoice: true,
78+
wrapperKind: 'choice' as const,
79+
selectedMemberIndex,
80+
fields: memberFields,
81+
maxOccurs: 'unbounded' as const,
82+
} as unknown as typeof baseField;
83+
}
84+
85+
function createMockCollectionField() {
86+
const baseField = targetDoc.fields[0];
87+
return {
88+
...baseField,
89+
name: 'collectionField',
90+
displayName: 'collectionField',
91+
maxOccurs: 'unbounded' as const,
92+
} as unknown as typeof baseField;
93+
}
94+
5695
describe('engageMapping with choice source', () => {
5796
let localTargetDocNode: TargetDocumentNodeData;
5897

@@ -443,4 +482,120 @@ describe('MappingActionService / choice field mappings', () => {
443482
expect(nestedDirect1Item.nodePath.pathSegments).not.toContain(freshInnerChoice.id);
444483
});
445484
});
485+
486+
describe('collection choice wrapper mappings (S2 scenario)', () => {
487+
let localTargetDocNode: TargetDocumentNodeData;
488+
489+
beforeEach(() => {
490+
localTargetDocNode = new TargetDocumentNodeData(targetDoc, tree);
491+
});
492+
493+
it('should create ForEachItem wrapping ChooseItem when mapping collection choice wrapper to collection field', () => {
494+
const collectionChoiceField = createMockCollectionChoiceField([{ name: 'email' }, { name: 'phone' }]);
495+
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, collectionChoiceField);
496+
const collectionTargetField = createMockCollectionField();
497+
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, collectionTargetField);
498+
499+
MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);
500+
501+
expect(tree.children.length).toEqual(1);
502+
const targetFieldItem = tree.children[0];
503+
expect(targetFieldItem).toBeInstanceOf(FieldItem);
504+
expect(targetFieldItem.children.length).toEqual(1);
505+
506+
const forEachItem = targetFieldItem.children[0];
507+
expect(forEachItem).toBeInstanceOf(ForEachItem);
508+
expect(forEachItem.children.length).toEqual(1);
509+
510+
const chooseItem = forEachItem.children[0] as ChooseItem;
511+
expect(chooseItem).toBeInstanceOf(ChooseItem);
512+
expect(chooseItem.when.length).toEqual(2);
513+
expect(chooseItem.otherwise).toBeInstanceOf(OtherwiseItem);
514+
});
515+
516+
it('ForEachItem expression should be the XPath of the collection choice wrapper', () => {
517+
const collectionChoiceField = createMockCollectionChoiceField([{ name: 'email' }, { name: 'phone' }]);
518+
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, collectionChoiceField);
519+
const collectionTargetField = createMockCollectionField();
520+
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, collectionTargetField);
521+
522+
MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);
523+
524+
const forEachItem = tree.children[0].children[0] as ForEachItem;
525+
// ForEachItem expression should be set (XPath to the collection choice wrapper)
526+
expect(forEachItem.expression).toBeTruthy();
527+
expect(forEachItem.expression.length).toBeGreaterThan(0);
528+
});
529+
530+
it('WhenItem expressions inside ForEachItem should reference the choice members', () => {
531+
const collectionChoiceField = createMockCollectionChoiceField([{ name: 'email' }, { name: 'phone' }]);
532+
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, collectionChoiceField);
533+
const collectionTargetField = createMockCollectionField();
534+
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, collectionTargetField);
535+
536+
MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);
537+
538+
const forEachItem = tree.children[0].children[0] as ForEachItem;
539+
const chooseItem = forEachItem.children[0] as ChooseItem;
540+
// WhenItem test expressions reference the choice members
541+
expect(chooseItem.when[0].expression).toContain('email');
542+
expect(chooseItem.when[1].expression).toContain('phone');
543+
// WhenItem value selectors should have paths to the members
544+
const emailSelector = chooseItem.when[0].children.find((c) => c instanceof ValueSelector) as ValueSelector;
545+
const phoneSelector = chooseItem.when[1].children.find((c) => c instanceof ValueSelector) as ValueSelector;
546+
expect(emailSelector.expression).toContain('email');
547+
expect(phoneSelector.expression).toContain('phone');
548+
});
549+
550+
it('should create only ChooseItem (no ForEachItem) when mapping collection choice wrapper to non-collection field (S1)', () => {
551+
const collectionChoiceField = createMockCollectionChoiceField([{ name: 'email' }, { name: 'phone' }]);
552+
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, collectionChoiceField);
553+
const nonCollectionTargetField = targetDoc.fields[0]; // maxOccurs = 1
554+
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, nonCollectionTargetField);
555+
556+
MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);
557+
558+
expect(tree.children.length).toEqual(1);
559+
const targetFieldItem = tree.children[0];
560+
expect(targetFieldItem).toBeInstanceOf(FieldItem);
561+
expect(targetFieldItem.children.length).toEqual(1);
562+
563+
const chooseItem = targetFieldItem.children[0];
564+
expect(chooseItem).toBeInstanceOf(ChooseItem);
565+
expect(chooseItem).not.toBeInstanceOf(ForEachItem);
566+
expect((chooseItem as ChooseItem).when.length).toEqual(2);
567+
});
568+
569+
it('should create only ChooseItem when mapping non-collection choice wrapper to collection field', () => {
570+
const nonCollectionChoiceField = createMockChoiceField([{ name: 'email' }, { name: 'phone' }]);
571+
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, nonCollectionChoiceField);
572+
const collectionTargetField = createMockCollectionField();
573+
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, collectionTargetField);
574+
575+
MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);
576+
577+
expect(tree.children.length).toEqual(1);
578+
const targetFieldItem = tree.children[0];
579+
expect(targetFieldItem).toBeInstanceOf(FieldItem);
580+
expect(targetFieldItem.children.length).toEqual(1);
581+
582+
const chooseItem = targetFieldItem.children[0];
583+
expect(chooseItem).toBeInstanceOf(ChooseItem);
584+
expect(chooseItem).not.toBeInstanceOf(ForEachItem);
585+
});
586+
587+
it('should not create duplicate ForEachItem when mapping the same collection choice source twice', () => {
588+
const collectionChoiceField = createMockCollectionChoiceField([{ name: 'email' }, { name: 'phone' }]);
589+
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, collectionChoiceField);
590+
const collectionTargetField = createMockCollectionField();
591+
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, collectionTargetField);
592+
593+
MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);
594+
MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);
595+
596+
const targetFieldItem = tree.children[0];
597+
const forEachItems = targetFieldItem.children.filter((c) => c instanceof ForEachItem);
598+
expect(forEachItems.length).toEqual(1);
599+
});
600+
});
446601
});

packages/ui/src/services/visualization/mapping-action.service.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ export class MappingActionService {
188188
if (sourceNode.choiceField) {
189189
const item = MappingActionService.getOrCreateFieldItem(targetNode);
190190
MappingService.mapToField(sourceNode.field, item);
191+
} else if (
192+
VisualizationUtilService.isCollectionField(sourceNode) &&
193+
VisualizationUtilService.isCollectionField(targetNode)
194+
) {
195+
MappingActionService.createForEachWithChooseFromChoice(sourceNode.field, targetNode);
191196
} else {
192197
MappingActionService.createChooseFromChoice(sourceNode.field, targetNode);
193198
}
@@ -218,16 +223,37 @@ export class MappingActionService {
218223
const targetItem = MappingActionService.getOrCreateFieldItem(targetNode);
219224
if (targetItem.children.some((c) => c instanceof ChooseItem)) return;
220225
targetItem.children = targetItem.children.filter((c) => !(c instanceof ValueSelector));
221-
const chooseItem = new ChooseItem(targetItem, targetItem instanceof FieldItem ? targetItem.field : undefined);
222226

227+
const chooseItem = MappingActionService.buildChooseFromChoiceMembers(targetItem, sourceField, targetItem);
228+
targetItem.children.push(chooseItem);
229+
}
230+
231+
private static createForEachWithChooseFromChoice(sourceField: IField, targetNode: TargetNodeData) {
232+
const targetItem = MappingActionService.getOrCreateFieldItem(targetNode);
233+
if (targetItem.children.some((c) => c instanceof ForEachItem)) return;
234+
targetItem.children = targetItem.children.filter((c) => !(c instanceof ValueSelector));
235+
236+
const forEachItem = new ForEachItem(targetItem);
237+
MappingService.mapToCondition(forEachItem, sourceField);
238+
239+
const chooseItem = MappingActionService.buildChooseFromChoiceMembers(forEachItem, sourceField, targetItem);
240+
forEachItem.children.push(chooseItem);
241+
targetItem.children.push(forEachItem);
242+
}
243+
244+
private static buildChooseFromChoiceMembers(
245+
parent: MappingItem,
246+
sourceField: IField,
247+
targetItem: MappingItem,
248+
): ChooseItem {
249+
const chooseItem = new ChooseItem(parent, targetItem instanceof FieldItem ? targetItem.field : undefined);
223250
for (const member of sourceField.fields ?? []) {
224251
const whenItem = MappingService.addWhen(chooseItem);
225252
MappingService.mapToCondition(whenItem, member);
226253
MappingService.mapToField(member, whenItem);
227254
}
228-
229255
MappingService.addOtherwise(chooseItem);
230-
targetItem.children.push(chooseItem);
256+
return chooseItem;
231257
}
232258

233259
private static getOrCreateFieldItem(nodeData: TargetNodeData): MappingItem {

0 commit comments

Comments
 (0)