Skip to content

Commit 8611978

Browse files
authored
Merge pull request #455 from plotly/copilot/provide-dynamic-columns-detail-grid
Add dynamic `detailCellRendererParams` support for Master/Detail detail-grid columns
2 parents 0eab401 + 87b73b4 commit 8611978

5 files changed

Lines changed: 206 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source D
88

99
### Added
1010
- [#453](https://github.com/plotly/dash-ag-grid/pull/453) Test for changelog entry
11+
- [#455](https://github.com/plotly/dash-ag-grid/pull/455) Added support for dynamic `detailCellRendererParams` in Master/Detail, including dynamic detail-grid column definitions.
1112

1213
### Changed
1314
- [#452](https://github.com/plotly/dash-ag-grid/pull/452)

docs/examples/enterprise/master_detail.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
{"headerName": "Pop. (Metro area)", "field": "population_metro"},
2727
]
2828

29+
detailColumnDefsSimple = [
30+
{"headerName": "City", "field": "city"},
31+
{"headerName": "Pop. (City proper)", "field": "population_city"},
32+
]
33+
2934
rowData = [
3035
{
3136
"country": "China",
@@ -164,6 +169,31 @@
164169
),
165170
body=True,
166171
),
172+
html.Hr(),
173+
dbc.Card(
174+
dcc.Markdown(
175+
"Use a JavaScript function in `detailCellRendererParams` to dynamically define detail columns per expanded row."
176+
),
177+
body=True,
178+
),
179+
dbc.Card(
180+
dag.AgGrid(
181+
id="master-detail-table-dynamic-columns",
182+
columnDefs=masterColumnDefs,
183+
rowData=rowData,
184+
columnSize="sizeToFit",
185+
enableEnterpriseModules=True,
186+
masterDetail=True,
187+
detailCellRendererParams={
188+
"function": """params.data.region === "Asia"
189+
? {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}
190+
: {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}"""
191+
% (detailColumnDefsSimple, detailColumnDefs)
192+
},
193+
dashGridOptions={"detailRowAutoHeight": True},
194+
),
195+
body=True,
196+
),
167197
]
168198
)
169199

src/lib/components/AgGrid.react.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -600,22 +600,30 @@ DashAgGrid.propTypes = {
600600
* Specifies the params to be used by the default detail Cell Renderer. See Detail
601601
* Grids.
602602
*/
603-
detailCellRendererParams: PropTypes.shape({
604-
/**
605-
* Grid options for detail grid in master-detail view.
606-
*/
607-
detailGridOptions: PropTypes.any,
603+
detailCellRendererParams: PropTypes.oneOfType([
604+
PropTypes.shape({
605+
/**
606+
* Grid options for detail grid in master-detail view.
607+
*/
608+
detailGridOptions: PropTypes.any,
608609

609-
/**
610-
* Column name where detail grid data is located in main dataset, for master-detail view.
611-
*/
612-
detailColName: PropTypes.string,
610+
/**
611+
* Column name where detail grid data is located in main dataset, for master-detail view.
612+
*/
613+
detailColName: PropTypes.string,
613614

614-
/**
615-
* Default: true. If true, suppresses the Dash callback in favor of using the data embedded in rowData at the given detailColName.
616-
*/
617-
suppressCallback: PropTypes.bool,
618-
}),
615+
/**
616+
* Default: true. If true, suppresses the Dash callback in favor of using the data embedded in rowData at the given detailColName.
617+
*/
618+
suppressCallback: PropTypes.bool,
619+
}),
620+
PropTypes.shape({
621+
/**
622+
* JavaScript function that receives detail row params and returns detailCellRendererParams.
623+
*/
624+
function: PropTypes.string,
625+
}),
626+
]),
619627

620628
/**
621629
* The style to give a particular row. See Row Style.

src/lib/fragments/AgGrid.react.js

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,33 @@ export function DashAgGrid(props) {
588588
const convertOneRef = useRef();
589589
const convertAllPropsRef = useRef();
590590

591+
const normalizeDetailCellRendererParams = useCallback(
592+
(value) => {
593+
if (!value || typeof value !== 'object') {
594+
return value;
595+
}
596+
597+
let adjustedVal = value;
598+
if ('suppressCallback' in value) {
599+
adjustedVal = {
600+
...adjustedVal,
601+
getDetailRowData: value.suppressCallback
602+
? suppressGetDetail(value.detailColName)
603+
: callbackGetDetail,
604+
};
605+
}
606+
if ('detailGridOptions' in value) {
607+
adjustedVal = assocPath(
608+
['detailGridOptions', 'components'],
609+
components,
610+
adjustedVal
611+
);
612+
}
613+
return convertAllPropsRef.current(adjustedVal);
614+
},
615+
[suppressGetDetail, callbackGetDetail, components]
616+
);
617+
591618
const convertOne = useCallback(
592619
(value, target) => {
593620
if (value) {
@@ -617,29 +644,32 @@ export function DashAgGrid(props) {
617644
}, value);
618645
}
619646
if (GRID_NESTED_FUNCTIONS[target]) {
620-
let adjustedVal = value;
621647
if (
622648
target === 'rowSelection' &&
623649
typeof value === 'string'
624650
) {
625651
// to still support rowSelection='single' | 'multiple' deprecated in v32.3.4
626652
return value;
627653
}
628-
if ('suppressCallback' in value) {
629-
adjustedVal = {
630-
...adjustedVal,
631-
getDetailRowData: value.suppressCallback
632-
? suppressGetDetail(value.detailColName)
633-
: callbackGetDetail,
634-
};
635-
}
636-
if ('detailGridOptions' in value) {
637-
adjustedVal = assocPath(
638-
['detailGridOptions', 'components'],
639-
components,
640-
adjustedVal
641-
);
654+
if (target === 'detailCellRendererParams') {
655+
if (has('function', value)) {
656+
const dynamicDetailParams =
657+
convertMaybeFunction(value);
658+
if (typeof dynamicDetailParams === 'function') {
659+
return (params) =>
660+
normalizeDetailCellRendererParams(
661+
dynamicDetailParams(params)
662+
);
663+
}
664+
return normalizeDetailCellRendererParams(
665+
dynamicDetailParams
666+
);
667+
}
642668
}
669+
const adjustedVal =
670+
target === 'detailCellRendererParams'
671+
? normalizeDetailCellRendererParams(value)
672+
: value;
643673
return convertAllPropsRef.current(adjustedVal);
644674
}
645675
if (GRID_DANGEROUS_FUNCTIONS[target]) {
@@ -675,13 +705,14 @@ export function DashAgGrid(props) {
675705
[
676706
convertCol,
677707
convertMaybeFunctionNoParams,
708+
convertMaybeFunction,
709+
normalizeDetailCellRendererParams,
678710
suppressGetDetail,
679711
callbackGetDetail,
680712
components,
681713
convertAllPropsRef.current,
682714
convertFunction,
683715
handleDynamicStyle,
684-
convertMaybeFunction,
685716
]
686717
);
687718

tests/test_recursive_functions.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_rf001_recursive_functions(dash_duo):
6969
{
7070
"city": "Shanghai",
7171
"population_city": 24870895,
72-
"population_metro": "NA",
72+
"population_metro": 0,
7373
},
7474
{
7575
"city": "Beijing",
@@ -268,6 +268,111 @@ def test_rf001_recursive_functions(dash_duo):
268268
).get_attribute("style")
269269

270270

271+
def test_rf003_master_detail_dynamic_columns(dash_duo):
272+
app = Dash(__name__)
273+
masterColumnDefs = [
274+
{
275+
"headerName": "Country",
276+
"field": "country",
277+
"cellRenderer": "agGroupCellRenderer",
278+
},
279+
{"headerName": "Region", "field": "region"},
280+
]
281+
282+
detailColumnDefsSimple = [
283+
{"headerName": "City", "field": "city"},
284+
{"headerName": "Pop. (City proper)", "field": "population_city"},
285+
]
286+
detailColumnDefs = detailColumnDefsSimple + [
287+
{"headerName": "Pop. (Metro area)", "field": "population_metro"},
288+
]
289+
290+
rowData = [
291+
{
292+
"country": "China",
293+
"region": "Asia",
294+
"cities": [
295+
{
296+
"city": "Shanghai",
297+
"population_city": 24870895,
298+
"population_metro": 0,
299+
},
300+
],
301+
},
302+
{
303+
"country": "United States",
304+
"region": "Americas",
305+
"cities": [
306+
{
307+
"city": "New York",
308+
"population_city": 8398748,
309+
"population_metro": 19303808,
310+
},
311+
],
312+
},
313+
]
314+
315+
app.layout = html.Div(
316+
[
317+
dag.AgGrid(
318+
id="grid",
319+
columnDefs=masterColumnDefs,
320+
rowData=rowData,
321+
columnSize="sizeToFit",
322+
enableEnterpriseModules=True,
323+
masterDetail=True,
324+
detailCellRendererParams={
325+
"function": """params.data.region === "Asia"
326+
? {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}
327+
: {detailGridOptions: {columnDefs: %s}, detailColName: "cities", suppressCallback: true}"""
328+
% (detailColumnDefsSimple, detailColumnDefs)
329+
},
330+
dashGridOptions={"detailRowAutoHeight": True},
331+
)
332+
]
333+
)
334+
335+
dash_duo.start_server(app)
336+
337+
grid = utils.Grid(dash_duo, "grid")
338+
grid.wait_for_cell_text(0, 0, "China")
339+
340+
grid.get_cell_expandable(0, 0).click()
341+
until(
342+
lambda: [
343+
e.text
344+
for e in dash_duo.find_elements(
345+
'#grid .ag-details-grid [aria-rowindex="1"] .ag-header-cell-text'
346+
)
347+
]
348+
== ["City", "Pop. (City proper)"],
349+
timeout=3,
350+
)
351+
dash_duo.wait_for_text_to_equal(
352+
'#grid .ag-details-grid [row-index="0"] [aria-colindex="2"]', "24870895"
353+
)
354+
355+
grid.get_cell_collapsable(0, 0).click()
356+
until(
357+
lambda: len(dash_duo.find_elements("#grid .ag-details-grid")) == 0,
358+
timeout=3,
359+
)
360+
grid.get_cell_expandable(1, 0).click()
361+
until(
362+
lambda: [
363+
e.text
364+
for e in dash_duo.find_elements(
365+
'#grid .ag-details-grid [aria-rowindex="1"] .ag-header-cell-text'
366+
)
367+
]
368+
== ["City", "Pop. (City proper)", "Pop. (Metro area)"],
369+
timeout=3,
370+
)
371+
dash_duo.wait_for_text_to_equal(
372+
'#grid .ag-details-grid [row-index="0"] [aria-colindex="3"]', "19303808"
373+
)
374+
375+
271376
def test_rf002_recursive_functions_server(dash_duo):
272377
app = Dash(__name__)
273378
masterColumnDefs = [

0 commit comments

Comments
 (0)