diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea4fe6a..c329e90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 12f0b956..36f18345 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -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 && @@ -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 } diff --git a/src/lib/utils/propCategories.js b/src/lib/utils/propCategories.js index 253fe6d3..89852f10 100644 --- a/src/lib/utils/propCategories.js +++ b/src/lib/utils/propCategories.js @@ -298,6 +298,7 @@ export const COLUMN_NESTED_OR_OBJ_OF_FUNCTIONS = { export const COLUMN_ARRAY_NESTED_FUNCTIONS = { children: 1, filterOptions: 1, + filters: 1, }; /** diff --git a/tests/assets/dashAgGridFunctions.js b/tests/assets/dashAgGridFunctions.js index eacb3e65..76d399e0 100644 --- a/tests/assets/dashAgGridFunctions.js +++ b/tests/assets/dashAgGridFunctions.js @@ -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 @@ -502,4 +537,5 @@ dagfuncs.TestEvent = (params, setEventData) => { dagfuncs.testToyota = (params) => { return params.data.make == 'Toyota' ? {'color': 'blue'} : {} -} \ No newline at end of file +} + diff --git a/tests/test_column_state.py b/tests/test_column_state.py index 2b5b45cc..529f0541 100644 --- a/tests/test_column_state.py +++ b/tests/test_column_state.py @@ -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 @@ -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 diff --git a/tests/test_custom_filter.py b/tests/test_custom_filter.py index b0e6c545..f7bc73be 100644 --- a/tests/test_custom_filter.py +++ b/tests/test_custom_filter.py @@ -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") \ No newline at end of file