@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,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>
@@ -12,12 +12,13 @@ import PrimerDesignGatewayBP from './PrimerDesignGatewayBP';
12
12
  import PrimerDesignEBIC from './PrimerDesignEBIC';
13
13
  import PrimerDesignRestriction from './PrimerDesignRestriction';
14
14
 
15
+ const { setMainSequenceId } = cloningActions;
16
+
15
17
  function PrimerDesigner() {
16
18
  const { updateStoreEditor } = useStoreEditor();
17
19
  const dispatch = useDispatch();
18
- const { setMainSequenceId } = cloningActions;
19
20
 
20
- const { finalSource, otherInputIds, pcrSources, outputSequences } = useSelector((state) => getPrimerDesignObject(state.cloning), isEqual);
21
+ const { finalSource, otherInputIds, pcrSources, outputSequences, assemblyInputsInOrder } = useSelector((state) => getPrimerDesignObject(state.cloning), isEqual);
21
22
 
22
23
  const mainSequenceId = useSelector((state) => state.cloning.mainSequenceId);
23
24
 
@@ -36,36 +37,30 @@ function PrimerDesigner() {
36
37
  const showPrimerDesigner = [...templateSequencesIds, ...otherInputIds].includes(mainSequenceId);
37
38
 
38
39
  let component = null;
39
- // Check conditions for different types of primer design
40
40
  if (finalSource === null && pcrSources.length === 1 && outputSequences[0].primer_design === 'restriction_ligation') {
41
41
  component = <PrimerDesignRestriction pcrSource={pcrSources[0]} />;
42
- }
43
- if (finalSource === null && pcrSources.length === 1 && outputSequences[0].primer_design === 'simple_pair') {
42
+ } else if (finalSource === null && pcrSources.length === 1 && outputSequences[0].primer_design === 'simple_pair') {
44
43
  component = <PrimerDesignSimplePair pcrSource={pcrSources[0]} />;
45
- }
46
- if (finalSource?.type === 'GibsonAssemblySource' || finalSource?.type === 'InFusionSource' || finalSource?.type === 'InVivoAssemblySource' || finalSource?.type === 'CreLoxRecombinationSource') {
47
- component = <PrimerDesignGibsonAssembly pcrSources={pcrSources} />;
48
- }
49
- if (finalSource?.type === 'HomologousRecombinationSource' && otherInputIds.length === 1 && pcrSources.length === 1) {
44
+ } else if (finalSource?.type === 'GibsonAssemblySource' || finalSource?.type === 'InFusionSource' || finalSource?.type === 'InVivoAssemblySource') {
45
+ component = <PrimerDesignGibsonAssembly assemblyInputsInOrder={assemblyInputsInOrder} circularAssembly={finalSource.circular_assembly} />;
46
+ } else if (finalSource?.type === 'HomologousRecombinationSource' && otherInputIds.length === 1 && pcrSources.length === 1) {
50
47
  component = (
51
48
  <PrimerDesignHomologousRecombination
52
49
  homologousRecombinationTargetId={otherInputIds[0]}
53
50
  pcrSource={pcrSources[0]}
54
51
  />
55
52
  );
56
- }
57
- if (finalSource?.type === 'GatewaySource' && otherInputIds.length === 1 && pcrSources.length === 1 && outputSequences[0].primer_design === 'gateway_bp') {
53
+ } else if (finalSource?.type === 'GatewaySource' && otherInputIds.length === 1 && pcrSources.length === 1 && outputSequences[0].primer_design === 'gateway_bp') {
58
54
  component = <PrimerDesignGatewayBP donorVectorId={otherInputIds[0]} pcrSource={pcrSources[0]} />;
59
- }
60
- if (finalSource?.type === 'RestrictionAndLigationSource' && outputSequences.every((outputSequence) => outputSequence.primer_design === 'ebic')) {
55
+ } else if (finalSource?.type === 'RestrictionAndLigationSource' && outputSequences.every((outputSequence) => outputSequence.primer_design === 'ebic')) {
61
56
  component = <PrimerDesignEBIC pcrSources={pcrSources} />;
62
57
  }
63
58
  return (
64
59
  <>
65
60
  {!showPrimerDesigner && (
66
- <div>
67
- <Button sx={{ mb: 4 }} variant="contained" color="success" onClick={openPrimerDesigner}>Open primer designer</Button>
68
- </div>
61
+ <div>
62
+ <Button sx={{ mb: 4 }} variant="contained" color="success" onClick={openPrimerDesigner}>Open primer designer</Button>
63
+ </div>
69
64
  )}
70
65
  <Box className="primer-design" sx={{ display: showPrimerDesigner ? 'auto' : 'none', width: '60%', minWidth: '600px', margin: 'auto', border: 1, borderRadius: 2, overflow: 'hidden', borderColor: 'primary.main', marginBottom: 5 }}>
71
66
  <Box sx={{ margin: 'auto', display: 'flex', height: 'auto', borderBottom: 2, borderColor: 'primary.main', backgroundColor: 'primary.main' }}>
@@ -3,9 +3,10 @@ import { FormControl, TextField, Box } from '@mui/material';
3
3
  import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
4
4
  import CollapsableLabel from './CollapsableLabel';
5
5
  import { usePrimerDesign } from './PrimerDesignContext';
6
+ import { getSequenceLabel } from './utils/getSequenceLabel';
6
7
 
7
8
  function PrimerSpacerForm({ open = true }) {
8
- const { spacers, setSpacers, circularAssembly, templateSequenceNames, templateSequenceIds } = usePrimerDesign();
9
+ const { spacers, setSpacers, circularAssembly, templateSequenceNames, templateSequenceIds, isAmplified } = usePrimerDesign();
9
10
 
10
11
  const fragmentCount = templateSequenceIds.length;
11
12
 
@@ -19,7 +20,7 @@ function PrimerSpacerForm({ open = true }) {
19
20
  const getSequenceName = (seqIndex) => {
20
21
  const name = sequenceNamesWrapped[seqIndex];
21
22
  const id = templateSequenceIdsWrapped[seqIndex];
22
- return name && name !== 'name' ? `Seq. ${id} (${name})` : `Seq. ${id}`;
23
+ return getSequenceLabel(id, name);
23
24
  };
24
25
 
25
26
  const getSpacerLabel = (index) => {
@@ -40,6 +41,9 @@ function PrimerSpacerForm({ open = true }) {
40
41
  <Box>
41
42
  {spacers.map((spacer, index) => {
42
43
  const error = stringIsNotDNA(spacer) ? 'Invalid DNA sequence' : '';
44
+ const isFirstSpacerDisabled = !circularAssembly && index === 0 && !isAmplified[0];
45
+ const isLastSpacerDisabled = !circularAssembly && index === fragmentCount && !isAmplified[fragmentCount - 1];
46
+ const disabled = isFirstSpacerDisabled || isLastSpacerDisabled;
43
47
  return (
44
48
  <FormControl key={index} fullWidth sx={{ mb: 2 }}>
45
49
  <TextField
@@ -48,12 +52,12 @@ function PrimerSpacerForm({ open = true }) {
48
52
  onChange={(e) => handleSpacerChange(index, e.target.value)}
49
53
  variant="outlined"
50
54
  size="small"
55
+ disabled={disabled}
51
56
  inputProps={{
52
57
  id: 'sequence',
53
58
  }}
54
- // Error if not DNA
55
59
  error={error !== ''}
56
- helperText={error}
60
+ helperText={disabled ? 'Not editable (adjacent fragment is not amplified)' : error}
57
61
  />
58
62
  </FormControl>
59
63
  );
@@ -6,65 +6,7 @@ import StepNavigation from './StepNavigation';
6
6
  import { useSelector } from 'react-redux';
7
7
  import EnzymeMultiSelect from '../../../form/EnzymeMultiSelect';
8
8
  import { isEqual } from 'lodash-es';
9
- import { getSequenceWithinRange } from '@teselagen/range-utils';
10
- import { aliasedEnzymesByName, cutSequenceByRestrictionEnzyme } from '@teselagen/sequence-utils';
11
-
12
- function trimPadding({ templateSequence, padding_left, padding_right, restrictionSitesToAvoid, roi, max_inside, max_outside }) {
13
- const { start, end } = roi.selectionLayer;
14
- const leftAnnotationRange = { start: start - padding_left, end: start - 1 };
15
- const leftArm = getSequenceWithinRange(leftAnnotationRange, templateSequence.sequence);
16
- const rightAnnotationRange = { start: end + 1, end: end + padding_right };
17
- const rightArm = getSequenceWithinRange(rightAnnotationRange, templateSequence.sequence);
18
-
19
- const leftMargin = { start: start - max_outside, end: start + max_inside - 1 };
20
- const rightMargin = { start: end - max_inside, end: end + max_outside - 1 };
21
- const leftMarginArm = getSequenceWithinRange(leftMargin, templateSequence.sequence);
22
- const rightMarginArm = getSequenceWithinRange(rightMargin, templateSequence.sequence);
23
-
24
- const enzymes = restrictionSitesToAvoid.map((enzyme) => aliasedEnzymesByName[enzyme.toLowerCase()]);
25
- if (enzymes.length === 0) {
26
- return { padding_left, padding_right, cutsitesInMargins: false };
27
- }
28
-
29
- const cutsInLeftMargin = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
30
- leftMarginArm,
31
- true,
32
- enzyme
33
- ));
34
- const cutsInRightMargin = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
35
- rightMarginArm,
36
- false,
37
- enzyme
38
- ));
39
-
40
- const cutsitesInMargins = cutsInLeftMargin.length > 0 || cutsInRightMargin.length > 0;
41
-
42
- const leftCutsites = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
43
- leftArm,
44
- true,
45
- enzyme
46
- ));
47
- const rightCutsites = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
48
- rightArm,
49
- false,
50
- enzyme
51
- ));
52
-
53
- let paddingLeft = padding_left;
54
- let paddingRight = padding_right;
55
- if (leftCutsites.length > 0) {
56
- paddingLeft = leftArm.length - 1 - Math.max(...leftCutsites.map((cutsite) => cutsite.recognitionSiteRange.end));
57
- }
58
- if (rightCutsites.length > 0) {
59
- paddingRight = Math.min(...rightCutsites.map((cutsite) => cutsite.recognitionSiteRange.start));
60
- }
61
- return {
62
- padding_left: paddingLeft,
63
- padding_right: paddingRight,
64
- cutsitesInMargins,
65
- };
66
-
67
- }
9
+ import trimPadding from './utils/trimPadding';
68
10
 
69
11
  function TabPanelEBICSettings() {
70
12
  const { error, selectedTab, sequenceIds, primers, submissionPreventedMessage, designPrimers, primerDesignSettings, rois } = usePrimerDesign();