From 1df316b7f9a54931dd7bc4e3a9f26c259f918473 Mon Sep 17 00:00:00 2001 From: Manuel Lera-Ramirez Date: Mon, 9 Dec 2024 13:42:04 +0000 Subject: [PATCH] Issues demo (#320) * closes #318, also fixes it for PCR * closes #316, tests missing --- ShareYourCloning_backend | 2 +- cypress/e2e/primer_design.cy.js | 61 ++++++++++++++++ .../PrimerDesignSimplePair.jsx | 69 ++++++++++++++++--- src/utils/transformCoords.js | 15 ++-- 4 files changed, 127 insertions(+), 20 deletions(-) diff --git a/ShareYourCloning_backend b/ShareYourCloning_backend index 8b17294..34f9e49 160000 --- a/ShareYourCloning_backend +++ b/ShareYourCloning_backend @@ -1 +1 @@ -Subproject commit 8b17294999df91ce639b2bbb4aa059942ff6af75 +Subproject commit 34f9e497642f6b1bacd986ec198bcef81b685ac3 diff --git a/cypress/e2e/primer_design.cy.js b/cypress/e2e/primer_design.cy.js index 59acee7..7ef3cf7 100644 --- a/cypress/e2e/primer_design.cy.js +++ b/cypress/e2e/primer_design.cy.js @@ -348,16 +348,22 @@ describe('Test primer designer functionality', () => { // One enzyme is enough to submit, either one setAutocompleteValue('Left enzyme', 'EcoRI', '.primer-design'); cy.contains('.primer-design button', 'Design primers').should('exist'); + cy.get('.veCutsiteLabel').filter(':contains("EcoRI")').should('exist'); clearAutocompleteValue('Left enzyme', '.primer-design'); + cy.get('.veCutsiteLabel').filter(':contains("EcoRI")').should('not.exist'); + cy.contains('.primer-design button', 'Design primers').should('not.exist'); setAutocompleteValue('Right enzyme', 'BamHI', '.primer-design'); cy.contains('.primer-design button', 'Design primers').should('exist'); + cy.get('.veCutsiteLabel').filter(':contains("BamHI")').should('exist'); // There should be a single primer tail feature displayed cy.get('.veLabelText').filter(':contains("primer tail")').should('have.length', 1); setAutocompleteValue('Left enzyme', 'EcoRI', '.primer-design'); // There should be two now + cy.get('.veCutsiteLabel').filter(':contains("BamHI")').should('exist'); + cy.get('.veCutsiteLabel').filter(':contains("EcoRI")').should('exist'); cy.get('.veLabelText').filter(':contains("primer tail")').should('have.length', 2); // Go to sequence tab cy.get('.veTabSequenceMap').contains('Sequence Map').click(); @@ -377,6 +383,8 @@ describe('Test primer designer functionality', () => { cy.wait('@primerDesign').then((interception) => { expect(interception.request.query.minimal_hybridization_length).to.equal('10'); expect(interception.request.query.target_tm).to.equal('40'); + expect(interception.request.query.left_enzyme_inverted).to.equal('false'); + expect(interception.request.query.right_enzyme_inverted).to.equal('false'); }); // We should be on the Results tab @@ -403,6 +411,59 @@ describe('Test primer designer functionality', () => { cy.get('li').contains('PCR with primers seq_2_EcoRI_fwd and seq_2_BamHI_rvs').should('exist'); }); + it.only('Restriction ligation primer design - invert site', () => { + const sequence = 'ATCTAACTTTACTTGGAAAGCGTTTCACGT'.toLowerCase(); + manuallyTypeSequence(sequence); + addSource('PCRSource'); + const ampSequence = sequence.slice(1, 30); + return; + // Click on design primers + cy.get('button').contains('Design primers').click(); + clickMultiSelectOption('Purpose of primers', 'Restriction and Ligation', 'li'); + + // Click on axis tick 1 + cy.get('.veAxisTick[data-test="1"]').first().click({ force: true }); + + // Click on axis tick 30 while holding shift + cy.get('.veAxisTick[data-test="30"]').first().click({ shiftKey: true }); + + // Set selection + cy.get('button').contains('Set from selection').click(); + + // Go to other settings tab + cy.get('button.MuiTab-root').contains('Other settings').click(); + + // Select EcoRI, should not be possible to select inverted + setAutocompleteValue('Left enzyme', 'EcoRI', '.primer-design'); + cy.get('.primer-design').contains('Invert site').should('not.exist'); + + // Should be possible to select when BsaI is selected + setAutocompleteValue('Left enzyme', 'BsaI', '.primer-design'); + cy.get('.primer-design').contains('Invert site').should('exist'); + + // Go to sequence tab + cy.get('.veTabSequenceMap').contains('Sequence Map').click(); + + // THere should be the forward sequence displayed: + cy.get('svg.rowViewTextContainer text').contains(`TTTggtctc${ampSequence}`); + + // Invert the site, should show the reverse sequence + cy.get('.primer-design').contains('Invert site').click(); + cy.get('svg.rowViewTextContainer text').contains(`TTTgagacc${ampSequence}`); + + // Select EcoRI, should not be possible to select inverted + setAutocompleteValue('Right enzyme', 'EcoRI', '.primer-design'); + cy.get('.primer-design').contains('Invert site').should('not.exist'); + cy.get('.primer-design').filter(':contains("Invert site")').should('have.length', 1); + + // Should be possible to select when BsaI is selected + setAutocompleteValue('Right enzyme', 'BsaI', '.primer-design'); + cy.get('.primer-design').filter(':contains("Invert site")').should('have.length', 2); + cy.get('svg.rowViewTextContainer text').contains(`TTTgagacc${ampSequence}ggtctcAAA`); + cy.get('.primer-design').contains('Invert site').eq(1).click(); + cy.get('svg.rowViewTextContainer text').contains(`TTTgagacc${ampSequence}gagaccAAA`); + }); + it('Simple pair primer design', () => { const sequence = 'ATCTAACTTTACTTGGAAAGCGTTTCACGT'; manuallyTypeSequence(sequence); diff --git a/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx b/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx index c8730b4..e5f510a 100644 --- a/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +++ b/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { Box, Tab, Tabs, FormControl, Button, Alert, TextField, Tooltip, FormLabel } from '@mui/material'; +import { Box, Tab, Tabs, FormControl, Button, Alert, TextField, Tooltip, FormLabel, FormControlLabel, Checkbox } from '@mui/material'; import { batch, useDispatch, useSelector, useStore } from 'react-redux'; import InfoIcon from '@mui/icons-material/Info'; import { aliasedEnzymesByName, getReverseComplementSequenceString as reverseComplement } from '@teselagen/sequence-utils'; +import { updateEditor } from '@teselagen/ove'; import { cloningActions } from '../../../../store/cloning'; import useStoreEditor from '../../../../hooks/useStoreEditor'; import TabPanel from '../../../navigation/TabPanel'; @@ -31,6 +32,11 @@ function getRecognitionSequence(enzyme) { return recognitionSeq.split('').map((base) => (base in ambiguousDnaBases ? ambiguousDnaBases[base] : base)).join(''); } +function isEnzymePalyndromic(enzyme) { + const recognitionSeq = getRecognitionSequence(enzyme); + return recognitionSeq === reverseComplement(recognitionSeq); +} + function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) { const templateSequenceId = pcrSource.input[0]; const { setMainSequenceId, setCurrentTab, addPrimersToPCRSource } = cloningActions; @@ -45,6 +51,8 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) { const primerDesignSettings = usePrimerDesignSettings({ homologyLength: null, hybridizationLength: 20, targetTm: 55 }); const [leftEnzyme, setLeftEnzyme] = React.useState(null); const [rightEnzyme, setRightEnzyme] = React.useState(null); + const [leftEnzymeInverted, setLeftEnzymeInverted] = React.useState(false); + const [rightEnzymeInverted, setRightEnzymeInverted] = React.useState(false); const [spacers, setSpacers] = React.useState(['', '']); const [fillerBases, setFillerBases] = React.useState('TTT'); const [amplificationDirection, setAmplificationDirection] = React.useState('forward'); @@ -61,8 +69,10 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) { React.useEffect(() => { if (rois.every((region) => region !== null) && spacersAreValid && fillersAreValid) { - const forwardPrimerStartingSeq = (leftEnzyme ? fillerBases : '') + getRecognitionSequence(leftEnzyme) + spacers[0]; - const reversePrimerStartingSeq = reverseComplement((rightEnzyme ? fillerBases : '') + getRecognitionSequence(rightEnzyme) + reverseComplement(spacers[1])); + const leftEnzymeSeq = leftEnzymeInverted ? reverseComplement(getRecognitionSequence(leftEnzyme)) : getRecognitionSequence(leftEnzyme); + const rightEnzymeSeq = rightEnzymeInverted ? reverseComplement(getRecognitionSequence(rightEnzyme)) : getRecognitionSequence(rightEnzyme); + const forwardPrimerStartingSeq = (leftEnzyme ? fillerBases : '') + leftEnzymeSeq + spacers[0]; + const reversePrimerStartingSeq = reverseComplement((rightEnzyme ? fillerBases : '') + rightEnzymeSeq + reverseComplement(spacers[1])); const { teselaJsonCache } = store.getState().cloning; const templateSequence = teselaJsonCache[templateSequenceId]; const newSequenceProduct = joinEntitiesIntoSingleSequence([templateSequence], rois.map((s) => s.selectionLayer), [amplificationDirection], [forwardPrimerStartingSeq, reversePrimerStartingSeq], false, 'primer tail'); @@ -71,7 +81,25 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) { } else { setSequenceProduct(null); } - }, [fillerBases, rightEnzyme, leftEnzyme, spacers, rois, spacersAreValid, fillersAreValid, templateSequenceId, store, setSequenceProduct, amplificationDirection]); + }, [fillerBases, rightEnzyme, leftEnzyme, spacers, rois, spacersAreValid, fillersAreValid, templateSequenceId, store, setSequenceProduct, amplificationDirection, leftEnzymeInverted, rightEnzymeInverted]); + + // When the user changes enzyme, reset the enzyme inversion state + React.useEffect(() => { + setLeftEnzymeInverted(false); + }, [leftEnzyme]); + React.useEffect(() => { + setRightEnzymeInverted(false); + }, [rightEnzyme]); + + // When enzymes change, update the displayed enzymes in the editor + React.useEffect(() => { + const allEnzymes = [leftEnzyme, rightEnzyme].filter((enzyme) => enzyme !== null); + const filteredRestrictionEnzymes = allEnzymes.map((enzyme) => ({ + canBeHidden: true, + value: enzyme, + })); + updateEditor(store, 'mainEditor', { annotationVisibility: { cutsites: leftEnzyme || rightEnzyme }, restrictionEnzymes: { filteredRestrictionEnzymes, isEnzymeFilterAnd: false } }); + }, [leftEnzyme, rightEnzyme]); const addPrimers = () => { batch(() => { @@ -87,6 +115,7 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) { onTabChange(null, 0); document.getElementById(`source-${pcrSource.id}`)?.scrollIntoView(); updateStoreEditor('mainEditor', null); + updateEditor(store, 'mainEditor', { annotationVisibility: { cutsites: false } }); }; const onPrimerDesign = async () => { @@ -95,6 +124,8 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) { target_tm: primerDesignSettings.targetTm, left_enzyme: leftEnzyme, right_enzyme: rightEnzyme, + left_enzyme_inverted: leftEnzymeInverted, + right_enzyme_inverted: rightEnzymeInverted, filler_bases: fillerBases, }; const serverError = await designPrimers(rois, params, [amplificationDirection], spacers); @@ -119,13 +150,29 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) { {restrictionLigation && ( <> Restriction enzyme sites - - - - - - - + + + + + + {leftEnzyme && !isEnzymePalyndromic(leftEnzyme) && ( + setLeftEnzymeInverted(e.target.checked)} />} + label="Invert site" + /> + )} + + + + + + {rightEnzyme && !isEnzymePalyndromic(rightEnzyme) && ( + setRightEnzymeInverted(e.target.checked)} />} + label="Invert site" + /> + )} + e.id === f.sequence); const { size } = sequence; const { left_location: left, right_location: right } = f; - const leftEdge = count; + const leftStart = left?.start || 0; const rightStart = right?.start || 0; const rightEnd = right?.end || size; - const rightEdge = count + rightEnd - (left?.start || 0); // Ranges are 0-based, but [0:0] is not empty, it's the equivalent to python's [0:1] - f.rangeInAssembly = translateRange({ start: leftEdge, end: rightEdge - 1 }, 0, productLength); + const rangeLength = getRangeLength({ start: leftStart, end: rightEnd - 1 }, size); + f.rangeInAssembly = translateRange({ start: 0, end: rangeLength - 1 }, count, productLength); f.size = size; - - count += (rightStart - (left?.start || 0)); + count += getRangeLength({ start: leftStart, end: rightStart - 1 }, size); }); const rangeInParent = (selection, id) => { if (selection.start === -1) { @@ -43,7 +42,7 @@ export default function getTransformCoords({ assembly, type: sourceType }, paren const { rangeInAssembly, left_location: left, reverse_complemented, size } = fragment; const startInParent = left?.start || 0; if (isRangeWithinRange(selection, rangeInAssembly, productLength)) { - const selectionShifted = selection.start < selection.end ? selection : { start: selection.start, end: selection.end + productLength }; + const selectionShifted = selection.start <= selection.end ? selection : { start: selection.start, end: selection.end + productLength }; const outRange = translateRange(selectionShifted, -rangeInAssembly.start + startInParent, size); if (reverse_complemented) { return flipContainedRange(outRange, { start: 0, end: size - 1 }, size);