From 629276dc8f14dc71be22f13c74adf8ad206645f7 Mon Sep 17 00:00:00 2001 From: Nebojsa Date: Thu, 23 Jul 2026 12:50:46 +0200 Subject: [PATCH 1/2] fix(Grid): sorting of columns identified by a value selector instead of a field - prepareData no longer fabricates a { field: undefined, direction: undefined } sorter when the sortField/sortDirection bindings are declared but empty; this crashed renderHeader (direction.toLowerCase) and silently discarded the value selector captured on header click, making value-only columns unsortable - a sort is now identified by its (field, value selector) pair instead of the field name alone, both for the sorted header indicator and for toggling in onHeaderClick; previously any field-less sortable column matched a field-less sorter (undefined == undefined) and lit up together, and columns sharing a field name hijacked each other's sort direction - clicking a field-less column no longer toggles the sort of the field currently stored in the sortField binding - the sorter fix-up loop in prepareData now also matches columns by sortField and covers line0 Sort key precedence is unchanged: sortValue > value > sortField > field. --- packages/cx/src/widgets/grid/Grid.tsx | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/cx/src/widgets/grid/Grid.tsx b/packages/cx/src/widgets/grid/Grid.tsx index 157abe289..1db9b0e27 100644 --- a/packages/cx/src/widgets/grid/Grid.tsx +++ b/packages/cx/src/widgets/grid/Grid.tsx @@ -919,10 +919,13 @@ export class Grid extends ContainerBase, GridInstance let sortField = null; - if (isDefined(this.sortField) && isDefined(this.sortDirection)) { + // rebuild sorters from the sortField/sortDirection bindings only if a sort field + // is actually set; sorts identified by a value selector (columns without a field) + // live in data.sorters/state.sorters and cannot round-trip through a field name + if (isDefined(this.sortField) && isDefined(this.sortDirection) && data.sortField) { let sorter = { field: data.sortField, - direction: data.sortDirection, + direction: data.sortDirection || "ASC", }; sortField = data.sortField; data.sorters = [sorter]; @@ -939,11 +942,11 @@ export class Grid extends ContainerBase, GridInstance } if (sortField) { - for (let l = 1; l < 10; l++) { + for (let l = 0; l < 10; l++) { let line = instance.row[`line${l}`]; - let sortColumn = line && line.columns && line.columns.find((c: any) => c.field == sortField); + let sortColumn = line && line.columns && line.columns.find((c: any) => (c.sortField || c.field) == sortField); if (sortColumn) { - data.sorters[0].value = sortColumn.sortValue || sortColumn.value; + data.sorters[0].value = isDefined(sortColumn.sortValue) ? sortColumn.sortValue : sortColumn.value; data.sorters[0].comparer = sortColumn.comparer; data.sorters[0].sortOptions = sortColumn.sortOptions; break; @@ -1307,8 +1310,18 @@ export class Grid extends ContainerBase, GridInstance if (hdwidget.sortable && header.widget.allowSorting) { mods.push("sortable"); - if (data.sorters && data.sorters[0].field == (hdwidget.sortField || hdwidget.field)) { - mods.push("sorted-" + data.sorters[0].direction.toLowerCase()); + let sorter = data.sorters && data.sorters[0]; + let sortColumnField = hdwidget.sortField || hdwidget.field; + let sortColumnValue = isDefined(hdwidget.sortValue) ? hdwidget.sortValue : hdwidget.value; + // a sort is identified by its (field, value selector) pair, so columns + // sorting by the same field through different value selectors don't both match + let sorted = + sorter && + !!sorter.direction && + (sortColumnField ? sorter.field == sortColumnField : !sorter.field && isDefined(sortColumnValue)) && + sorter.value === sortColumnValue; + if (sorted) { + mods.push("sorted-" + sorter.direction.toLowerCase()); sortIcon = ; } } @@ -1481,17 +1494,16 @@ export class Grid extends ContainerBase, GridInstance let header = column.components[`header${headerLine + 1}`]; let field = column.sortField || column.field; - let value = column.sortValue || column.value; + let value = isDefined(column.sortValue) ? column.sortValue : column.value; let comparer = column.comparer; let sortOptions = column.sortOptions; - if (header && header.allowSorting && column.sortable && (field || value || data.sortField)) { + if (header && header.allowSorting && column.sortable && (field || isDefined(value))) { let direction = column.primarySortDirection ?? "ASC"; - if ( - isNonEmptyArray(data.sorters) && - ((!!data.sorters[0].field && data.sorters[0].field == (field || data.sortField)) || - (!!value && data.sorters[0].value == value)) - ) { + // the column matches the active sorter only if both the field and the value + // selector are the same; two columns may sort by the same field through + // different value selectors and represent different sorts + if (isNonEmptyArray(data.sorters) && data.sorters[0].field == field && data.sorters[0].value === value) { if (data.sorters[0].direction == "ASC" && (!this.clearableSort || direction == "ASC")) direction = "DESC"; else if (data.sorters[0].direction == "DESC" && (!this.clearableSort || direction == "DESC")) direction = "ASC"; From 6195f507b0a1ca32dc0822763258b6e25e35cdac Mon Sep 17 00:00:00 2001 From: Nebojsa Date: Thu, 23 Jul 2026 13:09:41 +0200 Subject: [PATCH 2/2] docs(Grid): document value and sortValue column properties --- homedocs/src/pages/docs/tables/grid.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homedocs/src/pages/docs/tables/grid.mdx b/homedocs/src/pages/docs/tables/grid.mdx index e28fc5018..60b02134b 100644 --- a/homedocs/src/pages/docs/tables/grid.mdx +++ b/homedocs/src/pages/docs/tables/grid.mdx @@ -359,8 +359,10 @@ See also: [Row Drag and Drop](/docs/tables/row-drag-and-drop) | `header` | `string \| object` | Text or configuration object for the column header. | | `format` | `string` | Format string for cell values. | | `align` | `string` | Column alignment: `left`, `right`, or `center`. | +| `value` | `selector` | Selector (binding, template, expression or computable) for the displayed cell value. Use it for computed values or columns without a direct record field. | | `sortable` | `boolean` | Set to `true` to make the column sortable. | | `sortField` | `string` | Alternative field used for sorting. | +| `sortValue` | `selector` | Selector used for sorting instead of the field or the displayed value. Takes precedence over all other sort sources. | | `primarySortDirection` | `string` | Initial sort direction on first click: `ASC` or `DESC`. | | `aggregate` | `string` | Aggregate function: `sum`, `count`, `distinct`, `avg`. | | `aggregateField` | `string` | Field used for aggregation if different from `field`. |