diff --git a/homedocs/src/examples/tables/GroupingExample.tsx b/homedocs/src/examples/tables/GroupingExample.tsx
index 64b341f7c..84ac33724 100644
--- a/homedocs/src/examples/tables/GroupingExample.tsx
+++ b/homedocs/src/examples/tables/GroupingExample.tsx
@@ -111,11 +111,13 @@ export default (
style="height: 500px"
scrollable
border
+ sortGroups
grouping={[{ key: {}, showFooter: true }, "continent"]}
columns={[
{
header: "Name",
field: "fullName",
+ sortable: true,
aggregate: "count",
footer: tpl(m.$group.fullName, "{0} people"),
},
@@ -136,6 +138,7 @@ export default (
header: "Visits",
field: "visits",
align: "right",
+ sortable: true,
aggregate: "sum",
},
]}
diff --git a/homedocs/src/pages/docs/tables/grid.mdx b/homedocs/src/pages/docs/tables/grid.mdx
index 6a1b47538..e28fc5018 100644
--- a/homedocs/src/pages/docs/tables/grid.mdx
+++ b/homedocs/src/pages/docs/tables/grid.mdx
@@ -154,7 +154,10 @@ Each grouping level supports the following options:
| `showHeader` | `boolean` | Show a header row within each group. Useful for long printable reports. |
| `showCaption` | `boolean` | Show a caption row at the start of each group. Caption content is defined in the column's `caption` property. |
| `text` | `string` | A selector for text available as `$group.$name` in templates. |
-| `comparer` | `function` | A function to determine group ordering. |
+| `sortField` | `string` | Sort groups by this field, resolved against the group's key fields, aggregate aliases and `$name`. |
+| `sortDirection` | `string` | Sort direction for `sortField` (`ASC` or `DESC`). Defaults to `ASC`. |
+| `sorters` | `array` | Multi-field group sorting: `[{ field, direction }, ...]`. |
+| `comparer` | `function` | A function to determine group ordering. Takes precedence over `sortField`/`sorters`. |
Column aggregates (`sum`, `count`, `avg`, `distinct`) are automatically calculated and available in footer/caption templates via `$group`.
@@ -266,6 +269,7 @@ See also: [Row Drag and Drop](/docs/tables/row-drag-and-drop)
| `groupingParams` | `any` | Parameters passed to `onGetGrouping` for dynamic grouping. |
| `onGetGrouping` | `function` | Callback to dynamically generate grouping when params change. |
| `preserveGroupOrder`| `boolean` | Keep groups in the same order as source records. |
+| `sortGroups` | `boolean` | Reorder groups when a column is sorted, if its field is a group key or aggregate. Ignored with `preserveGroupOrder`. |
### Filtering
diff --git a/homedocs/src/pages/docs/tables/group-adapter.mdx b/homedocs/src/pages/docs/tables/group-adapter.mdx
index 6b06ad1d2..d9e8ed348 100644
--- a/homedocs/src/pages/docs/tables/group-adapter.mdx
+++ b/homedocs/src/pages/docs/tables/group-adapter.mdx
@@ -81,8 +81,9 @@ The `$group` object exposes the following fields in headers and footers:
| `indexName` | `string` | Alias used to expose record index. Default is `$index`. |
| `keyField` | `string` | Field used as the unique record key. |
| `groupName` | `string` | Alias used to expose group data. Default is `$group`. |
-| `groupings` | `array` | Defines criteria for grouping records. Supports header and footer configuration. |
+| `groupings` | `array` | Defines criteria for grouping records. Each level also accepts `sortField`/`sortDirection`, a `sorters` array, or a custom `comparer` to order groups by key, aggregate or name. |
| `aggregates` | `object` | Defines computed values based on grouped records (e.g., count, sum, avg). |
+| `sortGroupsBySorters` | `boolean` | When enabled, the active record sorters also reorder groups (resolved against each group's key/aggregates/name). The Grid's `sortGroups` prop sets this. |
| `groupRecordsName` | `string` | Alias used to expose records within a group. Default is `$records`. |
| `groupRecordsAlias` | `string` | Alias used to expose group records outside the group. |
| `sortOptions` | `object` | Options for data sorting. See [Intl.Collator options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator). |
diff --git a/homedocs/src/pages/docs/tables/grouping.mdx b/homedocs/src/pages/docs/tables/grouping.mdx
index 5139761a6..d9dd3faa1 100644
--- a/homedocs/src/pages/docs/tables/grouping.mdx
+++ b/homedocs/src/pages/docs/tables/grouping.mdx
@@ -27,7 +27,7 @@ Grid supports grouping records by one or more fields with aggregation and custom
-Records are grouped by continent with aggregate calculations shown in the footer.
+Records are grouped by continent with aggregate calculations shown in the footer. Because the grid sets `sortGroups`, clicking the **Visits** header also reorders the continents by their total visits, and **Name** orders them by head count.
## Basic Grouping
@@ -118,6 +118,30 @@ Add a column with the built-in `cxe-grid-row-number` class to display automatic
The counter resets on the group caption row, so the grouping level needs a caption (`showCaption` or `caption`) for the reset to take effect.
+## Sorting Groups
+
+By default groups are ordered by their key, ascending. There are three ways to change that.
+
+**Sort by an aggregate or key declaratively.** Set `sortField` (and optionally `sortDirection`) on a grouping level. The field is resolved against the group's key fields, aggregate aliases and `$name`, so an aggregate alias sorts groups by that aggregate:
+
+```tsx
+grouping={[
+ {
+ key: { continent: { bind: "$record.continent" } },
+ sortField: "visits", // aggregate alias
+ sortDirection: "DESC", // highest total first
+ },
+]}
+```
+
+Use a `sorters` array for multi-field group sorting (`[{ field, direction }, ...]`). A custom `comparer` still takes precedence over both.
+
+**Sort groups by clicking column headers.** Set `sortGroups` on the grid. When the user sorts a column whose field is a group key or aggregate, the groups reorder to match; sorting any other column leaves the group order untouched. This is ignored when `preserveGroupOrder` is set.
+
+```tsx
+
+```
+
## Group Data
The `$group` object contains:
@@ -141,5 +165,9 @@ The `$group` object contains:
| `resetRowNumbers` | `boolean` | Restart `cxe-grid-row-number` numbering from 1 at the start of each group at this level. Requires a caption. |
| `caption` | `StringProp` | Caption template. |
| `text` | `StringProp` | Group name template. |
+| `sortField` | `string` | Sort groups by this field, resolved against the group's key fields, aggregate aliases and `$name`. |
+| `sortDirection` | `string` | Sort direction for `sortField` (`ASC` or `DESC`). Defaults to `ASC`. |
+| `sorters` | `array` | Multi-field group sorting: `[{ field, direction }, ...]`. |
+| `comparer` | `function` | Custom group comparer. Takes precedence over `sortField`/`sorters`. |
See also: [Dynamic Grouping](/docs/tables/dynamic-grouping), [GroupAdapter](/docs/tables/group-adapter)
diff --git a/packages/cx/src/data/comparer.ts b/packages/cx/src/data/comparer.ts
index 6e7fdf6cc..011bdda7a 100644
--- a/packages/cx/src/data/comparer.ts
+++ b/packages/cx/src/data/comparer.ts
@@ -1,17 +1,16 @@
import { getSelector } from "./getSelector";
import { isDefined } from "../util/isDefined";
import { defaultCompare } from "./defaultCompare";
+import type { CollatorOptions, Sorter } from "../ui/Prop";
-interface Sorter {
- value?: any;
- field?: string;
- direction?: string;
+export interface ExtendedSorter extends Sorter {
comparer?: (a: any, b: any) => number;
compare?: (a: any, b: any) => number;
+ sortOptions?: CollatorOptions;
}
export function getComparer(
- sorters: Sorter[],
+ sorters: ExtendedSorter[],
dataAccessor?: (x: any) => any,
comparer?: (a: any, b: any) => number,
): (a: any, b: any) => number {
@@ -51,7 +50,7 @@ export function getComparer(
}
export function indexSorter(
- sorters: Sorter[],
+ sorters: ExtendedSorter[],
dataAccessor?: (x: any) => any,
compare?: (a: any, b: any) => number,
): (data: any[]) => number[] {
@@ -64,7 +63,7 @@ export function indexSorter(
}
export function sorter(
- sorters: Sorter[],
+ sorters: ExtendedSorter[],
dataAccessor?: (x: any) => any,
compare?: (a: any, b: any) => number,
): (data: any[]) => any[] {
diff --git a/packages/cx/src/ui/Prop.ts b/packages/cx/src/ui/Prop.ts
index 69514dc04..fd4015b47 100644
--- a/packages/cx/src/ui/Prop.ts
+++ b/packages/cx/src/ui/Prop.ts
@@ -127,7 +127,7 @@ export type SortDirection = "ASC" | "DESC";
export interface Sorter {
field?: string;
- value?: (record: DataRecord) => any;
+ value?: Prop;
direction: SortDirection;
}
diff --git a/packages/cx/src/ui/adapter/ArrayAdapter.ts b/packages/cx/src/ui/adapter/ArrayAdapter.ts
index d1f23f305..6e84de59a 100644
--- a/packages/cx/src/ui/adapter/ArrayAdapter.ts
+++ b/packages/cx/src/ui/adapter/ArrayAdapter.ts
@@ -1,6 +1,6 @@
import { DataAdapter, DataAdapterRecord, DataAdapterConfig } from "./DataAdapter";
import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
-import { sorter } from "../../data/comparer";
+import { sorter, type ExtendedSorter } from "../../data/comparer";
import { isArray } from "../../util/isArray";
import { ArrayElementView } from "../../data/ArrayElementView";
import { Accessor, getAccessor } from "../../data/getAccessor";
@@ -9,7 +9,7 @@ import { isDefined, isObject } from "../../util";
import { RenderingContext } from "../RenderingContext";
import { Instance } from "../Instance";
import { View } from "../../data/View";
-import { Prop, Sorter, CollatorOptions } from "../Prop";
+import { Prop, CollatorOptions } from "../Prop";
export interface RecordStoreCache {
recordStoreCache: WeakMap;
@@ -26,10 +26,8 @@ export interface ArrayAdapterConfig extends DataAdapterConfig {
preserveOrder?: boolean;
}
-export interface ExtendedSorter extends Sorter {
- comparer?: (a: any, b: any) => number;
- sortOptions?: CollatorOptions;
-}
+// Re-exported for back-compat; the canonical definition lives in `data/comparer`.
+export type { ExtendedSorter } from "../../data/comparer";
export interface ResolvedSorter {
getter: (x: any) => any;
@@ -213,9 +211,9 @@ export class ArrayAdapter extends DataAdapter {
}
}
- public sort(sorters?: Sorter[] | ExtendedSorter[]): void {
+ public sort(sorters?: ExtendedSorter[]): void {
if (sorters) {
- this.buildSorter(sorters as ExtendedSorter[]);
+ this.buildSorter(sorters);
}
}
}
diff --git a/packages/cx/src/ui/adapter/GroupAdapter.spec.ts b/packages/cx/src/ui/adapter/GroupAdapter.spec.ts
index 29ff0a7c4..48536c80d 100644
--- a/packages/cx/src/ui/adapter/GroupAdapter.spec.ts
+++ b/packages/cx/src/ui/adapter/GroupAdapter.spec.ts
@@ -39,4 +39,79 @@ describe("GroupAdapter", () => {
assert.deepStrictEqual(newKeys, [], `Grouping config was mutated. New keys: ${newKeys.join(", ")}`);
});
+
+ // Region totals after aggregation: A=10, B=30, C=20.
+ const records = [
+ { region: "A", sales: 10, product: "p3" },
+ { region: "B", sales: 30, product: "p1" },
+ { region: "C", sales: 20, product: "p2" },
+ ];
+
+ const baseGrouping: GroupingConfig = {
+ key: { region: { bind: "$record.region" } },
+ aggregates: { total: { type: "sum", value: { bind: "$record.sales" } } },
+ };
+
+ // Runs the full grouping pipeline and returns the resulting group order (region keys).
+ function groupOrder(grouping: GroupingConfig[], options?: { sortGroupsBySorters?: boolean }, sorters?: any[]) {
+ const adapter = new GroupAdapter({ groupings: grouping, ...options });
+ adapter.init();
+ if (sorters) adapter.sort(sorters);
+ const result = adapter.getRecords({} as any, {} as any, records, new Store({ data: {} }));
+ return result.filter((r) => r.type === "group-header").map((r: any) => r.group.region);
+ }
+
+ it("sorts groups by an aggregate via sortField/sortDirection", () => {
+ assert.deepStrictEqual(groupOrder([{ ...baseGrouping, sortField: "total", sortDirection: "DESC" }]), [
+ "B",
+ "C",
+ "A",
+ ]);
+ });
+
+ it("sorts groups by a key field via sortField", () => {
+ assert.deepStrictEqual(groupOrder([{ ...baseGrouping, sortField: "region", sortDirection: "DESC" }]), [
+ "C",
+ "B",
+ "A",
+ ]);
+ });
+
+ it("supports a multi-field sorters array", () => {
+ assert.deepStrictEqual(groupOrder([{ ...baseGrouping, sorters: [{ field: "total", direction: "ASC" }] }]), [
+ "A",
+ "C",
+ "B",
+ ]);
+ });
+
+ it("defaults to ascending sort by key", () => {
+ assert.deepStrictEqual(groupOrder([baseGrouping]), ["A", "B", "C"]);
+ });
+
+ it("lets an explicit comparer win over sortField", () => {
+ // Custom comparer sorts groups by total ascending, despite sortField asking for DESC.
+ const comparer = (a: any, b: any) => a.aggregates.total - b.aggregates.total;
+ assert.deepStrictEqual(groupOrder([{ ...baseGrouping, comparer, sortField: "total", sortDirection: "DESC" }]), [
+ "A",
+ "C",
+ "B",
+ ]);
+ });
+
+ it("reorders groups by the active column sort when sortGroupsBySorters is set", () => {
+ // The record-level value selector must be ignored; the column field drives group order.
+ const order = groupOrder([baseGrouping], { sortGroupsBySorters: true }, [
+ { field: "total", direction: "DESC", value: () => 999 },
+ ]);
+ assert.deepStrictEqual(order, ["B", "C", "A"]);
+ });
+
+ it("keeps configured group order when the sorted column is not a group field", () => {
+ // `product` is neither a key nor an aggregate, so groups keep their default (key ASC) order.
+ const order = groupOrder([baseGrouping], { sortGroupsBySorters: true }, [
+ { field: "product", direction: "DESC" },
+ ]);
+ assert.deepStrictEqual(order, ["A", "B", "C"]);
+ });
});
diff --git a/packages/cx/src/ui/adapter/GroupAdapter.ts b/packages/cx/src/ui/adapter/GroupAdapter.ts
index cd480a91d..c22da6305 100644
--- a/packages/cx/src/ui/adapter/GroupAdapter.ts
+++ b/packages/cx/src/ui/adapter/GroupAdapter.ts
@@ -4,11 +4,13 @@ import { ReadOnlyDataView } from "../../data/ReadOnlyDataView";
import { View } from "../../data/View";
import { isDataRecord } from "../../util";
import { isArray } from "../../util/isArray";
+import { isDefined } from "../../util/isDefined";
+import { isNonEmptyArray } from "../../util/isNonEmptyArray";
import { Culture } from "../Culture";
import { Instance } from "../Instance";
import { Prop, SortDirection, Sorter, StructuredProp } from "../Prop";
import { RenderingContext } from "../RenderingContext";
-import { ArrayAdapter, ArrayAdapterConfig, RecordStoreCache } from "./ArrayAdapter";
+import { ArrayAdapter, ArrayAdapterConfig, ExtendedSorter, RecordStoreCache } from "./ArrayAdapter";
import { DataAdapterRecord } from "./DataAdapter";
export interface GroupKey {
@@ -21,7 +23,17 @@ export interface GroupingConfig {
text?: Prop;
includeHeader?: boolean;
includeFooter?: boolean;
+ /** Custom group comparer. Takes precedence over `sortField`/`sortDirection`/`sorters`. */
comparer?: ((a: any, b: any) => number) | null;
+ /**
+ * Sort groups by a single field resolved against the group's `key`, `aggregates` and `name`.
+ * Use an aggregate alias (e.g. `"total"`) to sort by an aggregate, or a key field to sort by key.
+ */
+ sortField?: string;
+ /** Sort direction used together with `sortField`. Defaults to `"ASC"`. */
+ sortDirection?: SortDirection;
+ /** Multi-field group sorting. Each sorter's `field` is resolved against the group's `key`, `aggregates` and `name`. */
+ sorters?: Sorter[];
header?: any;
footer?: any;
}
@@ -50,6 +62,8 @@ export interface GroupAdapterConfig extends ArrayAdapterConfig {
groupRecordsName?: string;
groupings?: GroupingConfig[] | null;
groupName?: string;
+ /** When enabled, the active record sorters also reorder groups (resolved against each group's key/aggregates/name). */
+ sortGroupsBySorters?: boolean;
}
export class GroupAdapter extends ArrayAdapter {
@@ -58,11 +72,41 @@ export class GroupAdapter extends ArrayAdapter {
declare public groupRecordsName?: string;
declare public groupings?: ResolvedGrouping[] | null;
declare public groupName: string;
+ declare public sortGroupsBySorters?: boolean;
+
+ /**
+ * Per-level comparer derived from the active record sorters, used to reorder groups when
+ * `sortGroupsBySorters` is set. A level's entry is `null` when no active sorter maps to that
+ * level's key/aggregate, so the grouping's own configured order is kept.
+ */
+ protected groupSortComparers?: (((a: any, b: any) => number) | null)[];
constructor(config?: GroupAdapterConfig) {
super(config);
}
+ public sort(sorters?: ExtendedSorter[]): void {
+ super.sort(sorters);
+
+ // When enabled, derive a group comparer from the active record sorters so that interactive
+ // column sorting also reorders the groups. A column only reorders groups at levels where its
+ // field is a group key or aggregate; other levels keep their configured order. The record-level
+ // value selector is ignored — the field is resolved against the group's key/aggregates/name.
+ if (this.sortGroupsBySorters && this.groupings) {
+ const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
+ const colSorters: ExtendedSorter[] = isNonEmptyArray(sorters)
+ ? sorters.map((s) => ({ field: s.field, direction: s.direction, comparer: s.comparer }))
+ : [];
+
+ this.groupSortComparers = this.groupings.map((g) => {
+ if (colSorters.length === 0) return null;
+ const fields = groupFieldNames(g, this.aggregates);
+ const applicable = colSorters.filter((s) => s.field && fields.has(s.field));
+ return applicable.length > 0 ? buildGroupComparer(applicable, cultureComparer) : null;
+ });
+ }
+ }
+
public init(): void {
super.init();
@@ -116,8 +160,13 @@ export class GroupAdapter extends ArrayAdapter {
grouper.processAll(records);
let results = grouper.getResults();
- if (grouping.comparer && !this.preserveOrder) {
- results.sort(grouping.comparer);
+ // An active column sort that maps to this level's key/aggregate (when `sortGroupsBySorters` is
+ // enabled) takes over; otherwise the grouping's configured comparer (or implicit key order) applies.
+ const dynamicComparer = this.sortGroupsBySorters ? this.groupSortComparers?.[level] : undefined;
+ const comparer = dynamicComparer ?? grouping.comparer;
+
+ if (comparer && !this.preserveOrder) {
+ results.sort(comparer);
}
results.forEach((gr) => {
@@ -208,15 +257,22 @@ export class GroupAdapter extends ArrayAdapter {
g.text,
);
+ const cultureComparer = this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined;
+
+ // Sort groups by an aggregate/key/name through `sortField`/`sortDirection` or a `sorters` array.
+ let sortSorters: Sorter[] | null = null;
+ if (isNonEmptyArray(g.sorters)) sortSorters = g.sorters!;
+ else if (g.sortField) sortSorters = [{ field: g.sortField, direction: g.sortDirection ?? "ASC" }];
+
+ // `comparer`, `sortField`/`sorters` and the implicit key order are equivalent alternatives; an
+ // explicit `comparer` wins, then declarative sorters, then sorting by key in declaration order.
const comparer =
g.comparer ??
- (groupSorters.length > 0
- ? getComparer(
- groupSorters,
- (x: any) => x.key,
- this.sortOptions ? Culture.getComparer(this.sortOptions) : undefined,
- )
- : null);
+ (sortSorters
+ ? buildGroupComparer(sortSorters, cultureComparer)
+ : groupSorters.length > 0
+ ? getComparer(groupSorters, (x: any) => x.key, cultureComparer)
+ : null);
return {
...g,
@@ -233,6 +289,43 @@ export class GroupAdapter extends ArrayAdapter {
GroupAdapter.prototype.groupName = "$group";
GroupAdapter.prototype.preserveOrder = false;
+GroupAdapter.prototype.sortGroupsBySorters = false;
+
+// The set of fields a group can be sorted by at a given level: its key fields, its aggregate aliases
+// and the group name. Used to decide whether an active column sort applies to that grouping level.
+function groupFieldNames(grouping: ResolvedGrouping, adapterAggregates?: StructuredProp): Set {
+ const names = new Set();
+ for (const k of grouping.grouper.keys) names.add(k.name);
+ const aggregates = { ...adapterAggregates, ...grouping.aggregates };
+ for (const a in aggregates) names.add(a);
+ names.add("name");
+ names.add("$name");
+ return names;
+}
+
+// Reads a sort field straight from the GroupResult — first its key fields, then its aggregates, then
+// its name. Returns a plain selector so groups can be sorted without cloning the result per comparison.
+function groupFieldSelector(field: string): (gr: any) => any {
+ if (field === "name" || field === "$name") return (gr) => gr?.name;
+ return (gr) => {
+ if (gr?.key && field in gr.key) return gr.key[field];
+ if (gr?.aggregates && field in gr.aggregates) return gr.aggregates[field];
+ return undefined;
+ };
+}
+
+// Builds a group comparer from a list of sorters. Each sorter resolves by its explicit `value`
+// selector, or by `field` looked up against the group's key/aggregates/name.
+function buildGroupComparer(
+ sorters: ExtendedSorter[],
+ cultureComparer?: (a: any, b: any) => number,
+): (a: any, b: any) => number {
+ return getComparer(
+ sorters.map((s) => (isDefined(s.value) || !s.field ? s : { ...s, value: groupFieldSelector(s.field) })),
+ undefined,
+ cultureComparer,
+ );
+}
function serializeKey(data: any): string {
if (isDataRecord(data)) {
diff --git a/packages/cx/src/widgets/form/NumberField.scss b/packages/cx/src/widgets/form/NumberField.scss
index 92a3c638f..33c51fe90 100644
--- a/packages/cx/src/widgets/form/NumberField.scss
+++ b/packages/cx/src/widgets/form/NumberField.scss
@@ -42,6 +42,10 @@
.#{$state}icon > & {
padding-left: cx-calc(cx-top($padding), $cx-default-input-left-tool-size, $cx-default-input-left-tool-spacing);
}
+
+ .#{$state}clear > & {
+ padding-right: cx-calc(cx-top($padding), $cx-default-clear-size, $cx-default-clear-spacing);
+ }
}
.#{$element}#{$name}-tool {
diff --git a/packages/cx/src/widgets/form/NumberField.tsx b/packages/cx/src/widgets/form/NumberField.tsx
index 40780a71d..58e1c8e18 100644
--- a/packages/cx/src/widgets/form/NumberField.tsx
+++ b/packages/cx/src/widgets/form/NumberField.tsx
@@ -312,6 +312,7 @@ class Input extends VDOM.Component {
visited: state.visited,
focus: this.state.focus,
icon: !!icon,
+ clear: insideButton != null,
empty: empty && !data.placeholder,
error:
data.error &&
diff --git a/packages/cx/src/widgets/grid/Grid.tsx b/packages/cx/src/widgets/grid/Grid.tsx
index 441851b77..5565160fe 100644
--- a/packages/cx/src/widgets/grid/Grid.tsx
+++ b/packages/cx/src/widgets/grid/Grid.tsx
@@ -23,6 +23,7 @@ import {
NumberProp,
Prop,
RecordAlias,
+ Sorter,
SortDirection,
SortersProp,
StringProp,
@@ -165,6 +166,15 @@ export interface GridGroupingConfig {
name?: StringProp;
text?: StringProp;
comparer?: (a: GroupingResult, b: GroupingResult) => number;
+ /**
+ * Sort groups by a single field resolved against the group's key, aggregates and name.
+ * Use an aggregate alias to sort by an aggregate, or a key field to sort by key.
+ */
+ sortField?: string;
+ /** Sort direction used together with `sortField`. Defaults to `"ASC"`. */
+ sortDirection?: SortDirection;
+ /** Multi-field group sorting. Each sorter's `field` is resolved against the group's key, aggregates and name. */
+ sorters?: Sorter[];
}
export interface GridColumnHeaderConfig {
@@ -502,6 +512,13 @@ export interface GridConfig extends StyledContainerConfig {
/** When enabled, groups are shown in the same order as the source records. */
preserveGroupOrder?: boolean;
+
+ /**
+ * When enabled, sorting by a column header also reorders groups. The active sort field is resolved
+ * against each group's key fields, aggregates and name, so it works for both grouping columns and
+ * aggregate columns. Ignored when `preserveGroupOrder` is set.
+ */
+ sortGroups?: boolean;
}
export interface GridCellInfo {
@@ -635,6 +652,7 @@ export class Grid extends ContainerBase, GridInstance
declare onCreateIsRecordDraggable?: any;
declare onRef?: any;
declare preserveGroupOrder: boolean;
+ declare sortGroups: boolean;
declare styled: boolean;
declare selectable?: boolean;
declare recordsAccessor: any;
@@ -844,6 +862,7 @@ export class Grid extends ContainerBase, GridInstance
sortOptions: this.sortOptions,
groupings: grouping,
preserveOrder: this.preserveGroupOrder,
+ sortGroupsBySorters: this.sortGroups,
},
this.dataAdapter,
);
@@ -1903,6 +1922,7 @@ Grid.prototype.hoverChannel = "default";
Grid.prototype.focusable = null; // automatically resolved
Grid.prototype.allowsFileDrops = false;
Grid.prototype.preserveGroupOrder = false;
+Grid.prototype.sortGroups = false;
Widget.alias("grid", Grid);
Localization.registerPrototype("cx/widgets/Grid", Grid);