Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ const gridOptions = {
{
field: 'totalBigInt',
cellDataType: 'bigint',
// BigInt Filter is used by default in Community version for bigInt columns
filter: true,
},
{
field: 'ledgerId',
// Explicitly configure column to use the BigInt Filter
filter: 'agBigIntColumnFilter',
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ The example below demonstrates filtering using Simple Column Filters, note the f

{% gridExampleRunner title="Server-Side Filtering" name="infinite-simple" /%}

## Date Filters

For more details on Date Filter model properties and available options see [Date Filter](./filter-date/) and [Filter Model](./filter-date/#filter-model).

### Date Range Filters

When using a Date Filter with built-in date ranges (for example, Today or Last 7 Days), the Grid State model and the SSRM request model are identical. Both use the preset `type` and leave `dateFrom` and `dateTo` as `undefined`. Your server (or custom filtering code) must interpret the preset and apply the equivalent date range logic using its own time zone and locale rules.

{% interfaceDocumentation interfaceName="PresetDateRangeFilterModel" config={"description":""} /%}

## Set Filtering

Filtering using the [Set Filter](./filter-set/) has a few differences to filtering with Simple Filters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ const gridOptions: GridOptions<IOlympicData> = {
align: 'left',
statusPanelParams: {
valueFormatter: (params: IStatusPanelValueFormatterParams) => {
const { value } = params;
if (value > 1000) {
const { value, bigintValue } = params;
if (bigintValue != null) {
return bigintValue.toString();
}
if (typeof value === 'number' && value > 1000) {
return (value / 1000).toFixed(1) + ' K';
}
return String(value);
Expand Down
26 changes: 23 additions & 3 deletions packages/ag-grid-community/src/filter/provided/date/iDateFilter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IFilterParams } from '../../../interfaces/iFilter';
import type { IScalarFilterParams } from '../iScalarFilter';
import type { ISimpleFilterModel } from '../iSimpleFilter';
import type { ISimpleFilterModel, ISimpleFilterModelPresetType } from '../iSimpleFilter';

// The date filter model takes strings, although the filter actually works with dates. This is because a Date object
// won't convert easily to JSON. When the model is used for doing the filtering, it's converted to a Date object.
Expand All @@ -14,12 +14,32 @@ export interface DateFilterModel extends ISimpleFilterModel {
* If `useIsoSeparator = true`, the format is instead `YYYY-MM-DDThh:mm:ss`.
* Custom filters can have no values (hence both are optional). Range filter has two values (from and to).
*/
dateFrom: string | null;
dateFrom: string | null | undefined;
/**
* Range filter `to` date value.
*/
dateTo: string | null;
dateTo: string | null | undefined;
}

/**
* Date filter model used when a built-in preset range (for example, Today or Last 7 Days) is selected.
* `dateFrom` and `dateTo` are always `undefined` and the `type` carries the preset key.
*/
export interface PresetDateRangeFilterModel extends DateFilterModel {
/**
* Preset range type (for example, `today` or `last7Days`).
*/
type: ISimpleFilterModelPresetType;
/**
* Preset range does not provide an explicit `from` date.
*/
dateFrom: undefined;
/**
* Preset range does not provide an explicit `to` date.
*/
dateTo: undefined;
}

/**
* Parameters provided by the grid to the `init` method of a `DateFilter`.
* Do not use in `colDef.filterParams` - see `IDateFilterParams` instead.
Expand Down
2 changes: 2 additions & 0 deletions packages/ag-grid-community/src/interfaces/iStatusPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface StatusPanelDef {
export interface IStatusPanelValueFormatterParams<TData = any, TContext = any> extends AgGridCommon<TData, TContext> {
/* The value of the current Status Bar Panel */
value: number | null;
/* The bigint value of the current Status Bar Panel (when applicable) */
bigintValue?: bigint;
/* The total row count of the grid. */
totalRows: number;
/* The name of the current Status Bar Panel */
Expand Down
1 change: 1 addition & 0 deletions packages/ag-grid-community/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export {
DateFilterParams,
IDateComparatorFunc,
IDateFilterParams,
PresetDateRangeFilterModel,
} from './filter/provided/date/iDateFilter';
export {
IProvidedFilter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ export class ValueService extends BeanStub implements NamedBean {

this.valueCache?.onDataChanged();

const savedValue = this.getValue(column, rowNode, 'edit');
const savedValue = this.getValue(column, rowNode, 'data');

this.dispatchCellValueChangedEvent(rowNode, params, savedValue, eventSource);
if ((rowNode as RowNode).pinnedSibling) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1049,12 +1049,12 @@ export class LazyCache extends BeanStub {
/**
* Client side sorting
*/
public clientSideSortRows() {
public clientSideSortRows(): boolean {
const sortOptions = this.sortSvc?.getSortOptions() ?? [];
const isAnySort = sortOptions.some((opt) => opt.sort != null);
const rowNodeSorter = this.rowNodeSorter;
if (!isAnySort || !rowNodeSorter) {
return;
return false;
}

// the node map does not need entirely recreated, only the indexes need updated.
Expand All @@ -1068,6 +1068,7 @@ export class LazyCache extends BeanStub {
const node = sortedNodes[i];
nodesMap.set({ id: node.id!, node, index: i });
}
return true;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,15 @@ export class LazyStore extends BeanStub implements IServerSideStore {

// gets called when row data updated, and no more refreshing needed
public fireRefreshFinishedEvent(): void {
const isClientSideSortingEnabled = this.gos.get('serverSideEnableClientSideSort');
const isClientSideSort = isClientSideSortingEnabled && this.cache.isStoreFullyLoaded();
if (isClientSideSort) {
// ensure refreshed rows remain in client-side sorted order
const didSort = this.cache.clientSideSortRows();
if (didSort) {
this.fireStoreUpdatedEvent();
}
}
this.eventSvc.dispatchEvent({
type: 'storeRefreshed',
route: this.parentRowNode.getRoute(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const AgNameValueElement: ElementParams = {
{ tag: 'span', ref: 'eValue', cls: 'ag-status-name-value-value' },
],
};

const MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);

export class AgNameValue extends Component {
private readonly eLabel: HTMLElement = RefPlaceholder;
private readonly eValue: HTMLElement = RefPlaceholder;
Expand All @@ -29,13 +33,25 @@ export class AgNameValue extends Component {
}

public setValue(value: number | bigint | null, totalRows: number): void {
let numericValue: number | null = null;
let bigintValue: bigint | undefined;

if (typeof value === 'bigint') {
this.eValue.textContent = value.toString();
return;
bigintValue = value;
if (value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT) {
numericValue = Number(value);
}
} else {
numericValue = value;
}

this.eValue.textContent = this.valueFormatter(
_addGridCommonParams(this.gos, { value, totalRows, key: this.key })
_addGridCommonParams(this.gos, {
value: numericValue,
bigintValue,
totalRows,
key: this.key,
})
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,13 @@ export class AggregationComp extends Component implements IStatusPanelComp {

const valueFormatter =
params.valueFormatter ??
(({ value }) => _formatNumberTwoDecimalPlacesAndCommas(value, this.getLocaleTextFunc.bind(this)));
((params) => {
const { value, bigintValue } = params;
if (bigintValue != null) {
return bigintValue.toString();
}
return _formatNumberTwoDecimalPlacesAndCommas(value, this.getLocaleTextFunc.bind(this));
});

const aggFuncNames: AggregationStatusPanelAggFunc[] = ['avg', 'count', 'min', 'max', 'sum'];
for (const key of aggFuncNames) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ function mergeRespectingChildOverrides(parent, child, pickFields = []) {
// Normal spread merge to get the correct order wipes out child overrides
// Hence the manual approach to the merge here.
Object.entries(filteredParent).forEach(([k, v]) => {
if (!merged[k]) {
const optionalKey = k.endsWith('?') ? k.slice(0, -1) : `${k}?`;
if (!merged[k] && !merged[optionalKey]) {
merged[k] = v;
}
});
Expand Down
145 changes: 145 additions & 0 deletions testing/behavioural/src/cell-editing/cell-editing-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from 'ag-grid-enterprise';

import {
GridRows,
TestGridsManager,
asyncSetTimeout,
fakeElementAttribute,
Expand Down Expand Up @@ -837,4 +838,148 @@ describe('Cell Editing Regression', () => {
expect(valueSetterCalls[1].oldValue).toBe(42);
expect(scoreCell).toHaveTextContent('0');
});

test.each(['ui', 'data'] as const)(
'onCellValueChanged receives transformed value from valueSetter via %s',
async (source) => {
// This test verifies that when a valueSetter transforms the value (e.g., to uppercase),
// the onCellValueChanged event receives the transformed value, not the original input.
const cellValueChangedEvents: Array<{ oldValue: any; newValue: any }> = [];

const api = await gridMgr.createGridAndWait(`myGrid-${source}`, {
columnDefs: [
{
field: 'athlete',
editable: true,
valueSetter: (params) => {
// Transform value to uppercase
params.data.athlete = params.newValue.toUpperCase();
return true;
},
},
{ field: 'age' },
{ field: 'country' },
],
rowData: [{ athlete: 'Michael Phelps', age: 30, country: 'USA' }],
onCellValueChanged: (event) => {
cellValueChangedEvents.push({
oldValue: event.oldValue,
newValue: event.newValue,
});
},
});

const gridDiv = getGridElement(api)! as HTMLElement;
await asyncSetTimeout(1);

if (source === 'ui') {
// Edit via UI: double-click and type
const athleteCell = getByTestId(gridDiv, agTestIdFor.cell('0', 'athlete'));
await userEvent.dblClick(athleteCell);
await asyncSetTimeout(1);

const input = await waitForInput(gridDiv, athleteCell, { popup: false });
await userEvent.clear(input);
await userEvent.type(input, 'usain bolt{Enter}');
await asyncSetTimeout(1);
} else {
// Edit via data: use rowNode.setDataValue
const rowNode = api.getDisplayedRowAtIndex(0)!;
rowNode.setDataValue('athlete', 'usain bolt');
await asyncSetTimeout(1);
}

// Verify the grid state with GridRows snapshot
const afterRows = new GridRows(api, `after ${source} edit`);
await afterRows.check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:0 athlete:"USAIN BOLT" age:30 country:"USA"
`);

// Verify the cell displays the uppercase value
const athleteCell = getByTestId(gridDiv, agTestIdFor.cell('0', 'athlete'));
expect(athleteCell).toHaveTextContent('USAIN BOLT');

// Verify the data was transformed
expect(api.getDisplayedRowAtIndex(0)?.data?.athlete).toBe('USAIN BOLT');

// Verify onCellValueChanged received the transformed (uppercase) value
expect(cellValueChangedEvents).toHaveLength(1);
expect(cellValueChangedEvents[0].oldValue).toBe('Michael Phelps');
// The newValue should be the transformed uppercase value, not the original lowercase input
expect(cellValueChangedEvents[0].newValue).toBe('USAIN BOLT');
}
);

test.each(['ui', 'data'] as const)(
'onCellValueChanged receives correct newValue with nested valueGetter via %s',
async (source) => {
// This test verifies that when using a valueGetter that reads from nested data,
// onCellValueChanged receives the correct primitive value, not an object.
const cellValueChangedEvents: Array<{ oldValue: any; newValue: any }> = [];

const api = await gridMgr.createGridAndWait(`myGrid-nested-${source}`, {
columnDefs: [
{ field: 'name' },
{
colId: 'country',
headerName: 'Country',
editable: true,
valueGetter: (params) => params.data?.person?.country,
valueSetter: (params) => {
params.data.person.country = params.newValue;
return true;
},
},
],
rowData: [{ name: 'John', person: { country: 'United States' } }],
onCellValueChanged: (event) => {
cellValueChangedEvents.push({
oldValue: event.oldValue,
newValue: event.newValue,
});
},
});

const gridDiv = getGridElement(api)! as HTMLElement;
await asyncSetTimeout(1);

if (source === 'ui') {
// Edit via UI: double-click and type
const countryCell = getByTestId(gridDiv, agTestIdFor.cell('0', 'country'));
await userEvent.dblClick(countryCell);
await asyncSetTimeout(1);

const input = await waitForInput(gridDiv, countryCell, { popup: false });
await userEvent.clear(input);
await userEvent.type(input, 'Canada{Enter}');
await asyncSetTimeout(1);
} else {
// Edit via data: use rowNode.setDataValue
const rowNode = api.getDisplayedRowAtIndex(0)!;
rowNode.setDataValue('country', 'Canada');
await asyncSetTimeout(1);
}

// Verify the grid state with GridRows snapshot
const afterRows = new GridRows(api, `after ${source} edit`);
await afterRows.check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:0 name:"John" country:"Canada"
`);

// Verify the cell displays the new value
const countryCell = getByTestId(gridDiv, agTestIdFor.cell('0', 'country'));
expect(countryCell).toHaveTextContent('Canada');

// Verify the nested data was updated
expect(api.getDisplayedRowAtIndex(0)?.data?.person?.country).toBe('Canada');

// Verify onCellValueChanged received the correct primitive value, not an object
expect(cellValueChangedEvents).toHaveLength(1);
expect(cellValueChangedEvents[0].oldValue).toBe('United States');
// The newValue should be the primitive string, not an object like {country: 'Canada'}
expect(cellValueChangedEvents[0].newValue).toBe('Canada');
}
);
});
Loading
Loading