Skip to content

Commit 2bdd882

Browse files
committed
chore(DataMapper): Add ForEachGroupItem and GroupingStrategy
Fixes: #2861
1 parent e309e18 commit 2bdd882

2 files changed

Lines changed: 122 additions & 8 deletions

File tree

packages/ui/src/models/datamapper/mapping.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { DocumentDefinitionType, DocumentType } from './document';
22
import {
33
ChooseItem,
4+
ForEachGroupItem,
45
ForEachItem,
6+
GroupingStrategy,
57
IfItem,
68
isExpressionHolder,
79
MappingTree,
@@ -25,6 +27,7 @@ describe('mapping.ts', () => {
2527
expect(isExpressionHolder(new IfItem(tree))).toBe(true);
2628
expect(isExpressionHolder(new WhenItem(tree))).toBe(true);
2729
expect(isExpressionHolder(new ForEachItem(tree))).toBe(true);
30+
expect(isExpressionHolder(new ForEachGroupItem(tree))).toBe(true);
2831
expect(isExpressionHolder(new ValueSelector(tree))).toBe(true);
2932
expect(isExpressionHolder(new VariableItem(tree, 'myVar'))).toBe(true);
3033
});
@@ -151,6 +154,63 @@ describe('mapping.ts', () => {
151154
});
152155
});
153156

157+
describe('ForEachGroupItem', () => {
158+
it('should default to GROUP_BY strategy with empty expressions', () => {
159+
const item = new ForEachGroupItem(tree);
160+
expect(item.groupingStrategy).toBe(GroupingStrategy.GROUP_BY);
161+
expect(item.groupingExpression).toBe('');
162+
expect(item.expression).toBe('');
163+
});
164+
165+
it('doClone() should copy sortItems', () => {
166+
const item = new ForEachGroupItem(tree);
167+
const sort = new SortItem();
168+
sort.expression = '@price';
169+
sort.order = 'descending';
170+
item.sortItems = [sort];
171+
172+
const cloned = item.clone();
173+
174+
expect(cloned.sortItems).toHaveLength(1);
175+
expect(cloned.sortItems[0].expression).toBe('@price');
176+
expect(cloned.sortItems[0].order).toBe('descending');
177+
expect(cloned.sortItems[0]).not.toBe(sort);
178+
});
179+
180+
it('clone() should copy expression, groupingStrategy, groupingExpression, and children', () => {
181+
const item = new ForEachGroupItem(tree);
182+
item.expression = '/Order/Items/Item';
183+
item.groupingStrategy = GroupingStrategy.GROUP_ADJACENT;
184+
item.groupingExpression = 'Category';
185+
const child = new ValueSelector(item);
186+
child.expression = 'ItemId';
187+
item.children = [child];
188+
189+
const cloned = item.clone();
190+
191+
expect(cloned.expression).toBe('/Order/Items/Item');
192+
expect(cloned.groupingStrategy).toBe(GroupingStrategy.GROUP_ADJACENT);
193+
expect(cloned.groupingExpression).toBe('Category');
194+
expect(cloned.children).toHaveLength(1);
195+
expect((cloned.children[0] as ValueSelector).expression).toBe('ItemId');
196+
expect(cloned).not.toBe(item);
197+
});
198+
199+
it('contextPath getter should not mutate the original PathExpression', () => {
200+
const item = new ForEachGroupItem(tree);
201+
item.expression = '/Order/Items/Item';
202+
203+
const firstCall = item.contextPath;
204+
const secondCall = item.contextPath;
205+
206+
expect(firstCall).not.toBe(secondCall);
207+
208+
if (firstCall && secondCall) {
209+
expect(firstCall.contextPath).toEqual(secondCall.contextPath);
210+
}
211+
});
212+
});
213+
154214
describe('ValueSelector', () => {
155215
it('clone() should copy expression and valueType', () => {
156216
const item = new ValueSelector(tree, ValueType.ATTRIBUTE);

packages/ui/src/models/datamapper/mapping.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ export class OtherwiseItem extends InstructionItem {
184184
}
185185
}
186186

187+
const extractContextPath = (item: ForEachItem | ForEachGroupItem) => {
188+
const answer = XPathService.extractFieldPaths(item.expression)[0];
189+
if (answer) {
190+
const pathExpr = new PathExpression(item.parent.contextPath, answer.isRelative);
191+
pathExpr.pathSegments = answer.pathSegments;
192+
pathExpr.documentReferenceName = answer.documentReferenceName;
193+
return pathExpr;
194+
}
195+
return item.parent.contextPath;
196+
};
197+
187198
/**
188199
* Represents an `xsl:for-each` instruction.
189200
* {@link expression} selects the node-set to iterate over.
@@ -198,14 +209,7 @@ export class ForEachItem extends InstructionItem implements IExpressionHolder {
198209
expression = '';
199210

200211
get contextPath(): PathExpression | undefined {
201-
const answer = XPathService.extractFieldPaths(this.expression)[0];
202-
if (answer) {
203-
const pathExpr = new PathExpression(this.parent.contextPath, answer.isRelative);
204-
pathExpr.pathSegments = answer.pathSegments;
205-
pathExpr.documentReferenceName = answer.documentReferenceName;
206-
return pathExpr;
207-
}
208-
return this.parent.contextPath;
212+
return extractContextPath(this);
209213
}
210214

211215
sortItems: SortItem[] = [];
@@ -228,6 +232,56 @@ export class ForEachItem extends InstructionItem implements IExpressionHolder {
228232
}
229233
}
230234

235+
/** Selects which grouping attribute is emitted on `xsl:for-each-group`. */
236+
export enum GroupingStrategy {
237+
GROUP_BY = 'group-by',
238+
GROUP_ADJACENT = 'group-adjacent',
239+
GROUP_STARTING_WITH = 'group-starting-with',
240+
GROUP_ENDING_WITH = 'group-ending-with',
241+
}
242+
243+
/**
244+
* Represents an `xsl:for-each-group` instruction.
245+
* {@link expression} selects the population to group; {@link groupingStrategy}
246+
* and {@link groupingExpression} control the grouping attribute.
247+
* Overrides {@link contextPath} so that descendant XPath expressions
248+
* are evaluated relative to each group.
249+
*/
250+
export class ForEachGroupItem extends InstructionItem implements IExpressionHolder {
251+
constructor(public parent: MappingParentType) {
252+
super(parent, 'for-each-group');
253+
}
254+
255+
expression = '';
256+
groupingStrategy: GroupingStrategy = GroupingStrategy.GROUP_BY;
257+
groupingExpression = '';
258+
259+
get contextPath(): PathExpression | undefined {
260+
return extractContextPath(this);
261+
}
262+
263+
sortItems: SortItem[] = [];
264+
265+
doClone() {
266+
const cloned = new ForEachGroupItem(this.parent);
267+
cloned.sortItems = this.sortItems.map((sort) => {
268+
return {
269+
expression: sort.expression,
270+
order: sort.order,
271+
} as SortItem;
272+
});
273+
return cloned;
274+
}
275+
276+
clone() {
277+
const cloned = super.clone() as ForEachGroupItem;
278+
cloned.expression = this.expression;
279+
cloned.groupingStrategy = this.groupingStrategy;
280+
cloned.groupingExpression = this.groupingExpression;
281+
return cloned;
282+
}
283+
}
284+
231285
/** Sorting criteria for an `xsl:sort` element. Used by `xsl:for-each` and `xsl:for-each-group` instructions. */
232286
export class SortItem {
233287
expression: string = '';

0 commit comments

Comments
 (0)