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 }) => {