From c5f9dd7dd6341c29ceb366c366661179c32badbd Mon Sep 17 00:00:00 2001 From: Timmy Huang Date: Tue, 18 Jun 2024 13:18:21 -0700 Subject: [PATCH] feat: side by side embeddings --- client/__tests__/e2e/cellxgeneActions.ts | 36 +- client/__tests__/e2e/e2e.test.ts | 24 +- client/__tests__/reducers/cascade.test.ts | 3 + client/configuration/babel/babel.dev.js | 1 + client/configuration/babel/babel.prod.js | 1 + client/package-lock.json | 273 ++++--- client/package.json | 2 + client/src/actions/embedding.ts | 23 +- client/src/actions/index.ts | 8 +- client/src/annoMatrix/normalize.ts | 14 +- client/src/common/selectors.ts | 18 +- client/src/common/types/schema.ts | 2 + .../src/components/PanelEmbedding/index.tsx | 123 +++ client/src/components/PanelEmbedding/util.ts | 5 + client/src/components/app.tsx | 49 +- .../components/brushableHistogram/index.tsx | 65 +- .../components/categorical/category/index.tsx | 172 ++--- .../components/categorical/categoryContext.ts | 6 +- client/src/components/categorical/index.tsx | 13 +- .../components/categorical/value/index.tsx | 22 +- .../src/components/continuous/continuous.tsx | 9 +- .../src/components/continuousLegend/index.tsx | 3 +- .../datasetSelector/datasetSelector.tsx | 6 +- client/src/components/embedding/index.tsx | 193 +++-- client/src/components/framework/layout.tsx | 3 +- client/src/components/geneExpression/gene.tsx | 30 +- .../geneExpression/geneInfo/geneInfo.tsx | 258 ------- .../src/components/geneExpression/index.tsx | 10 +- .../infoPanel/datasetInfo/connect.ts | 38 + .../datasetInfo/datasetInfoFormat.tsx} | 19 +- .../infoPanel/datasetInfo/index.tsx | 22 + .../infoPanel/datasetInfo/types.ts | 13 + .../infoPanel/geneInfo/connect.ts | 11 + .../infoPanel/geneInfo/index.tsx | 116 +++ .../{ => infoPanel}/geneInfo/style.ts | 68 +- .../infoPanel/geneInfo/types.ts | 21 + .../geneExpression/infoPanel/index.tsx | 94 +++ .../geneExpression/infoPanel/style.ts | 71 ++ .../geneExpression/infoPanel/types.ts | 15 + client/src/components/graph/graph.tsx | 209 ++---- client/src/components/graph/setupLasso.ts | 18 +- .../src/components/graph/setupSVGandBrush.ts | 9 +- client/src/components/graph/types.ts | 32 +- client/src/components/graph/util.ts | 7 + client/src/components/hotkeys/index.tsx | 1 + .../src/components/infoDrawer/infoDrawer.tsx | 98 --- client/src/components/leftSidebar/index.tsx | 63 +- .../leftSidebar/leftSidebarSkeleton.tsx | 6 +- client/src/components/leftSidebar/style.ts | 15 + client/src/components/menubar/clip.tsx | 4 +- .../src/components/menubar/diffexpButtons.tsx | 4 +- client/src/components/menubar/index.tsx | 701 +++++++++--------- client/src/components/menubar/menubar.css | 4 - client/src/components/menubar/style.ts | 38 + client/src/components/menubar/subset.tsx | 4 +- client/src/components/rightSidebar/index.tsx | 40 +- .../rightSidebar/rightSidebarSkeleton.tsx | 8 +- client/src/components/rightSidebar/style.ts | 11 + .../components/scatterplot/scatterplot.tsx | 27 +- client/src/globals.ts | 1 + client/src/reducers/annoMatrix.ts | 7 +- client/src/reducers/annotations.ts | 136 ---- client/src/reducers/cascade.ts | 27 +- client/src/reducers/centroidLabels.ts | 2 +- client/src/reducers/config.ts | 2 +- client/src/reducers/controls.ts | 244 +----- client/src/reducers/genesets.ts | 6 +- client/src/reducers/genesetsUI.ts | 2 +- client/src/reducers/index.ts | 33 +- client/src/reducers/layoutChoice.ts | 26 +- client/src/reducers/obsCrossfilter.ts | 4 +- client/src/reducers/panelEmbedding.ts | 75 ++ client/src/reducers/pointDilation.ts | 4 +- client/src/reducers/quickGenes.ts | 4 +- client/src/selectors/annoMatrix.ts | 13 +- client/src/selectors/categoricalSelection.ts | 10 +- client/src/selectors/colors.ts | 7 +- client/src/selectors/config.ts | 10 +- client/src/selectors/continuousSelection.ts | 11 +- client/src/selectors/controls.ts | 15 +- client/src/selectors/genesets.ts | 10 +- client/src/selectors/layoutChoice.ts | 9 +- client/src/util/catLabelSort.ts | 10 +- client/src/util/centroid.ts | 9 +- client/src/util/layout.ts | 21 + .../util/stateManager/annotationsHelpers.ts | 12 - .../src/util/stateManager/controlsHelpers.ts | 3 - 87 files changed, 1964 insertions(+), 1908 deletions(-) create mode 100644 client/src/components/PanelEmbedding/index.tsx create mode 100644 client/src/components/PanelEmbedding/util.ts delete mode 100644 client/src/components/geneExpression/geneInfo/geneInfo.tsx create mode 100644 client/src/components/geneExpression/infoPanel/datasetInfo/connect.ts rename client/src/components/{infoDrawer/infoFormat.tsx => geneExpression/infoPanel/datasetInfo/datasetInfoFormat.tsx} (95%) create mode 100644 client/src/components/geneExpression/infoPanel/datasetInfo/index.tsx create mode 100644 client/src/components/geneExpression/infoPanel/datasetInfo/types.ts create mode 100644 client/src/components/geneExpression/infoPanel/geneInfo/connect.ts create mode 100644 client/src/components/geneExpression/infoPanel/geneInfo/index.tsx rename client/src/components/geneExpression/{ => infoPanel}/geneInfo/style.ts (54%) create mode 100644 client/src/components/geneExpression/infoPanel/geneInfo/types.ts create mode 100644 client/src/components/geneExpression/infoPanel/index.tsx create mode 100644 client/src/components/geneExpression/infoPanel/style.ts create mode 100644 client/src/components/geneExpression/infoPanel/types.ts delete mode 100644 client/src/components/infoDrawer/infoDrawer.tsx create mode 100644 client/src/components/leftSidebar/style.ts delete mode 100644 client/src/components/menubar/menubar.css create mode 100644 client/src/components/menubar/style.ts create mode 100644 client/src/components/rightSidebar/style.ts delete mode 100644 client/src/reducers/annotations.ts create mode 100644 client/src/reducers/panelEmbedding.ts create mode 100644 client/src/util/layout.ts diff --git a/client/__tests__/e2e/cellxgeneActions.ts b/client/__tests__/e2e/cellxgeneActions.ts index 89db19a50..1946fbe6e 100644 --- a/client/__tests__/e2e/cellxgeneActions.ts +++ b/client/__tests__/e2e/cellxgeneActions.ts @@ -605,17 +605,14 @@ export async function requestGeneInfo(gene: string, page: Page): Promise { await expect(page.getByTestId(`${gene}:gene-info`)).toBeTruthy(); } -export async function assertGeneInfoCardExists( +export async function assertInfoPanelExists( gene: string, page: Page ): Promise { await expect(page.getByTestId(`${gene}:gene-info`)).toBeTruthy(); - await expect(page.getByTestId(`gene-info-header`)).toBeTruthy(); - await expect(page.getByTestId(`min-gene-info`)).toBeTruthy(); + await expect(page.getByTestId(`info-panel-header`)).toBeTruthy(); + await expect(page.getByTestId(`min-info-panel`)).toBeTruthy(); - await expect(page.getByTestId(`clear-info-summary`).innerText).not.toEqual( - "" - ); await expect(page.getByTestId(`gene-info-synonyms`).innerText).not.toEqual( "" ); @@ -623,19 +620,19 @@ export async function assertGeneInfoCardExists( await expect(page.getByTestId(`gene-info-link`)).toBeTruthy(); } -export async function minimizeGeneInfo(page: Page): Promise { - await page.getByTestId("min-gene-info").click(); +export async function minimizeInfoPanel(page: Page): Promise { + await page.getByTestId("min-info-panel").click(); } -export async function assertGeneInfoCardIsMinimized( +export async function assertInfoPanelIsMinimized( gene: string, page: Page ): Promise { const testIds = [ `${gene}:gene-info`, - "gene-info-header", - "min-gene-info", - "clear-gene-info", + "info-panel-header", + "max-info-panel", + "close-info-panel", ]; await tryUntil( @@ -644,27 +641,24 @@ export async function assertGeneInfoCardIsMinimized( const result = await page.getByTestId(id).isVisible(); await expect(result).toBe(true); } - - const result = await page.getByTestId("gene-info-symbol").isVisible(); - await expect(result).toBe(false); }, { page } ); } -export async function removeGeneInfo(page: Page): Promise { - await page.getByTestId("clear-gene-info").click(); +export async function closeInfoPanel(page: Page): Promise { + await page.getByTestId("close-info-panel").click(); } -export async function assertGeneInfoDoesNotExist( +export async function assertInfoPanelClosed( gene: string, page: Page ): Promise { const testIds = [ `${gene}:gene-info`, - "gene-info-header", - "min-gene-info", - "clear-gene-info", + "info-panel-header", + "min-info-panel", + "close-info-panel", ]; await tryUntil( async () => { diff --git a/client/__tests__/e2e/e2e.test.ts b/client/__tests__/e2e/e2e.test.ts index 308c1e1a8..5656b86c4 100644 --- a/client/__tests__/e2e/e2e.test.ts +++ b/client/__tests__/e2e/e2e.test.ts @@ -40,12 +40,12 @@ import { selectCategory, addGeneToSetAndExpand, requestGeneInfo, - assertGeneInfoCardExists, - assertGeneInfoCardIsMinimized, - minimizeGeneInfo, - removeGeneInfo, + assertInfoPanelExists, + assertInfoPanelIsMinimized, + minimizeInfoPanel, + closeInfoPanel, addGeneToSearch, - assertGeneInfoDoesNotExist, + assertInfoPanelClosed, waitUntilNoSkeletonDetected, checkGenesetDescription, assertUndoRedo, @@ -1215,9 +1215,7 @@ for (const testDataset of testDatasets) { ); }); - test("open gene info card and hide/remove", async ({ - page, - }, testInfo) => { + test("open info panel and hide/remove", async ({ page }, testInfo) => { await setup({ option, page, url, testInfo }); await addGeneToSearch(geneToRequestInfo, page); @@ -1226,7 +1224,7 @@ for (const testDataset of testDatasets) { await tryUntil( async () => { await requestGeneInfo(geneToRequestInfo, page); - await assertGeneInfoCardExists(geneToRequestInfo, page); + await assertInfoPanelExists(geneToRequestInfo, page); }, { page } ); @@ -1235,8 +1233,8 @@ for (const testDataset of testDatasets) { await tryUntil( async () => { - await minimizeGeneInfo(page); - await assertGeneInfoCardIsMinimized(geneToRequestInfo, page); + await minimizeInfoPanel(page); + await assertInfoPanelIsMinimized(geneToRequestInfo, page); }, { page } ); @@ -1245,8 +1243,8 @@ for (const testDataset of testDatasets) { await tryUntil( async () => { - await removeGeneInfo(page); - await assertGeneInfoDoesNotExist(geneToRequestInfo, page); + await closeInfoPanel(page); + await assertInfoPanelClosed(geneToRequestInfo, page); }, { page } ); diff --git a/client/__tests__/reducers/cascade.test.ts b/client/__tests__/reducers/cascade.test.ts index f8ec1273a..cc7928ed8 100644 --- a/client/__tests__/reducers/cascade.test.ts +++ b/client/__tests__/reducers/cascade.test.ts @@ -1,3 +1,6 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- okay to disable for tests +// @ts-nocheck -- Typing getting in way of unit test + import { expect, test } from "@playwright/test"; import cascadeReducers from "../../src/reducers/cascade"; diff --git a/client/configuration/babel/babel.dev.js b/client/configuration/babel/babel.dev.js index 2a095ba3a..67e8c81fe 100644 --- a/client/configuration/babel/babel.dev.js +++ b/client/configuration/babel/babel.dev.js @@ -22,5 +22,6 @@ module.exports = { "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator", + "@emotion/babel-plugin", ], }; diff --git a/client/configuration/babel/babel.prod.js b/client/configuration/babel/babel.prod.js index 8a5d47a75..617bb63e5 100644 --- a/client/configuration/babel/babel.prod.js +++ b/client/configuration/babel/babel.prod.js @@ -23,5 +23,6 @@ module.exports = { "@babel/plugin-transform-runtime", "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator", + "@emotion/babel-plugin", ], }; diff --git a/client/package-lock.json b/client/package-lock.json index 76bbe58b8..796c17330 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -75,6 +75,7 @@ "@babel/runtime": "^7.13.16", "@blueprintjs/eslint-plugin": "^0.3.0", "@chromatic-com/playwright": "^0.5.3", + "@emotion/babel-plugin": "^11.11.0", "@playwright/test": "^1.40.1", "@sentry/webpack-plugin": "^1.15.0", "@storybook/addon-essentials": "^7.6.12", @@ -154,6 +155,7 @@ "lodash.zip": "^4.2.0", "mini-css-extract-plugin": "^1.5.0", "prettier": "^2.0.5", + "resize-observer-polyfill": "^1.5.1", "rimraf": "^3.0.2", "serve-favicon": "^2.5.0", "storybook": "^7.6.12", @@ -179,6 +181,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "devOptional": true, "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -834,6 +837,7 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "devOptional": true, "engines": { "node": ">=6.9.0" } @@ -842,6 +846,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "devOptional": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -870,12 +875,14 @@ "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true }, "node_modules/@babel/generator": { "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "devOptional": true, "dependencies": { "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", @@ -890,6 +897,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "devOptional": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -927,6 +935,7 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "devOptional": true, "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-validator-option": "^7.23.5", @@ -942,6 +951,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, "dependencies": { "yallist": "^3.0.2" } @@ -949,7 +959,8 @@ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.23.10", @@ -1012,6 +1023,7 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "devOptional": true, "engines": { "node": ">=6.9.0" } @@ -1020,6 +1032,7 @@ "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "devOptional": true, "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" @@ -1032,6 +1045,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "devOptional": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -1066,6 +1080,7 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "devOptional": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -1096,6 +1111,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -1138,6 +1154,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "devOptional": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -1161,6 +1178,7 @@ "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "devOptional": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -1188,6 +1206,7 @@ "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "devOptional": true, "engines": { "node": ">=6.9.0" } @@ -1210,6 +1229,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "devOptional": true, "dependencies": { "@babel/template": "^7.23.9", "@babel/traverse": "^7.23.9", @@ -1300,6 +1320,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "devOptional": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -1661,6 +1682,7 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -2994,6 +3016,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "devOptional": true, "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/parser": "^7.23.9", @@ -3007,6 +3030,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "devOptional": true, "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/generator": "^7.23.6", @@ -4630,27 +4654,28 @@ } }, "node_modules/@emotion/babel-plugin": { - "version": "11.10.2", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz", - "integrity": "sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "dependencies": { "@babel/helper-module-imports": "^7.16.7", - "@babel/plugin-syntax-jsx": "^7.17.12", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.0.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "stylis": "4.2.0" } }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/@emotion/cache": { "version": "11.10.3", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.3.tgz", @@ -4684,9 +4709,9 @@ } }, "node_modules/@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.0", @@ -4697,9 +4722,9 @@ } }, "node_modules/@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { "version": "11.10.4", @@ -4729,14 +4754,14 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", - "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", "dependencies": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, @@ -4772,9 +4797,9 @@ } }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -4785,9 +4810,9 @@ } }, "node_modules/@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "node_modules/@emotion/weak-memoize": { "version": "0.3.0", @@ -5495,6 +5520,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "devOptional": true, "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -5507,6 +5533,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -5515,6 +5542,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -5546,12 +5574,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -12918,6 +12948,7 @@ "version": "4.22.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -13084,6 +13115,7 @@ "version": "1.0.30001581", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -14523,6 +14555,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, "dependencies": { "ms": "2.1.2" }, @@ -15112,7 +15145,8 @@ "node_modules/electron-to-chromium": { "version": "1.4.652", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.652.tgz", - "integrity": "sha512-XvQaa8hVUAuEJtLw6VKQqvdOxTOfBLWfI10t2xWpezx4XXD3k8bdLweEKeItqaa0+OkJX5l0mP1W+JWobyIDrg==" + "integrity": "sha512-XvQaa8hVUAuEJtLw6VKQqvdOxTOfBLWfI10t2xWpezx4XXD3k8bdLweEKeItqaa0+OkJX5l0mP1W+JWobyIDrg==", + "devOptional": true }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -15375,6 +15409,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true, "engines": { "node": ">=6" } @@ -17279,6 +17314,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "devOptional": true, "engines": { "node": ">=6.9.0" } @@ -17470,6 +17506,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "devOptional": true, "engines": { "node": ">=4" } @@ -18976,6 +19013,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "devOptional": true, "bin": { "jsesc": "bin/jsesc" }, @@ -19010,6 +19048,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "devOptional": true, "bin": { "json5": "lib/cli.js" }, @@ -19932,7 +19971,8 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true }, "node_modules/nanoid": { "version": "3.3.7", @@ -20121,7 +20161,8 @@ "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "devOptional": true }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -20918,7 +20959,8 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "devOptional": true }, "node_modules/picomatch": { "version": "2.3.1", @@ -22890,6 +22932,12 @@ "node": ">=0.10.5" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -23067,6 +23115,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, "bin": { "semver": "bin/semver.js" } @@ -24819,6 +24868,7 @@ "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -25601,6 +25651,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "devOptional": true, "requires": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -26174,12 +26225,14 @@ "@babel/compat-data": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==" + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "devOptional": true }, "@babel/core": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "devOptional": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -26201,7 +26254,8 @@ "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true } } }, @@ -26209,6 +26263,7 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "devOptional": true, "requires": { "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", @@ -26220,6 +26275,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "devOptional": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -26250,6 +26306,7 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "devOptional": true, "requires": { "@babel/compat-data": "^7.23.5", "@babel/helper-validator-option": "^7.23.5", @@ -26262,6 +26319,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, "requires": { "yallist": "^3.0.2" } @@ -26269,7 +26327,8 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true } } }, @@ -26318,12 +26377,14 @@ "@babel/helper-environment-visitor": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "devOptional": true }, "@babel/helper-function-name": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "devOptional": true, "requires": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" @@ -26333,6 +26394,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "devOptional": true, "requires": { "@babel/types": "^7.22.5" } @@ -26358,6 +26420,7 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "devOptional": true, "requires": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -26378,7 +26441,8 @@ "@babel/helper-plugin-utils": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true }, "@babel/helper-remap-async-to-generator": { "version": "7.22.20", @@ -26406,6 +26470,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "devOptional": true, "requires": { "@babel/types": "^7.22.5" } @@ -26423,6 +26488,7 @@ "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "devOptional": true, "requires": { "@babel/types": "^7.22.5" } @@ -26440,7 +26506,8 @@ "@babel/helper-validator-option": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==" + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "devOptional": true }, "@babel/helper-wrap-function": { "version": "7.22.20", @@ -26457,6 +26524,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "devOptional": true, "requires": { "@babel/template": "^7.23.9", "@babel/traverse": "^7.23.9", @@ -26527,7 +26595,8 @@ "@babel/parser": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==" + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "devOptional": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.23.3", @@ -26757,6 +26826,7 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } @@ -27652,6 +27722,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "devOptional": true, "requires": { "@babel/code-frame": "^7.23.5", "@babel/parser": "^7.23.9", @@ -27662,6 +27733,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "devOptional": true, "requires": { "@babel/code-frame": "^7.23.5", "@babel/generator": "^7.23.6", @@ -28871,22 +28943,28 @@ "dev": true }, "@emotion/babel-plugin": { - "version": "11.10.2", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz", - "integrity": "sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "requires": { "@babel/helper-module-imports": "^7.16.7", - "@babel/plugin-syntax-jsx": "^7.17.12", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.0.13" + "stylis": "4.2.0" + }, + "dependencies": { + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + } } }, "@emotion/cache": { @@ -28914,9 +28992,9 @@ } }, "@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "@emotion/is-prop-valid": { "version": "1.2.0", @@ -28927,9 +29005,9 @@ } }, "@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "@emotion/react": { "version": "11.10.4", @@ -28947,14 +29025,14 @@ } }, "@emotion/serialize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", - "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", "requires": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, @@ -28977,9 +29055,9 @@ } }, "@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -28988,9 +29066,9 @@ "requires": {} }, "@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "@emotion/weak-memoize": { "version": "0.3.0", @@ -29416,6 +29494,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "devOptional": true, "requires": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -29424,12 +29503,14 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "devOptional": true }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "devOptional": true }, "@jridgewell/source-map": { "version": "0.3.5", @@ -29457,12 +29538,14 @@ "@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "devOptional": true }, "@jridgewell/trace-mapping": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "devOptional": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -34872,6 +34955,7 @@ "version": "4.22.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "devOptional": true, "requires": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -34984,7 +35068,8 @@ "caniuse-lite": { "version": "1.0.30001581", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", - "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==" + "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", + "devOptional": true }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -36074,6 +36159,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, "requires": { "ms": "2.1.2" } @@ -36517,7 +36603,8 @@ "electron-to-chromium": { "version": "1.4.652", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.652.tgz", - "integrity": "sha512-XvQaa8hVUAuEJtLw6VKQqvdOxTOfBLWfI10t2xWpezx4XXD3k8bdLweEKeItqaa0+OkJX5l0mP1W+JWobyIDrg==" + "integrity": "sha512-XvQaa8hVUAuEJtLw6VKQqvdOxTOfBLWfI10t2xWpezx4XXD3k8bdLweEKeItqaa0+OkJX5l0mP1W+JWobyIDrg==", + "devOptional": true }, "emoji-regex": { "version": "9.2.2", @@ -36735,7 +36822,8 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "devOptional": true }, "escape-html": { "version": "1.0.3", @@ -38176,7 +38264,8 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "devOptional": true }, "get-func-name": { "version": "2.0.2", @@ -38319,7 +38408,8 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "devOptional": true }, "globby": { "version": "11.1.0", @@ -39373,7 +39463,8 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "devOptional": true }, "json-loader": { "version": "0.5.7", @@ -39401,7 +39492,8 @@ "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "devOptional": true }, "jsonfile": { "version": "4.0.0", @@ -40140,7 +40232,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true }, "nanoid": { "version": "3.3.7", @@ -40292,7 +40385,8 @@ "node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "devOptional": true }, "normalize-package-data": { "version": "2.5.0", @@ -40866,7 +40960,8 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "devOptional": true }, "picomatch": { "version": "2.3.1", @@ -42278,6 +42373,12 @@ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -42408,7 +42509,8 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true }, "semver-compare": { "version": "1.0.0", @@ -43768,6 +43870,7 @@ "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "devOptional": true, "requires": { "escalade": "^3.1.1", "picocolors": "^1.0.0" diff --git a/client/package.json b/client/package.json index e270f77b2..cf689e396 100644 --- a/client/package.json +++ b/client/package.json @@ -119,6 +119,7 @@ "@babel/runtime": "^7.13.16", "@blueprintjs/eslint-plugin": "^0.3.0", "@chromatic-com/playwright": "^0.5.3", + "@emotion/babel-plugin": "^11.11.0", "@playwright/test": "^1.40.1", "@sentry/webpack-plugin": "^1.15.0", "@storybook/addon-essentials": "^7.6.12", @@ -198,6 +199,7 @@ "lodash.zip": "^4.2.0", "mini-css-extract-plugin": "^1.5.0", "prettier": "^2.0.5", + "resize-observer-polyfill": "^1.5.1", "rimraf": "^3.0.2", "serve-favicon": "^2.5.0", "storybook": "^7.6.12", diff --git a/client/src/actions/embedding.ts b/client/src/actions/embedding.ts index 41c7b3e62..3e8b21056 100644 --- a/client/src/actions/embedding.ts +++ b/client/src/actions/embedding.ts @@ -55,8 +55,15 @@ async function _switchEmbedding( export const layoutChoiceAction: ActionCreator< ThunkAction, RootState, never, Action<"set layout choice">> > = - (newLayoutChoice: string) => + (newLayoutChoice: string, isSidePanel = false) => async (dispatch: AppDispatch, getState: GetState): Promise => { + if (isSidePanel) { + dispatch({ + type: "set panel embedding layout choice", + layoutChoice: newLayoutChoice, + }); + return; + } /** * Bruce: On layout choice, make sure we have selected all on the previous layout, AND the new * layout. @@ -91,3 +98,17 @@ export const layoutChoiceAction: ActionCreator< annoMatrix, }); }; + +export const swapLayoutChoicesAction: ActionCreator< + ThunkAction, RootState, never, Action<"set layout choice">> +> = + () => + async (dispatch: AppDispatch, getState: GetState): Promise => { + const { layoutChoice, panelEmbedding } = getState(); + // get main and side layout choices + const mainLayoutChoice = layoutChoice.current; + const sideLayoutChoice = panelEmbedding.layoutChoice.current; + + await dispatch(layoutChoiceAction(mainLayoutChoice, true)); + await dispatch(layoutChoiceAction(sideLayoutChoice, false)); + }; diff --git a/client/src/actions/index.ts b/client/src/actions/index.ts index e4b9daf73..b404d7e02 100644 --- a/client/src/actions/index.ts +++ b/client/src/actions/index.ts @@ -32,7 +32,7 @@ import AnnoMatrix from "../annoMatrix/annoMatrix"; import { DATASET_METADATA_RESPONSE } from "../../__tests__/__mocks__/apiMock"; import { selectAvailableLayouts } from "../selectors/layoutChoice"; -import { getBestDefaultLayout } from "../reducers/layoutChoice"; +import { getBestDefaultLayout } from "../util/layout"; function setGlobalConfig(config: Config) { /** @@ -306,6 +306,7 @@ const requestDifferentialExpression = 2. get expression data for each */ const { annoMatrix } = getState(); + const varIndexName = annoMatrix.schema.annotations.var.index; // // Legal values are null, Array or TypedArray. Null is initial state. @@ -348,7 +349,8 @@ const requestDifferentialExpression = } const response = await res.json(); - const varIndex = await annoMatrix.fetch("var", varIndexName); + + const varIndex = await annoMatrix.fetch(Field.var, varIndexName); const diffexpLists = { negative: [], positive: [] }; for (const polarity of Object.keys( diffexpLists @@ -356,6 +358,7 @@ const requestDifferentialExpression = diffexpLists[polarity] = response[polarity].map( // TODO: swap out with type defined at genesets reducer when made (v: [LabelIndex, number, number, number]) => [ + // @ts-expect-error (seve): fix downstream lint errors as a result of detailed app store typing varIndex.at(v[0], varIndexName), ...v.slice(1), ] @@ -524,6 +527,7 @@ export default { subsetAction: viewActions.subsetAction, resetSubsetAction: viewActions.resetSubsetAction, layoutChoiceAction: embActions.layoutChoiceAction, + swapLayoutChoicesAction: embActions.swapLayoutChoicesAction, setCellSetFromSelection: selnActions.setCellSetFromSelection, genesetDelete: genesetActions.genesetDelete, genesetAddGenes: genesetActions.genesetAddGenes, diff --git a/client/src/annoMatrix/normalize.ts b/client/src/annoMatrix/normalize.ts index f8d93db9c..cfd290f45 100644 --- a/client/src/annoMatrix/normalize.ts +++ b/client/src/annoMatrix/normalize.ts @@ -108,7 +108,6 @@ export function normalizeWritableCategoricalSchema( // TODO #35: Use type guards instead of casting (colSchema as CategoricalAnnotationColumnSchema).categories = catLabelSort( - true, Array.from(categorySet) ); return colSchema as CategoricalAnnotationColumnSchema; @@ -142,10 +141,9 @@ export function normalizeCategorical( // if no overflow, just UI sort schema categories and return if (allCategories.size <= TopN) { - (colSchema as CategoricalAnnotationColumnSchema).categories = catLabelSort( - writable, - [...allCategories.keys()] - ); + (colSchema as CategoricalAnnotationColumnSchema).categories = catLabelSort([ + ...allCategories.keys(), + ]); return df; } @@ -185,10 +183,8 @@ export function normalizeCategorical( revisedCategories.splice(revisedCategories.indexOf(overflowCatName), 1)[0] ); // TODO #35: Use type guards instead of casting - (colSchema as CategoricalAnnotationColumnSchema).categories = catLabelSort( - writable, - revisedCategories - ); + (colSchema as CategoricalAnnotationColumnSchema).categories = + catLabelSort(revisedCategories); return df; } diff --git a/client/src/common/selectors.ts b/client/src/common/selectors.ts index b0b7a2e0b..6d867e6cc 100644 --- a/client/src/common/selectors.ts +++ b/client/src/common/selectors.ts @@ -4,10 +4,13 @@ import { selectIsDeepZoomSourceValid, selectS3URI } from "../selectors/config"; import { getFeatureFlag } from "../util/featureFlags/featureFlags"; import { FEATURES } from "../util/featureFlags/features"; -export function isSpatialMode(props: Partial) { - const { layoutChoice } = props; +export function isSpatialMode(props: ShouldShowOpenseadragonProps): boolean { + const { layoutChoice, panelEmbedding } = props; - return layoutChoice?.current?.includes(spatialEmbeddingKeyword); + return !!( + layoutChoice?.current?.includes(spatialEmbeddingKeyword) || + panelEmbedding?.layoutChoice?.current?.includes(spatialEmbeddingKeyword) + ); } const isSpatial = getFeatureFlag(FEATURES.SPATIAL); @@ -15,7 +18,14 @@ const isSpatial = getFeatureFlag(FEATURES.SPATIAL); /** * (thuang): Selector to determine if the OpenSeadragon viewer should be shown */ -export function shouldShowOpenseadragon(props: Partial) { + +export interface ShouldShowOpenseadragonProps { + config: RootState["config"]; + layoutChoice: RootState["layoutChoice"]; + panelEmbedding?: RootState["panelEmbedding"]; +} + +export function shouldShowOpenseadragon(props: ShouldShowOpenseadragonProps) { return ( isSpatial && selectIsDeepZoomSourceValid(props) && diff --git a/client/src/common/types/schema.ts b/client/src/common/types/schema.ts index 6b50bf61e..55820cc6f 100644 --- a/client/src/common/types/schema.ts +++ b/client/src/common/types/schema.ts @@ -12,6 +12,8 @@ export type AnnotationColumnSchema = | { name: string; type: "string" | "float32" | "int32" | "boolean"; + + // TODO(seve): remove writable from schema (user annotations) writable: boolean; categories?: Category[]; }; diff --git a/client/src/components/PanelEmbedding/index.tsx b/client/src/components/PanelEmbedding/index.tsx new file mode 100644 index 000000000..798c90a97 --- /dev/null +++ b/client/src/components/PanelEmbedding/index.tsx @@ -0,0 +1,123 @@ +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { IconNames } from "@blueprintjs/icons"; +import { Button } from "@blueprintjs/core"; + +import * as globals from "../../globals"; +import { height, width } from "./util"; +import Graph from "../graph/graph"; +import Controls from "../controls"; +import Embedding from "../embedding"; +import { AppDispatch, RootState } from "../../reducers"; +import actions from "../../actions"; + +interface StateProps { + isMinimized: boolean; + isOpen: RootState["panelEmbedding"]["open"]; +} + +interface DispatchProps { + dispatch: AppDispatch; +} + +const mapStateToProps = (state: RootState): StateProps => ({ + isMinimized: state.panelEmbedding.minimized, + isOpen: state.panelEmbedding.open, +}); + +const PanelEmbedding = (props: StateProps & DispatchProps) => { + const [viewportRef, setViewportRef] = useState(null); + + const { isMinimized, isOpen, dispatch } = props; + + const handleSwapLayoutChoices = async (): Promise => { + await dispatch(actions.swapLayoutChoicesAction()); + }; + + if (!isOpen) return null; + + return ( +
{ + setViewportRef(ref); + }} + > + +
+
+ +
+
+
+
+
+ {viewportRef && ( + + )} +
+ ); +}; + +export default connect(mapStateToProps)(PanelEmbedding); diff --git a/client/src/components/PanelEmbedding/util.ts b/client/src/components/PanelEmbedding/util.ts new file mode 100644 index 000000000..af56cd201 --- /dev/null +++ b/client/src/components/PanelEmbedding/util.ts @@ -0,0 +1,5 @@ +export const width = 400; +export const height = 400; +export const innerHeight = height - 2; + +export const devicePixelRatio = window.devicePixelRatio || 1; diff --git a/client/src/components/app.tsx b/client/src/components/app.tsx index cbde64b38..6798535c5 100644 --- a/client/src/components/app.tsx +++ b/client/src/components/app.tsx @@ -20,20 +20,36 @@ import { RootState, AppDispatch } from "../reducers"; import GlobalHotkeys from "./hotkeys"; import { selectIsSeamlessEnabled } from "../selectors/datasetMetadata"; import Graph from "./graph/graph"; +import Scatterplot from "./scatterplot/scatterplot"; +import PanelEmbedding from "./PanelEmbedding"; -interface Props { - dispatch: AppDispatch; - loading: boolean; - error: string; +interface StateProps { + loading: RootState["controls"]["loading"]; + error: RootState["controls"]["error"]; graphRenderCounter: number; tosURL: string | undefined; - privacyURL: string | undefined; + privacyURL: string; seamlessEnabled: boolean; - datasetMetadataError: string | null; - isCellGuideCxg: boolean; + datasetMetadataError: RootState["datasetMetadata"]["error"]; + isCellGuideCxg: RootState["controls"]["isCellGuideCxg"]; + scatterplotXXaccessor: RootState["controls"]["scatterplotXXaccessor"]; + scatterplotYYaccessor: RootState["controls"]["scatterplotYYaccessor"]; } -class App extends React.Component { +const mapStateToProps = (state: RootState): StateProps => ({ + loading: state.controls.loading, + error: state.controls.error, + graphRenderCounter: state.controls.graphRenderCounter, + tosURL: state.config?.parameters?.about_legal_tos, + privacyURL: state.config?.parameters?.about_legal_privacy || "", + seamlessEnabled: selectIsSeamlessEnabled(state), + datasetMetadataError: state.datasetMetadata.error, + isCellGuideCxg: state.controls.isCellGuideCxg, + scatterplotXXaccessor: state.controls.scatterplotXXaccessor, + scatterplotYYaccessor: state.controls.scatterplotYYaccessor, +}); + +class App extends React.Component { componentDidMount(): void { const { dispatch } = this.props; dispatch(actions.doInitialDataLoad()); @@ -51,6 +67,8 @@ class App extends React.Component { seamlessEnabled, datasetMetadataError, isCellGuideCxg, + scatterplotXXaccessor, + scatterplotYYaccessor, } = this.props; return ( @@ -90,6 +108,10 @@ class App extends React.Component { viewportRef={viewportRef} key={graphRenderCounter} /> + {scatterplotXXaccessor && scatterplotYYaccessor && ( + + )} + @@ -108,13 +130,4 @@ class App extends React.Component { } } -export default connect((state: RootState) => ({ - loading: state.controls.loading, - error: state.controls.error, - graphRenderCounter: state.controls.graphRenderCounter, - tosURL: state.config?.parameters?.about_legal_tos, - privacyURL: state.config?.parameters?.about_legal_privacy, - seamlessEnabled: selectIsSeamlessEnabled(state), - datasetMetadataError: state.datasetMetadata.error, - isCellGuideCxg: state.controls.isCellGuideCxg, -}))(App); +export default connect(mapStateToProps)(App); diff --git a/client/src/components/brushableHistogram/index.tsx b/client/src/components/brushableHistogram/index.tsx index 5a88bbf78..64e8ec742 100644 --- a/client/src/components/brushableHistogram/index.tsx +++ b/client/src/components/brushableHistogram/index.tsx @@ -14,7 +14,10 @@ import ErrorLoading from "./error"; import { Dataframe } from "../../util/dataframe"; import { track } from "../../analytics"; import { EVENTS } from "../../analytics/events"; -import { RootState } from "../../reducers"; +import { AppDispatch, RootState } from "../../reducers"; +import { AnnoMatrixClipView } from "../../annoMatrix/views"; +import { Query } from "../../annoMatrix/query"; +import { Field } from "../../common/types/schema"; const MARGIN = { LEFT: 10, // Space for 0 tick label on X axis @@ -38,12 +41,33 @@ interface BrushableHistogramOwnProps { isUserDefined?: boolean; isGeneSetSummary?: boolean; field: string; + onGeneExpressionComplete: () => void; + zebra?: boolean; + mini?: boolean; + width?: number; + setGenes?: Map; } -type BrushableHistogramProps = Partial & BrushableHistogramOwnProps; +interface StateProps { + annoMatrix: RootState["annoMatrix"]; + isScatterplotXXaccessor: boolean; + isScatterplotYYaccessor: boolean; + continuousSelectionRange: RootState["continuousSelection"][string]; + isColorAccessor: boolean; + singleContinuousValues: RootState["singleContinuousValue"]["singleContinuousValues"]; +} +interface DispatchProps { + dispatch: AppDispatch; +} + +type BrushableHistogramProps = BrushableHistogramOwnProps & + StateProps & + DispatchProps; -// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message -@connect((state: RootState, ownProps: BrushableHistogramOwnProps) => { +const mapStateToProps = ( + state: RootState, + ownProps: BrushableHistogramOwnProps +): StateProps => { const { isObs, isUserDefined, isGeneSetSummary, field } = ownProps; const myName = makeContinuousDimensionName( { isObs, isUserDefined, isGeneSetSummary }, @@ -59,7 +83,7 @@ type BrushableHistogramProps = Partial & BrushableHistogramOwnProps; state.colors.colorMode !== "color by categorical metadata", singleContinuousValues: state.singleContinuousValue.singleContinuousValues, }; -}) +}; class HistogramBrush extends React.PureComponent { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. static watchAsync(props: any, prevProps: any) { @@ -83,7 +107,7 @@ class HistogramBrush extends React.PureComponent { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. onBrush = (selection: any, x: any, eventType: any) => { const type = `continuous metadata histogram ${eventType}`; - return () => { + return async () => { const { dispatch, field, isObs, isUserDefined, isGeneSetSummary } = this.props; @@ -108,7 +132,7 @@ class HistogramBrush extends React.PureComponent { isGeneSetSummary, }, }; - dispatch( + await dispatch( actions.selectContinuousMetadataAction(type, query, range, otherProps) ); }; @@ -121,7 +145,7 @@ class HistogramBrush extends React.PureComponent { x: any // eslint-disable-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. ) => // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - () => { + async () => { const { dispatch, field, isObs, isUserDefined, isGeneSetSummary } = this.props; const minAllowedBrushSize = 10; @@ -177,7 +201,7 @@ class HistogramBrush extends React.PureComponent { isGeneSetSummary, }, }; - dispatch( + await dispatch( actions.selectContinuousMetadataAction(type, query, range, otherProps) ); track(EVENTS.EXPLORER_SELECT_HISTOGRAM); @@ -244,7 +268,8 @@ class HistogramBrush extends React.PureComponent { dispatch, singleContinuousValues, } = this.props; - const { isClipped } = annoMatrix; + + const { isClipped } = annoMatrix as AnnoMatrixClipView; if (singleContinuousValues.has(field)) { return { histogram: undefined, @@ -268,6 +293,7 @@ class HistogramBrush extends React.PureComponent { OK2Render: false, }; } + const df: Dataframe = await annoMatrix.fetch(...query, globals.numBinsObsX); const column = df.icol(0); @@ -310,10 +336,11 @@ class HistogramBrush extends React.PureComponent { } const unclippedRangeColor = [ - !annoMatrix.isClipped || annoMatrix.clipRange[0] === 0 + !isClipped || (annoMatrix as AnnoMatrixClipView).clipRange[0] === 0 ? "#bbb" : globals.blue, - !annoMatrix.isClipped || annoMatrix.clipRange[1] === 1 + + !isClipped || (annoMatrix as AnnoMatrixClipView).clipRange[1] === 1 ? "#bbb" : globals.blue, ]; @@ -402,19 +429,18 @@ class HistogramBrush extends React.PureComponent { return histogramCache; } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - createQuery() { + createQuery(): [Field, Query] | null { const { isObs, isGeneSetSummary, field, setGenes, annoMatrix } = this.props; const { schema } = annoMatrix; if (isObs) { - return ["obs", field]; + return [Field.obs, field]; } const varIndex = schema?.annotations?.var?.index; if (!varIndex) return null; - if (isGeneSetSummary) { + if (isGeneSetSummary && setGenes) { return [ - "X", + Field.X, { summarize: { method: "mean", @@ -428,7 +454,7 @@ class HistogramBrush extends React.PureComponent { // else, we assume it is a gene expression return [ - "X", + Field.X, { where: { field: "var", @@ -439,7 +465,6 @@ class HistogramBrush extends React.PureComponent { ]; } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. render() { const { dispatch, @@ -561,4 +586,4 @@ class HistogramBrush extends React.PureComponent { } } -export default HistogramBrush; +export default connect(mapStateToProps)(HistogramBrush); diff --git a/client/src/components/categorical/category/index.tsx b/client/src/components/categorical/category/index.tsx index 9915ee418..5c1db08b8 100644 --- a/client/src/components/categorical/category/index.tsx +++ b/client/src/components/categorical/category/index.tsx @@ -3,8 +3,7 @@ import React, { useRef, useEffect } from "react"; import { connect, shallowEqual } from "react-redux"; import { FaChevronRight, FaChevronDown } from "react-icons/fa"; import { AnchorButton, Classes, Position, Tooltip } from "@blueprintjs/core"; -import { Flipper, Flipped } from "react-flip-toolkit"; -import Async from "react-async"; +import Async, { AsyncProps } from "react-async"; import memoize from "memoize-one"; import Value from "../value"; @@ -16,60 +15,63 @@ import { createCategorySummaryFromDfCol } from "../../../util/stateManager/contr import { createColorTable, createColorQuery, + ColorTable, } from "../../../util/stateManager/colorHelpers"; import actions from "../../../actions"; import { Dataframe } from "../../../util/dataframe"; import { track } from "../../../analytics"; import { EVENTS } from "../../../analytics/events"; -import { Schema } from "../../../common/types/schema"; +import { RootState } from "../../../reducers"; const LABEL_WIDTH = globals.leftSidebarWidth - 100; -const ANNO_BUTTON_WIDTH = 50; -const LABEL_WIDTH_ANNO = LABEL_WIDTH - ANNO_BUTTON_WIDTH; -interface PureCategoryProps { - metadataField: string; - colorMode: string; - categorySummary: any; - colorAccessor: string; +type CategoryAsyncProps = { + categoryData: Dataframe; + categorySummary: ReturnType; colorData: Dataframe | null; - categoryData: any; + colorTable: ColorTable; isColorAccessor: boolean; - colorTable: any; - handleCategoryToggleAllClick: any; + handleCategoryToggleAllClick: () => void; +} & StateProps["colors"]; + +interface PureCategoryProps { + metadataField: string; + isExpanded: boolean; + onExpansionChange: (metadataField: string) => void; + categoryType: string; } -type CategoryProps = PureCategoryProps & { - colors: any; - categoricalSelection: any; - annotations: any; - annoMatrix: any; - schema: Schema; - crossfilter: any; - isUserAnno: boolean; - genesets: any; +interface StateProps { + colors: RootState["colors"]; + categoricalSelection: RootState["categoricalSelection"][string]; + annoMatrix: RootState["annoMatrix"]; + schema: RootState["annoMatrix"]["schema"]; + crossfilter: RootState["obsCrossfilter"]; + genesets: RootState["genesets"]["genesets"]; isCellGuideCxg: boolean; -}; +} + +type CategoryProps = PureCategoryProps & StateProps; -// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message -@connect((state: RootState, ownProps: PureCategoryProps) => { +const mapStateToProps = ( + state: RootState, + ownProps: PureCategoryProps +): StateProps => { const schema = state.annoMatrix?.schema; const { metadataField } = ownProps; - const isUserAnno = schema?.annotations?.obsByName[metadataField]?.writable; const categoricalSelection = state.categoricalSelection?.[metadataField]; return { colors: state.colors, categoricalSelection, - annotations: state.annotations, annoMatrix: state.annoMatrix, schema, crossfilter: state.obsCrossfilter, - isUserAnno, genesets: state.genesets.genesets, isCellGuideCxg: state.controls.isCellGuideCxg, }; -}) -class Category extends React.PureComponent { +}; + +class Category extends React.PureComponent { static getSelectionState( // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. categoricalSelection: any, @@ -104,7 +106,6 @@ class Category extends React.PureComponent { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. getSelectionState(categorySummary: any) { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'categoricalSelection' does not exist on ... Remove this comment to see the full error message const { categoricalSelection, metadataField } = this.props; return Category.getSelectionState( categoricalSelection, @@ -129,16 +130,9 @@ class Category extends React.PureComponent { }); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. handleCategoryClick = () => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'annotations' does not exist on type 'Rea... Remove this comment to see the full error message - const { annotations, metadataField, onExpansionChange } = this.props; - const editingCategory = - annotations.isEditingCategoryName && - annotations.categoryBeingEdited === metadataField; - if (!editingCategory) { - onExpansionChange(metadataField); - } + const { metadataField, onExpansionChange } = this.props; + onExpansionChange(metadataField); }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. @@ -160,11 +154,10 @@ class Category extends React.PureComponent { } }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - fetchAsyncProps = async (props: any) => { + fetchAsyncProps = async ( + props: AsyncProps + ): Promise => { const { annoMatrix, metadataField, colors } = props.watchProps; - // @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on type 'Rea... Remove this comment to see the full error message - const { crossfilter, isCellGuideCxg } = this.props; const [categoryData, categorySummary, colorData] = await this.fetchData( annoMatrix, @@ -176,8 +169,6 @@ class Category extends React.PureComponent { categoryData, categorySummary, colorData, - isCellGuideCxg, - crossfilter, ...this.updateColorTable(colorData), handleCategoryToggleAllClick: () => this.handleToggleAllClick(categorySummary), @@ -205,7 +196,6 @@ class Category extends React.PureComponent { */ const { schema } = annoMatrix; const { colorAccessor, colorMode } = colors; - // @ts-expect-error ts-migrate(2339) FIXME: Property 'genesets' does not exist on type 'Readon... Remove this comment to see the full error message const { genesets } = this.props; let colorDataPromise: Promise = Promise.resolve(null); if (colorAccessor) { @@ -234,10 +224,11 @@ class Category extends React.PureComponent { return [categoryData, categorySummary, colorData, colorMode]; } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -- - FIXME: disabled temporarily on migrate to TS. - updateColorTable(colorData: Dataframe | null) { + updateColorTable(colorData: Dataframe | null): { + isColorAccessor: boolean; + colorTable: ColorTable; + } & StateProps["colors"] { // color table, which may be null - // @ts-expect-error ts-migrate(2339) FIXME: Property 'schema' does not exist on type 'Readonly... Remove this comment to see the full error message const { schema, colors, metadataField } = this.props; const { colorAccessor, userColors, colorMode } = colors; return { @@ -286,22 +277,17 @@ class Category extends React.PureComponent { render(): JSX.Element { const { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'metadataField' does not exist on type 'R... Remove this comment to see the full error message metadataField, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'isExpanded' does not exist on type 'Read... Remove this comment to see the full error message isExpanded, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'categoricalSelection' does not exist on ... Remove this comment to see the full error message categoricalSelection, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'crossfilter' does not exist on type 'Rea... Remove this comment to see the full error message crossfilter, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'colors' does not exist on type 'Readonly... Remove this comment to see the full error message colors, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'annoMatrix' does not exist on type 'Read... Remove this comment to see the full error message annoMatrix, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'isUserAnno' does not exist on type 'Read... Remove this comment to see the full error message - isUserAnno, + isCellGuideCxg, } = this.props; + const { colorAccessor } = colors; + const checkboxID = `category-select-${metadataField}`; return ( @@ -325,9 +311,8 @@ class Category extends React.PureComponent { )} - {(asyncProps: CategoryProps) => { + {(asyncProps: CategoryAsyncProps) => { const { - colorAccessor, colorTable, colorData, categoryData, @@ -335,7 +320,6 @@ class Category extends React.PureComponent { isColorAccessor, handleCategoryToggleAllClick, colorMode, - isCellGuideCxg, } = asyncProps; const selectionState = this.getSelectionState(categorySummary); return ( @@ -343,7 +327,6 @@ class Category extends React.PureComponent { // @ts-expect-error ts-migrate(2322) FIXME: Type '{ metadataField: any; checkboxID: string; is... Remove this comment to see the full error message metadataField={metadataField} checkboxID={checkboxID} - isUserAnno={isUserAnno} isExpanded={isExpanded} isColorAccessor={isColorAccessor} selectionState={selectionState} @@ -368,7 +351,7 @@ class Category extends React.PureComponent { } } -export default Category; +export default connect(mapStateToProps)(Category); /** * We are still loading this category, so render a "busy" signal. @@ -401,7 +384,6 @@ const ErrorLoading = ({ metadataField, error }: any) => { interface CategoryHeaderProps { metadataField: any; checkboxID: any; - isUserAnno: boolean; isColorAccessor: boolean; isExpanded: boolean; selectionState: any; @@ -415,7 +397,6 @@ const CategoryHeader = React.memo( ({ metadataField, checkboxID, - isUserAnno, isColorAccessor, isExpanded, selectionState, @@ -471,7 +452,7 @@ const CategoryHeader = React.memo( - {tuples.map(([value, index]) => ( - - ))} - - ); - } - - /* User annotation */ - const flipKey = tuples.map((t) => t[0]).join(""); return ( - + <> {tuples.map(([value, index]) => ( - - - + ))} - + ); } ); diff --git a/client/src/components/categorical/categoryContext.ts b/client/src/components/categorical/categoryContext.ts index 5ec1b29f9..030c8a0fa 100644 --- a/client/src/components/categorical/categoryContext.ts +++ b/client/src/components/categorical/categoryContext.ts @@ -1,7 +1,9 @@ import React from "react"; +import { AnnoMatrixObsCrossfilter } from "../../annoMatrix"; /* -CategoryCrossfilterContext is used to pass a snapshot of the crossfilter +CategoryCrossfilterContext is used to pass a snapshot of the crossfilter matching the current category summary. */ -export const CategoryCrossfilterContext = React.createContext(null); +export const CategoryCrossfilterContext = + React.createContext(null); diff --git a/client/src/components/categorical/index.tsx b/client/src/components/categorical/index.tsx index 2490c6399..db0d05d6a 100644 --- a/client/src/components/categorical/index.tsx +++ b/client/src/components/categorical/index.tsx @@ -31,11 +31,6 @@ class Categories extends React.Component<{}, State> { constructor(props: {}) { super(props); this.state = { - createAnnoModeActive: false, - // eslint-disable-next-line react/no-unused-state --- FIXME: disabled temporarily - newCategoryText: "", - // eslint-disable-next-line react/no-unused-state --- FIXME: disabled temporarily - categoryToDuplicate: null, expandedCats: new Set(), }; } @@ -129,7 +124,7 @@ class Categories extends React.Component<{}, State> { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. render() { - const { createAnnoModeActive, expandedCats } = this.state; + const { expandedCats } = this.state; // @ts-expect-error ts-migrate(2339) FIXME: Property 'schema' does not exis... Remove this comment to see the full error message const { schema, isCellGuideCxg } = this.props; @@ -168,11 +163,9 @@ class Categories extends React.Component<{}, State> { standardCategoryNames.map((catName: string) => ( ))} @@ -187,11 +180,9 @@ class Categories extends React.Component<{}, State> { {standardCategoryNames.map((catName: string) => ( ))} @@ -205,11 +196,9 @@ class Categories extends React.Component<{}, State> { {authorCategoryNames.map((catName: string) => ( ))} diff --git a/client/src/components/categorical/value/index.tsx b/client/src/components/categorical/value/index.tsx index c1b4b11fd..b760daafc 100644 --- a/client/src/components/categorical/value/index.tsx +++ b/client/src/components/categorical/value/index.tsx @@ -19,7 +19,6 @@ import { EVENTS } from "../../../analytics/events"; import { RootState, AppDispatch } from "../../../reducers"; import { Schema, Category } from "../../../common/types/schema"; import { isDataframeDictEncodedColumn } from "../../../util/dataframe/types"; -import { AnnotationsState } from "../../../reducers/annotations"; import { CategorySummary } from "../../../util/stateManager/controlsHelpers"; import { ColorTable } from "../../../util/stateManager/colorHelpers"; @@ -38,12 +37,10 @@ interface PureCategoryValueProps { colorTable: ColorTable; colorData: Dataframe | null; categoryData: Dataframe; - isUserAnno: boolean; } interface StateProps { - annotations: AnnotationsState; - schema: Schema; + schema: RootState["annoMatrix"]["schema"]; isDilated: boolean; isSelected: boolean; label: string; @@ -85,13 +82,12 @@ const mapStateToProps = ( const labelName = isDataframeDictEncodedColumn(col) ? col.codeMapping[parseInt(label as string, 10)] : label; - const isSelected = category.get(label) ?? true; + const isSelected = category.get(label as string) ?? true; const isColorBy = metadataField === colorAccessor && colorMode === "color by categorical metadata"; return { - annotations: state.annotations, schema: state.annoMatrix?.schema, isDilated, isSelected, @@ -206,7 +202,6 @@ class CategoryValue extends React.Component { const valueSelectionChange = isSelected !== newIsSelected; const colorAccessorChange = props.colorAccessor !== nextProps.colorAccessor; - const annotationsChange = props.annotations !== nextProps.annotations; const colorModeChange = props.colorMode !== nextProps.colorMode; const editingLabel = state.editedLabelText !== nextState.editedLabelText; const dilationChange = props.isDilated !== nextProps.isDilated; @@ -226,7 +221,6 @@ class CategoryValue extends React.Component { labelChanged || valueSelectionChange || colorAccessorChange || - annotationsChange || editingLabel || dilationChange || countChanged || @@ -364,8 +358,8 @@ class CategoryValue extends React.Component { // If coloring by and this isn't the colorAccessor and it isn't being edited shouldRenderStackedBarOrHistogram() { - const { colorAccessor, isColorBy, annotations } = this.props; - return !!colorAccessor && !isColorBy && !annotations.isEditingLabelName; + const { colorAccessor, isColorBy } = this.props; + return !!colorAccessor && !isColorBy; } currentLabelAsString() { @@ -515,7 +509,6 @@ class CategoryValue extends React.Component { categoryIndex, colorAccessor, colorTable, - isUserAnno, isDilated, isSelected, categorySummary, @@ -538,16 +531,11 @@ class CategoryValue extends React.Component { const LEFT_MARGIN = 60; const CHECKBOX = 26; const CELL_NUMBER = 50; - const ANNO_MENU = 26; const LABEL_MARGIN = 16; const CHART_MARGIN = 24; const otherElementsWidth = - LEFT_MARGIN + - CHECKBOX + - CELL_NUMBER + - LABEL_MARGIN + - (isUserAnno ? ANNO_MENU : 0); + LEFT_MARGIN + CHECKBOX + CELL_NUMBER + LABEL_MARGIN; const labelWidth = colorAccessor && !isColorBy diff --git a/client/src/components/continuous/continuous.tsx b/client/src/components/continuous/continuous.tsx index 4ffbc5a44..2ac425c99 100644 --- a/client/src/components/continuous/continuous.tsx +++ b/client/src/components/continuous/continuous.tsx @@ -8,16 +8,16 @@ import { RootState } from "../../reducers"; import AnnoMatrix from "../../annoMatrix/annoMatrix"; import { AnnotationColumnSchema } from "../../common/types/schema"; -interface ContinuousProps { - schema: AnnoMatrix["schema"]; +interface StateProps { + schema?: AnnoMatrix["schema"]; } -function mapStateToProps(state: RootState) { +function mapStateToProps(state: RootState): StateProps { return { schema: state.annoMatrix?.schema, }; } -class Continuous extends React.PureComponent { +class Continuous extends React.PureComponent { render() { /* initial value for iterator to simulate index, ranges is an object */ const { schema } = this.props; @@ -42,6 +42,7 @@ class Continuous extends React.PureComponent { isObs zebra={index % 2 === 0} onGeneExpressionComplete={() => {}} + mini={false} /> ))} diff --git a/client/src/components/continuousLegend/index.tsx b/client/src/components/continuousLegend/index.tsx index fa7e64812..84e4ff4b0 100644 --- a/client/src/components/continuousLegend/index.tsx +++ b/client/src/components/continuousLegend/index.tsx @@ -4,7 +4,6 @@ import { connect, shallowEqual } from "react-redux"; import * as d3 from "d3"; import { interpolateCool } from "d3-scale-chromatic"; import Async, { AsyncProps } from "react-async"; -import AnnoMatrix from "../../annoMatrix/annoMatrix"; import { AppDispatch, RootState } from "../../reducers"; import { @@ -125,7 +124,7 @@ interface FetchedAsyncProps { } interface StateProps { - annoMatrix: AnnoMatrix; + annoMatrix: RootState["annoMatrix"]; colors: ColorsState; genesets: Genesets; } diff --git a/client/src/components/datasetSelector/datasetSelector.tsx b/client/src/components/datasetSelector/datasetSelector.tsx index 1e7cf9f79..b19b22d51 100644 --- a/client/src/components/datasetSelector/datasetSelector.tsx +++ b/client/src/components/datasetSelector/datasetSelector.tsx @@ -32,8 +32,8 @@ export type NavigateCheckUserState = (url: string) => void; * Props selected from store. */ interface StateProps { - datasetMetadata: DatasetMetadata; - portalUrl: string; + datasetMetadata: RootState["datasetMetadata"]["datasetMetadata"]; + portalUrl: RootState["datasetMetadata"]["portalUrl"]; seamlessEnabled: boolean; } @@ -104,7 +104,7 @@ const DatasetSelector: FC = ({ truncatingBreadcrumbProps: TruncatingBreadcrumbProps ): JSX.Element => { const { collection_datasets: datasets, dataset_id: selectedDatasetId } = - datasetMetadata; + datasetMetadata as DatasetMetadata; return isDatasetSingleton(datasets) ? renderBreadcrumb(truncatingBreadcrumbProps) : renderBreadcrumbMenu( diff --git a/client/src/components/embedding/index.tsx b/client/src/components/embedding/index.tsx index d2b1b3527..f65661ede 100644 --- a/client/src/components/embedding/index.tsx +++ b/client/src/components/embedding/index.tsx @@ -1,49 +1,90 @@ -import React from "react"; +import React, { FormEvent } from "react"; import { connect } from "react-redux"; import { useAsync } from "react-async"; import { Button, ButtonGroup, H4, - Popover, Position, Radio, RadioGroup, Tooltip, } from "@blueprintjs/core"; +import { IconNames } from "@blueprintjs/icons"; +import { Popover2 } from "@blueprintjs/popover2"; import * as globals from "../../globals"; import actions from "../../actions"; import { getDiscreteCellEmbeddingRowIndex } from "../../util/stateManager/viewStackHelpers"; import { track } from "../../analytics"; import { EVENTS } from "../../analytics/events"; -import { RootState } from "../../reducers"; +import { AppDispatch, RootState } from "../../reducers"; import { LAYOUT_CHOICE_TEST_ID } from "../../util/constants"; +import { Schema } from "../../common/types/schema"; +import { AnnoMatrixObsCrossfilter } from "../../annoMatrix"; +import { getFeatureFlag } from "../../util/featureFlags/featureFlags"; +import { FEATURES } from "../../util/featureFlags/features"; -type Props = RootState; +interface StateProps { + layoutChoice: RootState["layoutChoice"]; + schema?: Schema; + crossfilter: RootState["obsCrossfilter"]; + imageUnderlay: RootState["controls"]["imageUnderlay"]; + sideIsOpen: RootState["panelEmbedding"]["open"]; +} + +interface OwnProps { + isSidePanel: boolean; +} -// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message -@connect((state: RootState) => ({ - layoutChoice: state.layoutChoice, +interface DispatchProps { + dispatch: AppDispatch; +} + +type Props = StateProps & DispatchProps & OwnProps; + +const mapStateToProps = (state: RootState, props: OwnProps): StateProps => ({ + layoutChoice: props.isSidePanel + ? state.panelEmbedding.layoutChoice + : state.layoutChoice, schema: state.annoMatrix?.schema, crossfilter: state.obsCrossfilter, - imageUnderlay: state.imageUnderlay, -})) -class Embedding extends React.PureComponent { - handleLayoutChoiceClick = (): void => { + imageUnderlay: state.controls.imageUnderlay, + sideIsOpen: state.panelEmbedding.open, +}); + +const Embedding = (props: Props) => { + const { + layoutChoice, + schema, + crossfilter, + dispatch, + imageUnderlay, + isSidePanel, + sideIsOpen, + } = props; + const { annoMatrix } = crossfilter || {}; + if (!crossfilter || !annoMatrix) return null; + + const isSpatial = getFeatureFlag(FEATURES.SPATIAL); + + const handleLayoutChoiceClick = (): void => { track(EVENTS.EXPLORER_EMBEDDING_CLICKED); }; - handleLayoutChoiceChange = (e: React.ChangeEvent) => { - const { dispatch, imageUnderlay } = this.props; - + const handleLayoutChoiceChange = async ( + e: FormEvent + ): Promise => { track(EVENTS.EXPLORER_EMBEDDING_SELECTED, { embedding: e.currentTarget.value, }); - dispatch(actions.layoutChoiceAction(e.currentTarget.value)); + await dispatch( + actions.layoutChoiceAction(e.currentTarget.value, isSidePanel) + ); if ( imageUnderlay && - !e.target.value.includes(globals.spatialEmbeddingKeyword) + !isSidePanel && + !e.currentTarget.value.includes(globals.spatialEmbeddingKeyword) ) { dispatch({ type: "toggle image underlay", @@ -52,41 +93,20 @@ class Embedding extends React.PureComponent { } }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - render() { - const { layoutChoice, schema, crossfilter } = this.props; - const { annoMatrix } = crossfilter || {}; - - if (!crossfilter) return null; + const handleOpenPanelEmbedding = async (): Promise => { + dispatch({ + type: "toggle panel embedding", + }); + }; - return ( - - - - - } + return ( +
+ + { There are {schema?.dataframe?.nObs} cells in the entire dataset.

} - /> + > + + + + + {!isSidePanel && isSpatial && ( + - -
-
- {/* loading tab */} - {!minimized && geneName === "" && infoError === null ? ( -
- {gene} - loading... -
- ) : null} - {/* failed gene search */} - {!minimized && infoError !== null ? ( -
- {gene} - Sorry, this gene could not be found on NCBI. - - Search on Google - -
- ) : null} - {!minimized && geneName !== "" && infoError === null ? ( -
- {showWarningBanner ? ( - - - - NCBI didn't return an exact match for this gene. - - - ) : null} - {gene} - {geneName} - {geneSummary === "" ? ( - - This gene does not currently have a summary in NCBI. - - ) : ( - - {geneSummary} - - )} - {synonymList ? ( -

- Synonyms - - {synonymList} - -

- ) : null} - {geneUrl !== "" ? ( - - View on NCBI - - ) : null} -
- ) : null} -
- - ); - } -} - -export default GeneInfo; diff --git a/client/src/components/geneExpression/index.tsx b/client/src/components/geneExpression/index.tsx index 1ed38ebf4..c7ab879ea 100644 --- a/client/src/components/geneExpression/index.tsx +++ b/client/src/components/geneExpression/index.tsx @@ -8,6 +8,7 @@ import QuickGene from "./quickGene"; import CreateGenesetDialogue from "./menus/createGenesetDialogue"; import { track } from "../../analytics"; import { EVENTS } from "../../analytics/events"; +import * as globals from "../../globals"; import { Dataframe, DataframeValue } from "../../util/dataframe"; import { MARKER_GENE_SUFFIX_IDENTIFIER } from "./constants"; @@ -175,7 +176,14 @@ class GeneExpression extends React.Component<{}, State> { const { isCellGuideCxg } = this.props; const { geneSetsExpanded, markerGeneSetsExpanded } = this.state; return ( -
+
selectableCategoryNames(schema).sort(), + [schema] + ); + + const allSingleValues = useMemo(() => { + const singleValues = new Map(); + + allCategoryNames.forEach((catName) => { + const colSchema = schema.annotations.obsByName[catName]; + const isUserAnno = colSchema?.writable; + if (!isUserAnno && colSchema.categories?.length === 1) { + singleValues.set(catName, colSchema.categories[0]); + } + }); + + singleContinuousValues.forEach((value, catName) => { + singleValues.set(catName, value); + }); + + return singleValues; + }, [schema, allCategoryNames, singleContinuousValues]); + + return { + allSingleValues, + }; +} diff --git a/client/src/components/infoDrawer/infoFormat.tsx b/client/src/components/geneExpression/infoPanel/datasetInfo/datasetInfoFormat.tsx similarity index 95% rename from client/src/components/infoDrawer/infoFormat.tsx rename to client/src/components/geneExpression/infoPanel/datasetInfo/datasetInfoFormat.tsx index 00dec6d32..d94d8ba1f 100644 --- a/client/src/components/infoDrawer/infoFormat.tsx +++ b/client/src/components/geneExpression/infoPanel/datasetInfo/datasetInfoFormat.tsx @@ -6,12 +6,12 @@ import React, { CSSProperties } from "react"; import { Author, Consortium, - DatasetMetadata, Link, PublisherMetadata, -} from "../../common/types/entities"; -import { Category } from "../../common/types/schema"; -import * as globals from "../../globals"; +} from "../../../../common/types/entities"; +import { Category } from "../../../../common/types/schema"; +import * as globals from "../../../../globals"; +import { RootState } from "../../../../reducers"; const COLLECTION_LINK_ORDER_BY = [ "DOI", @@ -34,7 +34,7 @@ interface MetadataView { } interface Props { - datasetMetadata: DatasetMetadata; + datasetMetadata: RootState["datasetMetadata"]["datasetMetadata"]; allSingleValues: SingleValues; } @@ -207,9 +207,8 @@ const isAuthorPerson = (author: Author | Consortium): author is Author => * @param datasetMetadata - Dataset metadata containing collection link information to be displayed * @returns Markup displaying contact and collection-related links. */ -const renderCollectionLinks = ( - datasetMetadata: DatasetMetadata -): JSX.Element => { +const renderCollectionLinks = (datasetMetadata: Props["datasetMetadata"]) => { + if (!datasetMetadata) return null; const { collection_contact_name: contactName, collection_contact_email: contactEmail, @@ -348,8 +347,8 @@ const buildDatasetMetadataViews = ( const InfoFormat = React.memo(({ datasetMetadata, allSingleValues }) => (
-

{datasetMetadata.collection_name}

-

{datasetMetadata.collection_description}

+

{datasetMetadata?.collection_name || "Collection"}

+ {datasetMetadata &&

{datasetMetadata.collection_description}

} {renderCollectionLinks(datasetMetadata)} {renderDatasetMetadata(allSingleValues)}
diff --git a/client/src/components/geneExpression/infoPanel/datasetInfo/index.tsx b/client/src/components/geneExpression/infoPanel/datasetInfo/index.tsx new file mode 100644 index 000000000..b5b4c84e7 --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/datasetInfo/index.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { connect } from "react-redux"; + +import InfoFormat from "./datasetInfoFormat"; +import { Props, mapStateToProps } from "./types"; +import { useConnect } from "./connect"; + +function DatasetInfo(props: Props) { + const { datasetMetadata, schema, singleContinuousValues } = props; + const { allSingleValues } = useConnect({ schema, singleContinuousValues }); + + return ( + + ); +} + +export default connect(mapStateToProps)(DatasetInfo); diff --git a/client/src/components/geneExpression/infoPanel/datasetInfo/types.ts b/client/src/components/geneExpression/infoPanel/datasetInfo/types.ts new file mode 100644 index 000000000..7016ca6fc --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/datasetInfo/types.ts @@ -0,0 +1,13 @@ +import { RootState } from "../../../../reducers"; + +export interface Props { + datasetMetadata: RootState["datasetMetadata"]["datasetMetadata"]; + schema: RootState["annoMatrix"]["schema"]; + singleContinuousValues: RootState["singleContinuousValue"]["singleContinuousValues"]; +} + +export const mapStateToProps = (state: RootState): Props => ({ + datasetMetadata: state.datasetMetadata.datasetMetadata, + schema: state.annoMatrix.schema, + singleContinuousValues: state.singleContinuousValue.singleContinuousValues, +}); diff --git a/client/src/components/geneExpression/infoPanel/geneInfo/connect.ts b/client/src/components/geneExpression/infoPanel/geneInfo/connect.ts new file mode 100644 index 000000000..2554dcd92 --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/geneInfo/connect.ts @@ -0,0 +1,11 @@ +export function useConnect({ geneSynonyms }: { geneSynonyms: string[] }) { + let synonymList; + if (geneSynonyms.length > 1) { + synonymList = geneSynonyms.join(", "); + } else if (geneSynonyms.length === 1) { + synonymList = geneSynonyms[0]; + } else { + synonymList = null; + } + return { synonymList }; +} diff --git a/client/src/components/geneExpression/infoPanel/geneInfo/index.tsx b/client/src/components/geneExpression/infoPanel/geneInfo/index.tsx new file mode 100644 index 000000000..33051ab07 --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/geneInfo/index.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { connect } from "react-redux"; +import { Icon } from "czifui"; +import { + SynHeader, + Synonyms, + Link, + Content, + GeneSymbol, + GeneInfoContainer, + GeneInfoEmpty, + GeneInfoWrapper, + WarningBanner, + NoGeneSelectedDiv, + MessageDiv, + CustomIcon, +} from "./style"; +import { Props, mapStateToProps } from "./types"; +import { useConnect } from "./connect"; + +function GeneInfo(props: Props) { + const { + geneSummary, + geneName, + gene, + geneUrl, + geneSynonyms, + infoError, + showWarningBanner, + } = props; + + const { synonymList } = useConnect({ geneSynonyms }); + + return ( + + + {geneName === "" && infoError === null && gene !== null ? ( + + {gene} + loading... + + ) : null} + {gene === null && infoError === null ? ( + + + + + No Gene Selected + + Choose a gene above or search the NCBI database. + + + + ) : null} + {infoError !== null ? ( + + {gene} + Sorry, this gene could not be found on NCBI. + + Search on Google + + + ) : null} + {geneName !== "" && infoError === null ? ( + + {showWarningBanner ? ( + + + + NCBI didn't return an exact match for this gene. + + + ) : null} + {gene} + {geneName} + {geneSummary === "" ? ( + + This gene does not currently have a summary in NCBI. + + ) : ( + {geneSummary} + )} + {synonymList ? ( +

+ Synonyms + + {synonymList} + +

+ ) : null} + {geneUrl !== "" ? ( + + View on NCBI + + ) : null} +
+ ) : null} +
+
+ ); +} + +export default connect(mapStateToProps)(GeneInfo); diff --git a/client/src/components/geneExpression/geneInfo/style.ts b/client/src/components/geneExpression/infoPanel/geneInfo/style.ts similarity index 54% rename from client/src/components/geneExpression/geneInfo/style.ts rename to client/src/components/geneExpression/infoPanel/geneInfo/style.ts index e585cf2ad..adf54b677 100644 --- a/client/src/components/geneExpression/geneInfo/style.ts +++ b/client/src/components/geneExpression/infoPanel/geneInfo/style.ts @@ -5,28 +5,30 @@ import { fontBodyS, getColors, fontHeaderL, + fontHeaderM, } from "czifui"; +import { Icon } from "@blueprintjs/core"; + +import * as globals from "../../../../globals"; +import * as styles from "../../util"; +import { gray100, gray500 } from "../../../theme"; export const GeneInfoWrapper = styled.div` - position: fixed; - border-radius: 3px 3px 0px 0px; - padding: 0px 20px 20px 0px; - background: white; - box-shadow: 0px 0px 3px 2px rgba(153, 153, 153, 0.2); - z-index: 2; + display: flex; + bottom: ${globals.bottomToolbarGutter}px; + left: ${globals.leftSidebarWidth + globals.scatterplotMarginLeft}px; `; -export const GeneHeader = styled.p` - font-weight: 500; - ${fontBodyS} - - ${(props) => { - const colors = getColors(props); +export const GeneInfoContainer = styled.div` + width: ${styles.width + styles.margin.left + styles.margin.right}px; + height: "auto"; +`; - return ` - color: ${colors?.gray[500]}; - `; - }} +export const GeneInfoEmpty = styled.div` + margin-top: ${styles.margin.top}px; + margin-left: ${styles.margin.left}px; + margin-right: ${styles.margin.right}px; + margin-bottom: ${styles.margin.bottom}px; `; export const GeneSymbol = styled.h1` @@ -45,6 +47,10 @@ export const Content = styled.p` font-weight: 500; color: black; ${fontBodyXs} + display: "-webkit_box"; + -webkit-line-clamp: 7; + -webkit-box-orient: vertical; + overflow: hidden; `; export const SynHeader = styled.span` @@ -104,3 +110,33 @@ export const WarningBanner = styled.div` `; }} `; + +export const NoGeneSelectedDiv = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: ${gray100}; + padding: 20px; + border-radius: 3px; +`; + +export const MessageDiv = styled.div` + ${fontBodyXs} + font-weight: 400; + color: ${gray500}; + padding: 10px; + &.title { + color: black; + ${fontHeaderM} + font-weight: 700; + } +`; + +export const CustomIcon = styled(Icon)` + && { + color: ${gray500}; + } +`; diff --git a/client/src/components/geneExpression/infoPanel/geneInfo/types.ts b/client/src/components/geneExpression/infoPanel/geneInfo/types.ts new file mode 100644 index 000000000..b23705c1b --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/geneInfo/types.ts @@ -0,0 +1,21 @@ +import { RootState } from "../../../../reducers"; + +export interface Props { + geneSummary: RootState["controls"]["geneSummary"]; + geneName: RootState["controls"]["geneName"]; + gene: RootState["controls"]["gene"]; + geneUrl: RootState["controls"]["geneUrl"]; + geneSynonyms: RootState["controls"]["geneSynonyms"]; + showWarningBanner: RootState["controls"]["showWarningBanner"]; + infoError: RootState["controls"]["infoError"]; +} + +export const mapStateToProps = (state: RootState): Props => ({ + geneSummary: state.controls.geneSummary, + geneName: state.controls.geneName, + gene: state.controls.gene, + geneUrl: state.controls.geneUrl, + geneSynonyms: state.controls.geneSynonyms, + showWarningBanner: state.controls.showWarningBanner, + infoError: state.controls.infoError, +}); diff --git a/client/src/components/geneExpression/infoPanel/index.tsx b/client/src/components/geneExpression/infoPanel/index.tsx new file mode 100644 index 000000000..5caa87115 --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/index.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { connect } from "react-redux"; +import { AnchorButton, ButtonGroup } from "@blueprintjs/core"; +import { + CollapseToggle, + InfoPanelContent, + InfoPanelHeader, + InfoPanelTabs, + InfoPanelWrapper, + StyledAnchorButton, +} from "./style"; +import GeneInfo from "./geneInfo"; +import DatasetInfo from "./datasetInfo"; +import { Props, mapStateToProps } from "./types"; + +function InfoPanel(props: Props) { + const { activeTab, dispatch, infoPanelMinimized, infoPanelHidden } = props; + + return ( + + + + + dispatch({ + type: "toggle active info panel", + activeTab: "Gene", + }) + } + /> + + dispatch({ + type: "toggle active info panel", + activeTab: "Dataset", + }) + } + /> + + + + { + dispatch({ type: "minimize/maximize info panel" }); + }} + /> + + dispatch({ + type: "close info panel", + }) + } + /> + + + + + {activeTab === "Gene" && } + {activeTab === "Dataset" && } + + + ); +} + +export default connect(mapStateToProps)(InfoPanel); diff --git a/client/src/components/geneExpression/infoPanel/style.ts b/client/src/components/geneExpression/infoPanel/style.ts new file mode 100644 index 000000000..d5cc04a7d --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/style.ts @@ -0,0 +1,71 @@ +import styled from "@emotion/styled"; +import { fontBodyXs } from "czifui"; +import { AnchorButton } from "@blueprintjs/core"; +import { gray300 } from "../../theme"; + +interface InfoPanelWrapperProps { + isHidden: boolean; + isMinimized: boolean; +} + +export const InfoPanelWrapper = styled.div` + display: flex; + flex-direction: row; + width: 100%; + visibility: ${(props) => (props.isHidden ? "hidden" : "visible")}; + overflow: hidden; + position: ${(props) => (props.isMinimized ? "absolute" : "relative")}; + bottom: ${(props) => (props.isMinimized ? "0" : "auto")}; + height: ${(props) => (props.isMinimized ? "40px" : "auto")}; +`; + +export const InfoPanelContent = styled.div` + width: 100%; + padding: 30px 0px 0px 0px; + position: relative; + overflow-y: auto; + max-height: ${(props) => (props.isMinimized ? "0" : "400px")}; +`; + +export const InfoPanelHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + border-top: 1px solid ${gray300}; + border-bottom: 1px solid ${gray300}; + padding: 10px 0px 5px 10px; + height: 38px; + position: absolute; + background: white; + z-index: 1; +`; + +export const StyledAnchorButton = styled(AnchorButton)` + ${fontBodyXs} + color: #ccc; + + &.active { + font-weight: 600; + color: black; + border-bottom: 3px solid #0073ff; + border-radius: 0px; + } +`; + +export const InfoPanelTabs = styled.div` + display: flex; + flex-direction: row; + width: 200px; + justify-content: space-between; +`; + +export const CollapseToggle = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-end; + width: 50px; + padding: 0px 0px 0px 20px; + cursor: pointer; +`; diff --git a/client/src/components/geneExpression/infoPanel/types.ts b/client/src/components/geneExpression/infoPanel/types.ts new file mode 100644 index 000000000..ab20cff47 --- /dev/null +++ b/client/src/components/geneExpression/infoPanel/types.ts @@ -0,0 +1,15 @@ +import { AppDispatch, RootState } from "../../../reducers"; + +interface StateProps { + activeTab: string; + infoPanelMinimized: boolean; + infoPanelHidden: boolean; +} + +export type Props = StateProps & { dispatch: AppDispatch }; + +export const mapStateToProps = (state: RootState): StateProps => ({ + activeTab: state.controls.activeTab, + infoPanelMinimized: state.controls.infoPanelMinimized, + infoPanelHidden: state.controls.infoPanelHidden, +}); diff --git a/client/src/components/graph/graph.tsx b/client/src/components/graph/graph.tsx index e0e8325aa..195b48f01 100644 --- a/client/src/components/graph/graph.tsx +++ b/client/src/components/graph/graph.tsx @@ -5,7 +5,7 @@ import { connect, shallowEqual } from "react-redux"; import { mat3, vec2 } from "gl-matrix"; import _regl, { DrawCommand, Regl } from "regl"; import memoize from "memoize-one"; -import Async from "react-async"; +import Async, { AsyncProps } from "react-async"; import { Button, Icon } from "@blueprintjs/core"; import Openseadragon, { Viewer } from "openseadragon"; @@ -47,11 +47,12 @@ import { getSpatialPrefixUrl, getSpatialTileSources, loadImage, + sidePanelAttributeNameChange, } from "./util"; import { COMMON_CANVAS_STYLE } from "./constants"; import { THROTTLE_MS } from "../../util/constants"; -import { GraphProps, GraphState } from "./types"; +import { GraphProps, OwnProps, GraphState, StateProps } from "./types"; import { isSpatialMode, shouldShowOpenseadragon } from "../../common/selectors"; import { fetchDeepZoomImageFailed } from "../../actions/config"; import { track } from "../../analytics"; @@ -67,13 +68,14 @@ interface GraphAsyncProps { screenCap: boolean; } -// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message -@connect((state: RootState) => ({ +const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => ({ annoMatrix: state.annoMatrix, crossfilter: state.obsCrossfilter, selectionTool: state.graphSelection.tool, currentSelection: state.graphSelection.selection, - layoutChoice: state.layoutChoice, + layoutChoice: ownProps.isSidePanel + ? state.panelEmbedding.layoutChoice + : state.layoutChoice, graphInteractionMode: state.controls.graphInteractionMode, colors: state.colors, pointDilation: state.pointDilation, @@ -82,7 +84,8 @@ interface GraphAsyncProps { mountCapture: state.controls.mountCapture, imageUnderlay: state.controls.imageUnderlay, config: state.config, -})) +}); + class Graph extends React.Component { static createReglState(canvas: HTMLCanvasElement): { camera: Camera; @@ -115,8 +118,11 @@ class Graph extends React.Component { }; } - static watchAsync(props: GraphProps, prevProps: GraphProps): boolean { - return !shallowEqual(props.watchProps, prevProps.watchProps); + static watchAsync( + watchProps: AsyncProps, + prevWatchProps: AsyncProps + ): boolean { + return !shallowEqual(watchProps.watchProps, prevWatchProps.watchProps); } /** @@ -289,7 +295,6 @@ class Graph extends React.Component { componentDidUpdate(prevProps: GraphProps, prevState: GraphState): void { const { selectionTool, - currentSelection, graphInteractionMode, screenCap, imageUnderlay, @@ -323,21 +328,7 @@ class Graph extends React.Component { ...this.createToolSVG(), }; } - /* - if the selection tool or state has changed, ensure that the selection - tool correctly reflects the underlying selection. - */ - if ( - currentSelection !== prevProps.currentSelection || - graphInteractionMode !== prevProps.graphInteractionMode || - stateChanges.toolSVG - ) { - const { tool, container } = this.state; - this.selectionToolUpdate( - stateChanges.tool ? stateChanges.tool : tool!, - stateChanges.container ? stateChanges.container : container! - ); - } + if (Object.keys(stateChanges).length > 0) { this.setState((state) => ({ ...state, ...stateChanges })); } @@ -480,7 +471,6 @@ class Graph extends React.Component { const southeast = this.mapScreenToPoint(s[1]); const [minX, maxY] = northwest; const [maxX, minY] = southeast; - await dispatch( actions.graphBrushEndAction(layoutChoice?.current, { minX, @@ -510,6 +500,7 @@ class Graph extends React.Component { async handleLassoEnd(polygon: [number, number][]): Promise { const minimumPolygonArea = 10; const { dispatch, layoutChoice } = this.props; + if ( polygon.length < 3 || Math.abs(d3.polygonArea(polygon)) < minimumPolygonArea @@ -692,12 +683,20 @@ class Graph extends React.Component { Called from componentDidUpdate. Create the tool SVG, and return any state changes that should be passed to setState(). */ - const { selectionTool, graphInteractionMode } = this.props; + const { + selectionTool, + graphInteractionMode, + isSidePanel = false, + } = this.props; const { viewport } = this.state; /* clear out whatever was on the div, even if nothing, but usually the brushes etc */ - const lasso = d3.select("#lasso-layer"); + const lasso = d3.select( + sidePanelAttributeNameChange(`#lasso-layer`, isSidePanel) + ); if (lasso.empty()) return {}; // still initializing - lasso.selectAll(".lasso-group").remove(); + lasso + .selectAll(sidePanelAttributeNameChange(`.lasso-group`, isSidePanel)) + .remove(); // Don't render or recreate toolSVG if currently in zoom mode if (graphInteractionMode !== "select") { // don't return "change" of state unless we are really changing it! @@ -732,6 +731,7 @@ class Graph extends React.Component { handleStartAction: handleStart, handleEndAction: handleEnd, handleCancelAction: handleCancel, + isSidePanel, }); } if (!ret) return {}; @@ -744,7 +744,9 @@ class Graph extends React.Component { return this.underlayImage; }; - fetchAsyncProps = async (props: GraphProps): Promise => { + fetchAsyncProps = async ( + props: AsyncProps + ): Promise => { const { annoMatrix, colors: colorsProp, @@ -860,63 +862,17 @@ class Graph extends React.Component { Promise, Promise ] = [ - annoMatrix.fetch("emb", layoutChoice?.current, globals.numBinsEmb), + annoMatrix.fetch(Field.emb, layoutChoice?.current, globals.numBinsEmb), query ? annoMatrix.fetch(...query, globals.numBinsObsX) : Promise.resolve(null), pointDilationAccessor - ? annoMatrix.fetch("obs", pointDilationAccessor) + ? annoMatrix.fetch(Field.obs, pointDilationAccessor) : Promise.resolve(null), ]; return Promise.all(promises); } - brushToolUpdate( - tool: d3.BrushBehavior, - container: d3.Selection - ): void { - /* - this is called from componentDidUpdate(), so be very careful using - anything from this.state, which may be updated asynchronously. - */ - const { currentSelection } = this.props; - const node = container.node(); - if (node) { - const toolCurrentSelection = d3.brushSelection(node); - if (currentSelection.mode === "within-rect") { - /* - if there is a selection, make sure the brush tool matches - */ - const screenCoords = [ - this.mapPointToScreen(currentSelection.brushCoords.northwest), - this.mapPointToScreen(currentSelection.brushCoords.southeast), - ]; - if (!toolCurrentSelection) { - /* tool is not selected, so just move the brush */ - container.call(tool.move, screenCoords); - } else { - /* there is an active selection and a brush - make sure they match */ - /* this just sums the difference of each dimension, of each point */ - let delta = 0; - for (let x = 0; x < 2; x += 1) { - for (let y = 0; y < 2; y += 1) { - delta += Math.abs( - screenCoords[x][y] - - (toolCurrentSelection as [number, number][])[x][y] - ); - } - } - if (delta > 0) { - container.call(tool.move, screenCoords); - } - } - } else if (toolCurrentSelection) { - /* no selection, so clear the brush tool if it is set */ - container.call(tool.move, null); - } - } - } - lassoToolUpdate(tool: LassoFunctionWithAttributes): void { /* this is called from componentDidUpdate(), so be very careful using @@ -936,28 +892,6 @@ class Graph extends React.Component { } } - selectionToolUpdate( - tool: GraphState["tool"], - container: d3.Selection - ): void { - /* - this is called from componentDidUpdate(), so be very careful using - anything from this.state, which may be updated asynchronously. - */ - const { selectionTool } = this.props; - switch (selectionTool) { - case "brush": - this.brushToolUpdate(tool as d3.BrushBehavior, container); - break; - case "lasso": - this.lassoToolUpdate(tool as LassoFunctionWithAttributes); - break; - default: - /* punt? */ - break; - } - } - mapScreenToPoint(pin: [number, number]): vec2 { /* Map an XY coordinates from screen domain to cell/point range, @@ -1095,6 +1029,7 @@ class Graph extends React.Component { const { annoMatrix, genesets } = this.props; const { schema } = annoMatrix; const { colorMode, colorAccessor } = colors; + // @ts-expect-error (seve): fix downstream lint errors as a result of detailed app store typing return createColorQuery(colorMode, colorAccessor, schema, genesets); } @@ -1105,20 +1040,23 @@ class Graph extends React.Component { const { config: { s3URI }, + isSidePanel = false, + imageUnderlay, } = this.props; if ( this.openseadragon || + !imageUnderlay || !shouldShowOpenseadragon(this.props) || !width || - !height + !height || + !s3URI ) { return; } this.openseadragon = Openseadragon({ - // (thuang): This id will need to be unique for multiple graphs - id: "openseadragon", + id: sidePanelAttributeNameChange(`openseadragon`, isSidePanel), prefixUrl: getSpatialPrefixUrl(s3URI), tileSources: getSpatialTileSources(s3URI), showNavigationControl: false, @@ -1253,7 +1191,8 @@ class Graph extends React.Component { crossfilter, screenCap, imageUnderlay, - spatial, + isSidePanel = false, + isHidden = false, } = this.props; const { @@ -1270,16 +1209,16 @@ class Graph extends React.Component { return (
@@ -1301,24 +1240,30 @@ class Graph extends React.Component { Re-center Embedding )} - - - - + {/* If sidepanel don't show centroids */} + {!isSidePanel && ( + + + + )} { /> {shouldShowOpenseadragon(this.props) && (
{ ...COMMON_CANVAS_STYLE, shapeRendering: "crispEdges", }} - className="graph-canvas" - data-testid="layout-graph" + id={sidePanelAttributeNameChange(`graph-canvas`, isSidePanel)} + data-testid={sidePanelAttributeNameChange( + `layout-graph`, + isSidePanel + )} ref={this.setReglCanvas} onMouseDown={this.handleCanvasEvent} onMouseUp={this.handleCanvasEvent} @@ -1371,7 +1319,10 @@ class Graph extends React.Component { COMMON_CANVAS_STYLE } alt="" - data-testid="graph-image" + data-testid={sidePanelAttributeNameChange( + `graph-image`, + isSidePanel + )} src={testImageSrc} /> )} @@ -1387,7 +1338,6 @@ class Graph extends React.Component { viewport, screenCap, imageUnderlay, - spatial, }} > @@ -1480,5 +1430,4 @@ const StillLoading = ({ displayName, width, height }: StillLoadingProps) => (
); - -export default Graph; +export default connect(mapStateToProps)(Graph); diff --git a/client/src/components/graph/setupLasso.ts b/client/src/components/graph/setupLasso.ts index c816c0bc4..04e2972e3 100644 --- a/client/src/components/graph/setupLasso.ts +++ b/client/src/components/graph/setupLasso.ts @@ -1,5 +1,6 @@ import * as d3 from "d3"; import { Colors } from "@blueprintjs/core"; +import { sidePanelAttributeNameChange } from "./util"; type LassoFunction = ( svg: d3.Selection @@ -19,7 +20,8 @@ const Lasso = () => { // (seve): I really can't seem to correctly type this function with dynamic attributes const lasso: LassoFunctionWithAttributes = (( - svg + svg, + isSidePanel = false ) => { const svgNode = svg.node()!; let lassoPolygon: [number, number][] | null; @@ -57,7 +59,10 @@ const Lasso = () => { lassoPath = g .append("path") - .attr("data-testid", "lasso-element") + .attr( + "data-testid", + sidePanelAttributeNameChange("lasso-element", isSidePanel) + ) .attr("fill-opacity", 0.1) .attr("stroke-dasharray", "3, 3"); @@ -128,7 +133,9 @@ const Lasso = () => { }; // append a with a rect - const g = svg.append("g").attr("class", "lasso-group"); + const g = svg + .append("g") + .attr("class", sidePanelAttributeNameChange("lasso-group", isSidePanel)); const bbox = svgNode.getBoundingClientRect(); const area = g .append("rect") @@ -161,7 +168,10 @@ const Lasso = () => { lassoPolygon = polygon; lassoPath = g .append("path") - .attr("data-testid", "lasso-element") + .attr( + "data-testid", + sidePanelAttributeNameChange("lasso-element", isSidePanel) + ) .attr("fill", lassoPathColor) .attr("fill-opacity", 0.1) .attr("stroke", lassoPathColor) diff --git a/client/src/components/graph/setupSVGandBrush.ts b/client/src/components/graph/setupSVGandBrush.ts index ad5e4a621..973b48fae 100644 --- a/client/src/components/graph/setupSVGandBrush.ts +++ b/client/src/components/graph/setupSVGandBrush.ts @@ -1,5 +1,6 @@ import * as d3 from "d3"; import Lasso, { LassoFunctionWithAttributes } from "./setupLasso"; +import { sidePanelAttributeNameChange } from "./util"; /****************************************** ******************************************* @@ -52,19 +53,21 @@ export function setupLasso({ handleStartAction, handleEndAction, handleCancelAction, + isSidePanel, }: { selectionToolType: "lasso"; handleStartAction: () => void; handleEndAction: (polygon: [number, number][]) => void; handleCancelAction: () => void; + isSidePanel: boolean; }): { svg?: d3.Selection; container?: d3.Selection; tool?: LassoFunctionWithAttributes; } { const svg: d3.Selection = d3 - .select("#graph-wrapper") - .select("#lasso-layer"); + .select(sidePanelAttributeNameChange("#graph-wrapper", isSidePanel)) + .select(sidePanelAttributeNameChange("#lasso-layer", isSidePanel)); if (svg.empty()) { return {}; } @@ -74,7 +77,7 @@ export function setupLasso({ .on("start", handleStartAction) .on("cancel", handleCancelAction); - const lassoContainer = svg.call(lasso); + const lassoContainer = svg.call(lasso, isSidePanel); return { svg, container: lassoContainer, tool: lasso }; } diff --git a/client/src/components/graph/types.ts b/client/src/components/graph/types.ts index 312e0db91..172dc8d2c 100644 --- a/client/src/components/graph/types.ts +++ b/client/src/components/graph/types.ts @@ -10,9 +10,7 @@ import { Dataframe } from "../../util/dataframe"; import { LassoFunctionWithAttributes } from "./setupLasso"; -import { RootState } from "../../reducers"; - -export type GraphProps = Partial; +import { AppDispatch, RootState } from "../../reducers"; export type GraphState = { regl: Regl | null; @@ -46,3 +44,31 @@ export type GraphState = { testImageSrc: string | null; isImageLayerInViewport: boolean; }; + +export interface StateProps { + annoMatrix: RootState["annoMatrix"]; + crossfilter: RootState["obsCrossfilter"]; + selectionTool: RootState["graphSelection"]["tool"]; + currentSelection: RootState["graphSelection"]["selection"]; + layoutChoice: RootState["layoutChoice"]; + graphInteractionMode: RootState["controls"]["graphInteractionMode"]; + colors: RootState["colors"]; + pointDilation: RootState["pointDilation"]; + genesets: RootState["genesets"]["genesets"]; + screenCap: RootState["controls"]["screenCap"]; + mountCapture: RootState["controls"]["mountCapture"]; + imageUnderlay: RootState["controls"]["imageUnderlay"]; + config: RootState["config"]; +} + +export interface OwnProps { + viewportRef: HTMLDivElement; + isSidePanel?: boolean; + isHidden?: boolean; +} + +interface DispatchProps { + dispatch: AppDispatch; +} + +export type GraphProps = StateProps & DispatchProps & OwnProps; diff --git a/client/src/components/graph/util.ts b/client/src/components/graph/util.ts index 64cbed6ae..22bb50c34 100644 --- a/client/src/components/graph/util.ts +++ b/client/src/components/graph/util.ts @@ -2,6 +2,13 @@ import { mat3 } from "gl-matrix"; import { toPng } from "html-to-image"; import { LayoutChoiceState } from "../../reducers/layoutChoice"; +export function sidePanelAttributeNameChange( + name: string, + isSidePanel: boolean +): string { + return `${name}${isSidePanel ? "-side" : ""}`; +} + export async function captureLegend( colors: any, ctx: CanvasRenderingContext2D | null, diff --git a/client/src/components/hotkeys/index.tsx b/client/src/components/hotkeys/index.tsx index 87bfa96c6..34d080348 100644 --- a/client/src/components/hotkeys/index.tsx +++ b/client/src/components/hotkeys/index.tsx @@ -18,6 +18,7 @@ type Props = DispatchProps; const performSubset = () => (dispatch: AppDispatch, getState: GetState) => { const state = getState(); const crossfilter = state.obsCrossfilter; + if (!crossfilter) return; const selectedCount = crossfilter.countSelected(); const subsetPossible = selectedCount !== 0 && selectedCount !== crossfilter.size(); diff --git a/client/src/components/infoDrawer/infoDrawer.tsx b/client/src/components/infoDrawer/infoDrawer.tsx deleted file mode 100644 index 6006a3ece..000000000 --- a/client/src/components/infoDrawer/infoDrawer.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* Core dependencies */ -import React, { PureComponent } from "react"; -import { connect } from "react-redux"; -import { Drawer, Position } from "@blueprintjs/core"; - -/* App dependencies */ -import InfoFormat, { SingleValues } from "./infoFormat"; -import { AppDispatch, RootState } from "../../reducers"; -import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers"; -import { DatasetMetadata } from "../../common/types/entities"; -import { Schema } from "../../common/types/schema"; -import { SingleContinuousValueState } from "../../reducers/singleContinuousValue"; - -/** - * Actions dispatched by info drawer. - */ -interface DispatchProps { - toggleDrawer: () => void; -} - -/** - * Props passed in from parent. - */ -interface OwnProps { - position?: Position; -} - -/** - * Props selected from store. - */ -interface StateProps { - datasetMetadata: DatasetMetadata; - isOpen: boolean; - schema: Schema; - singleContinuousValues: SingleContinuousValueState["singleContinuousValues"]; -} - -type Props = DispatchProps & OwnProps & StateProps; - -/** - * Map values selected from store to props. - */ -const mapStateToProps = (state: RootState): StateProps => ({ - datasetMetadata: state.datasetMetadata?.datasetMetadata, - isOpen: state.controls.datasetDrawer, - schema: state.annoMatrix?.schema, - singleContinuousValues: state.singleContinuousValue.singleContinuousValues, -}); - -/** - * Map actions dispatched by info drawer to props. - */ -const mapDispatchToProps = (dispatch: AppDispatch): DispatchProps => ({ - toggleDrawer: () => dispatch({ type: "toggle dataset drawer" }), -}); - -class InfoDrawer extends PureComponent { - handleClose = () => { - const { toggleDrawer } = this.props; - toggleDrawer(); - }; - - render(): JSX.Element { - const { - datasetMetadata, - position, - schema, - isOpen, - singleContinuousValues, - } = this.props; - - const allCategoryNames = selectableCategoryNames(schema).sort(); - const allSingleValues: SingleValues = new Map(); - - allCategoryNames.forEach((catName) => { - const isUserAnno = schema?.annotations?.obsByName[catName]?.writable; - const colSchema = schema.annotations.obsByName[catName]; - if (!isUserAnno && colSchema.categories?.length === 1) { - allSingleValues.set(catName, colSchema.categories[0]); - } - }); - singleContinuousValues.forEach((value, catName) => { - allSingleValues.set(catName, value); - }); - return ( - - - - ); - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(InfoDrawer); diff --git a/client/src/components/leftSidebar/index.tsx b/client/src/components/leftSidebar/index.tsx index 995cdbb90..74dae3d23 100644 --- a/client/src/components/leftSidebar/index.tsx +++ b/client/src/components/leftSidebar/index.tsx @@ -1,63 +1,18 @@ -/* Core dependencies */ -import React, { CSSProperties } from "react"; -import { connect } from "react-redux"; +import React from "react"; -/* App dependencies */ import Categorical from "../categorical"; -import * as globals from "../../globals"; -import DynamicScatterplot from "../scatterplot/scatterplot"; -import GeneInfo from "../geneExpression/geneInfo/geneInfo"; import Continuous from "../continuous/continuous"; -import { RootState } from "../../reducers"; +import { LeftSidebarContainer, LeftSidebarWrapper } from "./style"; -/* Styles */ -export const STYLE_LEFT_SIDEBAR: CSSProperties = { - /* x y blur spread color */ - borderRight: `1px solid ${globals.lightGrey}`, - display: "flex", - flexDirection: "column", - height: "100%", -}; - -interface Props { - scatterplotXXaccessor: string; - scatterplotYYaccessor: string; - geneIsOpen: boolean; -} - -const LeftSideBar = (props: Props) => { - const { scatterplotXXaccessor, scatterplotYYaccessor, geneIsOpen } = props; +function LeftSideBar() { return ( -
-
+ + -
- {scatterplotXXaccessor && scatterplotYYaccessor ? ( - - ) : null} - {geneIsOpen ? ( - - ) : null} -
+ + ); -}; +} -export default connect((state: RootState) => ({ - scatterplotXXaccessor: state.controls.scatterplotXXaccessor, - scatterplotYYaccessor: state.controls.scatterplotYYaccessor, - geneIsOpen: state.controls.geneIsOpen, -}))(LeftSideBar); +export default LeftSideBar; diff --git a/client/src/components/leftSidebar/leftSidebarSkeleton.tsx b/client/src/components/leftSidebar/leftSidebarSkeleton.tsx index dba306bbb..0ceb9aa65 100644 --- a/client/src/components/leftSidebar/leftSidebarSkeleton.tsx +++ b/client/src/components/leftSidebar/leftSidebarSkeleton.tsx @@ -3,9 +3,9 @@ import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes"; import React, { CSSProperties } from "react"; /* Styles */ -import { STYLE_LEFT_SIDEBAR } from "."; import StillLoading from "../brushableHistogram/loading"; import { StillLoading as CategoryLoading } from "../categorical/category"; +import { LeftSidebarWrapper } from "./style"; const STYLE_SUPER_CATEGORY: CSSProperties = { height: 22, @@ -19,7 +19,7 @@ const STYLE_SUPER_CATEGORY: CSSProperties = { */ function LeftSidebarSkeleton(): JSX.Element { return ( -
+ {/* Categorical */}
@@ -39,7 +39,7 @@ function LeftSidebarSkeleton(): JSX.Element { ))}
-
+
); } diff --git a/client/src/components/leftSidebar/style.ts b/client/src/components/leftSidebar/style.ts new file mode 100644 index 000000000..8899f8e23 --- /dev/null +++ b/client/src/components/leftSidebar/style.ts @@ -0,0 +1,15 @@ +import styled from "@emotion/styled"; +import * as globals from "../../globals"; + +export const LeftSidebarWrapper = styled.div` + border-right: 1px solid ${globals.lightGrey}; + display: flex; + flex-direction: column; + height: 100%; +`; + +export const LeftSidebarContainer = styled.div` + height: 100%; + width: ${globals.leftSidebarWidth}; + overflow-y: auto; +`; diff --git a/client/src/components/menubar/clip.tsx b/client/src/components/menubar/clip.tsx index 115d77bf7..49a165313 100644 --- a/client/src/components/menubar/clip.tsx +++ b/client/src/components/menubar/clip.tsx @@ -12,8 +12,6 @@ import { import { IconNames } from "@blueprintjs/icons"; import { tooltipHoverOpenDelay } from "../../globals"; -// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message -import styles from "./menubar.css"; import { track } from "../../analytics"; import { EVENTS } from "../../analytics/events"; @@ -56,7 +54,7 @@ const Clip = React.memo((props) => { }; return ( - + diffexpCellcountMax; return ( - + {/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */} {/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */} diff --git a/client/src/components/menubar/index.tsx b/client/src/components/menubar/index.tsx index 4cd775fec..2a36f0ccf 100644 --- a/client/src/components/menubar/index.tsx +++ b/client/src/components/menubar/index.tsx @@ -1,19 +1,16 @@ -import React from "react"; +import React, { cloneElement, useEffect, useRef, useState } from "react"; import { connect } from "react-redux"; -import { ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core"; +import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core"; import { IconNames } from "@blueprintjs/icons"; +import ResizeObserver from "resize-observer-polyfill"; import * as globals from "../../globals"; -// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message -import styles from "./menubar.css"; import actions from "../../actions"; import Clip from "./clip"; -import InfoDrawer from "../infoDrawer/infoDrawer"; import Subset from "./subset"; import DiffexpButtons from "./diffexpButtons"; import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers"; -import { selectIsSeamlessEnabled } from "../../selectors/datasetMetadata"; import { track } from "../../analytics"; import { EVENTS } from "../../analytics/events"; import Embedding from "../embedding"; @@ -21,54 +18,126 @@ import { getFeatureFlag } from "../../util/featureFlags/featureFlags"; import { FEATURES } from "../../util/featureFlags/features"; import { shouldShowOpenseadragon } from "../../common/selectors"; import { GRAPH_AS_IMAGE_TEST_ID } from "../../util/constants"; +import { AppDispatch, RootState } from "../../reducers"; +import { AnnoMatrixClipView } from "../../annoMatrix/views"; +import { + MenubarRightOverflowColumn, + StyledMenubar, + StyledMenubarRight, + StyledMenubarRightRow, +} from "./style"; + +interface StateProps { + subsetPossible: boolean; + subsetResetPossible: boolean; + graphInteractionMode: RootState["controls"]["graphInteractionMode"]; + clipPercentileMin: number; + clipPercentileMax: number; + colorAccessor: RootState["colors"]["colorAccessor"]; + disableDiffexp: boolean; + showCentroidLabels: RootState["centroidLabels"]["showLabels"]; + categoricalSelection: RootState["categoricalSelection"]; + screenCap: RootState["controls"]["screenCap"]; + imageUnderlay: RootState["controls"]["imageUnderlay"]; + // eslint-disable-next-line react/no-unused-prop-types -- used in shouldShowOpenseadragon + layoutChoice: RootState["layoutChoice"]; + // eslint-disable-next-line react/no-unused-prop-types -- used in shouldShowOpenseadragon + config: RootState["config"]; + // eslint-disable-next-line react/no-unused-prop-types -- used in shouldShowOpenseadragon + panelEmbedding: RootState["panelEmbedding"]; +} -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. -type State = any; - -// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message -@connect((state: State) => { +const mapStateToProps = (state: RootState): StateProps => { const { annoMatrix } = state; const crossfilter = state.obsCrossfilter; const selectedCount = crossfilter?.countSelected?.() || 0; const subsetPossible = - selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all and not none are selected + selectedCount !== 0 && selectedCount !== crossfilter?.size(); // ie, not all and not none are selected const embSubsetView = getEmbSubsetView(annoMatrix); const subsetResetPossible = !embSubsetView ? annoMatrix?.nObs !== annoMatrix?.schema.dataframe.nObs : annoMatrix?.nObs !== embSubsetView?.nObs; + const [clipRangeMin, clipRangeMax] = (annoMatrix as AnnoMatrixClipView) + ?.clipRange ?? [0, 1]; + return { subsetPossible, subsetResetPossible, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. + config: state.config, graphInteractionMode: state.controls.graphInteractionMode, - clipPercentileMin: Math.round(100 * (annoMatrix?.clipRange?.[0] ?? 0)), - clipPercentileMax: Math.round(100 * (annoMatrix?.clipRange?.[1] ?? 1)), - userDefinedGenes: state.quickGenes.userDefinedGenes, + clipPercentileMin: Math.round(100 * clipRangeMin), + clipPercentileMax: Math.round(100 * clipRangeMax), colorAccessor: state.colors.colorAccessor, - scatterplotXXaccessor: state.controls.scatterplotXXaccessor, - scatterplotYYaccessor: state.controls.scatterplotYYaccessor, - libraryVersions: state.config?.library_versions, - aboutLink: state.config?.links?.["about-dataset"], disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false, - diffexpMayBeSlow: - state.config?.parameters?.["diffexp-may-be-slow"] ?? false, showCentroidLabels: state.centroidLabels.showLabels, - tosURL: state.config?.parameters?.about_legal_tos, - privacyURL: state.config?.parameters?.about_legal_privacy, categoricalSelection: state.categoricalSelection, - seamlessEnabled: selectIsSeamlessEnabled(state), screenCap: state.controls.screenCap, imageUnderlay: state.controls.imageUnderlay, layoutChoice: state.layoutChoice, - config: state.config, + panelEmbedding: state.panelEmbedding, }; -}) -// eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS. -class MenuBar extends React.PureComponent<{}, State> { - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - static isValidDigitKeyEvent(e: any) { +}; + +interface DispatchProps { + dispatch: AppDispatch; +} +export type MenuBarProps = StateProps & DispatchProps; +interface State { + pendingClipPercentiles: { + clipPercentileMin: number | undefined; + clipPercentileMax: number | undefined; + }; +} + +const DIFFEXP_WIDTH = 150; +const BUTTON_WIDTH = 38; + +function MenuBar(props: MenuBarProps) { + const { + dispatch, + disableDiffexp, + clipPercentileMin, + clipPercentileMax, + graphInteractionMode, + showCentroidLabels, + categoricalSelection, + colorAccessor, + subsetPossible, + subsetResetPossible, + screenCap, + imageUnderlay, + } = props; + + const [pendingClipPercentiles, setPendingClipPercentiles] = useState< + State["pendingClipPercentiles"] + >({ + clipPercentileMin: undefined, + clipPercentileMax: undefined, + }); + + const [availableButtonSlots, setAvailableButtonSlots] = useState(8); + + const ref = useRef(null); + useEffect(() => { + const observer = new ResizeObserver((rightMenuBar) => { + setAvailableButtonSlots( + // Diffexp will never be popped into the column so ignore that when calculating width units available + (rightMenuBar[0].contentRect.width - DIFFEXP_WIDTH) / BUTTON_WIDTH + ); + }); + + if (ref.current) { + observer.observe(ref.current); + } + + return () => { + observer.disconnect(); + }; + }, []); + + function isValidDigitKeyEvent(e: any) { /* Return true if this event is necessary to enter a percent number input. Return false if not. @@ -92,155 +161,95 @@ class MenuBar extends React.PureComponent<{}, State> { return key >= 0 && key <= 9; } - // eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS. - constructor(props: {}) { - super(props); - // @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message - const { layoutChoice, dispatch } = this.props; - this.state = { - pendingClipPercentiles: null, - }; - const currentConditionMet = layoutChoice?.current?.includes( - globals.spatialEmbeddingKeyword - ); - // (seve): On some datasets, the app initially loads with a different layout selected, then switches to spatial. - // This triggers the componentDidUpdate, which toggles the image underlay. - // Other datasets correctly load with the spatial layout initially selected, but then don't trigger the componentDidUpdate. - if (currentConditionMet) { - dispatch({ - type: "toggle image underlay", - toggle: true, - }); - } - } - - componentDidUpdate(prevProps: any): void { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message - const { layoutChoice, dispatch } = this.props; - const prevConditionMet = prevProps.layoutChoice?.current?.includes( - globals.spatialEmbeddingKeyword - ); - const currentConditionMet = layoutChoice?.current?.includes( - globals.spatialEmbeddingKeyword - ); - - if (!prevConditionMet && currentConditionMet) { - dispatch({ - type: "toggle image underlay", - toggle: true, - }); - } - } - - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - isClipDisabled = () => { + const isClipDisabled = () => { /* return true if clip button should be disabled. */ - const { pendingClipPercentiles } = this.state; - const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin; - const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax; + const pendingClipPercentileMin = pendingClipPercentiles?.clipPercentileMin; + const pendingClipPercentileMax = pendingClipPercentiles?.clipPercentileMax; const { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'clipPercentileMin' does not exist on typ... Remove this comment to see the full error message clipPercentileMin: currentClipMin, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'clipPercentileMax' does not exist on typ... Remove this comment to see the full error message clipPercentileMax: currentClipMax, - } = this.props; + } = props; // if you change this test, be careful with logic around // comparisons between undefined / NaN handling. const isDisabled = - !(clipPercentileMin < clipPercentileMax) || - (clipPercentileMin === currentClipMin && - clipPercentileMax === currentClipMax); + !pendingClipPercentileMin || + !pendingClipPercentileMax || + !(pendingClipPercentileMin < pendingClipPercentileMax) || + (pendingClipPercentileMin === currentClipMin && + pendingClipPercentileMax === currentClipMax); return isDisabled; }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - handleClipOnKeyPress = (e: any) => { + const handleClipOnKeyPress = (e: any) => { /* allow only numbers, plus other critical keys which may be required to make a number */ - if (!MenuBar.isValidDigitKeyEvent(e)) { + if (!isValidDigitKeyEvent(e)) { e.preventDefault(); } }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - handleClipPercentileMinValueChange = (v: any) => { + const handleClipPercentileMinValueChange = (v: any) => { /* Ignore anything that isn't a legit number */ if (!Number.isFinite(v)) return; - const { pendingClipPercentiles } = this.state; - const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax; - /* clamp to [0, currentClipPercentileMax] */ if (v <= 0) v = 0; if (v > 100) v = 100; - const clipPercentileMin = Math.round(v); // paranoia - this.setState({ - pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }, + setPendingClipPercentiles({ + clipPercentileMin: Math.round(v), // paranoia + clipPercentileMax: pendingClipPercentiles?.clipPercentileMax, }); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. - handleClipPercentileMaxValueChange = (v: any) => { + const handleClipPercentileMaxValueChange = (v: any) => { /* Ignore anything that isn't a legit number */ if (!Number.isFinite(v)) return; - const { pendingClipPercentiles } = this.state; - const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin; - /* clamp to [0, 100] */ if (v < 0) v = 0; if (v > 100) v = 100; - const clipPercentileMax = Math.round(v); // paranoia - this.setState({ - pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }, + setPendingClipPercentiles({ + clipPercentileMin: pendingClipPercentiles?.clipPercentileMin, + clipPercentileMax: Math.round(v), // paranoia }); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleClipCommit = () => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message - const { dispatch } = this.props; - const { pendingClipPercentiles } = this.state; - const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles; - const min = clipPercentileMin / 100; - const max = clipPercentileMax / 100; + const handleClipCommit = () => { + const min = pendingClipPercentiles.clipPercentileMin! / 100; + const max = pendingClipPercentiles.clipPercentileMax! / 100; dispatch(actions.clipAction(min, max)); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleClipOpening = () => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'clipPercentileMin' does not exist on typ... Remove this comment to see the full error message - const { clipPercentileMin, clipPercentileMax } = this.props; - this.setState({ - pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }, + const handleClipOpening = () => { + setPendingClipPercentiles({ + clipPercentileMin, + clipPercentileMax, }); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleClipClosing = () => { - this.setState({ pendingClipPercentiles: null }); + const handleClipClosing = () => { + setPendingClipPercentiles({ + clipPercentileMax: undefined, + clipPercentileMin: undefined, + }); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleCentroidChange = () => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message - const { dispatch, showCentroidLabels } = this.props; - + const handleCentroidChange = () => { track(EVENTS.EXPLORER_CENTROID_LABEL_TOGGLE_BUTTON_CLICKED); dispatch({ @@ -249,266 +258,224 @@ class MenuBar extends React.PureComponent<{}, State> { }); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleSubset = () => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message - const { dispatch } = this.props; - + const handleSubset = () => { track(EVENTS.EXPLORER_SUBSET_BUTTON_CLICKED); dispatch(actions.subsetAction()); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - handleSubsetReset = () => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message - const { dispatch } = this.props; - + const handleSubsetReset = () => { track(EVENTS.EXPLORER_RESET_SUBSET_BUTTON_CLICKED); dispatch(actions.resetSubsetAction()); }; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types --- FIXME: disabled temporarily on migrate to TS. - render() { - const { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'dispatch' does not exist on type 'Readon... Remove this comment to see the full error message - dispatch, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'disableDiffexp' does not exist on type '... Remove this comment to see the full error message - disableDiffexp, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'selectionTool' does not exist on type 'R... Remove this comment to see the full error message - selectionTool, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'clipPercentileMin' does not exist on typ... Remove this comment to see the full error message - clipPercentileMin, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'clipPercentileMax' does not exist on typ... Remove this comment to see the full error message - clipPercentileMax, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'graphInteractionMode' does not exist on ... Remove this comment to see the full error message - graphInteractionMode, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'showCentroidLabels' does not exist on ty... Remove this comment to see the full error message - showCentroidLabels, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'categoricalSelection' does not exist on ... Remove this comment to see the full error message - categoricalSelection, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'colorAccessor' does not exist on type 'R... Remove this comment to see the full error message - colorAccessor, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'subsetPossible' does not exist on type '... Remove this comment to see the full error message - subsetPossible, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'subsetResetPossible' does not exist on t... Remove this comment to see the full error message - subsetResetPossible, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'subsetResetPossible' does not exist on t... Remove this comment to see the full error message - screenCap, - // @ts-expect-error ts-migrate(2339) FIXME: Property 'subsetResetPossible' does not exist on t... Remove this comment to see the full error message - imageUnderlay, - } = this.props; - const { pendingClipPercentiles } = this.state; - - const isColoredByCategorical = !!categoricalSelection?.[colorAccessor]; - - const isTest = getFeatureFlag(FEATURES.TEST); - const isDownload = getFeatureFlag(FEATURES.DOWNLOAD); - - // constants used to create selection tool button - const [selectionTooltip, selectionButtonIcon] = - selectionTool === "brush" - ? ["Brush selection", "Lasso selection"] - : ["select", "polygon-filter"]; - - return ( -
, + + + { + track(EVENTS.EXPLORER_MODE_LASSO_BUTTON_CLICKED); + + dispatch({ + type: "change graph interaction mode", + data: "select", + }); + }} + /> + + + { + track(EVENTS.EXPLORER_MODE_PAN_ZOOM_BUTTON_CLICKED); + + dispatch({ + type: "change graph interaction mode", + data: "zoom", + }); + }} + /> + + , + + + , + shouldShowOpenseadragon(props) && ( + + { + track( + /** + * (thuang): If `imageUnderlay` is currently `true`, then + * we're about to deselect it thus firing the deselection event. + */ + imageUnderlay + ? EVENTS.EXPLORER_IMAGE_DESELECT + : EVENTS.EXPLORER_IMAGE_SELECT + ); + dispatch({ + type: "toggle image underlay", + toggle: !imageUnderlay, + }); + }} + /> + + ), + , + isTest && ( + dispatch({ type: "test: screencap start" })} + /> + ), + isDownload && ( + -
- -
-
- - { - dispatch({ type: "toggle dataset drawer" }); - }} - style={{ - cursor: "pointer", - }} - data-testid="drawer" - /> - - {isDownload && ( - - dispatch({ type: "graph: screencap start" })} - /> - - )} - {isTest && ( - dispatch({ type: "test: screencap start" })} - /> - )} - dispatch({ type: "graph: screencap start" })} + /> + + ), + { + dispatch({ + type: "toggle active info panel", + activeTab: "Dataset", + }); + }} + style={{ + cursor: "pointer", + }} + data-testid="drawer" + />, + ]; + + let usedButtonSlots = 0; + let columnStartingIndex: null | number = null; + + return ( + + + + + {disableDiffexp ? null : } + {rightMenuBarComponents.map((component, i) => { + if (!component || columnStartingIndex !== null) { + return null; } - handleClipPercentileMinValueChange={ - this.handleClipPercentileMinValueChange + // if current component is a ButtonGroup component, increment the usedButtonSlots by 2 otherwise its a single button and increment by 1 + if (component.type === ButtonGroup) { + usedButtonSlots += 2; + } else { + usedButtonSlots += 1; } - /> - - - - {shouldShowOpenseadragon(this.props) && ( - - - { - track( - /** - * (thuang): If `imageUnderlay` is currently `true`, then - * we're about to deselect it thus firing the deselection event. - */ - imageUnderlay - ? EVENTS.EXPLORER_IMAGE_DESELECT - : EVENTS.EXPLORER_IMAGE_SELECT - ); - dispatch({ - type: "toggle image underlay", - toggle: !imageUnderlay, - }); - }} - /> - - - )} - - - { - track(EVENTS.EXPLORER_MODE_LASSO_BUTTON_CLICKED); - - dispatch({ - type: "change graph interaction mode", - data: "select", - }); - }} - /> - - - { - track(EVENTS.EXPLORER_MODE_PAN_ZOOM_BUTTON_CLICKED); - - dispatch({ - type: "change graph interaction mode", - data: "zoom", - }); - }} - /> - - - - {disableDiffexp ? null : } - -
-
- ); - } + + if (usedButtonSlots >= availableButtonSlots) { + columnStartingIndex = i; + return null; + } + return component; + })} + + + {columnStartingIndex !== null && + rightMenuBarComponents + .slice(columnStartingIndex) + .map((component) => { + if (component && component.type === ButtonGroup) { + return cloneElement(component, { vertical: true }); + } + return component; + })} + + + + ); } -export default MenuBar; +export default connect(mapStateToProps)(MenuBar); diff --git a/client/src/components/menubar/menubar.css b/client/src/components/menubar/menubar.css deleted file mode 100644 index 72b1ac348..000000000 --- a/client/src/components/menubar/menubar.css +++ /dev/null @@ -1,4 +0,0 @@ -:local(.menubarButton) { - margin-top: 8px; - margin-left: 8px; -} diff --git a/client/src/components/menubar/style.ts b/client/src/components/menubar/style.ts new file mode 100644 index 000000000..7acee0eb8 --- /dev/null +++ b/client/src/components/menubar/style.ts @@ -0,0 +1,38 @@ +import { Classes } from "@blueprintjs/core"; +import styled from "@emotion/styled"; + +export const StyledMenubar = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin-top: 8px; + width: 100%; + & .${Classes.BUTTON_GROUP} { + flex: 0 0 auto; + } +`; + +export const StyledMenubarRight = styled.div` + width: 40%; + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const StyledMenubarRightRow = styled.div` + display: flex; + flex-wrap: nowrap; + justify-content: flex-end; + gap: 8px; + width: 100%; +`; + +export const MenubarRightOverflowColumn = styled.div` + align-self: flex-end; + align-items: flex-end; + display: flex; + flex-direction: column; + height: fit-content; + width: fit-content; + gap: 8px; +`; diff --git a/client/src/components/menubar/subset.tsx b/client/src/components/menubar/subset.tsx index ca96329e2..d6398e204 100644 --- a/client/src/components/menubar/subset.tsx +++ b/client/src/components/menubar/subset.tsx @@ -1,7 +1,5 @@ import React from "react"; import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core"; -// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module './menubar.css' or its correspo... Remove this comment to see the full error message -import styles from "./menubar.css"; import * as globals from "../../globals"; const Subset = React.memo((props) => { @@ -17,7 +15,7 @@ const Subset = React.memo((props) => { } = props; return ( - + + + + + ); +} -const RightSidebar = () => ( -
- -
-); - -export default connect((state: RootState) => ({ - scatterplotXXaccessor: state.controls.scatterplotXXaccessor, - scatterplotYYaccessor: state.controls.scatterplotYYaccessor, -}))(RightSidebar); +export default RightSidebar; diff --git a/client/src/components/rightSidebar/rightSidebarSkeleton.tsx b/client/src/components/rightSidebar/rightSidebarSkeleton.tsx index 251d4e968..80655d1a2 100644 --- a/client/src/components/rightSidebar/rightSidebarSkeleton.tsx +++ b/client/src/components/rightSidebar/rightSidebarSkeleton.tsx @@ -1,9 +1,7 @@ /* Core dependencies */ import { SKELETON } from "@blueprintjs/core/lib/esnext/common/classes"; import React from "react"; - -/* Styles */ -import { STYLE_RIGHT_SIDEBAR } from "."; +import { RightSidebarWrapper } from "./style"; /** * Skeleton of right side bar, to be displayed during data load. @@ -11,7 +9,7 @@ import { STYLE_RIGHT_SIDEBAR } from "."; */ function RightSidebarSkeleton(): JSX.Element { return ( -
+ {/* Quick gene search */} {/* Gene menu */}
-
+ ); } diff --git a/client/src/components/rightSidebar/style.ts b/client/src/components/rightSidebar/style.ts new file mode 100644 index 000000000..838f2bef1 --- /dev/null +++ b/client/src/components/rightSidebar/style.ts @@ -0,0 +1,11 @@ +import styled from "@emotion/styled"; + +export const RightSidebarWrapper = styled.div` + border-left: 1px solid #e5e5e5; + display: flex; + flex-direction: column; + height: inherit; + overflow-y: inherit; + position: relative; + width: inherit; +`; diff --git a/client/src/components/scatterplot/scatterplot.tsx b/client/src/components/scatterplot/scatterplot.tsx index 113adc2a6..d60d03dc6 100644 --- a/client/src/components/scatterplot/scatterplot.tsx +++ b/client/src/components/scatterplot/scatterplot.tsx @@ -23,6 +23,7 @@ import { flagHighlight, } from "../../util/glHelpers"; import { Dataframe, DataframeColumn } from "../../util/dataframe"; +import { RootState } from "../../reducers"; // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. function createProjectionTF(viewportWidth: any, viewportHeight: any) { @@ -45,35 +46,27 @@ const getYScale = memoize(getScale); // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. type State = any; -// @ts-expect-error ts-migrate(1238) FIXME: Unable to resolve signature of class decorator whe... Remove this comment to see the full error message -@connect((state) => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'obsCrossfilter' does not exist on type '... Remove this comment to see the full error message +const mapStateToProps = (state: RootState) => { const { obsCrossfilter: crossfilter } = state; const { scatterplotXXaccessor, scatterplotYYaccessor, scatterplotIsMinimized, - scatterplotLevel, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - } = (state as any).controls; + } = state.controls; return { - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - annoMatrix: (state as any).annoMatrix, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - colors: (state as any).colors, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - pointDilation: (state as any).pointDilation, + annoMatrix: state.annoMatrix, + colors: state.colors, + pointDilation: state.pointDilation, // Accessors are var/gene names (strings) scatterplotXXaccessor, scatterplotYYaccessor, scatterplotIsMinimized, - scatterplotLevel, crossfilter, - // eslint-disable-next-line @typescript-eslint/no-explicit-any --- FIXME: disabled temporarily on migrate to TS. - genesets: (state as any).genesets.genesets, + genesets: state.genesets.genesets, }; -}) +}; + // eslint-disable-next-line @typescript-eslint/ban-types --- FIXME: disabled temporarily on migrate to TS. class Scatterplot extends React.PureComponent<{}, State> { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- - FIXME: disabled temporarily on migrate to TS. @@ -610,7 +603,7 @@ class Scatterplot extends React.PureComponent<{}, State> { } } -export default Scatterplot; +export default connect(mapStateToProps)(Scatterplot); const ScatterplotAxis = React.memo( ({ diff --git a/client/src/globals.ts b/client/src/globals.ts index 37a2f2942..17f94a732 100644 --- a/client/src/globals.ts +++ b/client/src/globals.ts @@ -160,6 +160,7 @@ export const leftSidebarSectionHeading = { letterSpacing: ".05em", }; export const leftSidebarSectionPadding = 10; +export const rightSidebarSectionPadding = 10; export const categoryLabelDisplayStringLongLength = 27; export const categoryLabelDisplayStringShortLength = 11; export const categoryDisplayStringMaxLength = 33; diff --git a/client/src/reducers/annoMatrix.ts b/client/src/reducers/annoMatrix.ts index cdf208e9f..a71d347c3 100644 --- a/client/src/reducers/annoMatrix.ts +++ b/client/src/reducers/annoMatrix.ts @@ -4,11 +4,14 @@ Reducer for the annoMatrix import { AnyAction } from "redux"; import AnnoMatrix from "../annoMatrix/annoMatrix"; +import { AnnoMatrixClipView } from "../annoMatrix/views"; + +export type AnnoMatrixState = AnnoMatrix | AnnoMatrixClipView; const AnnoMatrixReducer = ( - state: AnnoMatrix | null = null, + state: AnnoMatrixState, action: AnyAction -) => { +): AnnoMatrixState => { if (action.annoMatrix) { return action.annoMatrix; } diff --git a/client/src/reducers/annotations.ts b/client/src/reducers/annotations.ts deleted file mode 100644 index 3b9d62ef9..000000000 --- a/client/src/reducers/annotations.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* -Reducers for annotation UI-state. -*/ - -import { AnyAction } from "redux"; - -export interface AnnotationsState { - dataCollectionNameIsReadOnly: boolean; - dataCollectionName: string | null; - isEditingCategoryName: boolean; - isEditingLabelName: boolean; - categoryBeingEdited: string | null; - categoryAddingNewLabel: string | null; - labelEditable: { category: string | null; label: number | null }; - promptForFilename: boolean; -} -const Annotations = ( - state: AnnotationsState = { - /* - Annotations collection name - which will be used to save the named set of annotations - in some persistent store (database, file system, etc). - - The backend may expect this to be a legal file name, which is typically alpha-numeric, plus [_-,.]. - Keep it simple or the server may return an error. - - If `dataCollectionNameIsReadOnly` is true, you may NOT change the data collection name. - If false, you may change `dataCollectionName` and it will be used at the time the annotations are - written to the back-end. - - */ - dataCollectionNameIsReadOnly: true, - dataCollectionName: null, - - /* - Annotations UI component state - */ - isEditingCategoryName: false, - isEditingLabelName: false, - categoryBeingEdited: null, - categoryAddingNewLabel: null, - labelEditable: { category: null, label: null }, - promptForFilename: true, - }, - action: AnyAction -) => { - switch (action.type) { - case "configuration load complete": { - const dataCollectionName = - action.config.parameters?.["annotations-data-collection-name"] ?? null; - const dataCollectionNameIsReadOnly = - (action.config.parameters?.[ - "annotations-data-collection-is-read-only" - ] ?? - false) && - (action.config.parameters?.annotations_genesets_name_is_read_only ?? - true); - - const promptForFilename = - action.config.parameters?.user_annotation_collection_name_enabled; - return { - ...state, - dataCollectionNameIsReadOnly, - dataCollectionName, - promptForFilename, - }; - } - - case "set annotations collection name": { - if (state.dataCollectionNameIsReadOnly) { - throw new Error("data collection name is read only"); - } - return { - ...state, - dataCollectionName: action.data, - }; - } - - /* CATEGORY */ - case "annotation: activate add new label mode": { - return { - ...state, - isAddingNewLabel: true, - categoryAddingNewLabel: action.data, - }; - } - - case "annotation: disable add new label mode": { - return { - ...state, - isAddingNewLabel: false, - categoryAddingNewLabel: null, - }; - } - - case "annotation: activate category edit mode": { - return { - ...state, - isEditingCategoryName: true, - categoryBeingEdited: action.data, - }; - } - - case "annotation: disable category edit mode": { - return { - ...state, - isEditingCategoryName: false, - categoryBeingEdited: null, - }; - } - - /* LABEL */ - case "annotation: activate edit label mode": { - return { - ...state, - isEditingLabelName: true, - labelEditable: { - category: action.metadataField, - label: action.categoryIndex, - }, - }; - } - - case "annotation: cancel edit label mode": { - return { - ...state, - isEditingLabelName: false, - labelEditable: { category: null, label: null }, - }; - } - - default: - return state; - } -}; - -export default Annotations; diff --git a/client/src/reducers/cascade.ts b/client/src/reducers/cascade.ts index 1f355d41f..9b4b41d94 100644 --- a/client/src/reducers/cascade.ts +++ b/client/src/reducers/cascade.ts @@ -1,16 +1,21 @@ import { AnyAction, Reducer } from "redux"; -import type { RootState } from "."; +import { type RootState } from "."; -export type ReducerFunction = ( - prevStateForKey: RootState[keyof RootState], +export type ReducerFunction = ( + prevStateForKey: RootState[T] | undefined, action: AnyAction, nextState?: RootState, prevState?: RootState -) => RootState; +) => RootState[T]; + +type CascadedReducersMap = Map< + keyof RootState, + ReducerFunction +>; type CascadedReducers = - | [string, ReducerFunction][] - | Map; + | [keyof RootState, ReducerFunction][] + | CascadedReducersMap; export default function cascadeReducers(arg: CascadedReducers): Reducer { /** @@ -34,11 +39,12 @@ export default function cascadeReducers(arg: CascadedReducers): Reducer { * - reducers guaranteed to be called in order * - each reducer will receive shared objects */ - const reducers = arg instanceof Map ? arg : new Map(arg); - const reducerKeys = [...reducers.keys()]; + const reducers = + arg instanceof Map ? arg : (new Map(arg) as CascadedReducersMap); + const reducerKeys = [...reducers.keys()] as (keyof RootState)[]; - return (prevState: RootState, action: AnyAction) => { - const nextState: RootState = {}; + return (prevState: RootState, action: AnyAction): RootState => { + const nextState = {} as RootState; let stateChange = false; for (let i = 0, l = reducerKeys.length; i < l; i += 1) { const key = reducerKeys[i]; @@ -51,6 +57,7 @@ export default function cascadeReducers(arg: CascadedReducers): Reducer { nextState, prevState ); + // @ts-expect-error -- can't seem to convince TS that nextStateForKey is the correct type nextState[key] = nextStateForKey; stateChange = stateChange || nextStateForKey !== prevStateForKey; } diff --git a/client/src/reducers/centroidLabels.ts b/client/src/reducers/centroidLabels.ts index 5be992b60..706eb3301 100644 --- a/client/src/reducers/centroidLabels.ts +++ b/client/src/reducers/centroidLabels.ts @@ -32,7 +32,7 @@ const centroidLabels = ( // then clear the labels and make sure the toggle is off return { ...state, - showLabels: colorAccessor && showLabels, + showLabels: !!colorAccessor && showLabels, }; case "reset centroid labels": diff --git a/client/src/reducers/config.ts b/client/src/reducers/config.ts index 367880009..72c8f3591 100644 --- a/client/src/reducers/config.ts +++ b/client/src/reducers/config.ts @@ -22,7 +22,7 @@ const ConfigReducer = ( isDeepZoomSourceValid: true, }, action: AnyAction -) => { +): ConfigState => { switch (action.type) { case "initial data load start": return { diff --git a/client/src/reducers/controls.ts b/client/src/reducers/controls.ts index 87d02bd22..fad10a73b 100644 --- a/client/src/reducers/controls.ts +++ b/client/src/reducers/controls.ts @@ -1,50 +1,9 @@ import { AnyAction } from "redux"; -type Level = "top" | "bottom" | ""; - -interface StackLevels { - geneLevel: Level; - scatterplotLevel: Level; +export enum ActiveTab { + Gene = "Gene", + Dataset = "Dataset", } -/* logic for minimizing and maximizing pop-ups */ -const minimizeMaximizePopUps = ( - geneLevel: Level, - geneIsMinimized: boolean, - geneIsOpen: boolean, - scatterplotLevel: Level, - scatterplotIsMinimized: boolean, - scatterplotIsOpen: boolean -): StackLevels => { - if ( - geneIsMinimized && - geneIsOpen && - scatterplotIsMinimized && - scatterplotIsOpen - ) { - return { - geneLevel, - scatterplotLevel, - }; - } - if (!geneIsMinimized) { - return { - geneLevel: scatterplotIsMinimized && scatterplotIsOpen ? "top" : "bottom", - scatterplotLevel: - scatterplotIsMinimized && scatterplotIsOpen ? "bottom" : "", - }; - } - if (!scatterplotIsMinimized) { - return { - scatterplotLevel: geneIsMinimized && geneIsOpen ? "top" : "bottom", - geneLevel: geneIsMinimized && geneIsOpen ? "bottom" : "", - }; - } - return { - geneLevel: geneIsMinimized ? "bottom" : "top", - scatterplotLevel: scatterplotIsMinimized ? "bottom" : "top", - }; -}; - interface ControlsState { loading: boolean; error: Error | string | null; @@ -52,12 +11,8 @@ interface ControlsState { graphInteractionMode: "zoom" | "select"; scatterplotXXaccessor: string | false; scatterplotYYaccessor: string | false; - geneIsOpen: boolean; scatterplotIsMinimized: boolean; - geneIsMinimized: boolean; - scatterplotLevel: Level; scatterplotIsOpen: boolean; - geneLevel: Level; gene: string | null; infoError: string | null; graphRenderCounter: number; @@ -72,6 +27,9 @@ interface ControlsState { mountCapture: boolean; showWarningBanner: boolean; imageUnderlay: boolean; + activeTab: ActiveTab; + infoPanelHidden: boolean; + infoPanelMinimized: boolean; } const Controls = ( state: ControlsState = { @@ -83,16 +41,12 @@ const Controls = ( graphInteractionMode: "select", scatterplotXXaccessor: false, // just easier to read scatterplotYYaccessor: false, - geneIsOpen: false, scatterplotIsMinimized: false, - geneIsMinimized: false, geneUrl: "", geneSummary: "", geneSynonyms: [""], geneName: "", - scatterplotLevel: "", scatterplotIsOpen: false, - geneLevel: "", gene: null, infoError: null, graphRenderCounter: 0 /* integer as { @@ -180,117 +137,27 @@ const Controls = ( Gene Info *******************************/ case "open gene info": { - // if scatterplot is already open - if ( - !state.scatterplotIsMinimized && - state.scatterplotXXaccessor && - state.scatterplotYYaccessor - ) { - state.scatterplotIsMinimized = true; - state.geneIsMinimized = false; - } - - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - true, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - return { ...state, - geneIsOpen: true, gene: action.gene, geneUrl: action.url, geneSummary: action.summary, geneSynonyms: action.synonyms, geneName: action.name, - geneIsMinimized: false, - geneLevel: stackLevels.geneLevel, infoError: action.infoError, showWarningBanner: action.showWarningBanner, - scatterplotLevel: stackLevels.scatterplotLevel, }; } case "load gene info": { - // if scatterplot is already open - if ( - !state.scatterplotIsMinimized && - state.scatterplotXXaccessor && - state.scatterplotYYaccessor - ) { - state.scatterplotIsMinimized = true; - state.geneIsMinimized = false; - } - - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - true, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - return { ...state, - geneIsOpen: true, gene: action.gene, geneUrl: "", geneSummary: "", geneSynonyms: [""], geneName: "", - geneIsMinimized: false, - geneLevel: stackLevels.geneLevel, infoError: null, - scatterplotLevel: stackLevels.scatterplotLevel, - }; - } - - case "minimize/maximize gene info": { - state.geneIsMinimized = !state.geneIsMinimized; - if (!state.geneIsMinimized) { - state.scatterplotIsMinimized = true; - } - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - state.geneIsOpen, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - - return { - ...state, - geneIsMinimized: state.geneIsMinimized, - geneLevel: stackLevels.geneLevel, - scatterplotIsMinimized: state.scatterplotIsMinimized, - scatterplotLevel: stackLevels.scatterplotLevel, - infoError: state.infoError, - }; - } - - case "clear gene info": { - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - false, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - - return { - ...state, - geneIsOpen: false, - geneIsMinimized: false, - geneLevel: stackLevels.geneLevel, - scatterplotLevel: stackLevels.scatterplotLevel, - infoError: action.infoError, }; } @@ -298,119 +165,68 @@ const Controls = ( Scatterplot *******************************/ case "set scatterplot x": { - // gene info is already open - if ( - !state.geneIsMinimized && - state.geneIsOpen && - state.scatterplotYYaccessor - ) { - state.geneIsMinimized = true; + if (state.scatterplotYYaccessor) { state.scatterplotIsMinimized = false; state.scatterplotIsOpen = true; } - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - state.geneIsOpen, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - state.geneLevel = stackLevels.geneLevel; - state.scatterplotLevel = stackLevels.scatterplotLevel; - return { ...state, scatterplotXXaccessor: action.data, scatterplotIsMinimized: false, - scatterplotLevel: state.scatterplotLevel, }; } case "set scatterplot y": { - // gene info is already open - if ( - !state.geneIsMinimized && - state.geneIsOpen && - state.scatterplotXXaccessor - ) { - state.geneIsMinimized = true; + if (state.scatterplotXXaccessor) { state.scatterplotIsMinimized = false; state.scatterplotIsOpen = true; } - - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - state.geneIsOpen, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - state.geneLevel = stackLevels.geneLevel; - state.scatterplotLevel = stackLevels.scatterplotLevel; - return { ...state, scatterplotYYaccessor: action.data, scatterplotIsMinimized: false, - scatterplotLevel: state.scatterplotLevel, }; } case "minimize/maximize scatterplot": { state.scatterplotIsMinimized = !state.scatterplotIsMinimized; - if (!state.scatterplotIsMinimized) { - state.geneIsMinimized = true; - } - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - state.geneIsOpen, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - state.geneLevel = stackLevels.geneLevel; - state.scatterplotLevel = stackLevels.scatterplotLevel; return { ...state, scatterplotIsMinimized: state.scatterplotIsMinimized, - scatterplotLevel: state.scatterplotLevel, - geneIsMinimized: state.geneIsMinimized, }; } case "clear scatterplot": { state.scatterplotIsOpen = false; - const stackLevels = minimizeMaximizePopUps( - state.geneLevel, - state.geneIsMinimized, - state.geneIsOpen, - state.scatterplotLevel, - state.scatterplotIsMinimized, - state.scatterplotIsOpen - ); - state.geneLevel = stackLevels.geneLevel; - state.scatterplotLevel = stackLevels.scatterplotLevel; return { ...state, scatterplotXXaccessor: false, scatterplotYYaccessor: false, scatterplotIsMinimized: false, - scatterplotLevel: state.scatterplotLevel, }; } - /************************** - Dataset Drawer - **************************/ - case "toggle dataset drawer": - return { ...state, datasetDrawer: !state.datasetDrawer }; - + Info Panel + **************************/ + case "toggle active info panel": + return { + ...state, + activeTab: action.activeTab, + infoPanelHidden: false, + }; + case "close info panel": + return { + ...state, + infoPanelHidden: true, + }; + case "minimize/maximize info panel": + return { + ...state, + infoPanelMinimized: !state.infoPanelMinimized, + }; /************************** Screen Capture **************************/ diff --git a/client/src/reducers/genesets.ts b/client/src/reducers/genesets.ts index 6baa66306..6fa4cdf67 100644 --- a/client/src/reducers/genesets.ts +++ b/client/src/reducers/genesets.ts @@ -38,20 +38,20 @@ interface Geneset { export type Genesets = Map; -export interface State { +export interface GeneSetsState { initialized: boolean; lastTid?: number; genesets: Genesets; } const GeneSets = ( - state: State = { + state: GeneSetsState = { initialized: false, lastTid: undefined, genesets: new Map(), }, action: AnyAction -): State => { +): GeneSetsState => { switch (action.type) { case "geneset: initial load": { const { data } = action; diff --git a/client/src/reducers/genesetsUI.ts b/client/src/reducers/genesetsUI.ts index 3abfeaa11..50212695a 100644 --- a/client/src/reducers/genesetsUI.ts +++ b/client/src/reducers/genesetsUI.ts @@ -15,7 +15,7 @@ const GeneSetsUI = ( isAddingGenesToGeneset: false, }, action: AnyAction -) => { +): GeneSetsUIState => { switch (action.type) { /** * Activate interface for adding a new geneset diff --git a/client/src/reducers/index.ts b/client/src/reducers/index.ts index 379004e5b..99b7044e0 100644 --- a/client/src/reducers/index.ts +++ b/client/src/reducers/index.ts @@ -20,13 +20,13 @@ import colors from "./colors"; import differential from "./differential"; import layoutChoice from "./layoutChoice"; import controls from "./controls"; -import annotations from "./annotations"; import genesets from "./genesets"; import genesetsUI from "./genesetsUI"; import centroidLabels from "./centroidLabels"; -import pointDialation from "./pointDilation"; +import pointDilation from "./pointDilation"; import quickGenes from "./quickGenes"; import singleContinuousValue from "./singleContinuousValue"; +import panelEmbedding from "./panelEmbedding"; import { gcMiddleware as annoMatrixGC } from "../annoMatrix"; @@ -37,10 +37,10 @@ const AppReducer = undoable( ["config", config], ["annoMatrix", annoMatrix], ["obsCrossfilter", obsCrossfilter], - ["annotations", annotations], ["genesets", genesets], ["genesetsUI", genesetsUI], ["layoutChoice", layoutChoice], + ["panelEmbedding", panelEmbedding], ["singleContinuousValue", singleContinuousValue], ["categoricalSelection", categoricalSelection], ["continuousSelection", continuousSelection], @@ -50,9 +50,9 @@ const AppReducer = undoable( ["quickGenes", quickGenes], ["differential", differential], ["centroidLabels", centroidLabels], - ["pointDilation", pointDialation], + ["pointDilation", pointDilation], ["datasetMetadata", datasetMetadata], - ]), + ] as Parameters[0]), [ "annoMatrix", "obsCrossfilter", @@ -74,7 +74,7 @@ const AppReducer = undoable( const RootReducer: Reducer = (state: RootState, action: Action) => { // when a logout action is dispatched it will reset redux state if (action.type === "reset") { - state = undefined; + state = {} as RootState; } return AppReducer(state, action); @@ -82,7 +82,26 @@ const RootReducer: Reducer = (state: RootState, action: Action) => { const store = createStore(RootReducer, applyMiddleware(thunk, annoMatrixGC)); -export type RootState = ReturnType; +export type RootState = { + config: ReturnType; + annoMatrix: ReturnType; + obsCrossfilter: ReturnType; + genesets: ReturnType; + genesetsUI: ReturnType; + layoutChoice: ReturnType; + panelEmbedding: ReturnType; + singleContinuousValue: ReturnType; + categoricalSelection: ReturnType; + continuousSelection: ReturnType; + graphSelection: ReturnType; + colors: ReturnType; + controls: ReturnType; + quickGenes: ReturnType; + differential: ReturnType; + centroidLabels: ReturnType; + pointDilation: ReturnType; + datasetMetadata: ReturnType; +}; export type AppDispatch = ThunkDispatch; diff --git a/client/src/reducers/layoutChoice.ts b/client/src/reducers/layoutChoice.ts index 3a7130b63..1b4c914e5 100644 --- a/client/src/reducers/layoutChoice.ts +++ b/client/src/reducers/layoutChoice.ts @@ -8,10 +8,10 @@ about commonly used names. Preferentially, pick in the following order: 4. give up, use the first available */ -import type { Action, AnyAction } from "redux"; -import type { RootState } from "."; +import { type Action, type AnyAction } from "redux"; +import { type RootState } from "."; import { selectAvailableLayouts } from "../selectors/layoutChoice"; -import { selectSchema } from "../selectors/annoMatrix"; +import { getCurrentLayout } from "../util/layout"; export interface LayoutChoiceState { available: Array; @@ -52,23 +52,3 @@ const LayoutChoice = ( }; export default LayoutChoice; - -function getCurrentLayout( - state: RootState, - layoutChoice: string -): { - current: string; - currentDimNames: Array; -} { - const schema = selectSchema(state); - const currentDimNames = schema.layout.obsByName[layoutChoice].dims; - - return { current: layoutChoice, currentDimNames }; -} - -export function getBestDefaultLayout(layouts: Array): string { - const preferredNames = ["umap", "tsne", "pca"]; - const idx = preferredNames.findIndex((name) => layouts.indexOf(name) !== -1); - if (idx !== -1) return preferredNames[idx]; - return layouts[0]; -} diff --git a/client/src/reducers/obsCrossfilter.ts b/client/src/reducers/obsCrossfilter.ts index 2c637d1b7..66dd849c6 100644 --- a/client/src/reducers/obsCrossfilter.ts +++ b/client/src/reducers/obsCrossfilter.ts @@ -6,9 +6,9 @@ import { AnyAction } from "redux"; import { AnnoMatrixObsCrossfilter } from "../annoMatrix"; const ObsCrossfilter = ( - state: AnnoMatrixObsCrossfilter | null = null, + state: AnnoMatrixObsCrossfilter, action: AnyAction -) => { +): AnnoMatrixObsCrossfilter => { if (action.obsCrossfilter) { return action.obsCrossfilter; } diff --git a/client/src/reducers/panelEmbedding.ts b/client/src/reducers/panelEmbedding.ts new file mode 100644 index 000000000..0c3621f67 --- /dev/null +++ b/client/src/reducers/panelEmbedding.ts @@ -0,0 +1,75 @@ +import { type Action, type AnyAction } from "redux"; +import { type RootState } from "."; +import { getCurrentLayout } from "../util/layout"; +import { selectAvailableLayouts } from "../selectors/layoutChoice"; + +export interface PanelEmbeddingState { + layoutChoice: { + available: Array; + current: string; + currentDimNames: Array; + }; + open: boolean; + minimized: boolean; +} + +export interface LayoutChoiceAction extends Action { + layoutChoice: string; +} + +const panelEmbedding = ( + state: PanelEmbeddingState, + action: AnyAction, + nextSharedState: RootState +): PanelEmbeddingState => { + switch (action.type) { + case "initial data load complete": { + return { + ...state, + open: false, + minimized: false, + }; + } + + case "toggle panel embedding": { + return { + ...state, + open: !state.open, + layoutChoice: { + available: selectAvailableLayouts(nextSharedState), + ...getCurrentLayout( + nextSharedState, + nextSharedState.layoutChoice.current + ), + }, + minimized: false, + }; + } + + case "toggle minimize panel embedding": { + return { + ...state, + minimized: !state.minimized, + }; + } + + case "set panel embedding layout choice": { + const { layoutChoice } = action as LayoutChoiceAction; + + return { + ...state, + layoutChoice: { + ...state.layoutChoice, + ...getCurrentLayout(nextSharedState, layoutChoice), + }, + minimized: false, + }; + } + + default: { + return state; + } + } +}; + +export default panelEmbedding; diff --git a/client/src/reducers/pointDilation.ts b/client/src/reducers/pointDilation.ts index 4df9e91fe..e1736ec60 100644 --- a/client/src/reducers/pointDilation.ts +++ b/client/src/reducers/pointDilation.ts @@ -10,7 +10,7 @@ const initialState: PointDilationState = { categoryField: "", }; -const pointDialation = (state = initialState, action: AnyAction) => { +const pointDilation = (state = initialState, action: AnyAction) => { const { metadataField, label: categoryField } = action; switch (action.type) { @@ -35,4 +35,4 @@ const pointDialation = (state = initialState, action: AnyAction) => { } }; -export default pointDialation; +export default pointDilation; diff --git a/client/src/reducers/quickGenes.ts b/client/src/reducers/quickGenes.ts index 15b7743c3..70da4aa7b 100644 --- a/client/src/reducers/quickGenes.ts +++ b/client/src/reducers/quickGenes.ts @@ -72,8 +72,8 @@ const quickGenes = ( const isQuickGene = userDefinedGenes.includes(selection) || userDefinedGenes.includes(gene) || - userDefinedGenes.includes(scatterplotXXaccessor) || - userDefinedGenes.includes(scatterplotYYaccessor); + userDefinedGenes.includes(scatterplotXXaccessor || "") || + userDefinedGenes.includes(scatterplotYYaccessor || ""); switch (action.type) { case "continuous metadata histogram end": { diff --git a/client/src/selectors/annoMatrix.ts b/client/src/selectors/annoMatrix.ts index db91c87f7..7bda54507 100644 --- a/client/src/selectors/annoMatrix.ts +++ b/client/src/selectors/annoMatrix.ts @@ -1,18 +1,14 @@ /* App dependencies */ -import AnnoMatrix from "../annoMatrix/annoMatrix"; -import { AnnotationColumnSchema } from "../common/types/schema"; +import { type AnnotationColumnSchema } from "../common/types/schema"; import { type RootState } from "../reducers"; -export const selectAnnoMatrix = (state: RootState): AnnoMatrix => - state.annoMatrix; - /* Returns true if user defined category has been created indicating work is in progress. @param annoMatrix from state @returns boolean */ export const selectIsUserStateDirty = (state: RootState): boolean => { - const annoMatrix = selectAnnoMatrix(state); + const { annoMatrix } = state; return Boolean( annoMatrix?.schema.annotations.obs.columns.some( @@ -20,8 +16,3 @@ export const selectIsUserStateDirty = (state: RootState): boolean => { ) ); }; - -export function selectSchema(state: RootState) { - const annoMatrix = selectAnnoMatrix(state); - return annoMatrix?.schema; -} diff --git a/client/src/selectors/categoricalSelection.ts b/client/src/selectors/categoricalSelection.ts index 2638ae1df..6a71dcfee 100644 --- a/client/src/selectors/categoricalSelection.ts +++ b/client/src/selectors/categoricalSelection.ts @@ -2,18 +2,14 @@ import { RootState } from "../reducers"; import { CategoricalSelection } from "../util/stateManager/controlsHelpers"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- update typings once categoricalSelection reducer state is typed. -export const selectCategoricalSelection = (state: RootState): any => - state.categoricalSelection; - /* Returns true if categorical selection controls have been touched indicating work is in progress. @param categoricalSelection from state @returns boolean */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- update typings once categoricalSelection reducer state is typed. -export const selectIsUserStateDirty = (state: any): boolean => { - const categoricalSelection = selectCategoricalSelection(state); + +export const selectIsUserStateDirty = (state: RootState): boolean => { + const { categoricalSelection } = state; return Boolean(isCategoricalSelectionInProgress(categoricalSelection)); }; diff --git a/client/src/selectors/colors.ts b/client/src/selectors/colors.ts index 3e7e15f00..c3ec21d8a 100644 --- a/client/src/selectors/colors.ts +++ b/client/src/selectors/colors.ts @@ -1,16 +1,13 @@ /* App dependencies */ import { RootState } from "../reducers"; -import { ColorsState } from "../reducers/colors"; - -export const selectColors = (state: RootState): ColorsState => state.colors; /* Returns true if categorical metadata color selection controls have been touched indicating work is in progress. @param colors from state @returns boolean */ -export const selectIsUserStateDirty = (state: ColorsState): boolean => { - const colors = selectColors(state); +export const selectIsUserStateDirty = (state: RootState): boolean => { + const { colors } = state; return Boolean(colors.colorAccessor); }; diff --git a/client/src/selectors/config.ts b/client/src/selectors/config.ts index e1121244d..8c65b52dc 100644 --- a/client/src/selectors/config.ts +++ b/client/src/selectors/config.ts @@ -1,18 +1,20 @@ -import { RootState } from "../reducers"; +import { type ShouldShowOpenseadragonProps } from "../common/selectors"; import { ConfigState } from "../reducers/config"; -export function selectConfig(state: RootState): ConfigState { +export function selectConfig(state: ShouldShowOpenseadragonProps): ConfigState { return state.config; } -export function selectS3URI(state: RootState): ConfigState["s3URI"] { +export function selectS3URI( + state: ShouldShowOpenseadragonProps +): ConfigState["s3URI"] { const config = selectConfig(state); return config.s3URI; } export function selectIsDeepZoomSourceValid( - state: RootState + state: ShouldShowOpenseadragonProps ): ConfigState["isDeepZoomSourceValid"] { const config = selectConfig(state); diff --git a/client/src/selectors/continuousSelection.ts b/client/src/selectors/continuousSelection.ts index d28329bc9..b4e8dd8ca 100644 --- a/client/src/selectors/continuousSelection.ts +++ b/client/src/selectors/continuousSelection.ts @@ -1,20 +1,13 @@ /* App dependencies */ import { RootState } from "../reducers"; -import { ContinuousSelectionState } from "../reducers/continuousSelection"; - -export const selectContinuousSelection = ( - state: RootState -): ContinuousSelectionState => state.continuousSelection; /* Returns true if histogram brush controls have been touched indicating work is in progress. @param continuousSelection from state @returns boolean */ -export const selectIsUserStateDirty = ( - state: ContinuousSelectionState -): boolean => { - const continuousSelection = selectContinuousSelection(state); +export const selectIsUserStateDirty = (state: RootState): boolean => { + const { continuousSelection } = state; return Boolean(Object.keys(continuousSelection).length); }; diff --git a/client/src/selectors/controls.ts b/client/src/selectors/controls.ts index 6f2d0b78a..5e559bc61 100644 --- a/client/src/selectors/controls.ts +++ b/client/src/selectors/controls.ts @@ -1,17 +1,14 @@ /* App dependencies */ -import { RootState } from "../reducers"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any --- update typings once controls reducer state is typed. -export const selectControls = (state: RootState): any => state.controls; - /* Returns true if individual genes have been created indicating work is in progress. @param controls from state @returns boolean */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any --- update typings once controls reducer state is typed. -export const selectIsUserStateDirty = (state: any): boolean => { - const controls = selectControls(state); - return Boolean(controls?.userDefinedGenes?.length); +import { RootState } from "../reducers"; + +export const selectIsUserStateDirty = (state: RootState): boolean => { + const { quickGenes } = state; + + return Boolean(quickGenes?.userDefinedGenes?.length); }; diff --git a/client/src/selectors/genesets.ts b/client/src/selectors/genesets.ts index cc1e3cc56..cac9cc9bf 100644 --- a/client/src/selectors/genesets.ts +++ b/client/src/selectors/genesets.ts @@ -1,16 +1,14 @@ /* App dependencies */ -import { RootState } from "../reducers"; -import { State } from "../reducers/genesets"; -export const selectGeneSets = (state: RootState): State => state.genesets; +import { RootState } from "../reducers"; /* Returns true if genesets have been created indicating work is in progress. @param genesets from state @returns boolean */ -export const selectIsUserStateDirty = (state: State): boolean => { - const geneSets = selectGeneSets(state); +export const selectIsUserStateDirty = (state: RootState): boolean => { + const { genesets } = state; - return Boolean(geneSets?.genesets?.size); + return Boolean(genesets?.genesets?.size); }; diff --git a/client/src/selectors/layoutChoice.ts b/client/src/selectors/layoutChoice.ts index 37c6472f5..ca45393cb 100644 --- a/client/src/selectors/layoutChoice.ts +++ b/client/src/selectors/layoutChoice.ts @@ -1,11 +1,12 @@ import { EmbeddingSchema } from "../common/types/schema"; import { type RootState } from "../reducers"; -import { selectAnnoMatrix } from "./annoMatrix"; -export function selectAvailableLayouts(state: RootState): string[] { - const annoMatrix = selectAnnoMatrix(state); +export function selectAvailableLayouts(state: { + annoMatrix: RootState["annoMatrix"]; +}): string[] { + const { annoMatrix } = state; - return (annoMatrix.schema?.layout?.obs || []) + return annoMatrix.schema.layout.obs .map((v: EmbeddingSchema) => v.name) .sort(); } diff --git a/client/src/util/catLabelSort.ts b/client/src/util/catLabelSort.ts index 980a88743..e361288c5 100644 --- a/client/src/util/catLabelSort.ts +++ b/client/src/util/catLabelSort.ts @@ -9,7 +9,6 @@ TL;DR: sort order is: */ import isNumber from "is-number"; -import * as globals from "../globals"; import { Category } from "../common/types/schema"; function caseInsensitiveCompare( @@ -26,10 +25,7 @@ function isNumberForReal(tbd: unknown): tbd is number { return isNumber(tbd); } -const catLabelSort = ( - isUserAnno: boolean, - values: Array -): Array => { +const catLabelSort = (values: Array): Array => { /* this sort could be memoized for perf */ const strings: (string | number | boolean)[] = []; @@ -37,9 +33,7 @@ const catLabelSort = ( const unassignedOrNaN: (string | number | boolean)[] = []; values?.forEach((v) => { - if (isUserAnno && v === globals.unassignedCategoryLabel) { - unassignedOrNaN.push(v); - } else if (String(v).toLowerCase() === "nan") { + if (String(v).toLowerCase() === "nan") { unassignedOrNaN.push(v); } else if (isNumberForReal(v)) { ints.push(v); diff --git a/client/src/util/centroid.ts b/client/src/util/centroid.ts index b485a56a5..a0133b24b 100644 --- a/client/src/util/centroid.ts +++ b/client/src/util/centroid.ts @@ -1,7 +1,6 @@ import quantile from "./quantile"; import { memoize } from "./dataframe/util"; import { Dataframe, DataframeValue } from "./dataframe"; -import { unassignedCategoryLabel } from "../globals"; import { createCategorySummaryFromDfCol, isSelectableCategoryName, @@ -55,8 +54,7 @@ const getCoordinatesByLabel = ( ] as CategoricalAnnotationColumnSchema ); - const { isUserAnno, categoryValueIndices, categoryValueCounts } = - categorySummary; + const { categoryValueIndices, categoryValueCounts } = categorySummary; // Iterate over all cells for (let i = 0, len = categoryArray.length; i < len; i += 1) { @@ -71,10 +69,7 @@ const getCoordinatesByLabel = ( // labeled on the graph // If the user created this category, // do not create a coord for the `unassigned` label - if ( - labelIndex !== undefined && - !(isUserAnno && label === unassignedCategoryLabel) - ) { + if (labelIndex !== undefined) { // Create/fetch the scratchpad value let coords = coordsByCategoryLabel.get(label); if (coords === undefined) { diff --git a/client/src/util/layout.ts b/client/src/util/layout.ts new file mode 100644 index 000000000..07dc8d422 --- /dev/null +++ b/client/src/util/layout.ts @@ -0,0 +1,21 @@ +import { type RootState } from "../reducers"; + +export function getCurrentLayout( + state: RootState, + layoutChoice: string +): { + current: string; + currentDimNames: Array; +} { + const { schema } = state.annoMatrix; + const currentDimNames = schema.layout.obsByName[layoutChoice].dims; + + return { current: layoutChoice, currentDimNames }; +} + +export function getBestDefaultLayout(layouts: Array): string { + const preferredNames = ["umap", "tsne", "pca"]; + const idx = preferredNames.findIndex((name) => layouts.indexOf(name) !== -1); + if (idx !== -1) return preferredNames[idx]; + return layouts[0]; +} diff --git a/client/src/util/stateManager/annotationsHelpers.ts b/client/src/util/stateManager/annotationsHelpers.ts index 65c4308ac..a790f5cd5 100644 --- a/client/src/util/stateManager/annotationsHelpers.ts +++ b/client/src/util/stateManager/annotationsHelpers.ts @@ -3,7 +3,6 @@ Helper functions for user-editable annotations state management. See also reducers/annotations.js */ -import AnnoMatrix from "../../annoMatrix/annoMatrix"; import { Schema } from "../../common/types/schema"; import { Dataframe, LabelType } from "../dataframe"; @@ -36,17 +35,6 @@ export function isCategoricalAnnotation( return type === "string" || type === "boolean" || type === "categorical"; } -function _isUserAnnotation(schema: Schema, name: string): boolean { - return schema.annotations.obsByName[name]?.writable || false; -} - -export function isUserAnnotation( - annoMatrix: AnnoMatrix, - name: string -): boolean { - return _isUserAnnotation(annoMatrix.schema, name); -} - /** * @returns `true` if all rows as indicated by mask have the `colname` set to * label. False, if not. diff --git a/client/src/util/stateManager/controlsHelpers.ts b/client/src/util/stateManager/controlsHelpers.ts index 365cb7d73..c9db56df1 100644 --- a/client/src/util/stateManager/controlsHelpers.ts +++ b/client/src/util/stateManager/controlsHelpers.ts @@ -76,7 +76,6 @@ export interface CategorySummary { numCategoryValues: number; // Array of cardinality of each category, (top N) categoryValueCounts: number[]; - isUserAnno: boolean; } /** @@ -88,7 +87,6 @@ export function createCategorySummaryFromDfCol( dfCol: DataframeColumn, colSchema: CategoricalAnnotationColumnSchema ): CategorySummary { - const { writable: isUserAnno } = colSchema; const summary = dfCol.summarizeCategorical(); const { categories: allCategoryValues } = colSchema; const categoryValues = allCategoryValues; @@ -105,7 +103,6 @@ export function createCategorySummaryFromDfCol( categoryValueIndices, numCategoryValues, categoryValueCounts, - isUserAnno, }; }