diff --git a/package-lock.json b/package-lock.json index 27fa22c83..1d88cfdf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,7 @@ "resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.38.0.tgz", "integrity": "sha512-0/zFmTz/sKf8rvB8EHMuWIE5miY1gSAvTr5q4fPIiQJQwMAlQyXfH3oy++/MsiC30HyT3Mp93scxX2F1ErKL4g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@internationalized/string": "^3.2.5", "@react-aria/i18n": "^3.12.4", @@ -2710,9 +2711,9 @@ } }, "node_modules/@deephaven/components": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-1.17.0.tgz", - "integrity": "sha512-0H/W0q3iH07rKvS/Ev3OLLfeAQtZi/sujuZL+MnQInPJTX3rNHb8XcwAKI5bI/u5a/+PvGkNswZcS3njvKxJ0Q==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@deephaven/components/-/components-1.21.0.tgz", + "integrity": "sha512-mJGZybJAggwRtxlCGbpV6gGqTBrwuZfAMKkn1wI+7qi+0DyoBjFah7FV4pTdhysINknnzOT0aAIsZcxQcj327g==", "license": "Apache-2.0", "dependencies": { "@adobe/react-spectrum": "3.38.0", @@ -2726,13 +2727,20 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@hello-pangea/dnd": "^18.0.1", "@internationalized/date": "^3.5.5", + "@react-aria/focus": "^3.21.0", + "@react-aria/i18n": "^3.12.11", + "@react-spectrum/label": "^3.16.17", + "@react-spectrum/overlays": "^5.8.0", "@react-spectrum/theme-default": "^3.5.1", "@react-spectrum/toast": "^3.0.0-beta.16", "@react-spectrum/utils": "^3.11.5", + "@react-stately/overlays": "^3.6.18", + "@react-stately/utils": "^3.10.8", "@react-types/combobox": "3.13.1", "@react-types/radio": "^3.8.1", "@react-types/shared": "^3.22.1", "@react-types/textfield": "^3.9.1", + "@spectrum-icons/ui": "^3.6.18", "bootstrap": "4.6.2", "classnames": "^2.3.1", "event-target-shim": "^6.0.2", @@ -2831,16 +2839,16 @@ } }, "node_modules/@deephaven/dashboard": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-1.17.1.tgz", - "integrity": "sha512-ToEAhv9Im/kumvv+OLJgQO27rc+jRJGoNbej4rbRRU2PQGjKh9+eJlYMA4kHM4jSQrM9EJUtmF5MAoU9fwwyPA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@deephaven/dashboard/-/dashboard-1.21.0.tgz", + "integrity": "sha512-OUwmEJ+k5Tnmg+QkGHRT2cRv+9oXxLfT1/zgT1ploKUYWU1J8KPY9CpnLLuIaXu9qu9C28aEpGnLeI/9D7zN5Q==", "license": "Apache-2.0", "dependencies": { - "@deephaven/components": "^1.17.0", - "@deephaven/golden-layout": "^1.17.1", + "@deephaven/components": "^1.21.0", + "@deephaven/golden-layout": "^1.21.0", "@deephaven/log": "^1.8.0", "@deephaven/react-hooks": "^1.14.0", - "@deephaven/redux": "^1.17.0", + "@deephaven/redux": "^1.19.0", "@deephaven/utils": "^1.10.0", "classnames": "^2.3.1", "fast-deep-equal": "^3.1.3", @@ -3001,12 +3009,12 @@ } }, "node_modules/@deephaven/golden-layout": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-1.17.1.tgz", - "integrity": "sha512-lnA87WSFcFoceK7DtsxNqKjEYCF7L427VxtMdMR7xU/tsTJUQnlT5MNR9BV/2Ybz8AR8Kh1qQv/3EmY1vlHGmA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@deephaven/golden-layout/-/golden-layout-1.21.0.tgz", + "integrity": "sha512-PW1mmUytVjcCIsNa0gErf1GIq0GHsUSFJXZAkrVuZSvCQ3UDWN9sQ6GjRXI3lkaue+o+QRMq0WYX34IwbZGV6g==", "license": "Apache-2.0", "dependencies": { - "@deephaven/components": "^1.17.0", + "@deephaven/components": "^1.21.0", "jquery": "^3.6.0", "nanoid": "^5.0.7" }, @@ -3146,6 +3154,10 @@ "resolved": "plugins/pivot/src/js", "link": true }, + "node_modules/@deephaven/js-plugin-pivot-builder": { + "resolved": "plugins/pivot-builder/src/js", + "link": true + }, "node_modules/@deephaven/js-plugin-plotly-express": { "resolved": "plugins/plotly-express/src/js", "link": true @@ -3320,12 +3332,12 @@ } }, "node_modules/@deephaven/react-hooks": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@deephaven/react-hooks/-/react-hooks-1.14.0.tgz", - "integrity": "sha512-VWRU6Hka5GyN0zO5LJYI5YgKrEsf0xAKrQ5LnEX4WSloB1C5DFoS1K1kH3fPqVBhid5JTu7R7oe0y4Tvt4wesQ==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@deephaven/react-hooks/-/react-hooks-1.21.1.tgz", + "integrity": "sha512-i0rx4hsoGyD8o91tyv600RPkbBd0JI4u4NYasttZXUEHNCjhTGrreG82ssrtlkFkzNgpQr7FI4GJBYPNFDCqcA==", "license": "Apache-2.0", "dependencies": { - "@adobe/react-spectrum": "3.38.0", + "@adobe/react-spectrum": "3.47.0", "@deephaven/log": "^1.8.0", "@deephaven/utils": "^1.10.0", "lodash.debounce": "^4.0.8", @@ -3339,6 +3351,46 @@ "react": ">=16.8.0" } }, + "node_modules/@deephaven/react-hooks/node_modules/@adobe/react-spectrum": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.47.0.tgz", + "integrity": "sha512-EDQuMzz0kUeiMUUlxoeLFQyyxOXaAC7qlBw2PYOUfFLYd87xcV7VVV0JxiYx8zGk1IIY3UgQHgXrS1fv7CgezQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@internationalized/date": "^3.12.1", + "@react-types/shared": "^3.34.0", + "@spectrum-icons/ui": "^3.7.0", + "@spectrum-icons/workflow": "^4.3.0", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "clsx": "^2.0.0", + "react-aria": "3.48.0", + "react-aria-components": "1.17.0", + "react-stately": "3.46.0", + "react-transition-group": "^4.4.5", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@deephaven/react-hooks/node_modules/@adobe/react-spectrum/node_modules/@spectrum-icons/workflow": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.1.tgz", + "integrity": "sha512-kDF+/EbFVyLGytotqqdYt4uSij4j/PQmDQO5km/C6DyzKjyuic3FnSBFinR+mA6oFv1OjMcLvrrDBqK3wbqRlA==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum-workflow": "2.3.5", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "@adobe/react-spectrum": "^3.47.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@deephaven/react-hooks/node_modules/nanoid": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", @@ -3358,9 +3410,9 @@ } }, "node_modules/@deephaven/redux": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-1.17.0.tgz", - "integrity": "sha512-pbq1Npd0JHkZDiK7gt5Oj4EVJuikQ76Jd0qoo20P5Ouan5M2iZg3HZNHVmicAJHA9p7+EmYAeXpHS52rUmQszg==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@deephaven/redux/-/redux-1.19.0.tgz", + "integrity": "sha512-ChzeUsaaoTMhM9Qrw9t0yCmEjdNNBoVf9RDfBQhTb2ifxNh2ZVxAgN4/yWB7kIlynfEmUwqYNQ7f2gcRNeEIHw==", "license": "Apache-2.0", "dependencies": { "@deephaven/jsapi-types": "^1.0.0-dev0.40.4", @@ -6998,6 +7050,20 @@ "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", "license": "MIT" }, + "node_modules/@react-aria/focus": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.22.0.tgz", + "integrity": "sha512-ZfDOVuVhqDsM9mkNji3QUZ/d40JhlVgXrDkrfXylM1035QCrcTHN7m2DpbE95sU2A8EQb4wikvt5jM6K/73BPg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-aria": "3.48.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@react-aria/i18n": { "version": "3.13.0", "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.13.0.tgz", @@ -7129,22 +7195,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/accordion/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/accordion/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7200,22 +7250,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/actionbar/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/actionbar/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7271,22 +7305,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/actiongroup/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/actiongroup/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7341,22 +7359,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/avatar/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/avatar/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7411,22 +7413,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/badge/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/badge/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7482,22 +7468,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/breadcrumbs/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/breadcrumbs/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7552,22 +7522,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/button/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/button/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7622,22 +7576,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/buttongroup/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/buttongroup/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7692,22 +7630,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/calendar/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/calendar/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7762,22 +7684,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/checkbox/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/checkbox/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7833,22 +7739,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/color/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/color/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7904,22 +7794,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/combobox/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/combobox/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -7974,22 +7848,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/contextualhelp/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/contextualhelp/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8044,22 +7902,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/datepicker/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/datepicker/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8114,22 +7956,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/dialog/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/dialog/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8184,22 +8010,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/divider/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/divider/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8256,22 +8066,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/dnd/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/dnd/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8326,22 +8120,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/dropzone/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/dropzone/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8410,22 +8188,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/form/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/form/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8480,22 +8242,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/icon/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/icon/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8550,22 +8296,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/illustratedmessage/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/illustratedmessage/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8620,22 +8350,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/image/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/image/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8690,14 +8404,13 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/inlinealert/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", + "node_modules/@react-spectrum/inlinealert/node_modules/@spectrum-icons/workflow": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", + "integrity": "sha512-ILuhgWh9jMXaEVMRuOYgTAjMc22cKyvCtUDyZmc8OEMfOYuejj+Gcp5t6DhaCfE0M9rORtVxCrRgsO2WyEgfUw==", "license": "Apache-2.0", "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", + "@adobe/react-spectrum-workflow": "2.3.5", "@swc/helpers": "^0.5.0" }, "peerDependencies": { @@ -8706,7 +8419,46 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/inlinealert/node_modules/@spectrum-icons/workflow": { + "node_modules/@react-spectrum/label": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@react-spectrum/label/-/label-3.17.0.tgz", + "integrity": "sha512-cv3cHYSOvKfDvjyYSZylyhxZHnWDEm6k0RvqxAv9DKu3KMPgNxiUHoQAWHhJ9pzz4Jqch7DF9ZiL9t6TNDfb3Q==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum": "3.47.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-spectrum/label/node_modules/@adobe/react-spectrum": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.47.0.tgz", + "integrity": "sha512-EDQuMzz0kUeiMUUlxoeLFQyyxOXaAC7qlBw2PYOUfFLYd87xcV7VVV0JxiYx8zGk1IIY3UgQHgXrS1fv7CgezQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@internationalized/date": "^3.12.1", + "@react-types/shared": "^3.34.0", + "@spectrum-icons/ui": "^3.7.0", + "@spectrum-icons/workflow": "^4.3.0", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "clsx": "^2.0.0", + "react-aria": "3.48.0", + "react-aria-components": "1.17.0", + "react-stately": "3.46.0", + "react-transition-group": "^4.4.5", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-spectrum/label/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", "integrity": "sha512-ILuhgWh9jMXaEVMRuOYgTAjMc22cKyvCtUDyZmc8OEMfOYuejj+Gcp5t6DhaCfE0M9rORtVxCrRgsO2WyEgfUw==", @@ -8760,22 +8512,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/labeledvalue/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/labeledvalue/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8831,22 +8567,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/layout/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/layout/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8901,22 +8621,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/link/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/link/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -8972,22 +8676,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/list/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/list/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9043,22 +8731,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/listbox/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/listbox/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9114,22 +8786,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/menu/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/menu/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9184,22 +8840,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/meter/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/meter/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9254,22 +8894,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/numberfield/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/numberfield/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9324,22 +8948,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/overlays/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/overlays/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9395,22 +9003,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/picker/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/picker/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9465,22 +9057,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/progress/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/progress/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9536,22 +9112,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/provider/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/provider/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9606,22 +9166,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/radio/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/radio/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9676,22 +9220,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/searchfield/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/searchfield/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9746,22 +9274,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/slider/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/slider/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9816,22 +9328,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/statuslight/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/statuslight/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9886,22 +9382,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/switch/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/switch/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -9943,32 +9423,16 @@ "@react-types/shared": "^3.34.0", "@spectrum-icons/ui": "^3.7.0", "@spectrum-icons/workflow": "^4.3.0", - "@swc/helpers": "^0.5.0", - "client-only": "^0.0.1", - "clsx": "^2.0.0", - "react-aria": "3.48.0", - "react-aria-components": "1.17.0", - "react-stately": "3.46.0", - "react-transition-group": "^4.4.5", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-spectrum/table/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "clsx": "^2.0.0", + "react-aria": "3.48.0", + "react-aria-components": "1.17.0", + "react-stately": "3.46.0", + "react-transition-group": "^4.4.5", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } @@ -10028,22 +9492,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/tabs/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/tabs/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10099,22 +9547,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/tag/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/tag/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10169,22 +9601,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/text/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/text/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10239,22 +9655,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/textfield/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/textfield/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10309,22 +9709,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/theme-dark/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/theme-dark/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10379,22 +9763,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/theme-default/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/theme-default/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10449,22 +9817,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/theme-light/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/theme-light/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10519,22 +9871,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/toast/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/toast/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10589,22 +9925,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/tooltip/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/tooltip/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10660,22 +9980,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/utils/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/utils/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10730,22 +10034,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/view/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/view/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10800,22 +10088,6 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-spectrum/well/node_modules/@spectrum-icons/ui": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", - "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-spectrum/well/node_modules/@spectrum-icons/workflow": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.0.tgz", @@ -10859,6 +10131,20 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-stately/overlays": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.7.0.tgz", + "integrity": "sha512-VyFlju6JqEUTyr+igrEjTeUi2MXw7IBOxWYzLoq26UJxf+45okqUWfyKRdXTvNjGJqQol9fqIg5Nv8fU4H/CvQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-stately": "3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@react-stately/radio": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.12.0.tgz", @@ -10873,6 +10159,20 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-stately/utils": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.12.0.tgz", + "integrity": "sha512-7q+iHz9cENvro1dVKgdTxNh1i1mtWuLUI6UHp10TAgpxM9DyRDvmuN35zLXYCmMDgx3WLY2xkwqoez8xd+CdxQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0", + "react-stately": "3.46.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@react-types/combobox": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.13.1.tgz", @@ -11491,6 +10791,22 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@spectrum-icons/ui": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.0.tgz", + "integrity": "sha512-86iQSDfJb3Ama1WSJ/mEiFy4DJT7e/v4pSmEuX4aKKMzbNYft+O40N18S2POUnmblrb7MQneLC/pgIp1SDBwEQ==", + "license": "Apache-2.0", + "dependencies": { + "@adobe/react-spectrum-ui": "1.2.1", + "@babel/runtime": "^7.24.4", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "@adobe/react-spectrum": "^3.47.0", + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@swc/core": { "version": "1.15.30", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.30.tgz", @@ -31665,6 +30981,43 @@ "node": "^18 || >=20" } }, + "plugins/pivot-builder/src/js": { + "name": "@deephaven/js-plugin-pivot-builder", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "@deephaven/components": "^1.17.0", + "@deephaven/dashboard": "^1.18.0", + "@deephaven/dashboard-core-plugins": "^1.18.0", + "@deephaven/icons": "^1.2.0", + "@deephaven/iris-grid": "^1.18.0", + "@deephaven/js-plugin-pivot": "*", + "@deephaven/jsapi-bootstrap": "^1.17.0", + "@deephaven/jsapi-types": "^1.0.0-dev0.39.6", + "@deephaven/jsapi-utils": "^1.16.0", + "@deephaven/log": "^1.8.0", + "@deephaven/plugin": "^1.18.0", + "@deephaven/react-hooks": "^1.18.0", + "@deephaven/utils": "^1.10.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fortawesome/fontawesome-svg-core": "^6.2.1", + "@fortawesome/react-fontawesome": "^0.2.0", + "fast-deep-equal": "^3.1.3" + }, + "devDependencies": { + "@deephaven-enterprise/jsapi-coreplus-types": "^1.20240517.518", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "plugins/pivot/src/js": { "name": "@deephaven/js-plugin-pivot", "version": "0.4.0", diff --git a/plans/DH-21476-pivot-builder-aggregate-values-wiring.md b/plans/DH-21476-pivot-builder-aggregate-values-wiring.md new file mode 100644 index 000000000..b0fa1f889 --- /dev/null +++ b/plans/DH-21476-pivot-builder-aggregate-values-wiring.md @@ -0,0 +1,266 @@ +# DH-21476 — Pivot Builder: Aggregate values wiring + +Phase 3 follow-up to +[DH-21476-pivot-builder-rollup-rows-wiring.md](./DH-21476-pivot-builder-rollup-rows-wiring.md). +Wires the **Aggregate values** card in `PivotConfigSection` to the +underlying `IrisGridModel` so it works: + +1. **standalone** (no rollup, no pivot) → totals row at top/bottom, and +2. **with rollup** (and later, with pivot) → aggregations merged into the + rollup config. + +Pivot composition is deferred to its own phase. This doc explicitly +ignores `pivotConfig` — when both pivot and aggregates are on, the +aggregates card just behaves as if no pivot were present. + +## TL;DR + +- Card state shape changes from "list of aggregate entries" (one fn + + many columns each) to the host-native [`AggregationSettings`][1] + shape (one operation per entry + ordered list). This unlocks reuse of + `IrisGridUtils.getModelRollupConfig` and the existing operation-map + builders. +- New sync effect in `CreatePivotPage` keyed on + `[model, aggregatesOn, aggregationSettings, rollupRowsOn, mockRollupRows, …]` + decides each render whether to push to `model.totalsConfig` (no + rollup) or to fold into `model.rollupConfig` (rollup active), then + clears the other. +- `AggregateRow` UI gains an inline expand that lets the user pick + operation + columns; "Edit" opens it (no separate modal). Picker + reuses the `ColumnPicker` introduced in the rollup-rows phase. +- Effect runs guarded by `deepEqual` to avoid resetting the model on + every render. + +## Background — how IrisGrid wires aggregations today + +Two model surfaces, both abstract on +[`IrisGridModel`](../../web-client-ui/packages/iris-grid/src/IrisGridModel.ts): + +| Property | Standalone aggregations? | Combined with rollup? | +| --------------------- | ------------------------ | --------------------- | +| `model.totalsConfig` | yes (totals row) | **must be cleared** | +| `model.rollupConfig` | n/a | yes (folded in) | + +`IrisGrid.tsx` composes both in [`getModelTotalsConfig`][2] and +[`getModelRollupConfig`][3], then `IrisGridModelUpdater` assigns them. +Critically: when rollup is on, `getModelTotalsConfig` returns `null` — +i.e. **rollup wins** and the totals row is suppressed. + +Source-of-truth state: +[`AggregationSettings`](../../web-client-ui/packages/iris-grid/src/sidebar/aggregations/Aggregations.tsx): + +```ts +type Aggregation = { + operation: AggregationOperation; // "Sum" | "Avg" | "Min" | … + selected: readonly string[]; // column names + invert: boolean; // when true, selected = excluded +}; +type AggregationSettings = { + aggregations: readonly Aggregation[]; + showOnTop: boolean; +}; +``` + +Two helpers from `IrisGrid` we will need (or re-implement small pieces +of): + +- `getOperationMap(columns, aggregations) → { [columnName]: operation[] }` + builds the per-column op array used by `UITotalsTableConfig`. +- `getOperationOrder(aggregations) → operation[]` preserves user order. + +Both currently live as instance methods on `IrisGrid` and aren't +exported. We can either: + +1. **Re-implement them inline in the plugin** (≤20 lines each, no + external deps), or +2. Push for promoting them to `IrisGridUtils` in `web-client-ui` first. + +Recommended: option 1 for now, with a `TODO(DH-21476): promote to +IrisGridUtils` comment. The current PR thread is already touching +iris-grid surface — defer the host change to avoid scope creep. + +## Phase A — Reshape card state (purely refactor, no behaviour change) + +Today's card state per entry is: + +```ts +type AggregateEntry = { id; fn: string; columns: string[] }; +``` + +This 1:many shape matches the design mock but **does not match +`Aggregation`**, which is 1:1 (one operation per entry, columns +multi-select). The mock UI groups by operation purely for display. + +Change `PivotConfigSection.tsx` so the props are: + +```ts +aggregationSettings: AggregationSettings; +onAggregationSettingsChange: (next: AggregationSettings) => void; +aggregatesOn: boolean; +onAggregatesOnChange: (next: boolean) => void; +``` + +Inside the card we keep rendering one row per `Aggregation` entry, +labelled `${operation} (${selected.join(', ')})`. `Add` opens an +operation picker (reuse `ColumnPicker` with the list of +`AggregationOperation` values minus already-used ones — matches +existing `Aggregations.tsx` UX). `Edit` toggles an inline expand below +the row with two controls: + +- operation `` row at the bottom listing all + `availableColumns` minus already-selected; pick a value → append + and remove the picker. + - Cancel button on the picker row. + - Same treatment is **not** applied to Pivot columns / Filterable + columns this phase (still placeholder). + +### Modified — plugin (optional, defer if scope creeps) + +- `plugins/pivot-builder/src/js/src/pivotBuilderModel.ts` + - Document the rollup/pivot mutex assumption in the file header. + - No code change needed; the existing proxy delegates `rollupConfig` + to the underlying `IrisGridProxyModel` for free. + +## Rollup ↔ Pivot interaction + +The two features are intentionally allowed to coexist in the UI; the +later "compose rollup + pivot" phase will define how they combine on +the model. For this phase only Rollup rows writes to the model, so +there is nothing to coordinate yet. No UI mutex is added. + +## Behaviour + +1. Open Create Pivot sidebar with a flat table. + - Card seed: empty Rollup rows (assuming no persisted rollup), both + footer checkboxes default to `true`. +2. Toggle Rollup rows ON, click Add, pick `Sym` → grid rerenders as + `table.rollup({ columns: ['Sym'], includeConstituents: true, + includeDescriptions: true })` plus an empty + `AggregationSettings` (so the call resolves to + `{ aggregations: { First: [] } }` via + `getModelRollupConfig` — the host's existing behaviour for + "rollup with no explicit aggregations"). +3. Toggle the "Non-aggregated in rollup rows" checkbox off → grid + rerenders without the auto-`First` aggregation. +4. Remove the last column → `model.rollupConfig = null` reverts the + grid. +5. The legacy column-selector block at the bottom of the page is no + longer rendered (commented out). The previous Pivot Apply path is + therefore unreachable from the UI this phase; the proxy still + exposes `pivotConfig` so a follow-up phase can wire it through the + Pivot columns / Aggregate values cards. +6. Reopening the panel re-seeds the Rollup rows card from + `model.rollupConfig` (if any). + +## Out of scope (still) + +- Wiring Pivot columns, Aggregate values, or Add filterable columns to + any model state. +- Composing rollup + pivot on the same model (later phase). +- Editing an existing aggregate via the pencil button. +- Drag-and-drop reordering for the Rollup rows list. +- Deleting the commented-out legacy selectors. They stay as inert + reference until Pivot columns / Aggregate values are wired. +- Migrating away from the host's existing Rollups & Aggregations + sidebar (this card-based UI is purely additive for now). +- Tests. + +## Phases & Steps + +### Phase 1 — Plumb `model.rollupConfig` + +1. Add a `useEffect` in `CreatePivotPage` keyed on + `[model, rollupRowsOn, mockRollupRows, mockIncludeConstituents, + mockNonAggregatedInRollup]` that: + a. Bails if `!isPivotBuilderIrisGridModel(model)`. + b. Builds a `UIRollupConfig` from state (or `undefined` when the + card is OFF / empty). + c. Calls `IrisGridUtils.getModelRollupConfig(model.sourceTable.columns, + uiConfig, { aggregations: [], showOnTop: false })`. + d. Compares against `model.rollupConfig` via `fast-deep-equal`; if + different, assigns. Empty/OFF → assign `null`. +2. Seed the three pieces of state from `model.rollupConfig` on mount + (read once via `useState` initializer). +3. Comment out the legacy Row keys / Column keys / aggregation + function / Columns selectors and Apply/Reset buttons in the same + commit so the UI stops showing two ways to do the same thing. + +### Phase 2 — Real column picker for Rollup rows + +1. Update `PivotConfigSection.tsx` to expose a per-card "picker mode" + (controlled prop or local state in `ConfigCard`). +2. Render an inline ` focus. + const id = requestAnimationFrame(() => selectRef.current?.focus()); + return () => cancelAnimationFrame(id); + }, []); + + const isColumnValid = useCallback( + (name: string): boolean => { + const t = columnTypes[name]; + if (t == null) return true; + return AggregationUtils.isValidOperation( + operation as AggregationOperation, + t + ); + }, + [columnTypes, operation] + ); + + // Drop any selections that aren't valid for the current operation. + useEffect(() => { + setSelected(prev => { + let changed = false; + const next = new Set(); + prev.forEach(name => { + if (isColumnValid(name)) next.add(name); + else changed = true; + }); + return changed ? next : prev; + }); + }, [isColumnValid]); + + const filteredColumns = useMemo(() => { + const q = query.trim().toLowerCase(); + return q === '' + ? availableColumns + : availableColumns.filter(c => c.toLowerCase().includes(q)); + }, [availableColumns, query]); + + useEffect(() => { + function handleClickOutside(e: MouseEvent): void { + if ( + containerRef.current != null && + e.target instanceof Node && + !containerRef.current.contains(e.target) + ) { + onClose(); + } + } + function handleKey(e: KeyboardEvent): void { + if (e.key === 'Escape') onClose(); + } + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKey); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKey); + }; + }, [onClose]); + + const toggleColumn = useCallback( + (name: string) => { + if (!isColumnValid(name)) return; + setSelected(prev => { + const next = new Set(prev); + if (next.has(name)) next.delete(name); + else next.add(name); + return next; + }); + }, + [isColumnValid] + ); + + const handleSelectAll = useCallback(() => { + setSelected(prev => { + const next = new Set(prev); + filteredColumns.forEach(c => { + if (isColumnValid(c)) next.add(c); + }); + return next; + }); + }, [filteredColumns, isColumnValid]); + + const handleClear = useCallback(() => { + setSelected(new Set()); + }, []); + + const handleCommit = useCallback(() => { + onCommit({ + operation: operation as AggregationOperation, + // Preserve order of availableColumns for stable output. + selected: availableColumns.filter(c => selected.has(c)), + invert: false, + }); + }, [operation, selected, availableColumns, onCommit]); + + return createPortal( +
+
+
Select aggregation
+ +
+
+
+ Select column(s) + * +
+ setQuery(e.target.value)} + /> +
+ {filteredColumns.length === 0 ? ( +
No columns
+ ) : ( + filteredColumns.map(name => { + const valid = isColumnValid(name); + return ( + toggleColumn(name)} + > + {name} + + ); + }) + )} +
+
+
+ + + + +
+
, + document.body + ); +} + +export function PivotConfigSection({ + availableColumns, + columnTypes, + rollupRows, + onRollupRowsChange, + rollupRowsOn, + onRollupRowsOnChange, + pivotColumns, + onPivotColumnsChange, + pivotColumnsOn, + onPivotColumnsOnChange, + pivotColumnsDisabled, + aggregationSettings, + onAggregationSettingsChange, + aggregatesOn, + onAggregatesOnChange, + filterableColumns, + onFilterableColumnsChange, + filterableColumnsOn, + onFilterableColumnsOnChange, + includeConstituents, + onIncludeConstituentsChange, + nonAggregatedInRollup, + onNonAggregatedInRollupChange, + canUndo, + canRedo, + onUndo, + onRedo, + onClearAll, +}: PivotConfigSectionProps): JSX.Element { + const [rollupPickerOpen, setRollupPickerOpen] = useState(false); + const [pivotPickerOpen, setPivotPickerOpen] = useState(false); + // `null` = closed. `{ mode: 'add' }` = adding new. `{ mode: 'edit', index }` + // = editing existing entry. + const [aggPickerState, setAggPickerState] = useState< + { mode: 'add' } | { mode: 'edit'; index: number } | null + >(null); + // Tracks the source droppable while a drag is in progress; null when + // nothing is being dragged. Used to toggle the `is-dragging` modifier + // on the root so the drop zones render the marching-ants effect. + const [dragSource, setDragSource] = useState(null); + // Id of the droppable/row currently under the pointer during a drag. + // Drives the cross-card insertion indicator; null when idle. + const [overId, setOverId] = useState(null); + + // Local toggle for the "Show hidden columns in menu" overflow item. There + // is no hidden-columns concept threaded through the plugin yet, so this + // simply tracks the checkmark state for now. + const [showHiddenColumns, setShowHiddenColumns] = useState(false); + + // Only one popover (Add picker) may be open at a time across the cards. + // Opening any Add picker or overflow menu dismisses the others. + const closeAllPickers = useCallback(() => { + setRollupPickerOpen(false); + setPivotPickerOpen(false); + setAggPickerState(null); + }, []); + + const handleAddRollupRow = useCallback(() => { + setPivotPickerOpen(false); + setAggPickerState(null); + setRollupPickerOpen(open => !open); + }, []); + + const handlePickRollupRow = useCallback( + (name: string) => { + onRollupRowsChange([...rollupRows, name]); + setRollupPickerOpen(false); + }, + [rollupRows, onRollupRowsChange] + ); + + const handleAddPivotColumn = useCallback(() => { + setRollupPickerOpen(false); + setAggPickerState(null); + setPivotPickerOpen(open => !open); + }, []); + + const handlePickPivotColumn = useCallback( + (name: string) => { + onPivotColumnsChange([...pivotColumns, name]); + setPivotPickerOpen(false); + }, + [pivotColumns, onPivotColumnsChange] + ); + + const usedOperations = useMemo( + () => aggregationSettings.aggregations.map(a => a.operation as string), + [aggregationSettings.aggregations] + ); + + // Map of operation -> selected columns, so the Add picker can show the + // columns already chosen for whichever function is selected. + const aggSelectionsByOperation = useMemo< + Record + >(() => { + const map: Record = {}; + aggregationSettings.aggregations.forEach(a => { + map[a.operation as string] = a.selected; + }); + return map; + }, [aggregationSettings.aggregations]); + + const selectableOperations = useMemo( + () => + SELECTABLE_OPERATIONS.filter( + op => !AggregationUtils.isRollupProhibited(op) + ).map(op => op as string), + [] + ); + + const closeAggPicker = useCallback(() => setAggPickerState(null), []); + + const handleAddAggregate = useCallback(() => { + setRollupPickerOpen(false); + setPivotPickerOpen(false); + setAggPickerState(s => (s?.mode === 'add' ? null : { mode: 'add' })); + }, []); + + const handleCommitAggregate = useCallback( + (next: Aggregation) => { + const aggregations = aggregationSettings.aggregations.slice(); + if (aggPickerState?.mode === 'edit') { + aggregations[aggPickerState.index] = next; + } else { + // Operations are unique per card: if an entry for this function + // already exists, merge the new columns into it (de-duped, order + // preserved) instead of pushing a duplicate entry. + const existingIndex = aggregations.findIndex( + a => a.operation === next.operation + ); + if (existingIndex >= 0) { + const existing = aggregations[existingIndex]; + const selected = [...existing.selected]; + next.selected.forEach(col => { + if (!selected.includes(col)) { + selected.push(col); + } + }); + aggregations[existingIndex] = { ...existing, selected }; + } else { + aggregations.push(next); + } + } + onAggregationSettingsChange({ ...aggregationSettings, aggregations }); + setAggPickerState(null); + }, + [aggPickerState, aggregationSettings, onAggregationSettingsChange] + ); + + const handleChangeAggregateOperation = useCallback( + (index: number, nextOp: string) => { + const aggregations = aggregationSettings.aggregations.slice(); + const current = aggregations[index]; + if (current == null || current.operation === nextOp) { + return; + } + // Operations are unique per card; ignore a change that collides with + // an operation already used by another entry. + if (aggregations.some((a, i) => i !== index && a.operation === nextOp)) { + return; + } + aggregations[index] = { + ...current, + operation: nextOp as AggregationOperation, + }; + onAggregationSettingsChange({ ...aggregationSettings, aggregations }); + }, + [aggregationSettings, onAggregationSettingsChange] + ); + + // Ungrouped layout (pivot/rollup present): change the function of a + // single function/column pair. Moves the column out of its current entry + // into the entry for the target function (creating one if needed). The + // underlying model keeps one entry per operation, so the column merges + // into any existing entry for `nextOp`. + const handleChangeAggregatePairOperation = useCallback( + (entryIndex: number, column: string, nextOp: string) => { + let aggregations = aggregationSettings.aggregations.map(a => ({ + ...a, + selected: a.selected.slice(), + })); + const source = aggregations[entryIndex]; + if (source == null || source.operation === nextOp) { + return; + } + if (column === '') { + // Empty placeholder entry: relabel it, or drop it if the target + // already exists. + const dest = aggregations.find(a => a.operation === nextOp); + if (dest == null) { + source.operation = nextOp as AggregationOperation; + } else { + aggregations = aggregations.filter(a => a !== source); + } + } else { + source.selected = source.selected.filter(c => c !== column); + const dest = aggregations.find(a => a.operation === nextOp); + if (dest == null) { + aggregations.push({ + operation: nextOp as AggregationOperation, + selected: [column], + invert: false, + }); + } else if (!dest.selected.includes(column)) { + dest.selected.push(column); + } + if (source.selected.length === 0) { + aggregations = aggregations.filter(a => a !== source); + } + } + onAggregationSettingsChange({ ...aggregationSettings, aggregations }); + }, + [aggregationSettings, onAggregationSettingsChange] + ); + + // Ungrouped layout: remove a single function/column pair. Removes the + // column from its entry, dropping the entry if it becomes empty. + const handleDeleteAggregatePair = useCallback( + (entryIndex: number, column: string) => { + let aggregations = aggregationSettings.aggregations.map(a => ({ + ...a, + selected: a.selected.slice(), + })); + const source = aggregations[entryIndex]; + if (source == null) { + return; + } + if (column === '') { + aggregations = aggregations.filter(a => a !== source); + } else { + source.selected = source.selected.filter(c => c !== column); + if (source.selected.length === 0) { + aggregations = aggregations.filter(a => a !== source); + } + } + onAggregationSettingsChange({ ...aggregationSettings, aggregations }); + }, + [aggregationSettings, onAggregationSettingsChange] + ); + + const handleDeleteAggregate = useCallback( + (index: number) => { + const aggregations = aggregationSettings.aggregations.filter( + (_, i) => i !== index + ); + onAggregationSettingsChange({ ...aggregationSettings, aggregations }); + setAggPickerState(curr => { + if (curr?.mode !== 'edit') return curr; + if (curr.index === index) return null; + return curr.index > index + ? { mode: 'edit', index: curr.index - 1 } + : curr; + }); + }, + [aggregationSettings, onAggregationSettingsChange] + ); + + // Operations available to a given picker invocation. "Add" always lists + // every selectable function (including ones already in use); "edit" + // excludes the operations used by other entries but keeps the current. + const pickerAvailableOps = useMemo(() => { + if (aggPickerState == null || aggPickerState.mode === 'add') { + return selectableOperations; + } + const currentOp = + aggregationSettings.aggregations[aggPickerState.index]?.operation; + return selectableOperations.filter( + op => op === currentOp || !usedOperations.includes(op) + ); + }, [ + aggPickerState, + aggregationSettings, + selectableOperations, + usedOperations, + ]); + + const pickerInitial = useMemo(() => { + if (aggPickerState?.mode === 'edit') { + const e = aggregationSettings.aggregations[aggPickerState.index]; + if (e != null) return e; + } + return { + operation: + (pickerAvailableOps[0] as AggregationOperation) ?? + AggregationOperation.SUM, + selected: [], + invert: false, + }; + }, [aggPickerState, aggregationSettings, pickerAvailableOps]); + + const removeAt = (arr: T[], index: number): T[] => { + const next = arr.slice(); + next.splice(index, 1); + return next; + }; + + const moveItem = (arr: readonly T[], from: number, to: number): T[] => { + const next = arr.slice(); + if (from === to) return next; + const [item] = next.splice(from, 1); + next.splice(to, 0, item); + return next; + }; + + // Flip `dragSource` in `onDragStart`. With @dnd-kit's + // MeasuringStrategy.Always (set on the DndContext), every droppable + // is re-measured continuously, so the empty drop-zones can expand + // from 0px to their full hit-area after the drag starts and the + // marching-ants class is applied. + // Track the active draggable's id for the DragOverlay preview. + const [activeId, setActiveId] = useState(null); + // Container of the in-flight drag. Unlike `activeId`/`dragSource` (cleared + // at the top of `handleDragEnd`), this survives the drop so the drag + // overlay can still tell what kind of item it just released while it plays + // its drop animation. Reset only on the next drag start. + const activeContainerRef = useRef(null); + const handleDragStart = useCallback((event: DragStartEvent): void => { + const container = String(event.active.data.current?.container ?? ''); + activeContainerRef.current = container === '' ? null : container; + setDragSource(container === '' ? null : container); + setActiveId(String(event.active.id)); + setOverId(null); + }, []); + + const handleDragOver = useCallback((event: DragOverEvent): void => { + const { over } = event; + setOverId(over == null ? null : String(over.id)); + }, []); + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 4 } }) + ); + + // Only measure droppables continuously *while dragging*. With + // `MeasuringStrategy.Always` left on permanently, dnd-kit keeps its + // droppable ResizeObserver/MutationObserver active when idle, so any + // body-portal overlay (e.g. the Spectrum overflow menu) shifts layout, + // triggers a re-measure, and re-renders this whole subtree — which + // closes the just-opened menu and flickers the trigger. `WhileDragging` + // (the default) disables idle measuring; we switch to `Always` for the + // duration of a drag so empty drop-zones still expand to their full + // hit-area after the marching-ants class is applied. + const measuring = useMemo( + () => ({ + droppable: { + strategy: + activeId != null + ? MeasuringStrategy.Always + : MeasuringStrategy.WhileDragging, + }, + }), + [activeId] + ); + + const resolveContainerOfId = useCallback((id: string): string | null => { + // Container ids are exact matches; item ids are namespaced as + // `${container}:...`. + if ( + id === ROLLUP_ROWS_DROPPABLE || + id === PIVOT_COLUMNS_DROPPABLE || + id === AGGREGATIONS_DROPPABLE + ) { + return id; + } + const colonIdx = id.indexOf(':'); + return colonIdx === -1 ? null : id.slice(0, colonIdx); + }, []); + + const handleDragEnd = useCallback( + (event: DragEndEvent): void => { + setDragSource(null); + setActiveId(null); + setOverId(null); + const { active, over } = event; + if (over == null) return; + + const activeIdStr = String(active.id); + const overIdStr = String(over.id); + const fromId = resolveContainerOfId(activeIdStr); + const toId = resolveContainerOfId(overIdStr); + if (fromId == null || toId == null) return; + + // Aggregations are a separate scope — reorder only. + if (fromId === AGGREGATIONS_DROPPABLE) { + if (toId !== AGGREGATIONS_DROPPABLE) return; + + // Grouped layout (no pivot/rollup): ids are `aggregations:` + // and each row is a whole entry — reorder the entries directly. + const groupedFromIdx = aggregationSettings.aggregations.findIndex( + entry => aggregationRowId(entry.operation as string) === activeIdStr + ); + if (groupedFromIdx >= 0) { + const toIdx = + overIdStr === AGGREGATIONS_DROPPABLE + ? aggregationSettings.aggregations.length - 1 + : aggregationSettings.aggregations.findIndex( + entry => + aggregationRowId(entry.operation as string) === overIdStr + ); + if (toIdx < 0 || groupedFromIdx === toIdx) return; + onAggregationSettingsChange({ + ...aggregationSettings, + aggregations: moveItem( + aggregationSettings.aggregations, + groupedFromIdx, + toIdx + ), + }); + return; + } + + // Ungrouped layout (pivot/rollup active): ids are + // `aggregations:\u0000`, one row per function/column + // pair. Reorder the flat list of pairs, then rebuild one entry per + // operation (in order of first appearance). The model can't represent + // interleaved operations, so a column dropped between two columns of a + // different operation regroups under its own entry. + const pairs: { operation: string; column: string }[] = []; + const pairIds: string[] = []; + aggregationSettings.aggregations.forEach(entry => { + // Count is collapsed into a single column-less row in this layout, + // so it reorders as one unit (matching the rendered row id). + if ( + entry.selected.length === 0 || + isCountOperation(entry.operation as string) + ) { + pairs.push({ operation: entry.operation as string, column: '' }); + pairIds.push(aggregationPairId(entry.operation as string, '')); + } else { + entry.selected.forEach(column => { + pairs.push({ operation: entry.operation as string, column }); + pairIds.push( + aggregationPairId(entry.operation as string, column) + ); + }); + } + }); + const fromIdx = pairIds.indexOf(activeIdStr); + if (fromIdx < 0) return; + const toIdx = + overIdStr === AGGREGATIONS_DROPPABLE + ? pairIds.length - 1 + : pairIds.indexOf(overIdStr); + if (toIdx < 0 || fromIdx === toIdx) return; + + const reordered = moveItem(pairs, fromIdx, toIdx); + + const invertByOp = new Map(); + // Count collapses to a column-less pair above, so its columns are not + // carried in `reordered`; preserve the original selection so the + // reorder stays display-only and the data round-trips. + const selectedByOp = new Map(); + aggregationSettings.aggregations.forEach(entry => { + invertByOp.set(entry.operation as string, entry.invert); + selectedByOp.set(entry.operation as string, entry.selected.slice()); + }); + const byOp = new Map(); + reordered.forEach(({ operation, column }) => { + let entry = byOp.get(operation); + if (entry == null) { + entry = { + selected: [], + invert: invertByOp.get(operation) ?? false, + }; + byOp.set(operation, entry); + } + if (column !== '') { + entry.selected.push(column); + } + }); + // Restore Count's original columns (it contributes a column-less pair). + byOp.forEach((entry, operation) => { + if (isCountOperation(operation)) { + const original = selectedByOp.get(operation); + if (original != null) { + entry.selected.push(...original); + } + } + }); + onAggregationSettingsChange({ + ...aggregationSettings, + aggregations: Array.from(byOp.entries()).map( + ([operation, { selected, invert }]) => ({ + operation: operation as AggregationOperation, + selected, + invert, + }) + ), + }); + return; + } + // Columns can never land in the aggregations list. + if (toId === AGGREGATIONS_DROPPABLE) return; + + const lists: Record< + string, + { items: string[]; set: (next: string[]) => void } + > = { + [ROLLUP_ROWS_DROPPABLE]: { + items: rollupRows, + set: onRollupRowsChange, + }, + [PIVOT_COLUMNS_DROPPABLE]: { + items: pivotColumns, + set: onPivotColumnsChange, + }, + }; + const from = lists[fromId]; + const to = lists[toId]; + if (from == null || to == null) return; + + // Recover the moved column name from the active id + // (`${container}:${name}`). + const colonIdx = activeIdStr.indexOf(':'); + if (colonIdx === -1) return; + const moved = activeIdStr.slice(colonIdx + 1); + const fromIdx = from.items.indexOf(moved); + if (fromIdx < 0) return; + + let toIdx: number; + if (overIdStr === toId) { + // Dropped on container background — append. + toIdx = to.items.length; + } else { + const overColon = overIdStr.indexOf(':'); + const overName = + overColon === -1 ? overIdStr : overIdStr.slice(overColon + 1); + const overIdx = to.items.indexOf(overName); + toIdx = overIdx < 0 ? to.items.length : overIdx; + } + + if (fromId === toId) { + if (fromIdx === toIdx) return; + from.set(moveItem(from.items, fromIdx, toIdx)); + return; + } + + // Cross-list move. Drop silently if the column already exists in + // the destination list (no duplicates within a card). + if (to.items.includes(moved)) return; + from.set(removeAt(from.items.slice(), fromIdx)); + const nextTo = to.items.slice(); + nextTo.splice(Math.min(toIdx, nextTo.length), 0, moved); + to.set(nextTo); + }, + [ + aggregationSettings, + onAggregationSettingsChange, + onPivotColumnsChange, + onRollupRowsChange, + pivotColumns, + resolveContainerOfId, + rollupRows, + ] + ); + + const handleDragCancel = useCallback((): void => { + setDragSource(null); + setActiveId(null); + setOverId(null); + }, []); + + // Index at which a cross-card insertion indicator should render in the + // column list `targetContainer` (or null for none). Only shown for a + // column drag originating in the *other* column card — same-card reorders + // already open a gap via SortableContext. The index mirrors the drop + // position computed in `handleDragEnd`: before the hovered row, or at the + // end when hovering the empty container background. + const columnInsertionIndex = useCallback( + (targetContainer: string, items: readonly string[]): number | null => { + if (activeId == null || overId == null) { + return null; + } + const activeContainer = resolveContainerOfId(activeId); + if ( + activeContainer == null || + activeContainer === targetContainer || + (activeContainer !== ROLLUP_ROWS_DROPPABLE && + activeContainer !== PIVOT_COLUMNS_DROPPABLE) + ) { + return null; + } + if (resolveContainerOfId(overId) !== targetContainer) { + return null; + } + if (overId === targetContainer) { + return items.length; + } + const overColon = overId.indexOf(':'); + const overName = overColon === -1 ? overId : overId.slice(overColon + 1); + const idx = items.indexOf(overName); + return idx < 0 ? items.length : idx; + }, + [activeId, overId, resolveContainerOfId] + ); + + const rollupItemIds = useMemo( + () => rollupRows.map(n => columnRowId(ROLLUP_ROWS_DROPPABLE, n)), + [rollupRows] + ); + const pivotItemIds = useMemo( + () => pivotColumns.map(n => columnRowId(PIVOT_COLUMNS_DROPPABLE, n)), + [pivotColumns] + ); + + // Columns already used by either the Rollup rows or Pivot columns card. + // Excluded from both Add pickers so a column can't be selected twice. + const usedColumns = useMemo( + () => [...rollupRows, ...pivotColumns], + [rollupRows, pivotColumns] + ); + + const pivotActive = + pivotColumnsOn && pivotColumns.length > 0 && pivotColumnsDisabled !== true; + const rollupActive = rollupRowsOn && rollupRows.length > 0; + + // Transient undo/redo, surfaced in every card's overflow (⋮) menu just + // before the Clear items. Shared section + disabled keys so all three + // menus offer the same actions; the keys are disabled when there is no + // history to traverse in that direction. + const undoRedoSection = useMemo( + () => ({ + key: 'undoRedo', + items: [ + { + key: 'undo', + label: 'Undo', + shortcut: GLOBAL_SHORTCUTS.UNDO.getDisplayText(), + }, + { + key: 'redo', + label: 'Redo', + shortcut: GLOBAL_SHORTCUTS.REDO.getDisplayText(), + }, + ], + }), + [] + ); + const undoRedoDisabledKeys = useMemo( + () => [...(canUndo ? [] : ['undo']), ...(canRedo ? [] : ['redo'])], + [canUndo, canRedo] + ); + + // Items for the Rollup card overflow (⋮) menu. Memoized so the Spectrum + // `Menu` keeps a stable `sections` reference across parent renders. Each + // section is separated by a divider. The leading sections are + // aggregate-wide actions/toggles; the final section holds the rollup-row + // toggles, which show a checkmark when enabled and are disabled while a + // pivot is active. + const rollupMenuSections = useMemo( + () => [ + { + key: 'rollupToggles', + items: [ + { + key: 'includeConstituents', + label: 'Include constituents in rollup rows', + isSelected: includeConstituents, + }, + { + key: 'nonAggregatedInRollup', + label: 'Non-aggregated in rollup rows', + isSelected: nonAggregatedInRollup, + }, + ], + }, + { + key: 'showHiddenColumns', + items: [ + { + key: 'showHiddenColumns', + label: 'Show hidden columns in menu', + isSelected: showHiddenColumns, + }, + ], + }, + undoRedoSection, + { + key: 'clearAllRollupRows', + items: [ + { + key: 'clearAllRollupRows', + label: 'Clear all rollup rows', + }, + { + key: 'clearAll', + label: 'Clear all', + }, + ], + }, + ], + [ + showHiddenColumns, + includeConstituents, + nonAggregatedInRollup, + undoRedoSection, + ] + ); + + const rollupMenuDisabledKeys = useMemo( + () => [ + ...(pivotActive ? ['includeConstituents', 'nonAggregatedInRollup'] : []), + ...undoRedoDisabledKeys, + ], + [pivotActive, undoRedoDisabledKeys] + ); + + // Items for the Aggregate values card overflow (⋮) menu. Shares the three + // aggregate-wide actions/toggles with the Rollup menu, each in its own + // section so a divider is drawn between them. + const aggregateMenuSections = useMemo( + () => [ + { + key: 'moveTotalsToTop', + items: [ + { + key: 'moveTotalsToTop', + label: 'Move totals to top', + isSelected: aggregationSettings.showOnTop, + }, + ], + }, + { + key: 'showHiddenColumns', + items: [ + { + key: 'showHiddenColumns', + label: 'Show hidden columns in menu', + isSelected: showHiddenColumns, + }, + ], + }, + undoRedoSection, + { + key: 'clearAllAggregations', + items: [ + { + key: 'clearAllAggregations', + label: 'Clear all aggregations', + }, + { + key: 'clearAll', + label: 'Clear all', + }, + ], + }, + ], + [aggregationSettings.showOnTop, showHiddenColumns, undoRedoSection] + ); + + // Items for the Pivot columns card overflow (⋮) menu. Each item in its + // own section so a divider is drawn between them. + const pivotMenuSections = useMemo( + () => [ + { + key: 'showHiddenColumns', + items: [ + { + key: 'showHiddenColumns', + label: 'Show hidden columns in menu', + isSelected: showHiddenColumns, + }, + ], + }, + undoRedoSection, + { + key: 'clearAllPivotColumns', + items: [ + { + key: 'clearAllPivotColumns', + label: 'Clear all pivot columns', + }, + { + key: 'clearAll', + label: 'Clear all', + }, + ], + }, + ], + [showHiddenColumns, undoRedoSection] + ); + + const handleConfigMenuAction = useCallback( + (key: string) => { + if (key === 'undo') { + onUndo(); + } else if (key === 'redo') { + onRedo(); + } else if (key === 'includeConstituents') { + onIncludeConstituentsChange(!includeConstituents); + } else if (key === 'nonAggregatedInRollup') { + onNonAggregatedInRollupChange(!nonAggregatedInRollup); + } else if (key === 'moveTotalsToTop') { + onAggregationSettingsChange({ + ...aggregationSettings, + showOnTop: !aggregationSettings.showOnTop, + }); + } else if (key === 'showHiddenColumns') { + setShowHiddenColumns(prev => !prev); + } else if (key === 'clearAllAggregations') { + onAggregationSettingsChange({ + ...aggregationSettings, + aggregations: [], + }); + } else if (key === 'clearAllRollupRows') { + onRollupRowsChange([]); + } else if (key === 'clearAllPivotColumns') { + onPivotColumnsChange([]); + } else if (key === 'clearAll') { + onClearAll(); + } + }, + [ + includeConstituents, + nonAggregatedInRollup, + aggregationSettings, + onIncludeConstituentsChange, + onNonAggregatedInRollupChange, + onAggregationSettingsChange, + onRollupRowsChange, + onPivotColumnsChange, + onUndo, + onRedo, + onClearAll, + ] + ); + + // With neither a pivot nor a rollup configured, the aggregate card groups + // columns by function (one row per function). When a pivot or rollup is + // present we keep the same two-line picker layout but list every + // function/column pair separately (ungrouped). + const onlyAggregates = !pivotActive && !rollupActive; + + // Flattened function/column pairs for the ungrouped layout. Entries with + // no columns yet are surfaced as a single placeholder pair so the row + // still renders. + const aggregatePairs = useMemo(() => { + const pairs: { + operation: string; + column: string; + entryIndex: number; + columnIndex: number; + }[] = []; + aggregationSettings.aggregations.forEach((entry, entryIndex) => { + // Count collapses into a single column-less row regardless of how many + // columns it has selected (display-only; the entry keeps its columns). + if ( + entry.selected.length === 0 || + isCountOperation(entry.operation as string) + ) { + pairs.push({ + operation: entry.operation as string, + column: '', + entryIndex, + columnIndex: -1, + }); + } else { + entry.selected.forEach((column, columnIndex) => { + pairs.push({ + operation: entry.operation as string, + column, + entryIndex, + columnIndex, + }); + }); + } + }); + return pairs; + }, [aggregationSettings.aggregations]); + + const aggItemIds = useMemo( + () => + onlyAggregates + ? aggregationSettings.aggregations.map(entry => + aggregationRowId(entry.operation as string) + ) + : aggregatePairs.map(p => aggregationPairId(p.operation, p.column)), + [onlyAggregates, aggregationSettings.aggregations, aggregatePairs] + ); + + // Resolve the preview for DragOverlay. + const activeColumnName = (() => { + if (activeId == null) { + return null; + } + const container = resolveContainerOfId(activeId); + if ( + container !== ROLLUP_ROWS_DROPPABLE && + container !== PIVOT_COLUMNS_DROPPABLE + ) { + return null; + } + const colonIdx = activeId.indexOf(':'); + return colonIdx === -1 ? null : activeId.slice(colonIdx + 1); + })(); + const activeAggregation = (() => { + if (activeId == null) { + return null; + } + const container = resolveContainerOfId(activeId); + if (container !== AGGREGATIONS_DROPPABLE) { + return null; + } + const colonIdx = activeId.indexOf(':'); + if (colonIdx === -1) { + return null; + } + // Grouped layout ids are `AGGREGATIONS:`; ungrouped + // (function/column pair) ids are `AGGREGATIONS:\u0000`. + const suffix = activeId.slice(colonIdx + 1); + const sepIdx = suffix.indexOf('\u0000'); + if (sepIdx === -1) { + return ( + aggregationSettings.aggregations.find(a => a.operation === suffix) ?? + null + ); + } + const operation = suffix.slice(0, sepIdx); + const column = suffix.slice(sepIdx + 1); + const entry = aggregationSettings.aggregations.find( + a => a.operation === operation + ); + if (entry == null) { + return null; + } + // Preview just the single dragged column so the overlay matches the row. + return column === '' ? entry : { ...entry, selected: [column] }; + })(); + + return ( + + +
+ + } + picker={anchorRef => + rollupPickerOpen ? ( + setRollupPickerOpen(false)} + /> + ) : null + } + > + + {withDropIndicator( + rollupRows.map((name, i) => ( + onRollupRowsChange(removeAt(rollupRows, i))} + /> + )), + columnInsertionIndex(ROLLUP_ROWS_DROPPABLE, rollupRows) + )} + + + + + } + picker={anchorRef => + pivotPickerOpen ? ( + setPivotPickerOpen(false)} + /> + ) : null + } + > + + {withDropIndicator( + pivotColumns.map((name, i) => ( + + onPivotColumnsChange(removeAt(pivotColumns, i)) + } + /> + )), + columnInsertionIndex(PIVOT_COLUMNS_DROPPABLE, pivotColumns) + )} + + + + + } + picker={anchorRef => + aggPickerState != null ? ( + + ) : null + } + > + + {onlyAggregates + ? aggregationSettings.aggregations.map((entry, i) => ( + + op === entry.operation || !usedOperations.includes(op) + )} + onOperationChange={op => + handleChangeAggregateOperation(i, op) + } + onDelete={() => handleDeleteAggregate(i)} + /> + )) + : aggregatePairs.map(pair => ( + + handleChangeAggregatePairOperation( + pair.entryIndex, + pair.column, + op + ) + } + onDelete={() => + handleDeleteAggregatePair(pair.entryIndex, pair.column) + } + /> + ))} + + + + {/* Filterable columns card hidden for now \u2014 props are still threaded + through so it can be re-enabled without churn. */} +
+ {createPortal( + // Aggregations can't be interleaved across operations (the pivot + // payload groups columns by operation), so a cross-operation drop + // regroups the dragged row back into its own operation. Play the + // default drop animation for aggregation drags so that snap-back is + // visible instead of looking like a no-op; column drags keep the + // instant drop (no animation). + + {activeColumnName != null ? ( + + ) : activeAggregation != null ? ( + + ) : null} + , + document.body + )} +
+ ); +} + +export default PivotConfigSection; diff --git a/plugins/pivot-builder/src/js/src/PivotServiceContext.ts b/plugins/pivot-builder/src/js/src/PivotServiceContext.ts new file mode 100644 index 000000000..d550a0dd6 --- /dev/null +++ b/plugins/pivot-builder/src/js/src/PivotServiceContext.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; + +/** + * Availability status of the CorePlus `PivotService` widget on the worker + * backing the current panel. The middleware kicks off a one-shot fetch on + * mount and propagates the result to the sidebar `CreatePivotPage` via this + * context. + */ +export type PivotServiceStatus = 'loading' | 'ready' | 'unavailable'; + +export const PivotServiceContext = createContext('loading'); + +export function usePivotServiceStatus(): PivotServiceStatus { + return useContext(PivotServiceContext); +} diff --git a/plugins/pivot-builder/src/js/src/createMiddleware.tsx b/plugins/pivot-builder/src/js/src/createMiddleware.tsx new file mode 100644 index 000000000..f8f9daf05 --- /dev/null +++ b/plugins/pivot-builder/src/js/src/createMiddleware.tsx @@ -0,0 +1,148 @@ +import { + forwardRef, + type ComponentType, + type ForwardRefExoticComponent, + type ReactElement, + type RefAttributes, +} from 'react'; +import type { WidgetComponentProps, WidgetPanelProps } from '@deephaven/plugin'; + +/** + * TEMPORARY LOCAL COPY. + * + * These helpers (and the two middleware prop types below) are duplicated from + * `@deephaven/plugin` (see `PluginUtils.tsx` / `PluginTypes.ts` in web-client-ui). + * The upstream package version installed here predates them, so we vendor a copy + * to unblock plugin development. Delete this file and import from + * `@deephaven/plugin` once a web-client-ui release that exports them is published. + */ + +/** + * Props passed to middleware components that wrap a base widget. + * Extends WidgetComponentProps with the wrapped component. + */ +export interface WidgetMiddlewareComponentProps + extends WidgetComponentProps { + /** + * The next component in the middleware chain. + * Middleware should render this component to continue the chain. + */ + Component: ComponentType>; +} + +/** + * Props passed to middleware panel components that wrap a base panel. + * Extends WidgetPanelProps with the wrapped panel component. + */ +export interface WidgetMiddlewarePanelProps + extends WidgetPanelProps { + /** + * The next panel component in the middleware chain. + * Middleware should render this component to continue the chain. + * + * This is ref-capable: middleware that transparently wraps a single inner + * panel should forward its own `ref` to this component. Golden-layout binds + * a ref to the registered panel to persist class-component state into its + * `componentState`; if a middleware swallows the ref, the wrapped panel's + * state (sorts, filters, column moves, etc.) is never serialized and is lost + * on reload. + */ + Component: ForwardRefExoticComponent< + WidgetPanelProps & RefAttributes + >; +} + +/** + * What a middleware body hook returns. Both fields are optional: + * + * - `inject`: extra props merged onto the wrapped `Component`, threaded down + * the middleware chain. Use it to forward IrisGrid-aware props (e.g. + * `transformModel`, `transformTableOptions`, `onModelChanged`) without + * hand-writing the widening cast on `Component`. + * - `wrap`: an optional wrapper placed *around* the wrapped component (e.g. a + * context provider). Receives the already-rendered child element and must + * return an element that renders it. + */ +export interface MiddlewareBodyResult { + inject?: Record; + wrap?: (child: ReactElement) => ReactElement; +} + +/** + * A hook implementing the body of a middleware. Receives the incoming props + * (without `Component`) and returns an optional set of props to inject plus an + * optional wrapper. The same body hook can back both a panel and a widget + * middleware (see {@link createPanelMiddleware} / {@link createWidgetMiddleware}), + * so a plugin expresses its behavior once. + * + * Type the `props` parameter as wide as the middleware needs (e.g. intersect + * with `IrisGridTableOptionsWidgetProps`) — the factory passes the runtime + * props through unchanged. + */ +export type MiddlewareBody

= (props: P) => MiddlewareBodyResult; + +/** + * Builds a panel-path middleware component from a single body hook, owning the + * `forwardRef` ceremony and ref forwarding that golden-layout state persistence + * depends on. + * + * The returned component is ref-capable and always forwards its `ref` to the + * wrapped `Component`, so a middleware author can never accidentally drop it + * (which would silently break `componentState` persistence — sorts, filters, + * column moves — on reload). The body hook only decides what to inject and how + * to wrap; it never sees the ref. + */ +export function createPanelMiddleware< + T = unknown, + P extends WidgetPanelProps = WidgetPanelProps, +>( + useBody: MiddlewareBody

, + displayName = 'PanelMiddleware' +): ForwardRefExoticComponent< + WidgetMiddlewarePanelProps & RefAttributes +> { + const PanelMiddleware = forwardRef>( + ({ Component, ...rest }, ref) => { + const { inject, wrap } = useBody(rest as unknown as P); + const Next = Component as unknown as ForwardRefExoticComponent< + Record & RefAttributes + >; + const child = ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ); + return wrap != null ? wrap(child) : child; + } + ); + PanelMiddleware.displayName = displayName; + return PanelMiddleware; +} + +/** + * Builds a widget-path middleware component from a single body hook. The widget + * path takes no ref, so this is a plain function component; otherwise it mirrors + * {@link createPanelMiddleware} (same `inject` / `wrap` contract), letting a + * plugin reuse one body hook for both paths. + */ +export function createWidgetMiddleware< + T = unknown, + P extends WidgetComponentProps = WidgetComponentProps, +>( + useBody: MiddlewareBody

, + displayName = 'WidgetMiddleware' +): ComponentType> { + function WidgetMiddleware({ + Component, + ...rest + }: WidgetMiddlewareComponentProps): ReactElement { + const { inject, wrap } = useBody(rest as unknown as P); + const Next = Component as unknown as ComponentType>; + const child = ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ); + return wrap != null ? wrap(child) : child; + } + WidgetMiddleware.displayName = displayName; + return WidgetMiddleware; +} diff --git a/plugins/pivot-builder/src/js/src/createPivotItemType.ts b/plugins/pivot-builder/src/js/src/createPivotItemType.ts new file mode 100644 index 000000000..3a836aab5 --- /dev/null +++ b/plugins/pivot-builder/src/js/src/createPivotItemType.ts @@ -0,0 +1,6 @@ +/** + * Stable type key for the Create Pivot sidebar item. The + * `plugin::` convention keeps plugin contributions from + * colliding with built-in `OptionType` values or with other plugins. + */ +export const CREATE_PIVOT_ITEM_TYPE = 'plugin:pivot-builder:create-pivot'; diff --git a/plugins/pivot-builder/src/js/src/index.ts b/plugins/pivot-builder/src/js/src/index.ts new file mode 100644 index 000000000..039e12d2f --- /dev/null +++ b/plugins/pivot-builder/src/js/src/index.ts @@ -0,0 +1,27 @@ +import { PivotBuilderPlugin } from './PivotBuilderPlugin'; + +export { CREATE_PIVOT_ITEM_TYPE } from './createPivotItemType'; +export { CreatePivotPage } from './CreatePivotPage'; +export { + augmentPivotBuilderModel, + isPivotBuilderIrisGridModel, + makeDefaultPivotConfig, + type PivotBuilderProxyModel, + type PivotConfig, +} from './pivotBuilderModel'; +export { makePivotModelTransform } from './makePivotModelTransform'; +export { PivotBuilderMiddleware } from './PivotBuilderMiddleware'; +export { PivotBuilderPanelMiddleware } from './PivotBuilderPanelMiddleware'; +export { PivotBuilderPlugin } from './PivotBuilderPlugin'; +export { makeCreatePivotTransform } from './makeCreatePivotTransform'; +export type { + IrisGridTableOptionsWidgetProps, + OptionItem, + TableOptionsTransform, +} from './tableOptionsTypes'; +export type { + IrisGridModelTransform, + IrisGridModelWidgetProps, +} from './modelTypes'; + +export default PivotBuilderPlugin; diff --git a/plugins/pivot-builder/src/js/src/makeCreatePivotTransform.ts b/plugins/pivot-builder/src/js/src/makeCreatePivotTransform.ts new file mode 100644 index 000000000..0f15bc6d4 --- /dev/null +++ b/plugins/pivot-builder/src/js/src/makeCreatePivotTransform.ts @@ -0,0 +1,54 @@ +import { dhPivotTable } from '@deephaven/icons'; +import { CreatePivotPage } from './CreatePivotPage'; +import { CREATE_PIVOT_ITEM_TYPE } from './createPivotItemType'; +import { + type OptionItem, + type TableOptionsTransform, +} from './tableOptionsTypes'; + +/** + * Built-in Table Options items superseded by the unified Create Pivot + * page. Matched on the `OptionType` enum's string values (`ROLLUP_ROWS`, + * `AGGREGATIONS`) so this works without a runtime dependency on the + * `OptionType` enum from the installed `@deephaven/iris-grid`. + */ +const HIDDEN_BUILT_IN_TYPES: ReadonlySet = new Set([ + 'ROLLUP_ROWS', + 'AGGREGATIONS', +]); + +/** + * Builds a pure `transformTableOptions` that runs the upstream transform + * (if any) first so contributions compose, hides the built-in Rollup Rows + * and Aggregations items (superseded by the unified Create Pivot page), + * then appends the Create/Edit Pivot item. Its `order` (650) positions it + * between the built-in Aggregate Columns (600) and Select Distinct Values + * (700) entries. + * + * `isPivot` is a snapshot of model state (derived in the middleware from + * model events), passed in as a value rather than read from the model + * inside the transform — this keeps the transform pure and lets + * `IrisGrid` re-run it only when the snapshot (and thus the transform + * identity) changes. + */ +export function makeCreatePivotTransform( + upstream: TableOptionsTransform | undefined, + isPivot: boolean +): TableOptionsTransform { + return defaults => { + const base = upstream != null ? upstream(defaults) : defaults; + const filtered = base.filter( + option => !HIDDEN_BUILT_IN_TYPES.has(String(option.type)) + ); + const item: OptionItem = { + type: CREATE_PIVOT_ITEM_TYPE, + title: 'Rollup, Aggregate and Pivot', + icon: dhPivotTable, + order: 650, + configPage: CreatePivotPage, + }; + return [...filtered, item]; + }; +} + +export default makeCreatePivotTransform; diff --git a/plugins/pivot-builder/src/js/src/makePivotModelTransform.ts b/plugins/pivot-builder/src/js/src/makePivotModelTransform.ts new file mode 100644 index 000000000..1ab75667b --- /dev/null +++ b/plugins/pivot-builder/src/js/src/makePivotModelTransform.ts @@ -0,0 +1,82 @@ +import Log from '@deephaven/log'; +import { type IrisGridModel } from '@deephaven/iris-grid'; +import { isCorePlusDh } from '@deephaven/js-plugin-pivot'; +import type { dh as DhType } from '@deephaven/jsapi-types'; +import type { dh as CorePlusDhType } from '@deephaven-enterprise/jsapi-coreplus-types'; +import { + augmentPivotBuilderModel, + type PivotBuilderConfig, + type PivotBuilderProxyModel, +} from './pivotBuilderModel'; +import { type IrisGridModelTransform } from './modelTypes'; + +const log = Log.module( + '@deephaven/js-plugin-pivot-builder/makePivotModelTransform' +); + +/** + * Build an {@link IrisGridModelTransform} that augments the host-built + * `IrisGridProxyModel` into a pivot-builder proxy (see + * {@link augmentPivotBuilderModel}) and, optionally, hydrates a persisted + * builder config before the model is published. + * + * Designed to be referentially stable: pass `getPersistedConfig` as a + * stable function (e.g. a ref reader) so the latest persisted value is read + * at model-build time without the transform's identity changing whenever + * the persisted config changes (which would rebuild the model). + * + * Composes on top of any `upstream` transform threaded down the middleware + * chain, so the host-built model is first passed through the upstream + * transform and then augmented here. + * + * @param dh CorePlus-capable API. + * @param getPspWidget Lazily fetches the PivotService widget on first apply. + * @param getPersistedConfig Reads the latest persisted builder config (or + * `null`). Read once per model build; restored synchronously before the + * model is published to avoid a hydration race. + * @param upstream Optional upstream model transform to compose under. + */ +export function makePivotModelTransform( + dh: typeof DhType | typeof CorePlusDhType, + getPspWidget: () => Promise, + getPersistedConfig: () => PivotBuilderConfig | null = () => null, + upstream?: IrisGridModelTransform +): IrisGridModelTransform { + if (!isCorePlusDh(dh)) { + throw new Error('CorePlus is not available; pivot builder requires DHE'); + } + return async (model: IrisGridModel) => { + const base = upstream != null ? await upstream(model) : model; + log.info('Augmenting host model into pivot builder proxy'); + const augmented: PivotBuilderProxyModel = augmentPivotBuilderModel( + dh, + base, + getPspWidget + ); + // Hydrate persisted builder config synchronously *before* returning the + // model. Doing this here (instead of via a post-mount effect) avoids a + // race where a listener fires with the pre-hydration (empty) config and + // overwrites the persisted value. + // + // We also *await* the resulting inner-model swap before returning. The + // pivot/rollup build is routed through the host proxy's async + // `setNextModel`, so without this await the host would run + // `hydrateIrisGridState` while the inner model is still the flat source — + // pushing the persisted sort/filter onto the wrong model and losing them + // once the pivot/rollup swaps in (the host does not re-push onto the + // in-place proxy). Awaiting ensures the host hydrates against the derived + // model's columns. + const persisted = getPersistedConfig(); + if (persisted != null) { + try { + log.info('Restoring persisted builder config', persisted); + await augmented.applyPivotBuilderConfig(persisted); + } catch (err) { + log.warn('Failed to restore persisted builder config', err); + } + } + return augmented; + }; +} + +export default makePivotModelTransform; diff --git a/plugins/pivot-builder/src/js/src/modelTypes.ts b/plugins/pivot-builder/src/js/src/modelTypes.ts new file mode 100644 index 000000000..fbba59887 --- /dev/null +++ b/plugins/pivot-builder/src/js/src/modelTypes.ts @@ -0,0 +1,45 @@ +import { + type IrisGridModel, + type IrisGridRenderer, + type MouseHandlersProp, + type GetMetricCalculatorType, +} from '@deephaven/iris-grid'; + +/** + * Local copy of the model-transform props contract added to + * `@deephaven/iris-grid` in web-client-ui. Duplicated here until that + * version is published and installed, at which point these can be replaced + * with imports from `@deephaven/iris-grid`. + */ + +/** + * Transform applied to the model an IrisGrid host (panel or widget) builds, + * before it is handed to ``. Lets middleware wrap/augment the + * host-built model without taking over model construction. + */ +export type IrisGridModelTransform = ( + model: IrisGridModel +) => IrisGridModel | Promise; + +/** + * Opt-in prop for components that build an `IrisGridModel` from a `fetch` + * (e.g. `IrisGridPanel`, `GridWidgetPlugin`), threaded down the middleware + * chain. Must be referentially stable. + */ +export interface IrisGridModelWidgetProps { + transformModel?: IrisGridModelTransform; +} + +/** + * Local mirror of `IrisGridViewProps` from `@deephaven/iris-grid`: the bag of + * view-concern overrides (theme, renderer, mouse handlers, metric calculator) + * an IrisGrid host forwards to `` as a single prop. Duplicated here + * until that version is published and installed, at which point this can be + * replaced with an import from `@deephaven/iris-grid`. + */ +export interface IrisGridViewProps { + theme?: Record; + renderer?: IrisGridRenderer; + mouseHandlers?: MouseHandlersProp; + getMetricCalculator?: GetMetricCalculatorType; +} diff --git a/plugins/pivot-builder/src/js/src/pivotBuilderModel.ts b/plugins/pivot-builder/src/js/src/pivotBuilderModel.ts new file mode 100644 index 000000000..6df9aff2c --- /dev/null +++ b/plugins/pivot-builder/src/js/src/pivotBuilderModel.ts @@ -0,0 +1,582 @@ +import deepEqual from 'fast-deep-equal'; +import { + IrisGridModel, + AggregationOperation, + type AggregationSettings, + type UITotalsTableConfig, +} from '@deephaven/iris-grid'; +import { IrisGridPivotModel, isCorePlusDh } from '@deephaven/js-plugin-pivot'; +import Log from '@deephaven/log'; +import { EventShimCustomEvent } from '@deephaven/utils'; +import type { dh as DhType } from '@deephaven/jsapi-types'; +import type { dh as CorePlusDhType } from '@deephaven-enterprise/jsapi-coreplus-types'; + +const log = Log.module('@deephaven/js-plugin-pivot-builder/pivotBuilderModel'); + +const NUMERIC_TYPES = new Set([ + 'int', + 'long', + 'short', + 'byte', + 'double', + 'float', + 'java.lang.Integer', + 'java.lang.Long', + 'java.lang.Short', + 'java.lang.Byte', + 'java.lang.Double', + 'java.lang.Float', + 'java.math.BigDecimal', + 'java.math.BigInteger', +]); + +/** Sentinel installed on the proxy so `isPivotBuilderIrisGridModel` works. */ +const PIVOT_BUILDER_TAG = Symbol.for( + '@deephaven/js-plugin-pivot-builder/PivotBuilderProxy' +); + +/** + * Event dispatched on the proxy whenever `applyPivotBuilderConfig` runs. + * The `detail` is the new `PivotBuilderConfig`. Used by the panel + * middleware to persist the latest intent via `usePersistentState`. + */ +export const PIVOT_BUILDER_CONFIG_CHANGED = + '@deephaven/js-plugin-pivot-builder/PIVOT_BUILDER_CONFIG_CHANGED'; + +/** + * User-configured pivot settings. Shape mirrors the request payload accepted + * by `coreplus.pivot.PivotService#createPivotTable`. + */ +export interface PivotConfig { + rowKeys: string[]; + columnKeys: string[]; + /** e.g. `{ Sum: ['price', 'qty'] }`. */ + aggregations: Record; +} + +/** + * Pure UI state of the four config cards. Persisted alongside the derived + * model config so the sidebar restores switch positions AND card contents + * exactly on reopen/reload. The derived `pivot`/`rollup`/`totals` collapse + * "card off" and "card on but empty" into the same value, so they cannot + * recover the switch positions (or the contents of a card that was toggled + * off) on their own. Optional/absent for configs persisted before this + * field existed — the sidebar falls back to deriving seed state from the + * model config in that case. + */ +export interface PivotBuilderUiState { + rollupRowsOn: boolean; + rollupRows: string[]; + includeConstituents: boolean; + nonAggregatedInRollup: boolean; + aggregatesOn: boolean; + aggregations: AggregationSettings; + pivotColumnsOn: boolean; + pivotColumns: string[]; + filterableOn: boolean; + filterableColumns: string[]; +} + +/** + * High-level pivot-builder intent. The proxy diffs against its last + * applied intent internally; callers can pass the same value across + * unrelated re-renders without causing redundant writes. + */ +export interface PivotBuilderConfig { + pivot: PivotConfig | null; + rollup: DhType.RollupConfig | null; + totals: UITotalsTableConfig | null; + /** + * Pure UI/card state (switch positions + card contents). Decoupled from + * the derived model config above so reopening the sidebar restores the + * exact card state the user left, including toggled-off cards. Absent on + * configs persisted before this field existed. + */ + ui?: PivotBuilderUiState | null; +} + +/** + * An `IrisGridProxyModel` (the host's own proxy) augmented with a + * `pivotConfig` accessor that swaps its inner model between the flat + * `IrisGridTableModel` and an `IrisGridPivotModel`. + */ +export interface PivotBuilderProxyModel extends IrisGridModel { + pivotConfig: PivotConfig | null; + /** The original (pre-pivot) source table. */ + readonly sourceTable: DhType.Table; + /** Last applied builder config; mirrors `applyPivotBuilderConfig` input. */ + readonly builderConfig: PivotBuilderConfig; + /** + * Apply pivot/rollup/totals atomically. + * + * The proxy owns ordering (pivot supersedes rollup/totals; otherwise + * rollup is cleared/applied before totals), diffs each field against + * the last applied intent, and queues `totals` writes that land while + * a model swap is in progress (the host proxy's `set totalsConfig` + * silently drops mid-swap writes). Queued totals are flushed on the + * next `COLUMNS_CHANGED` / `TABLE_CHANGED`. + * + * Dispatches `PIVOT_BUILDER_CONFIG_CHANGED` with the new config as + * `detail` after each call so listeners (e.g. the panel middleware's + * persistence layer) can react. Direct writes to + * `proxy.rollupConfig` / `proxy.totalsConfig` are stored on the proxy + * but NOT propagated to the inner model — the pivot-builder sidebar + * replaces those host surfaces and owns inner-model swaps. + * + * Returns a promise that resolves once any inner-model swap triggered by + * this call (the async pivot/rollup build routed through the host proxy's + * `setNextModel`) has settled. Synchronous callers (the sidebar) can ignore + * it; the reload transform awaits it so the host hydrates sort/filter + * against the derived model rather than the still-flat source. + */ + applyPivotBuilderConfig: (config: PivotBuilderConfig) => Promise; + [PIVOT_BUILDER_TAG]: true; +} + +export function isNumericColumn(column: DhType.Column): boolean { + return NUMERIC_TYPES.has(column.type); +} + +export function isPivotBuilderIrisGridModel( + model: unknown +): model is PivotBuilderProxyModel { + return ( + typeof model === 'object' && + model !== null && + (model as { [PIVOT_BUILDER_TAG]?: true })[PIVOT_BUILDER_TAG] === true + ); +} + +class SupersededError extends Error { + constructor() { + super('superseded'); + this.name = 'SupersededError'; + } +} + +/** + * Augment a host-built `IrisGridProxyModel` (the model the host's + * `IrisGridPanel` / `GridWidgetPlugin` constructs from the source table) + * **in place**, installing a `pivotConfig` accessor that — when set — + * produces a pivot via `PivotService.createPivotTable` and hands it to the + * proxy's `setNextModel`. The proxy fires the standard + * `COLUMNS_CHANGED` / `UPDATED` events, so IrisGrid re-renders in place + * exactly like rollups. + * + * This is wired as an `IrisGridModelTransform` (see the host + * `transformModel` seam): the host owns model construction, error/loading + * state, and `close()`; the pivot-builder only wraps the result. That lets + * the pivot-builder middleware stay a *chained* layer (rendering the host + * `Component`) instead of mounting its own `IrisGrid` / `IrisGridPanel`. + * + * Returns the same proxy instance it was given (mutated), narrowed to + * `PivotBuilderProxyModel`. + */ +export function augmentPivotBuilderModel( + dh: typeof DhType | typeof CorePlusDhType, + model: IrisGridModel, + getPspWidget: () => Promise +): PivotBuilderProxyModel { + if (!isCorePlusDh(dh)) { + throw new Error('CorePlus is not available; pivot builder requires DHE'); + } + const corePlusDh = dh as typeof CorePlusDhType; + + const proxy = model as IrisGridModel & { + setNextModel: (promise: Promise) => void; + // IrisGridProxyModel exposes `originalModel` (own prop reachable via + // the model's Proxy get-trap); the pivot is always built off the + // original (pre-pivot) source table. + originalModel: IrisGridModel; + }; + + // The original (pre-pivot) source table, taken from the host proxy's + // original flat model so the pivot is always built off the source table + // regardless of the proxy's current inner model. + const { table } = proxy.originalModel as unknown as { table: DhType.Table }; + + // The pivot service hangs the worker on an aggregation that targets no + // columns the same way it rejects an empty aggregations map — e.g. a + // degenerate `{ Count: [] }` (Count over zero columns). Such an entry can + // arrive from a stale persisted `builderConfig` baked by an earlier build + // and is re-applied on every reload, so we sanitize it here at the single + // `createPivotTable` choke point rather than trust the incoming map. + // + // We first drop every aggregation whose column list is empty. If the user + // has pivot columns selected but no (valid) aggregations — i.e. the + // Aggregate values card is off or empty — a column-less pivot would render + // only key columns with no values, which is rarely what's wanted. So when + // the sanitized map is empty we synthesize a `Count` over a single source + // column (the first column not already used as a row/column key, falling + // back to the first column overall). This produces a meaningful count + // pivot. The fallback is a BUILD-TIME detail only: it is NOT folded into + // the persisted `builderConfig`/intent and never surfaces in the Aggregate + // values card — the persisted config keeps the user's actual (empty) + // aggregations. + const withFallbackAggregations = ( + config: PivotConfig + ): Record => { + const sanitized = Object.fromEntries( + Object.entries(config.aggregations).filter( + ([, columns]) => columns.length > 0 + ) + ); + + if (Object.keys(sanitized).length > 0) { + return sanitized; + } + + const usedKeys = new Set([...config.rowKeys, ...config.columnKeys]); + const fallbackColumn = + table.columns.find(column => !usedKeys.has(column.name)) ?? + table.columns[0]; + + if (fallbackColumn == null) { + return sanitized; + } + + return { [AggregationOperation.COUNT]: [fallbackColumn.name] }; + }; + + let current: PivotConfig | null = null; + // Monotonic token for in-flight pivot creations. Every `pivotConfig` write + // increments it; async build steps abort early when their captured token + // is stale. The host already cancels superseded model promises, but + // bailing out before contacting the pivot service avoids wasted RPCs and + // makes `pivotConfig` writes safe under rapid succession (e.g. drag flows + // that flip config several times before the first build resolves). + let pivotToken = 0; + + const applyPivotConfig = (config: PivotConfig | null): void => { + if (deepEqual(current, config)) return; + current = config; + pivotToken += 1; + const token = pivotToken; + + if (config == null) { + proxy.setNextModel(Promise.resolve(proxy.originalModel)); + return; + } + + const promise = (async (): Promise => { + log.info('Creating pivot with config:', config); + const pspWidget = await getPspWidget(); + if (token !== pivotToken) throw new SupersededError(); + const pivotService = + await corePlusDh.coreplus.pivot.PivotService.getInstance(pspWidget); + if (token !== pivotToken) throw new SupersededError(); + const pivotTable = await pivotService.createPivotTable({ + source: table as unknown as CorePlusDhType.Table, + rowKeys: config.rowKeys, + columnKeys: config.columnKeys, + aggregations: withFallbackAggregations(config), + }); + if (token !== pivotToken) { + // Build resolved after a newer request superseded it. Close the + // orphan table directly — the host's cancel handler won't run on a + // promise that throws. + pivotTable.close?.(); + throw new SupersededError(); + } + return new IrisGridPivotModel(corePlusDh, pivotTable); + })(); + promise.catch(e => { + if (e instanceof SupersededError) { + log.debug2('pivot build superseded', config); + return; + } + log.error('createPivotTable failed for config', config, e); + }); + + proxy.setNextModel(promise); + }; + + Object.defineProperty(proxy, PIVOT_BUILDER_TAG, { + value: true, + enumerable: false, + configurable: false, + writable: false, + }); + + Object.defineProperty(proxy, 'sourceTable', { + value: table, + enumerable: false, + configurable: false, + writable: false, + }); + + Object.defineProperty(proxy, 'pivotConfig', { + configurable: true, + enumerable: true, + get(): PivotConfig | null { + return current; + }, + set(config: PivotConfig | null): void { + log.debug('set pivotConfig', config); + applyPivotConfig(config); + }, + }); + + // The proxy owns `rollupConfig` / `totalsConfig` storage so dehydration + // captures the pivot-builder's latest intent. Direct writes (from the + // host's `IrisGridModelUpdater` at hydration time, or any other host + // surface) are stored but NOT applied to the inner model — the + // pivot-builder sidebar replaces those host surfaces and routes + // inner-model swaps through `applyPivotBuilderConfig`. + // `totalsConfig` writes from `applyPivotBuilderConfig` are queued when + // a model swap is in progress, because the host proxy's `set + // totalsConfig` silently drops mid-swap writes. + const proto = Object.getPrototypeOf(proxy); + const rollupDesc = Object.getOwnPropertyDescriptor(proto, 'rollupConfig'); + + let storedRollup: DhType.RollupConfig | null = null; + let storedTotals: UITotalsTableConfig | null = null; + let lastIntent: PivotBuilderConfig = { + pivot: null, + rollup: null, + totals: null, + }; + let pendingTotals: UITotalsTableConfig | null | undefined; + // Mirror of the totals config actually written to the source (base) + // model. Inner totals writes are diffed against this — NOT against + // `lastIntent.totals` — because the pivot and rollup branches force + // `lastIntent.totals` to `null` when they supersede totals without ever + // touching the base model. Diffing against `lastIntent.totals` would then + // wrongly suppress the clearing write when returning to a plain-totals + // view, leaving a stale Totals row on the restored base model. + let appliedInnerTotals: UITotalsTableConfig | null = null; + + const proxyAsAny = proxy as unknown as { modelPromise: unknown }; + const originalWritable = proxy.originalModel as unknown as { + totalsConfig: UITotalsTableConfig | null; + }; + + const writeTotalsToInner = (v: UITotalsTableConfig | null): void => { + // Totals only ever apply to the source (base) model — rollup/pivot + // models supersede them. Write to the stable `originalModel` rather than + // the proxy's swap-sensitive current inner model so the clearing / + // restoring write always lands on the base model regardless of which + // model is currently displayed, and survives model swaps. + originalWritable.totalsConfig = v; + appliedInnerTotals = v; + }; + + const flushPendingTotals = (): void => { + if (pendingTotals === undefined) return; + if (proxyAsAny.modelPromise != null) return; // wait for next event + const v = pendingTotals; + pendingTotals = undefined; + writeTotalsToInner(v); + }; + + // Same-columns swaps (e.g. rollup-A → rollup-B) only fire TABLE_CHANGED; + // pivot transitions only fire COLUMNS_CHANGED. Listen to both. + proxy.addEventListener( + IrisGridModel.EVENT.COLUMNS_CHANGED, + flushPendingTotals + ); + proxy.addEventListener(IrisGridModel.EVENT.TABLE_CHANGED, flushPendingTotals); + + Object.defineProperty(proxy, 'rollupConfig', { + configurable: true, + enumerable: true, + get(): DhType.RollupConfig | null { + return storedRollup; + }, + set(v: DhType.RollupConfig | null): void { + // Store-only — host writes do not reach the inner model. The + // pivot-builder sidebar drives inner-model swaps via + // `applyPivotBuilderConfig`. + if (deepEqual(v, storedRollup)) return; + log.debug2('storing rollupConfig (no inner-model write)', v); + storedRollup = v; + // `IrisGridPanel`'s pre-`modelInitialized` `modelQueue` advances + // on COLUMNS_CHANGED (the event the host's own rollup setter + // emits after `setNextModel` resolves). Since we suppressed the + // inner-model swap, emit it ourselves so the queue advances and + // hydration completes for legacy rollup+aggregations layouts. + proxy.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.COLUMNS_CHANGED, { + detail: proxy.columns, + }) + ); + }, + }); + + Object.defineProperty(proxy, 'totalsConfig', { + configurable: true, + enumerable: true, + get(): UITotalsTableConfig | null { + return storedTotals; + }, + set(v: UITotalsTableConfig | null): void { + log.debug2('storing totalsConfig (no inner-model write)', v); + storedTotals = v; + }, + }); + + Object.defineProperty(proxy, 'builderConfig', { + configurable: true, + enumerable: true, + get(): PivotBuilderConfig { + return lastIntent; + }, + }); + + Object.defineProperty(proxy, 'applyPivotBuilderConfig', { + configurable: true, + enumerable: false, + value(config: PivotBuilderConfig): Promise { + // The pivot/rollup swap is routed through the host proxy's async + // `setNextModel`, so the inner model is not updated synchronously. + // `settle` resolves once any in-flight swap has finished (its + // `setModel` runs in the proxy's own `.then`, registered before this + // await, so the inner model is already swapped when we resume). The + // reload transform awaits this so the host hydrates sort/filter against + // the derived model; sidebar callers can ignore it. + const settle = (): Promise => { + const pending = proxyAsAny.modelPromise as PromiseLike | null; + return pending != null + ? Promise.resolve(pending).then( + () => undefined, + () => undefined + ) + : Promise.resolve(); + }; + // No-op when the config is unchanged. `CreatePivotPage` reconciles + // on mount (and on every relevant state change), so reopening the + // sidebar page re-applies the already-applied intent. Without this + // guard we'd still dispatch `PIVOT_BUILDER_CONFIG_CHANGED`, which + // calls `setPersistedConfig` upstream and re-renders the host + // `IrisGrid` one frame after the sidebar's slide-in starts — + // tearing down the in-flight push/pop animation (the page snaps in + // instead of sliding, and the Stack's view hook flickers). + if (deepEqual(config, lastIntent)) { + log.debug2('applyPivotBuilderConfig no-op (unchanged)', config); + return settle(); + } + // Raise the IrisGrid loading scrim *only* when this apply queued an + // async model swap (pivot/rollup change → `setNextModel`). Those swaps + // resolve into a COLUMNS_CHANGED / UPDATED event that clears the scrim + // automatically. A totals-only change (toggling the aggregate card) + // writes synchronously to the base model and produces no such event on + // the proxy, so raising the scrim there would leave it stuck forever — + // we must not raise it. Call this right before returning, after all + // mutations have had a chance to set `modelPromise`. + const raisePendingIfSwapping = (): void => { + if (proxyAsAny.modelPromise != null) { + proxy.dispatchEvent( + new EventShimCustomEvent(IrisGridModel.EVENT.PENDING, { + detail: { text: 'Updating pivot...' }, + }) + ); + } + }; + const proxyWithPivot = proxy as unknown as { + pivotConfig: PivotConfig | null; + }; + if (config.pivot != null) { + // Pivot supersedes rollup/totals. The pivot itself is built off + // the source table directly, so we don't apply rollup/totals to + // the inner model — but we must clear the host's *internal* + // `this.rollup` cache (only updated via the host setter) so a + // later rollup-back transition can't be `deepEqual`-suppressed + // against a stale cached value. The transient + // `setNextModel(originalModel)` queued by this clear is + // immediately superseded — and safely cancelled — by the pivot + // `setNextModel` below; `originalModel` is special-cased to not + // close on cancel. + if (lastIntent.rollup != null) { + log.debug('Clearing host rollup cache before pivot'); + rollupDesc?.set?.call(proxy, null); + } + if (!deepEqual(config.pivot, lastIntent.pivot)) { + log.debug('Applying pivotConfig', config.pivot); + proxyWithPivot.pivotConfig = config.pivot; + } + // Mirror intent into proxy storage so dehydration is correct. + storedRollup = config.rollup; + storedTotals = config.totals; + lastIntent = config; + raisePendingIfSwapping(); + proxy.dispatchEvent( + new EventShimCustomEvent(PIVOT_BUILDER_CONFIG_CHANGED, { + detail: config, + }) + ); + return settle(); + } + + // Pivot inactive — clear it before reconciling rollup/totals. + if (lastIntent.pivot != null) { + log.debug('Clearing pivotConfig (pivot inactive)'); + proxyWithPivot.pivotConfig = null; + } + + if (!deepEqual(config.rollup, lastIntent.rollup)) { + log.debug('Applying rollupConfig', config.rollup); + rollupDesc?.set?.call(proxy, config.rollup); + } + storedRollup = config.rollup; + + // Diff against the base model's real totals (the queued write if one + // is pending, otherwise the last applied value) — not + // `lastIntent.totals`, which is `null` after any pivot/rollup supersede + // and would mask a needed clearing write. + const effectiveInnerTotals = + pendingTotals !== undefined ? pendingTotals : appliedInnerTotals; + if (!deepEqual(config.totals, effectiveInnerTotals)) { + log.debug('Applying totalsConfig', config.totals); + if (proxyAsAny.modelPromise != null) { + // Mid-swap — queue and flush on next COLUMNS_CHANGED/TABLE_CHANGED. + pendingTotals = config.totals; + } else { + pendingTotals = undefined; + writeTotalsToInner(config.totals); + } + } + storedTotals = config.totals; + + lastIntent = config; + raisePendingIfSwapping(); + proxy.dispatchEvent( + new EventShimCustomEvent(PIVOT_BUILDER_CONFIG_CHANGED, { + detail: config, + }) + ); + return settle(); + }, + }); + + return proxy as unknown as PivotBuilderProxyModel; +} + +/** + * Spike-quality default config derived from the columns of the source + * table. Picks the first non-numeric column as the row key, the second + * non-numeric as the column key (if any), and aggregates all numeric + * columns as `Sum`. Falls back to `Count` when no numeric columns exist. + */ +export function makeDefaultPivotConfig( + columns: readonly DhType.Column[] +): PivotConfig { + const numeric: string[] = []; + const nonNumeric: string[] = []; + for (const col of columns) { + if (NUMERIC_TYPES.has(col.type)) { + numeric.push(col.name); + } else { + nonNumeric.push(col.name); + } + } + const rowKeys = + nonNumeric.length > 0 + ? nonNumeric.slice(0, 1) + : columns.length > 0 + ? [columns[0].name] + : []; + const columnKeys = nonNumeric.length > 1 ? nonNumeric.slice(1, 2) : []; + const aggregations: Record = + numeric.length > 0 ? { Sum: numeric } : { Count: [] }; + return { rowKeys, columnKeys, aggregations }; +} diff --git a/plugins/pivot-builder/src/js/src/tableOptionsTypes.ts b/plugins/pivot-builder/src/js/src/tableOptionsTypes.ts new file mode 100644 index 000000000..e5d6b88ca --- /dev/null +++ b/plugins/pivot-builder/src/js/src/tableOptionsTypes.ts @@ -0,0 +1,61 @@ +import { type ComponentType } from 'react'; +import { type OptionType, type IrisGridModel } from '@deephaven/iris-grid'; +import { type IconDefinition } from '@deephaven/icons'; + +/** + * Local copies of the Table Options props contract added to + * `@deephaven/iris-grid` in web-client-ui (PR #2688). Duplicated here + * until that version is published and installed, at which point these + * can be replaced with imports from `@deephaven/iris-grid`. + */ + +/** + * Built-in items use the `OptionType` enum; plugin-contributed items use + * a namespaced string key (convention `plugin::`). + */ +export type OptionItemKey = OptionType | string; + +/** + * Props passed to a plugin-supplied sidebar page (an item whose + * `configPage` is set). + */ +export type IrisGridTableOptionsPageProps = { + /** Current model the grid is rendering. */ + model: IrisGridModel; + /** Pop the current page off the sidebar stack. */ + onBack: () => void; +}; + +/** A single entry in the Table Options sidebar menu. */ +export type OptionItem = { + type: OptionItemKey; + title: string; + subtitle?: string; + icon?: IconDefinition; + isOn?: boolean; + onChange?: () => void; + /** + * Optional sort weight for positioning the item within the menu. Items + * are stably sorted by ascending `order`; an omitted `order` sinks the + * item to the end of the menu. Built-in items are numbered with a stride + * of 100. + */ + order?: number; + configPage?: ComponentType; +}; + +/** + * Transform applied to the built-in Table Options items before they are + * rendered. Must be referentially stable and side-effect-free. + */ +export type TableOptionsTransform = ( + defaults: readonly OptionItem[] +) => readonly OptionItem[]; + +/** + * Opt-in props for components that wrap `` / ``, + * threaded down the middleware chain. + */ +export interface IrisGridTableOptionsWidgetProps { + transformTableOptions?: TableOptionsTransform; +} diff --git a/plugins/pivot-builder/src/js/vite.config.ts b/plugins/pivot-builder/src/js/vite.config.ts new file mode 100644 index 000000000..f5181e057 --- /dev/null +++ b/plugins/pivot-builder/src/js/vite.config.ts @@ -0,0 +1,48 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => ({ + build: { + minify: false, + lib: { + entry: './src/index.ts', + fileName: () => 'index.js', + formats: ['cjs'], + }, + rollupOptions: { + output: { + exports: 'named', + }, + // Externalize peer deps following the grid-toolbar pattern. + // These are provided at runtime by DHE's remote-component.config.ts + // resolve map (or by the loaded js-plugin-pivot bundle in DHE's + // plugin loader). `fast-deep-equal` is not in DHE's resolve map, + // so we let it bundle. + // + // TODO: `@dnd-kit/*` and `@fortawesome/react-fontawesome` are bundled for + // now. Once a web-client-ui release re-exports them from + // `@deephaven/iris-grid` / `@deephaven/components`, switch the plugin + // to import from those packages and externalize them again. + external: [ + 'react', + 'react-dom', + '@deephaven/components', + '@deephaven/dashboard', + '@deephaven/dashboard-core-plugins', + '@deephaven/iris-grid', + '@deephaven/jsapi-bootstrap', + '@deephaven/jsapi-types', + '@deephaven/jsapi-utils', + '@deephaven/js-plugin-pivot', + '@deephaven/log', + '@deephaven/plugin', + '@deephaven/react-hooks', + ], + }, + }, + define: + mode === 'production' ? { 'process.env.NODE_ENV': '"production"' } : {}, + plugins: [react()], +})); diff --git a/plugins/pivot/src/js/src/IrisGridPivotMetricCalculator.ts b/plugins/pivot/src/js/src/IrisGridPivotMetricCalculator.ts index 56fdd00a1..ee4557ab3 100644 --- a/plugins/pivot/src/js/src/IrisGridPivotMetricCalculator.ts +++ b/plugins/pivot/src/js/src/IrisGridPivotMetricCalculator.ts @@ -21,6 +21,24 @@ import { } from './IrisGridPivotTypes'; import { getKeyColumnGroups } from './PivotUtils'; +// [DIAG] Lightweight call-counter to detect per-frame draw storms while the +// page is frozen. Logs every Nth hit so the console buffer reveals which +// method is being hammered. TODO: remove before merging. +const __diagCounts: Record = {}; +function diagBump(label: string, every = 200, extra?: unknown): void { + __diagCounts[label] = (__diagCounts[label] ?? 0) + 1; + const n = __diagCounts[label]; + if (n % every === 0) { + if (extra !== undefined) { + // eslint-disable-next-line no-console + console.log(`[DIAG ${label}] n=${n}`, extra); + } else { + // eslint-disable-next-line no-console + console.log(`[DIAG ${label}] n=${n}`); + } + } +} + /** * Get the width of a column that may be not in the viewport, * based on the user, calculated, and theme widths. @@ -68,6 +86,7 @@ export function getColumnHeaderCoordinates( state: IrisGridPivotRenderState, group: PivotColumnHeaderGroup ): BoxCoordinates | null { + diagBump('getColumnHeaderCoordinates', 200); const { metrics, theme } = state; const { childIndexes, depth } = group; const firstChildIndex = childIndexes[0]; @@ -177,6 +196,7 @@ class IrisGridPivotMetricCalculator extends IrisGridMetricCalculator { state: IrisGridPivotMetricState, maxColumnWidth: number ): number { + diagBump('getColumnHeaderGroupWidth', 200, { modelColumn, depth }); const baseWidth = super.getColumnHeaderGroupWidth( modelColumn, depth, @@ -213,6 +233,7 @@ class IrisGridPivotMetricCalculator extends IrisGridMetricCalculator { model: IrisGridPivotModel, state: IrisGridPivotMetricState ): number { + diagBump('calculateColumnSourceLabelWidth', 200); const { theme } = state; const { headerHorizontalPadding, maxColumnWidth } = theme; const keyColumnGroups = getKeyColumnGroups(model); @@ -234,9 +255,23 @@ class IrisGridPivotMetricCalculator extends IrisGridMetricCalculator { const { model } = state; if (!isIrisGridPivotModel(model)) { - throw new Error('Model is not an IrisGridPivotModel'); + // Transient: this calculator lives in the host's component state and a + // pivot-config swap can pair it with a non-pivot model for a frame + // before the host rebuilds the calculator on COLUMNS_CHANGED. Fall back + // to base metrics (with a zero source-label width) instead of throwing; + // the next render installs the correct calculator. + return { + ...super.getMetrics(state), + columnSourceLabelWidth: 0, + }; } + // [DIAG] count getMetrics calls to detect a per-frame draw storm + diagBump('getMetrics', 200, { + cols: model.columnCount, + rows: model.rowCount, + }); + // Update column widths if columns in the cached model don't match the current model passed in the state const columnSourceLabelWidth = this.calculateColumnSourceLabelWidth( model, diff --git a/plugins/pivot/src/js/src/IrisGridPivotModel.ts b/plugins/pivot/src/js/src/IrisGridPivotModel.ts index d338182f7..5c01dabe0 100644 --- a/plugins/pivot/src/js/src/IrisGridPivotModel.ts +++ b/plugins/pivot/src/js/src/IrisGridPivotModel.ts @@ -57,6 +57,25 @@ import { type IrisGridPivotThemeType } from './IrisGridPivotTheme'; const log = Log.module('@deephaven/js-plugin-pivot/IrisGridPivotModel'); +// [DIAG] Lightweight call-counter to detect per-frame render storms while the +// page is frozen. Increments a global tally per label and logs every Nth hit +// so the console buffer reveals which method is being hammered. +// TODO: remove before merging. +const __diagCounts: Record = {}; +function diagBump(label: string, every = 200, extra?: unknown): void { + __diagCounts[label] = (__diagCounts[label] ?? 0) + 1; + const n = __diagCounts[label]; + if (n % every === 0) { + if (extra !== undefined) { + // eslint-disable-next-line no-console + console.log(`[DIAG ${label}] n=${n}`, extra); + } else { + // eslint-disable-next-line no-console + console.log(`[DIAG ${label}] n=${n}`); + } + } +} + const SET_VIEWPORT_THROTTLE = 150; const APPLY_VIEWPORT_THROTTLE = 0; const ROW_BUFFER_PAGES = 1; @@ -329,6 +348,13 @@ class IrisGridPivotModel virtualColumns: readonly PivotDisplayColumn[], valueSources: readonly CorePlusDhType.coreplus.pivot.PivotSource[] ) => { + // [DIAG] A cache MISS here means one of the memo inputs changed identity. + // If this fires every frame, the columns array is being rebuilt in a loop. + diagBump('getCachedColumns(miss)', 50, { + snapshotColumns: snapshotColumns != null, + virtualColumns: virtualColumns.length, + valueSources: valueSources.length, + }); const columns = []; this.pivotTable.columnSources.forEach((source, col) => { const index = -this.pivotTable.columnSources.length + col; @@ -338,6 +364,14 @@ class IrisGridPivotModel if (snapshotColumns == null) { return columns; } + log.info('[DIAG] getCachedColumns materializing', { + totalCount: snapshotColumns.totalCount, + offset: snapshotColumns.offset, + count: snapshotColumns.count, + valueSources: valueSources.length, + virtualColumns: virtualColumns.length, + toMaterialize: snapshotColumns.totalCount * valueSources.length, + }); for (let i = 0; i < snapshotColumns.totalCount; i += 1) { const isColumnInViewport = i >= snapshotColumns.offset && @@ -435,6 +469,7 @@ class IrisGridPivotModel ); get virtualColumns(): readonly PivotDisplayColumn[] { + diagBump('get virtualColumns'); return this.getCachedVirtualColumns( this.groupColumn, this.keyColumns, @@ -557,6 +592,7 @@ class IrisGridPivotModel x: ModelIndex, depth = 0 ): PivotColumnHeaderGroup | DisplayColumn | undefined { + diagBump('columnAtDepth', 500, { x, depth }); if (depth === 0) { return this.columns[x]; } @@ -568,12 +604,19 @@ class IrisGridPivotModel return undefined; } + // Walk up the parent chain until we reach the requested depth. Guard + // against cycles/self-references in the parent map (which can occur when + // the column header groups are momentarily out of sync with the columns + // array): a finite acyclic chain always terminates, so revisiting a name + // means there is a cycle and we should bail rather than loop forever. + const visited = new Set([group.name]); let currentDepth = group.depth; while (currentDepth < depth) { group = this.columnHeaderParentMap.get(group.name); - if (!group) { + if (!group || visited.has(group.name)) { return undefined; } + visited.add(group.name); currentDepth = group.depth; } @@ -596,6 +639,7 @@ class IrisGridPivotModel // Having negative indexes in the columns array is risky, because they can be lost in maps, spreads, etc, // but it is the least invasive change adding column source support to the existing IrisGrid functionality. // A better solution would be to use string keys for indexing columns. + diagBump('get columns'); return this.getCachedColumns( this.snapshotColumns, this.virtualColumns, @@ -716,6 +760,7 @@ class IrisGridPivotModel handleModelEvent(event: CustomEvent): void { log.debug2('handleModelEvent', event); + diagBump('handleModelEvent', 20, event?.type); const { detail, type } = event; this.dispatchEvent(new EventShimCustomEvent(type, { detail })); } @@ -723,6 +768,8 @@ class IrisGridPivotModel handlePivotUpdated( event: CorePlusDhType.Event ): void { + // [DIAG] If this fires repeatedly, snapshots are streaming in a loop. + diagBump('handlePivotUpdated', 1); // Get the data from the snapshot, store in the model, // dispatch column and model update events const prevColumns = this.columns; @@ -859,8 +906,7 @@ class IrisGridPivotModel } truncationCharForCell(x: ModelIndex): '#' | undefined { - const column = this.columns[x]; - const { type } = column; + const type = this.columns[x]?.type; if ( TableUtils.isNumberType(type) && @@ -887,12 +933,14 @@ class IrisGridPivotModel // Fallback to formatting based on the value/type of the cell if (value != null) { const column = this.sourceColumn(x, y); - return IrisGridUtils.colorForValue( - theme, - column.type, - column.name, - value - ); + if (column != null) { + return IrisGridUtils.colorForValue( + theme, + column.type, + column.name, + value + ); + } } } @@ -909,6 +957,9 @@ class IrisGridPivotModel textAlignForCell(x: ModelIndex, y: ModelIndex): CanvasTextAlign { const column = this.sourceColumn(x, y); + if (column == null) { + return 'left'; + } return IrisGridUtils.textAlignForValue(column.type, column.name); } @@ -1204,6 +1255,14 @@ class IrisGridPivotModel const column = this.columns[x]; + // The grid can momentarily request a cell for a column index that is + // beyond the current columns array (e.g. metrics computed against a + // previous, wider column set before the model catches up). Treat a + // missing column as "not available yet" rather than throwing. + if (column == null) { + return undefined; + } + // Determine the source column type for formatting // Group column displays key values - use the appropriate key column's type let columnType = column.type; @@ -1269,6 +1328,7 @@ class IrisGridPivotModel } dataForCell(x: ModelIndex, y: ModelIndex): CellData | undefined { + diagBump('dataForCell', 500); const keyCount = this.keyColumns.length; const groupOffset = this.groupColumn == null ? 0 : 1; if (groupOffset === 1 && x === 0) { @@ -1312,6 +1372,7 @@ class IrisGridPivotModel } row(y: ModelIndex): R | null { + diagBump('row', 500, { y }); if (y === 0) { return this.viewportData?.totalsRow ?? null; } diff --git a/plugins/pivot/src/js/src/IrisGridPivotRenderer.ts b/plugins/pivot/src/js/src/IrisGridPivotRenderer.ts index 7646cbdf4..3887ba469 100644 --- a/plugins/pivot/src/js/src/IrisGridPivotRenderer.ts +++ b/plugins/pivot/src/js/src/IrisGridPivotRenderer.ts @@ -35,6 +35,16 @@ export class IrisGridPivotRenderer extends IrisGridRenderer { ): void { super.drawColumnHeaders(context, state); + // The host Grid holds this renderer in its requestAnimationFrame draw + // loop, decoupled from the model state at draw time. During a pivot + // config update the proxy can briefly pair this pivot renderer with a + // non-pivot inner model for a single frame before the host re-renders + // with the base renderer. Skip pivot-only drawing for that frame rather + // than throwing; the next render corrects it. + if (!isIrisGridPivotModel(state.model)) { + return; + } + // Draw column source filters on top of headers this.drawColumnSourceFilters(context, state); } @@ -48,7 +58,11 @@ export class IrisGridPivotRenderer extends IrisGridRenderer { ): void { const { isFilterBarShown, metrics, model, theme } = state; if (!isIrisGridPivotModel(model)) { - throw new Error('Unsupported model type'); + // Transient: the host's rAF draw loop can run this pivot renderer + // against a non-pivot model for one frame mid pivot-config swap. Fall + // back to the base column-header drawing instead of throwing. + super.drawColumnHeadersAtDepth(context, state, range, bounds, depth); + return; } const { modelColumns } = metrics; const { columnHeaderHeight } = theme; @@ -106,9 +120,18 @@ export class IrisGridPivotRenderer extends IrisGridRenderer { if (coords != null) { const { x1: columnGroupLeft, x2: columnGroupRight } = coords; - // Set column index to end of the current group - columnIndex = + // Advance to the end of the current group, but never move backward + // or to a non-finite index. If the column header groups are + // momentarily out of sync with the columns array, a group's last + // child index can be <= the current column (or undefined when the + // group has no children yet). Assigning that directly would make + // this loop fail to progress and spin forever (hard page freeze), + // since the trailing `columnIndex += 1` could never pass endIndex. + const lastChildIndex = headerGroup.childIndexes[headerGroup.childIndexes.length - 1]; + if (lastChildIndex != null && lastChildIndex > columnIndex) { + columnIndex = lastChildIndex; + } const columnWidth = columnGroupRight - columnGroupLeft; diff --git a/plugins/pivot/src/js/src/PivotUtils.test.ts b/plugins/pivot/src/js/src/PivotUtils.test.ts index 0b0ee62fa..8f82877be 100644 --- a/plugins/pivot/src/js/src/PivotUtils.test.ts +++ b/plugins/pivot/src/js/src/PivotUtils.test.ts @@ -3,7 +3,10 @@ import { TestUtils } from '@deephaven/test-utils'; import { GRAND_TOTALS_GROUP_NAME, makeColumnGroups, + makeSnapshotColumnGroups, + NULL_KEY_TOKEN, ROOT_DEPTH, + TOTALS_GROUP_NAME, } from './PivotUtils'; const { createMockProxy } = TestUtils; @@ -86,4 +89,97 @@ describe('getColumnGroups', () => { }) ); }); + + it('distinguishes a real null key from a rollup total placeholder', () => { + // Two column sources (e.g. Level, AuthenticatedUser) where the second + // source contains a real null value. The "INFO" rollup total and the + // "INFO + null AuthenticatedUser" leaf both carry the key array + // ['INFO', null] and used to collapse to the same name, producing a + // duplicate grid column. + const levelSource = + createMockProxy({ + name: 'Level', + type: 'string', + }); + const userSource = + createMockProxy({ + name: 'AuthenticatedUser', + type: 'string', + }); + const valueSource = + createMockProxy({ + name: 'Timestamp', + type: 'long', + }); + + // c0: INFO rollup total (depth 2), c1: INFO + null user leaf (depth 3), + // c2: INFO + "iris" user leaf (depth 3) + const keysByIndex = [ + ['INFO', null], + ['INFO', null], + ['INFO', 'iris'], + ]; + const depthByIndex = [2, 3, 3]; + const expandedByIndex = [true, false, false]; + + const snapshotColumns = + createMockProxy({ + offset: 0, + count: 3, + totalCount: 3, + getKeys: jest.fn((i: number) => keysByIndex[i]), + getDepth: jest.fn((i: number) => depthByIndex[i]), + isExpanded: jest.fn((i: number) => expandedByIndex[i]), + }); + + const result = makeSnapshotColumnGroups( + snapshotColumns, + [levelSource, userSource], + [valueSource] + ); + + // The INFO total leaf and the null-user leaf must resolve to distinct + // value columns. + expect(result).toContainEqual( + expect.objectContaining({ + name: 'INFO/AuthenticatedUser', + isTotalGroup: true, + displayName: TOTALS_GROUP_NAME, + children: ['INFO/Timestamp'], + }) + ); + expect(result).toContainEqual( + expect.objectContaining({ + name: `INFO/${NULL_KEY_TOKEN}`, + isTotalGroup: false, + children: [`INFO/${NULL_KEY_TOKEN}/Timestamp`], + }) + ); + expect(result).toContainEqual( + expect.objectContaining({ + name: 'INFO/iris', + isTotalGroup: false, + displayName: 'iris', + children: ['INFO/iris/Timestamp'], + }) + ); + + // The top-level INFO group references all three distinct child groups. + expect(result).toContainEqual( + expect.objectContaining({ + name: 'INFO', + children: [ + 'INFO/AuthenticatedUser', + `INFO/${NULL_KEY_TOKEN}`, + 'INFO/iris', + ], + }) + ); + + // No duplicate leaf value column names across all groups. + const leafNames = result.flatMap(g => + g.children.filter(c => c.endsWith('/Timestamp')) + ); + expect(new Set(leafNames).size).toBe(leafNames.length); + }); }); diff --git a/plugins/pivot/src/js/src/PivotUtils.ts b/plugins/pivot/src/js/src/PivotUtils.ts index 1b30e4c7e..5745ef5ab 100644 --- a/plugins/pivot/src/js/src/PivotUtils.ts +++ b/plugins/pivot/src/js/src/PivotUtils.ts @@ -36,6 +36,32 @@ export const GRAND_TOTALS_GROUP_NAME = 'Grand Total'; export const TOTALS_GROUP_NAME = 'Total'; export const ROOT_DEPTH = 2; +/** + * Reserved token used to represent a real `null` value in a generated column or + * group name. A pivot key can be `null` for two different reasons: + * 1. It is a rollup/total placeholder for a grouping level deeper than the + * column's depth (these slots are always `null`). + * 2. It is an actual `null` value present in the source data. + * Both used to collapse to the same generated name, producing duplicate grid + * columns. Encoding real nulls with this token keeps them distinct. + * + * The token contains a `#`, which `encodeURIComponent` always escapes to `%23`. + * Real values are run through `encodeURIComponent` (see {@link encodeKey}), so + * the encoded form of a real value can never contain a raw `#` and therefore + * can never collide with this token — including the literal string `"#NULL"`, + * which encodes to `%23NULL`. + */ +export const NULL_KEY_TOKEN = '#NULL'; + +/** + * Encode a single pivot key segment for use in a generated name. + * Real `null` values are encoded with {@link NULL_KEY_TOKEN} so they remain + * distinct from rollup placeholders. + */ +function encodeKey(key: unknown): string { + return key == null ? NULL_KEY_TOKEN : encodeURIComponent(String(key)); +} + export type SnapshotDimensionKeys = readonly (unknown | null)[]; export type SnapshotDimensionKeyMap = Map; @@ -159,35 +185,50 @@ export function makeGrandTotalColumnName( } /** - * Create a column name for the grid based on the pivot dimension keys and depth + * Create a column name for the grid based on the pivot dimension keys and depth. + * Only the real grouping levels (`depth - 1` of them) are included; the + * remaining keys are rollup placeholders. Real `null` values are preserved via + * {@link NULL_KEY_TOKEN} so they don't collide with rollup placeholders. + * @param keys Column keys + * @param depth Snapshot depth of the column (1-based: grand total = 1) + * @returns Generated column name */ export function makeColumnName( keys: SnapshotDimensionKeys, depth: number ): string { return keys - .slice(0, depth + 1) - .filter(k => k != null) - .map(k => encodeURIComponent(String(k))) + .slice(0, depth - 1) + .map(encodeKey) .join('/'); } /** - * Get the column group name for a specific depth + * Get the column group name for a specific hierarchy level. + * Slots beyond the column's real grouping depth (`snapshotDepth - 1`) are + * rollup/total placeholders and are named after their column source. Slots + * within the real grouping depth use the actual key value (real `null`s are + * encoded with {@link NULL_KEY_TOKEN}). * @param keys Column keys * @param columnSources Column sources - * @param depth Column depth + * @param level Hierarchy level index (0-based) + * @param snapshotDepth Snapshot depth of the column (1-based: grand total = 1) * @returns Column group name */ export function makeColumnGroupName( keys: SnapshotDimensionKeys, columnSources: readonly CorePlusDhType.coreplus.pivot.PivotSource[], - depth: number + level: number, + snapshotDepth: number ): string { + const groupingDepth = snapshotDepth - 1; return keys - .slice(0, depth + 1) - .map((k, i) => (k == null ? columnSources[i].name : k)) - .map(k => encodeURIComponent(String(k))) + .slice(0, level + 1) + .map((k, i) => + i >= groupingDepth + ? encodeURIComponent(columnSources[i].name) + : encodeKey(k) + ) .join('/'); } @@ -398,17 +439,26 @@ export function makeSnapshotColumnGroups( const keys = snapshotColumns.getKeys(c); const depth = snapshotColumns.getDepth(c); const isExpanded = snapshotColumns.isExpanded(c); + // Number of real grouping levels for this column; slots at or beyond this + // index are rollup/total placeholders rather than real (possibly null) keys. + const groupingDepth = depth - 1; columnSources.forEach((_, i) => { - // Join keys, replace nulls with the source name for the current level - const name = makeColumnGroupName(keys, columnSources, i); - const isTotalGroup = keys[i] == null; - const parentKey = i > 0 ? keys[i - 1] : null; - const totalsGroupDisplayName = parentKey == null ? '' : groupName; + const name = makeColumnGroupName(keys, columnSources, i, depth); + // A level is a total only if it is beyond the real grouping depth. A real + // null value (i < groupingDepth) must not be treated as a total. + const isTotalGroup = i >= groupingDepth; + // Only the first total level (directly under a real group) gets the + // "Total(s)" label; deeper nested total levels are left blank. + const totalsGroupDisplayName = + i > 0 && i - 1 < groupingDepth ? groupName : ''; const group = groupMap.get(name) ?? new PivotColumnHeaderGroup({ name, - displayName: isTotalGroup ? totalsGroupDisplayName : keys[i], + displayName: isTotalGroup + ? totalsGroupDisplayName + : formatValue?.(keys[i], columnSources[i].type) ?? + String(keys[i] ?? ''), isTotalGroup, children: [], depth: maxDepth - i, @@ -421,10 +471,10 @@ export function makeSnapshotColumnGroups( i === columnSources.length - 1 ? // The last group contains all value source columns valueSources.map(v => - makeValueSourceColumnName(makeColumnName(keys, depth - 1), v) + makeValueSourceColumnName(makeColumnName(keys, depth), v) ) : // Add the next group in the hierarchy as a child - [makeColumnGroupName(keys, columnSources, i + 1)] + [makeColumnGroupName(keys, columnSources, i + 1, depth)] ); groupMap.set(name, group); }); diff --git a/plugins/pivot/src/js/src/index.ts b/plugins/pivot/src/js/src/index.ts index 878b1f931..bc9fd002a 100644 --- a/plugins/pivot/src/js/src/index.ts +++ b/plugins/pivot/src/js/src/index.ts @@ -3,4 +3,16 @@ import { PivotPlugin } from './PivotPlugin'; // Export legacy dashboard plugin as named export for compatibility with Grizzly export * from './DashboardPlugin'; +// Re-exports consumed by downstream plugins (e.g. pivot-builder) that need +// to construct pivot models directly. +export { + default as IrisGridPivotModel, + isIrisGridPivotModel, +} from './IrisGridPivotModel'; +export { isCorePlusDh } from './PivotUtils'; +export { default as usePivotMouseHandlers } from './hooks/usePivotMouseHandlers'; +export { default as usePivotRenderer } from './hooks/usePivotRenderer'; +export { default as usePivotMetricCalculatorFactory } from './hooks/usePivotMetricCalculatorFactory'; +export { default as usePivotTheme } from './hooks/usePivotTheme'; + export default PivotPlugin;