From c5f9dd7dd6341c29ceb366c366661179c32badbd Mon Sep 17 00:00:00 2001 From: Timmy Huang Date: Tue, 18 Jun 2024 13:18:21 -0700 Subject: [PATCH 1/2] 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, }; } From 723105a7ee75a30f5e53e8154cba61819fff0926 Mon Sep 17 00:00:00 2001 From: Timmy Huang Date: Tue, 18 Jun 2024 16:13:06 -0700 Subject: [PATCH 2/2] test: side by side test fixes (#989) Co-authored-by: kaloster --- client/__tests__/e2e/cell-guide.test.ts | 4 - client/__tests__/e2e/cellxgeneActions.ts | 26 +- client/__tests__/e2e/data.ts | 4 + client/__tests__/e2e/e2e.test.ts | 2074 +++++++++-------- ...-breadcrumbs-appears-2-chromium-darwin.txt | 1 + ...m-breadcrumbs-appears-2-chromium-linux.txt | 1 + ...ctions-whole-diffexp-1-chromium-darwin.txt | 1 + ...actions-whole-diffexp-1-chromium-linux.txt | 1 + ...ctions-whole-diffexp-2-chromium-darwin.txt | 1 + ...actions-whole-diffexp-2-chromium-linux.txt | 1 + ...-breadcrumbs-appears-1-chromium-darwin.txt | 1 + ...m-breadcrumbs-appears-1-chromium-linux.txt | 1 + ...ctions-whole-diffexp-1-chromium-darwin.txt | 1 + ...actions-whole-diffexp-1-chromium-linux.txt | 1 + ...-breadcrumbs-appears-1-chromium-darwin.txt | 1 + ...m-breadcrumbs-appears-1-chromium-linux.txt | 1 + ...ctions-whole-diffexp-2-chromium-darwin.txt | 1 + ...actions-whole-diffexp-2-chromium-linux.txt | 1 + ...-breadcrumbs-appears-2-chromium-darwin.txt | 1 + ...m-breadcrumbs-appears-2-chromium-linux.txt | 1 + client/__tests__/util/helpers.ts | 31 + client/configuration/babel/babel.dev.js | 1 - client/configuration/babel/babel.prod.js | 1 - client/package-lock.json | 273 +-- client/package.json | 2 - client/playwright.config.ts | 2 +- client/src/common/selectors.ts | 14 +- client/src/components/embedding/index.tsx | 22 +- client/src/components/graph/graph.tsx | 30 + client/src/components/menubar/clip.tsx | 4 +- .../src/components/menubar/diffexpButtons.tsx | 4 +- client/src/components/menubar/index.tsx | 587 ++--- client/src/components/menubar/menubar.css | 4 + client/src/components/menubar/style.ts | 38 - client/src/components/menubar/subset.tsx | 4 +- client/src/reducers/panelEmbedding.ts | 10 +- 36 files changed, 1627 insertions(+), 1524 deletions(-) create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt create mode 100644 client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt create mode 100644 client/src/components/menubar/menubar.css delete mode 100644 client/src/components/menubar/style.ts diff --git a/client/__tests__/e2e/cell-guide.test.ts b/client/__tests__/e2e/cell-guide.test.ts index 7fb04454f..ee151b30c 100644 --- a/client/__tests__/e2e/cell-guide.test.ts +++ b/client/__tests__/e2e/cell-guide.test.ts @@ -32,10 +32,6 @@ describe("CellGuideCXG", () => { test("page launched", async ({ page }, testInfo) => { await goToPage(page, pageURLCellGuide); - const element = await page.getByTestId("header").innerHTML(); - - expect(element).toMatchSnapshot(); - await snapshotTestGraph(page, testInfo); }); diff --git a/client/__tests__/e2e/cellxgeneActions.ts b/client/__tests__/e2e/cellxgeneActions.ts index 1946fbe6e..06bf905bc 100644 --- a/client/__tests__/e2e/cellxgeneActions.ts +++ b/client/__tests__/e2e/cellxgeneActions.ts @@ -780,6 +780,8 @@ export async function assertUndoRedo( ); } +const WAIT_FOR_GRAPH_AS_IMAGE_TIMEOUT_MS = 10_000; + export async function snapshotTestGraph(page: Page, testInfo: TestInfo) { const imageID = "graph-image"; @@ -789,7 +791,13 @@ export async function snapshotTestGraph(page: Page, testInfo: TestInfo) { async () => { await page.getByTestId(GRAPH_AS_IMAGE_TEST_ID).click({ force: true }); - await page.getByTestId(imageID).waitFor(); + await page + .getByTestId(imageID) + /** + * (thuang): Without explicit `timeout` option, the default timeout is + * 3 minutes, which is too long for this test. + */ + .waitFor({ timeout: WAIT_FOR_GRAPH_AS_IMAGE_TIMEOUT_MS }); await takeSnapshot(page, testInfo); @@ -802,8 +810,18 @@ export async function snapshotTestGraph(page: Page, testInfo: TestInfo) { ); } -export async function selectLayout(layoutChoice: string, page: Page) { - await page.getByTestId(LAYOUT_CHOICE_TEST_ID).click(); +export async function selectLayout( + layoutChoice: string, + graphTsetId: string, + sidePanel: string, + page: Page +) { + let layoutChoiceTestId = LAYOUT_CHOICE_TEST_ID; + if (graphTsetId === sidePanel) { + layoutChoiceTestId = `${LAYOUT_CHOICE_TEST_ID}-side`; + } + + await page.getByTestId(layoutChoiceTestId).click(); /** * (thuang): For blueprint radio buttons, we need to tab first to go to the @@ -839,7 +857,7 @@ export async function selectLayout(layoutChoice: string, page: Page) { } } - await page.getByTestId(LAYOUT_CHOICE_TEST_ID).click(); + await page.getByTestId(layoutChoiceTestId).click(); await page.waitForTimeout(WAIT_FOR_SWITCH_LAYOUT_MS); } diff --git a/client/__tests__/e2e/data.ts b/client/__tests__/e2e/data.ts index e5e90d0ee..3b9a9d749 100644 --- a/client/__tests__/e2e/data.ts +++ b/client/__tests__/e2e/data.ts @@ -42,6 +42,7 @@ export const datasets = { { "coordinates-as-percent": { x1: 0.1, y1: 0.25, x2: 0.7, y2: 0.75 }, count: "930", + count_side: "1211", }, ], invalidLasso: [ @@ -113,6 +114,7 @@ export const datasets = { lasso: { "coordinates-as-percent": { x1: 0.3, y1: 0.3, x2: 0.5, y2: 0.5 }, count: "37", + count_side: "45", }, }, }, @@ -211,6 +213,7 @@ export const datasets = { { "coordinates-as-percent": { x1: 0.1, y1: 0.25, x2: 0.7, y2: 0.75 }, count: "2025", + count_side: "1775", }, ], invalidLasso: [ @@ -284,6 +287,7 @@ export const datasets = { lasso: { "coordinates-as-percent": { x1: 0.3, y1: 0.3, x2: 0.5, y2: 0.5 }, count: "221", + count_side: "221", }, }, }, diff --git a/client/__tests__/e2e/e2e.test.ts b/client/__tests__/e2e/e2e.test.ts index 5656b86c4..d08e5d0b8 100644 --- a/client/__tests__/e2e/e2e.test.ts +++ b/client/__tests__/e2e/e2e.test.ts @@ -62,7 +62,13 @@ import { testURL, pageURLSpatial, } from "../common/constants"; -import { goToPage } from "../util/helpers"; +import { + conditionallyToggleSidePanel, + goToPage, + shouldSkipTests, + skipIfSidePanel, + toggleSidePanel, +} from "../util/helpers"; import { SCALE_MAX } from "../../src/util/constants"; const { describe, skip } = test; @@ -101,8 +107,13 @@ test.beforeEach(mockSetup); const SPATIAL_DATASET = "super-cool-spatial.cxg"; +const MAIN_PANEL = "layout-graph"; +const SIDE_PANEL = "layout-graph-side"; + const testDatasets = [DATASET, SPATIAL_DATASET] as (keyof typeof datasets)[]; +const graphInstanceTestIds = ["layout-graph", "layout-graph-side"]; + const testURLs = { [DATASET]: testURL, [SPATIAL_DATASET]: pageURLSpatial, @@ -111,1240 +122,1355 @@ const testURLs = { for (const testDataset of testDatasets) { const data = datasets[testDataset]; const url = testURLs[testDataset]; + for (const graphTestId of graphInstanceTestIds) { + const shouldSkip = shouldSkipTests(graphTestId, SIDE_PANEL); + const describeFn = shouldSkip ? describe.skip : describe; - describe(`dataset: ${testDataset}`, () => { - describe("did launch", () => { - test("page launched", async ({ page }, testInfo) => { - await goToPage(page, url); + describe(`dataset: ${testDataset}`, () => { + describe(`graph instance: ${graphTestId}`, () => { + describeFn("did launch", () => { + test("page launched", async ({ page }, testInfo) => { + await goToPage(page, url); - const element = await page.getByTestId("header").innerHTML(); + await snapshotTestGraph(page, testInfo); + }); + }); - expect(element).toMatchSnapshot(); + describeFn("breadcrumbs loads", () => { + test("dataset and collection from breadcrumbs appears", async ({ + page, + }, testInfo) => { + await goToPage(page, url); - await snapshotTestGraph(page, testInfo); - }); - }); + const datasetElement = await page + .getByTestId("bc-Dataset") + .innerHTML(); + const collectionsElement = await page + .getByTestId("bc-Collection") + .innerHTML(); - describe("breadcrumbs loads", () => { - test("dataset and collection from breadcrumbs appears", async ({ - page, - }, testInfo) => { - await goToPage(page, url); + expect(datasetElement).toMatchSnapshot(); + expect(collectionsElement).toMatchSnapshot(); - const datasetElement = await page.getByTestId("bc-Dataset").innerHTML(); - const collectionsElement = await page - .getByTestId("bc-Collection") - .innerHTML(); + await snapshotTestGraph(page, testInfo); + }); - expect(datasetElement).toMatchSnapshot(); - expect(collectionsElement).toMatchSnapshot(); + test("datasets from breadcrumbs appears on clicking collections", async ({ + page, + }, testInfo) => { + await goToPage(page, url); - await snapshotTestGraph(page, testInfo); - }); + await page.getByTestId(`bc-Dataset`).click(); - test("datasets from breadcrumbs appears on clicking collections", async ({ - page, - }) => { - await goToPage(page, url); + await snapshotTestGraph(page, testInfo); + }); + }); - await page.getByTestId(`bc-Dataset`).click(); + describe("metadata loads", () => { + test("categories and values from dataset appear", async ({ + page, + }, testInfo) => { + await goToPage(page, url); + for (const label of Object.keys( + data.categorical + ) as (keyof typeof data.categorical)[]) { + await page.getByTestId(`${label}:category-expand`).click(); - const element = await page - .getByTestId("dataset-menu-item-Sed eu nisi condimentum") - .innerHTML(); + const categories = await getAllCategoriesAndCounts(label, page); - expect(element).toMatchSnapshot(); - }); - }); + expect(categories).toMatchObject(data.categorical[label]); - describe("metadata loads", () => { - test("categories and values from dataset appear", async ({ - page, - }, testInfo) => { - await goToPage(page, url); - for (const label of Object.keys( - data.categorical - ) as (keyof typeof data.categorical)[]) { - const element = await page - .getByTestId(`category-${label}`) - .innerHTML(); + await snapshotTestGraph(page, testInfo); + } + }); - expect(element).toMatchSnapshot(); + test("continuous data appears", async ({ page }, testInfo) => { + await goToPage(page, url); + for (const label of Object.keys(data.continuous)) { + await expect( + page.getByTestId(`histogram-${label}-plot`) + ).not.toHaveCount(0); - await page.getByTestId(`${label}:category-expand`).click(); + await snapshotTestGraph(page, testInfo); + } + }); + }); - const categories = await getAllCategoriesAndCounts(label, page); + test("resize graph", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - expect(categories).toMatchObject(data.categorical[label]); + await goToPage(page, url); await snapshotTestGraph(page, testInfo); - } - }); - test("continuous data appears", async ({ page }, testInfo) => { - await goToPage(page, url); - for (const label of Object.keys(data.continuous)) { - await expect( - page.getByTestId(`histogram-${label}-plot`) - ).not.toHaveCount(0); + await page.setViewportSize({ width: 600, height: 600 }); - await snapshotTestGraph(page, testInfo); - } - }); - }); - - test("resize graph", async ({ page }, testInfo) => { - await goToPage(page, url); - - await snapshotTestGraph(page, testInfo); - - await page.setViewportSize({ width: 600, height: 600 }); - - await page.waitForTimeout(WAIT_FOR_AFTER_RESIZE_MS); - - await snapshotTestGraph(page, testInfo); - }); + await page.waitForTimeout(WAIT_FOR_AFTER_RESIZE_MS); - describe("cell selection", () => { - test("selects all cells cellset 1", async ({ page }, testInfo) => { - await goToPage(page, url); - const cellCount = await getCellSetCount(1, page); + await snapshotTestGraph(page, testInfo); + }); - expect(cellCount).toBe(data.dataframe.nObs); - await snapshotTestGraph(page, testInfo); - }); + describe("cell selection", () => { + test("selects all cells cellset 1", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - test("selects all cells cellset 2", async ({ page }, testInfo) => { - await goToPage(page, url); - const cellCount = await getCellSetCount(2, page); + await goToPage(page, url); + const cellCount = await getCellSetCount(1, page); - expect(cellCount).toBe(data.dataframe.nObs); + expect(cellCount).toBe(data.dataframe.nObs); + await snapshotTestGraph(page, testInfo); + }); - await snapshotTestGraph(page, testInfo); - }); + test("selects all cells cellset 2", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - test("bug fix: invalid lasso cancels lasso overlay", async ({ - page, - }, testInfo) => { - await goToPage(page, url); + await goToPage(page, url); + const cellCount = await getCellSetCount(2, page); - for (const cellset of data.cellsets.invalidLasso) { - const cellset1 = await calcDragCoordinates( - "layout-graph", - cellset["coordinates-as-percent"], - page - ); + expect(cellCount).toBe(data.dataframe.nObs); - await drag({ - page, - testInfo, - testId: "layout-graph", - start: cellset1.start, - end: cellset1.end, - lasso: true, + await snapshotTestGraph(page, testInfo); }); - const cellCount = await getCellSetCount(1, page); + test("bug fix: invalid lasso cancels lasso overlay", async ({ + page, + }, testInfo) => { + await goToPage(page, url); - expect(cellCount).toBe(cellset.count); + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); - await snapshotTestGraph(page, testInfo); - } - }); + for (const cellset of data.cellsets.invalidLasso) { + const cellset1 = await calcDragCoordinates( + graphTestId, + cellset["coordinates-as-percent"], + page + ); - test("selects cells via lasso", async ({ page }, testInfo) => { - await goToPage(page, url); + await drag({ + page, + testInfo, + testId: graphTestId, + start: cellset1.start, + end: cellset1.end, + lasso: true, + }); - const originalCellCount = await getCellSetCount(1, page); + const cellCount = await getCellSetCount(1, page); - for (const cellset of data.cellsets.lasso) { - const cellset1 = await calcDragCoordinates( - "layout-graph", - cellset["coordinates-as-percent"], - page - ); + expect(cellCount).toBe(cellset.count); - await drag({ - page, - testInfo, - testId: "layout-graph", - start: cellset1.start, - end: cellset1.end, - lasso: true, + await snapshotTestGraph(page, testInfo); + } }); - expect(await getCellSetCount(1, page)).toBe(cellset.count); + test("selects cells via lasso", async ({ page }, testInfo) => { + // TODO: fix bug for side panel where subset1 doesn't reset after layout switch + skipIfSidePanel(graphTestId, MAIN_PANEL); - await snapshotTestGraph(page, testInfo); + await goToPage(page, url); - // switch layout should retain the selection - const { original, somethingElse } = data.embeddingChoice; + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); - await selectLayout(somethingElse, page); + const originalCellCount = await getCellSetCount(1, page); - expect(await getCellSetCount(1, page)).toBe(cellset.count); + for (const cellset of data.cellsets.lasso) { + const cellset1 = await calcDragCoordinates( + graphTestId, + cellset["coordinates-as-percent"], + page + ); - await snapshotTestGraph(page, testInfo); + await drag({ + page, + testInfo, + testId: graphTestId, + start: cellset1.start, + end: cellset1.end, + lasso: true, + }); - // switch back to original layout should reset the selection - await selectLayout(original, page); + const cellCount = + graphTestId === SIDE_PANEL ? cellset.count_side : cellset.count; - expect(await getCellSetCount(1, page)).toBe(originalCellCount); + expect(await getCellSetCount(1, page)).toBe(cellCount); - await snapshotTestGraph(page, testInfo); - } - }); + await snapshotTestGraph(page, testInfo); - test("selects cells via categorical", async ({ page }, testInfo) => { - await goToPage(page, url); - - for (const cellset of data.cellsets.categorical) { - await page.getByTestId(`${cellset.metadata}:category-expand`).click(); - await page - .getByTestId(`${cellset.metadata}:category-select`) - .click({ force: true }); - - for (const value of cellset.values) { - await page - .getByTestId( - `categorical-value-select-${cellset.metadata}-${value}` - ) - .click({ force: true }); - } + // switch layout should retain the selection + const { original, somethingElse } = data.embeddingChoice; - const cellCount = await getCellSetCount(1, page); + await selectLayout(somethingElse, graphTestId, SIDE_PANEL, page); - expect(cellCount).toBe(cellset.count); + expect(await getCellSetCount(1, page)).toBe(cellCount); - await snapshotTestGraph(page, testInfo); - } - }); + await snapshotTestGraph(page, testInfo); - test("selects cells via continuous", async ({ page }, testInfo) => { - await goToPage(page, url); - for (const cellset of data.cellsets.continuous) { - const histBrushableAreaId = `histogram-${cellset.metadata}-plot-brushable-area`; + // switch back to original layout should reset the selection + await selectLayout(original, graphTestId, SIDE_PANEL, page); - const coords = await calcDragCoordinates( - histBrushableAreaId, - cellset["coordinates-as-percent"], - page - ); + expect(await getCellSetCount(1, page)).toBe(originalCellCount); - await drag({ - page, - testInfo, - testId: histBrushableAreaId, - start: coords.start, - end: coords.end, + await snapshotTestGraph(page, testInfo); + } }); - const cellCount = await getCellSetCount(1, page); - - expect(cellCount).toBe(cellset.count); - - await snapshotTestGraph(page, testInfo); - } - }); - }); - - describe("subset", () => { - test("subset - cell count matches", async ({ page }, testInfo) => { - await goToPage(page, url); - for (const select of data.subset.cellset1) { - if (select.kind === "categorical") { - await selectCategory(select.metadata, select.values, page, true); - } - } - - await page.getByTestId("subset-button").click(); - - await assertCategoricalCounts(); + test("selects cells via categorical", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - // switch layout should retain the subset - const { original, somethingElse } = data.embeddingChoice; + await goToPage(page, url); - await selectLayout(somethingElse, page); + for (const cellset of data.cellsets.categorical) { + await page + .getByTestId(`${cellset.metadata}:category-expand`) + .click(); + await page + .getByTestId(`${cellset.metadata}:category-select`) + .click({ force: true }); - await assertCategoricalCounts(); + for (const value of cellset.values) { + await page + .getByTestId( + `categorical-value-select-${cellset.metadata}-${value}` + ) + .click({ force: true }); + } - await snapshotTestGraph(page, testInfo); + const cellCount = await getCellSetCount(1, page); - // switch back to original layout should retain the subset too - await selectLayout(original, page); + expect(cellCount).toBe(cellset.count); - await assertCategoricalCounts(); - - await snapshotTestGraph(page, testInfo); - - async function assertCategoricalCounts() { - for (const label of Object.keys( - data.subset.categorical - ) as (keyof typeof data.subset.categorical)[]) { - const categories = await getAllCategoriesAndCounts(label, page); + await snapshotTestGraph(page, testInfo); + } + }); - expect(categories).toMatchObject(data.subset.categorical[label]); + test("selects cells via continuous", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - await snapshotTestGraph(page, testInfo); - } - } - }); + await goToPage(page, url); + for (const cellset of data.cellsets.continuous) { + const histBrushableAreaId = `histogram-${cellset.metadata}-plot-brushable-area`; - test("subset - categories with zero cells are filtered out", async ({ - page, - }) => { - await goToPage(page, url); - const select = data.subset.cellset1[0]; - await selectCategory(select.metadata, select.values, page, true); - await page.getByTestId("subset-button").click(); - - const actualCategories = await getAllCategoriesAndCounts( - select.metadata, - page - ); - const actualCategoriesKeys = Object.keys(actualCategories).sort(); - const expectedCateogriesKeys = select.values.slice().sort(); - expect(actualCategoriesKeys).toEqual(expectedCateogriesKeys); - }); + const coords = await calcDragCoordinates( + histBrushableAreaId, + cellset["coordinates-as-percent"], + page + ); - test("lasso after subset", async ({ page }, testInfo) => { - await goToPage(page, url); - for (const select of data.subset.cellset1) { - if (select.kind === "categorical") { - await selectCategory(select.metadata, select.values, page, true); - } - } + await drag({ + page, + testInfo, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, + }); - await page.getByTestId("subset-button").click(); + const cellCount = await getCellSetCount(1, page); - const lassoSelection = await calcDragCoordinates( - "layout-graph", - data.subset.lasso["coordinates-as-percent"], - page - ); + expect(cellCount).toBe(cellset.count); - await drag({ - page, - testInfo, - testId: "layout-graph", - start: lassoSelection.start, - end: lassoSelection.end, - lasso: true, + await snapshotTestGraph(page, testInfo); + } + }); }); - const cellCount = await getCellSetCount(1, page); + describe("subset", () => { + test("subset - cell count matches", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - expect(cellCount).toBe(data.subset.lasso.count); - await snapshotTestGraph(page, testInfo); - }); - }); + await goToPage(page, url); + for (const select of data.subset.cellset1) { + if (select.kind === "categorical") { + await selectCategory( + select.metadata, + select.values, + page, + true + ); + } + } - describe("clipping", () => { - test("clip continuous", async ({ page }, testInfo) => { - await goToPage(page, url); - await clip(data.clip.min, data.clip.max, page); - const histBrushableAreaId = `histogram-${data.clip.metadata}-plot-brushable-area`; - const coords = await calcDragCoordinates( - histBrushableAreaId, - data.clip["coordinates-as-percent"], - page - ); - - await drag({ - page, - testInfo, - testId: histBrushableAreaId, - start: coords.start, - end: coords.end, - }); + await page.getByTestId("subset-button").click(); - const cellCount = await getCellSetCount(1, page); - expect(cellCount).toBe(data.clip.count); + await assertCategoricalCounts(); - // ensure categorical data appears properly - for (const label of Object.keys( - data.categorical - ) as (keyof typeof data.categorical)[]) { - const element = await page - .getByTestId(`category-${label}`) - .innerHTML(); + // switch layout should retain the subset + const { original, somethingElse } = data.embeddingChoice; - expect(element).toMatchSnapshot(); + await selectLayout(somethingElse, graphTestId, SIDE_PANEL, page); - await page.getByTestId(`${label}:category-expand`).click(); + await assertCategoricalCounts(); - const categories = await getAllCategoriesAndCounts(label, page); + await snapshotTestGraph(page, testInfo); - expect(categories).toMatchObject(data.categorical[label]); + // switch back to original layout should retain the subset too + await selectLayout(original, graphTestId, SIDE_PANEL, page); - await snapshotTestGraph(page, testInfo); - } - }); - }); - - // interact with UI elements just that they do not break - describe("ui elements don't error", () => { - test("color by", async ({ page }, testInfo) => { - await goToPage(page, url); - const allLabels = [ - ...Object.keys(data.categorical), - ...Object.keys(data.continuous), - ]; + await assertCategoricalCounts(); - for (const label of allLabels) { - await page.getByTestId(`colorby-${label}`).click(); - } - await snapshotTestGraph(page, testInfo); - }); + await snapshotTestGraph(page, testInfo); - test("pan and zoom", async ({ page }, testInfo) => { - await goToPage(page, url); - await page.getByTestId("mode-pan-zoom").click(); - const panCoords = await calcDragCoordinates( - "layout-graph", - data.pan["coordinates-as-percent"], - page - ); + async function assertCategoricalCounts() { + for (const label of Object.keys( + data.subset.categorical + ) as (keyof typeof data.subset.categorical)[]) { + const categories = await getAllCategoriesAndCounts(label, page); - await drag({ - page, - testInfo, - testId: "layout-graph", - start: panCoords.start, - end: panCoords.end, - }); + expect(categories).toMatchObject( + data.subset.categorical[label] + ); - await page.evaluate("window.scrollBy(0, 1000);"); + await snapshotTestGraph(page, testInfo); + } + } + }); - await snapshotTestGraph(page, testInfo); + test("subset - categories with zero cells are filtered out", async ({ + page, + }) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - // switch layout and back should reset pan and zoom - const { original, somethingElse } = data.embeddingChoice; + await goToPage(page, url); + const select = data.subset.cellset1[0]; + await selectCategory(select.metadata, select.values, page, true); + await page.getByTestId("subset-button").click(); - await selectLayout(somethingElse, page); + const actualCategories = await getAllCategoriesAndCounts( + select.metadata, + page + ); + const actualCategoriesKeys = Object.keys(actualCategories).sort(); + const expectedCateogriesKeys = select.values.slice().sort(); + expect(actualCategoriesKeys).toEqual(expectedCateogriesKeys); + }); - await snapshotTestGraph(page, testInfo); + test("lasso after subset", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - await selectLayout(original, page); + await goToPage(page, url); + for (const select of data.subset.cellset1) { + if (select.kind === "categorical") { + await selectCategory( + select.metadata, + select.values, + page, + true + ); + } + } - await snapshotTestGraph(page, testInfo); - }); + await page.getByTestId("subset-button").click(); - test("home button", async ({ page }, testInfo) => { - skip(testDataset !== SPATIAL_DATASET, "Only run on spatial dataset"); + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); - await goToPage(page, url); + const lassoSelection = await calcDragCoordinates( + graphTestId, + data.subset.lasso["coordinates-as-percent"], + page + ); - const homeButton = page.getByText("Re-center Embedding"); + await drag({ + page, + testInfo, + testId: graphTestId, + start: lassoSelection.start, + end: lassoSelection.end, + lasso: true, + }); - await expect(homeButton).not.toBeVisible(); + const cellCount = await getCellSetCount(1, page); - const { - homeButton: { pan }, - } = data; + expect(cellCount).toBe(data.subset.lasso.count); + await snapshotTestGraph(page, testInfo); + }); + }); - const { start, end } = await calcDragCoordinates( - "layout-graph", - pan["coordinates-as-percent"], - page - ); + describe("clipping", () => { + test("clip continuous", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - await page.getByTestId("mode-pan-zoom").click(); + await goToPage(page, url); + await clip(data.clip.min, data.clip.max, page); + const histBrushableAreaId = `histogram-${data.clip.metadata}-plot-brushable-area`; + const coords = await calcDragCoordinates( + histBrushableAreaId, + data.clip["coordinates-as-percent"], + page + ); - await tryUntil( - async () => { await drag({ page, testInfo, - testId: "layout-graph", - start, - end, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, }); - /** - * (thuang): We need to do the same drag twice, since the first drag - * doesn't move the image completely out of the viewport - */ - await drag({ - page, - testInfo, - testId: "layout-graph", - start, - end, - }); + const cellCount = await getCellSetCount(1, page); + expect(cellCount).toBe(data.clip.count); - await expect(homeButton).toBeVisible({ - timeout: CHECK_HOME_BUTTON_MS, - }); - }, - { page } - ); + // ensure categorical data appears properly + for (const label of Object.keys( + data.categorical + ) as (keyof typeof data.categorical)[]) { + await page.getByTestId(`${label}:category-expand`).click(); - await homeButton.click(); + const categories = await getAllCategoriesAndCounts(label, page); - await tryUntil( - async () => { - await expect(homeButton).not.toBeVisible(); - }, - { page } - ); - }); - }); + expect(categories).toMatchObject(data.categorical[label]); - describe("centroid labels", () => { - test("labels are created", async ({ page }, testInfo) => { - await goToPage(page, url); - const labels = Object.keys( - data.categorical - ) as (keyof typeof data.categorical)[]; - - await page.getByTestId(`colorby-${labels[0]}`).click(); - await page.getByTestId("centroid-label-toggle").click(); - - // Toggle colorby for each category and check to see if labels are generated - for (let i = 0, { length } = labels; i < length; i += 1) { - const label = labels[i]; - // first label is already enabled - if (i !== 0) await page.getByTestId(`colorby-${label}`).click(); - const generatedLabels = await page - .getByTestId("centroid-label") - .all(); - - // Number of labels generated should be equal to size of the object - expect(generatedLabels).toHaveLength( - Object.keys(data.categorical[label]).length - ); + await snapshotTestGraph(page, testInfo); + } + }); + }); - await snapshotTestGraph(page, testInfo); - } - }); - }); + // interact with UI elements just that they do not break + describe("ui elements don't error", () => { + test("color by", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); + + await goToPage(page, url); + const allLabels = [ + ...Object.keys(data.categorical), + ...Object.keys(data.continuous), + ]; + + for (const label of allLabels) { + await page.getByTestId(`colorby-${label}`).click(); + } + await snapshotTestGraph(page, testInfo); + }); + + test("pan and zoom", async ({ page }, testInfo) => { + await goToPage(page, url); + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); + + await page.getByTestId("mode-pan-zoom").click(); + const panCoords = await calcDragCoordinates( + graphTestId, + data.pan["coordinates-as-percent"], + page + ); - describe("graph overlay", () => { - test("transform centroids correctly", async ({ page }, testInfo) => { - await goToPage(page, url); - const category = Object.keys( - data.categorical - )[0] as keyof typeof data.categorical; - - await page.getByTestId(`colorby-${category}`).click(); - await page.getByTestId("centroid-label-toggle").click(); - await page.getByTestId("mode-pan-zoom").click(); - - const panCoords = await calcDragCoordinates( - "layout-graph", - data.pan["coordinates-as-percent"], - page - ); - - const categoryValue = Object.keys(data.categorical[category])[0]; - const initialCoordinates = await getElementCoordinates( - `centroid-label`, - categoryValue, - page - ); - - await tryUntil( - async () => { await drag({ page, testInfo, - testId: "layout-graph", + testId: graphTestId, start: panCoords.start, end: panCoords.end, }); - const terminalCoordinates = await getElementCoordinates( - `centroid-label`, - categoryValue, - page - ); - - expect(terminalCoordinates[0] - initialCoordinates[0]).toBeCloseTo( - panCoords.end.x - panCoords.start.x - ); - expect(terminalCoordinates[1] - initialCoordinates[1]).toBeCloseTo( - panCoords.end.y - panCoords.start.y - ); - }, - { page } - ); - - await snapshotTestGraph(page, testInfo); - }); - }); - - test("zoom limit is 12x", async ({ page }, testInfo) => { - await goToPage(page, url); - const category = Object.keys( - data.categorical - )[0] as keyof typeof data.categorical; - - await page.getByTestId(`colorby-${category}`).click(); - await page.getByTestId("centroid-label-toggle").click(); - await page.getByTestId("mode-pan-zoom").click(); - - const categoryValue = Object.keys(data.categorical[category])[0]; - const initialCoordinates = await getElementCoordinates( - `centroid-label`, - categoryValue, - page - ); - - await tryUntil( - async () => { - await scroll({ - testId: "layout-graph", - deltaY: -10000, - coords: initialCoordinates, - page, - }); + await page.evaluate("window.scrollBy(0, 1000);"); - const newGraph = page.getByTestId("graph-wrapper"); - const newDistance = - (await newGraph.getAttribute("data-camera-distance")) ?? "-1"; - expect(parseFloat(newDistance)).toBe(SCALE_MAX); - }, - { page } - ); + await snapshotTestGraph(page, testInfo); - await snapshotTestGraph(page, testInfo); - }); + // switch layout and back should reset pan and zoom + const { original, somethingElse } = data.embeddingChoice; - test("pan zoom mode resets lasso selection", async ({ page }, testInfo) => { - await goToPage(page, url); + await selectLayout(somethingElse, graphTestId, SIDE_PANEL, page); - await tryUntil( - async () => { - const panzoomLasso = data.features.panzoom.lasso; + await snapshotTestGraph(page, testInfo); - const lassoSelection = await calcDragCoordinates( - "layout-graph", - panzoomLasso["coordinates-as-percent"], - page - ); + await selectLayout(original, graphTestId, SIDE_PANEL, page); - await drag({ - page, - testInfo, - testId: "layout-graph", - start: lassoSelection.start, - end: lassoSelection.end, - lasso: true, + await snapshotTestGraph(page, testInfo); }); - await expect(page.getByTestId("lasso-element")).toBeVisible(); + test("home button", async ({ page }, testInfo) => { + skip( + testDataset !== SPATIAL_DATASET, + "Only run on spatial dataset" + ); - const initialCount = await getCellSetCount(1, page); + await goToPage(page, url); - expect(initialCount).toBe(panzoomLasso.count); + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); - await page.getByTestId("mode-pan-zoom").click(); - await page.getByTestId("mode-lasso").click(); + const homeButton = page.getByText("Re-center Embedding"); - const modeSwitchCount = await getCellSetCount(1, page); + await expect(homeButton).not.toBeVisible(); - expect(modeSwitchCount).toBe(initialCount); - }, - { page } - ); + const { + homeButton: { pan }, + } = data; - await snapshotTestGraph(page, testInfo); - }); + const { start, end } = await calcDragCoordinates( + graphTestId, + pan["coordinates-as-percent"], + page + ); - test("lasso moves after pan", async ({ page }, testInfo) => { - await goToPage(page, url); + await page.getByTestId("mode-pan-zoom").click(); + + await tryUntil( + async () => { + await drag({ + page, + testInfo, + testId: graphTestId, + start, + end, + }); + + /** + * (thuang): We need to do the same drag twice, since the first drag + * doesn't move the image completely out of the viewport + */ + await drag({ + page, + testInfo, + testId: graphTestId, + start, + end, + }); + + await expect(homeButton).toBeVisible({ + timeout: CHECK_HOME_BUTTON_MS, + }); + }, + { page } + ); - await tryUntil( - async () => { - const panzoomLasso = data.features.panzoom.lasso; - const coordinatesAsPercent = panzoomLasso["coordinates-as-percent"]; + await homeButton.click(); - const lassoSelection = await calcDragCoordinates( - "layout-graph", - coordinatesAsPercent, - page - ); + await tryUntil( + async () => { + await expect(homeButton).not.toBeVisible(); + }, + { page } + ); + }); + }); - await snapshotTestGraph(page, testInfo); + describe("centroid labels", () => { + test("labels are created", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); + + await goToPage(page, url); + const labels = Object.keys( + data.categorical + ) as (keyof typeof data.categorical)[]; + + await page.getByTestId(`colorby-${labels[0]}`).click(); + await page.getByTestId("centroid-label-toggle").click(); + + // Toggle colorby for each category and check to see if labels are generated + for (let i = 0, { length } = labels; i < length; i += 1) { + const label = labels[i]; + // first label is already enabled + if (i !== 0) await page.getByTestId(`colorby-${label}`).click(); + const generatedLabels = await page + .getByTestId("centroid-label") + .all(); + + // Number of labels generated should be equal to size of the object + expect(generatedLabels).toHaveLength( + Object.keys(data.categorical[label]).length + ); - await drag({ - page, - testInfo, - testId: "layout-graph", - start: lassoSelection.start, - end: lassoSelection.end, - lasso: true, + await snapshotTestGraph(page, testInfo); + } }); + }); - await snapshotTestGraph(page, testInfo); - - await expect(page.getByTestId("lasso-element")).toBeVisible(); + describe("graph overlay", () => { + test("transform centroids correctly", async ({ page }, testInfo) => { + skipIfSidePanel(graphTestId, MAIN_PANEL); - const initialCount = await getCellSetCount(1, page); + await goToPage(page, url); + const category = Object.keys( + data.categorical + )[0] as keyof typeof data.categorical; - expect(initialCount).toBe(panzoomLasso.count); + await page.getByTestId(`colorby-${category}`).click(); + await page.getByTestId("centroid-label-toggle").click(); + await page.getByTestId("mode-pan-zoom").click(); - await page.getByTestId("mode-pan-zoom").click(); + const panCoords = await calcDragCoordinates( + graphTestId, + data.pan["coordinates-as-percent"], + page + ); - await snapshotTestGraph(page, testInfo); + const categoryValue = Object.keys(data.categorical[category])[0]; + const initialCoordinates = await getElementCoordinates( + `centroid-label`, + categoryValue, + page + ); - const panCoords = await calcDragCoordinates( - "layout-graph", - coordinatesAsPercent, - page - ); + await tryUntil( + async () => { + await drag({ + page, + testInfo, + testId: graphTestId, + start: panCoords.start, + end: panCoords.end, + }); + + const terminalCoordinates = await getElementCoordinates( + `centroid-label`, + categoryValue, + page + ); + + expect( + terminalCoordinates[0] - initialCoordinates[0] + ).toBeCloseTo(panCoords.end.x - panCoords.start.x); + expect( + terminalCoordinates[1] - initialCoordinates[1] + ).toBeCloseTo(panCoords.end.y - panCoords.start.y); + }, + { page } + ); - await drag({ - page, - testInfo, - testId: "layout-graph", - start: panCoords.start, - end: panCoords.end, - lasso: true, + await snapshotTestGraph(page, testInfo); }); + }); - await snapshotTestGraph(page, testInfo); - - await page.getByTestId("mode-lasso").click(); - - const panCount = await getCellSetCount(2, page); - - expect(panCount).toBe(initialCount); - - await snapshotTestGraph(page, testInfo); - }, - { page } - ); - }); + test("zoom limit is 12x", async ({ page }, testInfo) => { + await goToPage(page, url); + const category = Object.keys( + data.categorical + )[0] as keyof typeof data.categorical; + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); - /* - Tests included below are specific to annotation features - */ + await page.getByTestId(`colorby-${category}`).click(); + await page.getByTestId("centroid-label-toggle").click(); + await page.getByTestId("mode-pan-zoom").click(); - const options = [ - { withSubset: true, tag: "subset" }, - { withSubset: false, tag: "whole" }, - ]; - - for (const option of options) { - describe(`geneSET crud operations and interactions ${option.tag}`, () => { - test("brush on geneset mean", async ({ page }, testInfo) => { - await setup({ option, page, url, testInfo }); - await createGeneset(meanExpressionBrushGenesetName, page); - await addGeneToSetAndExpand( - meanExpressionBrushGenesetName, - "SIK1", + const categoryValue = Object.keys(data.categorical[category])[0]; + const initialCoordinates = await getElementCoordinates( + `centroid-label`, + categoryValue, page ); - const histBrushableAreaId = `histogram-${meanExpressionBrushGenesetName}-plot-brushable-area`; + await tryUntil( + async () => { + await scroll({ + testId: graphTestId, + deltaY: -10000, + coords: initialCoordinates, + page, + }); - const coords = await calcDragCoordinates( - histBrushableAreaId, - { - x1: 0.1, - y1: 0.5, - x2: 0.9, - y2: 0.5, + const newGraph = page.getByTestId("graph-wrapper"); + const newDistance = + (await newGraph.getAttribute("data-camera-distance")) ?? "-1"; + expect(parseFloat(newDistance)).toBe(SCALE_MAX); }, - page - ); - - await drag({ - page, - testInfo, - testId: histBrushableAreaId, - start: coords.start, - end: coords.end, - }); - - await page.getByTestId(`cellset-button-1`).click(); - const cellCount = await getCellSetCount(1, page); - - // (seve): the withSubset version of this test is resulting in the unsubsetted value - if (option.withSubset) { - expect(cellCount).toBe(data.brushOnGenesetMean.withSubset); - await snapshotTestGraph(page, testInfo); - } else { - expect(cellCount).toBe(data.brushOnGenesetMean.default); - await snapshotTestGraph(page, testInfo); - } - }); - - test("color by mean expression", async ({ page }, testInfo) => { - await setup({ option, page, url, testInfo }); - await createGeneset(meanExpressionBrushGenesetName, page); - await addGeneToSetAndExpand( - meanExpressionBrushGenesetName, - "SIK1", - page + { page } ); - await colorByGeneset(meanExpressionBrushGenesetName, page); - await assertColorLegendLabel(meanExpressionBrushGenesetName, page); await snapshotTestGraph(page, testInfo); }); - test("color by mean expression changes sorting of categories in 'cell_type'", async ({ + + test("pan zoom mode resets lasso selection", async ({ page, }, testInfo) => { - await setup({ option, page, url, testInfo }); + await goToPage(page, url); + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); + + await tryUntil( + async () => { + const panzoomLasso = data.features.panzoom.lasso; + let panzoomLassoCount = panzoomLasso.count; + if (graphTestId === SIDE_PANEL) { + panzoomLassoCount = data.features.panzoom.lasso.count_side; + } + + const lassoSelection = await calcDragCoordinates( + graphTestId, + panzoomLasso["coordinates-as-percent"], + page + ); - const categories = await page - .locator('[data-testid*=":category-expand"]') - .all(); + await drag({ + page, + testInfo, + testId: graphTestId, + start: lassoSelection.start, + end: lassoSelection.end, + lasso: true, + }); - const category = categories[0]; + await expect(page.getByTestId("lasso-element")).toBeVisible(); - const categoryName = ( - await category.getAttribute("data-testid") - )?.split(":")[0] as string; + const initialCount = await getCellSetCount(1, page); - await expandCategory(categoryName, page); + expect(initialCount).toBe(panzoomLassoCount); - await createGeneset(meanExpressionBrushGenesetName, page); + await page.getByTestId("mode-pan-zoom").click(); + await page.getByTestId("mode-lasso").click(); - await addGeneToSetAndExpand( - meanExpressionBrushGenesetName, - "SIK1", - page + const modeSwitchCount = await getCellSetCount(1, page); + + expect(modeSwitchCount).toBe(initialCount); + }, + { page } ); - await waitUntilNoSkeletonDetected(page); + await snapshotTestGraph(page, testInfo); + }); + + test("lasso moves after pan", async ({ page }, testInfo) => { + await goToPage(page, url); + await conditionallyToggleSidePanel(page, graphTestId, SIDE_PANEL); await tryUntil( async () => { - // Check initial order of categories - const initialOrder = await getAllCategories(categoryName, page); + const panzoomLasso = data.features.panzoom.lasso; + const coordinatesAsPercent = + panzoomLasso["coordinates-as-percent"]; + + const lassoSelection = await calcDragCoordinates( + graphTestId, + coordinatesAsPercent, + page + ); - expect(initialOrder).not.toEqual([]); + await snapshotTestGraph(page, testInfo); - // Color by the geneset mean expression - await colorByGeneset(meanExpressionBrushGenesetName, page); + await drag({ + page, + testInfo, + testId: graphTestId, + start: lassoSelection.start, + end: lassoSelection.end, + lasso: true, + }); - await waitUntilNoSkeletonDetected(page); + await snapshotTestGraph(page, testInfo); - // Check order of categories after sorting by mean expression - const sortedOrder = await getAllCategories(categoryName, page); + await expect(page.getByTestId("lasso-element")).toBeVisible(); - expect(sortedOrder).not.toEqual([]); + const initialCount = await getCellSetCount(1, page); - // Expect the sorted order to be different from the initial order - expect(sortedOrder).not.toEqual(initialOrder); - }, - { page } - ); - }); + const expectedCellCount = + graphTestId === SIDE_PANEL + ? panzoomLasso.count_side + : panzoomLasso.count; - test("diffexp", async ({ page }, testInfo) => { - if (option.withSubset) return; + expect(initialCount).toBe(expectedCellCount); - const runningAgainstDeployment = !testURL.includes("localhost"); + await page.getByTestId("mode-pan-zoom").click(); - // this test will take longer if we're running against a deployment - if (runningAgainstDeployment) test.slow(); + await snapshotTestGraph(page, testInfo); - await setup({ option, page, url, testInfo }); + const panCoords = await calcDragCoordinates( + graphTestId, + coordinatesAsPercent, + page + ); - const { category, cellset1, cellset2 } = data.diffexp; + await drag({ + page, + testInfo, + testId: graphTestId, + start: panCoords.start, + end: panCoords.end, + lasso: true, + }); - await expandCategory(category, page); + await snapshotTestGraph(page, testInfo); - await page - .getByTestId(`${category}:category-select`) - .click({ force: true }); + await page.getByTestId("mode-lasso").click(); - const cellset1Selector = page.getByTestId( - `categorical-value-select-${category}-${cellset1.cellType}` - ); - const cellset2Selector = page.getByTestId( - `categorical-value-select-${category}-${cellset2.cellType}` - ); + const panCount = await getCellSetCount(2, page); - await cellset1Selector.click({ force: true }); + expect(panCount).toBe(initialCount); - await page.getByTestId(`cellset-button-1`).click(); + await snapshotTestGraph(page, testInfo); + }, + { page } + ); + }); - await cellset1Selector.click({ force: true }); + /* + Tests included below are specific to annotation features + */ - await cellset2Selector.click({ force: true }); + const options = [ + { withSubset: true, tag: "subset" }, + { withSubset: false, tag: "whole" }, + ]; - await page.getByTestId(`cellset-button-2`).click(); + for (const option of options) { + describeFn( + `geneSET crud operations and interactions ${option.tag}`, + () => { + test("brush on geneset mean", async ({ page }, testInfo) => { + await setup({ option, page, url, testInfo }); + await createGeneset(meanExpressionBrushGenesetName, page); + await addGeneToSetAndExpand( + meanExpressionBrushGenesetName, + "SIK1", + page + ); + + const histBrushableAreaId = `histogram-${meanExpressionBrushGenesetName}-plot-brushable-area`; + + const coords = await calcDragCoordinates( + histBrushableAreaId, + { + x1: 0.1, + y1: 0.5, + x2: 0.9, + y2: 0.5, + }, + page + ); + + await drag({ + page, + testInfo, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, + }); + + await page.getByTestId(`cellset-button-1`).click(); + const cellCount = await getCellSetCount(1, page); + + // (seve): the withSubset version of this test is resulting in the unsubsetted value + if (option.withSubset) { + expect(cellCount).toBe(data.brushOnGenesetMean.withSubset); + await snapshotTestGraph(page, testInfo); + } else { + expect(cellCount).toBe(data.brushOnGenesetMean.default); + await snapshotTestGraph(page, testInfo); + } + }); + + test("color by mean expression", async ({ page }, testInfo) => { + await setup({ option, page, url, testInfo }); + await createGeneset(meanExpressionBrushGenesetName, page); + await addGeneToSetAndExpand( + meanExpressionBrushGenesetName, + "SIK1", + page + ); + + await colorByGeneset(meanExpressionBrushGenesetName, page); + await assertColorLegendLabel( + meanExpressionBrushGenesetName, + page + ); + await snapshotTestGraph(page, testInfo); + }); + test("color by mean expression changes sorting of categories in 'cell_type'", async ({ + page, + }, testInfo) => { + await setup({ option, page, url, testInfo }); - // run diffexp - await page.getByTestId(`diffexp-button`).click(); - await page.getByTestId("pop-1-geneset-expand").click(); + const categories = await page + .locator('[data-testid*=":category-expand"]') + .all(); - await waitUntilNoSkeletonDetected(page); + const category = categories[0]; - let genesHTML = await page.getByTestId("gene-set-genes").innerHTML(); + const categoryName = ( + await category.getAttribute("data-testid") + )?.split(":")[0] as string; - expect(genesHTML).toMatchSnapshot(); - await snapshotTestGraph(page, testInfo); + await expandCategory(categoryName, page); - // (thuang): We need to assert Pop2 geneset is expanded, because sometimes - // the click is so fast that it's not registered - await tryUntil( - async () => { - await page.getByTestId("pop-1-geneset-expand").click(); - await page.getByTestId("pop-2-geneset-expand").click(); + await createGeneset(meanExpressionBrushGenesetName, page); - await waitUntilNoSkeletonDetected(page); + await addGeneToSetAndExpand( + meanExpressionBrushGenesetName, + "SIK1", + page + ); - expect(page.getByTestId("geneset")).toBeTruthy(); + await waitUntilNoSkeletonDetected(page); - await expect( - page.getByTestId(`${data.diffexp.pop2Gene}:gene-label`) - ).toBeVisible(); - }, - { page, timeoutMs: runningAgainstDeployment ? 20000 : undefined } - ); + await tryUntil( + async () => { + // Check initial order of categories + const initialOrder = await getAllCategories( + categoryName, + page + ); - genesHTML = await page.getByTestId("gene-set-genes").innerHTML(); + expect(initialOrder).not.toEqual([]); - expect(genesHTML).toMatchSnapshot(); - await snapshotTestGraph(page, testInfo); - }); + // Color by the geneset mean expression + await colorByGeneset(meanExpressionBrushGenesetName, page); - test("create a new geneset and undo/redo", async ({ - page, - }, testInfo) => { - /** - * (thuang): Test is flaky, so we need to retry until it passes - */ - await tryUntil( - async () => { - if (option.withSubset) return; + await waitUntilNoSkeletonDetected(page); - await setup({ option, page, url, testInfo }); + // Check order of categories after sorting by mean expression + const sortedOrder = await getAllCategories( + categoryName, + page + ); - await waitUntilNoSkeletonDetected(page); + expect(sortedOrder).not.toEqual([]); - const genesetName = `test-geneset-foo-123`; + // Expect the sorted order to be different from the initial order + expect(sortedOrder).not.toEqual(initialOrder); + }, + { page } + ); + }); - await assertGenesetDoesNotExist(genesetName, page); + test("diffexp", async ({ page }, testInfo) => { + if (option.withSubset) return; - await createGeneset(genesetName, page); + const runningAgainstDeployment = !testURL.includes("localhost"); - await assertGenesetExists(genesetName, page); + // this test will take longer if we're running against a deployment + if (runningAgainstDeployment) test.slow(); - await assertUndoRedo( - page, - () => assertGenesetDoesNotExist(genesetName, page), - () => assertGenesetExists(genesetName, page) - ); - }, - { page } - ); - }); + await setup({ option, page, url, testInfo }); - test("edit geneset name and undo/redo", async ({ page }, testInfo) => { - /** - * (thuang): Test is flaky, so we need to retry until it passes - */ - await tryUntil( - async () => { - await setup({ option, page, url, testInfo }); - await createGeneset(editableGenesetName, page); - await editGenesetName(editableGenesetName, newGenesetName, page); + const { category, cellset1, cellset2 } = data.diffexp; - await assertGenesetExists(newGenesetName, page); + await expandCategory(category, page); - await assertUndoRedo( - page, - () => assertGenesetExists(editableGenesetName, page), - () => assertGenesetExists(newGenesetName, page) - ); - }, - { page } - ); - }); + await page + .getByTestId(`${category}:category-select`) + .click({ force: true }); - test("delete a geneset and undo/redo", async ({ page }, testInfo) => { - /** - * (thuang): Test is flaky, so we need to retry until it passes - */ - await tryUntil( - async () => { - if (option.withSubset) return; + const cellset1Selector = page.getByTestId( + `categorical-value-select-${category}-${cellset1.cellType}` + ); + const cellset2Selector = page.getByTestId( + `categorical-value-select-${category}-${cellset2.cellType}` + ); - await setup({ option, page, url, testInfo }); + await cellset1Selector.click({ force: true }); - await createGeneset(genesetToDeleteName, page); + await page.getByTestId(`cellset-button-1`).click(); - await deleteGeneset(genesetToDeleteName, page); + await cellset1Selector.click({ force: true }); - await assertUndoRedo( - page, - () => assertGenesetExists(genesetToDeleteName, page), - () => assertGenesetDoesNotExist(genesetToDeleteName, page) - ); - }, - { page } - ); - }); + await cellset2Selector.click({ force: true }); - test("geneset description", async ({ page }, testInfo) => { - if (option.withSubset) return; + await page.getByTestId(`cellset-button-2`).click(); - await setup({ option, page, url, testInfo }); + // run diffexp + await page.getByTestId(`diffexp-button`).click(); + await page.getByTestId("pop-1-geneset-expand").click(); - await createGeneset( - genesetToCheckForDescription, - page, - genesetDescriptionString - ); + await waitUntilNoSkeletonDetected(page); - await checkGenesetDescription( - genesetToCheckForDescription, - genesetDescriptionString, - page - ); - }); - }); + let genesHTML = await page + .getByTestId("gene-set-genes") + .innerHTML(); - describe(`GENE crud operations and interactions ${option.tag}`, () => { - test("add a gene to geneset and undo/redo", async ({ - page, - }, testInfo) => { - /** - * (thuang): Test is flaky, so we need to retry until it passes - */ - await tryUntil( - async () => { - await setup({ option, page, url, testInfo }); - await createGeneset(setToAddGeneTo, page); - await addGeneToSetAndExpand(setToAddGeneTo, geneToAddToSet, page); - await assertGeneExistsInGeneset(geneToAddToSet, page); + expect(genesHTML).toMatchSnapshot(); + await snapshotTestGraph(page, testInfo); - await assertUndoRedo( - page, - () => assertGeneDoesNotExist(geneToAddToSet, page), - () => assertGeneExistsInGeneset(geneToAddToSet, page) - ); - }, - { page } - ); - }); + // (thuang): We need to assert Pop2 geneset is expanded, because sometimes + // the click is so fast that it's not registered + await tryUntil( + async () => { + await page.getByTestId("pop-1-geneset-expand").click(); + await page.getByTestId("pop-2-geneset-expand").click(); - test("expand gene and brush", async ({ page }, testInfo) => { - await setup({ option, page, url, testInfo }); - await createGeneset(brushThisGeneGeneset, page); - await addGeneToSetAndExpand( - brushThisGeneGeneset, - geneToBrushAndColorBy, - page - ); - await expandGene(geneToBrushAndColorBy, page); - const histBrushableAreaId = `histogram-${geneToBrushAndColorBy}-plot-brushable-area`; - - const coords = await calcDragCoordinates( - histBrushableAreaId, - { - x1: 0.25, - y1: 0.5, - x2: 0.55, - y2: 0.5, - }, - page - ); + await waitUntilNoSkeletonDetected(page); - await snapshotTestGraph(page, testInfo); + expect(page.getByTestId("geneset")).toBeTruthy(); - await drag({ - page, - testInfo, - testId: histBrushableAreaId, - start: coords.start, - end: coords.end, - }); + await expect( + page.getByTestId(`${data.diffexp.pop2Gene}:gene-label`) + ).toBeVisible(); + }, + { + page, + timeoutMs: runningAgainstDeployment ? 20000 : undefined, + } + ); - await snapshotTestGraph(page, testInfo); + genesHTML = await page + .getByTestId("gene-set-genes") + .innerHTML(); - const cellCount = await getCellSetCount(1, page); + expect(genesHTML).toMatchSnapshot(); + await snapshotTestGraph(page, testInfo); + }); - if (option.withSubset) { - expect(cellCount).toBe(data.expandGeneAndBrush.withSubset); - await snapshotTestGraph(page, testInfo); - } else { - expect(cellCount).toBe(data.expandGeneAndBrush.default); - await snapshotTestGraph(page, testInfo); - } - }); + test("create a new geneset and undo/redo", async ({ + page, + }, testInfo) => { + /** + * (thuang): Test is flaky, so we need to retry until it passes + */ + await tryUntil( + async () => { + if (option.withSubset) return; - test("color by gene in geneset", async ({ page }, testInfo) => { - await setup({ option, page, url, testInfo }); + await setup({ option, page, url, testInfo }); - await createGeneset(meanExpressionBrushGenesetName, page); + await waitUntilNoSkeletonDetected(page); - await addGeneToSetAndExpand( - meanExpressionBrushGenesetName, - "SIK1", - page - ); + const genesetName = `test-geneset-foo-123`; - await colorByGene("SIK1", page); - await assertColorLegendLabel("SIK1", page); - await snapshotTestGraph(page, testInfo); - }); - test("delete gene from geneset and undo/redo", async ({ - page, - }, testInfo) => { - /** - * (thuang): Test is flaky, so we need to retry until it passes - */ - await tryUntil( - async () => { - if (option.withSubset) return; + await assertGenesetDoesNotExist(genesetName, page); - await setup({ option, page, url, testInfo }); - await createGeneset(setToRemoveFrom, page); - await addGeneToSetAndExpand(setToRemoveFrom, geneToRemove, page); + await createGeneset(genesetName, page); - await removeGene(geneToRemove, page); + await assertGenesetExists(genesetName, page); - await assertGeneDoesNotExist(geneToRemove, page); + await assertUndoRedo( + page, + () => assertGenesetDoesNotExist(genesetName, page), + () => assertGenesetExists(genesetName, page) + ); + }, + { page } + ); + }); - await assertUndoRedo( + test("edit geneset name and undo/redo", async ({ page, - () => assertGeneExistsInGeneset(geneToRemove, page), - () => assertGeneDoesNotExist(geneToRemove, page) - ); - }, - { page } + }, testInfo) => { + /** + * (thuang): Test is flaky, so we need to retry until it passes + */ + await tryUntil( + async () => { + await setup({ option, page, url, testInfo }); + await createGeneset(editableGenesetName, page); + await editGenesetName( + editableGenesetName, + newGenesetName, + page + ); + + await assertGenesetExists(newGenesetName, page); + + await assertUndoRedo( + page, + () => assertGenesetExists(editableGenesetName, page), + () => assertGenesetExists(newGenesetName, page) + ); + }, + { page } + ); + }); + + test("delete a geneset and undo/redo", async ({ + page, + }, testInfo) => { + /** + * (thuang): Test is flaky, so we need to retry until it passes + */ + await tryUntil( + async () => { + if (option.withSubset) return; + + await setup({ option, page, url, testInfo }); + + await createGeneset(genesetToDeleteName, page); + + await deleteGeneset(genesetToDeleteName, page); + + await assertUndoRedo( + page, + () => assertGenesetExists(genesetToDeleteName, page), + () => assertGenesetDoesNotExist(genesetToDeleteName, page) + ); + }, + { page } + ); + }); + + test("geneset description", async ({ page }, testInfo) => { + if (option.withSubset) return; + + await setup({ option, page, url, testInfo }); + + await createGeneset( + genesetToCheckForDescription, + page, + genesetDescriptionString + ); + + await checkGenesetDescription( + genesetToCheckForDescription, + genesetDescriptionString, + page + ); + }); + } ); - }); - - test("open info panel and hide/remove", async ({ page }, testInfo) => { - await setup({ option, page, url, testInfo }); - await addGeneToSearch(geneToRequestInfo, page); - - await snapshotTestGraph(page, testInfo); - await tryUntil( - async () => { - await requestGeneInfo(geneToRequestInfo, page); - await assertInfoPanelExists(geneToRequestInfo, page); - }, - { page } + describeFn( + `GENE crud operations and interactions ${option.tag}`, + () => { + test("add a gene to geneset and undo/redo", async ({ + page, + }, testInfo) => { + /** + * (thuang): Test is flaky, so we need to retry until it passes + */ + await tryUntil( + async () => { + await setup({ option, page, url, testInfo }); + await createGeneset(setToAddGeneTo, page); + await addGeneToSetAndExpand( + setToAddGeneTo, + geneToAddToSet, + page + ); + await assertGeneExistsInGeneset(geneToAddToSet, page); + + await assertUndoRedo( + page, + () => assertGeneDoesNotExist(geneToAddToSet, page), + () => assertGeneExistsInGeneset(geneToAddToSet, page) + ); + }, + { page } + ); + }); + + test("expand gene and brush", async ({ page }, testInfo) => { + await setup({ option, page, url, testInfo }); + await createGeneset(brushThisGeneGeneset, page); + await addGeneToSetAndExpand( + brushThisGeneGeneset, + geneToBrushAndColorBy, + page + ); + await expandGene(geneToBrushAndColorBy, page); + const histBrushableAreaId = `histogram-${geneToBrushAndColorBy}-plot-brushable-area`; + + const coords = await calcDragCoordinates( + histBrushableAreaId, + { + x1: 0.25, + y1: 0.5, + x2: 0.55, + y2: 0.5, + }, + page + ); + + await snapshotTestGraph(page, testInfo); + + await drag({ + page, + testInfo, + testId: histBrushableAreaId, + start: coords.start, + end: coords.end, + }); + + await snapshotTestGraph(page, testInfo); + + const cellCount = await getCellSetCount(1, page); + + if (option.withSubset) { + expect(cellCount).toBe(data.expandGeneAndBrush.withSubset); + await snapshotTestGraph(page, testInfo); + } else { + expect(cellCount).toBe(data.expandGeneAndBrush.default); + await snapshotTestGraph(page, testInfo); + } + }); + + test("color by gene in geneset", async ({ page }, testInfo) => { + await setup({ option, page, url, testInfo }); + + await createGeneset(meanExpressionBrushGenesetName, page); + + await addGeneToSetAndExpand( + meanExpressionBrushGenesetName, + "SIK1", + page + ); + + await colorByGene("SIK1", page); + await assertColorLegendLabel("SIK1", page); + await snapshotTestGraph(page, testInfo); + }); + test("delete gene from geneset and undo/redo", async ({ + page, + }, testInfo) => { + /** + * (thuang): Test is flaky, so we need to retry until it passes + */ + await tryUntil( + async () => { + if (option.withSubset) return; + + await setup({ option, page, url, testInfo }); + await createGeneset(setToRemoveFrom, page); + await addGeneToSetAndExpand( + setToRemoveFrom, + geneToRemove, + page + ); + + await removeGene(geneToRemove, page); + + await assertGeneDoesNotExist(geneToRemove, page); + + await assertUndoRedo( + page, + () => assertGeneExistsInGeneset(geneToRemove, page), + () => assertGeneDoesNotExist(geneToRemove, page) + ); + }, + { page } + ); + }); + + test("open info panel and hide/remove", async ({ + page, + }, testInfo) => { + await setup({ option, page, url, testInfo }); + await addGeneToSearch(geneToRequestInfo, page); + + await snapshotTestGraph(page, testInfo); + + await tryUntil( + async () => { + await requestGeneInfo(geneToRequestInfo, page); + await assertInfoPanelExists(geneToRequestInfo, page); + }, + { page } + ); + + await snapshotTestGraph(page, testInfo); + + await tryUntil( + async () => { + await minimizeInfoPanel(page); + await assertInfoPanelIsMinimized(geneToRequestInfo, page); + }, + { page } + ); + + await snapshotTestGraph(page, testInfo); + + await tryUntil( + async () => { + await closeInfoPanel(page); + await assertInfoPanelClosed(geneToRequestInfo, page); + }, + { page } + ); + + await snapshotTestGraph(page, testInfo); + }); + } ); + } - await snapshotTestGraph(page, testInfo); - - await tryUntil( - async () => { - await minimizeInfoPanel(page); - await assertInfoPanelIsMinimized(geneToRequestInfo, page); - }, - { page } - ); + describeFn(`Image Download`, () => { + async function snapshotDownloadedImage( + page: Page, + info: TestInfo, + path: string + ) { + const imageFile = await fs.readFile(path, { encoding: "base64" }); + + // attach the image at path to the dom so we can snapshot it + // ensure that the image is rendered on top of the graph + await page.evaluate((imgData) => { + const img = document.createElement("img"); + img.id = "downloaded-image"; + img.src = `data:image/png;base64,${imgData}`; + img.style.height = "100vh"; + img.style.zIndex = "1000"; + img.style.background = "white"; + img.style.position = "absolute"; + img.style.top = "0"; + document.body.appendChild(img); + }, imageFile); + + await takeSnapshot(page, info); + + // remove the image from the dom + await page.evaluate(() => { + const img = document.getElementById("downloaded-image"); + img?.parentNode?.removeChild(img); + }); + } - await snapshotTestGraph(page, testInfo); + async function downloadAndSnapshotImage(page: Page, info: TestInfo) { + const downloads: Download[] = []; - await tryUntil( - async () => { - await closeInfoPanel(page); - await assertInfoPanelClosed(geneToRequestInfo, page); - }, - { page } - ); + page.on("download", (downloadData) => { + downloads.push(downloadData); + }); - await snapshotTestGraph(page, testInfo); - }); - }); - } - - describe(`Image Download`, () => { - async function snapshotDownloadedImage( - page: Page, - info: TestInfo, - path: string - ) { - const imageFile = await fs.readFile(path, { encoding: "base64" }); - - // attach the image at path to the dom so we can snapshot it - // ensure that the image is rendered on top of the graph - await page.evaluate((imgData) => { - const img = document.createElement("img"); - img.id = "downloaded-image"; - img.src = `data:image/png;base64,${imgData}`; - img.style.height = "100vh"; - img.style.zIndex = "1000"; - img.style.background = "white"; - img.style.position = "absolute"; - img.style.top = "0"; - document.body.appendChild(img); - }, imageFile); - - await takeSnapshot(page, info); - - // remove the image from the dom - await page.evaluate(() => { - const img = document.getElementById("downloaded-image"); - img?.parentNode?.removeChild(img); - }); - } + await page.getByTestId("download-graph-button").click(); + + await tryUntil( + async () => { + expect(downloads.length).toBe( + /** + * (thuang): If it's a categorical test, we expect 2 downloads + */ + info.title.includes("categorical") ? 2 : 1 + ); + }, + { page } + ); - async function downloadAndSnapshotImage(page: Page, info: TestInfo) { - const downloads: Download[] = []; + const [graphDownload, legendDownload] = downloads; - page.on("download", (downloadData) => { - downloads.push(downloadData); - }); + const tmp = os.tmpdir(); - await page.getByTestId("download-graph-button").click(); + // make the title safe for file system + const safeTitle = info.title.replace(/[^a-z0-9]/gi, "_"); + const graphPath = `${tmp}/${safeTitle}/${graphDownload.suggestedFilename()}`; - await tryUntil( - async () => { - expect(downloads.length).toBe( - /** - * (thuang): If it's a categorical test, we expect 2 downloads - */ - info.title.includes("categorical") ? 2 : 1 - ); - }, - { page } - ); + await graphDownload.saveAs(graphPath); - const [graphDownload, legendDownload] = downloads; + if (legendDownload) { + const legendPath = `${tmp}/${safeTitle}/${legendDownload.suggestedFilename()}`; + await legendDownload.saveAs(legendPath); - const tmp = os.tmpdir(); + await snapshotDownloadedImage(page, info, legendPath); + } - // make the title safe for file system - const safeTitle = info.title.replace(/[^a-z0-9]/gi, "_"); - const graphPath = `${tmp}/${safeTitle}/${graphDownload.suggestedFilename()}`; + await snapshotDownloadedImage(page, info, graphPath); + } - await graphDownload.saveAs(graphPath); + test("with continuous legend", async ({ page }, testInfo) => { + await goToPage(page, url); - if (legendDownload) { - const legendPath = `${tmp}/${safeTitle}/${legendDownload.suggestedFilename()}`; - await legendDownload.saveAs(legendPath); + await addGeneToSearch("SIK1", page); + await colorByGene("SIK1", page); - await snapshotDownloadedImage(page, info, legendPath); - } + await downloadAndSnapshotImage(page, testInfo); + }); - await snapshotDownloadedImage(page, info, graphPath); - } + test("with categorical legend", async ({ page }, testInfo) => { + await goToPage(page, url); - test("with continuous legend", async ({ page }, testInfo) => { - await goToPage(page, url); + const allLabels = [...Object.keys(data.categorical)]; + await page.getByTestId(`colorby-${allLabels[0]}`).click(); - await addGeneToSearch("SIK1", page); - await colorByGene("SIK1", page); + await downloadAndSnapshotImage(page, testInfo); + }); + }); + describeFn("Side Panel", () => { + test("open and close side panel", async ({ page }, testInfo) => { + await goToPage(page, url); - await downloadAndSnapshotImage(page, testInfo); - }); + await toggleSidePanel(page); - test("with categorical legend", async ({ page }, testInfo) => { - await goToPage(page, url); + await expect(page.getByTestId(SIDE_PANEL)).toBeVisible(); + await snapshotTestGraph(page, testInfo); - const allLabels = [...Object.keys(data.categorical)]; - await page.getByTestId(`colorby-${allLabels[0]}`).click(); + await toggleSidePanel(page); - await downloadAndSnapshotImage(page, testInfo); + await expect(page.getByTestId(SIDE_PANEL)).not.toBeVisible(); + await snapshotTestGraph(page, testInfo); + }); + }); }); }); - }); + } } test("categories and values from dataset appear and properly truncate if applicable", async ({ diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt new file mode 100644 index 000000000..d2790dc55 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet \ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt new file mode 100644 index 000000000..d2790dc55 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-26a66-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet \ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt new file mode 100644 index 000000000..503c7a017 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt @@ -0,0 +1 @@ +
CD79A79A
HLA-DRB1DRB1
HLA-DQA1DQA1
HLA-DPB1DPB1
HLA-DQB1DQB1
HLA-DPA1DPA1
MS4A14A1
LTBTB
CD79B79B
CD3737
HLA-DMA-DMA
TCL1AL1A
LINC0092600926
HLA-DMB-DMB
HVCN1CN1
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt new file mode 100644 index 000000000..503c7a017 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-7618b-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt @@ -0,0 +1 @@ +
CD79A79A
HLA-DRB1DRB1
HLA-DQA1DQA1
HLA-DPB1DPB1
HLA-DQB1DQB1
HLA-DPA1DPA1
MS4A14A1
LTBTB
CD79B79B
CD3737
HLA-DMA-DMA
TCL1AL1A
LINC0092600926
HLA-DMB-DMB
HVCN1CN1
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt new file mode 100644 index 000000000..8f9d04676 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt @@ -0,0 +1 @@ +
NKG7G7
GZMBMB
CTSWSW
PRF1F1
GNLYLY
GZMAMA
CST7T7
FGFBP2BP2
SRGNGN
CD247247
FCGR3AR3A
TYROBPOBP
FCER1GR1G
ID2D2
SPON2ON2
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt new file mode 100644 index 000000000..8f9d04676 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-b442e-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt @@ -0,0 +1 @@ +
NKG7G7
GZMBMB
CTSWSW
PRF1F1
GNLYLY
GZMAMA
CST7T7
FGFBP2BP2
SRGNGN
CD247247
FCGR3AR3A
TYROBPOBP
FCER1GR1G
ID2D2
SPON2ON2
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt new file mode 100644 index 000000000..0b74ab2e5 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt @@ -0,0 +1 @@ +Nullam ultrices urna nec congue aliquam \ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt new file mode 100644 index 000000000..0b74ab2e5 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-pbmc3k-cxg-graph-instance-layout-gra-bb623-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt @@ -0,0 +1 @@ +Nullam ultrices urna nec congue aliquam \ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt new file mode 100644 index 000000000..b50583253 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-darwin.txt @@ -0,0 +1 @@ +
ZFTATA
NBPF11F11
LINC0293702937
FGD3D3
NUAK1AK1
BMP3P3
TMEM237M237
IGIPIP
MROH7OH7
LAMP5MP5
PAPOLGOLG
RPA3A3
IL1818
TPMTMT
JADE2DE2
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt new file mode 100644 index 000000000..b50583253 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-2f523-rud-operations-and-interactions-whole-diffexp-1-chromium-linux.txt @@ -0,0 +1 @@ +
ZFTATA
NBPF11F11
LINC0293702937
FGD3D3
NUAK1AK1
BMP3P3
TMEM237M237
IGIPIP
MROH7OH7
LAMP5MP5
PAPOLGOLG
RPA3A3
IL1818
TPMTMT
JADE2DE2
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt new file mode 100644 index 000000000..0b74ab2e5 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-darwin.txt @@ -0,0 +1 @@ +Nullam ultrices urna nec congue aliquam \ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt new file mode 100644 index 000000000..0b74ab2e5 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8457a-taset-and-collection-from-breadcrumbs-appears-1-chromium-linux.txt @@ -0,0 +1 @@ +Nullam ultrices urna nec congue aliquam \ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt new file mode 100644 index 000000000..c291bbb85 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-darwin.txt @@ -0,0 +1 @@ +
RACK1CK1
CST3T3
IFITM3TM3
RPL21L21
RPS16S16
RPS10S10
COX4I14I1
RPL24L24
TAGLN2LN2
UBA52A52
SQSTM1TM1
RPL10L10
UBCBC
COX8AX8A
LTBP4BP4
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt new file mode 100644 index 000000000..c291bbb85 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-8de13-rud-operations-and-interactions-whole-diffexp-2-chromium-linux.txt @@ -0,0 +1 @@ +
RACK1CK1
CST3T3
IFITM3TM3
RPL21L21
RPS16S16
RPS10S10
COX4I14I1
RPL24L24
TAGLN2LN2
UBA52A52
SQSTM1TM1
RPL10L10
UBCBC
COX8AX8A
LTBP4BP4
\ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt new file mode 100644 index 000000000..d2790dc55 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-darwin.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet \ No newline at end of file diff --git a/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt new file mode 100644 index 000000000..d2790dc55 --- /dev/null +++ b/client/__tests__/e2e/e2e.test.ts-snapshots/dataset-super-cool-spatial-cxg-graph-instance-eb8ed-taset-and-collection-from-breadcrumbs-appears-2-chromium-linux.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet \ No newline at end of file diff --git a/client/__tests__/util/helpers.ts b/client/__tests__/util/helpers.ts index cb18e2f6c..64e5ed140 100644 --- a/client/__tests__/util/helpers.ts +++ b/client/__tests__/util/helpers.ts @@ -1,4 +1,5 @@ import { ElementHandle, expect, Locator, Page } from "@playwright/test"; +import { test } from "@chromatic-com/playwright"; import { ERROR_NO_TEST_ID_OR_LOCATOR } from "../common/constants"; import { waitUntilNoSkeletonDetected } from "../e2e/cellxgeneActions"; @@ -8,6 +9,8 @@ export const WAIT_FOR_TIMEOUT_MS = 3 * 1000; const GO_TO_PAGE_TIMEOUT_MS = 2 * 60 * 1000; +const { skip } = test; + export async function goToPage(page: Page, url = ""): Promise { await tryUntil( async () => { @@ -269,3 +272,31 @@ export async function checkTooltipContent( const tooltipText = await tooltipLocator.textContent(); expect(tooltipText).toContain(text); } + +export async function toggleSidePanel(page: Page): Promise { + await page.getByTestId("side-panel-toggle").click(); +} + +export async function conditionallyToggleSidePanel( + page: Page, + graphTestId: string, + SIDE_PANEL: string +): Promise { + if (graphTestId === SIDE_PANEL) { + await toggleSidePanel(page); + } +} + +export function skipIfSidePanel(graphTestId: string, MAIN_PANEL: string) { + const message = "This test is only for the main panel"; + if (graphTestId !== MAIN_PANEL) { + skip(true, message); + } +} + +export function shouldSkipTests( + graphTestId: string, + SIDE_PANEL: string +): boolean { + return graphTestId === SIDE_PANEL; +} diff --git a/client/configuration/babel/babel.dev.js b/client/configuration/babel/babel.dev.js index 67e8c81fe..2a095ba3a 100644 --- a/client/configuration/babel/babel.dev.js +++ b/client/configuration/babel/babel.dev.js @@ -22,6 +22,5 @@ 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 617bb63e5..8a5d47a75 100644 --- a/client/configuration/babel/babel.prod.js +++ b/client/configuration/babel/babel.prod.js @@ -23,6 +23,5 @@ 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 796c17330..76bbe58b8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -75,7 +75,6 @@ "@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", @@ -155,7 +154,6 @@ "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", @@ -181,7 +179,6 @@ "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" @@ -837,7 +834,6 @@ "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" } @@ -846,7 +842,6 @@ "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", @@ -875,14 +870,12 @@ "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==", - "devOptional": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "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", @@ -897,7 +890,6 @@ "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", @@ -935,7 +927,6 @@ "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", @@ -951,7 +942,6 @@ "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" } @@ -959,8 +949,7 @@ "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==", - "devOptional": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.23.10", @@ -1023,7 +1012,6 @@ "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" } @@ -1032,7 +1020,6 @@ "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" @@ -1045,7 +1032,6 @@ "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" }, @@ -1080,7 +1066,6 @@ "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", @@ -1111,7 +1096,6 @@ "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" } @@ -1154,7 +1138,6 @@ "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" }, @@ -1178,7 +1161,6 @@ "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" }, @@ -1206,7 +1188,6 @@ "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" } @@ -1229,7 +1210,6 @@ "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", @@ -1320,7 +1300,6 @@ "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" }, @@ -1682,7 +1661,6 @@ "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" }, @@ -3016,7 +2994,6 @@ "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", @@ -3030,7 +3007,6 @@ "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", @@ -4654,28 +4630,27 @@ } }, "node_modules/@emotion/babel-plugin": { - "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==", + "version": "11.10.2", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz", + "integrity": "sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==", "dependencies": { "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/serialize": "^1.1.2", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.0", "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.2.0" + "stylis": "4.0.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.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", @@ -4709,9 +4684,9 @@ } }, "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.0", @@ -4722,9 +4697,9 @@ } }, "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, "node_modules/@emotion/react": { "version": "11.10.4", @@ -4754,14 +4729,14 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", "csstype": "^3.0.2" } }, @@ -4797,9 +4772,9 @@ } }, "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -4810,9 +4785,9 @@ } }, "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" }, "node_modules/@emotion/weak-memoize": { "version": "0.3.0", @@ -5520,7 +5495,6 @@ "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" @@ -5533,7 +5507,6 @@ "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" } @@ -5542,7 +5515,6 @@ "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" } @@ -5574,14 +5546,12 @@ "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==", - "devOptional": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "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" @@ -12948,7 +12918,6 @@ "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", @@ -13115,7 +13084,6 @@ "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", @@ -14555,7 +14523,6 @@ "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" }, @@ -15145,8 +15112,7 @@ "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==", - "devOptional": true + "integrity": "sha512-XvQaa8hVUAuEJtLw6VKQqvdOxTOfBLWfI10t2xWpezx4XXD3k8bdLweEKeItqaa0+OkJX5l0mP1W+JWobyIDrg==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -15409,7 +15375,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "devOptional": true, "engines": { "node": ">=6" } @@ -17314,7 +17279,6 @@ "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" } @@ -17506,7 +17470,6 @@ "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" } @@ -19013,7 +18976,6 @@ "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" }, @@ -19048,7 +19010,6 @@ "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" }, @@ -19971,8 +19932,7 @@ "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==", - "devOptional": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -20161,8 +20121,7 @@ "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==", - "devOptional": true + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -20959,8 +20918,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "devOptional": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -22932,12 +22890,6 @@ "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", @@ -23115,7 +23067,6 @@ "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" } @@ -24868,7 +24819,6 @@ "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", @@ -25651,7 +25601,6 @@ "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" @@ -26225,14 +26174,12 @@ "@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==", - "devOptional": true + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==" }, "@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", @@ -26254,8 +26201,7 @@ "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==", - "devOptional": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" } } }, @@ -26263,7 +26209,6 @@ "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", @@ -26275,7 +26220,6 @@ "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", @@ -26306,7 +26250,6 @@ "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", @@ -26319,7 +26262,6 @@ "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" } @@ -26327,8 +26269,7 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "devOptional": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, @@ -26377,14 +26318,12 @@ "@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==", - "devOptional": true + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@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" @@ -26394,7 +26333,6 @@ "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" } @@ -26420,7 +26358,6 @@ "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", @@ -26441,8 +26378,7 @@ "@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==", - "dev": true + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" }, "@babel/helper-remap-async-to-generator": { "version": "7.22.20", @@ -26470,7 +26406,6 @@ "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" } @@ -26488,7 +26423,6 @@ "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" } @@ -26506,8 +26440,7 @@ "@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==", - "devOptional": true + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==" }, "@babel/helper-wrap-function": { "version": "7.22.20", @@ -26524,7 +26457,6 @@ "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", @@ -26595,8 +26527,7 @@ "@babel/parser": { "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", - "devOptional": true + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.23.3", @@ -26826,7 +26757,6 @@ "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" } @@ -27722,7 +27652,6 @@ "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", @@ -27733,7 +27662,6 @@ "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", @@ -28943,28 +28871,22 @@ "dev": true }, "@emotion/babel-plugin": { - "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==", + "version": "11.10.2", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz", + "integrity": "sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==", "requires": { "@babel/helper-module-imports": "^7.16.7", + "@babel/plugin-syntax-jsx": "^7.17.12", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/serialize": "^1.1.2", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/serialize": "^1.1.0", "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.2.0" - }, - "dependencies": { - "stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" - } + "stylis": "4.0.13" } }, "@emotion/cache": { @@ -28992,9 +28914,9 @@ } }, "@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", + "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, "@emotion/is-prop-valid": { "version": "1.2.0", @@ -29005,9 +28927,9 @@ } }, "@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, "@emotion/react": { "version": "11.10.4", @@ -29025,14 +28947,14 @@ } }, "@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==", "requires": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", + "@emotion/hash": "^0.9.0", + "@emotion/memoize": "^0.8.0", + "@emotion/unitless": "^0.8.0", + "@emotion/utils": "^1.2.0", "csstype": "^3.0.2" } }, @@ -29055,9 +28977,9 @@ } }, "@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" }, "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", @@ -29066,9 +28988,9 @@ "requires": {} }, "@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", + "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" }, "@emotion/weak-memoize": { "version": "0.3.0", @@ -29494,7 +29416,6 @@ "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" @@ -29503,14 +29424,12 @@ "@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==", - "devOptional": true + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" }, "@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==", - "devOptional": true + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/source-map": { "version": "0.3.5", @@ -29538,14 +29457,12 @@ "@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==", - "devOptional": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@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" @@ -34955,7 +34872,6 @@ "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", @@ -35068,8 +34984,7 @@ "caniuse-lite": { "version": "1.0.30001581", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", - "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", - "devOptional": true + "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -36159,7 +36074,6 @@ "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" } @@ -36603,8 +36517,7 @@ "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==", - "devOptional": true + "integrity": "sha512-XvQaa8hVUAuEJtLw6VKQqvdOxTOfBLWfI10t2xWpezx4XXD3k8bdLweEKeItqaa0+OkJX5l0mP1W+JWobyIDrg==" }, "emoji-regex": { "version": "9.2.2", @@ -36822,8 +36735,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "devOptional": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-html": { "version": "1.0.3", @@ -38264,8 +38176,7 @@ "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==", - "devOptional": true + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-func-name": { "version": "2.0.2", @@ -38408,8 +38319,7 @@ "globals": { "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 + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "globby": { "version": "11.1.0", @@ -39463,8 +39373,7 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "devOptional": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json-loader": { "version": "0.5.7", @@ -39492,8 +39401,7 @@ "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "devOptional": true + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, "jsonfile": { "version": "4.0.0", @@ -40232,8 +40140,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "devOptional": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { "version": "3.3.7", @@ -40385,8 +40292,7 @@ "node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "devOptional": true + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "normalize-package-data": { "version": "2.5.0", @@ -40960,8 +40866,7 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "devOptional": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "picomatch": { "version": "2.3.1", @@ -42373,12 +42278,6 @@ "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", @@ -42509,8 +42408,7 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "semver-compare": { "version": "1.0.0", @@ -43870,7 +43768,6 @@ "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 cf689e396..e270f77b2 100644 --- a/client/package.json +++ b/client/package.json @@ -119,7 +119,6 @@ "@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", @@ -199,7 +198,6 @@ "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/playwright.config.ts b/client/playwright.config.ts index a6207d612..f204ff5de 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -112,7 +112,7 @@ export default defineConfig({ }, maxFailures: process.env.CI ? CICD_MAX_FAILURE : undefined, - timeout: 2.5 * 60 * 1000, // 2.5 minutes + timeout: 3 * 60 * 1000, // 2.5 minutes /* Configure projects for major browsers */ projects: [ diff --git a/client/src/common/selectors.ts b/client/src/common/selectors.ts index 6d867e6cc..2b734f912 100644 --- a/client/src/common/selectors.ts +++ b/client/src/common/selectors.ts @@ -7,9 +7,19 @@ import { FEATURES } from "../util/featureFlags/features"; export function isSpatialMode(props: ShouldShowOpenseadragonProps): boolean { const { layoutChoice, panelEmbedding } = props; - return !!( + const { open, layoutChoice: panelEmbeddingLayoutChoice } = + panelEmbedding || {}; + + const isPanelEmbeddingInSpatialMode = + /** + * (thuang): If the side panel is not open, don't take its layout choice into account + */ + open && + panelEmbeddingLayoutChoice?.current?.includes(spatialEmbeddingKeyword); + + return Boolean( layoutChoice?.current?.includes(spatialEmbeddingKeyword) || - panelEmbedding?.layoutChoice?.current?.includes(spatialEmbeddingKeyword) + isPanelEmbeddingInSpatialMode ); } diff --git a/client/src/components/embedding/index.tsx b/client/src/components/embedding/index.tsx index f65661ede..2313e0c12 100644 --- a/client/src/components/embedding/index.tsx +++ b/client/src/components/embedding/index.tsx @@ -23,12 +23,12 @@ import { Schema } from "../../common/types/schema"; import { AnnoMatrixObsCrossfilter } from "../../annoMatrix"; import { getFeatureFlag } from "../../util/featureFlags/featureFlags"; import { FEATURES } from "../../util/featureFlags/features"; +import { sidePanelAttributeNameChange } from "../graph/util"; interface StateProps { layoutChoice: RootState["layoutChoice"]; schema?: Schema; crossfilter: RootState["obsCrossfilter"]; - imageUnderlay: RootState["controls"]["imageUnderlay"]; sideIsOpen: RootState["panelEmbedding"]["open"]; } @@ -48,7 +48,6 @@ const mapStateToProps = (state: RootState, props: OwnProps): StateProps => ({ : state.layoutChoice, schema: state.annoMatrix?.schema, crossfilter: state.obsCrossfilter, - imageUnderlay: state.controls.imageUnderlay, sideIsOpen: state.panelEmbedding.open, }); @@ -58,7 +57,6 @@ const Embedding = (props: Props) => { schema, crossfilter, dispatch, - imageUnderlay, isSidePanel, sideIsOpen, } = props; @@ -81,16 +79,6 @@ const Embedding = (props: Props) => { await dispatch( actions.layoutChoiceAction(e.currentTarget.value, isSidePanel) ); - if ( - imageUnderlay && - !isSidePanel && - !e.currentTarget.value.includes(globals.spatialEmbeddingKeyword) - ) { - dispatch({ - type: "toggle image underlay", - toggle: false, - }); - } }; const handleOpenPanelEmbedding = async (): Promise => { @@ -102,7 +90,7 @@ const Embedding = (props: Props) => { return (
@@ -139,7 +127,10 @@ const Embedding = (props: Props) => { >
+
+ + { + dispatch({ + type: "toggle active info panel", + activeTab: "Dataset", + }); + }} + style={{ + cursor: "pointer", + }} + data-testid="drawer" + /> + + {isDownload && ( + + dispatch({ type: "graph: screencap start" })} + /> + + )} + {isTest && ( + dispatch({ type: "test: screencap start" })} + /> + )} + = availableButtonSlots) { - columnStartingIndex = i; - return null; + handleClipPercentileMinValueChange={ + this.handleClipPercentileMinValueChange } - return component; - })} - - - {columnStartingIndex !== null && - rightMenuBarComponents - .slice(columnStartingIndex) - .map((component) => { - if (component && component.type === ButtonGroup) { - return cloneElement(component, { vertical: true }); - } - return component; - })} - - - - ); + /> + + + + {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 : } +
+
+ ); + } } export default connect(mapStateToProps)(MenuBar); diff --git a/client/src/components/menubar/menubar.css b/client/src/components/menubar/menubar.css new file mode 100644 index 000000000..72b1ac348 --- /dev/null +++ b/client/src/components/menubar/menubar.css @@ -0,0 +1,4 @@ +:local(.menubarButton) { + margin-top: 8px; + margin-left: 8px; +} diff --git a/client/src/components/menubar/style.ts b/client/src/components/menubar/style.ts deleted file mode 100644 index 7acee0eb8..000000000 --- a/client/src/components/menubar/style.ts +++ /dev/null @@ -1,38 +0,0 @@ -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 d6398e204..ca96329e2 100644 --- a/client/src/components/menubar/subset.tsx +++ b/client/src/components/menubar/subset.tsx @@ -1,5 +1,7 @@ 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) => { @@ -15,7 +17,7 @@ const Subset = React.memo((props) => { } = props; return ( - + { switch (action.type) { case "initial data load complete": { + const { layoutChoice } = action; + return { ...state, + layoutChoice: { + available: selectAvailableLayouts(nextSharedState), + ...getCurrentLayout(nextSharedState, layoutChoice), + }, open: false, minimized: false, }; @@ -36,7 +42,7 @@ const panelEmbedding = ( ...state, open: !state.open, layoutChoice: { - available: selectAvailableLayouts(nextSharedState), + ...state.layoutChoice, ...getCurrentLayout( nextSharedState, nextSharedState.layoutChoice.current