From a4761cc046261d207e40d6cadef523411a1675c3 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 17 Apr 2024 10:17:58 -0400 Subject: [PATCH 01/10] test: bump ts, eslint and prettier configs (#416) - Bumps configs to latest versions - Update failing types and formatting caught by newer versions --- package-lock.json | 158 ++++++++++++------ package.json | 10 +- .../src/js/src/PlotlyExpressChartModel.ts | 10 +- .../src/js/src/PlotlyExpressChartPanel.tsx | 4 +- .../src/js/src/PlotlyExpressChartUtils.ts | 8 +- .../src/js/src/useHandleSceneTicks.ts | 2 +- .../ui/src/js/src/elements/ElementUtils.tsx | 4 +- plugins/ui/src/js/src/elements/ObjectView.tsx | 2 +- plugins/ui/src/js/src/elements/Picker.tsx | 2 +- plugins/ui/src/js/src/elements/UITable.tsx | 2 +- .../js/src/elements/spectrum/ActionButton.tsx | 2 +- .../src/js/src/elements/spectrum/Button.tsx | 5 +- .../ui/src/js/src/elements/spectrum/Flex.tsx | 2 +- .../ui/src/js/src/elements/spectrum/Form.tsx | 2 +- .../js/src/elements/spectrum/RangeSlider.tsx | 2 +- .../src/js/src/elements/spectrum/Slider.tsx | 2 +- .../js/src/elements/spectrum/TabPanels.tsx | 4 +- .../js/src/elements/spectrum/TextField.tsx | 2 +- .../src/elements/spectrum/mapSpectrumProps.ts | 2 +- .../src/elements/spectrum/useButtonProps.ts | 7 +- .../spectrum/useFocusEventCallback.ts | 4 +- .../spectrum/useKeyboardEventCallback.ts | 2 +- .../spectrum/usePressEventCallback.ts | 4 +- .../ui/src/js/src/elements/usePickerProps.ts | 6 +- .../ui/src/js/src/layout/ParentItemContext.ts | 2 +- .../src/layout/PortalPanelManagerContext.ts | 2 +- plugins/ui/src/js/src/layout/ReactPanel.tsx | 2 +- .../ui/src/js/src/layout/ReactPanelContext.ts | 2 +- .../js/src/widget/DashboardWidgetHandler.tsx | 2 +- .../ui/src/js/src/widget/DocumentHandler.tsx | 2 +- .../ui/src/js/src/widget/WidgetHandler.tsx | 2 +- .../ui/src/js/src/widget/WidgetTestUtils.ts | 4 +- plugins/ui/src/js/src/widget/WidgetTypes.ts | 4 +- 33 files changed, 172 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99a042392..1516183e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "./plugins/*/src/js/" ], "devDependencies": { - "@deephaven/babel-preset": "^0.40.0", - "@deephaven/eslint-config": "^0.40.0", - "@deephaven/prettier-config": "^0.40.0", - "@deephaven/tsconfig": "^0.40.0", + "@deephaven/babel-preset": "^0.72.0", + "@deephaven/eslint-config": "^0.72.0", + "@deephaven/prettier-config": "^0.72.0", + "@deephaven/tsconfig": "^0.72.0", "@playwright/test": "^1.41.2", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", @@ -34,7 +34,7 @@ "lerna": "^6.6.1", "npm-run-all": "^4.1.5", "nx": "15.9.2", - "prettier": "^2.8.7", + "prettier": "3.0.0", "vite": "~4.1.4" } }, @@ -2300,9 +2300,9 @@ } }, "node_modules/@deephaven/babel-preset": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/babel-preset/-/babel-preset-0.40.0.tgz", - "integrity": "sha512-5ZoFXB1SJTJwhg5phupC+wWNR18Jir8uibILsQiKuMLcmZdfR1uRsJNzmMnz/MV/C+ifhip54waJQjEEljMrbQ==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/babel-preset/-/babel-preset-0.72.0.tgz", + "integrity": "sha512-flk1Pqq9YrwYtxUKDYZV1RYrnEbzdZ/52cxcU+P7c+zgM79bgzEfCRsab50y1m3PeWls9qlVi+iNlUGNz6z5GQ==", "dev": true, "dependencies": { "@babel/core": "^7.20.0", @@ -3526,9 +3526,9 @@ } }, "node_modules/@deephaven/eslint-config": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/eslint-config/-/eslint-config-0.40.0.tgz", - "integrity": "sha512-227bATcyanUNJ8jybJt/wZyoR7Hun/ZYI8EmRXKbO+0SMBG1LiyzEYqTnjB/VKzPF44OAk8ouo5LdJ1H3TIbnw==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/eslint-config/-/eslint-config-0.72.0.tgz", + "integrity": "sha512-8cDs2K1VxByED5L3U8wHrTe7Yoj/EOW4yUW2lOV5QlLmjTOgRf5pevNrBm2GnplBMefdyB49T5dEyhRBIPwthw==", "dev": true, "dependencies": { "eslint-config-airbnb": "^19.0.4", @@ -3541,7 +3541,7 @@ "eslint": "^8.29.0", "eslint-import-resolver-typescript": "^3.5.0", "eslint-plugin-es": "^4.1.0", - "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react-refresh": "0.3.4" } }, @@ -4170,12 +4170,12 @@ } }, "node_modules/@deephaven/prettier-config": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/prettier-config/-/prettier-config-0.40.0.tgz", - "integrity": "sha512-jnzyj7AFRtTVGflPJzbNvdVL2lSb0ah5NPwHZ9sNyfwHoB0mUK8Jy0JNv2DqHD96LpFQLdQS+2QK3ouTFEZHXg==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/prettier-config/-/prettier-config-0.72.0.tgz", + "integrity": "sha512-edYejgDJnIspoUXFC9NWWuwBUTwy898y8vcjKhfRRolovY8ihDDuGLwHwbnCaOYNlNukukhuHJKK1ychASQ8bQ==", "dev": true, "peerDependencies": { - "prettier": "^2.2.1" + "prettier": "^3.0.0" } }, "node_modules/@deephaven/react-hooks": { @@ -4291,9 +4291,9 @@ } }, "node_modules/@deephaven/tsconfig": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/tsconfig/-/tsconfig-0.40.0.tgz", - "integrity": "sha512-2VA+rSmvTLTfLy0Z61pPu+aZ+ljnOgTMVZaVnXbeZzKdbQgyM9a8W5OmsQ/Vq8K3JQ+qGJ7ghYYmZmJva+BKrw==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/tsconfig/-/tsconfig-0.72.0.tgz", + "integrity": "sha512-ER4+KsrTBO8rhd4YA6SY5dRBZjUajrNKb2yQijSXNZTbWbQCet/522Yui2YCgWFBRbM5GvYGDoUcc/07tZeLZQ==", "dev": true }, "node_modules/@deephaven/utils": { @@ -7876,6 +7876,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "peer": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@playwright/test": { "version": "1.41.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", @@ -16182,22 +16195,31 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", - "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", "dev": true, "peer": true, "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" }, "engines": { - "node": ">=6.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" }, "peerDependencies": { - "eslint": ">=5.0.0", - "prettier": ">=1.13.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, "eslint-config-prettier": { "optional": true } @@ -29042,15 +29064,15 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -31950,6 +31972,23 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "peer": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/table": { "version": "6.8.2", "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", @@ -37542,9 +37581,9 @@ } }, "@deephaven/babel-preset": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/babel-preset/-/babel-preset-0.40.0.tgz", - "integrity": "sha512-5ZoFXB1SJTJwhg5phupC+wWNR18Jir8uibILsQiKuMLcmZdfR1uRsJNzmMnz/MV/C+ifhip54waJQjEEljMrbQ==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/babel-preset/-/babel-preset-0.72.0.tgz", + "integrity": "sha512-flk1Pqq9YrwYtxUKDYZV1RYrnEbzdZ/52cxcU+P7c+zgM79bgzEfCRsab50y1m3PeWls9qlVi+iNlUGNz6z5GQ==", "dev": true, "requires": { "@babel/core": "^7.20.0", @@ -38475,9 +38514,9 @@ } }, "@deephaven/eslint-config": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/eslint-config/-/eslint-config-0.40.0.tgz", - "integrity": "sha512-227bATcyanUNJ8jybJt/wZyoR7Hun/ZYI8EmRXKbO+0SMBG1LiyzEYqTnjB/VKzPF44OAk8ouo5LdJ1H3TIbnw==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/eslint-config/-/eslint-config-0.72.0.tgz", + "integrity": "sha512-8cDs2K1VxByED5L3U8wHrTe7Yoj/EOW4yUW2lOV5QlLmjTOgRf5pevNrBm2GnplBMefdyB49T5dEyhRBIPwthw==", "dev": true, "requires": { "eslint-config-airbnb": "^19.0.4", @@ -40445,9 +40484,9 @@ } }, "@deephaven/prettier-config": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/prettier-config/-/prettier-config-0.40.0.tgz", - "integrity": "sha512-jnzyj7AFRtTVGflPJzbNvdVL2lSb0ah5NPwHZ9sNyfwHoB0mUK8Jy0JNv2DqHD96LpFQLdQS+2QK3ouTFEZHXg==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/prettier-config/-/prettier-config-0.72.0.tgz", + "integrity": "sha512-edYejgDJnIspoUXFC9NWWuwBUTwy898y8vcjKhfRRolovY8ihDDuGLwHwbnCaOYNlNukukhuHJKK1ychASQ8bQ==", "dev": true, "requires": {} }, @@ -40529,9 +40568,9 @@ } }, "@deephaven/tsconfig": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@deephaven/tsconfig/-/tsconfig-0.40.0.tgz", - "integrity": "sha512-2VA+rSmvTLTfLy0Z61pPu+aZ+ljnOgTMVZaVnXbeZzKdbQgyM9a8W5OmsQ/Vq8K3JQ+qGJ7ghYYmZmJva+BKrw==", + "version": "0.72.0", + "resolved": "https://registry.npmjs.org/@deephaven/tsconfig/-/tsconfig-0.72.0.tgz", + "integrity": "sha512-ER4+KsrTBO8rhd4YA6SY5dRBZjUajrNKb2yQijSXNZTbWbQCet/522Yui2YCgWFBRbM5GvYGDoUcc/07tZeLZQ==", "dev": true }, "@deephaven/utils": { @@ -43124,6 +43163,13 @@ "dev": true, "optional": true }, + "@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "peer": true + }, "@playwright/test": { "version": "1.41.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", @@ -49626,13 +49672,14 @@ } }, "eslint-plugin-prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", - "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", "dev": true, "peer": true, "requires": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" } }, "eslint-plugin-react": { @@ -58805,9 +58852,9 @@ "dev": true }, "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", "dev": true }, "prettier-linter-helpers": { @@ -61046,6 +61093,17 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "peer": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + } + }, "table": { "version": "6.8.2", "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", diff --git a/package.json b/package.json index 6e62cd333..64acaf276 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "update-dh-packages": "lerna run --concurrency 1 update-dh-packages" }, "devDependencies": { - "@deephaven/babel-preset": "^0.40.0", - "@deephaven/eslint-config": "^0.40.0", - "@deephaven/prettier-config": "^0.40.0", - "@deephaven/tsconfig": "^0.40.0", + "@deephaven/babel-preset": "^0.72.0", + "@deephaven/eslint-config": "^0.72.0", + "@deephaven/prettier-config": "^0.72.0", + "@deephaven/tsconfig": "^0.72.0", "@playwright/test": "^1.41.2", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.1.3", @@ -47,7 +47,7 @@ "lerna": "^6.6.1", "npm-run-all": "^4.1.5", "nx": "15.9.2", - "prettier": "^2.8.7", + "prettier": "3.0.0", "vite": "~4.1.4" }, "prettier": "@deephaven/prettier-config" diff --git a/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts b/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts index 244f7991c..e2a86a8f0 100644 --- a/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts +++ b/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts @@ -126,7 +126,7 @@ export class PlotlyExpressChartModel extends ChartModel { return this.layout; } - override close() { + override close(): void { super.close(); this.widget?.close(); this.widget = undefined; @@ -180,7 +180,7 @@ export class PlotlyExpressChartModel extends ChartModel { this.widget = undefined; } - updateLayout(data: PlotlyChartWidgetData) { + updateLayout(data: PlotlyChartWidgetData): void { const { figure } = data; const { plotly } = figure; const { layout: plotlyLayout = {} } = plotly; @@ -255,7 +255,7 @@ export class PlotlyExpressChartModel extends ChartModel { this.fireUpdate(this.getData()); } - addTable(id: number, table: Table) { + addTable(id: number, table: Table): void { if (this.tableReferenceMap.has(id)) { return; } @@ -267,7 +267,7 @@ export class PlotlyExpressChartModel extends ChartModel { } } - subscribeTable(id: number) { + subscribeTable(id: number): void { const table = this.tableReferenceMap.get(id); const columnReplacements = this.tableColumnReplacementMap.get(id); @@ -292,7 +292,7 @@ export class PlotlyExpressChartModel extends ChartModel { } } - removeTable(id: number) { + removeTable(id: number): void { this.subscriptionCleanupMap.get(id)?.(); this.tableSubscriptionMap.get(id)?.close(); diff --git a/plugins/plotly-express/src/js/src/PlotlyExpressChartPanel.tsx b/plugins/plotly-express/src/js/src/PlotlyExpressChartPanel.tsx index db2cd42ad..64887bd1e 100644 --- a/plugins/plotly-express/src/js/src/PlotlyExpressChartPanel.tsx +++ b/plugins/plotly-express/src/js/src/PlotlyExpressChartPanel.tsx @@ -7,7 +7,9 @@ import { useApi } from '@deephaven/jsapi-bootstrap'; import PlotlyExpressChartModel from './PlotlyExpressChartModel.js'; import { useHandleSceneTicks } from './useHandleSceneTicks.js'; -export function PlotlyExpressChartPanel(props: WidgetPanelProps) { +export function PlotlyExpressChartPanel( + props: WidgetPanelProps +): JSX.Element { const dh = useApi(); const { fetch, metadata = {}, ...rest } = props; const containerRef = useRef(null); diff --git a/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts b/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts index c91c66f82..48d78c35a 100644 --- a/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts +++ b/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts @@ -2,12 +2,12 @@ import type { Data, PlotlyDataLayoutConfig } from 'plotly.js'; import type { Table, Widget } from '@deephaven/jsapi-types'; export interface PlotlyChartWidget { - getDataAsBase64(): string; - exportedObjects: { fetch(): Promise }[]; - addEventListener( + getDataAsBase64: () => string; + exportedObjects: { fetch: () => Promise
}[]; + addEventListener: ( type: string, fn: (event: CustomEvent) => () => void - ): void; + ) => void; } export interface PlotlyChartWidgetData { diff --git a/plugins/plotly-express/src/js/src/useHandleSceneTicks.ts b/plugins/plotly-express/src/js/src/useHandleSceneTicks.ts index c40bb726c..1c1fb2a8b 100644 --- a/plugins/plotly-express/src/js/src/useHandleSceneTicks.ts +++ b/plugins/plotly-express/src/js/src/useHandleSceneTicks.ts @@ -4,7 +4,7 @@ import PlotlyExpressChartModel from './PlotlyExpressChartModel.js'; export function useHandleSceneTicks( model: PlotlyExpressChartModel | undefined, container: HTMLDivElement | null -) { +): void { useEffect(() => { // Plotly scenes and geo views reset when our data ticks // Pause rendering data updates when the user is manipulating a scene diff --git a/plugins/ui/src/js/src/elements/ElementUtils.tsx b/plugins/ui/src/js/src/elements/ElementUtils.tsx index 3058ec267..9fb4fca60 100644 --- a/plugins/ui/src/js/src/elements/ElementUtils.tsx +++ b/plugins/ui/src/js/src/elements/ElementUtils.tsx @@ -25,7 +25,7 @@ export type ObjectNode = { */ export type ElementNode< K extends string = string, - P extends Record = Record + P extends Record = Record, > = { /** * The type of this element. Can be something like `deephaven.ui.components.Panel`, or @@ -37,7 +37,7 @@ export type ElementNode< export type ElementNodeWithChildren< K extends string = string, - P extends Record = Record + P extends Record = Record, > = ElementNode & { props: React.PropsWithChildren

; }; diff --git a/plugins/ui/src/js/src/elements/ObjectView.tsx b/plugins/ui/src/js/src/elements/ObjectView.tsx index cc6083283..8f8b32f1f 100644 --- a/plugins/ui/src/js/src/elements/ObjectView.tsx +++ b/plugins/ui/src/js/src/elements/ObjectView.tsx @@ -6,7 +6,7 @@ import type { dh } from '@deephaven/jsapi-types'; const log = Log.module('@deephaven/js-plugin-ui/ObjectView'); export type ObjectViewProps = { object: dh.WidgetExportedObject }; -function ObjectView(props: ObjectViewProps) { +function ObjectView(props: ObjectViewProps): JSX.Element { const { object } = props; log.info('Object is', object); const { type } = object; diff --git a/plugins/ui/src/js/src/elements/Picker.tsx b/plugins/ui/src/js/src/elements/Picker.tsx index 7cc7f023f..1e564a33f 100644 --- a/plugins/ui/src/js/src/elements/Picker.tsx +++ b/plugins/ui/src/js/src/elements/Picker.tsx @@ -22,7 +22,7 @@ type WrappedDHPickerJSApiProps = Omit & { export type PickerProps = (DHPickerProps | WrappedDHPickerJSApiProps) & SerializedPickerEventProps; -function Picker({ children, ...props }: PickerProps) { +function Picker({ children, ...props }: PickerProps): JSX.Element { const settings = useSelector(getSettings); const pickerProps = usePickerProps(props); diff --git a/plugins/ui/src/js/src/elements/UITable.tsx b/plugins/ui/src/js/src/elements/UITable.tsx index 9d901f4df..0c137c27e 100644 --- a/plugins/ui/src/js/src/elements/UITable.tsx +++ b/plugins/ui/src/js/src/elements/UITable.tsx @@ -30,7 +30,7 @@ function UITable({ sorts, alwaysFetchColumns, table: exportedTable, -}: UITableProps) { +}: UITableProps): JSX.Element | null { const dh = useApi(); const [model, setModel] = useState(); const [columns, setColumns] = useState(); diff --git a/plugins/ui/src/js/src/elements/spectrum/ActionButton.tsx b/plugins/ui/src/js/src/elements/spectrum/ActionButton.tsx index f04f800dd..fac0936e5 100644 --- a/plugins/ui/src/js/src/elements/spectrum/ActionButton.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/ActionButton.tsx @@ -7,7 +7,7 @@ import { SerializedButtonEventProps, useButtonProps } from './useButtonProps'; function ActionButton( props: SpectrumActionButtonProps & SerializedButtonEventProps -) { +): JSX.Element { const buttonProps = useButtonProps(props); // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/plugins/ui/src/js/src/elements/spectrum/Button.tsx b/plugins/ui/src/js/src/elements/spectrum/Button.tsx index c15ee7939..a8b4f28b8 100644 --- a/plugins/ui/src/js/src/elements/spectrum/Button.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/Button.tsx @@ -5,7 +5,10 @@ import { } from '@adobe/react-spectrum'; import { SerializedButtonEventProps, useButtonProps } from './useButtonProps'; -function Button(props: SpectrumButtonProps & SerializedButtonEventProps) { +function Button( + variant: SpectrumButtonProps['variant'], + props: SpectrumButtonProps & SerializedButtonEventProps +): JSX.Element { const buttonProps = useButtonProps(props); // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/plugins/ui/src/js/src/elements/spectrum/Flex.tsx b/plugins/ui/src/js/src/elements/spectrum/Flex.tsx index 23362fce3..60a396bd2 100644 --- a/plugins/ui/src/js/src/elements/spectrum/Flex.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/Flex.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Flex as SpectrumFlex, FlexProps } from '@adobe/react-spectrum'; -function Flex(props: FlexProps) { +function Flex(props: FlexProps): JSX.Element { // eslint-disable-next-line react/jsx-props-no-spreading return ; } diff --git a/plugins/ui/src/js/src/elements/spectrum/Form.tsx b/plugins/ui/src/js/src/elements/spectrum/Form.tsx index 9dffdadfb..928effc33 100644 --- a/plugins/ui/src/js/src/elements/spectrum/Form.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/Form.tsx @@ -5,7 +5,7 @@ function Form( props: SpectrumFormProps & { onSubmit?: (data: { [key: string]: FormDataEntryValue }) => void; } -) { +): JSX.Element { const { onSubmit: propOnSubmit, ...otherProps } = props; const onSubmit = useCallback( diff --git a/plugins/ui/src/js/src/elements/spectrum/RangeSlider.tsx b/plugins/ui/src/js/src/elements/spectrum/RangeSlider.tsx index 0f164b7a8..073fb5fa3 100644 --- a/plugins/ui/src/js/src/elements/spectrum/RangeSlider.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/RangeSlider.tsx @@ -9,7 +9,7 @@ const VALUE_CHANGE_DEBOUNCE = 250; const EMPTY_FUNCTION = () => undefined; -function RangeSlider(props: SpectrumRangeSliderProps) { +function RangeSlider(props: SpectrumRangeSliderProps): JSX.Element { const { defaultValue = { start: 0, end: 0 }, value: propValue, diff --git a/plugins/ui/src/js/src/elements/spectrum/Slider.tsx b/plugins/ui/src/js/src/elements/spectrum/Slider.tsx index 28de56e84..f2bf43880 100644 --- a/plugins/ui/src/js/src/elements/spectrum/Slider.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/Slider.tsx @@ -9,7 +9,7 @@ const VALUE_CHANGE_DEBOUNCE = 250; const EMPTY_FUNCTION = () => undefined; -function Slider(props: SpectrumSliderProps) { +function Slider(props: SpectrumSliderProps): JSX.Element { const { defaultValue = 0, value: propValue, diff --git a/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx b/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx index b13310dc5..47febf562 100644 --- a/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/TabPanels.tsx @@ -4,7 +4,9 @@ import { SpectrumTabPanelsProps, } from '@adobe/react-spectrum'; -function TabPanels(props: SpectrumTabPanelsProps) { +function TabPanels( + props: SpectrumTabPanelsProps +): JSX.Element { const { UNSAFE_style: unsafeStyle, ...otherProps } = props; return ( diff --git a/plugins/ui/src/js/src/elements/spectrum/TextField.tsx b/plugins/ui/src/js/src/elements/spectrum/TextField.tsx index 40726a5ba..1cac27244 100644 --- a/plugins/ui/src/js/src/elements/spectrum/TextField.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/TextField.tsx @@ -9,7 +9,7 @@ const VALUE_CHANGE_DEBOUNCE = 250; const EMPTY_FUNCTION = () => undefined; -function TextField(props: SpectrumTextFieldProps) { +function TextField(props: SpectrumTextFieldProps): JSX.Element { const { defaultValue = '', value: propValue, diff --git a/plugins/ui/src/js/src/elements/spectrum/mapSpectrumProps.ts b/plugins/ui/src/js/src/elements/spectrum/mapSpectrumProps.ts index daba81688..c7c693473 100644 --- a/plugins/ui/src/js/src/elements/spectrum/mapSpectrumProps.ts +++ b/plugins/ui/src/js/src/elements/spectrum/mapSpectrumProps.ts @@ -6,7 +6,7 @@ import mapSpectrumChildren from './mapSpectrumChildren'; * @param props Props to map as spectrum props */ export function mapSpectrumProps< - T extends PropsWithChildren> + T extends PropsWithChildren>, >(props: T): T { return { ...props, diff --git a/plugins/ui/src/js/src/elements/spectrum/useButtonProps.ts b/plugins/ui/src/js/src/elements/spectrum/useButtonProps.ts index 4ecf16829..d0daa1297 100644 --- a/plugins/ui/src/js/src/elements/spectrum/useButtonProps.ts +++ b/plugins/ui/src/js/src/elements/spectrum/useButtonProps.ts @@ -43,7 +43,10 @@ export type SerializedButtonEventProps = { onKeyUp?: SerializedKeyboardEventCallback; }; -export function useButtonProps(props: SerializedButtonEventProps & T) { +// returns SpectrumButtonProps +export function useButtonProps( + props: SerializedButtonEventProps & T +): T & SerializedButtonEventProps { const { onPress: propOnPress, onPressStart: propsOnPressStart, @@ -75,5 +78,5 @@ export function useButtonProps(props: SerializedButtonEventProps & T) { onKeyDown, onKeyUp, ...mapSpectrumProps(otherProps), - }; + } as T & SerializedButtonEventProps; } diff --git a/plugins/ui/src/js/src/elements/spectrum/useFocusEventCallback.ts b/plugins/ui/src/js/src/elements/spectrum/useFocusEventCallback.ts index e259d0ee7..adb1565b8 100644 --- a/plugins/ui/src/js/src/elements/spectrum/useFocusEventCallback.ts +++ b/plugins/ui/src/js/src/elements/spectrum/useFocusEventCallback.ts @@ -31,7 +31,9 @@ export type SerializedFocusEventCallback = ( * @param callback FocusEvent callback to be called with the serialized event * @returns A callback to be passed into the Spectrum component that transforms the event and calls the provided callback */ -export function useFocusEventCallback(callback?: SerializedFocusEventCallback) { +export function useFocusEventCallback( + callback?: SerializedFocusEventCallback +): (e: FocusEvent) => void { return useCallback( (e: FocusEvent) => { callback?.(serializeFocusEvent(e)); diff --git a/plugins/ui/src/js/src/elements/spectrum/useKeyboardEventCallback.ts b/plugins/ui/src/js/src/elements/spectrum/useKeyboardEventCallback.ts index bd06e6fed..b04a207ca 100644 --- a/plugins/ui/src/js/src/elements/spectrum/useKeyboardEventCallback.ts +++ b/plugins/ui/src/js/src/elements/spectrum/useKeyboardEventCallback.ts @@ -33,7 +33,7 @@ export type SerializedKeyboardEventCallback = ( */ export function useKeyboardEventCallback( callback?: SerializedKeyboardEventCallback -) { +): (e: KeyboardEvent) => void { return useCallback( (e: KeyboardEvent) => { callback?.(serializeKeyboardEvent(e)); diff --git a/plugins/ui/src/js/src/elements/spectrum/usePressEventCallback.ts b/plugins/ui/src/js/src/elements/spectrum/usePressEventCallback.ts index da68bdd88..563660f0a 100644 --- a/plugins/ui/src/js/src/elements/spectrum/usePressEventCallback.ts +++ b/plugins/ui/src/js/src/elements/spectrum/usePressEventCallback.ts @@ -34,7 +34,9 @@ export type SerializedPressEventCallback = ( * @param callback PressEvent callback to be called with the serialized event * @returns A callback to be passed into the Spectrum component that transforms the event and calls the provided callback */ -export function usePressEventCallback(callback?: SerializedPressEventCallback) { +export function usePressEventCallback( + callback?: SerializedPressEventCallback +): (e: PressEvent) => void { return useCallback( (e: PressEvent) => { callback?.(serializePressEvent(e)); diff --git a/plugins/ui/src/js/src/elements/usePickerProps.ts b/plugins/ui/src/js/src/elements/usePickerProps.ts index 5261f3de6..d4d71d1df 100644 --- a/plugins/ui/src/js/src/elements/usePickerProps.ts +++ b/plugins/ui/src/js/src/elements/usePickerProps.ts @@ -26,7 +26,9 @@ export interface SerializedPickerEventProps { * @param props Props to wrap * @returns Wrapped props */ -export function usePickerProps(props: SerializedPickerEventProps & T) { +export function usePickerProps( + props: SerializedPickerEventProps & T +): T & SerializedPickerEventProps { const { onFocus, onBlur, onKeyDown, onKeyUp, ...otherProps } = props; const serializedOnFocus = useFocusEventCallback(onFocus); @@ -44,5 +46,5 @@ export function usePickerProps(props: SerializedPickerEventProps & T) { // handles nested children inside of `Item` and `Section` components, so // we are intentionally not wrapping `otherProps` in `mapSpectrumProps` ...otherProps, - }; + } as T & SerializedPickerEventProps; } diff --git a/plugins/ui/src/js/src/layout/ParentItemContext.ts b/plugins/ui/src/js/src/layout/ParentItemContext.ts index e6ae4c205..ad613ac40 100644 --- a/plugins/ui/src/js/src/layout/ParentItemContext.ts +++ b/plugins/ui/src/js/src/layout/ParentItemContext.ts @@ -4,7 +4,7 @@ import type { ContentItem } from '@deephaven/golden-layout'; export const ParentItemContext = createContext(null); -export function useParentItem() { +export function useParentItem(): ContentItem { const layoutManager = useLayoutManager(); const parentContextItem = useContext(ParentItemContext); const parentItem = useMemo( diff --git a/plugins/ui/src/js/src/layout/PortalPanelManagerContext.ts b/plugins/ui/src/js/src/layout/PortalPanelManagerContext.ts index 7ecc58902..c304098e4 100644 --- a/plugins/ui/src/js/src/layout/PortalPanelManagerContext.ts +++ b/plugins/ui/src/js/src/layout/PortalPanelManagerContext.ts @@ -7,7 +7,7 @@ export type PortalPanelMap = ReadonlyMap; export const PortalPanelManagerContext = React.createContext(null); -export function usePortalPanelManager() { +export function usePortalPanelManager(): PortalPanelMap { return useContextOrThrow(PortalPanelManagerContext, 'PortalPanelManager'); } diff --git a/plugins/ui/src/js/src/layout/ReactPanel.tsx b/plugins/ui/src/js/src/layout/ReactPanel.tsx index 17165eade..2688dcebd 100644 --- a/plugins/ui/src/js/src/layout/ReactPanel.tsx +++ b/plugins/ui/src/js/src/layout/ReactPanel.tsx @@ -23,7 +23,7 @@ const log = Log.module('@deephaven/js-plugin-ui/ReactPanel'); * Note that because the `PortalPanel` will be saved with the GoldenLayout config, it's possible there is already a panel that exists with the same ID. * In that case, the existing panel will be re-used. */ -function ReactPanel({ children, title }: ReactPanelProps) { +function ReactPanel({ children, title }: ReactPanelProps): JSX.Element | null { const layoutManager = useLayoutManager(); const { metadata, onClose, onOpen, panelId } = useReactPanel(); const portalManager = usePortalPanelManager(); diff --git a/plugins/ui/src/js/src/layout/ReactPanelContext.ts b/plugins/ui/src/js/src/layout/ReactPanelContext.ts index ff10a545d..590fb3e3c 100644 --- a/plugins/ui/src/js/src/layout/ReactPanelContext.ts +++ b/plugins/ui/src/js/src/layout/ReactPanelContext.ts @@ -9,6 +9,6 @@ export const ReactPanelContext = createContext(null); * Gets the panel ID from the nearest panel context. * @returns The panel ID or null if not in a panel */ -export function usePanelId() { +export function usePanelId(): string | null { return useContext(ReactPanelContext); } diff --git a/plugins/ui/src/js/src/widget/DashboardWidgetHandler.tsx b/plugins/ui/src/js/src/widget/DashboardWidgetHandler.tsx index e5e2f81cb..126db25b0 100644 --- a/plugins/ui/src/js/src/widget/DashboardWidgetHandler.tsx +++ b/plugins/ui/src/js/src/widget/DashboardWidgetHandler.tsx @@ -35,7 +35,7 @@ function DashboardWidgetHandler({ onClose, onDataChange, ...otherProps -}: DashboardWidgetHandlerProps) { +}: DashboardWidgetHandlerProps): JSX.Element { const handleClose = useCallback(() => { log.debug('handleClose', id); onClose?.(id); diff --git a/plugins/ui/src/js/src/widget/DocumentHandler.tsx b/plugins/ui/src/js/src/widget/DocumentHandler.tsx index 449e00da7..f0ae8a48e 100644 --- a/plugins/ui/src/js/src/widget/DocumentHandler.tsx +++ b/plugins/ui/src/js/src/widget/DocumentHandler.tsx @@ -44,7 +44,7 @@ function DocumentHandler({ initialData: data = EMPTY_OBJECT, onDataChange = EMPTY_FUNCTION, onClose, -}: DocumentHandlerProps) { +}: DocumentHandlerProps): JSX.Element { log.debug('Rendering document', widget); const panelOpenCountRef = useRef(0); const panelIdIndex = useRef(0); diff --git a/plugins/ui/src/js/src/widget/WidgetHandler.tsx b/plugins/ui/src/js/src/widget/WidgetHandler.tsx index 057be65b7..c27dbdb44 100644 --- a/plugins/ui/src/js/src/widget/WidgetHandler.tsx +++ b/plugins/ui/src/js/src/widget/WidgetHandler.tsx @@ -58,7 +58,7 @@ function WidgetHandler({ fetch, widget: descriptor, initialData: initialDataProp, -}: WidgetHandlerProps) { +}: WidgetHandlerProps): JSX.Element | null { const [widget, setWidget] = useState(); const [document, setDocument] = useState(); const [initialData] = useState(initialDataProp); diff --git a/plugins/ui/src/js/src/widget/WidgetTestUtils.ts b/plugins/ui/src/js/src/widget/WidgetTestUtils.ts index f7cec022e..278c0e50b 100644 --- a/plugins/ui/src/js/src/widget/WidgetTestUtils.ts +++ b/plugins/ui/src/js/src/widget/WidgetTestUtils.ts @@ -4,7 +4,7 @@ import type { dh } from '@deephaven/jsapi-types'; export function makeDocumentUpdatedJsonRpc( document: Record = {} -) { +): { jsonrpc: string; method: string; params: string[] } { return { jsonrpc: '2.0', method: 'documentUpdated', @@ -14,7 +14,7 @@ export function makeDocumentUpdatedJsonRpc( export function makeDocumentUpdatedJsonRpcString( document: Record = {} -) { +): string { return JSON.stringify(makeDocumentUpdatedJsonRpc(document)); } diff --git a/plugins/ui/src/js/src/widget/WidgetTypes.ts b/plugins/ui/src/js/src/widget/WidgetTypes.ts index 8c870cb53..20d898ac2 100644 --- a/plugins/ui/src/js/src/widget/WidgetTypes.ts +++ b/plugins/ui/src/js/src/widget/WidgetTypes.ts @@ -3,8 +3,8 @@ import type { dh } from '@deephaven/jsapi-types'; export type WidgetId = string; export interface WidgetMessageDetails { - getDataAsBase64(): string; - getDataAsString(): string; + getDataAsBase64: () => string; + getDataAsString: () => string; exportedObjects: dh.WidgetExportedObject[]; } From 67f82e3bc58c173852b160c50c46e13a92b8af45 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 17 Apr 2024 16:16:13 -0400 Subject: [PATCH 02/10] feat: improve default dh.ui layouts (#411) - ui.panel now has default padding and gaps - ui.panel now accepts a subset of flex and view props - ui.panel now wraps children in view and flex containers - ui.panels with only one child (of type grid or plot) have padding removed - ui.flex flex property now defaults to "auto" - ui.flex/ui.view switched to use the wrapped @deephaven/component version - updated package.json to use latest linting rules (to ignore UNSAFE casing) - Fixes #317 Breaking changes: - ui.panel now has default padding and gaps - ui.flex flex property now defaults to "auto" --- .../ui/src/deephaven/ui/components/panel.py | 51 +++++++++- .../deephaven/ui/components/spectrum/flex.py | 18 +++- .../js/src/elements/SpectrumElementUtils.ts | 3 +- .../ui/src/js/src/elements/spectrum/Flex.tsx | 11 --- .../ui/src/js/src/elements/spectrum/index.ts | 1 - plugins/ui/src/js/src/layout/Column.tsx | 2 +- plugins/ui/src/js/src/layout/ReactPanel.tsx | 87 +++++++++++++++++- plugins/ui/src/js/src/layout/Row.tsx | 2 +- plugins/ui/src/js/src/styles.scss | 33 +++++-- tests/ui.spec.ts | 4 +- .../UI-loads-1-chromium-linux.png | Bin 7363 -> 7433 bytes .../UI-loads-1-firefox-linux.png | Bin 14354 -> 14521 bytes .../UI-loads-1-webkit-linux.png | Bin 6621 -> 6732 bytes 13 files changed, 180 insertions(+), 32 deletions(-) delete mode 100644 plugins/ui/src/js/src/elements/spectrum/Flex.tsx diff --git a/plugins/ui/src/deephaven/ui/components/panel.py b/plugins/ui/src/deephaven/ui/components/panel.py index 2f446c683..82da15bec 100644 --- a/plugins/ui/src/deephaven/ui/components/panel.py +++ b/plugins/ui/src/deephaven/ui/components/panel.py @@ -2,16 +2,63 @@ from typing import Any from ..elements import BaseElement +from .._internal.utils import create_props +from .spectrum.layout import ( + Direction, + Wrap, + JustifyContent, + AlignContent, + AlignItems, + DimensionValue, +) -def panel(*children: Any, title: str | None = None, **kwargs: Any): +def panel( + *children: Any, + title: str | None = None, + direction: Direction | None = "column", + wrap: Wrap | None = None, + justify_content: JustifyContent | None = None, + align_content: AlignContent | None = None, + align_items: AlignItems | None = None, + gap: DimensionValue | None = "size-100", + column_gap: DimensionValue | None = None, + row_gap: DimensionValue | None = None, + padding: DimensionValue | None = "size-100", + padding_top: DimensionValue | None = None, + padding_bottom: DimensionValue | None = None, + padding_start: DimensionValue | None = None, + padding_end: DimensionValue | None = None, + padding_x: DimensionValue | None = None, + padding_y: DimensionValue | None = None, + **props: Any, +): """ A panel is a container that can be used to group elements. Args: children: Elements to render in the panel. title: Title of the panel. + direction: The direction in which to layout children. + wrap: Whether children should wrap when they exceed the panel's width. + justify_content: The distribution of space around items along the main axis. + align_content: The distribution of space between and around items along the cross axis. + align_items: The alignment of children within their container. + gap: The space to display between both rows and columns of children. + column_gap: The space to display between columns of children. + row_gap: The space to display between rows of children. + padding: The padding to apply around the element. + padding_top: The padding to apply above the element. + padding_bottom: The padding to apply below the element. + padding_start: The padding to apply before the element. + padding_end: The padding to apply after the element. + padding_x: The padding to apply to the left and right of the element. + padding_y: The padding to apply to the top and bottom of the element. """ + + children, props = create_props(locals()) return BaseElement( - "deephaven.ui.components.Panel", *children, title=title, **kwargs + "deephaven.ui.components.Panel", + *children, + **props, ) diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/flex.py b/plugins/ui/src/deephaven/ui/components/spectrum/flex.py index 91f5f2f69..0eb365e03 100644 --- a/plugins/ui/src/deephaven/ui/components/spectrum/flex.py +++ b/plugins/ui/src/deephaven/ui/components/spectrum/flex.py @@ -2,6 +2,7 @@ from typing import Any from .basic import spectrum_element from .layout import ( + LayoutFlex, Direction, Wrap, JustifyContent, @@ -13,6 +14,7 @@ def flex( *children: Any, + flex: LayoutFlex | None = "auto", direction: Direction | None = None, wrap: Wrap | None = None, justify_content: JustifyContent | None = None, @@ -24,12 +26,24 @@ def flex( **props: Any, ): """ - Python implementation for the Adobe React Spectrum Flex component. - https://react-spectrum.adobe.com/react-spectrum/Flex.html + Base Flex component for laying out children in a flexbox. + + Args: + children: Elements to render in the flexbox. + flex: The flex property of the flexbox. + direction: The direction in which to layout children. + wrap: Whether children should wrap when they exceed the panel's width. + justify_content: The distribution of space around items along the main axis. + align_content: The distribution of space between and around items along the cross axis. + align_items: The alignment of children within their container. + gap: The space to display between both rows and columns of children. + column_gap: The space to display between columns of children. + row_gap: The space to display between rows of children. """ return spectrum_element( "Flex", *children, + flex=flex, direction=direction, wrap=wrap, justify_content=justify_content, diff --git a/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts b/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts index 1f3131040..94c56e405 100644 --- a/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts +++ b/plugins/ui/src/js/src/elements/SpectrumElementUtils.ts @@ -14,13 +14,12 @@ import { TabList, Text, ToggleButton, - View, } from '@adobe/react-spectrum'; import { ValueOf } from '@deephaven/utils'; +import { Flex, View } from '@deephaven/components'; import { ActionButton, Button, - Flex, Form, RangeSlider, Slider, diff --git a/plugins/ui/src/js/src/elements/spectrum/Flex.tsx b/plugins/ui/src/js/src/elements/spectrum/Flex.tsx deleted file mode 100644 index 60a396bd2..000000000 --- a/plugins/ui/src/js/src/elements/spectrum/Flex.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { Flex as SpectrumFlex, FlexProps } from '@adobe/react-spectrum'; - -function Flex(props: FlexProps): JSX.Element { - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -} - -Flex.displayName = 'Flex'; - -export default Flex; diff --git a/plugins/ui/src/js/src/elements/spectrum/index.ts b/plugins/ui/src/js/src/elements/spectrum/index.ts index c7b144d93..a3e908593 100644 --- a/plugins/ui/src/js/src/elements/spectrum/index.ts +++ b/plugins/ui/src/js/src/elements/spectrum/index.ts @@ -6,7 +6,6 @@ */ export { default as ActionButton } from './ActionButton'; export { default as Button } from './Button'; -export { default as Flex } from './Flex'; export { default as Form } from './Form'; export { default as RangeSlider } from './RangeSlider'; export { default as Slider } from './Slider'; diff --git a/plugins/ui/src/js/src/layout/Column.tsx b/plugins/ui/src/js/src/layout/Column.tsx index eb9f21901..c60cbae36 100644 --- a/plugins/ui/src/js/src/layout/Column.tsx +++ b/plugins/ui/src/js/src/layout/Column.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useMemo } from 'react'; import { useLayoutManager } from '@deephaven/dashboard'; import type { RowOrColumn } from '@deephaven/golden-layout'; +import { Flex } from '@deephaven/components'; import { normalizeColumnChildren, type ColumnElementProps, } from './LayoutUtils'; import { ParentItemContext, useParentItem } from './ParentItemContext'; import { usePanelId } from './ReactPanelContext'; -import { Flex } from '../elements/spectrum'; function LayoutColumn({ children, diff --git a/plugins/ui/src/js/src/layout/ReactPanel.tsx b/plugins/ui/src/js/src/layout/ReactPanel.tsx index 2688dcebd..c4202739a 100644 --- a/plugins/ui/src/js/src/layout/ReactPanel.tsx +++ b/plugins/ui/src/js/src/layout/ReactPanel.tsx @@ -7,6 +7,7 @@ import { useLayoutManager, useListener, } from '@deephaven/dashboard'; +import { View, ViewProps, Flex, FlexProps } from '@deephaven/components'; import Log from '@deephaven/log'; import PortalPanel from './PortalPanel'; import { ReactPanelControl, useReactPanel } from './ReactPanelManager'; @@ -17,13 +18,64 @@ import { usePortalPanelManager } from './PortalPanelManagerContext'; const log = Log.module('@deephaven/js-plugin-ui/ReactPanel'); +interface Props + extends ReactPanelProps, + Pick< + ViewProps, + | 'backgroundColor' + | 'padding' + | 'paddingTop' + | 'paddingBottom' + | 'paddingStart' + | 'paddingEnd' + | 'paddingX' + | 'paddingY' + | 'UNSAFE_style' + | 'UNSAFE_className' + >, + Pick< + FlexProps, + | 'wrap' + | 'direction' + | 'justifyContent' + | 'alignContent' + | 'alignItems' + | 'gap' + | 'rowGap' + | 'columnGap' + > {} + /** * Adds and tracks a panel to the GoldenLayout. When the child element is updated, the contents of the panel will also be updated. When unmounted, the panel will be removed. * Will trigger an `onOpen` when the portal is opened, and `onClose` when closed. * Note that because the `PortalPanel` will be saved with the GoldenLayout config, it's possible there is already a panel that exists with the same ID. * In that case, the existing panel will be re-used. */ -function ReactPanel({ children, title }: ReactPanelProps): JSX.Element | null { +function ReactPanel({ + // Apply the same defaults as panel.py + // but also defined here, incase the panel + // is being implicitly created + children, + title, + backgroundColor, + direction = 'column', + wrap, + justifyContent, + alignContent, + alignItems, + gap = 'size-100', + rowGap, + columnGap, + padding = 'size-100', + paddingTop, + paddingBottom, + paddingStart, + paddingEnd, + paddingX, + paddingY, + UNSAFE_style, + UNSAFE_className, +}: Props): JSX.Element | null { const layoutManager = useLayoutManager(); const { metadata, onClose, onOpen, panelId } = useReactPanel(); const portalManager = usePortalPanelManager(); @@ -117,7 +169,38 @@ function ReactPanel({ children, title }: ReactPanelProps): JSX.Element | null { return portal ? ReactDOM.createPortal( - {children} + + + {children} + + , portal, contentKey diff --git a/plugins/ui/src/js/src/layout/Row.tsx b/plugins/ui/src/js/src/layout/Row.tsx index 8491aa93a..cad61b97e 100644 --- a/plugins/ui/src/js/src/layout/Row.tsx +++ b/plugins/ui/src/js/src/layout/Row.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useMemo } from 'react'; import { useLayoutManager } from '@deephaven/dashboard'; import type { RowOrColumn } from '@deephaven/golden-layout'; +import { Flex } from '@deephaven/components'; import { normalizeRowChildren, type RowElementProps } from './LayoutUtils'; import { ParentItemContext, useParentItem } from './ParentItemContext'; import { usePanelId } from './ReactPanelContext'; -import { Flex } from '../elements/spectrum'; function LayoutRow({ children, height }: RowElementProps): JSX.Element | null { const layoutManager = useLayoutManager(); diff --git a/plugins/ui/src/js/src/styles.scss b/plugins/ui/src/js/src/styles.scss index fde7155b8..840e5fc52 100644 --- a/plugins/ui/src/js/src/styles.scss +++ b/plugins/ui/src/js/src/styles.scss @@ -1,15 +1,32 @@ +@import '@deephaven/components/scss/custom.scss'; + .ui-portal-panel { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - overflow: hidden; - position: relative; + display: contents; } .ui-object-container { display: contents; - flex-grow: 1; - flex-shrink: 1; position: relative; } + +.dh-react-panel { + .dh-inner-react-panel .iris-grid { + border: 1px solid var(--dh-color-bg); + border-radius: $border-radius; + } + + &:has(.dh-inner-react-panel > .iris-grid:only-child), + &:has( + .dh-inner-react-panel + > .ui-object-container:only-child + > .iris-grid:only-child + ), + &:has(.dh-inner-react-panel > .chart-wrapper:only-child) { + // remove the default panel padding when grid or chart is the only child + padding: 0 !important; // important required to override inline spectrum style + .iris-grid { + border: none; + border-radius: 0; + } + } +} diff --git a/tests/ui.spec.ts b/tests/ui.spec.ts index ddc2897ff..179655618 100644 --- a/tests/ui.spec.ts +++ b/tests/ui.spec.ts @@ -3,6 +3,6 @@ import { openPanel, gotoPage } from './utils'; test('UI loads', async ({ page }) => { await gotoPage(page, ''); - await openPanel(page, 'ui_component', '.ui-portal-panel'); - await expect(page.locator('.ui-portal-panel')).toHaveScreenshot(); + await openPanel(page, 'ui_component', '.dh-react-panel'); + await expect(page.locator('.dh-react-panel')).toHaveScreenshot(); }); diff --git a/tests/ui.spec.ts-snapshots/UI-loads-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-loads-1-chromium-linux.png index 30c17ce6c8515523012b05d6c26976888868be4e..44c520c36748f3b5fb464674f14b00564dc9602c 100644 GIT binary patch literal 7433 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 literal 7363 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 diff --git a/tests/ui.spec.ts-snapshots/UI-loads-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-loads-1-firefox-linux.png index a6bf6f6dfac2a18ae4ca9271b5e85381c34639a2..ca0111fdba9b4558009ce11ea4c4b0eb059241ea 100644 GIT binary patch literal 14521 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 literal 14354 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 OV5o0&y5OY!?f(EtMNC!z diff --git a/tests/ui.spec.ts-snapshots/UI-loads-1-webkit-linux.png b/tests/ui.spec.ts-snapshots/UI-loads-1-webkit-linux.png index 183a38341ba26f45d0f4e60f6d4908e28c5f403a..813549a3909882edf5a694e82bead187f2581250 100644 GIT binary patch literal 6732 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 X%mbs`^9#lR0wD+c)5q{fFa7dA5^#K> literal 6621 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 a)=qD|zfATs*$(y-VsGbo?2XO2>;D1$JSi;z From fb512142681a86872776d5cb83ff7babcd467042 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Wed, 17 Apr 2024 16:21:14 -0400 Subject: [PATCH 03/10] chore(version): ui-v0.12.0 --- package-lock.json | 2 +- plugins/ui/CHANGELOG.md | 13 +++++++++++++ plugins/ui/setup.cfg | 2 +- plugins/ui/src/js/package.json | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1516183e0..a23e15663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35436,7 +35436,7 @@ }, "plugins/ui/src/js": { "name": "@deephaven/js-plugin-ui", - "version": "0.11.0", + "version": "0.12.0", "license": "Apache-2.0", "dependencies": { "@adobe/react-spectrum": "^3.34.1", diff --git a/plugins/ui/CHANGELOG.md b/plugins/ui/CHANGELOG.md index 525bff8b8..7e49e4211 100644 --- a/plugins/ui/CHANGELOG.md +++ b/plugins/ui/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## ui-v0.12.0 - 2024-04-17 +#### Documentation +- Combo box spec (#392) - (da1076a) - Joe +- toggle button (#402) - (702ad2a) - ethanalvizo +- date picker spec (#388) - (e1a135d) - Joe +#### Features +- improve default dh.ui layouts (#411) - (67f82e3) - Don +- Picker - format settings (#394) - (f9a0e34) - bmingles +#### Tests +- bump ts, eslint and prettier configs (#416) - (a4761cc) - Don + +- - - + ## ui-v0.11.0 - 2024-04-03 #### Bug Fixes - Re-opening widgets after re-hydrated (#379) - (42242a5) - mofojed diff --git a/plugins/ui/setup.cfg b/plugins/ui/setup.cfg index a4a8753e5..a4b0ddd32 100644 --- a/plugins/ui/setup.cfg +++ b/plugins/ui/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-ui description = deephaven.ui plugin long_description = file: README.md long_description_content_type = text/markdown -version = 0.11.0.dev0 +version = 0.12.0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json index a367cb3ea..a436c7693 100644 --- a/plugins/ui/src/js/package.json +++ b/plugins/ui/src/js/package.json @@ -1,6 +1,6 @@ { "name": "@deephaven/js-plugin-ui", - "version": "0.11.0", + "version": "0.12.0", "description": "Deephaven UI plugin", "keywords": [ "Deephaven", From bb4ac36e1637e507f9df2bcb1095502c3e62f050 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Wed, 17 Apr 2024 16:21:14 -0400 Subject: [PATCH 04/10] chore(version): update ui version to 0.12.0.dev0 --- plugins/ui/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ui/setup.cfg b/plugins/ui/setup.cfg index a4b0ddd32..08c3da53e 100644 --- a/plugins/ui/setup.cfg +++ b/plugins/ui/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-ui description = deephaven.ui plugin long_description = file: README.md long_description_content_type = text/markdown -version = 0.12.0 +version = 0.12.0.dev0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins From 5ed66a7a2d1587eae17db0e2a2d0d728094e24f7 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 18 Apr 2024 09:44:18 -0500 Subject: [PATCH 05/10] feat: python date picker implementation (#409) fixes #368 Implements the python side of date picking, including converting all date props to Java date types and wrapping the callback function with the appropriate conversion. --- plugins/ui/DESIGN.md | 2 +- .../ui/src/deephaven/ui/_internal/utils.py | 195 ++++++++++++- .../src/deephaven/ui/components/__init__.py | 2 + .../deephaven/ui/components/date_picker.py | 273 ++++++++++++++++++ .../ui/components/spectrum/__init__.py | 1 + .../ui/components/spectrum/date_picker.py | 8 + plugins/ui/src/deephaven/ui/types/types.py | 32 ++ .../ui/test/deephaven/ui/test_date_picker.py | 88 ++++++ plugins/ui/test/deephaven/ui/test_utils.py | 44 +++ 9 files changed, 643 insertions(+), 2 deletions(-) create mode 100644 plugins/ui/src/deephaven/ui/components/date_picker.py create mode 100644 plugins/ui/src/deephaven/ui/components/spectrum/date_picker.py create mode 100644 plugins/ui/test/deephaven/ui/test_date_picker.py diff --git a/plugins/ui/DESIGN.md b/plugins/ui/DESIGN.md index f101165ac..cbdb538bb 100644 --- a/plugins/ui/DESIGN.md +++ b/plugins/ui/DESIGN.md @@ -1385,7 +1385,7 @@ ui.date_picker( granularity: Granularity | None = None, on_change: Callable[[Date], None] | None = None, **props: Any -) -> ListViewElement +) -> DatePickerElement ``` ###### Parameters diff --git a/plugins/ui/src/deephaven/ui/_internal/utils.py b/plugins/ui/src/deephaven/ui/_internal/utils.py index 1cd6b66e3..3ee0fe706 100644 --- a/plugins/ui/src/deephaven/ui/_internal/utils.py +++ b/plugins/ui/src/deephaven/ui/_internal/utils.py @@ -4,11 +4,20 @@ from inspect import signature import sys from functools import partial +from deephaven.time import to_j_instant, to_j_zdt, to_j_local_date + +from ..types import Date, JavaDate _UNSAFE_PREFIX = "UNSAFE_" _ARIA_PREFIX = "aria_" _ARIA_PREFIX_REPLACEMENT = "aria-" +_CONVERTERS = { + "java.time.Instant": to_j_instant, + "java.time.ZonedDateTime": to_j_zdt, + "java.time.LocalDate": to_j_local_date, +} + def get_component_name(component: Any) -> str: """ @@ -194,6 +203,190 @@ def create_props(args: dict[str, Any]) -> tuple[tuple[Any], dict[str, Any]]: Returns: A tuple of children and props """ - children, props = args.pop("children"), args.pop("props") + children, props = args.pop("children", tuple()), args.pop("props", {}) props.update(args) return children, props + + +def _convert_to_java_date( + date: Date, +) -> JavaDate: + """ + Convert a Date to a Java date type. + In order of preference, tries to convert to Instant, ZonedDateTime, and LocalDate. + If none of these work, raises a TypeError. + + Args: + date: The date to convert to a Java date type. + + Returns: + The Java date type. + """ + try: + return to_j_instant(date) # type: ignore + except Exception: + # ignore, try next + pass + + try: + return to_j_zdt(date) # type: ignore + except Exception: + # ignore, try next + pass + + try: + return to_j_local_date(date) # type: ignore + except Exception: + raise TypeError( + f"Could not convert {date} to one of Instant, ZonedDateTime, or LocalDate." + ) + + +def get_jclass_name(value: Any) -> str: + """ + Get the name of the Java class of the value. + + Args: + value: The value to get the Java class name of. + + Returns: + The name of the Java class of the value. + """ + return str(value.jclass)[6:] + + +def _jclass_converter( + value: JavaDate, +) -> Callable[[Date], Any]: + """ + Get the converter for the Java date type. + + Args: + value: The Java date type to get the converter for. + + Returns: + The converter for the Java date type. + """ + return _CONVERTERS[get_jclass_name(value)] + + +def _wrap_date_callable( + date_callable: Callable[[Date], None], + converter: Callable[[Date], Any], +) -> Callable[[Date], None]: + """ + Wrap a callable to convert the Date argument to a Java date type. + This maintains the original callable signature so that the Date argument can be dropped. + + Args: + date_callable: The callable to wrap. + converter: The date converter to use. + + Returns: + The wrapped callable. + """ + return lambda date: wrap_callable(date_callable)(converter(date)) + + +def _get_first_set_key(props: dict[str, Any], sequence: Sequence[str]) -> str | None: + """ + Of the keys in sequence, get the first key that has a non-None value in props. + If none of the keys have a non-None value, return None. + + Args: + props: The props to check for non-None values. + sequence: The sequence to check. + + Returns: + The first non-None prop, or None if all props are None. + """ + for key in sequence: + if props.get(key) is not None: + return key + return None + + +def _prioritized_callable_converter( + props: dict[str, Any], + priority: Sequence[str], + default_converter: Callable[[Date], Any], +) -> Callable[[Date], Any]: + """ + Get a callable date converter based on the type of the first non-None prop set. + Checks the props in the order provided by the `priority` sequence. + All the props in `priority` should be Java date types already. + We do this so conversion so that the type returned on callbacks matches the type passed in by the user. + If none of the props in `priority` are present, returns the default converter. + + Args: + props: The props passed to the component. + priority: The priority of the props to check. + default_converter: The default converter to use if none of the priority props are present. + + Returns: + The callable date converter. + """ + + first_set_key = _get_first_set_key(props, priority) + return ( + _jclass_converter(props[first_set_key]) + if first_set_key is not None + else default_converter + ) + + +def convert_list_prop( + key: str, + value: list[Date] | None, +) -> list[JavaDate] | None: + """ + Convert a list of Dates to Java date types. + + Args: + key: The key of the prop. + value: A list of Dates to convert to Java date types. + + Returns: + The list of Java date types. + """ + if value is None: + return None + + if not isinstance(value, list): + raise TypeError(f"{key} must be a list of Dates") + return [_convert_to_java_date(date) for date in value] + + +def convert_date_props( + props: dict[str, Any], + simple_date_props: set[str], + callable_date_props: set[str], + priority: Sequence[str], + default_converter: Callable[[Date], Any] = to_j_instant, +) -> None: + """ + Convert date props to Java date types in place. + + Args: + props: The props passed to the component. + simple_date_props: A set of simple date keys to convert. The prop value should be a single Date. + callable_date_props: A set of callable date keys to convert. + The prop value should be a callable that takes a Date. + priority: The priority of the props to check. + default_converter: The default converter to use if none of the priority props are present. + + Returns: + The converted props. + """ + for key in simple_date_props: + if props.get(key) is not None: + props[key] = _convert_to_java_date(props[key]) + + # the simple props must be converted before this to simplify the callable conversion + converter = _prioritized_callable_converter(props, priority, default_converter) + + for key in callable_date_props: + if props.get(key) is not None: + if not callable(props[key]): + raise TypeError(f"{key} must be a callable") + props[key] = _wrap_date_callable(props[key], converter) diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index f0678f892..d18981f10 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -14,6 +14,7 @@ from .list_view import list_view from .list_action_group import list_action_group from .list_action_menu import list_action_menu +from .date_picker import date_picker from . import html @@ -28,6 +29,7 @@ "content", "contextual_help", "dashboard", + "date_picker", "flex", "form", "fragment", diff --git a/plugins/ui/src/deephaven/ui/components/date_picker.py b/plugins/ui/src/deephaven/ui/components/date_picker.py new file mode 100644 index 000000000..98160cf42 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/date_picker.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +from typing import Any, Sequence, Callable + +from .spectrum import ( + FocusEventCallable, + KeyboardEventCallable, + LayoutFlex, + Number, + DimensionValue, + AlignSelf, + JustifySelf, + Position, + AriaPressed, + CSSProperties, + LabelPosition, + Alignment, + ValidationBehavior, + NecessityIndicator, + ValidationState, + PageBehavior, + HourCycle, +) +from ..hooks import use_memo +from ..elements import Element, BaseElement +from .._internal.utils import ( + create_props, + convert_date_props, + convert_list_prop, +) +from ..types import Date, Granularity + +DatePickerElement = Element + +# All the props that can be date types +_SIMPLE_DATE_PROPS = { + "placeholder_value", + "value", + "default_value", + "min_value", + "max_value", +} +_LIST_DATE_PROPS = {"unavailable_values"} +_CALLABLE_DATE_PROPS = {"on_change"} + +# The priority of the date props to determine the format of the date passed to the callable date props +_DATE_PROPS_PRIORITY = ["value", "default_value", "placeholder_value"] + + +def _convert_date_picker_props( + props: dict[str, Any], +) -> dict[str, Any]: + """ + Convert date picker props to Java date types. + + Args: + props: The props passed to the date picker. + + Returns: + The converted props. + """ + + convert_date_props( + props, + _SIMPLE_DATE_PROPS, + _CALLABLE_DATE_PROPS, + _DATE_PROPS_PRIORITY, + ) + + return props + + +def date_picker( + placeholder_value: Date | None = None, + value: Date | None = None, + default_value: Date | None = None, + min_value: Date | None = None, + max_value: Date | None = None, + unavailable_values: Sequence[Date] | None = None, + granularity: Granularity | None = None, + page_behavior: PageBehavior | None = None, + hour_cycle: HourCycle | None = None, + hide_time_zone: bool = False, + should_force_leading_zeros: bool | None = None, + is_disabled: bool | None = None, + is_read_only: bool | None = None, + is_required: bool | None = None, + validation_behavior: ValidationBehavior | None = None, + auto_focus: bool | None = None, + label: Element | None = None, + description: Element | None = None, + error_message: Element | None = None, + is_open: bool | None = None, + default_open: bool | None = None, + name: str | None = None, + max_visible_months: int | None = None, + should_flip: bool | None = None, + is_quiet: bool | None = None, + show_format_help_text: bool | None = None, + label_position: LabelPosition | None = None, + label_align: Alignment | None = None, + necessity_indicator: NecessityIndicator | None = None, + contextual_help: Element | None = None, + validation_state: ValidationState | None = None, + on_focus: FocusEventCallable | None = None, + on_blur: FocusEventCallable | None = None, + on_focus_change: Callable[[bool], None] | None = None, + on_key_down: KeyboardEventCallable | None = None, + on_key_up: KeyboardEventCallable | None = None, + on_open_change: Callable[[bool], None] | None = None, + on_change: Callable[[Date], None] | None = None, + flex: LayoutFlex | None = None, + flex_grow: Number | None = None, + flex_shrink: Number | None = None, + flex_basis: DimensionValue | None = None, + align_self: AlignSelf | None = None, + justify_self: JustifySelf | None = None, + order: Number | None = None, + grid_area: str | None = None, + grid_row: str | None = None, + grid_row_start: str | None = None, + grid_row_end: str | None = None, + grid_column: str | None = None, + grid_column_start: str | None = None, + grid_column_end: str | None = None, + margin: DimensionValue | None = None, + margin_top: DimensionValue | None = None, + margin_bottom: DimensionValue | None = None, + margin_start: DimensionValue | None = None, + margin_end: DimensionValue | None = None, + margin_x: DimensionValue | None = None, + margin_y: DimensionValue | None = None, + width: DimensionValue | None = None, + height: DimensionValue | None = None, + min_width: DimensionValue | None = None, + min_height: DimensionValue | None = None, + max_width: DimensionValue | None = None, + max_height: DimensionValue | None = None, + position: Position | None = None, + top: DimensionValue | None = None, + bottom: DimensionValue | None = None, + start: DimensionValue | None = None, + end: DimensionValue | None = None, + left: DimensionValue | None = None, + right: DimensionValue | None = None, + z_index: Number | None = None, + is_hidden: bool | None = None, + id: str | None = None, + aria_label: str | None = None, + aria_labelledby: str | None = None, + aria_describedby: str | None = None, + aria_pressed: AriaPressed | None = None, + aria_details: str | None = None, + UNSAFE_class_name: str | None = None, + UNSAFE_style: CSSProperties | None = None, +) -> DatePickerElement: + """ + A date picker allows the user to select a date. + + + Args: + placeholder_value: A placeholder date that influences the format of the + placeholder shown when no value is selected. + Defaults to today at midnight in the user's timezone. + value: The current value (controlled). + default_value: The default value (uncontrolled). + min_value: The minimum allowed date that a user may select. + max_value: The maximum allowed date that a user may select. + unavailable_values: A list of dates that cannot be selected. + granularity: Determines the smallest unit that is displayed in the date picker. + By default, this is `"DAY"` for `LocalDate`, and `"SECOND"` otherwise. + page_behavior: Controls the behavior of paging. Pagination either works by + advancing the visible page by visibleDuration (default) + or one unit of visibleDuration. + hour_cycle: Whether to display the time in 12 or 24 hour format. + By default, this is determined by the user's locale. + hide_time_zone: Whether to hide the time zone abbreviation. + should_force_leading_zeros: Whether to always show leading zeros in the + month, day, and hour fields. + By default, this is determined by the user's locale. + is_disabled: Whether the input is disabled. + is_read_only: Whether the input can be selected but not changed by the user. + is_required: Whether user input is required on the input before form submission. + validation_behavior: Whether to use native HTML form validation to prevent form + submission when the value is missing or invalid, + or mark the field as required or invalid via ARIA. + auto_focus: Whether the element should receive focus on render. + label: The content to display as the label. + description: A description for the field. + Provides a hint such as specific requirements for what to choose. + error_message: An error message for the field. + is_open: Whether the overlay is open by default (controlled). + default_open: Whether the overlay is open by default (uncontrolled). + name: The name of the input element, used when submitting an HTML form. + max_visible_months: The maximum number of months to display at + once in the calendar popover, if screen space permits. + should_flip: Whether the calendar popover should automatically flip direction + when space is limited. + is_quiet: Whether the date picker should be displayed with a quiet style. + show_format_help_text: Whether to show the localized date format as help + text below the field. + label_position: The label's overall position relative to the element it is labeling. + label_align: The label's horizontal alignment relative to the element it is labeling. + necessity_indicator: Whether the required state should be shown as an icon or text. + contextual_help: A ContextualHelp element to place next to the label. + validation_state: Whether the input should display its "valid" or "invalid" visual styling. + on_focus: Function called when the button receives focus. + on_blur: Function called when the button loses focus. + on_focus_change: Function called when the focus state changes. + on_key_down: Function called when a key is pressed. + on_key_up: Function called when a key is released. + on_open_change: Handler that is called when the overlay's open state changes. + on_change: Handler that is called when the value changes. + The exact `Date` type will be the same as the type passed to + `value`, `default_value` or `placeholder_value`, in that order of precedence. + flex: When used in a flex layout, specifies how the element will grow or shrink to fit the space available. + flex_grow: When used in a flex layout, specifies how much the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how much the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial size of the element. + align_self: Overrides the align_items property of a flex or grid container. + justify_self: Specifies how the element is justified inside a flex or grid container. + order: The layout order for the element within a flex or grid container. + grid_area: The name of the grid area to place the element in. + grid_row: The name of the grid row to place the element in. + grid_row_start: The name of the grid row to start the element in. + grid_row_end: The name of the grid row to end the element in. + grid_column: The name of the grid column to place the element in. + grid_column_start: The name of the grid column to start the element in. + grid_column_end: The name of the grid column to end the element in. + margin: The margin to apply around the element. + margin_top: The margin to apply above the element. + margin_bottom: The margin to apply below the element. + margin_start: The margin to apply before the element. + margin_end: The margin to apply after the element. + margin_x: The margin to apply to the left and right of the element. + margin_y: The margin to apply to the top and bottom of the element. + width: The width of the element. + height: The height of the element. + min_width: The minimum width of the element. + min_height: The minimum height of the element. + max_width: The maximum width of the element. + max_height: The maximum height of the element. + position: Specifies how the element is positioned. + top: The distance from the top of the containing element. + bottom: The distance from the bottom of the containing element. + start: The distance from the start of the containing element. + end: The distance from the end of the containing element. + left: The distance from the left of the containing element. + right: The distance from the right of the containing element. + z_index: The stack order of the element. + is_hidden: Whether the element is hidden. + id: A unique identifier for the element. + aria_label: The label for the element. + aria_labelledby: The id of the element that labels the element. + aria_describedby: The id of the element that describes the element. + aria_pressed: Whether the element is pressed. + aria_details: The details for the element. + UNSAFE_class_name: A CSS class to apply to the element. + UNSAFE_style: A CSS style to apply to the element. + + Returns: + The date picker element. + """ + _, props = create_props(locals()) + + _convert_date_picker_props(props) + + props["unavailable_values"] = use_memo( + lambda: convert_list_prop("unavailable_values", props["unavailable_values"]), + [unavailable_values], + ) + + return BaseElement("deephaven.ui.components.DatePicker", **props) diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/__init__.py b/plugins/ui/src/deephaven/ui/components/spectrum/__init__.py index 3f5ce97a6..999367be5 100644 --- a/plugins/ui/src/deephaven/ui/components/spectrum/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/spectrum/__init__.py @@ -5,3 +5,4 @@ from .text_field import * from .toggle_button import * from .flex import * +from .date_picker import * diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/date_picker.py b/plugins/ui/src/deephaven/ui/components/spectrum/date_picker.py new file mode 100644 index 000000000..3c8be378d --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/spectrum/date_picker.py @@ -0,0 +1,8 @@ +from typing import Literal + +PageBehavior = Literal["single", "visible"] +HourCycle = Literal[12, 24] +ValidationBehavior = Literal["aria", "native"] +Alignment = Literal["start", "end"] +NecessityIndicator = Literal["label", "icon"] +ValidationState = Literal["valid", "invalid"] diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py index 90bfd8480..9641d5aab 100644 --- a/plugins/ui/src/deephaven/ui/types/types.py +++ b/plugins/ui/src/deephaven/ui/types/types.py @@ -1,5 +1,9 @@ +import datetime +import pandas +import numpy from typing import Any, Dict, Literal, Union, List, Tuple, Callable, TypedDict, Sequence from deephaven import SortDirection +from deephaven.dtypes import DType class CellData(TypedDict): @@ -106,6 +110,34 @@ class RowDataValue(CellData): Stringable = Union[str, int, float, bool] Key = Stringable ActionKey = Key +LocalDate = DType +Instant = DType +ZonedDateTime = DType +JavaDate = Union[LocalDate, Instant, ZonedDateTime] +LocalDateConvertible = Union[ + None, + LocalDate, + str, + datetime.date, + datetime.datetime, + numpy.datetime64, + pandas.Timestamp, +] +InstantConvertible = Union[ + None, Instant, int, str, datetime.datetime, numpy.datetime64, pandas.Timestamp # type: ignore +] +ZonedDateTimeConvertible = Union[ + None, ZonedDateTime, str, datetime.datetime, numpy.datetime64, pandas.Timestamp # type: ignore +] +Date = Union[ + Instant, + LocalDate, + ZonedDateTime, + LocalDateConvertible, + InstantConvertible, + ZonedDateTimeConvertible, +] +Granularity = Literal["DAY", "HOUR", "MINUTE", "SECOND"] Dependencies = Union[Tuple[Any], List[Any]] Selection = Sequence[Key] diff --git a/plugins/ui/test/deephaven/ui/test_date_picker.py b/plugins/ui/test/deephaven/ui/test_date_picker.py new file mode 100644 index 000000000..d7a2d9624 --- /dev/null +++ b/plugins/ui/test/deephaven/ui/test_date_picker.py @@ -0,0 +1,88 @@ +import unittest + +from .BaseTest import BaseTestCase + + +class DatePickerTest(BaseTestCase): + def test_convert_date_props(self): + from deephaven.time import to_j_instant, to_j_zdt, to_j_local_date + from deephaven.ui.components.date_picker import _convert_date_picker_props + from deephaven.ui._internal.utils import get_jclass_name, convert_list_prop + + def verify_is_local_date(date): + self.assertEqual(get_jclass_name(date), "java.time.LocalDate") + + def verify_is_instant(date): + self.assertEqual(get_jclass_name(date), "java.time.Instant") + + def verify_is_zdt(date): + self.assertEqual(get_jclass_name(date), "java.time.ZonedDateTime") + + def empty_on_change(): + pass + + props1 = { + "placeholder_value": "2021-01-01", + "value": "2021-01-01 UTC", + "default_value": "2021-01-01 ET", + "unavailable_dates": [to_j_instant("2021-01-01 UTC"), "2021-01-01"], + "min_value": to_j_zdt("2021-01-01 ET"), + "max_value": to_j_local_date("2021-01-01"), + } + + props2 = { + "value": to_j_local_date("2021-01-01"), + "default_value": to_j_zdt("2021-01-01 ET"), + "placeholder_value": to_j_instant("2021-01-01 UTC"), + "on_change": verify_is_local_date, + "unavailable_dates": None, + } + + props3 = { + "default_value": to_j_instant("2021-01-01 UTC"), + "placeholder_value": to_j_zdt("2021-01-01 ET"), + "on_change": verify_is_instant, + } + + props4 = { + "placeholder_value": to_j_zdt("2021-01-01 ET"), + "on_change": verify_is_zdt, + } + + props5 = {"on_change": verify_is_instant} + + props6 = {"on_change": empty_on_change} + + _convert_date_picker_props(props1) + props1["unavailable_dates"] = convert_list_prop( + "unavailable_dates", props1["unavailable_dates"] + ) + _convert_date_picker_props(props2) + props2["unavailable_dates"] = convert_list_prop( + "unavailable_dates", props2["unavailable_dates"] + ) + _convert_date_picker_props(props3) + _convert_date_picker_props(props4) + _convert_date_picker_props(props5) + _convert_date_picker_props(props6) + + verify_is_local_date(props1["max_value"]) + verify_is_zdt(props1["min_value"]) + verify_is_instant(props1["unavailable_dates"][0]) + verify_is_local_date(props1["unavailable_dates"][1]) + verify_is_instant(props1["value"]) + verify_is_instant(props1["default_value"]) + verify_is_local_date(props1["placeholder_value"]) + + props2["on_change"]("2021-01-01") + self.assertIsNone(props2["unavailable_dates"]) + props3["on_change"]("2021-01-01 UTC") + props4["on_change"]("2021-01-01 ET") + props5["on_change"]("2021-01-01 UTC") + + # pass an Instant but it should be dropped with no error + props6["on_change"]("2021-01-01 UTC") + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/ui/test/deephaven/ui/test_utils.py b/plugins/ui/test/deephaven/ui/test_utils.py index f67f2bb80..d98bca7e0 100644 --- a/plugins/ui/test/deephaven/ui/test_utils.py +++ b/plugins/ui/test/deephaven/ui/test_utils.py @@ -217,6 +217,50 @@ def test_func_with_all_args(a, /, b, *args, c=1, **kwargs): # Test that wrapping a function without a signature doesn't throw an error wrapped = wrap_callable(print) + def test_create_props(self): + from deephaven.ui._internal.utils import create_props + + children1, props1 = create_props( + { + "foo": "bar", + "baz": 42, + "fizz": "buzz", + } + ) + + self.assertEqual(children1, tuple()) + self.assertDictEqual( + props1, + { + "foo": "bar", + "baz": 42, + "fizz": "buzz", + }, + ) + + children2, props2 = create_props( + { + "children": ["item1", "item2"], + "test": "value", + "props": { + "foo": "bar", + "baz": 42, + "fizz": "buzz", + }, + } + ) + + self.assertEqual(children2, ["item1", "item2"]) + self.assertDictEqual( + props2, + { + "foo": "bar", + "baz": 42, + "fizz": "buzz", + "test": "value", + }, + ) + if __name__ == "__main__": unittest.main() From b10f67c5c1b034fd6e5cf45a548d609e2f32d7f1 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 18 Apr 2024 15:46:59 -0400 Subject: [PATCH 06/10] fix: buttons not working due to extra prop (#423) resolves #419 Broken by #416, extra prop slipped in while adding types and broke things. Removed the uncessary prop. --- plugins/ui/src/js/src/elements/spectrum/Button.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/ui/src/js/src/elements/spectrum/Button.tsx b/plugins/ui/src/js/src/elements/spectrum/Button.tsx index a8b4f28b8..7db01e1b5 100644 --- a/plugins/ui/src/js/src/elements/spectrum/Button.tsx +++ b/plugins/ui/src/js/src/elements/spectrum/Button.tsx @@ -6,7 +6,6 @@ import { import { SerializedButtonEventProps, useButtonProps } from './useButtonProps'; function Button( - variant: SpectrumButtonProps['variant'], props: SpectrumButtonProps & SerializedButtonEventProps ): JSX.Element { const buttonProps = useButtonProps(props); From 6ead733f736c8b714d1d883199d5a9f42830e607 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 18 Apr 2024 15:18:10 -0500 Subject: [PATCH 07/10] fix: add on_change to toggle_button (#426) Fixes #420 The code in the issue works now ``` import deephaven.ui as ui from deephaven.ui import use_memo, use_state from deephaven import agg import deephaven.plot.express as dx stocks = dx.data.stocks() def get_by_filter(**byargs): """ Gets a by filter where the arguments are all args passed in where the value is true. e.g. get_by_filter(sym=True, exchange=False) == ["sym"] get_by_filter(exchange=False) == [] get_by_filter(sym=True, exchange=True) == ["sym", "exchange"] """ return [k for k in byargs if byargs[k]] @ui.component def stock_table(source): is_sym, set_is_sym = ui.use_state(False) is_exchange, set_is_exchange = ui.use_state(False) highlight, set_highlight = ui.use_state("") aggs, set_aggs = ui.use_state(agg.avg(cols=["size", "price", "dollars"])) by = get_by_filter(sym=is_sym, exchange=is_exchange) formatted_table = ui.use_memo( lambda: source.format_row_where(f"sym=`{highlight}`", "LEMONCHIFFON"), [source, highlight], ) rolled_table = ui.use_memo( lambda: ( formatted_table if len(by) == 0 else formatted_table.rollup(aggs=aggs, by=by) ), [formatted_table, aggs, by], ) return ui.flex( ui.flex( ui.toggle_button(ui.icon("vsSymbolMisc"), "By Sym", on_change=set_is_sym), ui.toggle_button( ui.icon("vsBell"), "By Exchange", on_change=set_is_exchange ), ( ui.fragment( ui.text_field( label="Highlight Sym", label_position="side", value=highlight, on_change=set_highlight, ), ui.contextual_help( ui.heading("Highlight Sym"), ui.content("Enter a sym you would like highlighted."), ), ) if not is_sym and not is_exchange else None ), align_items="center", gap="size-100", margin="size-100", margin_bottom="0", ), rolled_table, direction="column", flex_grow=1, ) st = stock_table(stocks) ``` --- .../ui/src/deephaven/ui/components/spectrum/toggle_button.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/ui/src/deephaven/ui/components/spectrum/toggle_button.py b/plugins/ui/src/deephaven/ui/components/spectrum/toggle_button.py index e7ce1c445..9b058c3cd 100644 --- a/plugins/ui/src/deephaven/ui/components/spectrum/toggle_button.py +++ b/plugins/ui/src/deephaven/ui/components/spectrum/toggle_button.py @@ -31,6 +31,7 @@ def toggle_button( is_quiet: bool | None = None, static_color: StaticColor | None = None, type: ButtonType = "button", + on_change: Callable[[bool], None] | None = None, on_press: PressEventCallable | None = None, on_press_start: PressEventCallable | None = None, on_press_end: PressEventCallable | None = None, @@ -103,6 +104,7 @@ def toggle_button( is_quiet: Whether the button should be quiet. static_color: The static color style to apply. Useful when the button appears over a color background. type: The type of button to render. (default: "button") + on_change: Handler that is called when the element's selection state changes. on_press: Function called when the button is pressed. on_press_start: Function called when the button is pressed. on_press_end: Function called when a press interaction ends, either over the target or when the pointer leaves the target. From 5f4f2381949388260596a4ae7d8104b7ad6df118 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 18 Apr 2024 15:18:26 -0500 Subject: [PATCH 08/10] fix: Fix conditional use_effect in use_table_listener (#422) fixes #384 Moves the refreshing table check to ensure the `use_effect` is always called. Also added a `use_effect` to `use_table_data` to ensure that when a table is swapped it updates the resulting data Here is an example of flipping between ticking and static tables ``` import deephaven.ui as ui from deephaven.table import Table from deephaven import time_table, empty_table empty_t = empty_table(0) time_t = time_table("PT1S").tail(1) @ui.component def test_component(): empty, set_empty = ui.use_state(True) val = ui.use_table_data(empty_t if empty else time_t) button = ui.action_button(str(empty), on_press=lambda: set_empty(not empty)) return [button, str(val)] data = test_component() ``` --- .../src/deephaven/ui/hooks/use_table_data.py | 4 ++ .../deephaven/ui/hooks/use_table_listener.py | 9 ++-- plugins/ui/test/deephaven/ui/test_hooks.py | 45 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/hooks/use_table_data.py b/plugins/ui/src/deephaven/ui/hooks/use_table_data.py index 3f2714612..200fe7049 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_table_data.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_table_data.py @@ -142,6 +142,10 @@ def use_table_data( table_updated = lambda: _set_new_data(table, sentinel, set_data, set_is_sentinel) + # call table_updated in the case of new table or sentinel + ui.use_effect(table_updated, [table, sentinel]) + + # call table_updated every time the table updates ui.use_table_listener( table, partial(_on_update, ctx, table_updated, executor_name), [] ) diff --git a/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py b/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py index a7eef4ad5..06ab38fc8 100644 --- a/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py +++ b/plugins/ui/src/deephaven/ui/hooks/use_table_listener.py @@ -1,7 +1,7 @@ from __future__ import annotations from functools import partial -from typing import Any, Callable, Sequence +from typing import Callable from deephaven.table import Table from deephaven.table_listener import listen, TableUpdate, TableListener @@ -86,10 +86,6 @@ def use_table_listener( replay_lock: The lock type used during replay, default is ‘shared’, can also be ‘exclusive’. """ - if not table.is_refreshing and not do_replay: - # if the table is not refreshing, and is not replaying, there is nothing to listen to - return - def start_listener() -> Callable[[], None]: """ Start the listener. Returns a function that can be called to stop the listener by the use_effect hook. @@ -97,6 +93,9 @@ def start_listener() -> Callable[[], None]: Returns: A function that can be called to stop the listener by the use_effect hook. """ + if not table.is_refreshing and not do_replay: + return lambda: None + handle = listen( table, wrap_listener(listener), diff --git a/plugins/ui/test/deephaven/ui/test_hooks.py b/plugins/ui/test/deephaven/ui/test_hooks.py index dce874bb1..40cdf1900 100644 --- a/plugins/ui/test/deephaven/ui/test_hooks.py +++ b/plugins/ui/test/deephaven/ui/test_hooks.py @@ -274,6 +274,51 @@ def _test_table_data(t=table): self.assertEqual(result, expected) + def test_swapping_table_data(self): + from deephaven.ui.hooks import use_table_data + from deephaven import new_table + from deephaven.column import int_col + from deephaven import DynamicTableWriter + import deephaven.dtypes as dht + + table = new_table( + [ + int_col("X", [1, 2, 3]), + int_col("Y", [2, 4, 6]), + ] + ) + + def _test_table_data(t=table): + result = use_table_data(t, sentinel="sentinel") + return result + + render_result = render_hook(_test_table_data) + + result, rerender = itemgetter("result", "rerender")(render_result) + + column_definitions = {"Numbers": dht.int32, "Words": dht.string} + + table_writer = DynamicTableWriter(column_definitions) + dynamic_table = table_writer.table + + # Need two rerenders because the first one will call set_data, which queues state updates + # that are resolved at the start of the second rerender + # The second rerender will then have the expected state values and return the expected result + rerender(dynamic_table) + result = rerender(dynamic_table) + + # the initial render should return the sentinel value since the table is empty + self.assertEqual(result, "sentinel") + + self.verify_table_updated(table_writer, dynamic_table, (1, "Testing")) + + rerender(dynamic_table) + result = rerender(dynamic_table) + + expected = {"Numbers": [1], "Words": ["Testing"]} + + self.assertEqual(result, expected) + def test_column_data(self): from deephaven.ui.hooks import use_column_data from deephaven import new_table From 273f184fe62dc962b4ad41df2862cd85623ea900 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Thu, 18 Apr 2024 16:51:54 -0400 Subject: [PATCH 09/10] chore(version): ui-v0.13.0 --- package-lock.json | 2 +- plugins/ui/CHANGELOG.md | 10 ++++++++++ plugins/ui/setup.cfg | 2 +- plugins/ui/src/js/package.json | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a23e15663..a989475bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35436,7 +35436,7 @@ }, "plugins/ui/src/js": { "name": "@deephaven/js-plugin-ui", - "version": "0.12.0", + "version": "0.13.0", "license": "Apache-2.0", "dependencies": { "@adobe/react-spectrum": "^3.34.1", diff --git a/plugins/ui/CHANGELOG.md b/plugins/ui/CHANGELOG.md index 7e49e4211..8bb7cd8c3 100644 --- a/plugins/ui/CHANGELOG.md +++ b/plugins/ui/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## ui-v0.13.0 - 2024-04-18 +#### Bug Fixes +- Fix conditional use_effect in use_table_listener (#422) - (5f4f238) - Joe +- add on_change to toggle_button (#426) - (6ead733) - Joe +- buttons not working due to extra prop (#423) - (b10f67c) - Don +#### Features +- python date picker implementation (#409) - (5ed66a7) - Joe + +- - - + ## ui-v0.12.0 - 2024-04-17 #### Documentation - Combo box spec (#392) - (da1076a) - Joe diff --git a/plugins/ui/setup.cfg b/plugins/ui/setup.cfg index 08c3da53e..d6f67628b 100644 --- a/plugins/ui/setup.cfg +++ b/plugins/ui/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-ui description = deephaven.ui plugin long_description = file: README.md long_description_content_type = text/markdown -version = 0.12.0.dev0 +version = 0.13.0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json index a436c7693..5045bf969 100644 --- a/plugins/ui/src/js/package.json +++ b/plugins/ui/src/js/package.json @@ -1,6 +1,6 @@ { "name": "@deephaven/js-plugin-ui", - "version": "0.12.0", + "version": "0.13.0", "description": "Deephaven UI plugin", "keywords": [ "Deephaven", From 802361fd39de32adb29db3c9455e1a2d7b125271 Mon Sep 17 00:00:00 2001 From: Mike Bender Date: Thu, 18 Apr 2024 16:51:54 -0400 Subject: [PATCH 10/10] chore(version): update ui version to 0.13.0.dev0 --- plugins/ui/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ui/setup.cfg b/plugins/ui/setup.cfg index d6f67628b..a2553ad12 100644 --- a/plugins/ui/setup.cfg +++ b/plugins/ui/setup.cfg @@ -3,7 +3,7 @@ name = deephaven-plugin-ui description = deephaven.ui plugin long_description = file: README.md long_description_content_type = text/markdown -version = 0.13.0 +version = 0.13.0.dev0 url = https://github.com/deephaven/deephaven-plugins project_urls = Source Code = https://github.com/deephaven/deephaven-plugins