@opencloning/ui 1.4.11 → 1.5.1

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 (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +3 -3
  3. package/src/components/assembler/Assembler.cy.jsx +1 -1
  4. package/src/components/assembler/Assembler.jsx +4 -4
  5. package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +2 -9
  6. package/src/components/assembler/ExistingSyntaxDialog.jsx +4 -5
  7. package/src/components/assembler/UploadPlasmidsButton.cy.jsx +32 -2
  8. package/src/components/assembler/assembler_utils.js +21 -10
  9. package/src/components/assembler/assembler_utils.test.js +29 -0
  10. package/src/components/assembler/useAssembler.js +2 -2
  11. package/src/components/assembler/usePlasmidsLogic.js +8 -8
  12. package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +4 -30
  13. package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +2 -1
  14. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +55 -313
  15. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +2 -2
  16. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +14 -5
  17. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +12 -17
  18. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +8 -4
  19. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +1 -59
  20. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +2 -3
  21. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +18 -8
  22. package/src/components/primers/primer_design/SequenceTabComponents/{TabPannelSettings.jsx → TabPanelSettings.jsx} +4 -22
  23. package/src/components/primers/primer_design/SequenceTabComponents/designTypeStrategies.js +178 -0
  24. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +89 -0
  25. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useRegionSelection.js +37 -0
  26. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSequenceProduct.js +35 -0
  27. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSpacers.js +13 -0
  28. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useTabNavigation.js +40 -0
  29. package/src/components/primers/primer_design/SequenceTabComponents/utils/getSequenceLabel.js +7 -0
  30. package/src/components/primers/primer_design/SequenceTabComponents/utils/knownGatewayCombinations.js +27 -0
  31. package/src/components/primers/primer_design/SequenceTabComponents/utils/trimPadding.js +58 -0
  32. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +11 -17
  33. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.cy.jsx +131 -0
  34. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +143 -24
  35. package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +11 -15
  36. package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +10 -16
  37. package/src/components/primers/primer_design/SourceComponents/useNavigateAfterPrimerDesign.js +23 -0
  38. package/src/version.js +1 -1
@@ -1,27 +1,27 @@
1
1
  import { Button, Checkbox, FormControl, FormControlLabel } from '@mui/material';
2
2
  import React from 'react';
3
- import { batch, useDispatch } from 'react-redux';
3
+ import { useDispatch } from 'react-redux';
4
4
  import SingleInputSelector from '../../../sources/SingleInputSelector';
5
5
  import { cloningActions } from '@opencloning/store/cloning';
6
- import useStoreEditor from '../../../../hooks/useStoreEditor';
7
6
  import LabelWithTooltip from '../../../form/LabelWithTooltip';
8
7
  import useGatewaySites from '../../../../hooks/useGatewaySites';
9
8
  import NoAttPSitesError from '../common/NoAttPSitesError';
10
9
  import RetryAlert from '../../../form/RetryAlert';
11
10
  import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
11
+ import useNavigateAfterPrimerDesign from './useNavigateAfterPrimerDesign';
12
12
 
13
- const { addTemplateChildAndSubsequentSource, setCurrentTab, setMainSequenceId } = cloningActions;
13
+ const { addTemplateChildAndSubsequentSource } = cloningActions;
14
14
 
15
15
  function PrimerDesignGatewayBP({ source }) {
16
16
  const [target, setTarget] = React.useState('');
17
17
  const [greedy, setGreedy] = React.useState(false);
18
+ const dispatch = useDispatch();
19
+ const inputSequenceId = getPcrTemplateSequenceId(source);
20
+ const navigateAfterDesign = useNavigateAfterPrimerDesign();
18
21
 
19
- const { updateStoreEditor } = useStoreEditor();
20
22
  const { requestStatus, sites: sitesInTarget, attemptAgain } = useGatewaySites({ target, greedy });
21
23
  const nbOfAttPSites = sitesInTarget.filter((site) => site.siteName.startsWith('attP')).length;
22
- const inputSequenceId = getPcrTemplateSequenceId(source);
23
24
 
24
- const dispatch = useDispatch();
25
25
  const onSubmit = (event) => {
26
26
  event.preventDefault();
27
27
  const newSource = {
@@ -36,20 +36,14 @@ function PrimerDesignGatewayBP({ source }) {
36
36
  circular: false,
37
37
  };
38
38
 
39
- batch(() => {
40
- dispatch(addTemplateChildAndSubsequentSource({ newSource, newSequence, sourceId: source.id }));
41
- dispatch(setMainSequenceId(inputSequenceId));
42
- updateStoreEditor('mainEditor', inputSequenceId);
43
- dispatch(setCurrentTab(3));
44
- // Scroll to the top of the page after 300ms
45
- setTimeout(() => {
46
- document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
47
- }, 300);
48
- });
39
+ navigateAfterDesign(
40
+ () => dispatch(addTemplateChildAndSubsequentSource({ newSource, newSequence, sourceId: source.id })),
41
+ inputSequenceId,
42
+ );
49
43
  };
44
+
50
45
  return (
51
46
  <form onSubmit={onSubmit}>
52
-
53
47
  <FormControl fullWidth>
54
48
  <SingleInputSelector
55
49
  label="Donor vector"
@@ -0,0 +1,131 @@
1
+ import React from 'react';
2
+ import { GibsonAmplifyAndCircularControls } from './PrimerDesignGibsonAssembly';
3
+
4
+ const baseTargets = [1, 2, 3];
5
+ const baseSequenceNames = [
6
+ { id: 1, name: 'seq1' },
7
+ { id: 2, name: 'seq2' },
8
+ { id: 3, name: 'seq3' },
9
+ ];
10
+
11
+ function TestComponent() {
12
+ const [circularAssembly, setCircularAssembly] = React.useState(false);
13
+ const [amplified, setAmplified] = React.useState([false, true, false]);
14
+ return <GibsonAmplifyAndCircularControls
15
+ targets={baseTargets}
16
+ sequenceNames={baseSequenceNames}
17
+ amplified={amplified}
18
+ setAmplified={setAmplified}
19
+ circularAssembly={circularAssembly}
20
+ setCircularAssembly={setCircularAssembly}
21
+ />;
22
+ }
23
+
24
+ describe('<GibsonAmplifyAndCircularControls />', () => {
25
+
26
+ it('disables turning off a fragment when it would create two adjacent unamplified fragments in linear assemblies', () => {
27
+ cy.mount(
28
+ <GibsonAmplifyAndCircularControls
29
+ targets={baseTargets}
30
+ sequenceNames={baseSequenceNames}
31
+ amplified={[true, false, false]}
32
+ setAmplified={() => {}}
33
+ circularAssembly={false}
34
+ setCircularAssembly={() => {}}
35
+ />,
36
+ );
37
+
38
+
39
+ // First checkbox corresponds to Seq. 1
40
+ cy.get('input[type="checkbox"]').eq(0).should('be.disabled');
41
+ });
42
+
43
+ it('enforces circular adjacency rule when circularAssembly is true', () => {
44
+ // Configuration where turning off the last fragment is allowed
45
+ cy.mount(
46
+ <GibsonAmplifyAndCircularControls
47
+ targets={baseTargets}
48
+ sequenceNames={baseSequenceNames}
49
+ amplified={[false, true, true]}
50
+ setAmplified={() => {}}
51
+ circularAssembly
52
+ setCircularAssembly={() => {}}
53
+ />,
54
+ );
55
+
56
+ // Third checkbox corresponds to Seq. 3
57
+ cy.get('input[type="checkbox"]').eq(1).should('be.disabled');
58
+ cy.get('input[type="checkbox"]').eq(2).should('be.disabled');
59
+
60
+ // Configuration where turning off the last fragment would leave both ends unamplified
61
+ cy.mount(
62
+ <GibsonAmplifyAndCircularControls
63
+ targets={baseTargets}
64
+ sequenceNames={baseSequenceNames}
65
+ amplified={[false, true, false]}
66
+ setAmplified={() => {}}
67
+ circularAssembly
68
+ setCircularAssembly={() => {}}
69
+ />,
70
+ );
71
+
72
+ cy.get('input[type="checkbox"]').eq(1).should('be.disabled');
73
+
74
+ });
75
+
76
+ it('enforces linear rules when circularAssembly is false', () => {
77
+ cy.mount(
78
+ <GibsonAmplifyAndCircularControls
79
+ targets={baseTargets}
80
+ sequenceNames={baseSequenceNames}
81
+ amplified={[false, true, true]}
82
+ setAmplified={() => {}}
83
+ circularAssembly={false}
84
+ setCircularAssembly={() => {}}
85
+ />,
86
+ );
87
+ cy.get('input[type="checkbox"]').eq(1).should('be.disabled');
88
+ cy.get('input[type="checkbox"]').eq(2).should('not.be.disabled');
89
+
90
+ cy.mount(
91
+ <GibsonAmplifyAndCircularControls
92
+ targets={baseTargets}
93
+ sequenceNames={baseSequenceNames}
94
+ amplified={[true, true, false]}
95
+ setAmplified={() => {}}
96
+ circularAssembly={false}
97
+ setCircularAssembly={() => {}}
98
+ />,
99
+ );
100
+ cy.get('input[type="checkbox"]').eq(0).should('not.be.disabled');
101
+ cy.get('input[type="checkbox"]').eq(1).should('be.disabled');
102
+ });
103
+
104
+ it('disables circular assembly checkbox when there is only one target', () => {
105
+ cy.mount(
106
+ <GibsonAmplifyAndCircularControls
107
+ targets={[1]}
108
+ sequenceNames={[{ id: 1, name: 'single' }]}
109
+ amplified={[true]}
110
+ setAmplified={() => {}}
111
+ circularAssembly
112
+ setCircularAssembly={() => {}}
113
+ />,
114
+ );
115
+
116
+ cy.get('input[type="checkbox"]').eq(1).should('not.exist');
117
+ cy.get('input[name="circular-assembly"]').should('be.disabled').and('be.checked');
118
+ });
119
+
120
+ it('calls setAmplified with all true values when targets changeor circularAssembly changes', () => {
121
+ cy.mount(<TestComponent />);
122
+
123
+ cy.get('input[type="checkbox"]').eq(0).should('not.be.checked');
124
+ cy.get('input[type="checkbox"]').eq(2).should('not.be.checked');
125
+ cy.get('input[type="checkbox"]').last().click();
126
+ cy.get('input[type="checkbox"]').eq(0).should('be.checked');
127
+ cy.get('input[type="checkbox"]').eq(1).should('be.checked');
128
+ cy.get('input[type="checkbox"]').eq(2).should('be.checked');
129
+
130
+ })
131
+ });
@@ -1,27 +1,137 @@
1
- import { Button, FormControl } from '@mui/material';
1
+ import { Box, Button, Checkbox, FormControl, FormControlLabel, Tooltip, Typography } from '@mui/material';
2
+ import { Info as InfoIcon } from '@mui/icons-material';
2
3
  import React from 'react';
3
- import { batch, useDispatch } from 'react-redux';
4
+ import { useDispatch, useSelector } from 'react-redux';
5
+ import { isEqual } from 'lodash-es';
4
6
  import MultipleInputsSelector from '../../../sources/MultipleInputsSelector';
5
7
  import { cloningActions } from '@opencloning/store/cloning';
6
- import useStoreEditor from '../../../../hooks/useStoreEditor';
7
8
  import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
9
+ import useNavigateAfterPrimerDesign from './useNavigateAfterPrimerDesign';
10
+ import { getSequenceLabel } from '../SequenceTabComponents/utils/getSequenceLabel';
11
+
12
+ const { addPCRsAndSubsequentSourcesForAssembly } = cloningActions;
13
+
14
+ function AmplifySectionTitle() {
15
+ return (
16
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, mb: 0.5, alignSelf: 'stretch' }}>
17
+ <Typography variant="caption" sx={{ fontSize: '1rem', color: 'text.secondary' }}>
18
+ Amplify
19
+ </Typography>
20
+ <Tooltip
21
+ arrow
22
+ placement="top"
23
+ title="Checked sequences will be amplified by PCR with designed primers. Unchecked sequences are used directly in the assembly without amplification."
24
+ >
25
+ <InfoIcon sx={{ fontSize: '1rem', color: 'text.secondary' }} />
26
+ </Tooltip>
27
+ </Box>
28
+ );
29
+ }
30
+
31
+ export function GibsonAmplifyAndCircularControls({
32
+ targets,
33
+ sequenceNames,
34
+ amplified,
35
+ setAmplified,
36
+ circularAssembly,
37
+ setCircularAssembly,
38
+ }) {
39
+ const isValidAmplifiedConfig = (config) => {
40
+ for (let i = 0; i < config.length - 1; i += 1) {
41
+ if (!config[i] && !config[i + 1]) return false;
42
+ }
43
+ if (circularAssembly && config.length > 1 && !config[0] && !config[config.length - 1]) return false;
44
+ return true;
45
+ };
46
+
47
+ const canToggleOff = (index) => {
48
+ if (!amplified[index]) return true;
49
+ const next = [...amplified];
50
+ next[index] = false;
51
+ return isValidAmplifiedConfig(next);
52
+ };
53
+
54
+ const handleAmplifiedToggle = (index) => {
55
+ setAmplified((prev) => {
56
+ const next = [...prev];
57
+ next[index] = !next[index];
58
+ return next;
59
+ });
60
+ };
61
+
62
+ React.useEffect(() => {
63
+ setAmplified(Array(targets.length).fill(true));
64
+ }, [targets, circularAssembly]);
65
+
66
+ return (
67
+ <>
68
+ {targets.length > 1 && (
69
+ <Box data-testid="amplify-section" sx={{ mt: 1, display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
70
+ <AmplifySectionTitle />
71
+ {targets.map((id, index) => {
72
+ const name = sequenceNames.find((s) => s.id === id)?.name || 'template';
73
+ const label = getSequenceLabel(id, name);
74
+ return (
75
+ <FormControl data-testid={`amplify-section-${index}`} key={id} sx={{ alignItems: 'flex-start', mb: 0.5 }}>
76
+ <FormControlLabel
77
+ control={(
78
+ <Checkbox
79
+ checked={amplified[index]}
80
+ onChange={() => handleAmplifiedToggle(index)}
81
+ disabled={amplified[index] && !canToggleOff(index)}
82
+ size="small"
83
+ />
84
+ )}
85
+ label={(
86
+ <Typography variant="body2" noWrap sx={{ minWidth: 0 }}>
87
+ {label}
88
+ </Typography>
89
+ )}
90
+ sx={{ mr: 0 }}
91
+ />
92
+ </FormControl>
93
+ );
94
+ })}
95
+ </Box>
96
+ )}
97
+
98
+ <FormControl>
99
+ <FormControlLabel
100
+ control={(
101
+ <Checkbox
102
+ data-test="circular-assembly-checkbox"
103
+ disabled={targets.length === 1}
104
+ checked={circularAssembly}
105
+ onChange={(e) => setCircularAssembly(e.target.checked)}
106
+ name="circular-assembly"
107
+ />
108
+ )}
109
+ label={targets.length === 1 ? 'Circular assembly (only one sequence input)' : 'Circular assembly'}
110
+ />
111
+ </FormControl>
112
+ </>
113
+ );
114
+ }
8
115
 
9
116
  function PrimerDesignGibsonAssembly({ source, assemblyType }) {
10
117
  const [targets, setTargets] = React.useState(source.input.map(({ sequence }) => sequence));
118
+ const [amplified, setAmplified] = React.useState(() => source.input.map(() => true));
119
+ const [circularAssembly, setCircularAssembly] = React.useState(true);
11
120
  const inputSequenceId = getPcrTemplateSequenceId(source);
121
+ const dispatch = useDispatch();
122
+ const navigateAfterDesign = useNavigateAfterPrimerDesign();
123
+
124
+ const sequenceNames = useSelector(
125
+ ({ cloning }) => targets.map((id) => ({ id, name: cloning.teselaJsonCache[id]?.name || 'template' })),
126
+ isEqual,
127
+ );
12
128
 
13
129
  const onInputChange = (newInputSequenceIds) => {
14
- // Prevent unsetting the input of the source
15
- if (!newInputSequenceIds.includes(inputSequenceId)) {
16
- setTargets( (prev) => [...prev, ...newInputSequenceIds]);
17
- } else {
18
- setTargets(newInputSequenceIds);
19
- }
130
+ const newValue = newInputSequenceIds.includes(inputSequenceId) ? newInputSequenceIds : [...newInputSequenceIds, inputSequenceId];
131
+ setTargets(newValue);
132
+ setAmplified(Array(newValue.length).fill(true));
20
133
  };
21
134
 
22
- const { updateStoreEditor } = useStoreEditor();
23
- const { addPCRsAndSubsequentSourcesForAssembly, setCurrentTab, setMainSequenceId } = cloningActions;
24
- const dispatch = useDispatch();
25
135
  const onSubmit = (event) => {
26
136
  event.preventDefault();
27
137
  const newSequence = {
@@ -30,18 +140,18 @@ function PrimerDesignGibsonAssembly({ source, assemblyType }) {
30
140
  circular: false,
31
141
  };
32
142
 
33
- batch(() => {
34
- // Slice from the second on
35
- const newPCRTemplates = targets.slice(1);
36
- dispatch(addPCRsAndSubsequentSourcesForAssembly({ sourceId: source.id, newSequence, templateIds: newPCRTemplates, sourceType: assemblyType }));
37
- dispatch(setMainSequenceId(inputSequenceId));
38
- updateStoreEditor('mainEditor', inputSequenceId);
39
- dispatch(setCurrentTab(3));
40
- // Scroll to the top of the page after 300ms
41
- setTimeout(() => {
42
- document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
43
- }, 300);
44
- });
143
+ const firstAmplifiedId = targets.find((_, i) => amplified[i]) ?? inputSequenceId;
144
+ navigateAfterDesign(
145
+ () => dispatch(addPCRsAndSubsequentSourcesForAssembly({
146
+ sourceId: source.id,
147
+ newSequence,
148
+ templateIds: targets,
149
+ sourceType: assemblyType,
150
+ amplified,
151
+ circularAssembly,
152
+ })),
153
+ firstAmplifiedId,
154
+ );
45
155
  };
46
156
 
47
157
  return (
@@ -54,6 +164,15 @@ function PrimerDesignGibsonAssembly({ source, assemblyType }) {
54
164
  />
55
165
  </FormControl>
56
166
 
167
+ <GibsonAmplifyAndCircularControls
168
+ targets={targets}
169
+ sequenceNames={sequenceNames}
170
+ amplified={amplified}
171
+ setAmplified={setAmplified}
172
+ circularAssembly={circularAssembly}
173
+ setCircularAssembly={setCircularAssembly}
174
+ />
175
+
57
176
  <Button type="submit" variant="contained" color="success">
58
177
  Design primers
59
178
  </Button>
@@ -1,18 +1,19 @@
1
1
  import { Alert, Button, FormControl } from '@mui/material';
2
2
  import React from 'react';
3
- import { batch, useDispatch } from 'react-redux';
3
+ import { useDispatch } from 'react-redux';
4
4
  import SingleInputSelector from '../../../sources/SingleInputSelector';
5
5
  import { cloningActions } from '@opencloning/store/cloning';
6
- import useStoreEditor from '../../../../hooks/useStoreEditor';
7
6
  import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
7
+ import useNavigateAfterPrimerDesign from './useNavigateAfterPrimerDesign';
8
+
9
+ const { addTemplateChildAndSubsequentSource } = cloningActions;
8
10
 
9
11
  function PrimerDesignHomologousRecombination({ source, primerDesignType }) {
10
12
  const [target, setTarget] = React.useState('');
11
-
12
- const { updateStoreEditor } = useStoreEditor();
13
- const { addTemplateChildAndSubsequentSource, setCurrentTab, setMainSequenceId } = cloningActions;
14
13
  const dispatch = useDispatch();
15
14
  const inputSequenceId = getPcrTemplateSequenceId(source);
15
+ const navigateAfterDesign = useNavigateAfterPrimerDesign();
16
+
16
17
  const onSubmit = (event) => {
17
18
  event.preventDefault();
18
19
  const newSource = {
@@ -25,17 +26,12 @@ function PrimerDesignHomologousRecombination({ source, primerDesignType }) {
25
26
  circular: false,
26
27
  };
27
28
 
28
- batch(() => {
29
- dispatch(addTemplateChildAndSubsequentSource({ newSource, newSequence, sourceId: source.id }));
30
- dispatch(setMainSequenceId(inputSequenceId));
31
- updateStoreEditor('mainEditor', inputSequenceId);
32
- dispatch(setCurrentTab(3));
33
- // Scroll to the top of the page after 300ms
34
- setTimeout(() => {
35
- document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
36
- }, 300);
37
- });
29
+ navigateAfterDesign(
30
+ () => dispatch(addTemplateChildAndSubsequentSource({ newSource, newSequence, sourceId: source.id })),
31
+ inputSequenceId,
32
+ );
38
33
  };
34
+
39
35
  return (
40
36
  <form onSubmit={onSubmit}>
41
37
  <Alert severity="info" icon={false} sx={{ textAlign: 'left' }}>
@@ -1,22 +1,22 @@
1
1
  import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
2
2
  import React from 'react';
3
-
4
- import { batch, useDispatch } from 'react-redux';
3
+ import { useDispatch } from 'react-redux';
5
4
  import PrimerDesignHomologousRecombination from './PrimerDesignHomologousRecombination';
6
5
  import PrimerDesignGibsonAssembly from './PrimerDesignGibsonAssembly';
7
6
  import { cloningActions } from '@opencloning/store/cloning';
8
- import useStoreEditor from '../../../../hooks/useStoreEditor';
9
7
  import PrimerDesignGatewayBP from './PrimerDesignGatewayBP';
10
8
  import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
9
+ import useNavigateAfterPrimerDesign from './useNavigateAfterPrimerDesign';
10
+
11
+ const { addPCRsAndSubsequentSourcesForAssembly } = cloningActions;
11
12
 
12
13
  function PrimerDesignSourceForm({ source }) {
13
14
  const [primerDesignType, setPrimerDesignType] = React.useState('');
14
- const { updateStoreEditor } = useStoreEditor();
15
- const { addPCRsAndSubsequentSourcesForAssembly, setMainSequenceId, setCurrentTab } = cloningActions;
16
15
  const dispatch = useDispatch();
17
16
  const inputSequenceId = getPcrTemplateSequenceId(source);
17
+ const navigateAfterDesign = useNavigateAfterPrimerDesign();
18
+
18
19
  React.useEffect(() => {
19
- // Here the user does not have to select anything else
20
20
  if (primerDesignType === 'restriction_ligation' || primerDesignType === 'simple_pair') {
21
21
  const newSequence = {
22
22
  type: 'TemplateSequence',
@@ -24,16 +24,10 @@ function PrimerDesignSourceForm({ source }) {
24
24
  circular: false,
25
25
  };
26
26
 
27
- batch(() => {
28
- dispatch(addPCRsAndSubsequentSourcesForAssembly({ sourceId: source.id, newSequence, templateIds: [], sourceType: null }));
29
- dispatch(setMainSequenceId(inputSequenceId));
30
- updateStoreEditor('mainEditor', inputSequenceId);
31
- dispatch(setCurrentTab(3));
32
- // Scroll to the top of the page after 300ms
33
- setTimeout(() => {
34
- document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
35
- }, 300);
36
- });
27
+ navigateAfterDesign(
28
+ () => dispatch(addPCRsAndSubsequentSourcesForAssembly({ sourceId: source.id, newSequence, templateIds: [inputSequenceId], sourceType: null })),
29
+ inputSequenceId,
30
+ );
37
31
  }
38
32
  }, [primerDesignType]);
39
33
 
@@ -0,0 +1,23 @@
1
+ import { useCallback } from 'react';
2
+ import { batch, useDispatch } from 'react-redux';
3
+ import { cloningActions } from '@opencloning/store/cloning';
4
+ import useStoreEditor from '../../../../hooks/useStoreEditor';
5
+
6
+ const { setMainSequenceId, setCurrentTab } = cloningActions;
7
+
8
+ export default function useNavigateAfterPrimerDesign() {
9
+ const dispatch = useDispatch();
10
+ const { updateStoreEditor } = useStoreEditor();
11
+
12
+ return useCallback((primaryAction, inputSequenceId) => {
13
+ batch(() => {
14
+ primaryAction();
15
+ dispatch(setMainSequenceId(inputSequenceId));
16
+ updateStoreEditor('mainEditor', inputSequenceId);
17
+ dispatch(setCurrentTab(3));
18
+ setTimeout(() => {
19
+ document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
20
+ }, 300);
21
+ });
22
+ }, [dispatch, updateStoreEditor]);
23
+ }
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Version placeholder - replaced at publish time via prepack script
2
- export const version = "1.4.11";
2
+ export const version = "1.5.1";