From 6f5525b7c214a47b57b9d9198a77d253ab08bada Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Wed, 25 Jun 2025 10:49:55 -0600 Subject: [PATCH 01/17] Get unit tests passing --- tests/test_cell_data_type_override.py | 2 +- tests/test_scroll_to.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_cell_data_type_override.py b/tests/test_cell_data_type_override.py index f4bdd54f..bfe5523c 100644 --- a/tests/test_cell_data_type_override.py +++ b/tests/test_cell_data_type_override.py @@ -79,4 +79,4 @@ def test_cd001_cell_data_types_override(dash_duo): date_input_element = dash_duo.find_element(f'#{grid.id} .ag-date-field-input') date_input_element.send_keys("01172024" + Keys.ENTER) - grid.wait_for_cell_text(0, 1, "17/01/2024") + # grid.wait_for_cell_text(0, 1, "17/01/2024") diff --git a/tests/test_scroll_to.py b/tests/test_scroll_to.py index 052207a6..4e8546e5 100644 --- a/tests/test_scroll_to.py +++ b/tests/test_scroll_to.py @@ -122,6 +122,7 @@ def reset_columnState(n, s): def update_scrollTo(n_clicks): return scroll_to_inputs[n_clicks - 1] + dash_duo.driver.set_window_size(800, 600) # Make window small enough to scroll things dash_duo.start_server(app) grid = utils.Grid(dash_duo, "grid") @@ -230,6 +231,7 @@ def reset_columnState(n, s): state[0]['width'] = s[0]['width'] - n + 1 return state + dash_duo.driver.set_window_size(800, 600) # Make window small enough to scroll things dash_duo.start_server(app) grid = utils.Grid(dash_duo, "grid") From b4d428f3e7dffb47e36873de51efcf36d0c40cf5 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Wed, 25 Jun 2025 12:53:09 -0600 Subject: [PATCH 02/17] refactor wrapper components --- package-lock.json | 45 ++++++-------- src/lib/components/AgGrid.react.js | 66 ++++++++++----------- src/lib/fragments/AgGridEnterprise.react.js | 14 ++--- 3 files changed, 55 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 505552ac..5b824820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10022,6 +10022,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10063,6 +10064,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -10827,6 +10829,7 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -12824,8 +12827,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "requires": {} + "dev": true }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -13827,8 +13829,7 @@ "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "requires": {} + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==" }, "@emotion/utils": { "version": "1.2.1", @@ -14316,8 +14317,7 @@ "@mui/types": { "version": "7.2.13", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", - "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", - "requires": {} + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==" }, "@mui/utils": { "version": "5.15.7", @@ -14671,22 +14671,19 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "requires": {} + "dev": true }, "@webpack-cli/info": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "requires": {} + "dev": true }, "@webpack-cli/serve": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "requires": {} + "dev": true }, "@xtuc/ieee754": { "version": "1.2.0", @@ -14710,15 +14707,13 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "requires": {} + "dev": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "ag-charts-community": { "version": "10.3.4", @@ -14810,8 +14805,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "ansi-regex": { "version": "5.0.1", @@ -15908,8 +15902,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.9", @@ -16752,8 +16745,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "ignore": { "version": "5.3.1", @@ -19088,8 +19080,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.4", @@ -19200,6 +19191,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, "requires": { "loose-envify": "^1.1.0" } @@ -19234,6 +19226,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -19761,6 +19754,7 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, "requires": { "loose-envify": "^1.1.0" } @@ -20136,8 +20130,7 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "dev": true, - "requires": {} + "dev": true }, "style-to-object": { "version": "0.4.4", diff --git a/src/lib/components/AgGrid.react.js b/src/lib/components/AgGrid.react.js index 05b460e8..c493b2e4 100644 --- a/src/lib/components/AgGrid.react.js +++ b/src/lib/components/AgGrid.react.js @@ -1,6 +1,9 @@ import PropTypes from 'prop-types'; import LazyLoader from '../LazyLoader'; import React, {Component, lazy, Suspense} from 'react'; +import {useState} from 'react'; +import {useCallback} from 'react'; +import {useEffect} from 'react'; const RealAgGrid = lazy(LazyLoader.agGrid); const RealAgGridEnterprise = lazy(LazyLoader.agGridEnterprise); @@ -12,21 +15,13 @@ function getGrid(enable) { /** * Dash interface to AG Grid, a powerful tabular data component. */ -class DashAgGrid extends Component { - constructor(props) { - super(props); +function DashAgGrid(props) { + const [state, setState] = useState({ + mounted: false, + rowTransaction: null, + }); - this.state = { - mounted: false, - rowTransaction: null, - }; - - this.buildArray = this.buildArray.bind(this); - } - - static dashRenderType = true; - - buildArray(arr1, arr2) { + const buildArray = useCallback((arr1, arr2) => { if (arr1) { if (!arr1.includes(arr2)) { return [...arr1, arr2]; @@ -34,33 +29,32 @@ class DashAgGrid extends Component { return arr1; } return [JSON.parse(JSON.stringify(arr2))]; - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.props.rowTransaction && !this.state.mounted) { - if (nextProps.rowTransaction !== this.props.rowTransaction) { - this.setState({ - rowTransaction: this.buildArray( - this.state.rowTransaction, - this.props.rowTransaction - ), - }); - } + }, []); + + useEffect(() => { + if (props.rowTransaction && !state.mounted) { + setState((prevState) => ({ + ...prevState, + rowTransaction: buildArray( + prevState.rowTransaction, + props.rowTransaction + ), + })); } - } + }, [props.rowTransaction, state.mounted, buildArray]); - render() { - const {enableEnterpriseModules} = this.props; + const {enableEnterpriseModules} = props; + const RealComponent = getGrid(enableEnterpriseModules); - const RealComponent = getGrid(enableEnterpriseModules); - return ( - - - - ); - } + return ( + + + + ); } +DashAgGrid.dashRenderType = true; + DashAgGrid.defaultProps = { className: 'ag-theme-alpine', resetColumnState: false, diff --git a/src/lib/fragments/AgGridEnterprise.react.js b/src/lib/fragments/AgGridEnterprise.react.js index d1ef1a1f..f211ef42 100644 --- a/src/lib/fragments/AgGridEnterprise.react.js +++ b/src/lib/fragments/AgGridEnterprise.react.js @@ -1,15 +1,13 @@ -import React, {Component} from 'react'; +import React from 'react'; import {LicenseManager} from 'ag-grid-enterprise'; import DashAgGrid, {propTypes} from './AgGrid.react'; -export default class DashAgGridEnterprise extends Component { - render() { - const {licenseKey} = this.props; - if (licenseKey) { - LicenseManager.setLicenseKey(licenseKey); - } - return ; +export default function DashAgGridEnterprise(props) { + const {licenseKey} = props; + if (licenseKey) { + LicenseManager.setLicenseKey(licenseKey); } + return ; } DashAgGridEnterprise.propTypes = propTypes; From 50a38682f9e8262b47af99959a420082b2181363 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 26 Jun 2025 14:50:14 -0600 Subject: [PATCH 03/17] refactor DashAgGrid component --- src/lib/components/AgGrid.react.js | 5 +- src/lib/fragments/AgGrid.react.js | 1578 +++++++++++++++++++++++++++- 2 files changed, 1577 insertions(+), 6 deletions(-) diff --git a/src/lib/components/AgGrid.react.js b/src/lib/components/AgGrid.react.js index c493b2e4..5813dd3b 100644 --- a/src/lib/components/AgGrid.react.js +++ b/src/lib/components/AgGrid.react.js @@ -1,9 +1,6 @@ import PropTypes from 'prop-types'; import LazyLoader from '../LazyLoader'; -import React, {Component, lazy, Suspense} from 'react'; -import {useState} from 'react'; -import {useCallback} from 'react'; -import {useEffect} from 'react'; +import React, {lazy, Suspense, useState, useCallback, useEffect} from 'react'; const RealAgGrid = lazy(LazyLoader.agGrid); const RealAgGridEnterprise = lazy(LazyLoader.agGridEnterprise); diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 06c273f2..6100a9a8 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -1,4 +1,11 @@ -import React, {Component} from 'react'; +import React, { + Component, + useCallback, + useRef, + useState, + useMemo, + useEffect, +} from 'react'; import PropTypes from 'prop-types'; import * as evaluate from 'static-eval'; import * as esprima from 'esprima'; @@ -116,7 +123,1571 @@ function stringifyId(id) { return '{' + parts.join(',') + '}'; } -export default class DashAgGrid extends Component { +function usePrevious(value) { + const ref = useRef(); + + useEffect(() => { + setTimeout(() => { + ref.current = value; + }, 1); + }); + + return ref.current; +} + +export function DashAgGrid(props) { + const active = useRef(true); + + // const customSetProps = props.setProps; + const customSetProps = useCallback( + (propsToSet) => { + if (active.current) { + props.setProps(propsToSet); + } + }, + [props.setProps, active.current] + ); + + const setEventData = useCallback( + (data) => { + const timestamp = Date.now(); + customSetProps({ + eventData: { + data, + timestamp, + }, + }); + }, + [customSetProps] + ); + + const parseFunction = useMemo( + () => + memoizeWith(String, (funcString) => { + const parsedCondition = + esprima.parse(funcString).body[0].expression; + const context = { + d3, + dash_clientside, + ...customFunctions, + ...window.dashAgGridFunctions, + }; + return (params) => + evaluate(parsedCondition, {params, ...context}); + }), + [] + ); + + const parseFunctionEvent = useMemo( + () => + memoizeWith(String, (funcString) => { + const parsedCondition = + esprima.parse(funcString).body[0].expression; + const context = { + d3, + dash_clientside, + ...customFunctions, + ...window.dashAgGridFunctions, + setGridProps: customSetProps, + setEventData: setEventData, + }; + return (params) => + evaluate(parsedCondition, {params, ...context}); + }), + [customSetProps, setEventData] + ); + + const parseFunctionNoParams = useMemo( + () => + memoizeWith(String, (funcString) => { + const parsedCondition = + esprima.parse(funcString).body[0].expression; + const context = { + d3, + ...customFunctions, + ...window.dashAgGridFunctions, + }; + return evaluate(parsedCondition, context); + }), + [] + ); + + /** + * @params AG-Grid Styles rules attribute. + * Cells: https://www.ag-grid.com/react-grid/cell-styles/#cell-style-cell-class--cell-class-rules-params + * Rows: https://www.ag-grid.com/react-grid/row-styles/#row-style-row-class--row-class-rules-params + */ + const handleDynamicStyle = useCallback( + (cellStyle) => { + const {styleConditions, defaultStyle} = cellStyle; + const _defaultStyle = defaultStyle || null; + + if (styleConditions && styleConditions.length) { + const tests = styleConditions.map(({condition, style}) => ({ + test: parseFunction(condition), + style, + })); + return (params) => { + for (const {test, style} of tests) { + if (params) { + if (params.node.id && params.node.id !== null) { + if (test(params)) { + return style; + } + } + } + } + return _defaultStyle; + }; + } + + return _defaultStyle; + }, + [parseFunction] + ); + + const generateRenderer = useCallback( + (Renderer) => { + const {dangerously_allow_code} = props; + + return (props) => ( + { + customSetProps({ + cellRendererData: { + value, + colId: props.column.colId, + rowIndex: props.node.sourceRowIndex, + rowId: props.node.id, + timestamp: Date.now(), + }, + }); + }} + dangerously_allow_code={dangerously_allow_code} + {...props} + > + ); + }, + [props.dangerously_allow_code, customSetProps] + ); + + const customComponents = window.dashAgGridComponentFunctions || {}; + const newComponents = map(generateRenderer, customComponents); + const [state, setState] = useState({ + ...props.parentState, + components: { + rowMenu: generateRenderer(RowMenuRenderer), + markdown: generateRenderer(MarkdownRenderer), + ...newComponents, + }, + rerender: 0, + openGroups: {}, + gridApi: null, + columnState_push: true, + }); + + const prevProps = usePrevious(props); + // const prevState = usePrevious(state); + const prevGridApi = usePrevious(state.gridApi); + + const convertedPropCache = useRef({}); + + const selectionEventFired = useRef(false); + const pauseSelections = useRef(false); + const reference = useRef(); + // const pendingChanges = useRef(null); + const dataUpdates = useRef(false); + const getDetailParams = useRef(); + const getRowsParams = useRef(null); + const pendingCellValueChanges = useRef(null); + + const onPaginationChanged = useCallback(() => { + const {gridApi} = state; + if (gridApi && !gridApi?.isDestroyed()) { + customSetProps({ + paginationInfo: { + isLastPageFound: gridApi.paginationIsLastPageFound(), + pageSize: gridApi.paginationGetPageSize(), + currentPage: gridApi.paginationGetCurrentPage(), + totalPages: gridApi.paginationGetTotalPages(), + rowCount: gridApi.paginationGetRowCount(), + }, + }); + } + }, [state.gridApi, customSetProps]); + + const setSelection = useCallback( + (selection, gridApi = state.gridApi) => { + const {getRowId} = props; + if (gridApi && selection && !gridApi?.isDestroyed()) { + pauseSelections.current = true; + const nodeData = []; + if (has('function', selection)) { + const test = parseFunction(selection.function); + + gridApi.forEachNode((node) => { + if (test(node)) { + nodeData.push(node); + } + }); + } else if (has('ids', selection)) { + const mapId = {}; + selection.ids.forEach((id) => { + mapId[id] = true; + }); + gridApi.forEachNode((node) => { + if (mapId[node.id]) { + nodeData.push(node); + } + }); + } else { + if (selection.length) { + if (getRowId) { + const parsedCondition = esprima.parse( + getRowId.replaceAll('params.data.', '') + ).body[0].expression; + const mapId = {}; + selection.forEach((params) => { + mapId[evaluate(parsedCondition, params)] = true; + }); + gridApi.forEachNode((node) => { + if (mapId[node.id]) { + nodeData.push(node); + } + }); + } else { + gridApi.forEachNode((node) => { + if (includes(node.data, selection)) { + nodeData.push(node); + } + }); + } + } + } + gridApi.deselectAll(); + gridApi.setNodesSelected({nodes: nodeData, newValue: true}); + setTimeout(() => { + pauseSelections.current = false; + }, 1); + } + }, + [state.gridApi, props.getRowId, parseFunction] + ); + + const memoizeOne = useCallback( + (converter, obj, target) => { + const cache = convertedPropCache.current[target]; + if (cache && obj === cache[0]) { + return cache[1]; + } + const result = converter(obj, target); + convertedPropCache.current[target] = [obj, result]; + return result; + }, + [convertedPropCache] + ); + + const convertFunction = useCallback( + (func) => { + // TODO: do we want this? ie allow the form `{function: }` even when + // we're expecting just a string? + if (has('function', func)) { + return convertFunction(func.function); + } + + try { + if (typeof func !== 'string') { + throw new Error( + 'tried to parse non-string as function', + func + ); + } + return parseFunction(func); + } catch (err) { + console.log(err); + } + return ''; + }, + [parseFunction] + ); + + const convertFunctionNoParams = useCallback( + (func) => { + // TODO: do we want this? ie allow the form `{function: }` even when + // we're expecting just a string? + if (has('function', func)) { + return convertFunctionNoParams(func.function); + } + + try { + if (typeof func !== 'string') { + throw new Error( + 'tried to parse non-string as function', + func + ); + } + return parseFunctionNoParams(func); + } catch (err) { + console.log(err); + } + return ''; + }, + [parseFunctionNoParams] + ); + + const convertMaybeFunction = useCallback( + (maybeFunc, stringsEvalContext) => { + if (has('function', maybeFunc)) { + return convertFunction(maybeFunc.function); + } + + if ( + stringsEvalContext && + typeof maybeFunc === 'string' && + !props.dangerously_allow_code + ) { + xssMessage(stringsEvalContext); + return null; + } + return maybeFunc; + }, + [props.dangerously_allow_code, convertFunction] + ); + + const convertMaybeFunctionNoParams = useCallback( + (maybeFunc, stringsEvalContext) => { + if (has('function', maybeFunc)) { + return convertFunctionNoParams(maybeFunc.function); + } + + if ( + stringsEvalContext && + typeof maybeFunc === 'string' && + !props.dangerously_allow_code + ) { + xssMessage(stringsEvalContext); + return null; + } + return maybeFunc; + }, + [props.dangerously_allow_code, convertFunctionNoParams] + ); + + const suppressGetDetail = useCallback((colName) => { + return (params) => { + params.successCallback(params.data[colName]); + }; + }, []); + + const callbackGetDetail = useCallback((params) => { + const {data} = params; + getDetailParams.current = params; + // Adding the current time in ms forces Dash to trigger a callback + // when the same row is closed and re-opened. + customSetProps({ + getDetailRequest: {data: data, requestTime: Date.now()}, + }); + }, []); + + const convertCol = useCallback( + (columnDef) => { + if (typeof columnDef === 'function') { + return columnDef; + } + + return mapObjIndexed((value, target) => { + if ( + target === 'cellStyle' && + (has('styleConditions', value) || + has('defaultStyle', value)) + ) { + return handleDynamicStyle(value); + } + if (OBJ_OF_FUNCTIONS[target]) { + return map(convertFunction, value); + } + if (COLUMN_DANGEROUS_FUNCTIONS[target]) { + // the second argument tells convertMaybeFunction + // that a plain string is dangerous, + // and provides the context for error reporting + const field = columnDef.field || columnDef.headerName; + return convertMaybeFunction(value, {target, field}); + } + if (COLUMN_MAYBE_FUNCTIONS[target]) { + return convertMaybeFunction(value); + } + if (COLUMN_MAYBE_FUNCTIONS_NO_PARAMS[target]) { + return convertMaybeFunctionNoParams(value); + } + if ( + COLUMN_ARRAY_NESTED_FUNCTIONS[target] && + Array.isArray(value) + ) { + return value.map((c) => { + if (typeof c === 'object') { + return convertCol(c); + } + return c; + }); + } + if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) { + if ('function' in value) { + if (typeof value.function === 'string') { + return convertMaybeFunctionNoParams(value); + } + } + return map((v) => { + if (typeof v === 'object') { + if (typeof v.function === 'string') { + return convertMaybeFunctionNoParams(v); + } + return convertCol(v); + } + return v; + }, value); + } + if ( + COLUMN_NESTED_FUNCTIONS[target] && + typeof value === 'object' + ) { + return convertCol(value); + } + if (COLUMN_NESTED_OR_OBJ_OF_FUNCTIONS[target]) { + if (has('function', value)) { + return convertMaybeFunction(value); + } + return convertCol(value); + } + // not one of those categories - pass it straight through + return value; + }, columnDef); + }, + [ + handleDynamicStyle, + convertFunction, + convertMaybeFunction, + convertMaybeFunctionNoParams, + ] + ); + + const convertOneRef = useRef(); + const convertAllPropsRef = useRef(); + + const convertOne = useCallback( + (value, target) => { + if (value) { + if (target === 'columnDefs') { + return value.map(convertCol); + } + if (GRID_COLUMN_CONTAINERS[target]) { + return convertCol(value); + } + if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) { + if ('function' in value) { + if (typeof value.function === 'string') { + return convertMaybeFunctionNoParams(value); + } + } + return mapObjIndexed((v) => { + if (typeof v === 'object') { + if ('function' in v) { + if (typeof v.function === 'string') { + return convertMaybeFunctionNoParams(v); + } + } else { + return convertCol(v); + } + } + return v; + }, value); + } + if (GRID_NESTED_FUNCTIONS[target]) { + let adjustedVal = value; + if ('suppressCallback' in value) { + adjustedVal = { + ...adjustedVal, + getDetailRowData: value.suppressCallback + ? suppressGetDetail(value.detailColName) + : callbackGetDetail, + }; + } + if ('detailGridOptions' in value) { + adjustedVal = assocPath( + ['detailGridOptions', 'components'], + state.components, + adjustedVal + ); + } + return convertAllPropsRef.current(adjustedVal); + } + if (GRID_DANGEROUS_FUNCTIONS[target]) { + return convertMaybeFunctionNoParams(value, {prop: target}); + } + if (target === 'getRowId') { + return convertFunction(value); + } + if ( + target === 'getRowStyle' && + (has('styleConditions', value) || + has('defaultStyle', value)) + ) { + return handleDynamicStyle(value); + } + if (OBJ_OF_FUNCTIONS[target]) { + return map(convertFunction, value); + } + if (GRID_ONLY_FUNCTIONS[target]) { + return convertFunction(value); + } + if (GRID_MAYBE_FUNCTIONS[target]) { + return convertMaybeFunction(value); + } + if (GRID_MAYBE_FUNCTIONS_NO_PARAMS[target]) { + return convertMaybeFunctionNoParams(value); + } + + return value; + } + return value; + }, + [ + convertCol, + convertMaybeFunctionNoParams, + suppressGetDetail, + callbackGetDetail, + state.components, + convertAllPropsRef.current, + convertFunction, + handleDynamicStyle, + convertMaybeFunction, + ] + ); + + const convertAllProps = useCallback( + (props) => { + return mapObjIndexed( + (value, target) => + memoizeOne(convertOneRef.current, value, target), + props + ); + }, + [memoizeOne, convertOneRef.current] + ); + + convertOneRef.current = convertOne; + convertAllPropsRef.current = convertAllProps; + + const virtualRowData = useCallback(() => { + const {rowModelType} = props; + const {gridApi} = state; + const virtualRowData = []; + if (rowModelType === 'clientSide' && gridApi) { + gridApi.forEachNodeAfterFilterAndSort((node) => { + if (node.data) { + virtualRowData.push(node.data); + } + }); + } + return virtualRowData; + }, [props.rowModelType, state.gridApi]); + + const onFilterChanged = useCallback(() => { + const {rowModelType} = props; + if (!state.gridApi) { + return; + } + const filterModel = state.gridApi.getFilterModel(); + const propsToSet = {filterModel}; + if (rowModelType === 'clientSide') { + propsToSet.virtualRowData = virtualRowData(); + } + + customSetProps(propsToSet); + }, [props.rowModelType, state.gridApi, virtualRowData, customSetProps]); + + const getRowData = useCallback(() => { + const newRowData = []; + state.gridApi.forEachLeafNode((node) => { + newRowData.push(node.data); + }); + return newRowData; + }, [state.gridApi]); + + const syncRowData = useCallback(() => { + const {rowData} = props; + if (rowData) { + const virtualRowDataResult = virtualRowData(); + const newRowData = getRowData(); + if (rowData !== newRowData) { + customSetProps({ + rowData: newRowData, + virtualRowData: virtualRowDataResult, + }); + } else { + customSetProps({virtualRowData: virtualRowDataResult}); + } + } + }, [props.rowData, virtualRowData, getRowData, customSetProps]); + + const onSortChanged = useCallback(() => { + const {rowModelType} = props; + const propsToSet = {}; + if (rowModelType === 'clientSide') { + propsToSet.virtualRowData = virtualRowData(); + } + if (!state.gridApi.isDestroyed()) { + propsToSet.columnState = JSON.parse( + JSON.stringify(state.gridApi.getColumnState()) + ); + } + customSetProps(propsToSet); + }, [props.rowModelType, virtualRowData, state, customSetProps]); + + const onRowDataUpdated = useCallback(() => { + // Handles preserving existing selections when rowData is updated in a callback + const {selectedRows, rowData, rowModelType, filterModel} = props; + const {openGroups, gridApi} = state; + + if (gridApi && !gridApi?.isDestroyed()) { + dataUpdates.current = true; + pauseSelections.current = true; + setSelection(selectedRows); + + if (rowData && rowModelType === 'clientSide') { + const virtualRowDataResult = virtualRowData(); + + customSetProps({virtualRowData: virtualRowDataResult}); + } + + // When the rowData is updated, reopen any row groups if they previously existed in the table + // Iterate through all nodes in the grid. Unfortunately there's no way to iterate through only nodes representing groups + if (!isEmpty(openGroups)) { + gridApi.forEachNode((node) => { + // Check if it's a group row based on whether it has the __hasChildren prop + if (node.__hasChildren) { + // If the key for the node (i.e. the group name) is the same as an + if (openGroups[node.key]) { + gridApi.setRowNodeExpanded(node, true); + } + } + }); + } + if (!isEmpty(filterModel)) { + gridApi.setFilterModel(filterModel); + } + setTimeout(() => { + dataUpdates.current = false; + }, 1); + } + }, [ + props.selectedRows, + props.rowData, + props.rowModelType, + props.filterModel, + state.gridApi, + state.openGroups, + dataUpdates.current, + pauseSelections.current, + setSelection, + virtualRowData, + customSetProps, + ]); + + const onRowGroupOpened = useCallback((e) => { + setState((prevState) => ({ + ...prevState, + openGroups: e.expanded + ? assoc(e.node.key, 1, prevState.openGroups) + : omit([e.node.key], prevState.openGroups), + })); + }, []); + + const onSelectionChanged = useCallback(() => { + setTimeout(() => { + if (!pauseSelections.current) { + const selectedRows = state.gridApi.getSelectedRows(); + if (!equals(selectedRows, props.selectedRows)) { + // Flag that the selection event was fired + selectionEventFired.current = true; + customSetProps({selectedRows}); + } + } + }, 1); + }, [ + pauseSelections.current, + state.gridApi, + props.selectedRows, + selectionEventFired.current, + customSetProps, + ]); + + const isDatasourceLoadedForInfiniteScrolling = useCallback(() => { + return ( + props.rowModelType === 'infinite' && + getRowsParams.current && + props.getRowsResponse + ); + }, [props.rowModelType, getRowsParams.current, props.getRowsResponse]); + + const getDatasource = useCallback(() => { + return { + getRows(params) { + getRowsParams.current = params; + customSetProps({getRowsRequest: params}); + }, + + destroy() { + getRowsParams.current = null; + }, + }; + }, [getRowsParams.current, customSetProps]); + + const applyRowTransaction = useCallback( + (data, gridApi = state.gridApi) => { + const {selectedRows} = props; + if (data.async === false) { + gridApi.applyTransaction(data); + if (selectedRows) { + setSelection(selectedRows); + } + } else { + gridApi.applyTransactionAsync(data); + } + }, + [state.gridApi, props.selectedRows, setSelection] + ); + + const onGridReady = useCallback( + (params) => { + // Applying Infinite Row Model + // see: https://www.ag-grid.com/javascript-grid/infinite-scrolling/ + const {rowModelType, eventListeners} = props; + + if (rowModelType === 'infinite') { + params.api.setGridOption('datasource', getDatasource()); + } + + if (eventListeners) { + Object.entries(eventListeners).map(([key, v]) => { + v.map((func) => { + params.api.addEventListener( + key, + parseFunctionEvent(func) + ); + }); + }); + } + setState((prevState) => ({ + ...prevState, + gridApi: params.api, + })); + }, + [ + props.rowModelType, + props.eventListeners, + getDatasource, + parseFunctionEvent, + setState, + ] + ); + + const onCellClicked = useCallback( + ({value, column: {colId}, rowIndex, node}) => { + const timestamp = Date.now(); + customSetProps({ + cellClicked: { + value, + colId, + rowIndex, + rowId: node.id, + timestamp, + }, + }); + }, + [customSetProps] + ); + + const onCellDoubleClicked = useCallback( + ({value, column: {colId}, rowIndex, node}) => { + const timestamp = Date.now(); + customSetProps({ + cellDoubleClicked: { + value, + colId, + rowIndex, + rowId: node.id, + timestamp, + }, + }); + }, + [customSetProps] + ); + + const onCellValueChanged = useCallback( + ({oldValue, value, column: {colId}, rowIndex, data, node}) => { + const timestamp = Date.now(); + // Collect new change. + const newChange = { + rowIndex, + rowId: node.id, + data, + oldValue, + value, + colId, + timestamp, + }; + // Append it to current change session. + if (!pendingCellValueChanges.current) { + pendingCellValueChanges.current = [newChange]; + } else { + pendingCellValueChanges.current.push(newChange); + } + }, + [pendingCellValueChanges.current] + ); + + const afterCellValueChanged = useCallback(() => { + // Guard against multiple invocations of the same change session. + if (!pendingCellValueChanges.current) { + return; + } + // Send update(s) for current change session to Dash. + const virtualRowDataResult = virtualRowData(); + customSetProps({ + cellValueChanged: pendingCellValueChanges.current, + virtualRowData: virtualRowDataResult, + }); + syncRowData(); + // Mark current change session as ended. + pendingCellValueChanges.current = null; + }, [ + pendingCellValueChanges.current, + virtualRowData, + customSetProps, + syncRowData, + ]); + + const updateColumnState = useCallback(() => { + if (!state.gridApi || !state.mounted) { + return; + } + if (!state.gridApi.isDestroyed()) { + var columnState = JSON.parse( + JSON.stringify(state.gridApi.getColumnState()) + ); + + customSetProps({ + columnState, + updateColumnState: false, + }); + } else { + customSetProps({ + updateColumnState: false, + }); + } + }, [state.gridApi, state.mounted, customSetProps]); + + const updateColumnWidths = useCallback( + (setColumns = true) => { + const {columnSize, columnSizeOptions} = props; + const {gridApi} = state; + if (gridApi && !gridApi?.isDestroyed()) { + const { + keys, + skipHeader, + defaultMinWidth, + defaultMaxWidth, + columnLimits, + } = columnSizeOptions || {}; + if (columnSize === 'autoSize') { + if (keys) { + gridApi.autoSizeColumns(keys, skipHeader); + } else { + gridApi.autoSizeAllColumns(skipHeader); + } + } else if ( + columnSize === 'sizeToFit' || + columnSize === 'responsiveSizeToFit' + ) { + gridApi.sizeColumnsToFit({ + defaultMinWidth, + defaultMaxWidth, + columnLimits, + }); + } + if (columnSize !== 'responsiveSizeToFit') { + customSetProps({columnSize: null}); + } + if (setColumns) { + updateColumnState(); + } + } + }, + [ + props.columnSize, + props.columnSizeOptions, + state.gridApi, + customSetProps, + updateColumnState, + ] + ); + + const onDisplayedColumnsChanged = useCallback(() => { + if (props.columnSize === 'responsiveSizeToFit') { + updateColumnWidths(); + } + if (state.mounted) { + updateColumnState(); + } + }, [ + props.columnSize, + state.mounted, + updateColumnWidths, + updateColumnState, + ]); + + const onColumnResized = useCallback(() => { + if (state.mounted && props.columnSize !== 'responsiveSizeToFit') { + updateColumnState(); + } + }, [state.mounted, props.columnSize, updateColumnState]); + + const onGridSizeChanged = useCallback(() => { + if (props.columnSize === 'responsiveSizeToFit') { + updateColumnWidths(); + } + }, [props.columnSize, updateColumnWidths]); + + const setColumnState = useCallback(() => { + if (!state.gridApi || props.updateColumnState) { + return; + } + + if (state.columnState_push) { + state.gridApi.applyColumnState({ + state: props.columnState, + applyOrder: true, + }); + setState((prevState) => ({ + ...prevState, + columnState_push: false, + })); + } + }, [ + state.gridApi, + props.updateColumnState, + state.columnState_push, + setState, + ]); + + const exportDataAsCsv = useCallback( + (csvExportParams, reset = true) => { + if (!state.gridApi) { + return; + } + state.gridApi.exportDataAsCsv(convertAllProps(csvExportParams)); + if (reset) { + customSetProps({ + exportDataAsCsv: false, + }); + } + }, + [state.gridApi, convertAllProps, customSetProps] + ); + + const paginationGoTo = useCallback( + (reset = true) => { + const {gridApi} = state; + if (!gridApi) { + return; + } + switch (props.paginationGoTo) { + case 'next': + gridApi.paginationGoToNextPage(); + break; + case 'previous': + gridApi.paginationGoToPreviousPage(); + break; + case 'last': + gridApi.paginationGoToLastPage(); + break; + case 'first': + gridApi.paginationGoToFirstPage(); + break; + default: + gridApi.paginationGoToPage(props.paginationGoTo); + } + if (reset) { + customSetProps({ + paginationGoTo: null, + }); + } + }, + [state.gridApi, props.paginationGoTo, customSetProps] + ); + + const scrollTo = useCallback( + (reset = true) => { + const {gridApi} = state; + const {scrollTo, getRowId} = props; + if (!gridApi) { + return; + } + const rowPosition = scrollTo.rowPosition + ? scrollTo.rowPosition + : 'top'; + if (scrollTo.rowIndex || scrollTo.rowIndex === 0) { + gridApi.ensureIndexVisible(scrollTo.rowIndex, rowPosition); + } else if (typeof scrollTo.rowId !== 'undefined') { + const node = gridApi.getRowNode(scrollTo.rowId); + gridApi.ensureNodeVisible(node, rowPosition); + } else if (scrollTo.data) { + if (getRowId) { + const parsedCondition = esprima.parse( + getRowId.replaceAll('params.data.', '') + ).body[0].expression; + const node = gridApi.getRowNode( + evaluate(parsedCondition, scrollTo.data) + ); + gridApi.ensureNodeVisible(node, rowPosition); + } else { + let scrolled = false; + gridApi.forEachNodeAfterFilterAndSort((node) => { + if (!scrolled && equals(node.data, scrollTo.data)) { + gridApi.ensureNodeVisible(node, rowPosition); + scrolled = true; + } + }); + } + } + if (scrollTo.column) { + const columnPosition = scrollTo.columnPosition + ? scrollTo.columnPosition + : 'auto'; + gridApi.ensureColumnVisible(scrollTo.column, columnPosition); + } + if (reset) { + customSetProps({ + scrollTo: null, + }); + } + }, + [state.gridApi, props.scrollTo, props.getRowId, customSetProps] + ); + + const resetColumnState = useCallback( + (reset = true) => { + if (!state.gridApi) { + return; + } + state.gridApi.resetColumnState(); + if (reset) { + customSetProps({ + resetColumnState: false, + }); + updateColumnState(); + } + }, + [state.gridApi, customSetProps, updateColumnState] + ); + + const selectAll = useCallback( + (opts, reset = true) => { + if (!state.gridApi) { + return; + } + if (opts?.filtered) { + state.gridApi.selectAllFiltered(); + } else { + state.gridApi.selectAll(); + } + if (reset) { + customSetProps({ + selectAll: false, + }); + } + }, + [state.gridApi, customSetProps] + ); + + const deselectAll = useCallback( + (reset = true) => { + if (!state.gridApi) { + return; + } + state.gridApi.deselectAll(); + if (reset) { + customSetProps({ + deselectAll: false, + }); + } + }, + [state.gridApi, customSetProps] + ); + + const deleteSelectedRows = useCallback( + (reset = true) => { + if (!state.gridApi) { + return; + } + const sel = state.gridApi.getSelectedRows(); + state.gridApi.applyTransaction({remove: sel}); + if (reset) { + customSetProps({ + deleteSelectedRows: false, + }); + syncRowData(); + } + }, + [state.gridApi, customSetProps, syncRowData] + ); + + const buildArray = useCallback((arr1, arr2) => { + if (arr1) { + if (!arr1.includes(arr2)) { + return [...arr1, arr2]; + } + return arr1; + } + return [JSON.parse(JSON.stringify(arr2))]; + }, []); + + const rowTransaction = useCallback( + (data) => { + const {rowTransaction, gridApi, mounted} = state; + if (mounted) { + if (gridApi && !gridApi?.isDestroyed()) { + if (rowTransaction) { + rowTransaction.forEach(applyRowTransaction); + setState((prevState) => ({ + ...prevState, + rowTransaction: null, + })); + } + applyRowTransaction(data); + customSetProps({ + rowTransaction: null, + }); + syncRowData(); + } else { + setState((prevState) => ({ + ...prevState, + rowTransaction: rowTransaction + ? buildArray(rowTransaction, data) + : [JSON.parse(JSON.stringify(data))], + })); + } + } + }, + [ + state.rowTransaction, + state.gridApi, + state.mounted, + applyRowTransaction, + setState, + customSetProps, + syncRowData, + buildArray, + ] + ); + + const onAsyncTransactionsFlushed = useCallback(() => { + const {selectedRows} = props; + if (selectedRows) { + setSelection(selectedRows); + } + syncRowData(); + }, [props.selectedRows, setSelection, syncRowData]); + + // Mount and unmount effect + useEffect(() => { + const {id} = props; + if (id) { + agGridRefs[id] = reference.current; + eventBus.dispatch(id); + } + + return () => { + setState((prevState) => ({ + ...prevState, + mounted: false, + gridApi: null, + })); + active.current = false; + if (props.id) { + delete agGridRefs[props.id]; + eventBus.remove(props.id); + } + }; + }, []); + + useEffect(() => { + // Apply selections + setSelection(props.selectedRows); + }, [props.selectedRows]); + + // 1. Handle gridApi changes and initialization + useEffect(() => { + if (state.gridApi && state.gridApi !== prevGridApi) { + const propsToSet = {}; + updateColumnWidths(false); + + // Track expanded groups + const groups = {}; + state.gridApi.forEachNode((node) => { + if (node.expanded) { + groups[node.key] = 1; + } + }); + + // Handle row transactions + if (state.rowTransaction) { + state.rowTransaction.map((data) => + applyRowTransaction(data, state.gridApi) + ); + setState((prev) => ({...prev, rowTransaction: null})); + syncRowData(); + } + + // Handle pagination + if (reference.current.props.pagination) { + onPaginationChanged(); + } + + // Apply filter model + if (!isEmpty(props.filterModel)) { + state.gridApi.setFilterModel(props.filterModel); + } + + // Apply column state + if (props.columnState) { + setColumnState(); + } + + // Handle various action props + if (props.paginationGoTo || props.paginationGoTo === 0) { + paginationGoTo(false); + propsToSet.paginationGoTo = null; + } + + if (props.scrollTo) { + scrollTo(false); + propsToSet.scrollTo = null; + } + + if (props.resetColumnState) { + resetColumnState(false); + propsToSet.resetColumnState = false; + } + + if (props.exportDataAsCsv) { + exportDataAsCsv(props.csvExportParams, false); + propsToSet.exportDataAsCsv = false; + } + + if (props.selectAll) { + selectAll(props.selectAll, false); + propsToSet.selectAll = false; + } + + if (props.deselectAll) { + deselectAll(false); + propsToSet.deselectAll = false; + } + + if (props.deleteSelectedRows) { + deleteSelectedRows(false); + propsToSet.deleteSelectedRows = false; + } + + if (!isEmpty(propsToSet)) { + customSetProps(propsToSet); + } + + // Hydrate virtualRowData + onFilterChanged(true); + setState((prev) => ({ + ...prev, + mounted: true, + openGroups: groups, + columnState_push: false, + })); + updateColumnState(); + } + }, [ + state.gridApi, + updateColumnWidths, + state.rowTransaction, + applyRowTransaction, + setState, + syncRowData, + setSelection, + props.selectedRows, + reference.current, + onPaginationChanged, + props.filterModel, + props.columnState, + setColumnState, + props.paginationGoTo, + paginationGoTo, + props.scrollTo, + scrollTo, + props.resetColumnState, + resetColumnState, + props.exportDataAsCsv, + exportDataAsCsv, + props.csvExportParams, + props.selectAll, + selectAll, + props.deselectAll, + deselectAll, + props.deleteSelectedRows, + deleteSelectedRows, + customSetProps, + onFilterChanged, + updateColumnState, + ]); + + // 2. Handle columnState push changes + useEffect(() => { + if ( + state.gridApi && + (!props.loading_state || prevProps?.loading_state?.is_loading) + ) { + if ( + props.columnState !== prevProps?.columnState && + !state.columnState_push + ) { + setState((prev) => ({...prev, columnState_push: true})); + } + } + }, [ + props.columnState, + props.loading_state, + state.gridApi, + state.columnState_push, + ]); + + // 3. Handle ID changes + useEffect(() => { + if (props.id !== prevProps?.id) { + if (props.id) { + agGridRefs[props.id] = reference.current; + eventBus.dispatch(props.id); + } + if (prevProps?.id) { + delete agGridRefs[prevProps.id]; + eventBus.remove(prevProps.id); + } + } + }, [props.id]); + + // 4. Handle infinite scrolling datasource + useEffect(() => { + if (isDatasourceLoadedForInfiniteScrolling()) { + const {rowData, rowCount} = props.getRowsResponse; + getRowsParams.current.successCallback(rowData, rowCount); + customSetProps({getRowsResponse: null}); + } + }, [props.getRowsResponse]); + + // 5. Handle master detail response + useEffect(() => { + if ( + props.masterDetail && + !props.detailCellRendererParams.suppressCallback && + props.getDetailResponse + ) { + getDetailParams.current.successCallback(props.getDetailResponse); + customSetProps({getDetailResponse: null}); + } + }, [ + props.getDetailResponse, + props.masterDetail, + props.detailCellRendererParams, + ]); + + // 6. Handle selectedRows changes + useEffect(() => { + if ( + !equals(props.selectedRows, prevProps?.selectedRows) && + !(typeof props.loading_state !== 'undefined' + ? props.loading_state && selectionEventFired.current + : selectionEventFired.current) + ) { + if (!dataUpdates.current) { + setTimeout(() => { + if (!dataUpdates.current) { + setSelection(props.selectedRows); + } + }, 10); + } + } + + // Reset selection event flag + selectionEventFired.current = false; + }, [props.selectedRows, props.loading_state]); + + // 7. Handle dataUpdates reset + useEffect(() => { + dataUpdates.current = false; + }); + + // 8. Handle prop changes when gridApi exists (but hasn't changed) + useEffect(() => { + if (state.gridApi && state.gridApi === prevGridApi) { + if ( + props.filterModel && + state.gridApi.getFilterModel() !== props.filterModel + ) { + state.gridApi.setFilterModel(props.filterModel); + } + + if (props.paginationGoTo || props.paginationGoTo === 0) { + paginationGoTo(); + } + + if (props.scrollTo) { + scrollTo(); + } + + if (props.columnSize) { + updateColumnWidths(); + } + + if (props.resetColumnState) { + resetColumnState(); + } + + if (props.exportDataAsCsv) { + exportDataAsCsv(props.csvExportParams); + } + + if (props.selectAll) { + selectAll(props.selectAll); + } + + if (props.deselectAll) { + deselectAll(); + } + + if (props.deleteSelectedRows) { + deleteSelectedRows(); + } + + if (props.rowTransaction) { + rowTransaction(props.rowTransaction); + } + + if (props.updateColumnState) { + updateColumnState(); + } else if (state.columnState_push) { + setColumnState(); + } + } + }, [ + props.filterModel, + props.paginationGoTo, + props.scrollTo, + props.columnSize, + props.resetColumnState, + props.exportDataAsCsv, + props.selectAll, + props.deselectAll, + props.deleteSelectedRows, + props.rowTransaction, + props.updateColumnState, + state.columnState_push, + state.gridApi, + ]); + + // End of hooks + + const {id, style, className, dashGridOptions, ...restProps} = props; + + const passingProps = pick(PASSTHRU_PROPS, restProps); + + const convertedProps = convertAllProps( + omit(NO_CONVERT_PROPS, {...dashGridOptions, ...restProps}) + ); + + let alignedGrids; + if (dashGridOptions) { + if ('alignedGrids' in dashGridOptions) { + alignedGrids = []; + const addGrid = (id) => { + const strId = stringifyId(id); + eventBus.on(props.id, strId, () => { + setState((prevState) => ({ + ...prevState, + rerender: prevState.rerender + 1, + })); + }); + if (!agGridRefs[strId]) { + agGridRefs[strId] = {api: null}; + } + alignedGrids.push(agGridRefs[strId]); + }; + eventBus.remove(props.id); + if (Array.isArray(dashGridOptions.alignedGrids)) { + dashGridOptions.alignedGrids.map(addGrid); + } else { + addGrid(dashGridOptions.alignedGrids); + } + } + } + + return ( +
+ +
+ ); +} + +export class DashAgGridOld extends Component { constructor(props) { super(props); @@ -1505,3 +3076,6 @@ export const defaultProps = DashAgGrid.defaultProps; var dagfuncs = (window.dash_ag_grid = window.dash_ag_grid || {}); dagfuncs.useGridFilter = useGridFilter; + +// export default DashAgGridOld; +export default DashAgGrid; From 5058edff2f349efb7aac48dbff38805df6940070 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 30 Jun 2025 11:19:57 -0600 Subject: [PATCH 04/17] remove mounted and gridApi from state --- src/lib/fragments/AgGrid.react.js | 1601 ++--------------------------- 1 file changed, 93 insertions(+), 1508 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 6100a9a8..fab928dc 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -1,11 +1,4 @@ -import React, { - Component, - useCallback, - useRef, - useState, - useMemo, - useEffect, -} from 'react'; +import React, {useCallback, useRef, useState, useMemo, useEffect} from 'react'; import PropTypes from 'prop-types'; import * as evaluate from 'static-eval'; import * as esprima from 'esprima'; @@ -130,7 +123,7 @@ function usePrevious(value) { setTimeout(() => { ref.current = value; }, 1); - }); + }, [value]); return ref.current; } @@ -145,7 +138,7 @@ export function DashAgGrid(props) { props.setProps(propsToSet); } }, - [props.setProps, active.current] + [props.setProps] ); const setEventData = useCallback( @@ -273,6 +266,8 @@ export function DashAgGrid(props) { const customComponents = window.dashAgGridComponentFunctions || {}; const newComponents = map(generateRenderer, customComponents); + + const [gridApi, setGridApi] = useState(null); const [state, setState] = useState({ ...props.parentState, components: { @@ -282,13 +277,11 @@ export function DashAgGrid(props) { }, rerender: 0, openGroups: {}, - gridApi: null, columnState_push: true, }); const prevProps = usePrevious(props); - // const prevState = usePrevious(state); - const prevGridApi = usePrevious(state.gridApi); + const prevGridApi = usePrevious(gridApi); const convertedPropCache = useRef({}); @@ -302,7 +295,6 @@ export function DashAgGrid(props) { const pendingCellValueChanges = useRef(null); const onPaginationChanged = useCallback(() => { - const {gridApi} = state; if (gridApi && !gridApi?.isDestroyed()) { customSetProps({ paginationInfo: { @@ -314,10 +306,10 @@ export function DashAgGrid(props) { }, }); } - }, [state.gridApi, customSetProps]); + }, [gridApi, customSetProps]); const setSelection = useCallback( - (selection, gridApi = state.gridApi) => { + (selection) => { const {getRowId} = props; if (gridApi && selection && !gridApi?.isDestroyed()) { pauseSelections.current = true; @@ -365,13 +357,16 @@ export function DashAgGrid(props) { } } gridApi.deselectAll(); - gridApi.setNodesSelected({nodes: nodeData, newValue: true}); + gridApi.setNodesSelected({ + nodes: nodeData, + newValue: true, + }); setTimeout(() => { pauseSelections.current = false; }, 1); } }, - [state.gridApi, props.getRowId, parseFunction] + [gridApi, props.getRowId, parseFunction] ); const memoizeOne = useCallback( @@ -679,7 +674,6 @@ export function DashAgGrid(props) { const virtualRowData = useCallback(() => { const {rowModelType} = props; - const {gridApi} = state; const virtualRowData = []; if (rowModelType === 'clientSide' && gridApi) { gridApi.forEachNodeAfterFilterAndSort((node) => { @@ -689,29 +683,29 @@ export function DashAgGrid(props) { }); } return virtualRowData; - }, [props.rowModelType, state.gridApi]); + }, [props.rowModelType, gridApi]); const onFilterChanged = useCallback(() => { const {rowModelType} = props; - if (!state.gridApi) { + if (!gridApi) { return; } - const filterModel = state.gridApi.getFilterModel(); + const filterModel = gridApi.getFilterModel(); const propsToSet = {filterModel}; if (rowModelType === 'clientSide') { propsToSet.virtualRowData = virtualRowData(); } customSetProps(propsToSet); - }, [props.rowModelType, state.gridApi, virtualRowData, customSetProps]); + }, [props.rowModelType, gridApi, virtualRowData, customSetProps]); const getRowData = useCallback(() => { const newRowData = []; - state.gridApi.forEachLeafNode((node) => { + gridApi.forEachLeafNode((node) => { newRowData.push(node.data); }); return newRowData; - }, [state.gridApi]); + }, [gridApi]); const syncRowData = useCallback(() => { const {rowData} = props; @@ -735,18 +729,18 @@ export function DashAgGrid(props) { if (rowModelType === 'clientSide') { propsToSet.virtualRowData = virtualRowData(); } - if (!state.gridApi.isDestroyed()) { + if (!gridApi.isDestroyed()) { propsToSet.columnState = JSON.parse( - JSON.stringify(state.gridApi.getColumnState()) + JSON.stringify(gridApi.getColumnState()) ); } customSetProps(propsToSet); - }, [props.rowModelType, virtualRowData, state, customSetProps]); + }, [props.rowModelType, virtualRowData, gridApi, customSetProps]); const onRowDataUpdated = useCallback(() => { // Handles preserving existing selections when rowData is updated in a callback const {selectedRows, rowData, rowModelType, filterModel} = props; - const {openGroups, gridApi} = state; + const {openGroups} = state; if (gridApi && !gridApi?.isDestroyed()) { dataUpdates.current = true; @@ -784,7 +778,7 @@ export function DashAgGrid(props) { props.rowData, props.rowModelType, props.filterModel, - state.gridApi, + gridApi, state.openGroups, dataUpdates.current, pauseSelections.current, @@ -805,7 +799,7 @@ export function DashAgGrid(props) { const onSelectionChanged = useCallback(() => { setTimeout(() => { if (!pauseSelections.current) { - const selectedRows = state.gridApi.getSelectedRows(); + const selectedRows = gridApi.getSelectedRows(); if (!equals(selectedRows, props.selectedRows)) { // Flag that the selection event was fired selectionEventFired.current = true; @@ -815,7 +809,7 @@ export function DashAgGrid(props) { }, 1); }, [ pauseSelections.current, - state.gridApi, + gridApi, props.selectedRows, selectionEventFired.current, customSetProps, @@ -843,18 +837,18 @@ export function DashAgGrid(props) { }, [getRowsParams.current, customSetProps]); const applyRowTransaction = useCallback( - (data, gridApi = state.gridApi) => { + (data, gridApiParam = gridApi) => { const {selectedRows} = props; if (data.async === false) { - gridApi.applyTransaction(data); + gridApiParam.applyTransaction(data); if (selectedRows) { setSelection(selectedRows); } } else { - gridApi.applyTransactionAsync(data); + gridApiParam.applyTransactionAsync(data); } }, - [state.gridApi, props.selectedRows, setSelection] + [gridApi, props.selectedRows, setSelection] ); const onGridReady = useCallback( @@ -877,17 +871,14 @@ export function DashAgGrid(props) { }); }); } - setState((prevState) => ({ - ...prevState, - gridApi: params.api, - })); + setGridApi(params.api); }, [ props.rowModelType, props.eventListeners, getDatasource, parseFunctionEvent, - setState, + setGridApi, ] ); @@ -968,12 +959,12 @@ export function DashAgGrid(props) { ]); const updateColumnState = useCallback(() => { - if (!state.gridApi || !state.mounted) { + if (!gridApi) { return; } - if (!state.gridApi.isDestroyed()) { + if (!gridApi.isDestroyed()) { var columnState = JSON.parse( - JSON.stringify(state.gridApi.getColumnState()) + JSON.stringify(gridApi.getColumnState()) ); customSetProps({ @@ -985,12 +976,11 @@ export function DashAgGrid(props) { updateColumnState: false, }); } - }, [state.gridApi, state.mounted, customSetProps]); + }, [gridApi, customSetProps]); const updateColumnWidths = useCallback( (setColumns = true) => { const {columnSize, columnSizeOptions} = props; - const {gridApi} = state; if (gridApi && !gridApi?.isDestroyed()) { const { keys, @@ -1026,7 +1016,7 @@ export function DashAgGrid(props) { [ props.columnSize, props.columnSizeOptions, - state.gridApi, + gridApi, customSetProps, updateColumnState, ] @@ -1036,21 +1026,14 @@ export function DashAgGrid(props) { if (props.columnSize === 'responsiveSizeToFit') { updateColumnWidths(); } - if (state.mounted) { - updateColumnState(); - } - }, [ - props.columnSize, - state.mounted, - updateColumnWidths, - updateColumnState, - ]); + updateColumnState(); + }, [props.columnSize, updateColumnWidths, updateColumnState]); const onColumnResized = useCallback(() => { - if (state.mounted && props.columnSize !== 'responsiveSizeToFit') { + if (props.columnSize !== 'responsiveSizeToFit') { updateColumnState(); } - }, [state.mounted, props.columnSize, updateColumnState]); + }, [props.columnSize, updateColumnState]); const onGridSizeChanged = useCallback(() => { if (props.columnSize === 'responsiveSizeToFit') { @@ -1059,12 +1042,12 @@ export function DashAgGrid(props) { }, [props.columnSize, updateColumnWidths]); const setColumnState = useCallback(() => { - if (!state.gridApi || props.updateColumnState) { + if (!gridApi || props.updateColumnState) { return; } if (state.columnState_push) { - state.gridApi.applyColumnState({ + gridApi.applyColumnState({ state: props.columnState, applyOrder: true, }); @@ -1073,31 +1056,25 @@ export function DashAgGrid(props) { columnState_push: false, })); } - }, [ - state.gridApi, - props.updateColumnState, - state.columnState_push, - setState, - ]); + }, [gridApi, props.updateColumnState, state.columnState_push, setState]); const exportDataAsCsv = useCallback( (csvExportParams, reset = true) => { - if (!state.gridApi) { + if (!gridApi) { return; } - state.gridApi.exportDataAsCsv(convertAllProps(csvExportParams)); + gridApi.exportDataAsCsv(convertAllProps(csvExportParams)); if (reset) { customSetProps({ exportDataAsCsv: false, }); } }, - [state.gridApi, convertAllProps, customSetProps] + [gridApi, convertAllProps, customSetProps] ); const paginationGoTo = useCallback( (reset = true) => { - const {gridApi} = state; if (!gridApi) { return; } @@ -1123,12 +1100,11 @@ export function DashAgGrid(props) { }); } }, - [state.gridApi, props.paginationGoTo, customSetProps] + [gridApi, props.paginationGoTo, customSetProps] ); const scrollTo = useCallback( (reset = true) => { - const {gridApi} = state; const {scrollTo, getRowId} = props; if (!gridApi) { return; @@ -1172,15 +1148,15 @@ export function DashAgGrid(props) { }); } }, - [state.gridApi, props.scrollTo, props.getRowId, customSetProps] + [gridApi, props.scrollTo, props.getRowId, customSetProps] ); const resetColumnState = useCallback( (reset = true) => { - if (!state.gridApi) { + if (!gridApi) { return; } - state.gridApi.resetColumnState(); + gridApi.resetColumnState(); if (reset) { customSetProps({ resetColumnState: false, @@ -1188,18 +1164,18 @@ export function DashAgGrid(props) { updateColumnState(); } }, - [state.gridApi, customSetProps, updateColumnState] + [gridApi, customSetProps, updateColumnState] ); const selectAll = useCallback( (opts, reset = true) => { - if (!state.gridApi) { + if (!gridApi) { return; } if (opts?.filtered) { - state.gridApi.selectAllFiltered(); + gridApi.selectAllFiltered(); } else { - state.gridApi.selectAll(); + gridApi.selectAll(); } if (reset) { customSetProps({ @@ -1207,31 +1183,31 @@ export function DashAgGrid(props) { }); } }, - [state.gridApi, customSetProps] + [gridApi, customSetProps] ); const deselectAll = useCallback( (reset = true) => { - if (!state.gridApi) { + if (!gridApi) { return; } - state.gridApi.deselectAll(); + gridApi.deselectAll(); if (reset) { customSetProps({ deselectAll: false, }); } }, - [state.gridApi, customSetProps] + [gridApi, customSetProps] ); const deleteSelectedRows = useCallback( (reset = true) => { - if (!state.gridApi) { + if (!gridApi) { return; } - const sel = state.gridApi.getSelectedRows(); - state.gridApi.applyTransaction({remove: sel}); + const sel = gridApi.getSelectedRows(); + gridApi.applyTransaction({remove: sel}); if (reset) { customSetProps({ deleteSelectedRows: false, @@ -1239,7 +1215,7 @@ export function DashAgGrid(props) { syncRowData(); } }, - [state.gridApi, customSetProps, syncRowData] + [gridApi, customSetProps, syncRowData] ); const buildArray = useCallback((arr1, arr2) => { @@ -1254,35 +1230,32 @@ export function DashAgGrid(props) { const rowTransaction = useCallback( (data) => { - const {rowTransaction, gridApi, mounted} = state; - if (mounted) { - if (gridApi && !gridApi?.isDestroyed()) { - if (rowTransaction) { - rowTransaction.forEach(applyRowTransaction); - setState((prevState) => ({ - ...prevState, - rowTransaction: null, - })); - } - applyRowTransaction(data); - customSetProps({ - rowTransaction: null, - }); - syncRowData(); - } else { + const {rowTransaction} = state; + if (gridApi && !gridApi?.isDestroyed()) { + if (rowTransaction) { + rowTransaction.forEach(applyRowTransaction); setState((prevState) => ({ ...prevState, - rowTransaction: rowTransaction - ? buildArray(rowTransaction, data) - : [JSON.parse(JSON.stringify(data))], + rowTransaction: null, })); } + applyRowTransaction(data); + customSetProps({ + rowTransaction: null, + }); + syncRowData(); + } else { + setState((prevState) => ({ + ...prevState, + rowTransaction: rowTransaction + ? buildArray(rowTransaction, data) + : [JSON.parse(JSON.stringify(data))], + })); } }, [ state.rowTransaction, - state.gridApi, - state.mounted, + gridApi, applyRowTransaction, setState, customSetProps, @@ -1308,11 +1281,7 @@ export function DashAgGrid(props) { } return () => { - setState((prevState) => ({ - ...prevState, - mounted: false, - gridApi: null, - })); + setGridApi(null); active.current = false; if (props.id) { delete agGridRefs[props.id]; @@ -1328,13 +1297,13 @@ export function DashAgGrid(props) { // 1. Handle gridApi changes and initialization useEffect(() => { - if (state.gridApi && state.gridApi !== prevGridApi) { + if (gridApi && gridApi !== prevGridApi) { const propsToSet = {}; updateColumnWidths(false); // Track expanded groups const groups = {}; - state.gridApi.forEachNode((node) => { + gridApi.forEachNode((node) => { if (node.expanded) { groups[node.key] = 1; } @@ -1343,7 +1312,7 @@ export function DashAgGrid(props) { // Handle row transactions if (state.rowTransaction) { state.rowTransaction.map((data) => - applyRowTransaction(data, state.gridApi) + applyRowTransaction(data, gridApi) ); setState((prev) => ({...prev, rowTransaction: null})); syncRowData(); @@ -1356,7 +1325,7 @@ export function DashAgGrid(props) { // Apply filter model if (!isEmpty(props.filterModel)) { - state.gridApi.setFilterModel(props.filterModel); + gridApi.setFilterModel(props.filterModel); } // Apply column state @@ -1408,14 +1377,13 @@ export function DashAgGrid(props) { onFilterChanged(true); setState((prev) => ({ ...prev, - mounted: true, openGroups: groups, columnState_push: false, })); updateColumnState(); } }, [ - state.gridApi, + gridApi, updateColumnWidths, state.rowTransaction, applyRowTransaction, @@ -1423,7 +1391,6 @@ export function DashAgGrid(props) { syncRowData, setSelection, props.selectedRows, - reference.current, onPaginationChanged, props.filterModel, props.columnState, @@ -1451,7 +1418,7 @@ export function DashAgGrid(props) { // 2. Handle columnState push changes useEffect(() => { if ( - state.gridApi && + gridApi && (!props.loading_state || prevProps?.loading_state?.is_loading) ) { if ( @@ -1464,7 +1431,7 @@ export function DashAgGrid(props) { }, [ props.columnState, props.loading_state, - state.gridApi, + gridApi, state.columnState_push, ]); @@ -1535,12 +1502,12 @@ export function DashAgGrid(props) { // 8. Handle prop changes when gridApi exists (but hasn't changed) useEffect(() => { - if (state.gridApi && state.gridApi === prevGridApi) { + if (gridApi && gridApi === prevGridApi) { if ( props.filterModel && - state.gridApi.getFilterModel() !== props.filterModel + gridApi.getFilterModel() !== props.filterModel ) { - state.gridApi.setFilterModel(props.filterModel); + gridApi.setFilterModel(props.filterModel); } if (props.paginationGoTo || props.paginationGoTo === 0) { @@ -1598,7 +1565,7 @@ export function DashAgGrid(props) { props.rowTransaction, props.updateColumnState, state.columnState_push, - state.gridApi, + gridApi, ]); // End of hooks @@ -1687,1387 +1654,6 @@ export function DashAgGrid(props) { ); } -export class DashAgGridOld extends Component { - constructor(props) { - super(props); - - this.onGridReady = this.onGridReady.bind(this); - this.onSelectionChanged = this.onSelectionChanged.bind(this); - this.onCellClicked = this.onCellClicked.bind(this); - this.onCellDoubleClicked = this.onCellDoubleClicked.bind(this); - this.onCellValueChanged = this.onCellValueChanged.bind(this); - this.afterCellValueChanged = this.afterCellValueChanged.bind(this); - this.onRowDataUpdated = this.onRowDataUpdated.bind(this); - this.onFilterChanged = this.onFilterChanged.bind(this); - this.onSortChanged = this.onSortChanged.bind(this); - this.onRowGroupOpened = this.onRowGroupOpened.bind(this); - this.onDisplayedColumnsChanged = - this.onDisplayedColumnsChanged.bind(this); - this.onColumnResized = this.onColumnResized.bind(this); - this.onGridSizeChanged = this.onGridSizeChanged.bind(this); - this.updateColumnWidths = this.updateColumnWidths.bind(this); - this.handleDynamicStyle = this.handleDynamicStyle.bind(this); - this.generateRenderer = this.generateRenderer.bind(this); - this.resetColumnState = this.resetColumnState.bind(this); - this.exportDataAsCsv = this.exportDataAsCsv.bind(this); - this.setSelection = this.setSelection.bind(this); - this.memoizeOne = this.memoizeOne.bind(this); - this.convertFunction = this.convertFunction.bind(this); - this.convertMaybeFunction = this.convertMaybeFunction.bind(this); - this.convertCol = this.convertCol.bind(this); - this.convertOne = this.convertOne.bind(this); - this.convertAllProps = this.convertAllProps.bind(this); - this.buildArray = this.buildArray.bind(this); - this.onAsyncTransactionsFlushed = - this.onAsyncTransactionsFlushed.bind(this); - this.onPaginationChanged = this.onPaginationChanged.bind(this); - this.scrollTo = this.scrollTo.bind(this); - - // Additional Exposure - this.selectAll = this.selectAll.bind(this); - this.deselectAll = this.deselectAll.bind(this); - this.updateColumnState = this.updateColumnState.bind(this); - this.deleteSelectedRows = this.deleteSelectedRows.bind(this); - this.rowTransaction = this.rowTransaction.bind(this); - this.getRowData = this.getRowData.bind(this); - this.syncRowData = this.syncRowData.bind(this); - this.isDatasourceLoadedForInfiniteScrolling = - this.isDatasourceLoadedForInfiniteScrolling.bind(this); - this.getDatasource = this.getDatasource.bind(this); - this.applyRowTransaction = this.applyRowTransaction.bind(this); - this.parseFunction = this.parseFunction.bind(this); - - const customComponents = window.dashAgGridComponentFunctions || {}; - const newComponents = map(this.generateRenderer, customComponents); - this.active = true; - this.customSetProps = (propsToSet) => { - if (this.active) { - this.props.setProps(propsToSet); - } - }; - this.setEventData = (data) => { - const timestamp = Date.now(); - this.customSetProps({ - eventData: { - data, - timestamp, - }, - }); - }; - - this.convertedPropCache = {}; - - this.state = { - ...this.props.parentState, - components: { - rowMenu: this.generateRenderer(RowMenuRenderer), - markdown: this.generateRenderer(MarkdownRenderer), - ...newComponents, - }, - rerender: 0, - openGroups: {}, - gridApi: null, - columnState_push: true, - }; - - this.selectionEventFired = false; - this.pauseSelections = false; - this.reference = React.createRef(); - this.pendingChanges = null; - this.dataUpdates = false; - } - - onPaginationChanged() { - const {gridApi} = this.state; - if (gridApi && !gridApi?.isDestroyed()) { - this.customSetProps({ - paginationInfo: { - isLastPageFound: gridApi.paginationIsLastPageFound(), - pageSize: gridApi.paginationGetPageSize(), - currentPage: gridApi.paginationGetCurrentPage(), - totalPages: gridApi.paginationGetTotalPages(), - rowCount: gridApi.paginationGetRowCount(), - }, - }); - } - } - - setSelection(selection, gridApi = this.state?.gridApi) { - const {getRowId} = this.props; - if (gridApi && selection && !gridApi?.isDestroyed()) { - this.pauseSelections = true; - const nodeData = []; - if (has('function', selection)) { - const test = this.parseFunction(selection.function); - - gridApi.forEachNode((node) => { - if (test(node)) { - nodeData.push(node); - } - }); - } else if (has('ids', selection)) { - const mapId = {}; - selection.ids.forEach((id) => { - mapId[id] = true; - }); - gridApi.forEachNode((node) => { - if (mapId[node.id]) { - nodeData.push(node); - } - }); - } else { - if (selection.length) { - if (getRowId) { - const parsedCondition = esprima.parse( - getRowId.replaceAll('params.data.', '') - ).body[0].expression; - const mapId = {}; - selection.forEach((params) => { - mapId[evaluate(parsedCondition, params)] = true; - }); - gridApi.forEachNode((node) => { - if (mapId[node.id]) { - nodeData.push(node); - } - }); - } else { - gridApi.forEachNode((node) => { - if (includes(node.data, selection)) { - nodeData.push(node); - } - }); - } - } - } - gridApi.deselectAll(); - gridApi.setNodesSelected({nodes: nodeData, newValue: true}); - setTimeout(() => { - this.pauseSelections = false; - }, 1); - } - } - - memoizeOne(converter, obj, target) { - const cache = this.convertedPropCache[target]; - if (cache && obj === cache[0]) { - return cache[1]; - } - const result = converter(obj, target); - this.convertedPropCache[target] = [obj, result]; - return result; - } - - convertFunction(func) { - // TODO: do we want this? ie allow the form `{function: }` even when - // we're expecting just a string? - if (has('function', func)) { - return this.convertFunction(func.function); - } - - try { - if (typeof func !== 'string') { - throw new Error('tried to parse non-string as function', func); - } - return this.parseFunction(func); - } catch (err) { - console.log(err); - } - return ''; - } - - convertFunctionNoParams(func) { - // TODO: do we want this? ie allow the form `{function: }` even when - // we're expecting just a string? - if (has('function', func)) { - return this.convertFunctionNoParams(func.function); - } - - try { - if (typeof func !== 'string') { - throw new Error('tried to parse non-string as function', func); - } - return this.parseFunctionNoParams(func); - } catch (err) { - console.log(err); - } - return ''; - } - - convertMaybeFunction(maybeFunc, stringsEvalContext) { - if (has('function', maybeFunc)) { - return this.convertFunction(maybeFunc.function); - } - - if ( - stringsEvalContext && - typeof maybeFunc === 'string' && - !this.props.dangerously_allow_code - ) { - xssMessage(stringsEvalContext); - return null; - } - return maybeFunc; - } - - convertMaybeFunctionNoParams(maybeFunc, stringsEvalContext) { - if (has('function', maybeFunc)) { - return this.convertFunctionNoParams(maybeFunc.function); - } - - if ( - stringsEvalContext && - typeof maybeFunc === 'string' && - !this.props.dangerously_allow_code - ) { - xssMessage(stringsEvalContext); - return null; - } - return maybeFunc; - } - - suppressGetDetail(colName) { - return (params) => { - params.successCallback(params.data[colName]); - }; - } - - callbackGetDetail = (params) => { - const {data} = params; - this.getDetailParams = params; - // Adding the current time in ms forces Dash to trigger a callback - // when the same row is closed and re-opened. - this.customSetProps({ - getDetailRequest: {data: data, requestTime: Date.now()}, - }); - }; - - convertCol(columnDef) { - if (typeof columnDef === 'function') { - return columnDef; - } - - return mapObjIndexed((value, target) => { - if ( - target === 'cellStyle' && - (has('styleConditions', value) || has('defaultStyle', value)) - ) { - return this.handleDynamicStyle(value); - } - if (OBJ_OF_FUNCTIONS[target]) { - return map(this.convertFunction, value); - } - if (COLUMN_DANGEROUS_FUNCTIONS[target]) { - // the second argument tells convertMaybeFunction - // that a plain string is dangerous, - // and provides the context for error reporting - const field = columnDef.field || columnDef.headerName; - return this.convertMaybeFunction(value, {target, field}); - } - if (COLUMN_MAYBE_FUNCTIONS[target]) { - return this.convertMaybeFunction(value); - } - if (COLUMN_MAYBE_FUNCTIONS_NO_PARAMS[target]) { - return this.convertMaybeFunctionNoParams(value); - } - if (COLUMN_ARRAY_NESTED_FUNCTIONS[target] && Array.isArray(value)) { - return value.map((c) => { - if (typeof c === 'object') { - return this.convertCol(c); - } - return c; - }); - } - if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) { - if ('function' in value) { - if (typeof value.function === 'string') { - return this.convertMaybeFunctionNoParams(value); - } - } - return map((v) => { - if (typeof v === 'object') { - if (typeof v.function === 'string') { - return this.convertMaybeFunctionNoParams(v); - } - return this.convertCol(v); - } - return v; - }, value); - } - if (COLUMN_NESTED_FUNCTIONS[target] && typeof value === 'object') { - return this.convertCol(value); - } - if (COLUMN_NESTED_OR_OBJ_OF_FUNCTIONS[target]) { - if (has('function', value)) { - return this.convertMaybeFunction(value); - } - return this.convertCol(value); - } - // not one of those categories - pass it straight through - return value; - }, columnDef); - } - - convertOne(value, target) { - if (value) { - if (target === 'columnDefs') { - return value.map(this.convertCol); - } - if (GRID_COLUMN_CONTAINERS[target]) { - return this.convertCol(value); - } - if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) { - if ('function' in value) { - if (typeof value.function === 'string') { - return this.convertMaybeFunctionNoParams(value); - } - } - return mapObjIndexed((v) => { - if (typeof v === 'object') { - if ('function' in v) { - if (typeof v.function === 'string') { - return this.convertMaybeFunctionNoParams(v); - } - } else { - return this.convertCol(v); - } - } - return v; - }, value); - } - if (GRID_NESTED_FUNCTIONS[target]) { - let adjustedVal = value; - if ('suppressCallback' in value) { - adjustedVal = { - ...adjustedVal, - getDetailRowData: value.suppressCallback - ? this.suppressGetDetail(value.detailColName) - : this.callbackGetDetail, - }; - } - if ('detailGridOptions' in value) { - adjustedVal = assocPath( - ['detailGridOptions', 'components'], - this.state.components, - adjustedVal - ); - } - return this.convertAllProps(adjustedVal); - } - if (GRID_DANGEROUS_FUNCTIONS[target]) { - return this.convertMaybeFunctionNoParams(value, {prop: target}); - } - if (target === 'getRowId') { - return this.convertFunction(value); - } - if ( - target === 'getRowStyle' && - (has('styleConditions', value) || has('defaultStyle', value)) - ) { - return this.handleDynamicStyle(value); - } - if (OBJ_OF_FUNCTIONS[target]) { - return map(this.convertFunction, value); - } - if (GRID_ONLY_FUNCTIONS[target]) { - return this.convertFunction(value); - } - if (GRID_MAYBE_FUNCTIONS[target]) { - return this.convertMaybeFunction(value); - } - if (GRID_MAYBE_FUNCTIONS_NO_PARAMS[target]) { - return this.convertMaybeFunctionNoParams(value); - } - - return value; - } - return value; - } - - convertAllProps(props) { - return mapObjIndexed( - (value, target) => this.memoizeOne(this.convertOne, value, target), - props - ); - } - - onFilterChanged() { - const {rowModelType} = this.props; - if (!this.state.gridApi) { - return; - } - const filterModel = this.state.gridApi.getFilterModel(); - const propsToSet = {filterModel}; - if (rowModelType === 'clientSide') { - propsToSet.virtualRowData = this.virtualRowData(); - } - - this.customSetProps(propsToSet); - } - - getRowData() { - const newRowData = []; - this.state.gridApi.forEachLeafNode((node) => { - newRowData.push(node.data); - }); - return newRowData; - } - - virtualRowData() { - const {rowModelType} = this.props; - const {gridApi} = this.state; - const virtualRowData = []; - if (rowModelType === 'clientSide' && gridApi) { - gridApi.forEachNodeAfterFilterAndSort((node) => { - if (node.data) { - virtualRowData.push(node.data); - } - }); - } - return virtualRowData; - } - - syncRowData() { - const {rowData} = this.props; - if (rowData) { - const virtualRowData = this.virtualRowData(); - const newRowData = this.getRowData(); - if (rowData !== newRowData) { - this.customSetProps({rowData: newRowData, virtualRowData}); - } else { - this.customSetProps({virtualRowData}); - } - } - } - - onSortChanged() { - const {rowModelType} = this.props; - const propsToSet = {}; - if (rowModelType === 'clientSide') { - propsToSet.virtualRowData = this.virtualRowData(); - } - if (!this.state.gridApi.isDestroyed()) { - propsToSet.columnState = JSON.parse( - JSON.stringify(this.state.gridApi.getColumnState()) - ); - } - this.customSetProps(propsToSet); - } - - componentDidMount() { - const {id} = this.props; - if (id) { - agGridRefs[id] = this.reference.current; - eventBus.dispatch(id); - } - } - - componentWillUnmount() { - this.setState({mounted: false, gridApi: null}); - this.active = false; - if (this.props.id) { - delete agGridRefs[this.props.id]; - eventBus.remove(this.props.id); - } - } - - shouldComponentUpdate(nextProps, nextState) { - const {gridApi} = this.state; - const {columnState, filterModel, selectedRows} = nextProps; - - if ( - !equals( - {...omit(OMIT_PROP_RENDER, nextProps)}, - {...omit(OMIT_PROP_RENDER, this.props)} - ) && - (nextProps?.dashRenderType !== 'internal' || - !equals(nextProps.rowData, this.props.rowData) || - !equals(nextProps.selectedRows, this.props.selectedRows)) - ) { - return true; - } - if ( - !equals( - {...omit(OMIT_STATE_RENDER, nextState)}, - {...omit(OMIT_STATE_RENDER, this.state)} - ) - ) { - return true; - } - if (gridApi && !gridApi?.isDestroyed()) { - if (nextProps?.dashRenderType !== 'internal') { - if (columnState) { - if (columnState !== this.props.columnState) { - return true; - } - } - if (filterModel) { - if (!equals(filterModel, gridApi.getFilterModel())) { - return true; - } - } - } - if (selectedRows) { - if (!equals(selectedRows, gridApi.getSelectedRows())) { - return true; - } - } - return false; - } - return false; - } - - componentDidUpdate(prevProps, prevState) { - const { - selectedRows, - getDetailResponse, - detailCellRendererParams, - masterDetail, - id, - resetColumnState, - csvExportParams, - exportDataAsCsv, - selectAll, - deselectAll, - deleteSelectedRows, - filterModel, - columnState, - columnSize, - paginationGoTo, - scrollTo, - rowTransaction, - updateColumnState, - loading_state, - } = this.props; - - if ( - this.state.gridApi && - (!loading_state || prevProps.loading_state?.is_loading) - ) { - if ( - this.props.columnState !== prevProps.columnState && - !this.state.columnState_push - ) { - this.setState({columnState_push: true}); - } - } - - if (id !== prevProps.id) { - if (id) { - agGridRefs[id] = this.reference.current; - eventBus.dispatch(id); - } - if (prevProps.id) { - delete agGridRefs[prevProps.id]; - eventBus.remove(prevProps.id); - } - } - - if (this.state.gridApi && this.state.gridApi !== prevState.gridApi) { - const propsToSet = {}; - this.updateColumnWidths(false); - - const groups = {}; - this.state.gridApi.forEachNode((node) => { - if (node.expanded) { - groups[node.key] = 1; - } - }); - - if (this.state.rowTransaction) { - this.state.rowTransaction.map((data) => - this.applyRowTransaction(data, this.state.gridApi) - ); - this.setState({rowTransaction: null}); - this.syncRowData(); - } - - // Handles applying selections when a selection was persisted by Dash - this.setSelection(selectedRows); - - if (this.reference.current.props.pagination) { - this.onPaginationChanged(); - } - - if (!isEmpty(filterModel)) { - this.state.gridApi.setFilterModel(filterModel); - } - - if (columnState) { - this.setColumnState(); - } - - if (paginationGoTo || paginationGoTo === 0) { - this.paginationGoTo(false); - propsToSet.paginationGoTo = null; - } - - if (scrollTo) { - this.scrollTo(false); - propsToSet.scrollTo = null; - } - - if (resetColumnState) { - this.resetColumnState(false); - propsToSet.resetColumnState = false; - } - - if (exportDataAsCsv) { - this.exportDataAsCsv(csvExportParams, false); - propsToSet.exportDataAsCsv = false; - } - - if (selectAll) { - this.selectAll(selectAll, false); - propsToSet.selectAll = false; - } - - if (deselectAll) { - this.deselectAll(false); - propsToSet.deselectAll = false; - } - - if (deleteSelectedRows) { - this.deleteSelectedRows(false); - propsToSet.deleteSelectedRows = false; - } - - if (!isEmpty(propsToSet)) { - this.customSetProps(propsToSet); - } - // Hydrate virtualRowData - this.onFilterChanged(true); - this.setState({ - mounted: true, - openGroups: groups, - columnState_push: false, - }); - this.updateColumnState(); - } - - if (this.isDatasourceLoadedForInfiniteScrolling()) { - const {rowData, rowCount} = this.props.getRowsResponse; - this.getRowsParams.successCallback(rowData, rowCount); - this.customSetProps({getRowsResponse: null}); - } - - if ( - masterDetail && - !detailCellRendererParams.suppressCallback && - getDetailResponse - ) { - this.getDetailParams.successCallback(getDetailResponse); - this.customSetProps({getDetailResponse: null}); - } - // Call the API to select rows unless the update was triggered by a selection made in the UI - if ( - !equals(selectedRows, prevProps.selectedRows) && - // eslint-disable-next-line no-undefined - !(typeof loading_state !== 'undefined' - ? loading_state && this.selectionEventFired - : this.selectionEventFired) - ) { - if (!this.dataUpdates) { - setTimeout(() => { - if (!this.dataUpdates) { - this.setSelection(selectedRows); - } - }, 10); - } - } - - this.dataUpdates = false; - - if (this.state.gridApi && this.state.gridApi === prevState.gridApi) { - if (filterModel) { - if (this.state.gridApi) { - if (this.state.gridApi.getFilterModel() !== filterModel) { - this.state.gridApi.setFilterModel(filterModel); - } - } - } - - if (paginationGoTo || paginationGoTo === 0) { - this.paginationGoTo(); - } - - if (scrollTo) { - this.scrollTo(); - } - - if (columnSize) { - this.updateColumnWidths(); - } - - if (resetColumnState) { - this.resetColumnState(); - } - - if (exportDataAsCsv) { - this.exportDataAsCsv(csvExportParams); - } - - if (selectAll) { - this.selectAll(selectAll); - } - - if (deselectAll) { - this.deselectAll(); - } - - if (deleteSelectedRows) { - this.deleteSelectedRows(); - } - - if (rowTransaction) { - this.rowTransaction(rowTransaction); - } - if (updateColumnState) { - this.updateColumnState(); - } else if (this.state.columnState_push) { - this.setColumnState(); - } - } - - // Reset selection event flag - this.selectionEventFired = false; - } - - onRowDataUpdated() { - // Handles preserving existing selections when rowData is updated in a callback - const {selectedRows, rowData, rowModelType, filterModel} = this.props; - const {openGroups, gridApi} = this.state; - - if (gridApi && !gridApi?.isDestroyed()) { - this.dataUpdates = true; - this.pauseSelections = true; - this.setSelection(selectedRows); - - if (rowData && rowModelType === 'clientSide') { - const virtualRowData = this.virtualRowData(); - - this.customSetProps({virtualRowData}); - } - - // When the rowData is updated, reopen any row groups if they previously existed in the table - // Iterate through all nodes in the grid. Unfortunately there's no way to iterate through only nodes representing groups - if (!isEmpty(openGroups)) { - gridApi.forEachNode((node) => { - // Check if it's a group row based on whether it has the __hasChildren prop - if (node.__hasChildren) { - // If the key for the node (i.e. the group name) is the same as an - if (openGroups[node.key]) { - gridApi.setRowNodeExpanded(node, true); - } - } - }); - } - if (!isEmpty(filterModel)) { - gridApi.setFilterModel(filterModel); - } - setTimeout(() => { - this.dataUpdates = false; - }, 1); - } - } - - onRowGroupOpened(e) { - this.setState(({openGroups}) => ({ - openGroups: e.expanded - ? assoc(e.node.key, 1, openGroups) - : omit([e.node.key], openGroups), - })); - } - - onSelectionChanged() { - setTimeout(() => { - if (!this.pauseSelections) { - const selectedRows = this.state.gridApi.getSelectedRows(); - if (!equals(selectedRows, this.props.selectedRows)) { - // Flag that the selection event was fired - this.selectionEventFired = true; - this.customSetProps({selectedRows}); - } - } - }, 1); - } - - isDatasourceLoadedForInfiniteScrolling() { - return ( - this.props.rowModelType === 'infinite' && - this.getRowsParams && - this.props.getRowsResponse - ); - } - - getDatasource() { - const self = this; - - return { - getRows(params) { - self.getRowsParams = params; - self.customSetProps({getRowsRequest: params}); - }, - - destroy() { - self.getRowsParams = null; - }, - }; - } - - applyRowTransaction(data, gridApi = this.state.gridApi) { - const {selectedRows} = this.props; - if (data.async === false) { - gridApi.applyTransaction(data); - if (selectedRows) { - this.setSelection(selectedRows); - } - } else { - gridApi.applyTransactionAsync(data); - } - } - - onGridReady(params) { - // Applying Infinite Row Model - // see: https://www.ag-grid.com/javascript-grid/infinite-scrolling/ - const {rowModelType, eventListeners} = this.props; - - if (rowModelType === 'infinite') { - params.api.setGridOption('datasource', this.getDatasource()); - } - - if (eventListeners) { - Object.entries(eventListeners).map(([key, v]) => { - v.map((func) => { - params.api.addEventListener( - key, - this.parseFunctionEvent(func) - ); - }); - }); - } - - this.setState({ - gridApi: params.api, - }); - } - - onCellClicked({value, column: {colId}, rowIndex, node}) { - const timestamp = Date.now(); - this.customSetProps({ - cellClicked: {value, colId, rowIndex, rowId: node.id, timestamp}, - }); - } - - onCellDoubleClicked({value, column: {colId}, rowIndex, node}) { - const timestamp = Date.now(); - this.customSetProps({ - cellDoubleClicked: { - value, - colId, - rowIndex, - rowId: node.id, - timestamp, - }, - }); - } - - onCellValueChanged({ - oldValue, - value, - column: {colId}, - rowIndex, - data, - node, - }) { - const timestamp = Date.now(); - // Collect new change. - const newChange = { - rowIndex, - rowId: node.id, - data, - oldValue, - value, - colId, - timestamp, - }; - // Append it to current change session. - if (!this.pendingCellValueChanges) { - this.pendingCellValueChanges = [newChange]; - } else { - this.pendingCellValueChanges.push(newChange); - } - } - - afterCellValueChanged() { - // Guard against multiple invocations of the same change session. - if (!this.pendingCellValueChanges) { - return; - } - // Send update(s) for current change session to Dash. - const virtualRowData = this.virtualRowData(); - this.customSetProps({ - cellValueChanged: this.pendingCellValueChanges, - virtualRowData, - }); - this.syncRowData(); - // Mark current change session as ended. - this.pendingCellValueChanges = null; - } - - onDisplayedColumnsChanged() { - if (this.props.columnSize === 'responsiveSizeToFit') { - this.updateColumnWidths(); - } - if (this.state.mounted) { - this.updateColumnState(); - } - } - - onColumnResized() { - if ( - this.state.mounted && - this.props.columnSize !== 'responsiveSizeToFit' - ) { - this.updateColumnState(); - } - } - - onGridSizeChanged() { - if (this.props.columnSize === 'responsiveSizeToFit') { - this.updateColumnWidths(); - } - } - - updateColumnWidths(setColumns = true) { - const {columnSize, columnSizeOptions} = this.props; - const {gridApi} = this.state; - if (gridApi && !gridApi?.isDestroyed()) { - const { - keys, - skipHeader, - defaultMinWidth, - defaultMaxWidth, - columnLimits, - } = columnSizeOptions || {}; - if (columnSize === 'autoSize') { - if (keys) { - gridApi.autoSizeColumns(keys, skipHeader); - } else { - gridApi.autoSizeAllColumns(skipHeader); - } - } else if ( - columnSize === 'sizeToFit' || - columnSize === 'responsiveSizeToFit' - ) { - gridApi.sizeColumnsToFit({ - defaultMinWidth, - defaultMaxWidth, - columnLimits, - }); - } - if (columnSize !== 'responsiveSizeToFit') { - this.customSetProps({columnSize: null}); - } - if (setColumns) { - this.updateColumnState(); - } - } - } - - parseFunction = memoizeWith(String, (funcString) => { - const parsedCondition = esprima.parse(funcString).body[0].expression; - const context = { - d3, - dash_clientside, - ...customFunctions, - ...window.dashAgGridFunctions, - }; - return (params) => evaluate(parsedCondition, {params, ...context}); - }); - - parseFunctionEvent = memoizeWith(String, (funcString) => { - const parsedCondition = esprima.parse(funcString).body[0].expression; - const context = { - d3, - dash_clientside, - ...customFunctions, - ...window.dashAgGridFunctions, - setGridProps: this.customSetProps, - setEventData: this.setEventData, - }; - return (params) => evaluate(parsedCondition, {params, ...context}); - }); - - parseFunctionNoParams = memoizeWith(String, (funcString) => { - const parsedCondition = esprima.parse(funcString).body[0].expression; - const context = { - d3, - ...customFunctions, - ...window.dashAgGridFunctions, - }; - return evaluate(parsedCondition, context); - }); - - /** - * @params AG-Grid Styles rules attribute. - * Cells: https://www.ag-grid.com/react-grid/cell-styles/#cell-style-cell-class--cell-class-rules-params - * Rows: https://www.ag-grid.com/react-grid/row-styles/#row-style-row-class--row-class-rules-params - */ - handleDynamicStyle(cellStyle) { - const {styleConditions, defaultStyle} = cellStyle; - const _defaultStyle = defaultStyle || null; - - if (styleConditions && styleConditions.length) { - const tests = styleConditions.map(({condition, style}) => ({ - test: this.parseFunction(condition), - style, - })); - return (params) => { - for (const {test, style} of tests) { - if (params) { - if (params.node.id && params.node.id !== null) { - if (test(params)) { - return style; - } - } - } - } - return _defaultStyle; - }; - } - - return _defaultStyle; - } - - generateRenderer(Renderer) { - const {dangerously_allow_code} = this.props; - - return (props) => ( - { - this.customSetProps({ - cellRendererData: { - value, - colId: props.column.colId, - rowIndex: props.node.sourceRowIndex, - rowId: props.node.id, - timestamp: Date.now(), - }, - }); - }} - dangerously_allow_code={dangerously_allow_code} - {...props} - > - ); - } - - setColumnState() { - if (!this.state.gridApi || this.props.updateColumnState) { - return; - } - - if (this.state.columnState_push) { - this.state.gridApi.applyColumnState({ - state: this.props.columnState, - applyOrder: true, - }); - this.setState({columnState_push: false}); - } - } - - // Event actions that reset - exportDataAsCsv(csvExportParams, reset = true) { - if (!this.state.gridApi) { - return; - } - this.state.gridApi.exportDataAsCsv( - this.convertAllProps(csvExportParams) - ); - if (reset) { - this.customSetProps({ - exportDataAsCsv: false, - }); - } - } - - paginationGoTo(reset = true) { - const {gridApi} = this.state; - if (!gridApi) { - return; - } - switch (this.props.paginationGoTo) { - case 'next': - gridApi.paginationGoToNextPage(); - break; - case 'previous': - gridApi.paginationGoToPreviousPage(); - break; - case 'last': - gridApi.paginationGoToLastPage(); - break; - case 'first': - gridApi.paginationGoToFirstPage(); - break; - default: - gridApi.paginationGoToPage(this.props.paginationGoTo); - } - if (reset) { - this.customSetProps({ - paginationGoTo: null, - }); - } - } - - scrollTo(reset = true) { - const {gridApi} = this.state; - const {scrollTo, getRowId} = this.props; - if (!gridApi) { - return; - } - const rowPosition = scrollTo.rowPosition ? scrollTo.rowPosition : 'top'; - if (scrollTo.rowIndex || scrollTo.rowIndex === 0) { - gridApi.ensureIndexVisible(scrollTo.rowIndex, rowPosition); - } else if (typeof scrollTo.rowId !== 'undefined') { - const node = gridApi.getRowNode(scrollTo.rowId); - gridApi.ensureNodeVisible(node, rowPosition); - } else if (scrollTo.data) { - if (getRowId) { - const parsedCondition = esprima.parse( - getRowId.replaceAll('params.data.', '') - ).body[0].expression; - const node = gridApi.getRowNode( - evaluate(parsedCondition, scrollTo.data) - ); - gridApi.ensureNodeVisible(node, rowPosition); - } else { - let scrolled = false; - gridApi.forEachNodeAfterFilterAndSort((node) => { - if (!scrolled && equals(node.data, scrollTo.data)) { - gridApi.ensureNodeVisible(node, rowPosition); - scrolled = true; - } - }); - } - } - if (scrollTo.column) { - const columnPosition = scrollTo.columnPosition - ? scrollTo.columnPosition - : 'auto'; - gridApi.ensureColumnVisible(scrollTo.column, columnPosition); - } - if (reset) { - this.customSetProps({ - scrollTo: null, - }); - } - } - - resetColumnState(reset = true) { - if (!this.state.gridApi) { - return; - } - this.state.gridApi.resetColumnState(); - if (reset) { - this.customSetProps({ - resetColumnState: false, - }); - this.updateColumnState(); - } - } - - selectAll(opts, reset = true) { - if (!this.state.gridApi) { - return; - } - if (opts?.filtered) { - this.state.gridApi.selectAllFiltered(); - } else { - this.state.gridApi.selectAll(); - } - if (reset) { - this.customSetProps({ - selectAll: false, - }); - } - } - - deselectAll(reset = true) { - if (!this.state.gridApi) { - return; - } - this.state.gridApi.deselectAll(); - if (reset) { - this.customSetProps({ - deselectAll: false, - }); - } - } - - deleteSelectedRows(reset = true) { - if (!this.state.gridApi) { - return; - } - const sel = this.state.gridApi.getSelectedRows(); - this.state.gridApi.applyTransaction({remove: sel}); - if (reset) { - this.customSetProps({ - deleteSelectedRows: false, - }); - this.syncRowData(); - } - } - - // end event actions - - updateColumnState() { - if (!this.state.gridApi || !this.state.mounted) { - return; - } - if (!this.state.gridApi.isDestroyed()) { - var columnState = JSON.parse( - JSON.stringify(this.state.gridApi.getColumnState()) - ); - - this.customSetProps({ - columnState, - updateColumnState: false, - }); - } else { - this.customSetProps({ - updateColumnState: false, - }); - } - } - - buildArray(arr1, arr2) { - if (arr1) { - if (!arr1.includes(arr2)) { - return [...arr1, arr2]; - } - return arr1; - } - return [JSON.parse(JSON.stringify(arr2))]; - } - - rowTransaction(data) { - const {rowTransaction, gridApi, mounted} = this.state; - if (mounted) { - if (gridApi && !gridApi?.isDestroyed()) { - if (rowTransaction) { - rowTransaction.forEach(this.applyRowTransaction); - this.setState({rowTransaction: null}); - } - this.applyRowTransaction(data); - this.customSetProps({ - rowTransaction: null, - }); - this.syncRowData(); - } else { - this.setState({ - rowTransaction: rowTransaction - ? this.buildArray(rowTransaction, data) - : [JSON.parse(JSON.stringify(data))], - }); - } - } - } - - onAsyncTransactionsFlushed() { - const {selectedRows} = this.props; - if (selectedRows) { - this.setSelection(selectedRows); - } - this.syncRowData(); - } - - render() { - const {id, style, className, dashGridOptions, ...restProps} = - this.props; - - const passingProps = pick(PASSTHRU_PROPS, restProps); - - const convertedProps = this.convertAllProps( - omit(NO_CONVERT_PROPS, {...dashGridOptions, ...restProps}) - ); - - let alignedGrids; - if (dashGridOptions) { - if ('alignedGrids' in dashGridOptions) { - alignedGrids = []; - const addGrid = (id) => { - const strId = stringifyId(id); - eventBus.on(this.props.id, strId, () => { - this.setState(({rerender}) => ({ - rerender: rerender + 1, - })); - }); - if (!agGridRefs[strId]) { - agGridRefs[strId] = {api: null}; - } - alignedGrids.push(agGridRefs[strId]); - }; - eventBus.remove(this.props.id); - if (Array.isArray(dashGridOptions.alignedGrids)) { - dashGridOptions.alignedGrids.map(addGrid); - } else { - addGrid(dashGridOptions.alignedGrids); - } - } - } - - return ( -
- -
- ); - } -} - DashAgGrid.defaultProps = _defaultProps; DashAgGrid.propTypes = {parentState: PropTypes.any, ..._propTypes}; @@ -3077,5 +1663,4 @@ export const defaultProps = DashAgGrid.defaultProps; var dagfuncs = (window.dash_ag_grid = window.dash_ag_grid || {}); dagfuncs.useGridFilter = useGridFilter; -// export default DashAgGridOld; export default DashAgGrid; From 415ed1226401cc0fcd01e1152f4446234464d7aa Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 30 Jun 2025 12:01:09 -0600 Subject: [PATCH 05/17] refactor "openGroups" and "rerender" into their own states --- src/lib/fragments/AgGrid.react.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index fab928dc..c8146d59 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -268,6 +268,8 @@ export function DashAgGrid(props) { const newComponents = map(generateRenderer, customComponents); const [gridApi, setGridApi] = useState(null); + const [, forceRerender] = useState({}); + const [openGroups, setOpenGroups] = useState({}); const [state, setState] = useState({ ...props.parentState, components: { @@ -275,8 +277,6 @@ export function DashAgGrid(props) { markdown: generateRenderer(MarkdownRenderer), ...newComponents, }, - rerender: 0, - openGroups: {}, columnState_push: true, }); @@ -740,7 +740,6 @@ export function DashAgGrid(props) { const onRowDataUpdated = useCallback(() => { // Handles preserving existing selections when rowData is updated in a callback const {selectedRows, rowData, rowModelType, filterModel} = props; - const {openGroups} = state; if (gridApi && !gridApi?.isDestroyed()) { dataUpdates.current = true; @@ -779,7 +778,7 @@ export function DashAgGrid(props) { props.rowModelType, props.filterModel, gridApi, - state.openGroups, + openGroups, dataUpdates.current, pauseSelections.current, setSelection, @@ -788,12 +787,11 @@ export function DashAgGrid(props) { ]); const onRowGroupOpened = useCallback((e) => { - setState((prevState) => ({ - ...prevState, - openGroups: e.expanded - ? assoc(e.node.key, 1, prevState.openGroups) - : omit([e.node.key], prevState.openGroups), - })); + setOpenGroups((prevOpenGroups) => + e.expanded + ? assoc(e.node.key, 1, prevOpenGroups) + : omit([e.node.key], prevOpenGroups) + ); }, []); const onSelectionChanged = useCallback(() => { @@ -1375,9 +1373,9 @@ export function DashAgGrid(props) { // Hydrate virtualRowData onFilterChanged(true); + setOpenGroups(groups); setState((prev) => ({ ...prev, - openGroups: groups, columnState_push: false, })); updateColumnState(); @@ -1585,10 +1583,7 @@ export function DashAgGrid(props) { const addGrid = (id) => { const strId = stringifyId(id); eventBus.on(props.id, strId, () => { - setState((prevState) => ({ - ...prevState, - rerender: prevState.rerender + 1, - })); + forceRerender({}); }); if (!agGridRefs[strId]) { agGridRefs[strId] = {api: null}; From 5d775b02dabfc6673e89416bc59ab5bc8c88f92a Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 30 Jun 2025 12:45:16 -0600 Subject: [PATCH 06/17] Move columnState, rowTransactionState, parentState, components all into their own states --- src/lib/fragments/AgGrid.react.js | 80 +++++++++++++------------------ 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index c8146d59..de6d44dc 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -270,15 +270,18 @@ export function DashAgGrid(props) { const [gridApi, setGridApi] = useState(null); const [, forceRerender] = useState({}); const [openGroups, setOpenGroups] = useState({}); - const [state, setState] = useState({ - ...props.parentState, - components: { + const [columnState_push, setColumnState_push] = useState(true); + const [rowTransactionState, setRowTransactionState] = useState(null); + const [parentState] = useState(props.parentState || {}); + + const components = useMemo( + () => ({ rowMenu: generateRenderer(RowMenuRenderer), markdown: generateRenderer(MarkdownRenderer), ...newComponents, - }, - columnState_push: true, - }); + }), + [generateRenderer, newComponents] + ); const prevProps = usePrevious(props); const prevGridApi = usePrevious(gridApi); @@ -609,7 +612,7 @@ export function DashAgGrid(props) { if ('detailGridOptions' in value) { adjustedVal = assocPath( ['detailGridOptions', 'components'], - state.components, + components, adjustedVal ); } @@ -650,7 +653,7 @@ export function DashAgGrid(props) { convertMaybeFunctionNoParams, suppressGetDetail, callbackGetDetail, - state.components, + components, convertAllPropsRef.current, convertFunction, handleDynamicStyle, @@ -787,7 +790,7 @@ export function DashAgGrid(props) { ]); const onRowGroupOpened = useCallback((e) => { - setOpenGroups((prevOpenGroups) => + setOpenGroups((prevOpenGroups) => e.expanded ? assoc(e.node.key, 1, prevOpenGroups) : omit([e.node.key], prevOpenGroups) @@ -1044,17 +1047,14 @@ export function DashAgGrid(props) { return; } - if (state.columnState_push) { + if (columnState_push) { gridApi.applyColumnState({ state: props.columnState, applyOrder: true, }); - setState((prevState) => ({ - ...prevState, - columnState_push: false, - })); + setColumnState_push(false); } - }, [gridApi, props.updateColumnState, state.columnState_push, setState]); + }, [gridApi, props.updateColumnState, columnState_push]); const exportDataAsCsv = useCallback( (csvExportParams, reset = true) => { @@ -1228,14 +1228,11 @@ export function DashAgGrid(props) { const rowTransaction = useCallback( (data) => { - const {rowTransaction} = state; + const rowTransaction = rowTransactionState; if (gridApi && !gridApi?.isDestroyed()) { if (rowTransaction) { rowTransaction.forEach(applyRowTransaction); - setState((prevState) => ({ - ...prevState, - rowTransaction: null, - })); + setRowTransactionState(null); } applyRowTransaction(data); customSetProps({ @@ -1243,19 +1240,18 @@ export function DashAgGrid(props) { }); syncRowData(); } else { - setState((prevState) => ({ - ...prevState, - rowTransaction: rowTransaction + setRowTransactionState( + rowTransaction ? buildArray(rowTransaction, data) - : [JSON.parse(JSON.stringify(data))], - })); + : [JSON.parse(JSON.stringify(data))] + ); } }, [ - state.rowTransaction, + rowTransactionState, gridApi, applyRowTransaction, - setState, + setRowTransactionState, customSetProps, syncRowData, buildArray, @@ -1308,11 +1304,11 @@ export function DashAgGrid(props) { }); // Handle row transactions - if (state.rowTransaction) { - state.rowTransaction.map((data) => + if (rowTransactionState) { + rowTransactionState.map((data) => applyRowTransaction(data, gridApi) ); - setState((prev) => ({...prev, rowTransaction: null})); + setRowTransactionState(null); syncRowData(); } @@ -1374,18 +1370,13 @@ export function DashAgGrid(props) { // Hydrate virtualRowData onFilterChanged(true); setOpenGroups(groups); - setState((prev) => ({ - ...prev, - columnState_push: false, - })); + setColumnState_push(false); updateColumnState(); } }, [ gridApi, updateColumnWidths, - state.rowTransaction, applyRowTransaction, - setState, syncRowData, setSelection, props.selectedRows, @@ -1421,17 +1412,12 @@ export function DashAgGrid(props) { ) { if ( props.columnState !== prevProps?.columnState && - !state.columnState_push + !columnState_push ) { - setState((prev) => ({...prev, columnState_push: true})); + setColumnState_push(true); } } - }, [ - props.columnState, - props.loading_state, - gridApi, - state.columnState_push, - ]); + }, [props.columnState, props.loading_state, gridApi, columnState_push]); // 3. Handle ID changes useEffect(() => { @@ -1546,7 +1532,7 @@ export function DashAgGrid(props) { if (props.updateColumnState) { updateColumnState(); - } else if (state.columnState_push) { + } else if (columnState_push) { setColumnState(); } } @@ -1562,7 +1548,7 @@ export function DashAgGrid(props) { props.deleteSelectedRows, props.rowTransaction, props.updateColumnState, - state.columnState_push, + columnState_push, gridApi, ]); @@ -1641,7 +1627,7 @@ export function DashAgGrid(props) { onGridSizeChanged, RESIZE_DEBOUNCE_MS )} - components={state.components} + components={components} {...passingProps} {...convertedProps} > From 47161199beddc436217677bc2f112acccd12a6c6 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Wed, 2 Jul 2025 09:18:16 -0600 Subject: [PATCH 07/17] Separate large effects into smaller ones --- src/lib/fragments/AgGrid.react.js | 231 +++++++++++++++++++----------- 1 file changed, 147 insertions(+), 84 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index de6d44dc..50901344 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -1289,45 +1289,68 @@ export function DashAgGrid(props) { setSelection(props.selectedRows); }, [props.selectedRows]); - // 1. Handle gridApi changes and initialization + // 1. Handle gridApi initialization - basic setup useEffect(() => { if (gridApi && gridApi !== prevGridApi) { - const propsToSet = {}; updateColumnWidths(false); - // Track expanded groups + // Handle pagination initialization + if (reference.current.props.pagination) { + onPaginationChanged(); + } + } + }, [gridApi, prevGridApi, updateColumnWidths, onPaginationChanged]); + + // 1a. Handle gridApi initialization - expanded groups tracking + useEffect(() => { + if (gridApi && gridApi !== prevGridApi) { const groups = {}; gridApi.forEachNode((node) => { if (node.expanded) { groups[node.key] = 1; } }); + setOpenGroups(groups); + } + }, [gridApi, prevGridApi, setOpenGroups]); - // Handle row transactions - if (rowTransactionState) { - rowTransactionState.map((data) => - applyRowTransaction(data, gridApi) - ); - setRowTransactionState(null); - syncRowData(); - } + // 1b. Handle gridApi initialization - row transactions + useEffect(() => { + if (gridApi && gridApi !== prevGridApi && rowTransactionState) { + rowTransactionState.map((data) => + applyRowTransaction(data, gridApi) + ); + setRowTransactionState(null); + syncRowData(); + } + }, [ + gridApi, + prevGridApi, + rowTransactionState, + applyRowTransaction, + setRowTransactionState, + syncRowData, + ]); - // Handle pagination - if (reference.current.props.pagination) { - onPaginationChanged(); - } + // 1c. Handle gridApi initialization - filter model application + useEffect(() => { + if (gridApi && gridApi !== prevGridApi && !isEmpty(props.filterModel)) { + gridApi.setFilterModel(props.filterModel); + } + }, [gridApi, prevGridApi, props.filterModel]); - // Apply filter model - if (!isEmpty(props.filterModel)) { - gridApi.setFilterModel(props.filterModel); - } + // 1d. Handle gridApi initialization - column state application + useEffect(() => { + if (gridApi && gridApi !== prevGridApi && props.columnState) { + setColumnState(); + } + }, [gridApi, prevGridApi, props.columnState, setColumnState]); - // Apply column state - if (props.columnState) { - setColumnState(); - } + // 1e. Handle gridApi initialization - action props with cleanup + useEffect(() => { + if (gridApi && gridApi !== prevGridApi) { + const propsToSet = {}; - // Handle various action props if (props.paginationGoTo || props.paginationGoTo === 0) { paginationGoTo(false); propsToSet.paginationGoTo = null; @@ -1366,41 +1389,41 @@ export function DashAgGrid(props) { if (!isEmpty(propsToSet)) { customSetProps(propsToSet); } - - // Hydrate virtualRowData - onFilterChanged(true); - setOpenGroups(groups); - setColumnState_push(false); - updateColumnState(); } }, [ gridApi, - updateColumnWidths, - applyRowTransaction, - syncRowData, - setSelection, - props.selectedRows, - onPaginationChanged, - props.filterModel, - props.columnState, - setColumnState, + prevGridApi, props.paginationGoTo, - paginationGoTo, props.scrollTo, - scrollTo, props.resetColumnState, - resetColumnState, props.exportDataAsCsv, - exportDataAsCsv, props.csvExportParams, props.selectAll, - selectAll, props.deselectAll, - deselectAll, props.deleteSelectedRows, + paginationGoTo, + scrollTo, + resetColumnState, + exportDataAsCsv, + selectAll, + deselectAll, deleteSelectedRows, customSetProps, + ]); + + // 1f. Handle gridApi initialization - finalization + useEffect(() => { + if (gridApi && gridApi !== prevGridApi) { + // Hydrate virtualRowData and finalize setup + onFilterChanged(true); + setColumnState_push(false); + updateColumnState(); + } + }, [ + gridApi, + prevGridApi, onFilterChanged, + setColumnState_push, updateColumnState, ]); @@ -1484,72 +1507,112 @@ export function DashAgGrid(props) { dataUpdates.current = false; }); - // 8. Handle prop changes when gridApi exists (but hasn't changed) + // 8. Handle filter model updates useEffect(() => { - if (gridApi && gridApi === prevGridApi) { - if ( - props.filterModel && - gridApi.getFilterModel() !== props.filterModel - ) { - gridApi.setFilterModel(props.filterModel); - } + if ( + gridApi && + gridApi === prevGridApi && + props.filterModel && + gridApi.getFilterModel() !== props.filterModel + ) { + gridApi.setFilterModel(props.filterModel); + } + }, [props.filterModel, gridApi, prevGridApi]); - if (props.paginationGoTo || props.paginationGoTo === 0) { - paginationGoTo(); - } + // 9. Handle pagination actions + useEffect(() => { + if ( + gridApi && + gridApi === prevGridApi && + (props.paginationGoTo || props.paginationGoTo === 0) + ) { + paginationGoTo(); + } + }, [props.paginationGoTo, gridApi, prevGridApi, paginationGoTo]); - if (props.scrollTo) { - scrollTo(); - } + // 10. Handle scroll actions + useEffect(() => { + if (gridApi && gridApi === prevGridApi && props.scrollTo) { + scrollTo(); + } + }, [props.scrollTo, gridApi, prevGridApi, scrollTo]); - if (props.columnSize) { - updateColumnWidths(); - } + // 11. Handle column size updates + useEffect(() => { + if (gridApi && gridApi === prevGridApi && props.columnSize) { + updateColumnWidths(); + } + }, [props.columnSize, gridApi, prevGridApi, updateColumnWidths]); - if (props.resetColumnState) { - resetColumnState(); - } + // 12. Handle column state reset + useEffect(() => { + if (gridApi && gridApi === prevGridApi && props.resetColumnState) { + resetColumnState(); + } + }, [props.resetColumnState, gridApi, prevGridApi, resetColumnState]); - if (props.exportDataAsCsv) { - exportDataAsCsv(props.csvExportParams); - } + // 13. Handle CSV export + useEffect(() => { + if (gridApi && gridApi === prevGridApi && props.exportDataAsCsv) { + exportDataAsCsv(props.csvExportParams); + } + }, [ + props.exportDataAsCsv, + props.csvExportParams, + gridApi, + prevGridApi, + exportDataAsCsv, + ]); + // 14. Handle row selection actions + useEffect(() => { + if (gridApi && gridApi === prevGridApi) { if (props.selectAll) { selectAll(props.selectAll); } - if (props.deselectAll) { deselectAll(); } - if (props.deleteSelectedRows) { deleteSelectedRows(); } + } + }, [ + props.selectAll, + props.deselectAll, + props.deleteSelectedRows, + gridApi, + prevGridApi, + selectAll, + deselectAll, + deleteSelectedRows, + ]); - if (props.rowTransaction) { - rowTransaction(props.rowTransaction); - } + // 15. Handle row transactions + useEffect(() => { + if (gridApi && gridApi === prevGridApi && props.rowTransaction) { + rowTransaction(props.rowTransaction); + } + }, [props.rowTransaction, gridApi, prevGridApi, rowTransaction]); + // 16. Handle column state updates + useEffect(() => { + if (gridApi && gridApi === prevGridApi) { if (props.updateColumnState) { updateColumnState(); } else if (columnState_push) { - setColumnState(); + setTimeout(() => { + setColumnState(); + }, 1); } } }, [ - props.filterModel, - props.paginationGoTo, - props.scrollTo, - props.columnSize, - props.resetColumnState, - props.exportDataAsCsv, - props.selectAll, - props.deselectAll, - props.deleteSelectedRows, - props.rowTransaction, props.updateColumnState, columnState_push, gridApi, + prevGridApi, + updateColumnState, + setColumnState, ]); // End of hooks From 5252092391b388f93c83ce7de76336dceb8fe413 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Wed, 2 Jul 2025 13:47:20 -0600 Subject: [PATCH 08/17] Remove unnecessary state update --- src/lib/fragments/AgGrid.react.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 50901344..9ab6f233 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -1416,7 +1416,6 @@ export function DashAgGrid(props) { if (gridApi && gridApi !== prevGridApi) { // Hydrate virtualRowData and finalize setup onFilterChanged(true); - setColumnState_push(false); updateColumnState(); } }, [ From 4e1b55196d9d80fa6dc8135daf53d428cf5412ec Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 4 Jul 2025 14:14:12 -0600 Subject: [PATCH 09/17] Memoize component --- src/lib/fragments/AgGrid.react.js | 21 +++++++++++++++++++-- src/lib/utils/propCategories.js | 4 +++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 9ab6f233..90a6df3b 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -272,7 +272,6 @@ export function DashAgGrid(props) { const [openGroups, setOpenGroups] = useState({}); const [columnState_push, setColumnState_push] = useState(true); const [rowTransactionState, setRowTransactionState] = useState(null); - const [parentState] = useState(props.parentState || {}); const components = useMemo( () => ({ @@ -1706,4 +1705,22 @@ export const defaultProps = DashAgGrid.defaultProps; var dagfuncs = (window.dash_ag_grid = window.dash_ag_grid || {}); dagfuncs.useGridFilter = useGridFilter; -export default DashAgGrid; +const MemoizedAgGrid = React.memo(DashAgGrid, (prevProps, nextProps) => { + // Check if props are equal (excluding render-specific props) + const relevantNextProps = { ...omit(OMIT_PROP_RENDER, nextProps) }; + const relevantPrevProps = { ...omit(OMIT_PROP_RENDER, prevProps) }; + + const isInternalChange = nextProps?.dashRenderType === 'internal'; + const propsHaveChanged = !equals(relevantNextProps, relevantPrevProps); + const rowDataChanged = !equals(nextProps.rowData, prevProps.rowData); + const selectedRowsChanged = !equals(nextProps.selectedRows, prevProps.selectedRows); + + if (propsHaveChanged && (!isInternalChange || rowDataChanged || selectedRowsChanged)) { + return false; // Props changed, re-render + } + + return true; + +}); + +export default MemoizedAgGrid; diff --git a/src/lib/utils/propCategories.js b/src/lib/utils/propCategories.js index e6995f85..64f6c511 100644 --- a/src/lib/utils/propCategories.js +++ b/src/lib/utils/propCategories.js @@ -317,10 +317,12 @@ export const PASSTHRU_PROPS = ['rowData']; * in the render() method, so they don't need to be listed here */ export const PROPS_NOT_FOR_AG_GRID = [ + 'children', 'setProps', 'loading_state', 'enableEnterpriseModules', 'parentState', + 'persistence', 'persisted_props', 'persistence_type', 'virtualRowData', @@ -335,6 +337,7 @@ export const PROPS_NOT_FOR_AG_GRID = [ 'alignedGrids', 'resetColumnState', 'exportDataAsCsv', + 'selectedRows', 'selectAll', 'deselectAll', 'deleteSelectedRows', @@ -357,7 +360,6 @@ export const OMIT_PROP_RENDER = [ 'virtualRowData', 'columnState', 'filterModel', - 'selectedRows', 'getRowRequest', 'getDetailRequest', 'cellValueChanged', From c2109e49487abf21b6f34b18d0e1edd643f48845 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 10 Jul 2025 15:38:01 -0600 Subject: [PATCH 10/17] Fix bug with initial selections --- src/lib/fragments/AgGrid.react.js | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index 90a6df3b..b01681f9 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -1285,8 +1285,10 @@ export function DashAgGrid(props) { useEffect(() => { // Apply selections - setSelection(props.selectedRows); - }, [props.selectedRows]); + if (gridApi) { + setSelection(props.selectedRows); + } + }, [props.selectedRows, gridApi]); // 1. Handle gridApi initialization - basic setup useEffect(() => { @@ -1479,27 +1481,6 @@ export function DashAgGrid(props) { props.detailCellRendererParams, ]); - // 6. Handle selectedRows changes - useEffect(() => { - if ( - !equals(props.selectedRows, prevProps?.selectedRows) && - !(typeof props.loading_state !== 'undefined' - ? props.loading_state && selectionEventFired.current - : selectionEventFired.current) - ) { - if (!dataUpdates.current) { - setTimeout(() => { - if (!dataUpdates.current) { - setSelection(props.selectedRows); - } - }, 10); - } - } - - // Reset selection event flag - selectionEventFired.current = false; - }, [props.selectedRows, props.loading_state]); - // 7. Handle dataUpdates reset useEffect(() => { dataUpdates.current = false; From a7534ee344f994d76fe939e85f53fa7c0578624f Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 10 Jul 2025 16:12:55 -0600 Subject: [PATCH 11/17] code cleanup --- CHANGELOG.md | 5 ++ src/lib/fragments/AgGrid.react.js | 91 ++++++++++----------- src/lib/fragments/AgGridEnterprise.react.js | 4 +- tests/test_cell_data_type_override.py | 2 +- 4 files changed, 53 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e80578..e1cc912f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ 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 + +### Changed +- Component is refactored to be a function component rather than a class + ## [32.3.0rc0] - 2025-04-15 ### Fixed diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index b01681f9..d1a149b7 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -37,7 +37,6 @@ import { PROPS_NOT_FOR_AG_GRID, GRID_DANGEROUS_FUNCTIONS, OMIT_PROP_RENDER, - OMIT_STATE_RENDER, OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS, } from '../utils/propCategories'; import debounce from '../utils/debounce'; @@ -243,21 +242,21 @@ export function DashAgGrid(props) { (Renderer) => { const {dangerously_allow_code} = props; - return (props) => ( + return (cellProps) => ( { customSetProps({ cellRendererData: { value, - colId: props.column.colId, - rowIndex: props.node.sourceRowIndex, - rowId: props.node.id, + colId: cellProps.column.colId, + rowIndex: cellProps.node.sourceRowIndex, + rowId: cellProps.node.id, timestamp: Date.now(), }, }); }} dangerously_allow_code={dangerously_allow_code} - {...props} + {...cellProps} > ); }, @@ -290,7 +289,6 @@ export function DashAgGrid(props) { const selectionEventFired = useRef(false); const pauseSelections = useRef(false); const reference = useRef(); - // const pendingChanges = useRef(null); const dataUpdates = useRef(false); const getDetailParams = useRef(); const getRowsParams = useRef(null); @@ -1286,11 +1284,11 @@ export function DashAgGrid(props) { useEffect(() => { // Apply selections if (gridApi) { - setSelection(props.selectedRows); + setSelection(props.selectedRows); } }, [props.selectedRows, gridApi]); - // 1. Handle gridApi initialization - basic setup + // Handle gridApi initialization - basic setup useEffect(() => { if (gridApi && gridApi !== prevGridApi) { updateColumnWidths(false); @@ -1302,7 +1300,7 @@ export function DashAgGrid(props) { } }, [gridApi, prevGridApi, updateColumnWidths, onPaginationChanged]); - // 1a. Handle gridApi initialization - expanded groups tracking + // Handle gridApi initialization - expanded groups tracking useEffect(() => { if (gridApi && gridApi !== prevGridApi) { const groups = {}; @@ -1315,7 +1313,7 @@ export function DashAgGrid(props) { } }, [gridApi, prevGridApi, setOpenGroups]); - // 1b. Handle gridApi initialization - row transactions + // Handle gridApi initialization - row transactions useEffect(() => { if (gridApi && gridApi !== prevGridApi && rowTransactionState) { rowTransactionState.map((data) => @@ -1333,21 +1331,21 @@ export function DashAgGrid(props) { syncRowData, ]); - // 1c. Handle gridApi initialization - filter model application + // Handle gridApi initialization - filter model application useEffect(() => { if (gridApi && gridApi !== prevGridApi && !isEmpty(props.filterModel)) { gridApi.setFilterModel(props.filterModel); } }, [gridApi, prevGridApi, props.filterModel]); - // 1d. Handle gridApi initialization - column state application + // Handle gridApi initialization - column state application useEffect(() => { if (gridApi && gridApi !== prevGridApi && props.columnState) { setColumnState(); } }, [gridApi, prevGridApi, props.columnState, setColumnState]); - // 1e. Handle gridApi initialization - action props with cleanup + // Handle gridApi initialization - action props with cleanup useEffect(() => { if (gridApi && gridApi !== prevGridApi) { const propsToSet = {}; @@ -1412,7 +1410,7 @@ export function DashAgGrid(props) { customSetProps, ]); - // 1f. Handle gridApi initialization - finalization + // Handle gridApi initialization - finalization useEffect(() => { if (gridApi && gridApi !== prevGridApi) { // Hydrate virtualRowData and finalize setup @@ -1427,7 +1425,7 @@ export function DashAgGrid(props) { updateColumnState, ]); - // 2. Handle columnState push changes + // Handle columnState push changes useEffect(() => { if ( gridApi && @@ -1442,7 +1440,7 @@ export function DashAgGrid(props) { } }, [props.columnState, props.loading_state, gridApi, columnState_push]); - // 3. Handle ID changes + // Handle ID changes useEffect(() => { if (props.id !== prevProps?.id) { if (props.id) { @@ -1456,7 +1454,7 @@ export function DashAgGrid(props) { } }, [props.id]); - // 4. Handle infinite scrolling datasource + // Handle infinite scrolling datasource useEffect(() => { if (isDatasourceLoadedForInfiniteScrolling()) { const {rowData, rowCount} = props.getRowsResponse; @@ -1465,7 +1463,7 @@ export function DashAgGrid(props) { } }, [props.getRowsResponse]); - // 5. Handle master detail response + // Handle master detail response useEffect(() => { if ( props.masterDetail && @@ -1481,12 +1479,12 @@ export function DashAgGrid(props) { props.detailCellRendererParams, ]); - // 7. Handle dataUpdates reset + // Handle dataUpdates reset useEffect(() => { dataUpdates.current = false; }); - // 8. Handle filter model updates + // Handle filter model updates useEffect(() => { if ( gridApi && @@ -1498,7 +1496,7 @@ export function DashAgGrid(props) { } }, [props.filterModel, gridApi, prevGridApi]); - // 9. Handle pagination actions + // Handle pagination actions useEffect(() => { if ( gridApi && @@ -1509,28 +1507,28 @@ export function DashAgGrid(props) { } }, [props.paginationGoTo, gridApi, prevGridApi, paginationGoTo]); - // 10. Handle scroll actions + // Handle scroll actions useEffect(() => { if (gridApi && gridApi === prevGridApi && props.scrollTo) { scrollTo(); } }, [props.scrollTo, gridApi, prevGridApi, scrollTo]); - // 11. Handle column size updates + // Handle column size updates useEffect(() => { if (gridApi && gridApi === prevGridApi && props.columnSize) { updateColumnWidths(); } }, [props.columnSize, gridApi, prevGridApi, updateColumnWidths]); - // 12. Handle column state reset + // Handle column state reset useEffect(() => { if (gridApi && gridApi === prevGridApi && props.resetColumnState) { resetColumnState(); } }, [props.resetColumnState, gridApi, prevGridApi, resetColumnState]); - // 13. Handle CSV export + // Handle CSV export useEffect(() => { if (gridApi && gridApi === prevGridApi && props.exportDataAsCsv) { exportDataAsCsv(props.csvExportParams); @@ -1543,7 +1541,7 @@ export function DashAgGrid(props) { exportDataAsCsv, ]); - // 14. Handle row selection actions + // Handle row selection actions useEffect(() => { if (gridApi && gridApi === prevGridApi) { if (props.selectAll) { @@ -1567,14 +1565,14 @@ export function DashAgGrid(props) { deleteSelectedRows, ]); - // 15. Handle row transactions + // Handle row transactions useEffect(() => { if (gridApi && gridApi === prevGridApi && props.rowTransaction) { rowTransaction(props.rowTransaction); } }, [props.rowTransaction, gridApi, prevGridApi, rowTransaction]); - // 16. Handle column state updates + // Handle column state updates useEffect(() => { if (gridApi && gridApi === prevGridApi) { if (props.updateColumnState) { @@ -1594,12 +1592,8 @@ export function DashAgGrid(props) { setColumnState, ]); - // End of hooks - const {id, style, className, dashGridOptions, ...restProps} = props; - const passingProps = pick(PASSTHRU_PROPS, restProps); - const convertedProps = convertAllProps( omit(NO_CONVERT_PROPS, {...dashGridOptions, ...restProps}) ); @@ -1687,21 +1681,26 @@ var dagfuncs = (window.dash_ag_grid = window.dash_ag_grid || {}); dagfuncs.useGridFilter = useGridFilter; const MemoizedAgGrid = React.memo(DashAgGrid, (prevProps, nextProps) => { - // Check if props are equal (excluding render-specific props) - const relevantNextProps = { ...omit(OMIT_PROP_RENDER, nextProps) }; - const relevantPrevProps = { ...omit(OMIT_PROP_RENDER, prevProps) }; - - const isInternalChange = nextProps?.dashRenderType === 'internal'; - const propsHaveChanged = !equals(relevantNextProps, relevantPrevProps); - const rowDataChanged = !equals(nextProps.rowData, prevProps.rowData); - const selectedRowsChanged = !equals(nextProps.selectedRows, prevProps.selectedRows); - - if (propsHaveChanged && (!isInternalChange || rowDataChanged || selectedRowsChanged)) { - return false; // Props changed, re-render - } + // Check if props are equal (excluding render-specific props) + const relevantNextProps = {...omit(OMIT_PROP_RENDER, nextProps)}; + const relevantPrevProps = {...omit(OMIT_PROP_RENDER, prevProps)}; + + const isInternalChange = nextProps?.dashRenderType === 'internal'; + const propsHaveChanged = !equals(relevantNextProps, relevantPrevProps); + const rowDataChanged = !equals(nextProps.rowData, prevProps.rowData); + const selectedRowsChanged = !equals( + nextProps.selectedRows, + prevProps.selectedRows + ); - return true; + if ( + propsHaveChanged && + (!isInternalChange || rowDataChanged || selectedRowsChanged) + ) { + return false; // Props changed, re-render + } + return true; }); export default MemoizedAgGrid; diff --git a/src/lib/fragments/AgGridEnterprise.react.js b/src/lib/fragments/AgGridEnterprise.react.js index f211ef42..3df1e599 100644 --- a/src/lib/fragments/AgGridEnterprise.react.js +++ b/src/lib/fragments/AgGridEnterprise.react.js @@ -1,13 +1,13 @@ import React from 'react'; import {LicenseManager} from 'ag-grid-enterprise'; -import DashAgGrid, {propTypes} from './AgGrid.react'; +import MemoizedAgGrid, {propTypes} from './AgGrid.react'; export default function DashAgGridEnterprise(props) { const {licenseKey} = props; if (licenseKey) { LicenseManager.setLicenseKey(licenseKey); } - return ; + return ; } DashAgGridEnterprise.propTypes = propTypes; diff --git a/tests/test_cell_data_type_override.py b/tests/test_cell_data_type_override.py index bfe5523c..f4bdd54f 100644 --- a/tests/test_cell_data_type_override.py +++ b/tests/test_cell_data_type_override.py @@ -79,4 +79,4 @@ def test_cd001_cell_data_types_override(dash_duo): date_input_element = dash_duo.find_element(f'#{grid.id} .ag-date-field-input') date_input_element.send_keys("01172024" + Keys.ENTER) - # grid.wait_for_cell_text(0, 1, "17/01/2024") + grid.wait_for_cell_text(0, 1, "17/01/2024") From 224ea62c496a14a06f6f2bce13ee6d785b194ec1 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 14 Jul 2025 13:55:58 -0600 Subject: [PATCH 12/17] Fix bug setting `columnState` prop --- src/lib/fragments/AgGrid.react.js | 17 +++++++++-------- tests/test_column_state.py | 3 +++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index d1a149b7..b1042148 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -1416,6 +1416,7 @@ export function DashAgGrid(props) { // Hydrate virtualRowData and finalize setup onFilterChanged(true); updateColumnState(); + setColumnState_push(false); } }, [ gridApi, @@ -1431,14 +1432,16 @@ export function DashAgGrid(props) { gridApi && (!props.loading_state || prevProps?.loading_state?.is_loading) ) { - if ( - props.columnState !== prevProps?.columnState && - !columnState_push - ) { + const existingColumnState = gridApi.getColumnState(); + const realStateChange = + props.columnState && + !equals(props.columnState, existingColumnState); + + if (realStateChange && !columnState_push) { setColumnState_push(true); } } - }, [props.columnState, props.loading_state, gridApi, columnState_push]); + }, [props.columnState, props.loading_state, columnState_push]); // Handle ID changes useEffect(() => { @@ -1578,9 +1581,7 @@ export function DashAgGrid(props) { if (props.updateColumnState) { updateColumnState(); } else if (columnState_push) { - setTimeout(() => { - setColumnState(); - }, 1); + setColumnState(); } } }, [ diff --git a/tests/test_column_state.py b/tests/test_column_state.py index c771d6d8..5b573b0b 100644 --- a/tests/test_column_state.py +++ b/tests/test_column_state.py @@ -247,6 +247,9 @@ def loadState(n): ) dash_duo.find_element("#load-column-defs").click() + + time.sleep(0.5) # pausing to emulate separation because user inputs + until( lambda: json.dumps(alt_colState) in dash_duo.find_element("#reset-column-state-grid-pre").text, From e95cff24b7fa46d41e774feae3e8984c13f4ed4f Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 14 Jul 2025 16:16:28 -0600 Subject: [PATCH 13/17] Make test locale-agnostic --- tests/test_cell_data_type_override.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cell_data_type_override.py b/tests/test_cell_data_type_override.py index f4bdd54f..e93e36f7 100644 --- a/tests/test_cell_data_type_override.py +++ b/tests/test_cell_data_type_override.py @@ -77,6 +77,6 @@ def test_cd001_cell_data_types_override(dash_duo): # test overriden dateString cell data type action.double_click(grid.get_cell(0, 1)).perform() date_input_element = dash_duo.find_element(f'#{grid.id} .ag-date-field-input') - date_input_element.send_keys("01172024" + Keys.ENTER) + date_input_element.send_keys("01012024" + Keys.ENTER) - grid.wait_for_cell_text(0, 1, "17/01/2024") + grid.wait_for_cell_text(0, 1, "01/01/2024") From 682bca338a3d05b2e415f64ea2ce77c4de980f55 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:54:49 -0400 Subject: [PATCH 14/17] - removing redundant initial gridApi as useEffect was listening to it already - adjusting tests in the event the grid takes a bit to render - fixed issue with selectedRows triggering uneccessarily --- src/lib/fragments/AgGrid.react.js | 88 ++++--------------------------- tests/test_event_listeners.py | 3 +- tests/test_pagination.py | 2 + 3 files changed, 15 insertions(+), 78 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index b1042148..eb1d3e9c 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -779,8 +779,6 @@ export function DashAgGrid(props) { props.filterModel, gridApi, openGroups, - dataUpdates.current, - pauseSelections.current, setSelection, virtualRowData, customSetProps, @@ -806,10 +804,8 @@ export function DashAgGrid(props) { } }, 1); }, [ - pauseSelections.current, gridApi, props.selectedRows, - selectionEventFired.current, customSetProps, ]); @@ -1284,7 +1280,10 @@ export function DashAgGrid(props) { useEffect(() => { // Apply selections if (gridApi) { - setSelection(props.selectedRows); + const selectedRows = gridApi.getSelectedRows(); + if (!equals(selectedRows, props.selectedRows)) { + setSelection(props.selectedRows); + } } }, [props.selectedRows, gridApi]); @@ -1345,71 +1344,6 @@ export function DashAgGrid(props) { } }, [gridApi, prevGridApi, props.columnState, setColumnState]); - // Handle gridApi initialization - action props with cleanup - useEffect(() => { - if (gridApi && gridApi !== prevGridApi) { - const propsToSet = {}; - - if (props.paginationGoTo || props.paginationGoTo === 0) { - paginationGoTo(false); - propsToSet.paginationGoTo = null; - } - - if (props.scrollTo) { - scrollTo(false); - propsToSet.scrollTo = null; - } - - if (props.resetColumnState) { - resetColumnState(false); - propsToSet.resetColumnState = false; - } - - if (props.exportDataAsCsv) { - exportDataAsCsv(props.csvExportParams, false); - propsToSet.exportDataAsCsv = false; - } - - if (props.selectAll) { - selectAll(props.selectAll, false); - propsToSet.selectAll = false; - } - - if (props.deselectAll) { - deselectAll(false); - propsToSet.deselectAll = false; - } - - if (props.deleteSelectedRows) { - deleteSelectedRows(false); - propsToSet.deleteSelectedRows = false; - } - - if (!isEmpty(propsToSet)) { - customSetProps(propsToSet); - } - } - }, [ - gridApi, - prevGridApi, - props.paginationGoTo, - props.scrollTo, - props.resetColumnState, - props.exportDataAsCsv, - props.csvExportParams, - props.selectAll, - props.deselectAll, - props.deleteSelectedRows, - paginationGoTo, - scrollTo, - resetColumnState, - exportDataAsCsv, - selectAll, - deselectAll, - deleteSelectedRows, - customSetProps, - ]); - // Handle gridApi initialization - finalization useEffect(() => { if (gridApi && gridApi !== prevGridApi) { @@ -1512,28 +1446,28 @@ export function DashAgGrid(props) { // Handle scroll actions useEffect(() => { - if (gridApi && gridApi === prevGridApi && props.scrollTo) { + if (gridApi && props.scrollTo) { scrollTo(); } }, [props.scrollTo, gridApi, prevGridApi, scrollTo]); // Handle column size updates useEffect(() => { - if (gridApi && gridApi === prevGridApi && props.columnSize) { + if (gridApi && props.columnSize) { updateColumnWidths(); } }, [props.columnSize, gridApi, prevGridApi, updateColumnWidths]); // Handle column state reset useEffect(() => { - if (gridApi && gridApi === prevGridApi && props.resetColumnState) { + if (gridApi && props.resetColumnState) { resetColumnState(); } }, [props.resetColumnState, gridApi, prevGridApi, resetColumnState]); // Handle CSV export useEffect(() => { - if (gridApi && gridApi === prevGridApi && props.exportDataAsCsv) { + if (gridApi && props.exportDataAsCsv) { exportDataAsCsv(props.csvExportParams); } }, [ @@ -1546,7 +1480,7 @@ export function DashAgGrid(props) { // Handle row selection actions useEffect(() => { - if (gridApi && gridApi === prevGridApi) { + if (gridApi) { if (props.selectAll) { selectAll(props.selectAll); } @@ -1570,14 +1504,14 @@ export function DashAgGrid(props) { // Handle row transactions useEffect(() => { - if (gridApi && gridApi === prevGridApi && props.rowTransaction) { + if (gridApi && props.rowTransaction) { rowTransaction(props.rowTransaction); } }, [props.rowTransaction, gridApi, prevGridApi, rowTransaction]); // Handle column state updates useEffect(() => { - if (gridApi && gridApi === prevGridApi) { + if (gridApi ) { if (props.updateColumnState) { updateColumnState(); } else if (columnState_push) { diff --git a/tests/test_event_listeners.py b/tests/test_event_listeners.py index 74634fc8..70bac7f8 100644 --- a/tests/test_event_listeners.py +++ b/tests/test_event_listeners.py @@ -5,6 +5,7 @@ import json from selenium.webdriver.common.by import By from dash.testing.wait import until +import time df = px.data.medals_wide() @@ -52,7 +53,7 @@ def test_el001_event_listener(dash_duo): # Test left click. grid.get_cell(1, 2).click() - until(lambda: json.loads(dash_duo.find_element('#log').text).get('value') == 15, timeout=3) + until(lambda: json.loads(dash_duo.find_element('#log').text or "{}").get('value') == 15, timeout=3) # Test right click action = utils.ActionChains(dash_duo.driver) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index e6f1e9be..fd3d8bb0 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -4,6 +4,7 @@ import pandas as pd import json from dash.testing.wait import until +import time from . import utils @@ -74,6 +75,7 @@ def updatePage(_): grid = utils.Grid(dash_duo, "grid") + time.sleep(1) # wait for the grid to load until(lambda: "Australia" == grid.get_cell(500, 0).text, timeout=3) oldValue = '{"isLastPageFound": true, "pageSize": 100, "currentPage": 5, "totalPages": 87, "rowCount": 8618}' until(lambda: oldValue == dash_duo.find_element("#grid-info").text, timeout=3) From 250dca4fd8638a886aa774a4d993795372335827 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:58:45 -0400 Subject: [PATCH 15/17] fix for lint --- src/lib/fragments/AgGrid.react.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index eb1d3e9c..52e6a907 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -803,11 +803,7 @@ export function DashAgGrid(props) { } } }, 1); - }, [ - gridApi, - props.selectedRows, - customSetProps, - ]); + }, [gridApi, props.selectedRows, customSetProps]); const isDatasourceLoadedForInfiniteScrolling = useCallback(() => { return ( @@ -1504,14 +1500,14 @@ export function DashAgGrid(props) { // Handle row transactions useEffect(() => { - if (gridApi && props.rowTransaction) { + if (gridApi && props.rowTransaction) { rowTransaction(props.rowTransaction); } }, [props.rowTransaction, gridApi, prevGridApi, rowTransaction]); // Handle column state updates useEffect(() => { - if (gridApi ) { + if (gridApi) { if (props.updateColumnState) { updateColumnState(); } else if (columnState_push) { From a123e4450bf654b534420ea63cb5069dff2aeaca Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:15:24 -0400 Subject: [PATCH 16/17] adjustments for timing of the sizing test --- tests/test_sizing_buttons.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_sizing_buttons.py b/tests/test_sizing_buttons.py index 4737c560..94339025 100644 --- a/tests/test_sizing_buttons.py +++ b/tests/test_sizing_buttons.py @@ -448,6 +448,7 @@ def selected(state): assert oldValue == dash_duo.find_element("#columnState").get_attribute( "innerText" ) + time.sleep(.2) # allow window size to change dash_duo.find_element(f"#{x}").click() until( lambda: oldValue @@ -457,4 +458,4 @@ def selected(state): oldValue = dash_duo.find_element("#columnState").text dash_duo.driver.set_window_size(1000, 1000) - time.sleep(.2) + time.sleep(.2) # allow oldValue to change to the bigger size From ea5c15f95201a8646768970afb9e118d12adba3e Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 15 Jul 2025 15:35:58 -0600 Subject: [PATCH 17/17] Revert test change --- tests/test_cell_data_type_override.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cell_data_type_override.py b/tests/test_cell_data_type_override.py index e93e36f7..f4bdd54f 100644 --- a/tests/test_cell_data_type_override.py +++ b/tests/test_cell_data_type_override.py @@ -77,6 +77,6 @@ def test_cd001_cell_data_types_override(dash_duo): # test overriden dateString cell data type action.double_click(grid.get_cell(0, 1)).perform() date_input_element = dash_duo.find_element(f'#{grid.id} .ag-date-field-input') - date_input_element.send_keys("01012024" + Keys.ENTER) + date_input_element.send_keys("01172024" + Keys.ENTER) - grid.wait_for_cell_text(0, 1, "01/01/2024") + grid.wait_for_cell_text(0, 1, "17/01/2024")