Skip to content

Commit a7175ed

Browse files
committed
GridCommands: support optional properties for summary
1 parent 6d2f3e1 commit a7175ed

2 files changed

Lines changed: 121 additions & 8 deletions

File tree

packages/devextreme/js/__internal/grids/data_grid/ai_assistant/commands/__tests__/summary.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,74 @@ describe('summaryCommand', () => {
104104
totalItems: [{ column: 'amount', summaryType: 'sum', extra: 1 }],
105105
}).success).toBe(false);
106106
});
107+
108+
it('accepts showInColumn and displayFormat on totalItems', () => {
109+
expect(summaryCommand.schema.safeParse({
110+
totalItems: [{
111+
column: 'amount',
112+
summaryType: 'sum',
113+
showInColumn: 'name',
114+
displayFormat: 'Sum: {0}',
115+
}],
116+
}).success).toBe(true);
117+
});
118+
119+
it('accepts showInColumn, displayFormat, showInGroupFooter, alignByColumn on groupItems', () => {
120+
expect(summaryCommand.schema.safeParse({
121+
groupItems: [{
122+
column: 'amount',
123+
summaryType: 'avg',
124+
showInColumn: 'name',
125+
displayFormat: '{1}: {0}',
126+
showInGroupFooter: true,
127+
alignByColumn: true,
128+
}],
129+
}).success).toBe(true);
130+
});
131+
132+
it('rejects showInGroupFooter on totalItems (group-only field)', () => {
133+
expect(summaryCommand.schema.safeParse({
134+
totalItems: [{
135+
column: 'amount',
136+
summaryType: 'sum',
137+
showInGroupFooter: true,
138+
}],
139+
}).success).toBe(false);
140+
});
141+
142+
it('rejects alignByColumn on totalItems (group-only field)', () => {
143+
expect(summaryCommand.schema.safeParse({
144+
totalItems: [{
145+
column: 'amount',
146+
summaryType: 'sum',
147+
alignByColumn: true,
148+
}],
149+
}).success).toBe(false);
150+
});
151+
152+
it('rejects when showInColumn is not a string', () => {
153+
expect(summaryCommand.schema.safeParse({
154+
totalItems: [{ column: 'amount', summaryType: 'sum', showInColumn: 1 }],
155+
}).success).toBe(false);
156+
});
157+
158+
it('rejects when displayFormat is not a string', () => {
159+
expect(summaryCommand.schema.safeParse({
160+
totalItems: [{ column: 'amount', summaryType: 'sum', displayFormat: 5 }],
161+
}).success).toBe(false);
162+
});
163+
164+
it('rejects when showInGroupFooter is not a boolean', () => {
165+
expect(summaryCommand.schema.safeParse({
166+
groupItems: [{ column: 'amount', summaryType: 'sum', showInGroupFooter: 'yes' }],
167+
}).success).toBe(false);
168+
});
169+
170+
it('rejects when alignByColumn is not a boolean', () => {
171+
expect(summaryCommand.schema.safeParse({
172+
groupItems: [{ column: 'amount', summaryType: 'sum', alignByColumn: 1 }],
173+
}).success).toBe(false);
174+
});
107175
});
108176

109177
describe('execute', () => {
@@ -161,6 +229,23 @@ describe('summaryCommand', () => {
161229
expect(optionSpy).not.toHaveBeenCalledWith('summary', expect.anything());
162230
});
163231

232+
it('returns failure when showInColumn does not resolve to an existing column', async () => {
233+
const instance = await createGrid();
234+
const optionSpy = jest.spyOn(instance, 'option');
235+
const callbacks = createCallbacks();
236+
237+
const result = await summaryCommand.execute(instance, callbacks)({
238+
totalItems: [{
239+
column: 'amount',
240+
summaryType: 'sum',
241+
showInColumn: 'unknown',
242+
}],
243+
});
244+
245+
expect(result.status).toBe('failure');
246+
expect(optionSpy).not.toHaveBeenCalledWith('summary', expect.anything());
247+
});
248+
164249
it('calls component.option("summary", { totalItems, groupItems }) on success', async () => {
165250
const instance = await createGrid();
166251
const optionSpy = jest.spyOn(instance, 'option');

packages/devextreme/js/__internal/grids/data_grid/ai_assistant/commands/summary.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,29 @@ import type { Column } from './types';
77

88
const SUMMARY_TYPES = ['sum', 'min', 'max', 'avg', 'count'] as const satisfies readonly SummaryType[];
99

10-
const summaryItemSchema = z.object({
10+
const summaryItemBaseShape = {
1111
column: z.string(),
1212
summaryType: z.enum(SUMMARY_TYPES),
13+
showInColumn: z.string().optional(),
14+
displayFormat: z.string().optional(),
15+
};
16+
17+
const totalItemSchema = z.object(summaryItemBaseShape).strict();
18+
19+
const groupItemSchema = z.object({
20+
...summaryItemBaseShape,
21+
showInGroupFooter: z.boolean().optional(),
22+
alignByColumn: z.boolean().optional(),
1323
}).strict();
1424

1525
const summaryCommandSchema = z.object({
16-
totalItems: z.array(summaryItemSchema).optional(),
17-
groupItems: z.array(summaryItemSchema).optional(),
26+
totalItems: z.array(totalItemSchema).optional(),
27+
groupItems: z.array(groupItemSchema).optional(),
1828
}).strict();
1929

20-
type SummaryItem = z.infer<typeof summaryItemSchema>;
30+
type TotalItem = z.infer<typeof totalItemSchema>;
31+
type GroupItem = z.infer<typeof groupItemSchema>;
32+
type SummaryItem = TotalItem | GroupItem;
2133

2234
const SUMMARY_TYPE_LABELS: Record<SummaryItem['summaryType'], string> = {
2335
sum: 'sum',
@@ -57,7 +69,15 @@ const buildDefaultMessage = (
5769

5870
export const summaryCommand = defineGridCommand({
5971
name: 'summary',
60-
description: 'Configure column summaries. totalItems aggregate the entire data set; groupItems aggregate within each group. Provide at least one item across the two arrays — use clearSummary to remove all summaries. Replaces existing summaries entirely; pre-merge with the grid\'s current summary items if you intend to add rather than replace.',
72+
description: 'Configure column summaries. totalItems aggregate the entire data set; groupItems aggregate within each group. Provide at least one item across the two arrays — use clearSummary to remove all summaries. Replaces existing summaries entirely; pre-merge with the grid\'s current summary items if you intend to add rather than replace.\n'
73+
+ 'Each item supports:\n'
74+
+ '- column (required): dataField of the column to aggregate.\n'
75+
+ '- summaryType (required): one of "sum", "min", "max", "avg", "count".\n'
76+
+ '- showInColumn (optional): dataField of the column under which the summary value is rendered. For totalItems, controls which column\'s footer cell shows the value. For groupItems, used when showInGroupFooter=true or alignByColumn=true to pick the column the value is shown under. Must match an existing column.\n'
77+
+ '- displayFormat (optional): format template for the rendered value. Placeholders: "{0}" — the formatted summary value; "{1}" — the parent column\'s caption (for group items only resolvable when showInColumn is specified). Example: "Sum: {0}" or "{1}: {0}".\n'
78+
+ 'Group items additionally support:\n'
79+
+ '- showInGroupFooter (optional, default false): render in the group footer instead of the group row.\n'
80+
+ '- alignByColumn (optional, default false): when false, group summary items are listed in parentheses after the group row header. When true, items are aligned by their columns within the group row.',
6181
schema: summaryCommandSchema,
6282
execute: (component, { success, failure }) => (args): Promise<CommandResult> => {
6383
const columnsController = component.getController('columns');
@@ -71,9 +91,17 @@ export const summaryCommand = defineGridCommand({
7191
return Promise.resolve(failure(defaultMessage));
7292
}
7393

74-
const allColumnsResolve = allItems.every(
75-
(item) => columnsController.columnOption(item.column) !== undefined,
76-
);
94+
const allColumnsResolve = allItems.every((item) => {
95+
if (columnsController.columnOption(item.column) === undefined) {
96+
return false;
97+
}
98+
99+
if (item.showInColumn !== undefined) {
100+
return columnsController.columnOption(item.showInColumn) !== undefined;
101+
}
102+
103+
return true;
104+
});
77105

78106
if (!allColumnsResolve) {
79107
return Promise.resolve(failure(defaultMessage));

0 commit comments

Comments
 (0)