@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.
- package/CHANGELOG.md +24 -0
- package/package.json +3 -3
- package/src/components/assembler/Assembler.cy.jsx +1 -1
- package/src/components/assembler/Assembler.jsx +4 -4
- package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +2 -9
- package/src/components/assembler/ExistingSyntaxDialog.jsx +4 -5
- package/src/components/assembler/UploadPlasmidsButton.cy.jsx +32 -2
- package/src/components/assembler/assembler_utils.js +21 -10
- package/src/components/assembler/assembler_utils.test.js +29 -0
- package/src/components/assembler/useAssembler.js +2 -2
- package/src/components/assembler/usePlasmidsLogic.js +8 -8
- 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
|
@@ -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>
|
|
@@ -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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
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();
|