Skip to content

Commit 9d54d1a

Browse files
igarashitmlordrip
authored andcommitted
chore(DataMapper): Add ForEachGroupItem and GroupingStrategy
Fixes: #2861
1 parent 9c59568 commit 9d54d1a

2 files changed

Lines changed: 125 additions & 14 deletions

File tree

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

Lines changed: 63 additions & 6 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
});
@@ -137,17 +140,71 @@ describe('mapping.ts', () => {
137140
const item = new ForEachItem(tree);
138141
item.expression = '/Order/Items/Item';
139142

140-
// Get contextPath twice
141143
const firstCall = item.contextPath;
142144
const secondCall = item.contextPath;
143145

144-
// Both calls should return different object instances (not mutated)
145146
expect(firstCall).not.toBe(secondCall);
146147

147-
// But they should have the same values
148-
if (firstCall && secondCall) {
149-
expect(firstCall.contextPath).toEqual(secondCall.contextPath);
150-
}
148+
expect(firstCall?.pathSegments).toEqual(secondCall?.pathSegments);
149+
expect(firstCall?.isRelative).toBe(secondCall?.isRelative);
150+
expect(firstCall?.documentReferenceName).toBe(secondCall?.documentReferenceName);
151+
});
152+
});
153+
154+
describe('ForEachGroupItem', () => {
155+
it('should default to GROUP_BY strategy with empty expressions', () => {
156+
const item = new ForEachGroupItem(tree);
157+
expect(item.groupingStrategy).toBe(GroupingStrategy.GROUP_BY);
158+
expect(item.groupingExpression).toBe('');
159+
expect(item.expression).toBe('');
160+
});
161+
162+
it('doClone() should copy sortItems', () => {
163+
const item = new ForEachGroupItem(tree);
164+
const sort = new SortItem();
165+
sort.expression = '@price';
166+
sort.order = 'descending';
167+
item.sortItems = [sort];
168+
169+
const cloned = item.clone();
170+
171+
expect(cloned.sortItems).toHaveLength(1);
172+
expect(cloned.sortItems[0].expression).toBe('@price');
173+
expect(cloned.sortItems[0].order).toBe('descending');
174+
expect(cloned.sortItems[0]).not.toBe(sort);
175+
});
176+
177+
it('clone() should copy expression, groupingStrategy, groupingExpression, and children', () => {
178+
const item = new ForEachGroupItem(tree);
179+
item.expression = '/Order/Items/Item';
180+
item.groupingStrategy = GroupingStrategy.GROUP_ADJACENT;
181+
item.groupingExpression = 'Category';
182+
const child = new ValueSelector(item);
183+
child.expression = 'ItemId';
184+
item.children = [child];
185+
186+
const cloned = item.clone();
187+
188+
expect(cloned.expression).toBe('/Order/Items/Item');
189+
expect(cloned.groupingStrategy).toBe(GroupingStrategy.GROUP_ADJACENT);
190+
expect(cloned.groupingExpression).toBe('Category');
191+
expect(cloned.children).toHaveLength(1);
192+
expect((cloned.children[0] as ValueSelector).expression).toBe('ItemId');
193+
expect(cloned).not.toBe(item);
194+
});
195+
196+
it('contextPath getter should not mutate the original PathExpression', () => {
197+
const item = new ForEachGroupItem(tree);
198+
item.expression = '/Order/Items/Item';
199+
200+
const firstCall = item.contextPath;
201+
const secondCall = item.contextPath;
202+
203+
expect(firstCall).not.toBe(secondCall);
204+
205+
expect(firstCall?.pathSegments).toEqual(secondCall?.pathSegments);
206+
expect(firstCall?.isRelative).toBe(secondCall?.isRelative);
207+
expect(firstCall?.documentReferenceName).toBe(secondCall?.documentReferenceName);
151208
});
152209
});
153210

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)