From b3423598d5e0c0f08145c576cf6be3ce2719bf99 Mon Sep 17 00:00:00 2001 From: sunm19810-pki Date: Thu, 28 Nov 2024 16:09:33 +0800 Subject: [PATCH 01/14] fix open dialog issue for multiple Editor --- packages/ove/cypress/e2e/globalDialog.spec.js | 68 +++++++++++++++++++ packages/ove/src/GlobalDialogUtils.js | 1 + 2 files changed, 69 insertions(+) create mode 100644 packages/ove/cypress/e2e/globalDialog.spec.js diff --git a/packages/ove/cypress/e2e/globalDialog.spec.js b/packages/ove/cypress/e2e/globalDialog.spec.js new file mode 100644 index 00000000..3e6073a8 --- /dev/null +++ b/packages/ove/cypress/e2e/globalDialog.spec.js @@ -0,0 +1,68 @@ +describe("Global Dialogs", () => { + it(`feature/primer dialog should be able to open/close for multiple Editor`, () => { + cy.visit(""); + + const fistNodeSelector = ".standaloneDemoNode .veEditor.StandaloneEditor"; + const secondNodeSelector = ".standaloneDemoNode2 .veEditor.vector-editor2"; + + cy.contains(".bp3-button-text", "Show Sidebar").click(); + cy.contains(".demo-nav-link", "Standalone").should("exist"); + cy.contains(".demo-nav-link", "Standalone").click(); + + cy.contains(".bp3-button-text", "Open a second editor").click(); + cy.get(fistNodeSelector).should("exist"); + cy.get(secondNodeSelector).should("exist"); + + cy.contains( + `${fistNodeSelector} .ve-draggable-tabs .veTabProperties`, + "Properties" + ).click(); + cy.get( + `${fistNodeSelector} .ve-propertiesPanel .bp3-tabs .bp3-tab#bp3-tab-title_undefined_features` + ).click(); + cy.contains( + `${fistNodeSelector} .bp3-tab-panel .tg-cell-wrapper`, + "araC" + ).should("exist"); + + cy.contains( + `${secondNodeSelector} .ve-draggable-tabs .veTabProperties`, + "Properties" + ).click(); + cy.get( + `${secondNodeSelector} .ve-propertiesPanel .bp3-tabs .bp3-tab#bp3-tab-title_undefined_features` + ).click(); + cy.contains( + `${secondNodeSelector} .bp3-tab-panel .tg-cell-wrapper`, + "Untitled annotation" + ).should("exist"); + + cy.get( + `${fistNodeSelector} .veCircularView .circularViewSvg .veCircularViewTextWrapper` + ).click(); + cy.contains( + `${fistNodeSelector} .bp3-tab-panel .tg-cell-wrapper`, + "araC" + ).dblclick(); + cy.contains( + ".bp3-dialog .bp3-dialog-header .bp3-heading", + "Edit Feature" + ).should("exist"); + cy.get('.bp3-dialog input[value="araC"]').should("exist"); + cy.get(".bp3-dialog .bp3-dialog-close-button").click(); + + cy.get( + `${secondNodeSelector} .veCircularView .circularViewSvg .veCircularViewTextWrapper` + ).click(); + cy.contains( + `${secondNodeSelector} .bp3-tab-panel .tg-cell-wrapper`, + "Untitled annotation" + ).dblclick(); + cy.contains( + ".bp3-dialog .bp3-dialog-header .bp3-heading", + "Edit Feature" + ).should("exist"); + cy.get('.bp3-dialog input[value="Untitled annotation"]').should("exist"); + cy.get(".bp3-dialog .bp3-dialog-close-button").click(); + }); +}); diff --git a/packages/ove/src/GlobalDialogUtils.js b/packages/ove/src/GlobalDialogUtils.js index 5f8f98e7..bbfb8286 100644 --- a/packages/ove/src/GlobalDialogUtils.js +++ b/packages/ove/src/GlobalDialogUtils.js @@ -43,6 +43,7 @@ export function hideDialog() { delete dialogHolder.CustomModalComponent; delete dialogHolder.props; delete dialogHolder.overrideName; + delete dialogHolder.editorName; dialogHolder.setUniqKeyToForceRerender(); } From b352e4b26a9c87f81807c4b64ff69d374cb7e47f Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Wed, 11 Dec 2024 13:32:36 -0300 Subject: [PATCH 02/14] fix: ove test --- packages/ove/cypress/e2e/alignment.spec.js | 4 +- packages/ove/cypress/e2e/editing.spec.js | 4 +- packages/ove/cypress/e2e/editor.spec.js | 4 +- packages/ove/cypress/e2e/menuBar.spec.js | 11 +- packages/ove/src/DigestTool/DigestTool.js | 104 +++--- .../RemoveDuplicates/index.js | 304 +++++++++--------- packages/ove/src/utils/useFormValue.js | 7 + packages/ui/demo/src/examples/DataTable.js | 2 +- packages/ui/src/DataTable/index.js | 10 +- 9 files changed, 215 insertions(+), 235 deletions(-) create mode 100644 packages/ove/src/utils/useFormValue.js diff --git a/packages/ove/cypress/e2e/alignment.spec.js b/packages/ove/cypress/e2e/alignment.spec.js index 6976d4b4..49226505 100644 --- a/packages/ove/cypress/e2e/alignment.spec.js +++ b/packages/ove/cypress/e2e/alignment.spec.js @@ -105,7 +105,7 @@ describe("alignment", function () { cy.get("body").type("{shift}", { release: false }); cy.scrollAlignmentToPercent(0.99); cy.contains(`[data-alignment-track-index="1"] text`, 3510).click(); - cy.get(`[title="Selecting 3500 bps from 11 to 3510"]`); + cy.get(`[title="Selecting 3499 bps from 11 to 3509"]`); }); it("the alignment should show axis numbers correctly", function () { cy.visit("#/Alignment?alignmentDataId=39"); @@ -124,7 +124,7 @@ describe("alignment", function () { cy.get("body").type("{shift}", { release: false }); cy.scrollAlignmentToPercent(0.99); cy.contains(`[data-alignment-track-index="1"] text`, 3510).click(); - cy.get(`[title="Selecting 3455 bps from 56 to 3510"]`); + cy.get(`[title="Selecting 3454 bps from 56 to 3509"]`); }); it("scrolls the yellow scroll handle correctly", function () { cy.visit("#/Alignment"); diff --git a/packages/ove/cypress/e2e/editing.spec.js b/packages/ove/cypress/e2e/editing.spec.js index a5e8a9c0..007d270d 100644 --- a/packages/ove/cypress/e2e/editing.spec.js +++ b/packages/ove/cypress/e2e/editing.spec.js @@ -58,7 +58,7 @@ describe("editing", function () { cy.contains("Caret Between Bases 5293 and 1"); cy.contains(".ve-row-item-sequence", "5'gtcttatga"); }); - it(`should be able to insert data around the origin correctly + it(`should be able to insert data around the origin correctly - new sequence should be inserted after the origin`, () => { cy.selectRange(5297, 3); cy.replaceSelection("aaaaaa"); @@ -68,7 +68,7 @@ describe("editing", function () { cy.replaceSelection("tt"); cy.contains("Selecting 2 bps from 3 to 4"); }); - it(`should be able to revComp, comp selections that wrap the origin correctly + it(`should be able to revComp, comp selections that wrap the origin correctly - new sequence should be inserted after the origin`, () => { cy.selectRange(5297, 3); cy.contains("Jump to start").click(); diff --git a/packages/ove/cypress/e2e/editor.spec.js b/packages/ove/cypress/e2e/editor.spec.js index 2de4bd4a..55f35ee0 100644 --- a/packages/ove/cypress/e2e/editor.spec.js +++ b/packages/ove/cypress/e2e/editor.spec.js @@ -154,7 +154,7 @@ describe("editor", function () { cy.tgToggle("shouldAutosave"); cy.contains(".veRowViewPart", "Part 0").first().click(); cy.get(".veRowViewSelectionLayer").first().trigger("contextmenu"); - cy.get(".bp3-menu-item").contains("Cut").click(); + cy.get(".bp3-menu-item").contains("Cut").realClick(); cy.contains("onCopy callback triggered"); cy.contains("onSave callback triggered"); cy.contains("Selection Cut"); @@ -167,7 +167,7 @@ describe("editor", function () { cy.get(".veRowViewSelectionLayer").first().trigger("contextmenu"); //tnrnote: cut in cypress only works on electron, not firefox or chrome - cy.get(".bp3-menu-item").contains("Cut").click(); + cy.get(".bp3-menu-item").contains("Cut").realClick(); cy.contains("Selection Cut"); cy.get(".tg-menu-bar").contains("File").click(); cy.get(".bp3-menu-item").contains("Save As").click(); diff --git a/packages/ove/cypress/e2e/menuBar.spec.js b/packages/ove/cypress/e2e/menuBar.spec.js index 932fe613..e34bca0b 100644 --- a/packages/ove/cypress/e2e/menuBar.spec.js +++ b/packages/ove/cypress/e2e/menuBar.spec.js @@ -186,7 +186,6 @@ describe("menuBar", function () { // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(0); cy.focused().type("remove duplicate feature{enter}", { delay: 1 }); - cy.contains(".rt-td", "dbl term").should("exist"); cy.contains(".bp3-dialog button", "Remove 2 Duplicates"); cy.get(".bp3-dialog .bp3-icon-settings").click(); cy.get(".tg-test-ignore-name .tg-no-fill-field").click(); @@ -204,7 +203,7 @@ describe("menuBar", function () { cy.get(".tg-menu-bar-popover").contains("Select All").click(); cy.get(".tg-menu-bar").contains("Edit").click(); - cy.get(".tg-menu-bar-popover").contains("Cut").click(); + cy.get(".tg-menu-bar-popover").contains("Cut").realClick(); cy.get(".tg-menu-bar").contains("Edit").click({ force: true }); [ @@ -290,7 +289,7 @@ describe("menuBar", function () { }); it(` goTo, rotateTo work -can't go to a position outside of the sequence - -can go to a position inside the sequence + -can go to a position inside the sequence -can rotate the sequence to that position `, () => { cy.get(".tg-menu-bar").contains("Edit").click(); @@ -329,7 +328,7 @@ describe("menuBar", function () { it(` select range, copy, cut works -cannot select range outside of sequence //TODO - -can select a valid range + -can select a valid range -can copy the select bps -can cut the selected bps `, function () { @@ -348,10 +347,10 @@ describe("menuBar", function () { cy.get(".veStatusBar").contains(`5299`); cy.get(".tg-menu-bar").contains("Edit").click(); - cy.get(".tg-menu-bar-popover").contains("Copy").click(); + cy.get(".tg-menu-bar-popover").contains("Copy").realClick(); cy.contains("Selection Copied"); cy.get(".tg-menu-bar").contains("Edit").click(); - cy.get(".tg-menu-bar-popover").contains("Cut").click(); + cy.get(".tg-menu-bar-popover").contains("Cut").realClick(); cy.contains("Selection Cut"); cy.get(".veStatusBar").contains(`5288`); }); diff --git a/packages/ove/src/DigestTool/DigestTool.js b/packages/ove/src/DigestTool/DigestTool.js index 28322176..c1b1f6f2 100644 --- a/packages/ove/src/DigestTool/DigestTool.js +++ b/packages/ove/src/DigestTool/DigestTool.js @@ -1,13 +1,8 @@ -// import uniqid from "shortid"; -// import Ladder from "./Ladder"; -import { compose, withProps } from "recompose"; -// import selectionLayer from "../redux/selectionLayer"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { DataTable } from "@teselagen/ui"; import { getCutsiteType, getVirtualDigest } from "@teselagen/sequence-utils"; import CutsiteFilter from "../CutsiteFilter"; import Ladder from "./Ladder"; -// import getCutsiteType from "./getCutsiteType"; import { Tabs, Tab, @@ -27,18 +22,59 @@ export const DigestTool = props => { const [selectedTab, setSelectedTab] = useState("virtualDigest"); const { editorName, - // height = 100, dimensions = {}, - lanes, digestTool: { selectedFragment, computePartialDigest }, onDigestSave, - computePartialDigestDisabled, - computeDigestDisabled, updateComputePartialDigest, boxHeight, digestLaneRightClicked, - ladders + ladders, + sequenceData, + sequenceLength, + selectionLayerUpdate, + updateSelectedFragment } = props; + + const isCircular = sequenceData.circular; + const cutsites = sequenceData.cutsites; + const computePartialDigestDisabled = + cutsites.length > MAX_PARTIAL_DIGEST_CUTSITES; + const computeDigestDisabled = cutsites.length > MAX_DIGEST_CUTSITES; + + const lanes = useMemo(() => { + const { fragments } = getVirtualDigest({ + cutsites, + sequenceLength, + isCircular, + computePartialDigest, + computePartialDigestDisabled, + computeDigestDisabled + }); + const lanes = [ + fragments.map(f => ({ + ...f, + onFragmentSelect: () => { + selectionLayerUpdate({ + start: f.start, + end: f.end, + name: f.name + }); + updateSelectedFragment(f.Intentid); + } + })) + ]; + return lanes; + }, [ + computeDigestDisabled, + computePartialDigest, + computePartialDigestDisabled, + cutsites, + isCircular, + selectionLayerUpdate, + sequenceLength, + updateSelectedFragment + ]); + return (
{ - const isCircular = sequenceData.circular; - const cutsites = sequenceData.cutsites; - const computePartialDigestDisabled = - cutsites.length > MAX_PARTIAL_DIGEST_CUTSITES; - const computeDigestDisabled = cutsites.length > MAX_DIGEST_CUTSITES; - - const { fragments, overlappingEnzymes } = getVirtualDigest({ - cutsites, - sequenceLength, - isCircular, - computePartialDigest, - computePartialDigestDisabled, - computeDigestDisabled - }); - return { - computePartialDigestDisabled, - computeDigestDisabled, - lanes: [ - fragments.map(f => ({ - ...f, - onFragmentSelect: () => { - selectionLayerUpdate({ - start: f.start, - end: f.end, - name: f.name - }); - updateSelectedFragment(f.Intentid); - } - })) - ], - overlappingEnzymes - }; - } - ) -)(DigestTool); +export default withEditorInteractions(DigestTool); diff --git a/packages/ove/src/helperComponents/RemoveDuplicates/index.js b/packages/ove/src/helperComponents/RemoveDuplicates/index.js index 9ddbdef7..8b74a674 100644 --- a/packages/ove/src/helperComponents/RemoveDuplicates/index.js +++ b/packages/ove/src/helperComponents/RemoveDuplicates/index.js @@ -1,194 +1,178 @@ -import React from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { reduxForm } from "redux-form"; - import { wrapDialog, DataTable, - withSelectedEntities, SwitchField, - tgFormValues + useTableEntities } from "@teselagen/ui"; import { compose } from "redux"; import { Button, Classes, Popover } from "@blueprintjs/core"; import classNames from "classnames"; - import withEditorProps from "../../withEditorProps"; import { forEach, camelCase, startCase } from "lodash-es"; import { sizeSchema } from "../PropertiesDialog/utils"; import { getRangeLength } from "@teselagen/range-utils"; +import { useFormValue } from "../../utils/useFormValue"; + +const dialogFormName = "RemoveDuplicatesDialog"; +const dataTableFormName = "duplicatesToRemove"; +const checkboxStyle = { marginTop: 0, marginBottom: 0 }; + +const RemoveDuplicatesDialog = props => { + const { + type, + sequenceData = { sequence: "" }, + sequenceLength, + isProtein, + hideModal + } = props; -class RemoveDuplicatesDialog extends React.Component { - state = { - dups: [] - }; - componentDidMount() { - this.recomputeDups(); - } + const { selectedEntities } = useTableEntities(dataTableFormName); - checkboxStyle = { marginTop: 0, marginBottom: 0 }; + const ignoreName = useFormValue(dialogFormName, "ignoreName"); + const ignoreStartAndEnd = useFormValue(dialogFormName, "ignoreStartAndEnd"); + const ignoreStrand = useFormValue(dialogFormName, "ignoreStrand"); - delayedRecomputeDups = () => { - setTimeout(() => { - this.recomputeDups(); - }); - }; - recomputeDups = () => { - const { - // hideModal, - type, - sequenceData = { sequence: "" }, - // handleSubmit, - sequenceLength, - ignoreName, - ignoreStrand, - ignoreStartAndEnd - // circular, - // upsertFeature - } = this.props; + const recomputeDups = useCallback( + values => { + const ignoreName = values?.ignoreName; + const ignoreStartAndEnd = values?.ignoreStartAndEnd; + const ignoreStrand = values?.ignoreStrand; + const annotations = sequenceData[type]; + const newDups = []; + const seqsHashByStartEndStrandName = {}; + forEach(annotations, a => { + const hash = `${ignoreStartAndEnd ? "" : a.start}&${ + ignoreStartAndEnd ? "" : a.end + }&${ignoreStrand ? "" : a.strand}&${ignoreName ? "" : a.name}`; + if (seqsHashByStartEndStrandName[hash]) { + newDups.push({ ...a, size: getRangeLength(a, sequenceLength) }); + } else { + seqsHashByStartEndStrandName[hash] = true; + } + }); + return newDups; + }, + [sequenceData, sequenceLength, type] + ); - const annotations = sequenceData[type]; - const dups = []; - const seqsHashByStartEndStrandName = {}; - forEach(annotations, a => { - const hash = `${ignoreStartAndEnd ? "" : a.start}&${ - ignoreStartAndEnd ? "" : a.end - }&${ignoreStrand ? "" : a.strand}&${ignoreName ? "" : a.name}`; - if (seqsHashByStartEndStrandName[hash]) { - dups.push({ ...a, size: getRangeLength(a, sequenceLength) }); - } else { - seqsHashByStartEndStrandName[hash] = true; - } - }); - this.setState({ dups }); - }; - render() { - const { duplicatesToRemoveSelectedEntities, hideModal, type } = this.props; + const [dups, setDups] = useState(recomputeDups); + const selectedIds = useMemo(() => dups.map(d => d.id), [dups]); - const selectedIds = this.state.dups.map(d => d.id); + const fieldSubmit = useCallback( + (newVal, field) => { + const values = { + ignoreName, + ignoreStartAndEnd, + ignoreStrand, + [field]: newVal + }; + const newDups = recomputeDups(values); + setDups(newDups); + }, + [ignoreName, ignoreStartAndEnd, ignoreStrand, recomputeDups] + ); - const schema = { + const schema = useMemo( + () => ({ fields: [ - // ...(noColor - // ? [] - // : [ - // { - // path: "color", - // type: "string", - // render: color => { - // return ( - // - //
- // - // ); - // } - // } - // ]), { path: "name", type: "string" }, // ...(noType ? [] : [{ path: "type", type: "string" }]), - sizeSchema(this.props.isProtein), + sizeSchema(isProtein), { path: "strand", type: "string" } ] - }; - // const sequenceLength = sequenceData.sequence.length; - // const isCirc = (this.state || {}).circular; - return ( -
- {/* {dups.map((d) => { - return
+ }), + [isProtein] + ); -
- })} */} - + +
+ } + content={ +
+
Ignore These Fields While Finding Duplicates:
+
+ fieldSubmit(newVal, "ignoreName")} + style={checkboxStyle} + name="ignoreName" + label="Name" + /> + fieldSubmit(newVal, "ignoreStrand")} + style={checkboxStyle} + name="ignoreStrand" + label="Strand" + /> + + fieldSubmit(newVal, "ignoreStartAndEnd") + } + style={checkboxStyle} + name="ignoreStartAndEnd" + label="Start and End" + /> +
+ } /> -
{ + props[camelCase(`delete_${type}`).slice(0, -1)]( + Object.keys(selectedEntities || {}) + ); + window.toastr.success( + `Successfully Deleted ${ + Object.keys(selectedEntities || {}).length + } ${startCase(type)}` + ); + hideModal(); }} + disabled={!Object.keys(selectedEntities || {}).length} > - } - content={ -
-
Ignore These Fields While Finding Duplicates:
-

- - - -
- } - >
- - -
+ Remove {Object.keys(selectedEntities || {}).length} Duplicates +
- ); - } -} +
+ ); +}; export default compose( wrapDialog(), withEditorProps, - - withSelectedEntities("duplicatesToRemove"), - - reduxForm({ - form: "RemoveDuplicatesDialog" - }), - tgFormValues("ignoreName", "ignoreStrand", "ignoreStartAndEnd") + reduxForm({ form: dialogFormName }) )(RemoveDuplicatesDialog); diff --git a/packages/ove/src/utils/useFormValue.js b/packages/ove/src/utils/useFormValue.js new file mode 100644 index 00000000..ec654230 --- /dev/null +++ b/packages/ove/src/utils/useFormValue.js @@ -0,0 +1,7 @@ +/* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */ +import { useSelector } from "react-redux"; +import { get } from "lodash"; + +export const useFormValue = (formName, field) => { + return useSelector(state => get(state.form?.[formName]?.values, field)); +}; diff --git a/packages/ui/demo/src/examples/DataTable.js b/packages/ui/demo/src/examples/DataTable.js index b3500c8f..be77b6e0 100644 --- a/packages/ui/demo/src/examples/DataTable.js +++ b/packages/ui/demo/src/examples/DataTable.js @@ -586,7 +586,7 @@ const DataTableDemo = () => { onRefresh={onRefresh} onSingleRowSelect={noop} selectAllByDefault={selectAllByDefault} - initialSelectedIds={selectedIds} + selectedIds={selectedIds} shouldShowSubComponent={r => r.id !== 1} showCount={showCount} SubComponent={withSubComponent ? SubComp : undefined} diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index 6d03ed8d..3a33a4c2 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -425,7 +425,7 @@ const DataTable = ({ hideSelectedCount = isSimple, hideSetPageSize, hideTotalPages, - initialSelectedIds, + selectedIds, isCellEditable, isCopyable = true, isEntityDisabled = noop, @@ -1752,17 +1752,15 @@ const DataTable = ({ // We need to make sure this only runs at the beggining useEffect(() => { - if (initialSelectedIds) { - if (alreadySelected.current) return; - setSelectedIds(initialSelectedIds); - alreadySelected.current = true; + if (selectedIds) { + setSelectedIds(selectedIds); } if (selectAllByDefault && entities && entities.length) { if (alreadySelected.current) return; setSelectedIds(entities.map(getIdOrCodeOrIndex)); alreadySelected.current = true; } - }, [entities, initialSelectedIds, selectAllByDefault, setSelectedIds]); + }, [entities, selectedIds, selectAllByDefault, setSelectedIds]); const TheadComponent = useCallback( ({ className, style, children }) => { From b7d52156617e2bee9c95138218d3ec3a524487a6 Mon Sep 17 00:00:00 2001 From: Thomas Willheim Date: Wed, 11 Dec 2024 09:28:32 -0800 Subject: [PATCH 03/14] chore: remove unecessary if statement --- packages/ui/src/DataTable/index.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index 3a33a4c2..6559837c 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -501,16 +501,13 @@ const DataTable = ({ // This is because we need to maintain the reduxFormSelectedEntityIdMap and // allOrderedEntities updated useEffect(() => { - if (change) { - change("allOrderedEntities", entitiesAcrossPages); - if (entities.length === 0 || isEmpty(reduxFormSelectedEntityIdMap)) - return; - changeSelectedEntities({ - idMap: reduxFormSelectedEntityIdMap, - entities, - change - }); - } + change("allOrderedEntities", entitiesAcrossPages); + if (entities.length === 0 || isEmpty(reduxFormSelectedEntityIdMap)) return; + changeSelectedEntities({ + idMap: reduxFormSelectedEntityIdMap, + entities, + change + }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [entitiesAcrossPages, reduxFormSelectedEntityIdMap, change]); From dc743577a13b18be656e6d1ba3074564a9134122 Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Wed, 11 Dec 2024 15:07:24 -0300 Subject: [PATCH 04/14] fix errors --- packages/ove/cypress/e2e/alignment.spec.js | 4 ++-- packages/ove/cypress/e2e/menuBar.spec.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ove/cypress/e2e/alignment.spec.js b/packages/ove/cypress/e2e/alignment.spec.js index 49226505..6976d4b4 100644 --- a/packages/ove/cypress/e2e/alignment.spec.js +++ b/packages/ove/cypress/e2e/alignment.spec.js @@ -105,7 +105,7 @@ describe("alignment", function () { cy.get("body").type("{shift}", { release: false }); cy.scrollAlignmentToPercent(0.99); cy.contains(`[data-alignment-track-index="1"] text`, 3510).click(); - cy.get(`[title="Selecting 3499 bps from 11 to 3509"]`); + cy.get(`[title="Selecting 3500 bps from 11 to 3510"]`); }); it("the alignment should show axis numbers correctly", function () { cy.visit("#/Alignment?alignmentDataId=39"); @@ -124,7 +124,7 @@ describe("alignment", function () { cy.get("body").type("{shift}", { release: false }); cy.scrollAlignmentToPercent(0.99); cy.contains(`[data-alignment-track-index="1"] text`, 3510).click(); - cy.get(`[title="Selecting 3454 bps from 56 to 3509"]`); + cy.get(`[title="Selecting 3455 bps from 56 to 3510"]`); }); it("scrolls the yellow scroll handle correctly", function () { cy.visit("#/Alignment"); diff --git a/packages/ove/cypress/e2e/menuBar.spec.js b/packages/ove/cypress/e2e/menuBar.spec.js index e34bca0b..000c6590 100644 --- a/packages/ove/cypress/e2e/menuBar.spec.js +++ b/packages/ove/cypress/e2e/menuBar.spec.js @@ -272,7 +272,7 @@ describe("menuBar", function () { cy.selectRange(2, 5); cy.get(".tg-menu-bar").contains("Edit").trigger("mouseover"); - cy.get(".tg-menu-bar-popover").contains("Cut").click(); + cy.get(".tg-menu-bar-popover").contains("Cut").realClick(); cy.get(".tg-menu-bar").contains("File").click(); cy.get(`[cmd="saveSequence"]`).should("not.have.class", "bp3-disabled"); From 1ea85c2518edeab8a92f64090c4d939ba3591857 Mon Sep 17 00:00:00 2001 From: Thomas Willheim Date: Wed, 11 Dec 2024 10:52:27 -0800 Subject: [PATCH 05/14] feat: add isBeingCalledExcessively utility to prevent excessive function calls in DataTable --- packages/ui/src/DataTable/index.js | 3 +++ .../ui/src/utils/isBeingCalledExcessively.js | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 packages/ui/src/utils/isBeingCalledExcessively.js diff --git a/packages/ui/src/DataTable/index.js b/packages/ui/src/DataTable/index.js index 6559837c..bb0b34d4 100644 --- a/packages/ui/src/DataTable/index.js +++ b/packages/ui/src/DataTable/index.js @@ -102,6 +102,7 @@ import { useColumns } from "./Columns"; import { formValueSelector, change as _change } from "redux-form"; import { throwFormError } from "../throwFormError"; import { isObservableArray, toJS } from "mobx"; +import { isBeingCalledExcessively } from "../utils/isBeingCalledExcessively"; enablePatches(); const IS_LINUX = window.navigator.platform.toLowerCase().search("linux") > -1; @@ -501,6 +502,7 @@ const DataTable = ({ // This is because we need to maintain the reduxFormSelectedEntityIdMap and // allOrderedEntities updated useEffect(() => { + isBeingCalledExcessively({ uniqName: `dt_entities_${formName}` }); change("allOrderedEntities", entitiesAcrossPages); if (entities.length === 0 || isEmpty(reduxFormSelectedEntityIdMap)) return; changeSelectedEntities({ @@ -521,6 +523,7 @@ const DataTable = ({ } else { newTableConfig = getTableConfigFromStorage(formName); } + isBeingCalledExcessively({ uniqName: `dt_setTableConfig_${formName}` }); // if the tableConfig is the same as the newTableConfig, don't update setTableConfig(prev => { if (!newTableConfig) { diff --git a/packages/ui/src/utils/isBeingCalledExcessively.js b/packages/ui/src/utils/isBeingCalledExcessively.js new file mode 100644 index 00000000..beded458 --- /dev/null +++ b/packages/ui/src/utils/isBeingCalledExcessively.js @@ -0,0 +1,24 @@ +const keyCount = {}; +export const isBeingCalledExcessively = ({ uniqName }) => { + if (process.env.NODE_ENV !== "development") { + return; + } + if (!uniqName) { + throw new Error("uniqName is required"); + } + // if this function is hit more than 10 times in a row in 2 seconds with the same uniqName then throw an error + if (keyCount[uniqName + "_timeout"]) { + clearTimeout(keyCount[uniqName + "_timeout"]); + } + keyCount[uniqName] = keyCount[uniqName] || 0; + keyCount[uniqName]++; + + keyCount[uniqName + "_timeout"] = setTimeout(() => { + keyCount[uniqName] = 0; + }, 2000); + + if (keyCount[uniqName] > 20) { + keyCount[uniqName] = 0; + throw new Error(`isBeingCalledExcessively: ${uniqName}`); + } +}; From 86a17922db5f3d66a3f119d16fe19940cc76ee66 Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Thu, 12 Dec 2024 09:33:12 -0300 Subject: [PATCH 06/14] fix: Digest Tool --- packages/ove/cypress/e2e/oligoMode.spec.js | 2 +- packages/ove/cypress/e2e/properties.spec.js | 6 +- packages/ove/src/CreateAnnotationsPage.js | 2 +- packages/ove/src/DigestTool/DigestTool.js | 57 ++-- .../PropertiesDialog/CutsiteProperties.js | 261 +++++++++--------- .../SingleEnzymeCutsiteInfo.js | 110 ++++---- .../PropertiesDialog/index.js | 229 +++++++-------- 7 files changed, 339 insertions(+), 328 deletions(-) diff --git a/packages/ove/cypress/e2e/oligoMode.spec.js b/packages/ove/cypress/e2e/oligoMode.spec.js index a8429f4d..6c106c28 100644 --- a/packages/ove/cypress/e2e/oligoMode.spec.js +++ b/packages/ove/cypress/e2e/oligoMode.spec.js @@ -15,7 +15,7 @@ describe("oligo mode editing in OVE", function () { // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(0); cy.focused().type("gatccaauu{enter}"); - cy.contains("Selecting 9 bps from 10 to 18"); //the t's should be filtered out + cy.contains("Selecting 10 bps from 10 to 19"); //the t's should be filtered out cy.contains("gatccaauu"); cy.get(".veTabProperties").click(); cy.contains("Circular/Linear:").should("not.exist"); diff --git a/packages/ove/cypress/e2e/properties.spec.js b/packages/ove/cypress/e2e/properties.spec.js index 7bb4db53..087a9cd1 100644 --- a/packages/ove/cypress/e2e/properties.spec.js +++ b/packages/ove/cypress/e2e/properties.spec.js @@ -21,7 +21,7 @@ describe("properties", function () { cy.contains("textarea", `primer_bind complement(10..20)`); cy.contains("textarea", `/label="fakeprimer"`); }); - it(`should be able to delete a feature from the properties tab and not have the delete button still enabled; + it(`should be able to delete a feature from the properties tab and not have the delete button still enabled; - have the number of features correctly displayed -not be able to create a new feature if sequenceLength === 0`, () => { cy.get(".veTabProperties").click(); @@ -38,7 +38,7 @@ describe("properties", function () { cy.get(".tg-menu-bar").contains("Edit").click(); cy.get(".tg-menu-bar-popover").contains("Select All").click(); cy.get(".veSelectionLayer").first().trigger("contextmenu", { force: true }); - cy.get(".bp3-menu-item").contains("Cut").click(); + cy.get(".bp3-menu-item").contains("Cut").realClick(); cy.get(".tgNewAnnBtn").should("have.class", "bp3-disabled"); }); it(`a custom properties tab should be able to be added`, () => { @@ -53,7 +53,7 @@ describe("properties", function () { cy.get(".circularLinearSelect select").select("Linear"); cy.contains(".bp3-dialog", "Truncate Annotations").should("be.visible"); }); - it(`we should be able to view and edit a description in general properties + it(`we should be able to view and edit a description in general properties and have that visible within the genbank view as well we should be able to edit a description in general properties, not make any changes, hit ok, and have the description not clear (bug! https://github.com/TeselaGen/lims/issues/5492) // and have that visible within the genbank view as well`, () => { cy.get(".veTabProperties").click(); diff --git a/packages/ove/src/CreateAnnotationsPage.js b/packages/ove/src/CreateAnnotationsPage.js index 7e7a9838..9f9d9833 100644 --- a/packages/ove/src/CreateAnnotationsPage.js +++ b/packages/ove/src/CreateAnnotationsPage.js @@ -48,7 +48,7 @@ export default compose( }))} withCheckboxes schema={annotationType === "feature" ? schemaFeatures : schemaOther} - > + />
{ + onFragmentSelect(); +}; export const DigestTool = props => { const [selectedTab, setSelectedTab] = useState("virtualDigest"); @@ -31,7 +34,7 @@ export const DigestTool = props => { ladders, sequenceData, sequenceLength, - selectionLayerUpdate, + selectionLayerUpdate: _selectionLayerUpdate, updateSelectedFragment } = props; @@ -40,7 +43,12 @@ export const DigestTool = props => { const computePartialDigestDisabled = cutsites.length > MAX_PARTIAL_DIGEST_CUTSITES; const computeDigestDisabled = cutsites.length > MAX_DIGEST_CUTSITES; + // The selection layer update function is memoized to prevent re-renders + // It changes triggered by the DataTables below + const selectionLayerUpdate = useStableReference(_selectionLayerUpdate); + // This useMemo might not be necessary once if we figure out + // why the DataTables below triggers a re-render outside of them. const lanes = useMemo(() => { const { fragments } = getVirtualDigest({ cutsites, @@ -50,11 +58,11 @@ export const DigestTool = props => { computePartialDigestDisabled, computeDigestDisabled }); - const lanes = [ + const _lanes = [ fragments.map(f => ({ ...f, onFragmentSelect: () => { - selectionLayerUpdate({ + selectionLayerUpdate.current({ start: f.start, end: f.end, name: f.name @@ -63,7 +71,7 @@ export const DigestTool = props => { } })) ]; - return lanes; + return _lanes; }, [ computeDigestDisabled, computePartialDigest, @@ -75,6 +83,25 @@ export const DigestTool = props => { updateSelectedFragment ]); + // Same comment as above + const digestInfoLanes = useMemo( + () => + lanes[0].map(({ id, cut1, cut2, start, end, size, ...rest }) => { + return { + ...rest, + id, + start, + end, + length: size, + leftCutter: cut1.restrictionEnzyme.name, + rightCutter: cut2.restrictionEnzyme.name, + leftOverhang: getCutsiteType(cut1.restrictionEnzyme), + rightOverhang: getCutsiteType(cut2.restrictionEnzyme) + }; + }), + [lanes] + ); + return (
{ maxHeight={400} // noFooter withSearch={false} - onSingleRowSelect={({ onFragmentSelect }) => { - onFragmentSelect(); - }} + onSingleRowSelect={onSingleSelectRow} formName="digestInfoTable" - entities={lanes[0].map( - ({ id, cut1, cut2, start, end, size, ...rest }) => { - return { - ...rest, - id, - start, - end, - length: size, - leftCutter: cut1.restrictionEnzyme.name, - rightCutter: cut2.restrictionEnzyme.name, - leftOverhang: getCutsiteType(cut1.restrictionEnzyme), - rightOverhang: getCutsiteType(cut2.restrictionEnzyme) - }; - } - )} + entities={digestInfoLanes} schema={schema} /> } diff --git a/packages/ove/src/helperComponents/PropertiesDialog/CutsiteProperties.js b/packages/ove/src/helperComponents/PropertiesDialog/CutsiteProperties.js index 24350e73..677305aa 100644 --- a/packages/ove/src/helperComponents/PropertiesDialog/CutsiteProperties.js +++ b/packages/ove/src/helperComponents/PropertiesDialog/CutsiteProperties.js @@ -1,16 +1,12 @@ -import React from "react"; -import { - DataTable, - withSelectedEntities, - createCommandMenu -} from "@teselagen/ui"; -import { map, get } from "lodash-es"; +import React, { useCallback, useMemo } from "react"; +import { DataTable, createCommandMenu } from "@teselagen/ui"; +import { get } from "lodash-es"; import CutsiteFilter from "../../CutsiteFilter"; import { Button, ButtonGroup } from "@blueprintjs/core"; import { connectToEditor } from "../../withEditorProps"; import { compose } from "recompose"; import selectors from "../../selectors"; -import commands from "../../commands"; +import _commands from "../../commands"; import { userDefinedHandlersAndOpts } from "../../Editor/userDefinedHandlersAndOpts"; import { pick } from "lodash-es"; import SingleEnzymeCutsiteInfo from "./SingleEnzymeCutsiteInfo"; @@ -18,105 +14,107 @@ import { withRestrictionEnzymes } from "../../CutsiteFilter/withRestrictionEnzym import { cutsitesSubmenu } from "../../MenuBar/viewSubmenu"; import { getVisFilter } from "./GenericAnnotationProperties"; -class CutsiteProperties extends React.Component { - constructor(props) { - super(props); - this.commands = commands(this); - } +const schema = { + fields: [ + { path: "name", type: "string" }, + { path: "numberOfCuts", type: "number" }, + { path: "groups", type: "string" } + ] +}; - SubComponent = row => { - return ( - - ); - }; +const defaultValues = { order: ["numberOfCuts"] }; - schema = { - fields: [ - { path: "name", type: "string" }, - { path: "numberOfCuts", type: "number" }, - { path: "groups", type: "string" } - ] - }; +const CutsiteProperties = props => { + const commands = _commands({ props }); + const { + allRestrictionEnzymes, + allCutsites, + annotationVisibilityShow, + createNewDigest, + dispatch, + editorName, + filteredCutsites, + selectedAnnotationId + } = props; - onChangeHook = () => { - this.props.annotationVisibilityShow("cutsites"); - }; - render() { - const { + const SubComponent = useCallback( + row => ( + + ), + [ + allCutsites, + allRestrictionEnzymes, + dispatch, editorName, - createNewDigest, - filteredCutsites: allCutsites, + filteredCutsites, selectedAnnotationId - } = this.props; + ] + ); - const { cutsitesByName, cutsitesById } = allCutsites; - const cutsitesToUse = map(cutsitesByName, cutsiteGroup => { - const name = cutsiteGroup[0].restrictionEnzyme.name; - let groups = ""; - const exisitingEnzymeGroups = window.getExistingEnzymeGroups(); + const onChangeHook = useCallback(() => { + annotationVisibilityShow("cutsites"); + }, [annotationVisibilityShow]); - Object.keys(exisitingEnzymeGroups).forEach(key => { - if (exisitingEnzymeGroups[key].includes(name)) groups += key; - groups += " "; - }); + const { cutsitesByName, cutsitesById } = filteredCutsites; - return { - cutsiteGroup, - id: name, - name, - numberOfCuts: cutsiteGroup.length, - enzyme: cutsiteGroup[0].restrictionEnzyme, - groups - // size: getRangeLength(cutsiteGroup, sequenceData.sequence.length) - }; - }); - return ( - <> -
- {/* + const cutsitesToUse = useMemo( + () => + Object.values(cutsitesByName || {}).map(cutsiteGroup => { + const name = cutsiteGroup[0].restrictionEnzyme.name; + let groups = ""; + const exisitingEnzymeGroups = window.getExistingEnzymeGroups(); + + Object.keys(exisitingEnzymeGroups).forEach(key => { + if (exisitingEnzymeGroups[key].includes(name)) groups += key; + groups += " "; + }); + + return { + cutsiteGroup, + id: name, + name, + numberOfCuts: cutsiteGroup.length, + enzyme: cutsiteGroup[0].restrictionEnzyme, + groups + }; + }), + [cutsitesByName] + ); + + const selectedIds = useMemo( + () => get(cutsitesById[selectedAnnotationId], "restrictionEnzyme.name"), + [cutsitesById, selectedAnnotationId] + ); + + return ( + <> +
+ {getVisFilter( + createCommandMenu(cutsitesSubmenu, commands, { + useTicks: true + }) + )} + - {/* */} - - - -
- + - - ); - } -} +
+ + + ); +}; export default compose( connectToEditor((editorState, ownProps) => { @@ -180,6 +172,5 @@ export default compose( cutsites: cutsites.cutsitesArray }; }), - withRestrictionEnzymes, - withSelectedEntities("cutsiteProperties") + withRestrictionEnzymes )(CutsiteProperties); diff --git a/packages/ove/src/helperComponents/PropertiesDialog/SingleEnzymeCutsiteInfo.js b/packages/ove/src/helperComponents/PropertiesDialog/SingleEnzymeCutsiteInfo.js index fa7a793e..0168b607 100644 --- a/packages/ove/src/helperComponents/PropertiesDialog/SingleEnzymeCutsiteInfo.js +++ b/packages/ove/src/helperComponents/PropertiesDialog/SingleEnzymeCutsiteInfo.js @@ -1,11 +1,17 @@ -import React from "react"; +import React, { useCallback, useMemo } from "react"; import { DataTable } from "@teselagen/ui"; - import { CutsiteTag } from "../../CutsiteFilter/AdditionalCutsiteInfoDialog"; - import EnzymeViewer from "../../EnzymeViewer"; import { getEnzymeAliases } from "../../utils/editorUtils"; +const schema = { + fields: [ + { path: "topSnipPosition", displayName: "Top Snip", type: "string" }, + { path: "position", type: "string" }, + { path: "strand", type: "string" } + ] +}; + export default function SingleEnzymeCutsiteInfo({ cutsiteGroup, enzyme, @@ -16,40 +22,52 @@ export default function SingleEnzymeCutsiteInfo({ allCutsites, filteredCutsites: { cutsitesByName: cutsitesByNameActive } }) { - const onRowSelect = ([record]) => { - if (!record) return; + const onRowSelect = useCallback( + ([record]) => { + if (!record) return; - dispatch({ - type: "CARET_POSITION_UPDATE", - payload: record.topSnipPosition, - meta: { - editorName - } - }); - }; - const aliases = getEnzymeAliases(enzyme); - const entities = cutsiteGroup - .sort((a, b) => a.topSnipPosition - b.topSnipPosition) - .map( - ({ - restrictionEnzyme: { forwardRegex, reverseRegex } = {}, - forward, - id, - topSnipBeforeBottom, - topSnipPosition, - bottomSnipPosition - }) => { - return { - id, - topSnipPosition, - position: topSnipBeforeBottom - ? topSnipPosition + " - " + bottomSnipPosition - : bottomSnipPosition + " - " + topSnipPosition, - strand: - forwardRegex === reverseRegex ? "Palindromic" : forward ? "1" : "-1" - }; - } - ); + dispatch({ + type: "CARET_POSITION_UPDATE", + payload: record.topSnipPosition, + meta: { + editorName + } + }); + }, + [dispatch, editorName] + ); + + const aliases = useMemo(() => getEnzymeAliases(enzyme), [enzyme]); + const entities = useMemo( + () => + cutsiteGroup + .sort((a, b) => a.topSnipPosition - b.topSnipPosition) + .map( + ({ + restrictionEnzyme: { forwardRegex, reverseRegex } = {}, + forward, + id, + topSnipBeforeBottom, + topSnipPosition, + bottomSnipPosition + }) => { + return { + id, + topSnipPosition, + position: topSnipBeforeBottom + ? topSnipPosition + " - " + bottomSnipPosition + : bottomSnipPosition + " - " + topSnipPosition, + strand: + forwardRegex === reverseRegex + ? "Palindromic" + : forward + ? "1" + : "-1" + }; + } + ), + [cutsiteGroup] + ); return (
@@ -61,14 +79,12 @@ export default function SingleEnzymeCutsiteInfo({ > {enzyme && ( )} -

+
{entities && !!entities.length && (
+ /> ); })}
@@ -117,14 +133,6 @@ export default function SingleEnzymeCutsiteInfo({ ); } -const schema = { - fields: [ - { path: "topSnipPosition", displayName: "Top Snip", type: "string" }, - { path: "position", type: "string" }, - { path: "strand", type: "string" } - ] -}; - // export default compose( // withEditorProps, // withRestrictionEnzymes diff --git a/packages/ove/src/helperComponents/PropertiesDialog/index.js b/packages/ove/src/helperComponents/PropertiesDialog/index.js index 07c0b3a7..0f402b79 100644 --- a/packages/ove/src/helperComponents/PropertiesDialog/index.js +++ b/packages/ove/src/helperComponents/PropertiesDialog/index.js @@ -18,11 +18,11 @@ import { pick } from "lodash-es"; const PropertiesContainer = Comp => props => { const { additionalFooterEls, additionalHeaderEls, ...rest } = props; return ( - + <> {additionalHeaderEls} {additionalFooterEls} - + ); }; const allTabs = { @@ -35,129 +35,130 @@ const allTabs = { orfs: PropertiesContainer(OrfProperties), genbank: PropertiesContainer(GenbankView) }; -export class PropertiesDialog extends React.Component { - render() { - const { - propertiesTool = {}, - propertiesViewTabUpdate, - dimensions = {}, - height, - editorName, - onSave, - showReadOnly, - showAvailability, - isProtein, - annotationsToSupport = {}, - disableSetReadOnly, - propertiesList = [ - "general", - "features", - "parts", - "primers", - "translations", - "cutsites", - "orfs", - "genbank" - ], - closePanelButton - } = { ...this.props, ...this.props.PropertiesProps }; - const { width, height: heightFromDim } = dimensions; +export const PropertiesDialog = props => { + const { + propertiesTool = {}, + propertiesViewTabUpdate, + dimensions = {}, + height, + editorName, + onSave, + showReadOnly, + showAvailability, + isProtein, + annotationsToSupport = {}, + disableSetReadOnly, + propertiesList = [ + "general", + "features", + "parts", + "primers", + "translations", + "cutsites", + "orfs", + "genbank" + ], + closePanelButton + } = { ...props, ...props.PropertiesProps }; - let { tabId, selectedAnnotationId } = propertiesTool; - if ( - propertiesList - .map(nameOrOverride => nameOrOverride.name || nameOrOverride) - .indexOf(tabId) === -1 - ) { - tabId = propertiesList[0].name || propertiesList[0]; + const { width, height: heightFromDim } = dimensions; + + let { tabId, selectedAnnotationId } = propertiesTool; + if ( + propertiesList + .map(nameOrOverride => nameOrOverride.name || nameOrOverride) + .indexOf(tabId) === -1 + ) { + tabId = propertiesList[0].name || propertiesList[0]; + } + + const propertiesTabs = flatMap(propertiesList, nameOrOverride => { + if (annotationsToSupport[nameOrOverride] === false) { + return []; } - const propertiesTabs = flatMap(propertiesList, nameOrOverride => { - if (annotationsToSupport[nameOrOverride] === false) { - return []; - } - const name = nameOrOverride.name || nameOrOverride; - const Comp = nameOrOverride.Comp || allTabs[name]; - if (isProtein) { - if ( - name === "translations" || - name === "orfs" || - name === "primers" || - name === "cutsites" - ) { - return null; - } + const name = nameOrOverride.name || nameOrOverride; + const Comp = nameOrOverride.Comp || allTabs[name]; + if (isProtein) { + if ( + name === "translations" || + name === "orfs" || + name === "primers" || + name === "cutsites" + ) { + return null; } - const title = (() => { - if (nameOrOverride.Comp) return name; //just use the user supplied name because this is a custom panel - if (name === "orfs") return "ORFs"; - if (name === "cutsites") return "Cut Sites"; - return startCase(name); - })(); - return ( - - } - /> - ); - }); - const heightToUse = Math.max(0, Number((heightFromDim || height) - 30)); + } + const title = (() => { + if (nameOrOverride.Comp) return name; //just use the user supplied name because this is a custom panel + if (name === "orfs") return "ORFs"; + if (name === "cutsites") return "Cut Sites"; + return startCase(name); + })(); + return ( + + } + /> + ); + }); + const heightToUse = Math.max(0, Number((heightFromDim || height) - 30)); + return ( +
+ {closePanelButton}
- {closePanelButton} -
- {propertiesTabs.length ? ( - - - {propertiesTabs} - - - ) : ( -
- No Properties to display -
- )} -
+ {propertiesTabs.length ? ( + + + {propertiesTabs} + + + ) : ( +
+ No Properties to display +
+ )}
- ); - } -} +
+ ); +}; export default compose( connectToEditor(({ propertiesTool, annotationsToSupport }) => { From b0f357d8c782d11537721d7180c7bc694413dc21 Mon Sep 17 00:00:00 2001 From: Thomas Willheim Date: Mon, 16 Dec 2024 14:12:37 -0800 Subject: [PATCH 07/14] test fixes --- packages/ove/cypress/e2e/cutsiteInfoView.spec.js | 2 +- packages/ove/cypress/e2e/oligoMode.spec.js | 2 +- packages/ove/cypress/e2e/properties.spec.js | 5 +++-- packages/ove/src/withEditorInteractions/index.js | 15 ++++++++------- packages/ui/cypress/e2e/EditableCellTable.spec.js | 6 ++++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/ove/cypress/e2e/cutsiteInfoView.spec.js b/packages/ove/cypress/e2e/cutsiteInfoView.spec.js index c0064f92..0bed5503 100644 --- a/packages/ove/cypress/e2e/cutsiteInfoView.spec.js +++ b/packages/ove/cypress/e2e/cutsiteInfoView.spec.js @@ -17,7 +17,7 @@ describe("cutsiteInfoView", function () { cy.get(`[data-test="cutsiteToolDropdown"]`).click(); cy.get(`.veToolbarCutsiteFilterHolder input`).type("{selectAll}esp3i"); cy.contains(`These Hidden enzymes match`); - cy.contains("Esp3I (2 cuts)").click(); + cy.contains("Esp3I (2 cuts)").click({ force: true }); cy.get(`.veToolbarCutsiteFilterHolder input`).should("not.exist"); //clicking the hidden enzyme should close the filter cy.contains("Esp3I (2 cuts) hidden"); cy.get( diff --git a/packages/ove/cypress/e2e/oligoMode.spec.js b/packages/ove/cypress/e2e/oligoMode.spec.js index 6c106c28..09098b05 100644 --- a/packages/ove/cypress/e2e/oligoMode.spec.js +++ b/packages/ove/cypress/e2e/oligoMode.spec.js @@ -15,7 +15,7 @@ describe("oligo mode editing in OVE", function () { // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(0); cy.focused().type("gatccaauu{enter}"); - cy.contains("Selecting 10 bps from 10 to 19"); //the t's should be filtered out + cy.contains("Selecting 9 bps from 10 to 18"); cy.contains("gatccaauu"); cy.get(".veTabProperties").click(); cy.contains("Circular/Linear:").should("not.exist"); diff --git a/packages/ove/cypress/e2e/properties.spec.js b/packages/ove/cypress/e2e/properties.spec.js index 087a9cd1..a034ff18 100644 --- a/packages/ove/cypress/e2e/properties.spec.js +++ b/packages/ove/cypress/e2e/properties.spec.js @@ -33,11 +33,12 @@ describe("properties", function () { cy.get(`.tgDeleteAnnsBtn`).should("not.have.class", "bp3-disabled").click(); cy.get(`.tgDeleteAnnsBtn`).should("have.class", "bp3-disabled"); - cy.get(".tgNewAnnBtn").should("not.have.class", "bp3-disabled"); cy.get(".tg-menu-bar").contains("Edit").click(); cy.get(".tg-menu-bar-popover").contains("Select All").click(); - cy.get(".veSelectionLayer").first().trigger("contextmenu", { force: true }); + cy.get( + ".veSelectionLayer:contains(Selecting 5299 bps from 1 to 5299)" + ).trigger("contextmenu", { force: true }); cy.get(".bp3-menu-item").contains("Cut").realClick(); cy.get(".tgNewAnnBtn").should("have.class", "bp3-disabled"); }); diff --git a/packages/ove/src/withEditorInteractions/index.js b/packages/ove/src/withEditorInteractions/index.js index 734f7a84..361eaa98 100644 --- a/packages/ove/src/withEditorInteractions/index.js +++ b/packages/ove/src/withEditorInteractions/index.js @@ -602,8 +602,13 @@ function VectorInteractionHOC(Component /* options */) { return new Clipboard(`.${className}`, { action: () => action, text: () => { - const { selectionLayer, editorName, store } = this.props; - const { sequenceData, copyOptions } = + if (action === "copy") { + document.body.addEventListener("copy", this.handleCopy); + } else { + document.body.addEventListener("cut", this.handleCut); + } + const { editorName, store } = this.props; + const { sequenceData, copyOptions, selectionLayer } = store.getState().VectorEditor[editorName]; const selectedSeqData = getSequenceDataBetweenRange( @@ -628,11 +633,7 @@ function VectorInteractionHOC(Component /* options */) { sequenceData ); this.sequenceDataToCopy = sequenceDataToCopy; - if (action === "copy") { - document.body.addEventListener("copy", this.handleCopy); - } else { - document.body.addEventListener("cut", this.handleCut); - } + if (window.Cypress) { window.Cypress.textToCopy = sequenceDataToCopy.textToCopy; window.Cypress.seqDataToCopy = sequenceDataToCopy; diff --git a/packages/ui/cypress/e2e/EditableCellTable.spec.js b/packages/ui/cypress/e2e/EditableCellTable.spec.js index b0420794..fc314531 100644 --- a/packages/ui/cypress/e2e/EditableCellTable.spec.js +++ b/packages/ui/cypress/e2e/EditableCellTable.spec.js @@ -1,4 +1,5 @@ import path from "path"; +import os from "os"; describe("EditableCellTable.spec", () => { it(`cell checkboxes and the header checkbox should work`, () => { @@ -240,8 +241,9 @@ describe("EditableCellTable.spec", () => { ); }); it(`undo/redo should work`, () => { - const undoCmd = [`Meta`, `z`]; - const redoCmd = [`Meta`, `Shift`, `z`]; + const IS_LINUX = os.platform().toLowerCase().search("linux") > -1; + const undoCmd = IS_LINUX ? [`Alt`, `z`] : [`Meta`, `z`]; + const redoCmd = IS_LINUX ? [`Alt`, `Shift`, `z`] : [`Meta`, `Shift`, `z`]; cy.visit("#/DataTable%20-%20EditableCellTable"); cy.get(`.rt-td:contains(tom88)`).dblclick(); cy.focused().type("{selectall}tasty55{enter}"); From c150d2ad51d60f7459e175e9f220c2861b248a40 Mon Sep 17 00:00:00 2001 From: Guillermo Espinosa Date: Tue, 17 Dec 2024 13:05:37 -0300 Subject: [PATCH 08/14] refactor: AlignmentDemo --- packages/ove/demo/src/AlignmentDemo.js | 566 ++++++++++-------- packages/ove/demo/src/index.js | 2 +- packages/ove/demo/src/utils/useToggle.js | 216 +++++++ .../ui/src/DataTable/utils/queryParams.js | 1 + 4 files changed, 538 insertions(+), 247 deletions(-) create mode 100644 packages/ove/demo/src/utils/useToggle.js diff --git a/packages/ove/demo/src/AlignmentDemo.js b/packages/ove/demo/src/AlignmentDemo.js index 8fbdd7c1..1d690f6f 100644 --- a/packages/ove/demo/src/AlignmentDemo.js +++ b/packages/ove/demo/src/AlignmentDemo.js @@ -1,15 +1,19 @@ -import { setupOptions, setParamsIfNecessary } from "./utils/setupOptions"; -import React from "react"; +import React, { useEffect, useMemo, useState } from "react"; import store from "./store"; import msaAlignment from "./exampleData/msaAlignment.json"; import pairwiseAlignment from "./exampleData/pairwiseAlignment.json"; import sangerAlignment from "./exampleData/sangerAlignment.json"; import msaAlignmentWithGaps from "./exampleData/msaAlignment_withGaps.json"; import { addAlignment, AlignmentView /* updateEditor */ } from "../../src/"; -import renderToggle from "./utils/renderToggle"; -import { BPSelect } from "@teselagen/ui"; +import { useToggle } from "./utils/useToggle"; +import { + BPSelect, + getCurrentParamsFromUrl, + setCurrentParamsOnUrl +} from "@teselagen/ui"; import pairwiseAlignment2 from "./exampleData/pairwiseAlignment2.json"; import { Button } from "@blueprintjs/core"; +import { difference } from "lodash-es"; // Use the line below because using the full 30 sequences murders Redux dev tools. msaAlignment.alignmentTracks = msaAlignment.alignmentTracks.slice(0, 20); @@ -26,253 +30,323 @@ const defaultState = { noVisibilityOptions: false, setTickSpacing: false }; +const bpSelectOptions = [ + { + label: "Multiple Sequence Alignment", + value: msaAlignment.id + }, + { label: "Pairwise Alignment", value: pairwiseAlignment.id }, + { + label: "Pairwise Alignment 2", + value: pairwiseAlignment2.id + }, + { label: "Sanger Alignment", value: sangerAlignment.id }, + { label: "MSA with gaps", value: msaAlignmentWithGaps.id } +]; -// const basicActions = { selectionLayerUpdate, caretPositionUpdate }; - -export default class AlignmentDemo extends React.Component { - constructor(props) { - super(props); - setupOptions({ that: this, defaultState, props }); - } - - componentDidUpdate() { - setParamsIfNecessary({ that: this, defaultState }); - } +const alignmentViewStyle = { + marginRight: 10 +}; - componentDidMount() { +export default props => { + useEffect(() => { addAlignment(store, msaAlignment); addAlignment(store, pairwiseAlignment); addAlignment(store, pairwiseAlignment2); addAlignment(store, sangerAlignment); addAlignment(store, msaAlignmentWithGaps); - } - render() { - return ( -
- {this.state.showDemoOptions && ( -
-
- {renderToggle({ - that: this, - alwaysShow: true, - type: "showDemoOptions", - label: "Show Demo Options", - hotkey: `cmd+'` - })} -
- { - this.setState({ alignmentDataId: val }); - }} - options={[ - { - label: "Multiple Sequence Alignment", - value: msaAlignment.id - }, - { label: "Pairwise Alignment", value: pairwiseAlignment.id }, - { - label: "Pairwise Alignment 2", - value: pairwiseAlignment2.id - }, - { label: "Sanger Alignment", value: sangerAlignment.id }, - { label: "MSA with gaps", value: msaAlignmentWithGaps.id } - ]} - /> -
- {renderToggle({ - that: this, - type: "forceHeightMode", - label: "Force Height 500px", - description: - "You can force a height for the editor by passing height:500 (same for width)" - })} - {renderToggle({ - that: this, - type: "setAlignmentName", - label: "Set Alignment Name", - description: - "You can give the alignment a name by setting alignmentName:'Ref Seq Name'" - })} - {renderToggle({ - that: this, - type: "isFullyZoomedOut", - label: "View Zoomed-Out Alignment", - description: - "You can view the alignment zoomed-out by setting isFullyZoomedOut:true" - })} - {renderToggle({ - that: this, - type: "setMinimapLaneHeight", - label: "Set Minimap Lane Height 13px", - description: - "You can set a height for the minimap lanes by passing minimapLaneHeight:13" - })} - {renderToggle({ - that: this, - type: "setMinimapLaneSpacing", - label: "Set Minimap Lane Spacing 3px", - description: - "You can set a height for the space between minimap lanes by passing minimapLaneSpacing:3" - })} - {renderToggle({ - that: this, - type: "noClickDragHandlers", - label: "Disable Clicks, Dragging and Highlighting", - description: - "You can disable click-drag highlighting by setting noClickDragHandlers:true" - })} - {renderToggle({ - that: this, - type: "allowTrackNameEdit" - })} - {renderToggle({ - that: this, - type: "handleAlignmentRename" - })} - {renderToggle({ - that: this, - type: "shouldAutosave" - })} - {renderToggle({ - that: this, - type: "allowTrimming" - })} - {renderToggle({ - that: this, - type: "allowTrackRearrange" - })} - {renderToggle({ - that: this, - type: "hasTemplate", - label: "Specify Alignment with Template", - description: - "You can specify that the first sequence in an alignment is a template sequence by setting hasTemplate:true" - })} - {renderToggle({ - that: this, - type: "setTickSpacing", - label: "Force Tick Spacing 5 bps", - description: - "You can set force the spacing of tick marks on the axis by setting linearViewOptions:{tickSpacing:5}" - })} - {renderToggle({ - that: this, - type: "noVisibilityOptions", - label: "Disable Visibility Options", - description: - "You can disable the visibility options menu by setting noVisibilityOptions:true" - })} - {renderToggle({ - that: this, - type: "overrideSelectionRightClick", - label: "Override Selection Right Click", - description: - "You can override the selection right click by passing a selectionLayerRightClicked={(event)={}} prop" - })} - {renderToggle({ - that: this, - type: "addSelectionRightClickOptions", - label: "Add Selection Right Click Options", - description: `You can add options to the selection right click by passing additionalSelectionLayerRightClickedOptions={(event)=>({ - text: "I'm an additional option", - className: "createDiversityRegion", - onClick: () => this.addDiversityRegionIfPossible() - })} prop` - })} -

-

-

-

-

-
- )} - [ - { - text: "I'm an additional option", - className: "createDiversityRegion", - onClick: () => window.toastr.success("You did it!") - } - ] - }), - ...(this.state.overrideSelectionRightClick && { - selectionLayerRightClicked: () => { - window.toastr.success("lezzz goooo!"); - } - }), - additionalTopLeftEl: ( - - ), - additionalTopEl: , - id: this.state.alignmentDataId, - height: this.state.forceHeightMode ? 500 : undefined, - isFullyZoomedOut: this.state.isFullyZoomedOut, - minimapLaneHeight: this.state.setMinimapLaneHeight ? 13 : undefined, - minimapLaneSpacing: this.state.setMinimapLaneSpacing - ? 3 - : undefined, - handleAlignmentRename: this.state.handleAlignmentRename - ? newName => { - window.toastr.success( - `handleAlignmentRename triggered with ${newName}` - ); - this.setState({ alignmentName: newName }); - } - : undefined, - alignmentName: this.state.alignmentName - ? this.state.alignmentName - : this.state.alignmentName - ? "Ref Seq Name" - : "Alignment Name Placeholder", - noClickDragHandlers: this.state.noClickDragHandlers, - allowTrackNameEdit: this.state.allowTrackNameEdit, - allowTrimming: this.state.allowTrimming, - shouldAutosave: this.state.shouldAutosave, - handleAlignmentSave: this.state.shouldAutosave - ? () => { - window.toastr.success("Autosave Triggered"); - } - : undefined, - allowTrackRearrange: this.state.allowTrackRearrange, - hasTemplate: this.state.hasTemplate, - noVisibilityOptions: this.state.noVisibilityOptions, - linearViewOptions: { - ...(this.state.setTickSpacing && { tickSpacing: 10 }) - } - }} - /> -
+ }, []); + + const defaultValues = useMemo(() => { + const editorDemoState = getCurrentParamsFromUrl(props.history.location); + // localStorage.editorDemoState = props.history.location.search; + const massagedEditorDemoState = Object.keys(editorDemoState).reduce( + (acc, key) => { + if (editorDemoState[key] === "false") { + acc[key] = false; + } else if (editorDemoState[key] === "true") { + acc[key] = true; + } else { + acc[key] = editorDemoState[key]; + } + return acc; + }, + {} ); - } -} + return { + ...defaultState, + ...massagedEditorDemoState + }; + }, [props.history.location]); + const [forceHeightMode, forceHeightModeSwitch] = useToggle({ + type: "forceHeightMode", + label: "Force Height 500px", + description: + "You can force a height for the editor by passing height:500 (same for width)", + defaultValue: defaultValues.forceHeightMode + }); + + const [showDemoOptions, setShowDemoOptions] = useState( + defaultValues.showDemoOptions + ); + const [, showDemoOptionsSwitch] = useToggle({ + alwaysShow: true, + type: "showDemoOptions", + label: "Show Demo Options", + hotkey: `cmd+'`, + controlledValue: showDemoOptions, + setControlledValue: setShowDemoOptions, + defaultValue: defaultValues.showDemoOptions + }); + + const [alignmentDataId, setAlignmentDataId] = useState( + defaultValues.alignmentDataId + ); + const [setAlignmentName, setAlignmentNameSwitch] = useToggle({ + type: "setAlignmentName", + label: "Set Alignment Name", + description: + "You can give the alignment a name by setting alignmentName:'Ref Seq Name'", + defaultValue: defaultValues.setAlignmentName + }); + const [alignmentName, setAlignmentNameValue] = useState(); + const [isFullyZoomedOut, isFullyZoomedOutSwitch] = useToggle({ + type: "isFullyZoomedOut", + label: "View Zoomed-Out Alignment", + description: + "You can view the alignment zoomed-out by setting isFullyZoomedOut:true", + defaultValue: defaultValues.isFullyZoomedOut + }); + const [setMinimapLaneHeight, setMinimapLaneHeightSwitch] = useToggle({ + type: "setMinimapLaneHeight", + label: "Set Minimap Lane Height 13px", + description: + "You can set a height for the minimap lanes by passing minimapLaneHeight:13", + defaultValue: defaultValues.setMinimapLaneHeight + }); + const [setMinimapLaneSpacing, setMinimapLaneSpacingSwitch] = useToggle({ + type: "setMinimapLaneSpacing", + label: "Set Minimap Lane Spacing 3px", + description: + "You can set a height for the space between minimap lanes by passing minimapLaneSpacing:3", + defaultValue: defaultValues.setMinimapLaneSpacing + }); + const [noClickDragHandlers, noClickDragHandlersSwitch] = useToggle({ + type: "noClickDragHandlers", + label: "Disable Clicks, Dragging and Highlighting", + description: + "You can disable click-drag highlighting by setting noClickDragHandlers:true", + defaultValue: defaultValues.noClickDragHandlers + }); + const [allowTrackNameEdit, allowTrackNameEditSwitch] = useToggle({ + type: "allowTrackNameEdit" + }); + const [handleAlignmentRename, handleAlignmentRenameSwitch] = useToggle({ + type: "handleAlignmentRename" + }); + const [shouldAutosave, shouldAutosaveSwitch] = useToggle({ + type: "shouldAutosave" + }); + const [allowTrimming, allowTrimmingSwitch] = useToggle({ + type: "allowTrimming" + }); + const [allowTrackRearrange, allowTrackRearrangeSwitch] = useToggle({ + type: "allowTrackRearrange" + }); + const [hasTemplate, hasTemplateSwitch] = useToggle({ + type: "hasTemplate", + label: "Specify Alignment with Template", + description: + "You can specify that the first sequence in an alignment is a template sequence by setting hasTemplate:true", + defaultValue: defaultValues.hasTemplate + }); + const [setTickSpacing, setTickSpacingSwitch] = useToggle({ + type: "setTickSpacing", + label: "Force Tick Spacing 5 bps", + description: + "You can set force the spacing of tick marks on the axis by setting linearViewOptions:{tickSpacing:5}", + defaultValue: defaultValues.setTickSpacing + }); + const [noVisibilityOptions, noVisibilityOptionsSwitch] = useToggle({ + type: "noVisibilityOptions", + label: "Disable Visibility Options", + description: + "You can disable the visibility options menu by setting noVisibilityOptions:true", + defaultValue: defaultValues.noVisibilityOptions + }); + const [overrideSelectionRightClick, overrideSelectionRightClickSwitch] = + useToggle({ + type: "overrideSelectionRightClick", + label: "Override Selection Right Click", + description: + "You can override the selection right click by passing a selectionLayerRightClicked={(event)={}} prop" + }); + const [addSelectionRightClickOptions, addSelectionRightClickOptionsSwitch] = + useToggle({ + type: "addSelectionRightClickOptions", + label: "Add Selection Right Click Options", + description: `You can add options to the selection right click by passing additionalSelectionLayerRightClickedOptions={(event)=>({ + text: "I'm an additional option", + className: "createDiversityRegion", + onClick: () => this.addDiversityRegionIfPossible() + })} prop` + }); + + // constructor(props) { + // super(props); + // setupOptions({ that: this, defaultState, props }); + // } + + useEffect(() => { + if (props.history) { + const diff = difference( + { + alignmentDataId, + showDemoOptions, + forceHeightMode, + isFullyZoomedOut, + setMinimapLaneHeight, + setMinimapLaneSpacing, + setAlignmentName, + noClickDragHandlers, + hasTemplate, + noVisibilityOptions, + setTickSpacing + }, + defaultState + ); + setCurrentParamsOnUrl(diff, props.history.replace); + } + }, [ + alignmentDataId, + forceHeightMode, + hasTemplate, + isFullyZoomedOut, + noClickDragHandlers, + noVisibilityOptions, + props.history, + setAlignmentName, + setMinimapLaneHeight, + setMinimapLaneSpacing, + setTickSpacing, + showDemoOptions + ]); + + return ( +
+ {showDemoOptions && ( +
+
{showDemoOptionsSwitch}
+ { + setAlignmentDataId(val); + }} + options={bpSelectOptions} + /> +
+ {forceHeightModeSwitch} + {setAlignmentNameSwitch} + {isFullyZoomedOutSwitch} + {setMinimapLaneHeightSwitch} + {setMinimapLaneSpacingSwitch} + {noClickDragHandlersSwitch} + {allowTrackNameEditSwitch} + {handleAlignmentRenameSwitch} + {shouldAutosaveSwitch} + {allowTrimmingSwitch} + {allowTrackRearrangeSwitch} + {hasTemplateSwitch} + {setTickSpacingSwitch} + {noVisibilityOptionsSwitch} + {overrideSelectionRightClickSwitch} + {addSelectionRightClickOptionsSwitch} +

+

+

+

+

+
+ )} + [ + { + text: "I'm an additional option", + className: "createDiversityRegion", + onClick: () => window.toastr.success("You did it!") + } + ] + })} + {...(overrideSelectionRightClick && { + selectionLayerRightClicked: () => { + window.toastr.success("lezzz goooo!"); + } + })} + additionalTopLeftEl={ + } + id={alignmentDataId} + height={forceHeightMode ? 500 : undefined} + isFullyZoomedOut={isFullyZoomedOut} + minimapLaneHeight={setMinimapLaneHeight ? 13 : undefined} + minimapLaneSpacing={setMinimapLaneSpacing ? 3 : undefined} + handleAlignmentRename={ + handleAlignmentRename + ? newName => { + window.toastr.success( + `handleAlignmentRename triggered with ${newName}` + ); + setAlignmentNameValue(newName); + } + : undefined + } + alignmentName={ + alignmentName + ? alignmentName + : !setAlignmentName + ? "Ref Seq Name" + : "Alignment Name Placeholder" + } + noClickDragHandlers={noClickDragHandlers} + allowTrackNameEdit={allowTrackNameEdit} + allowTrimming={allowTrimming} + shouldAutosave={shouldAutosave} + handleAlignmentSave={ + shouldAutosave + ? () => { + window.toastr.success("Autosave Triggered"); + } + : undefined + } + allowTrackRearrange={allowTrackRearrange} + hasTemplate={hasTemplate} + noVisibilityOptions={noVisibilityOptions} + linearViewOptions={{ + ...(setTickSpacing && { tickSpacing: 10 }) + }} + /> +
+ ); +}; diff --git a/packages/ove/demo/src/index.js b/packages/ove/demo/src/index.js index 86c90e23..1b2543f9 100644 --- a/packages/ove/demo/src/index.js +++ b/packages/ove/demo/src/index.js @@ -116,7 +116,7 @@ const demos = { const Demo = () => { return ( - + ); }; diff --git a/packages/ove/demo/src/utils/useToggle.js b/packages/ove/demo/src/utils/useToggle.js new file mode 100644 index 00000000..84aa1eab --- /dev/null +++ b/packages/ove/demo/src/utils/useToggle.js @@ -0,0 +1,216 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + Switch, + Button, + HTMLSelect, + Dialog, + useHotkeys, + KeyCombo +} from "@blueprintjs/core"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { + doesSearchValMatchText, + getCurrentParamsFromUrl, + getStringFromReactComponent, + setCurrentParamsOnUrl +} from "@teselagen/ui"; +import { startCase } from "lodash-es"; + +const HandleHotkeys = ({ combo, onKeyDown }) => { + const hotkeys = useMemo( + () => [ + { + combo, + global: true, + onKeyDown + } + ], + [combo, onKeyDown] + ); + useHotkeys(hotkeys); + return null; +}; + +const ShowInfo = ({ description, info, type }) => { + const [isOpen, setOpen] = useState(false); + return ( + <> + { + setOpen(false); + }} + isOpen={isOpen} + > +
+ +
+
+ + {description || info ? ( +
+
+ ) : ( +
+ )} + + ); +}; + +const useToggle = ({ + type, + isButton, + label, + onClick, + info, + description, + hook, + defaultValue: _defaultValue, + options, + disabled, + isSelect, + hidden, + alwaysShow, + hotkey, + searchInput, + controlledValue, + setControlledValue, + ...rest +}) => { + const defaultValue = _defaultValue || options?.[0]?.value || options?.[0]; + const [val, _setVal] = useState(); + + useEffect(() => { + const demoState = getDemoState(); + const toSet = demoState[type] || defaultValue; + setControlledValue?.(toSet); + _setVal(toSet); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const value = useMemo(() => controlledValue || val, [controlledValue, val]); + + const setVal = newVal => { + const demoState = getDemoState(); + demoState[type] = newVal; + setCurrentParamsOnUrl({ [type]: newVal }, undefined, true); + if (setControlledValue) setControlledValue(newVal); + _setVal(newVal); + }; + let comp; + + if (hidden) comp = null; + let toggleOrButton; + const labelOrText = label ? {label} : startCase(type); + const sharedProps = { + style: { marginBottom: 0 }, + "data-test": type || label, + label: labelOrText, + text: labelOrText, + ...rest + }; + let switchOnChange; + if (searchInput && !alwaysShow) { + if ( + !doesSearchValMatchText( + searchInput, + getStringFromReactComponent(labelOrText) + ) + ) { + comp = null; + } + } + if (isButton) { + toggleOrButton =