@opencloning/ui 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/package.json +3 -3
- package/src/components/assembler/Assembler.cy.jsx +3 -0
- package/src/components/assembler/useAssembler.js +1 -0
- package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +4 -30
- package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +2 -1
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +55 -313
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +2 -2
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +14 -5
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +12 -17
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +8 -4
- package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +1 -59
- package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +2 -3
- package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +18 -8
- package/src/components/primers/primer_design/SequenceTabComponents/{TabPannelSettings.jsx → TabPanelSettings.jsx} +4 -22
- package/src/components/primers/primer_design/SequenceTabComponents/designTypeStrategies.js +178 -0
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +89 -0
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useRegionSelection.js +37 -0
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSequenceProduct.js +35 -0
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSpacers.js +13 -0
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useTabNavigation.js +40 -0
- package/src/components/primers/primer_design/SequenceTabComponents/utils/getSequenceLabel.js +7 -0
- package/src/components/primers/primer_design/SequenceTabComponents/utils/knownGatewayCombinations.js +27 -0
- package/src/components/primers/primer_design/SequenceTabComponents/utils/trimPadding.js +58 -0
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +11 -17
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.cy.jsx +131 -0
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +143 -24
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +11 -15
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +10 -16
- package/src/components/primers/primer_design/SourceComponents/useNavigateAfterPrimerDesign.js +23 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#653](https://github.com/manulera/OpenCloning_frontend/pull/653) [`e529568`](https://github.com/manulera/OpenCloning_frontend/commit/e5295684d62b25954e6095fc63fa7c9e65551fb6) Thanks [@manulera](https://github.com/manulera)! - Assembler - pass sort_by_recognition_sites=true to restriction & ligation endpoint
|
|
8
|
+
|
|
9
|
+
- Updated dependencies []:
|
|
10
|
+
- @opencloning/store@1.5.2
|
|
11
|
+
- @opencloning/utils@1.5.2
|
|
12
|
+
|
|
13
|
+
## 1.5.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#651](https://github.com/manulera/OpenCloning_frontend/pull/651) [`b5c89e2`](https://github.com/manulera/OpenCloning_frontend/commit/b5c89e23aa1065319cb07a9ac48a26693dd0a21a) Thanks [@manulera](https://github.com/manulera)! - Big refactor of Primer designer for better maintainability
|
|
18
|
+
|
|
19
|
+
- [#651](https://github.com/manulera/OpenCloning_frontend/pull/651) [`b5c89e2`](https://github.com/manulera/OpenCloning_frontend/commit/b5c89e23aa1065319cb07a9ac48a26693dd0a21a) Thanks [@manulera](https://github.com/manulera)! - Allow primer design of Gibson and similar assembly methods to include fragments that are not amplified. Similar to what the NEBuilder planner does, where not all products are amplified. Perhaps in the future it would be useful to also allow the option to restore the restriction sites on the edge.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [[`b5c89e2`](https://github.com/manulera/OpenCloning_frontend/commit/b5c89e23aa1065319cb07a9ac48a26693dd0a21a)]:
|
|
22
|
+
- @opencloning/store@1.5.1
|
|
23
|
+
- @opencloning/utils@1.5.1
|
|
24
|
+
|
|
3
25
|
## 1.5.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@emotion/styled": "^11.14.0",
|
|
26
26
|
"@mui/icons-material": "^5.15.17",
|
|
27
27
|
"@mui/material": "^5.15.17",
|
|
28
|
-
"@opencloning/store": "1.5.
|
|
29
|
-
"@opencloning/utils": "1.5.
|
|
28
|
+
"@opencloning/store": "1.5.2",
|
|
29
|
+
"@opencloning/utils": "1.5.2",
|
|
30
30
|
"@teselagen/bio-parsers": "^0.4.34",
|
|
31
31
|
"@teselagen/ove": "^0.8.34",
|
|
32
32
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -239,6 +239,9 @@ describe('<AssemblerComponent />', () => {
|
|
|
239
239
|
expect(req.body).to.have.property('source');
|
|
240
240
|
expect(req.body.source).to.have.property('restriction_enzymes');
|
|
241
241
|
expect(req.body.source.restriction_enzymes).to.include('assembly_enzyme');
|
|
242
|
+
// Check that the value of the sort_by_recognition_sites was set in the request query
|
|
243
|
+
expect(req.query).to.have.property('sort_by_recognition_sites');
|
|
244
|
+
expect(req.query.sort_by_recognition_sites).to.equal('true');
|
|
242
245
|
req.reply({
|
|
243
246
|
statusCode: 200,
|
|
244
247
|
body: dummyResponse2,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Alert, Box, FormControl, InputLabel, MenuItem, Select } from '@mui/material';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { getReverseComplementSequenceString as reverseComplement } from '@teselagen/sequence-utils';
|
|
4
3
|
import { isEqual } from 'lodash-es';
|
|
5
4
|
import { parseFeatureLocation } from '@teselagen/bio-parsers';
|
|
6
5
|
import { useDispatch, useSelector } from 'react-redux';
|
|
@@ -9,30 +8,9 @@ import useStoreEditor from '../../../../hooks/useStoreEditor';
|
|
|
9
8
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
10
9
|
import { usePrimerDesign } from './PrimerDesignContext';
|
|
11
10
|
import RequestStatusWrapper from '../../../form/RequestStatusWrapper';
|
|
11
|
+
import knownGatewayCombinations from './utils/knownGatewayCombinations';
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
{
|
|
15
|
-
siteNames: ['attP4', 'attP1'],
|
|
16
|
-
spacers: ['GGGGACAACTTTGTATAGAAAAGTTGNN', reverseComplement('GGGGACTGCTTTTTTGTACAAACTTGN')],
|
|
17
|
-
orientation: [true, true],
|
|
18
|
-
message: 'Primers tails designed based on pDONR™ P4-P1R',
|
|
19
|
-
translationFrame: [4, 6],
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
siteNames: ['attP1', 'attP2'],
|
|
23
|
-
spacers: ['GGGGACAAGTTTGTACAAAAAAGCAGGCTNN', reverseComplement('GGGGACCACTTTGTACAAGAAAGCTGGGTN')],
|
|
24
|
-
orientation: [true, false],
|
|
25
|
-
message: 'Primers tails designed based on pDONR™ 221',
|
|
26
|
-
translationFrame: [4, 6],
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
siteNames: ['attP2', 'attP3'],
|
|
30
|
-
spacers: ['GGGGACAGCTTTCTTGTACAAAGTGGNN', reverseComplement('GGGGACAACTTTGTATAATAAAGTTGN')],
|
|
31
|
-
orientation: [false, false],
|
|
32
|
-
message: 'Primers tails designed based on pDONR™ P2R-P3',
|
|
33
|
-
translationFrame: [4, 6],
|
|
34
|
-
},
|
|
35
|
-
];
|
|
13
|
+
const { setMainSequenceSelection } = cloningActions;
|
|
36
14
|
|
|
37
15
|
function SiteSelect({ donorSites, site, setSite, label }) {
|
|
38
16
|
return (
|
|
@@ -57,8 +35,6 @@ function SiteSelect({ donorSites, site, setSite, label }) {
|
|
|
57
35
|
);
|
|
58
36
|
}
|
|
59
37
|
|
|
60
|
-
const { setMainSequenceSelection } = cloningActions;
|
|
61
|
-
|
|
62
38
|
function GatewayRoiSelect({ id, greedy = false }) {
|
|
63
39
|
const [leftSite, setLeftSite] = React.useState(null);
|
|
64
40
|
const [rightSite, setRightSite] = React.useState(null);
|
|
@@ -104,14 +80,14 @@ function GatewayRoiSelect({ id, greedy = false }) {
|
|
|
104
80
|
const selection = { selectionLayer, caretPosition: -1 };
|
|
105
81
|
const siteNames = [newLeftSite.siteName, newRightSite.siteName];
|
|
106
82
|
const orientation = [newLeftSite.location.includes('(+)'), newRightSite.location.includes('(+)')];
|
|
107
|
-
const knownCombinationForward =
|
|
83
|
+
const knownCombinationForward = knownGatewayCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNames) && isEqual(knownOrientation, orientation));
|
|
108
84
|
if (knownCombinationForward) {
|
|
109
85
|
handleKnownCombinationChange(knownCombinationForward, selection);
|
|
110
86
|
return;
|
|
111
87
|
}
|
|
112
88
|
const siteNamesReverse = [newRightSite.siteName, newLeftSite.siteName];
|
|
113
89
|
const orientationReverse = [!newRightSite.location.includes('(+)'), !newLeftSite.location.includes('(+)')];
|
|
114
|
-
const knownCombinationReverse =
|
|
90
|
+
const knownCombinationReverse = knownGatewayCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNamesReverse) && isEqual(knownOrientation, orientationReverse));
|
|
115
91
|
if (knownCombinationReverse) {
|
|
116
92
|
handleKnownCombinationChange(knownCombinationReverse, selection);
|
|
117
93
|
return;
|
|
@@ -123,7 +99,6 @@ function GatewayRoiSelect({ id, greedy = false }) {
|
|
|
123
99
|
const onSiteSelectLeft = React.useCallback((site) => {
|
|
124
100
|
setLeftSite(site);
|
|
125
101
|
if (rightSite === null || isEqual(rightSite, site)) {
|
|
126
|
-
// Find the first different one
|
|
127
102
|
const differentSite = donorSites.find(({ location }) => location !== site.location);
|
|
128
103
|
setRightSite(differentSite);
|
|
129
104
|
checkKnownCombination(site, differentSite);
|
|
@@ -135,7 +110,6 @@ function GatewayRoiSelect({ id, greedy = false }) {
|
|
|
135
110
|
const onSiteSelectRight = React.useCallback((site) => {
|
|
136
111
|
setRightSite(site);
|
|
137
112
|
if (leftSite === null || isEqual(leftSite, site)) {
|
|
138
|
-
// Find the first different one
|
|
139
113
|
const differentSite = donorSites.find(({ location }) => location !== site.location);
|
|
140
114
|
setLeftSite(differentSite);
|
|
141
115
|
checkKnownCombination(differentSite, site);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { usePrimerDesign } from './PrimerDesignContext';
|
|
4
|
+
import { getSequenceLabel } from './utils/getSequenceLabel';
|
|
4
5
|
|
|
5
6
|
function OrientationPicker({ id, index }) {
|
|
6
7
|
const { designType, fragmentOrientations, handleFragmentOrientationChange, templateSequenceNames } = usePrimerDesign();
|
|
7
8
|
const sequenceName = templateSequenceNames[index];
|
|
8
|
-
let label =
|
|
9
|
+
let label = getSequenceLabel(id, sequenceName);
|
|
9
10
|
if (designType === 'homologous_recombination') {
|
|
10
11
|
label = 'Orientation of insert';
|
|
11
12
|
}
|
|
@@ -1,328 +1,89 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import { useSelector, useStore } from 'react-redux';
|
|
3
3
|
import { updateEditor } from '@teselagen/ove';
|
|
4
4
|
import { isEqual } from 'lodash-es';
|
|
5
|
-
import useBackendRoute from '../../../../hooks/useBackendRoute';
|
|
6
|
-
import { selectedRegion2SequenceLocation } from '@opencloning/utils/selectedRegionUtils';
|
|
7
|
-
import error2String from '@opencloning/utils/error2String';
|
|
8
|
-
import useStoreEditor from '../../../../hooks/useStoreEditor';
|
|
9
|
-
import { cloningActions } from '@opencloning/store/cloning';
|
|
10
5
|
import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
6
|
+
import useSpacers from './hooks/useSpacers';
|
|
7
|
+
import useTabNavigation from './hooks/useTabNavigation';
|
|
8
|
+
import useRegionSelection from './hooks/useRegionSelection';
|
|
9
|
+
import useSequenceProduct from './hooks/useSequenceProduct';
|
|
10
|
+
import useDesignPrimers from './hooks/useDesignPrimers';
|
|
17
11
|
|
|
18
12
|
const PrimerDesignContext = React.createContext();
|
|
19
13
|
|
|
20
|
-
export function PrimerDesignProvider({ children, designType, sequenceIds, primerDesignSettings, steps }) {
|
|
14
|
+
export function PrimerDesignProvider({ children, designType, sequenceIds, primerDesignSettings, steps, isAmplified: isAmplifiedProp, circularAssembly = false }) {
|
|
15
|
+
const isAmplified = useMemo(
|
|
16
|
+
() => isAmplifiedProp || sequenceIds.map(() => true),
|
|
17
|
+
[isAmplifiedProp, sequenceIds],
|
|
18
|
+
);
|
|
21
19
|
|
|
22
|
-
const templateSequenceIds =
|
|
20
|
+
const templateSequenceIds = useMemo(() => {
|
|
23
21
|
if (designType === 'homologous_recombination' || designType === 'gateway_bp') {
|
|
24
22
|
return sequenceIds.slice(0, 1);
|
|
25
23
|
}
|
|
26
24
|
return sequenceIds;
|
|
27
25
|
}, [sequenceIds, designType]);
|
|
28
26
|
|
|
29
|
-
// Compute initial values based on design type (props don't change, so compute once)
|
|
30
27
|
const initialFragmentOrientationsLength = templateSequenceIds.length;
|
|
31
|
-
const
|
|
32
|
-
const initialSpacersLength = initialCircularAssembly ? initialFragmentOrientationsLength : initialFragmentOrientationsLength + 1;
|
|
28
|
+
const initialSpacersLength = circularAssembly ? initialFragmentOrientationsLength : initialFragmentOrientationsLength + 1;
|
|
33
29
|
|
|
34
|
-
const [primers, setPrimers] = useState([]);
|
|
35
|
-
const [rois, setRois] = useState(Array(sequenceIds.length).fill(null));
|
|
36
|
-
const [error, setError] = useState('');
|
|
37
|
-
const [selectedTab, setSelectedTab] = useState(0);
|
|
38
|
-
const [sequenceProduct, setSequenceProduct] = useState(null);
|
|
39
30
|
const [fragmentOrientations, setFragmentOrientations] = useState(Array(initialFragmentOrientationsLength).fill('forward'));
|
|
40
|
-
const [circularAssembly, setCircularAssembly] = useState(initialCircularAssembly);
|
|
41
|
-
const [spacers, setSpacers] = useState(Array(initialSpacersLength).fill(''));
|
|
42
|
-
const sequenceProductTimeoutRef = React.useRef();
|
|
43
31
|
|
|
44
|
-
const
|
|
32
|
+
const store = useStore();
|
|
45
33
|
const sequenceNames = useSelector((state) => sequenceIds.map((id) => state.cloning.teselaJsonCache[id].name), isEqual);
|
|
46
34
|
const templateSequenceNames = useSelector((state) => templateSequenceIds.map((id) => state.cloning.teselaJsonCache[id].name), isEqual);
|
|
47
|
-
const mainSequenceId = useSelector((state) => state.cloning.mainSequenceId);
|
|
48
35
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const { updateStoreEditor } = useStoreEditor();
|
|
53
|
-
const { setMainSequenceId, addPrimersToPCRSource, setCurrentTab } = cloningActions;
|
|
54
|
-
const httpClient = useHttpClient();
|
|
36
|
+
const { spacers, setSpacers, spacersAreValid } = useSpacers({
|
|
37
|
+
initialLength: initialSpacersLength,
|
|
38
|
+
});
|
|
55
39
|
|
|
56
|
-
const
|
|
57
|
-
if (rois.some((region) => region === null)) {
|
|
58
|
-
return 'Not all regions have been selected';
|
|
59
|
-
} if (primerDesignSettings.error) {
|
|
60
|
-
return primerDesignSettings.error;
|
|
61
|
-
} if (spacers.some((spacer) => stringIsNotDNA(spacer))) {
|
|
62
|
-
return 'Spacer sequences not valid';
|
|
63
|
-
}
|
|
64
|
-
return '';
|
|
65
|
-
}, [rois, primerDesignSettings.error, spacers]);
|
|
66
|
-
|
|
67
|
-
React.useEffect(() => {
|
|
68
|
-
// Clear any existing timeout
|
|
69
|
-
clearTimeout(sequenceProductTimeoutRef.current);
|
|
70
|
-
|
|
71
|
-
// Debounce the heavy calculation
|
|
72
|
-
sequenceProductTimeoutRef.current = setTimeout(() => {
|
|
73
|
-
let newSequenceProduct = null;
|
|
74
|
-
if (submissionPreventedMessage === '') {
|
|
75
|
-
const { teselaJsonCache } = store.getState().cloning;
|
|
76
|
-
const sequences = sequenceIds.map((id) => teselaJsonCache[id]);
|
|
77
|
-
if (designType === 'simple_pair' || designType === 'restriction_ligation') {
|
|
78
|
-
const enzymeSpacers = designType === 'restriction_ligation' ? primerDesignSettings.enzymeSpacers : ['', ''];
|
|
79
|
-
const extendedSpacers = [enzymeSpacers[0] + spacers[0], spacers[1] + enzymeSpacers[1]];
|
|
80
|
-
newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, extendedSpacers, circularAssembly, 'primer tail');
|
|
81
|
-
newSequenceProduct.name = 'PCR product';
|
|
82
|
-
} else if (designType === 'gibson_assembly') {
|
|
83
|
-
newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, spacers, circularAssembly);
|
|
84
|
-
newSequenceProduct.name = 'Gibson Assembly product';
|
|
85
|
-
} else if (designType === 'homologous_recombination') {
|
|
86
|
-
newSequenceProduct = simulateHomologousRecombination(sequences[0], sequences[1], rois, fragmentOrientations[0] === 'reverse', spacers);
|
|
87
|
-
newSequenceProduct.name = 'Homologous recombination product';
|
|
88
|
-
} else if (designType === 'gateway_bp') {
|
|
89
|
-
newSequenceProduct = joinSequencesIntoSingleSequence([sequences[0]], [rois[0].selectionLayer], fragmentOrientations, spacers, false, 'primer tail');
|
|
90
|
-
newSequenceProduct.name = 'PCR product';
|
|
91
|
-
const { knownCombination } = primerDesignSettings;
|
|
92
|
-
const leftFeature = {
|
|
93
|
-
start: knownCombination.translationFrame[0],
|
|
94
|
-
end: spacers[0].length - 1,
|
|
95
|
-
type: 'CDS',
|
|
96
|
-
name: 'translation frame',
|
|
97
|
-
strand: 1,
|
|
98
|
-
forward: true,
|
|
99
|
-
};
|
|
100
|
-
const nbAas = Math.floor((spacers[1].length - knownCombination.translationFrame[1]) / 3);
|
|
101
|
-
const rightStart = newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - nbAas * 3;
|
|
102
|
-
const rightFeature = {
|
|
103
|
-
start: rightStart,
|
|
104
|
-
end: newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - 1,
|
|
105
|
-
type: 'CDS',
|
|
106
|
-
name: 'translation frame',
|
|
107
|
-
strand: 1,
|
|
108
|
-
forward: true,
|
|
109
|
-
};
|
|
110
|
-
newSequenceProduct.features.push(leftFeature);
|
|
111
|
-
newSequenceProduct.features.push(rightFeature);
|
|
112
|
-
} else if (designType === 'ebic') {
|
|
113
|
-
newSequenceProduct = ebicTemplateAnnotation(sequences[0], rois[0].selectionLayer, primerDesignSettings);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
setSequenceProduct({...newSequenceProduct, id: 'opencloning_primer_design_product'});
|
|
117
|
-
}, 300);
|
|
118
|
-
|
|
119
|
-
// Cleanup timeout on unmount or when dependencies change
|
|
120
|
-
return () => {
|
|
121
|
-
clearTimeout(sequenceProductTimeoutRef.current);
|
|
122
|
-
};
|
|
123
|
-
}, [rois, spacersAreValid, fragmentOrientations, circularAssembly, designType, spacers, primerDesignSettings, sequenceIds, templateSequenceIds, store, submissionPreventedMessage]);
|
|
40
|
+
const { selectedTab, onTabChange, handleNext, handleBack } = useTabNavigation({ sequenceIds });
|
|
124
41
|
|
|
42
|
+
const { rois, handleSelectRegion } = useRegionSelection({
|
|
43
|
+
sequenceCount: sequenceIds.length,
|
|
44
|
+
handleNext,
|
|
45
|
+
});
|
|
125
46
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
if (!circularAssembly && spacers.length !== templateSequenceIds.length + 1) {
|
|
131
|
-
setSpacers((current) => ['', ...current]);
|
|
132
|
-
}
|
|
133
|
-
}, [circularAssembly, spacers, templateSequenceIds.length]);
|
|
134
|
-
|
|
47
|
+
const handleFragmentOrientationChange = useCallback((index, orientation) => {
|
|
48
|
+
setFragmentOrientations((current) => current.map((v, i) => (i === index ? orientation : v)));
|
|
49
|
+
}, []);
|
|
135
50
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
setRois((c) => changeValueAtIndex(c, index, null));
|
|
140
|
-
return 'You have to select a region in the sequence editor!';
|
|
141
|
-
}
|
|
142
|
-
if (caretPosition === -1) {
|
|
143
|
-
setRois((c) => changeValueAtIndex(c, index, selectedRegion));
|
|
144
|
-
return '';
|
|
145
|
-
}
|
|
146
|
-
if (allowSinglePosition) {
|
|
147
|
-
setRois((c) => changeValueAtIndex(c, index, selectedRegion));
|
|
148
|
-
return '';
|
|
51
|
+
const submissionPreventedMessage = useMemo(() => {
|
|
52
|
+
if (rois.some((region, i) => region === null && isAmplified[i])) {
|
|
53
|
+
return 'Not all regions have been selected';
|
|
149
54
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}, [setRois]);
|
|
153
|
-
|
|
154
|
-
const onTabChange = useCallback((event, newValue) => {
|
|
155
|
-
setSelectedTab(newValue);
|
|
156
|
-
if (newValue < sequenceIds.length) {
|
|
157
|
-
updateStoreEditor('mainEditor', sequenceIds[newValue]);
|
|
158
|
-
dispatch(setMainSequenceId(sequenceIds[newValue]));
|
|
159
|
-
} else if (newValue === sequenceIds.length) {
|
|
160
|
-
// Don't update editor here - let the useEffect handle it when sequenceProduct is ready
|
|
161
|
-
// This avoids using stale data since sequenceProduct is debounced
|
|
162
|
-
} else {
|
|
163
|
-
updateStoreEditor('mainEditor', null);
|
|
55
|
+
if (primerDesignSettings.error) {
|
|
56
|
+
return primerDesignSettings.error;
|
|
164
57
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const handleNext = useCallback(() => {
|
|
168
|
-
onTabChange(null, selectedTab + 1);
|
|
169
|
-
}, [onTabChange, selectedTab]);
|
|
170
|
-
|
|
171
|
-
const handleBack = useCallback(() => {
|
|
172
|
-
onTabChange(null, selectedTab - 1);
|
|
173
|
-
}, [onTabChange, selectedTab]);
|
|
174
|
-
|
|
175
|
-
const handleSelectRegion = useCallback((index, selectedRegion, allowSinglePosition = false) => {
|
|
176
|
-
const regionError = onSelectRegion(index, selectedRegion, allowSinglePosition);
|
|
177
|
-
if (!regionError) {
|
|
178
|
-
handleNext();
|
|
58
|
+
if (spacers.some((spacer) => stringIsNotDNA(spacer))) {
|
|
59
|
+
return 'Spacer sequences not valid';
|
|
179
60
|
}
|
|
180
|
-
return
|
|
181
|
-
}, [
|
|
182
|
-
|
|
183
|
-
const handleFragmentOrientationChange = useCallback((index, orientation) => {
|
|
184
|
-
setFragmentOrientations((current) => changeValueAtIndex(current, index, orientation));
|
|
185
|
-
}, [setFragmentOrientations]);
|
|
61
|
+
return '';
|
|
62
|
+
}, [rois, isAmplified, primerDesignSettings.error, spacers]);
|
|
186
63
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (mainSequenceIndex !== -1) {
|
|
192
|
-
setSelectedTab(mainSequenceIndex);
|
|
193
|
-
}
|
|
194
|
-
}, [sequenceIds, mainSequenceId]);
|
|
64
|
+
const sequenceProduct = useSequenceProduct({
|
|
65
|
+
rois, spacers, spacersAreValid, fragmentOrientations, circularAssembly,
|
|
66
|
+
designType, primerDesignSettings, sequenceIds, submissionPreventedMessage,
|
|
67
|
+
});
|
|
195
68
|
|
|
196
|
-
//
|
|
69
|
+
// Sync the editor with the sequence product when on the settings tab
|
|
197
70
|
useEffect(() => {
|
|
198
71
|
if (selectedTab === sequenceIds.length) {
|
|
199
72
|
const timeoutId = setTimeout(() => {
|
|
200
73
|
updateEditor(store, 'mainEditor', { sequenceData: sequenceProduct || {}, selectionLayer: {} });
|
|
201
74
|
}, 100);
|
|
202
|
-
|
|
203
75
|
return () => clearTimeout(timeoutId);
|
|
204
76
|
}
|
|
77
|
+
return undefined;
|
|
205
78
|
}, [sequenceProduct, selectedTab, sequenceIds.length, store]);
|
|
206
79
|
|
|
207
|
-
const designPrimers =
|
|
208
|
-
|
|
209
|
-
fragmentOrientations
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
const { cloning: { sequences, teselaJsonCache, globalPrimerSettings } } = store.getState();
|
|
215
|
-
let requestData;
|
|
216
|
-
let params;
|
|
217
|
-
let endpoint;
|
|
218
|
-
const paramsForRequest = Object.fromEntries(
|
|
219
|
-
Object.entries(primerDesignSettings)
|
|
220
|
-
.filter(([_, value]) => typeof value !== 'function'),
|
|
221
|
-
);
|
|
222
|
-
if (designType === 'gibson_assembly') {
|
|
223
|
-
params = {
|
|
224
|
-
...paramsForRequest,
|
|
225
|
-
circular: circularAssembly,
|
|
226
|
-
};
|
|
227
|
-
requestData = {
|
|
228
|
-
pcr_templates: sequenceIds.map((id, index) => ({
|
|
229
|
-
sequence: sequences.find((e) => e.id === id),
|
|
230
|
-
location: selectedRegion2SequenceLocation(rois[index], teselaJsonCache[id].size),
|
|
231
|
-
forward_orientation: fragmentOrientations[index] === 'forward',
|
|
232
|
-
})),
|
|
233
|
-
spacers,
|
|
234
|
-
};
|
|
235
|
-
endpoint = 'gibson_assembly';
|
|
236
|
-
} else if (designType === 'homologous_recombination') {
|
|
237
|
-
const [pcrTemplateId, homologousRecombinationTargetId] = sequenceIds;
|
|
238
|
-
params = {
|
|
239
|
-
...paramsForRequest,
|
|
240
|
-
};
|
|
241
|
-
requestData = {
|
|
242
|
-
pcr_template: {
|
|
243
|
-
sequence: sequences.find((e) => e.id === pcrTemplateId),
|
|
244
|
-
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[pcrTemplateId].size),
|
|
245
|
-
forward_orientation: fragmentOrientations[0] === 'forward',
|
|
246
|
-
},
|
|
247
|
-
homologous_recombination_target: {
|
|
248
|
-
sequence: sequences.find((e) => e.id === homologousRecombinationTargetId),
|
|
249
|
-
location: selectedRegion2SequenceLocation(rois[1], teselaJsonCache[homologousRecombinationTargetId].size),
|
|
250
|
-
},
|
|
251
|
-
spacers,
|
|
252
|
-
};
|
|
253
|
-
endpoint = 'homologous_recombination';
|
|
254
|
-
} else if (designType === 'simple_pair' || designType === 'gateway_bp' || designType === 'restriction_ligation') {
|
|
255
|
-
const pcrTemplateId = sequenceIds[0];
|
|
256
|
-
params = {
|
|
257
|
-
...paramsForRequest,
|
|
258
|
-
};
|
|
80
|
+
const { primers, setPrimers, error, designPrimers, addPrimers } = useDesignPrimers({
|
|
81
|
+
designType, sequenceIds, templateSequenceIds, isAmplified,
|
|
82
|
+
rois, fragmentOrientations, spacers, circularAssembly, primerDesignSettings,
|
|
83
|
+
handleNext, onTabChange,
|
|
84
|
+
});
|
|
259
85
|
|
|
260
|
-
|
|
261
|
-
pcr_template: {
|
|
262
|
-
sequence: sequences.find((e) => e.id === pcrTemplateId),
|
|
263
|
-
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[pcrTemplateId].size),
|
|
264
|
-
forward_orientation: fragmentOrientations[0] === 'forward',
|
|
265
|
-
},
|
|
266
|
-
spacers,
|
|
267
|
-
};
|
|
268
|
-
endpoint = 'simple_pair';
|
|
269
|
-
} else if (designType === 'ebic') {
|
|
270
|
-
endpoint = 'ebic';
|
|
271
|
-
requestData = {
|
|
272
|
-
template: {
|
|
273
|
-
sequence: sequences.find((e) => e.id === templateSequenceIds[0]),
|
|
274
|
-
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[templateSequenceIds[0]].size),
|
|
275
|
-
// forward_orientation: fragmentOrientations[0] === 'forward',
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
params = {
|
|
279
|
-
...paramsForRequest,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
requestData.settings = globalPrimerSettings;
|
|
283
|
-
const url = backendRoute(`primer_design/${endpoint}`);
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const resp = await httpClient.post(url, requestData, { params });
|
|
287
|
-
setError('');
|
|
288
|
-
const newPrimers = resp.data.primers;
|
|
289
|
-
setPrimers(newPrimers);
|
|
290
|
-
handleNext();
|
|
291
|
-
return false;
|
|
292
|
-
} catch (thrownError) {
|
|
293
|
-
const errorMessage = error2String(thrownError);
|
|
294
|
-
setError(errorMessage);
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
}, [fragmentOrientations, rois, sequenceIds, templateSequenceIds, designType, circularAssembly, primerDesignSettings, spacers, store, httpClient, backendRoute, handleNext]);
|
|
298
|
-
|
|
299
|
-
const addPrimers = useCallback(() => {
|
|
300
|
-
const pcrSources = store.getState().cloning.sources.filter((source) => source.type === 'PCRSource');
|
|
301
|
-
let usedPCRSources;
|
|
302
|
-
if (designType === 'ebic') {
|
|
303
|
-
usedPCRSources = pcrSources.filter((source) => source.input.some((i) => i.sequence === templateSequenceIds[0]));
|
|
304
|
-
} else {
|
|
305
|
-
usedPCRSources = templateSequenceIds.map((id) => pcrSources.find((source) => source.input.some((i) => i.sequence === id)));
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
batch(() => {
|
|
309
|
-
usedPCRSources.forEach((pcrSource, index) => {
|
|
310
|
-
dispatch(addPrimersToPCRSource({
|
|
311
|
-
fwdPrimer: primers[index * 2],
|
|
312
|
-
revPrimer: primers[index * 2 + 1],
|
|
313
|
-
sourceId: pcrSource.id,
|
|
314
|
-
}));
|
|
315
|
-
});
|
|
316
|
-
dispatch(setMainSequenceId(null));
|
|
317
|
-
dispatch(setCurrentTab(0));
|
|
318
|
-
});
|
|
319
|
-
setPrimers([]);
|
|
320
|
-
onTabChange(null, 0);
|
|
321
|
-
document.getElementById(`source-${usedPCRSources[0].id}`)?.scrollIntoView();
|
|
322
|
-
updateStoreEditor('mainEditor', null);
|
|
323
|
-
}, [primers, dispatch, setMainSequenceId, setCurrentTab, onTabChange, updateStoreEditor, designType, templateSequenceIds, addPrimersToPCRSource, store]);
|
|
324
|
-
|
|
325
|
-
const value = React.useMemo(() => ({
|
|
86
|
+
const value = useMemo(() => ({
|
|
326
87
|
primers,
|
|
327
88
|
error,
|
|
328
89
|
rois,
|
|
@@ -344,38 +105,19 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
|
|
|
344
105
|
submissionPreventedMessage,
|
|
345
106
|
addPrimers,
|
|
346
107
|
circularAssembly,
|
|
347
|
-
setCircularAssembly,
|
|
348
108
|
templateSequenceIds,
|
|
349
109
|
templateSequenceNames,
|
|
350
110
|
designType,
|
|
351
111
|
steps,
|
|
112
|
+
isAmplified,
|
|
352
113
|
}), [
|
|
353
|
-
primers,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
handleNext,
|
|
361
|
-
handleBack,
|
|
362
|
-
handleSelectRegion,
|
|
363
|
-
sequenceIds,
|
|
364
|
-
fragmentOrientations,
|
|
365
|
-
circularAssembly,
|
|
366
|
-
spacers,
|
|
367
|
-
setFragmentOrientations,
|
|
368
|
-
setSpacers,
|
|
369
|
-
handleFragmentOrientationChange,
|
|
370
|
-
sequenceNames,
|
|
371
|
-
primerDesignSettings,
|
|
372
|
-
submissionPreventedMessage,
|
|
373
|
-
addPrimers,
|
|
374
|
-
setCircularAssembly,
|
|
375
|
-
templateSequenceIds,
|
|
376
|
-
templateSequenceNames,
|
|
377
|
-
designType,
|
|
378
|
-
steps,
|
|
114
|
+
primers, error, rois, designPrimers, setPrimers,
|
|
115
|
+
selectedTab, onTabChange, handleNext, handleBack, handleSelectRegion,
|
|
116
|
+
sequenceIds, fragmentOrientations, circularAssembly, spacers,
|
|
117
|
+
setFragmentOrientations, setSpacers, handleFragmentOrientationChange,
|
|
118
|
+
sequenceNames, primerDesignSettings, submissionPreventedMessage,
|
|
119
|
+
addPrimers, templateSequenceIds, templateSequenceNames,
|
|
120
|
+
designType, steps, isAmplified,
|
|
379
121
|
]);
|
|
380
122
|
|
|
381
123
|
return (
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { Box } from '@mui/material';
|
|
3
3
|
import PrimerDesignStepper from './PrimerDesignStepper';
|
|
4
4
|
import TabPanelSelectRoi from './TabPanelSelectRoi';
|
|
5
|
-
import
|
|
5
|
+
import TabPanelSettings from './TabPanelSettings';
|
|
6
6
|
import TabPanelResults from './TabPanelResults';
|
|
7
7
|
import { usePrimerDesign } from './PrimerDesignContext';
|
|
8
8
|
import TabPanelEBICSettings from './TabPanelEBICSettings';
|
|
@@ -19,7 +19,7 @@ function PrimerDesignForm() {
|
|
|
19
19
|
index={index}
|
|
20
20
|
/>
|
|
21
21
|
))}
|
|
22
|
-
{designType !== 'ebic' && <
|
|
22
|
+
{designType !== 'ebic' && <TabPanelSettings />}
|
|
23
23
|
{designType === 'ebic' && <TabPanelEBICSettings />}
|
|
24
24
|
<TabPanelResults />
|
|
25
25
|
</Box>
|
package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx
CHANGED
|
@@ -2,15 +2,22 @@ import React from 'react';
|
|
|
2
2
|
import { PrimerDesignProvider } from './PrimerDesignContext';
|
|
3
3
|
import PrimerDesignForm from './PrimerDesignForm';
|
|
4
4
|
import usePrimerDesignSettings from './usePrimerDesignSettings';
|
|
5
|
-
import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
|
|
6
5
|
|
|
7
|
-
export default function PrimerDesignGibsonAssembly({
|
|
8
|
-
const templateSequencesIds = React.useMemo(
|
|
6
|
+
export default function PrimerDesignGibsonAssembly({ assemblyInputsInOrder, circularAssembly }) {
|
|
7
|
+
const templateSequencesIds = React.useMemo(
|
|
8
|
+
() => assemblyInputsInOrder.map((x) => x.templateSequenceId),
|
|
9
|
+
[assemblyInputsInOrder],
|
|
10
|
+
);
|
|
11
|
+
const isAmplified = React.useMemo(
|
|
12
|
+
() => assemblyInputsInOrder.map((x) => x.isAmplified),
|
|
13
|
+
[assemblyInputsInOrder],
|
|
14
|
+
);
|
|
15
|
+
|
|
9
16
|
const steps = React.useMemo(() => [
|
|
10
|
-
...templateSequencesIds.map((id
|
|
17
|
+
...templateSequencesIds.map((id) => (
|
|
11
18
|
{ label: `Seq ${id}`, selectOrientation: true }
|
|
12
19
|
)),
|
|
13
|
-
], [
|
|
20
|
+
], [templateSequencesIds]);
|
|
14
21
|
|
|
15
22
|
const primerDesignSettings = usePrimerDesignSettings({ homology_length: 35, minimal_hybridization_length: 14, target_tm: 55 });
|
|
16
23
|
return (
|
|
@@ -19,6 +26,8 @@ export default function PrimerDesignGibsonAssembly({ pcrSources }) {
|
|
|
19
26
|
sequenceIds={templateSequencesIds}
|
|
20
27
|
primerDesignSettings={primerDesignSettings}
|
|
21
28
|
steps={steps}
|
|
29
|
+
isAmplified={isAmplified}
|
|
30
|
+
circularAssembly={circularAssembly}
|
|
22
31
|
>
|
|
23
32
|
<PrimerDesignForm />
|
|
24
33
|
</PrimerDesignProvider>
|