@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.
Files changed (31) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +3 -3
  3. package/src/components/assembler/Assembler.cy.jsx +3 -0
  4. package/src/components/assembler/useAssembler.js +1 -0
  5. package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +4 -30
  6. package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +2 -1
  7. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +55 -313
  8. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +2 -2
  9. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +14 -5
  10. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +12 -17
  11. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +8 -4
  12. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +1 -59
  13. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +2 -3
  14. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +18 -8
  15. package/src/components/primers/primer_design/SequenceTabComponents/{TabPannelSettings.jsx → TabPanelSettings.jsx} +4 -22
  16. package/src/components/primers/primer_design/SequenceTabComponents/designTypeStrategies.js +178 -0
  17. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +89 -0
  18. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useRegionSelection.js +37 -0
  19. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSequenceProduct.js +35 -0
  20. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSpacers.js +13 -0
  21. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useTabNavigation.js +40 -0
  22. package/src/components/primers/primer_design/SequenceTabComponents/utils/getSequenceLabel.js +7 -0
  23. package/src/components/primers/primer_design/SequenceTabComponents/utils/knownGatewayCombinations.js +27 -0
  24. package/src/components/primers/primer_design/SequenceTabComponents/utils/trimPadding.js +58 -0
  25. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +11 -17
  26. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.cy.jsx +131 -0
  27. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +143 -24
  28. package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +11 -15
  29. package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +10 -16
  30. package/src/components/primers/primer_design/SourceComponents/useNavigateAfterPrimerDesign.js +23 -0
  31. 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.0",
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.0",
29
- "@opencloning/utils": "1.5.0",
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,
@@ -71,6 +71,7 @@ export const useAssembler = () => {
71
71
  const config = {
72
72
  params: {
73
73
  circular_only: true,
74
+ sort_by_recognition_sites: true,
74
75
  }
75
76
  }
76
77
  const requestData = {
@@ -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 knownCombinations = [
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 = knownCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNames) && isEqual(knownOrientation, orientation));
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 = knownCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNamesReverse) && isEqual(knownOrientation, orientationReverse));
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 = sequenceName && sequenceName !== 'name' ? `Seq. ${id} (${sequenceName})` : `Seq. ${id}`;
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 { batch, useDispatch, useSelector, useStore } from 'react-redux';
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 { ebicTemplateAnnotation, joinSequencesIntoSingleSequence, simulateHomologousRecombination } from '@opencloning/utils/sequenceManipulation';
12
- import useHttpClient from '../../../../hooks/useHttpClient';
13
-
14
- function changeValueAtIndex(current, index, newValue) {
15
- return current.map((_, i) => (i === index ? newValue : current[i]));
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 = React.useMemo(() => {
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 initialCircularAssembly = designType === 'gibson_assembly';
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 spacersAreValid = React.useMemo(() => spacers.every((spacer) => !stringIsNotDNA(spacer)), [spacers]);
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 store = useStore();
50
- const backendRoute = useBackendRoute();
51
- const dispatch = useDispatch();
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 submissionPreventedMessage = React.useMemo(() => {
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
- React.useEffect(() => {
127
- if (circularAssembly && spacers.length !== templateSequenceIds.length) {
128
- setSpacers((current) => current.slice(1));
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 onSelectRegion = useCallback((index, selectedRegion, allowSinglePosition = false) => {
137
- const { caretPosition } = selectedRegion;
138
- if (caretPosition === undefined) {
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
- setRois((c) => changeValueAtIndex(c, index, null));
151
- return 'Select a region (not a single position) to amplify';
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
- }, [sequenceIds, updateStoreEditor, dispatch, setMainSequenceId, setSelectedTab]);
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 regionError;
181
- }, [onSelectRegion, handleNext]);
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
- // Focus on the right sequence when changing tabs
188
- useEffect(() => {
189
- // Focus on the correct sequence
190
- const mainSequenceIndex = sequenceIds.indexOf(mainSequenceId);
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
- // Update the sequence product in the editor if in the last tab
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 = useCallback(async () => {
208
- // Validate fragmentOrientations
209
- fragmentOrientations.forEach((orientation) => {
210
- if (orientation !== 'forward' && orientation !== 'reverse') {
211
- throw new Error('Invalid fragment orientation');
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
- requestData = {
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
- error,
355
- rois,
356
- designPrimers,
357
- setPrimers,
358
- selectedTab,
359
- onTabChange,
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 TabPannelSettings from './TabPannelSettings';
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' && <TabPannelSettings />}
22
+ {designType !== 'ebic' && <TabPanelSettings />}
23
23
  {designType === 'ebic' && <TabPanelEBICSettings />}
24
24
  <TabPanelResults />
25
25
  </Box>
@@ -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({ pcrSources }) {
8
- const templateSequencesIds = React.useMemo(() => pcrSources.map(getPcrTemplateSequenceId), [pcrSources]);
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, index) => (
17
+ ...templateSequencesIds.map((id) => (
11
18
  { label: `Seq ${id}`, selectOrientation: true }
12
19
  )),
13
- ], [pcrSources, templateSequencesIds]);
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>