diff --git a/CHANGELOG.md b/CHANGELOG.md index a6983a77..b978c937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source D ## [unreleased] ### Added - [#453](https://github.com/plotly/dash-ag-grid/pull/453) Test for changelog entry +- [#455](https://github.com/plotly/dash-ag-grid/pull/455) Added support for dynamic `detailCellRendererParams` in Master/Detail, including dynamic detail-grid column definitions. ### Changed - [#452](https://github.com/plotly/dash-ag-grid/pull/452) diff --git a/docs/examples/enterprise/master_detail.py b/docs/examples/enterprise/master_detail.py index c7ff68e4..c1940113 100644 --- a/docs/examples/enterprise/master_detail.py +++ b/docs/examples/enterprise/master_detail.py @@ -26,6 +26,11 @@ {"headerName": "Pop. (Metro area)", "field": "population_metro"}, ] +detailColumnDefsSimple = [ + {"headerName": "City", "field": "city"}, + {"headerName": "Pop. (City proper)", "field": "population_city"}, +] + rowData = [ { "country": "China", @@ -164,6 +169,31 @@ ), body=True, ), + html.Hr(), + dbc.Card( + dcc.Markdown( + "Use a JavaScript function in `detailCellRendererParams` to dynamically define detail columns per expanded row." + ), + body=True, + ), + dbc.Card( + dag.AgGrid( + id="master-detail-table-dynamic-columns", + columnDefs=masterColumnDefs, + rowData=rowData, + columnSize="sizeToFit", + enableEnterpriseModules=True, + masterDetail=True, + detailCellRendererParams={ + "function": """params.data.region === "Asia" + ? {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true} + : {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}""" + % (detailColumnDefsSimple, detailColumnDefs) + }, + dashGridOptions={"detailRowAutoHeight": True}, + ), + body=True, + ), ] ) diff --git a/src/lib/components/AgGrid.react.js b/src/lib/components/AgGrid.react.js index ce2e1c3a..2b287829 100644 --- a/src/lib/components/AgGrid.react.js +++ b/src/lib/components/AgGrid.react.js @@ -600,22 +600,30 @@ DashAgGrid.propTypes = { * Specifies the params to be used by the default detail Cell Renderer. See Detail * Grids. */ - detailCellRendererParams: PropTypes.shape({ - /** - * Grid options for detail grid in master-detail view. - */ - detailGridOptions: PropTypes.any, + detailCellRendererParams: PropTypes.oneOfType([ + PropTypes.shape({ + /** + * Grid options for detail grid in master-detail view. + */ + detailGridOptions: PropTypes.any, - /** - * Column name where detail grid data is located in main dataset, for master-detail view. - */ - detailColName: PropTypes.string, + /** + * Column name where detail grid data is located in main dataset, for master-detail view. + */ + detailColName: PropTypes.string, - /** - * Default: true. If true, suppresses the Dash callback in favor of using the data embedded in rowData at the given detailColName. - */ - suppressCallback: PropTypes.bool, - }), + /** + * Default: true. If true, suppresses the Dash callback in favor of using the data embedded in rowData at the given detailColName. + */ + suppressCallback: PropTypes.bool, + }), + PropTypes.shape({ + /** + * JavaScript function that receives detail row params and returns detailCellRendererParams. + */ + function: PropTypes.string, + }), + ]), /** * The style to give a particular row. See Row Style. diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 0ca51e81..78f93578 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -588,6 +588,33 @@ export function DashAgGrid(props) { const convertOneRef = useRef(); const convertAllPropsRef = useRef(); + const normalizeDetailCellRendererParams = useCallback( + (value) => { + if (!value || typeof value !== 'object') { + return value; + } + + let adjustedVal = value; + if ('suppressCallback' in value) { + adjustedVal = { + ...adjustedVal, + getDetailRowData: value.suppressCallback + ? suppressGetDetail(value.detailColName) + : callbackGetDetail, + }; + } + if ('detailGridOptions' in value) { + adjustedVal = assocPath( + ['detailGridOptions', 'components'], + components, + adjustedVal + ); + } + return convertAllPropsRef.current(adjustedVal); + }, + [suppressGetDetail, callbackGetDetail, components] + ); + const convertOne = useCallback( (value, target) => { if (value) { @@ -617,7 +644,6 @@ export function DashAgGrid(props) { }, value); } if (GRID_NESTED_FUNCTIONS[target]) { - let adjustedVal = value; if ( target === 'rowSelection' && typeof value === 'string' @@ -625,21 +651,25 @@ export function DashAgGrid(props) { // to still support rowSelection='single' | 'multiple' deprecated in v32.3.4 return value; } - if ('suppressCallback' in value) { - adjustedVal = { - ...adjustedVal, - getDetailRowData: value.suppressCallback - ? suppressGetDetail(value.detailColName) - : callbackGetDetail, - }; - } - if ('detailGridOptions' in value) { - adjustedVal = assocPath( - ['detailGridOptions', 'components'], - components, - adjustedVal - ); + if (target === 'detailCellRendererParams') { + if (has('function', value)) { + const dynamicDetailParams = + convertMaybeFunction(value); + if (typeof dynamicDetailParams === 'function') { + return (params) => + normalizeDetailCellRendererParams( + dynamicDetailParams(params) + ); + } + return normalizeDetailCellRendererParams( + dynamicDetailParams + ); + } } + const adjustedVal = + target === 'detailCellRendererParams' + ? normalizeDetailCellRendererParams(value) + : value; return convertAllPropsRef.current(adjustedVal); } if (GRID_DANGEROUS_FUNCTIONS[target]) { @@ -675,13 +705,14 @@ export function DashAgGrid(props) { [ convertCol, convertMaybeFunctionNoParams, + convertMaybeFunction, + normalizeDetailCellRendererParams, suppressGetDetail, callbackGetDetail, components, convertAllPropsRef.current, convertFunction, handleDynamicStyle, - convertMaybeFunction, ] ); diff --git a/tests/test_recursive_functions.py b/tests/test_recursive_functions.py index 0f6f811d..eb6c602d 100644 --- a/tests/test_recursive_functions.py +++ b/tests/test_recursive_functions.py @@ -69,7 +69,7 @@ def test_rf001_recursive_functions(dash_duo): { "city": "Shanghai", "population_city": 24870895, - "population_metro": "NA", + "population_metro": 0, }, { "city": "Beijing", @@ -268,6 +268,111 @@ def test_rf001_recursive_functions(dash_duo): ).get_attribute("style") +def test_rf003_master_detail_dynamic_columns(dash_duo): + app = Dash(__name__) + masterColumnDefs = [ + { + "headerName": "Country", + "field": "country", + "cellRenderer": "agGroupCellRenderer", + }, + {"headerName": "Region", "field": "region"}, + ] + + detailColumnDefsSimple = [ + {"headerName": "City", "field": "city"}, + {"headerName": "Pop. (City proper)", "field": "population_city"}, + ] + detailColumnDefs = detailColumnDefsSimple + [ + {"headerName": "Pop. (Metro area)", "field": "population_metro"}, + ] + + rowData = [ + { + "country": "China", + "region": "Asia", + "cities": [ + { + "city": "Shanghai", + "population_city": 24870895, + "population_metro": 0, + }, + ], + }, + { + "country": "United States", + "region": "Americas", + "cities": [ + { + "city": "New York", + "population_city": 8398748, + "population_metro": 19303808, + }, + ], + }, + ] + + app.layout = html.Div( + [ + dag.AgGrid( + id="grid", + columnDefs=masterColumnDefs, + rowData=rowData, + columnSize="sizeToFit", + enableEnterpriseModules=True, + masterDetail=True, + detailCellRendererParams={ + "function": """params.data.region === "Asia" + ? {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true} + : {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}""" + % (detailColumnDefsSimple, detailColumnDefs) + }, + dashGridOptions={"detailRowAutoHeight": True}, + ) + ] + ) + + dash_duo.start_server(app) + + grid = utils.Grid(dash_duo, "grid") + grid.wait_for_cell_text(0, 0, "China") + + grid.get_cell_expandable(0, 0).click() + until( + lambda: [ + e.text + for e in dash_duo.find_elements( + '#grid .ag-details-grid [aria-rowindex="1"] .ag-header-cell-text' + ) + ] + == ["City", "Pop. (City proper)"], + timeout=3, + ) + dash_duo.wait_for_text_to_equal( + '#grid .ag-details-grid [row-index="0"] [aria-colindex="2"]', "24870895" + ) + + grid.get_cell_collapsable(0, 0).click() + until( + lambda: len(dash_duo.find_elements("#grid .ag-details-grid")) == 0, + timeout=3, + ) + grid.get_cell_expandable(1, 0).click() + until( + lambda: [ + e.text + for e in dash_duo.find_elements( + '#grid .ag-details-grid [aria-rowindex="1"] .ag-header-cell-text' + ) + ] + == ["City", "Pop. (City proper)", "Pop. (Metro area)"], + timeout=3, + ) + dash_duo.wait_for_text_to_equal( + '#grid .ag-details-grid [row-index="0"] [aria-colindex="3"]', "19303808" + ) + + def test_rf002_recursive_functions_server(dash_duo): app = Dash(__name__) masterColumnDefs = [