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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,36 @@ All notable changes to `dash-ag-grid` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source Dash AG Grid repo

## [unreleased]
### Fixed
- [#408](https://github.com/plotly/dash-ag-grid/pull/408) fixed issue where the `columnState` would conflict with `columnDefs` updates
- fixes [#416] (https://github.com/plotly/dash-ag-grid/issues/416)
- fixes [#407](https://github.com/plotly/dash-ag-grid/issues/407)
- [#412](https://github.com/plotly/dash-ag-grid/issues/412) fix "Multi-Column Filter not properly recognized in filterParams"

## [32.3.2] - 2025-09-17

### Fixed
- [#403](https://github.com/plotly/dash-ag-grid/issues/403) fix "Maximum update depth exceeded" error

## [32.3.2rc1] - 2025-08-05

### Fixed
- [#394](https://github.com/plotly/dash-ag-grid/issues/394) allow `cellRenderer` column def to be a function

## [32.3.1] - 2025-08-05

### Fixed
- [#394](https://github.com/plotly/dash-ag-grid/issues/394) allow `cellRenderer` column def to be a function

## [33.3.2rc0] - 2025-07-29

### Changed
- bump to v`33.3.2` for the grid
- legacy (CSS-only) themes now require stylesheets to be loaded externally (for example, via the `external_stylesheets` kwarg to the Dash constructor). See `tests/examples/themes_legacy.py` for an example.
- dashGridOptions now accepts a `theme` string or function as per AG Grid's latest theming system. See `tests/examples/themes.py` for examples.
- defaultProps no longer used in modern React versions

## [32.3.0] - 2025-07-23

### Changed
Expand Down
14 changes: 9 additions & 5 deletions src/lib/fragments/AgGrid.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -1361,10 +1361,7 @@ export function DashAgGrid(props) {

// Handle columnState push changes
useEffect(() => {
if (
gridApi &&
(!props.loading_state || prevProps?.loading_state?.is_loading)
) {
if (gridApi && !(props.dashRenderType === 'internal')) {
const existingColumnState = gridApi.getColumnState();
const realStateChange =
props.columnState &&
Expand Down Expand Up @@ -1620,10 +1617,17 @@ const MemoizedAgGrid = React.memo(DashAgGrid, (prevProps, nextProps) => {
nextProps.selectedRows,
prevProps.selectedRows
);
const columnStateChanged = !equals(
nextProps.columnState,
prevProps.columnState
);

if (
propsHaveChanged &&
(!isInternalChange || rowDataChanged || selectedRowsChanged)
(!isInternalChange ||
rowDataChanged ||
selectedRowsChanged ||
columnStateChanged)
) {
return false; // Props changed, re-render
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/propCategories.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export const COLUMN_NESTED_OR_OBJ_OF_FUNCTIONS = {
export const COLUMN_ARRAY_NESTED_FUNCTIONS = {
children: 1,
filterOptions: 1,
filters: 1,
};

/**
Expand Down
38 changes: 37 additions & 1 deletion tests/assets/dashAgGridFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,41 @@ dagfuncs.startWith = ([filterValues], cellValue) => {
const name = cellValue ? cellValue.split(" ")[1] : ""
return name && name.toLowerCase().indexOf(filterValues.toLowerCase()) === 0
}

dagfuncs.dateFilterComparator = (filterLocalDateAtMidnight, cellValue) => {
const dateAsString = cellValue;

if (dateAsString == null) {
// Return -1 to show nulls "before" any date
return -1;
}

// The data from this CSV is in dd/mm/yyyy format
const dateParts = dateAsString.split("/");
if (dateParts.length !== 3) {
// Handle invalid format
return 0;
}

const day = Number(dateParts[0]);
const month = Number(dateParts[1]) - 1; // JS months are 0-indexed
const year = Number(dateParts[2]);
const cellDate = new Date(year, month, day);

// Check for invalid date (e.g., from "NaN")
if (isNaN(cellDate.getTime())) {
return 0;
}

// Now that both parameters are Date objects, we can compare
if (cellDate < filterLocalDateAtMidnight) {
return -1;
} else if (cellDate > filterLocalDateAtMidnight) {
return 1;
}
return 0;
};

// END test_custom_filter.py

// FOR test_quick_filter.py
Expand Down Expand Up @@ -502,4 +537,5 @@ dagfuncs.TestEvent = (params, setEventData) => {

dagfuncs.testToyota = (params) => {
return params.data.make == 'Toyota' ? {'color': 'blue'} : {}
}
}

70 changes: 69 additions & 1 deletion tests/test_column_state.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from dash import Dash, html, Output, Input, no_update, State, ctx, Patch
from dash import Dash, html, Output, Input, no_update, State, ctx, Patch, dcc
import dash_ag_grid as dag
import plotly.express as px
import json
import time
import pandas as pd

from . import utils
from dash.testing.wait import until
Expand Down Expand Up @@ -518,3 +519,70 @@ def remove_column(n):
dash_duo.find_element("#remove-column").click()
time.sleep(2) # pausing to emulate separation because user inputs
assert list(filter(lambda i: i.get("level") != "ERROR", dash_duo.get_logs())) == []

def test_toggle_column_visibility(dash_duo):
data = pd.DataFrame([
{"a": 1, "b": 2},
{"a": 3, "b": 4},
])

app = Dash(__name__)

app.layout = html.Div([
dcc.Dropdown(
id="select-columns",
value=list(data.columns),
options=[{"label": col, "value": col} for col in data.columns],
multi=True,
),
dag.AgGrid(
id="ag-grid",
style={"height": "75vh", "width": "100%"},
rowData=data.to_dict(orient="records"),
),
])

@app.callback(
Output("ag-grid", "columnDefs"),
Input("select-columns", "value"),
)
def toggle_column_visibility(selected_columns):
if not selected_columns:
return no_update
return [
{
"headerName": col_name,
"field": col_name,
"hide": col_name not in selected_columns,
}
for col_name in data.columns
]

dash_duo.start_server(app)

# Wait for grid to render
grid = utils.Grid(dash_duo, "ag-grid")

grid.wait_for_cell_text(0, 0, "1")

# Hide column 'b'
dropdown = dash_duo.find_element("#select-columns")
option_b = dash_duo.find_element('span.Select-value-icon:nth-child(1)')
option_b.click()
time.sleep(1)

# Only column 'a' should be visible
grid_headers = dash_duo.find_elements("div.ag-header-cell-label")
header_texts = [h.text for h in grid_headers]
assert "a" not in header_texts
assert "b" in header_texts

# Show both columns again
dropdown.click()
option_b = dash_duo.find_element('.Select-menu')
option_b.click()
time.sleep(1)
grid_headers = dash_duo.find_elements("div.ag-header-cell-label")
header_texts = [h.text for h in grid_headers]
assert "a" in header_texts
assert "b" in header_texts
98 changes: 98 additions & 0 deletions tests/test_custom_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,101 @@ def test_fi005_custom_filter(dash_duo):
# Test numberParser and numberFormatter
grid.set_filter(0, "$100,5")
grid.wait_for_cell_text(0, 0, "$200,00")

def test_fi006_custom_filter(dash_duo):
app = Dash(__name__)

df = pd.read_csv(
"https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [
{"field": "athlete",
"filter": "agMultiColumnFilter",
"filterParams": {
"filters": [
{"filter": "agTextColumnFilter"},
{"filter": "agSetColumnFilter"} # Example with Set Filter
]
}},
{"field": "country"},
{
"field": "date",
"filter": "agMultiColumnFilter",
"filterParams": {
"filters": [
{
"filter": "agSetColumnFilter",
'filterParams': {'excelMode': 'windows', 'buttons': ['apply', 'reset'],
}
},
{
"filter": "agDateColumnFilter",
'filterParams': {
'excelMode': 'windows',
'buttons': ['apply', 'reset'],
'comparator': {'function': 'dateFilterComparator'},
}
},
],

},
},
]


app.layout = html.Div(
[
dag.AgGrid(
id="date-filter-example",
enableEnterpriseModules=True,
columnDefs=columnDefs,
rowData=df.to_dict("records"),
defaultColDef={"flex": 1, "minWidth": 150, "floatingFilter": True},
dashGridOptions={"animateRows": False}
),
],
)

dash_duo.start_server(app)

grid = utils.Grid(dash_duo, "date-filter-example")

grid.wait_for_cell_text(0, 0, "Michael Phelps")

# Test Set Filter - click filter button on date column
dash_duo.find_element('.ag-floating-filter[aria-colindex="3"] button').click()

# Uncheck "Select All"
dash_duo.find_element('.ag-set-filter-list .ag-set-filter-item .ag-checkbox-input').click()

# Select "24/08/2008"
dash_duo.wait_for_element('.ag-set-filter-list .ag-virtual-list-item', timeout=10)
set_filter_items = dash_duo.find_elements('.ag-set-filter-list .ag-virtual-list-item')
checkboxes = dash_duo.find_elements('.ag-set-filter-list .ag-virtual-list-item .ag-checkbox-input')

for i, item in enumerate(set_filter_items):
if "24/08/2008" in item.text:
checkboxes[i].click()
break

# Apply
dash_duo.find_element('button[data-ref="applyFilterButton"]').click()
grid.wait_for_cell_text(0, 2, "24/08/2008")

# Reset
dash_duo.find_element('.ag-floating-filter[aria-colindex="3"] button').click()
dash_duo.find_element('button[data-ref="resetFilterButton"]').click()

# Test Date Filter - click filter button again
dash_duo.find_element('.ag-floating-filter[aria-colindex="3"] button').click()

# Type date
date_input = dash_duo.find_element('.ag-filter-wrapper .ag-date-filter input[class="ag-input-field-input ag-text-field-input"]')
date_input.click()
date_input.send_keys("08/24/2008")

# Apply
apply_buttons = dash_duo.find_elements('button[data-ref="applyFilterButton"]')
apply_buttons[1].click()
grid.wait_for_cell_text(0, 2, "24/08/2008")