@opencloning/ui 1.5.0 → 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 +12 -0
- package/package.json +3 -3
- 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
|
@@ -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();
|
|
@@ -7,7 +7,8 @@ import StepNavigation from './StepNavigation';
|
|
|
7
7
|
import PrimerResultForm from './PrimerResultForm';
|
|
8
8
|
|
|
9
9
|
function TabPanelResults() {
|
|
10
|
-
const { selectedTab, primers, addPrimers, setPrimers,
|
|
10
|
+
const { selectedTab, primers: allPrimers, addPrimers, setPrimers, sequenceIds } = usePrimerDesign();
|
|
11
|
+
const primers = React.useMemo(() => allPrimers.filter((primer) => primer !== null), [allPrimers]);
|
|
11
12
|
const existingPrimerNames = useSelector((state) => state.cloning.primers.map((p) => p.name), shallowEqual);
|
|
12
13
|
const primersAreValid = primers.length && primers.every((primer) => primer.name && !existingPrimerNames.includes(primer.name));
|
|
13
14
|
return (
|
|
@@ -26,8 +27,6 @@ function TabPanelResults() {
|
|
|
26
27
|
/>
|
|
27
28
|
))}
|
|
28
29
|
<StepNavigation
|
|
29
|
-
handleBack={handleBack}
|
|
30
|
-
handleNext={null}
|
|
31
30
|
onStepCompletion={addPrimers}
|
|
32
31
|
stepCompletionText="Save primers"
|
|
33
32
|
nextDisabled
|
|
@@ -9,7 +9,7 @@ import TabPanel from '../../../navigation/TabPanel';
|
|
|
9
9
|
import { usePrimerDesign } from './PrimerDesignContext';
|
|
10
10
|
|
|
11
11
|
function TabPanelSelectRoi({ step, index }) {
|
|
12
|
-
const { selectedTab, rois, handleSelectRegion, sequenceIds, primerDesignSettings, designType } = usePrimerDesign();
|
|
12
|
+
const { selectedTab, rois, handleSelectRegion, handleNext, sequenceIds, primerDesignSettings, designType, isAmplified } = usePrimerDesign();
|
|
13
13
|
const [error, setError] = React.useState('');
|
|
14
14
|
const editorHasSelection = useSelector((state) => state.cloning.mainSequenceSelection.caretPosition !== undefined);
|
|
15
15
|
const store = useStore();
|
|
@@ -21,18 +21,28 @@ function TabPanelSelectRoi({ step, index }) {
|
|
|
21
21
|
stepCompletionToolTip = 'Select a region in the editor',
|
|
22
22
|
} = step;
|
|
23
23
|
|
|
24
|
+
const notAmplified = !isAmplified[index];
|
|
25
|
+
|
|
24
26
|
const mode = designType === 'gateway_bp' && index === 1 ? 'gateway_bp' : 'editor';
|
|
25
|
-
const allowStepCompletion = (mode === 'editor' && editorHasSelection) || (mode === 'gateway_bp' && primerDesignSettings.knownCombination);
|
|
27
|
+
const allowStepCompletion = notAmplified || (mode === 'editor' && editorHasSelection) || (mode === 'gateway_bp' && primerDesignSettings.knownCombination);
|
|
26
28
|
const onStepCompletion = () => {
|
|
29
|
+
if (notAmplified) {
|
|
30
|
+
handleNext();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
27
33
|
const selectedRegion = store.getState().cloning.mainSequenceSelection;
|
|
28
34
|
setError(handleSelectRegion(index, selectedRegion, allowSinglePosition));
|
|
29
35
|
};
|
|
30
36
|
|
|
37
|
+
const allRequiredRoisSelected = rois.every((region, i) => region !== null || !isAmplified[i]);
|
|
38
|
+
|
|
31
39
|
return (
|
|
32
40
|
<TabPanel value={selectedTab} index={index} className={`select-roi-tab-${index}`}>
|
|
33
|
-
|
|
41
|
+
{notAmplified
|
|
42
|
+
? <Alert severity="info">The whole sequence will be used (not amplified by PCR). You can set orientation in the settings step.</Alert>
|
|
43
|
+
: <Alert severity="info">{description}</Alert>}
|
|
34
44
|
{error && (<Alert severity="error">{error}</Alert>)}
|
|
35
|
-
{mode === 'editor' && (
|
|
45
|
+
{!notAmplified && mode === 'editor' && (
|
|
36
46
|
<FormControl sx={{ py: 2 }}>
|
|
37
47
|
<TextField
|
|
38
48
|
label={inputLabel}
|
|
@@ -41,16 +51,16 @@ function TabPanelSelectRoi({ step, index }) {
|
|
|
41
51
|
/>
|
|
42
52
|
</FormControl>
|
|
43
53
|
)}
|
|
44
|
-
{mode === 'gateway_bp' && (
|
|
54
|
+
{!notAmplified && mode === 'gateway_bp' && (
|
|
45
55
|
<GatewayRoiSelect id={id} />
|
|
46
56
|
)}
|
|
47
57
|
<StepNavigation
|
|
48
58
|
isFirstStep={index === 0}
|
|
49
|
-
nextDisabled={(index === sequenceIds.length - 1) &&
|
|
59
|
+
nextDisabled={(index === sequenceIds.length - 1) && !allRequiredRoisSelected}
|
|
50
60
|
nextToolTip="You must select all regions before proceeding"
|
|
51
61
|
allowStepCompletion={allowStepCompletion}
|
|
52
|
-
stepCompletionText=
|
|
53
|
-
stepCompletionToolTip={stepCompletionToolTip}
|
|
62
|
+
stepCompletionText={notAmplified ? 'Next' : 'Choose region'}
|
|
63
|
+
stepCompletionToolTip={notAmplified ? '' : stepCompletionToolTip}
|
|
54
64
|
onStepCompletion={onStepCompletion}
|
|
55
65
|
/>
|
|
56
66
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Alert, Box,
|
|
2
|
+
import { Alert, Box, FormLabel } from '@mui/material';
|
|
3
3
|
import StepNavigation from './StepNavigation';
|
|
4
4
|
import TabPanel from '../../../navigation/TabPanel';
|
|
5
5
|
import PrimerSettingsForm from './PrimerSettingsForm';
|
|
@@ -8,8 +8,8 @@ import OrientationPicker from './OrientationPicker';
|
|
|
8
8
|
import { usePrimerDesign } from './PrimerDesignContext';
|
|
9
9
|
import RestrictionSpacerForm from './RestrictionSpacerForm';
|
|
10
10
|
|
|
11
|
-
function
|
|
12
|
-
const { error, templateSequenceIds, designType, selectedTab, sequenceIds,
|
|
11
|
+
function TabPanelSettings() {
|
|
12
|
+
const { error, templateSequenceIds, designType, selectedTab, sequenceIds, designPrimers, primers, primerDesignSettings, submissionPreventedMessage } = usePrimerDesign();
|
|
13
13
|
return (
|
|
14
14
|
<TabPanel value={selectedTab} index={sequenceIds.length}>
|
|
15
15
|
<Box sx={{ width: '80%', margin: 'auto' }}>
|
|
@@ -26,24 +26,6 @@ function TabPannelSettings() {
|
|
|
26
26
|
</Box>
|
|
27
27
|
{designType === 'restriction_ligation' && <RestrictionSpacerForm />}
|
|
28
28
|
<PrimerSpacerForm />
|
|
29
|
-
{designType === 'gibson_assembly' && (
|
|
30
|
-
<Box sx={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
|
31
|
-
<FormControl>
|
|
32
|
-
<FormControlLabel
|
|
33
|
-
control={(
|
|
34
|
-
<Checkbox
|
|
35
|
-
data-test="circular-assembly-checkbox"
|
|
36
|
-
disabled={templateSequenceIds.length === 1}
|
|
37
|
-
checked={circularAssembly}
|
|
38
|
-
onChange={(e) => setCircularAssembly(e.target.checked)}
|
|
39
|
-
name="circular-assembly"
|
|
40
|
-
/>
|
|
41
|
-
)}
|
|
42
|
-
label={templateSequenceIds.length === 1 ? 'Circular assembly (only one sequence input)' : 'Circular assembly'}
|
|
43
|
-
/>
|
|
44
|
-
</FormControl>
|
|
45
|
-
</Box>
|
|
46
|
-
)}
|
|
47
29
|
|
|
48
30
|
</Box>
|
|
49
31
|
{error && (<Alert severity="error" sx={{ width: 'fit-content', margin: 'auto', mb: 2 }}>{error}</Alert>)}
|
|
@@ -58,4 +40,4 @@ function TabPannelSettings() {
|
|
|
58
40
|
);
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
export default
|
|
43
|
+
export default TabPanelSettings;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { selectedRegion2SequenceLocation } from '@opencloning/utils/selectedRegionUtils';
|
|
2
|
+
import { ebicTemplateAnnotation, joinSequencesIntoSingleSequence, simulateHomologousRecombination } from '@opencloning/utils/sequenceManipulation';
|
|
3
|
+
|
|
4
|
+
const designTypeStrategies = {
|
|
5
|
+
simple_pair: {
|
|
6
|
+
computeProduct({ sequences, rois, fragmentOrientations, spacers, circularAssembly }) {
|
|
7
|
+
const product = joinSequencesIntoSingleSequence(
|
|
8
|
+
sequences, rois.map((s) => s.selectionLayer), fragmentOrientations,
|
|
9
|
+
spacers, circularAssembly, 'primer tail',
|
|
10
|
+
);
|
|
11
|
+
product.name = 'PCR product';
|
|
12
|
+
return product;
|
|
13
|
+
},
|
|
14
|
+
buildRequest({ sequences, sequenceIds, rois, fragmentOrientations, spacers, teselaJsonCache, paramsForRequest }) {
|
|
15
|
+
return {
|
|
16
|
+
endpoint: 'simple_pair',
|
|
17
|
+
params: { ...paramsForRequest },
|
|
18
|
+
requestData: {
|
|
19
|
+
pcr_template: {
|
|
20
|
+
sequence: sequences.find((e) => e.id === sequenceIds[0]),
|
|
21
|
+
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[sequenceIds[0]].size),
|
|
22
|
+
forward_orientation: fragmentOrientations[0] === 'forward',
|
|
23
|
+
},
|
|
24
|
+
spacers,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
restriction_ligation: {
|
|
31
|
+
computeProduct({ sequences, rois, fragmentOrientations, spacers, circularAssembly, primerDesignSettings }) {
|
|
32
|
+
const { enzymeSpacers } = primerDesignSettings;
|
|
33
|
+
const extendedSpacers = [enzymeSpacers[0] + spacers[0], spacers[1] + enzymeSpacers[1]];
|
|
34
|
+
const product = joinSequencesIntoSingleSequence(
|
|
35
|
+
sequences, rois.map((s) => s.selectionLayer), fragmentOrientations,
|
|
36
|
+
extendedSpacers, circularAssembly, 'primer tail',
|
|
37
|
+
);
|
|
38
|
+
product.name = 'PCR product';
|
|
39
|
+
return product;
|
|
40
|
+
},
|
|
41
|
+
buildRequest({ sequences, sequenceIds, rois, fragmentOrientations, spacers, teselaJsonCache, paramsForRequest }) {
|
|
42
|
+
return {
|
|
43
|
+
endpoint: 'simple_pair',
|
|
44
|
+
params: { ...paramsForRequest },
|
|
45
|
+
requestData: {
|
|
46
|
+
pcr_template: {
|
|
47
|
+
sequence: sequences.find((e) => e.id === sequenceIds[0]),
|
|
48
|
+
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[sequenceIds[0]].size),
|
|
49
|
+
forward_orientation: fragmentOrientations[0] === 'forward',
|
|
50
|
+
},
|
|
51
|
+
spacers,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
gibson_assembly: {
|
|
58
|
+
computeProduct({ sequences, rois, fragmentOrientations, spacers, circularAssembly }) {
|
|
59
|
+
const locations = rois.map((roi, i) => {
|
|
60
|
+
if (roi) return roi.selectionLayer;
|
|
61
|
+
return { start: 0, end: sequences[i].size - 1 };
|
|
62
|
+
});
|
|
63
|
+
const product = joinSequencesIntoSingleSequence(sequences, locations, fragmentOrientations, spacers, circularAssembly);
|
|
64
|
+
product.name = 'Gibson Assembly product';
|
|
65
|
+
return product;
|
|
66
|
+
},
|
|
67
|
+
buildRequest({ sequences, sequenceIds, rois, fragmentOrientations, spacers, isAmplified, teselaJsonCache, paramsForRequest, circularAssembly }) {
|
|
68
|
+
return {
|
|
69
|
+
endpoint: 'gibson_assembly',
|
|
70
|
+
params: { ...paramsForRequest, circular: circularAssembly },
|
|
71
|
+
requestData: {
|
|
72
|
+
pcr_templates: sequenceIds.map((id, index) => ({
|
|
73
|
+
sequence: sequences.find((e) => e.id === id),
|
|
74
|
+
location: isAmplified[index]
|
|
75
|
+
? selectedRegion2SequenceLocation(rois[index], teselaJsonCache[id].size)
|
|
76
|
+
: null,
|
|
77
|
+
forward_orientation: fragmentOrientations[index] === 'forward',
|
|
78
|
+
})),
|
|
79
|
+
spacers,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
homologous_recombination: {
|
|
86
|
+
computeProduct({ sequences, rois, fragmentOrientations, spacers }) {
|
|
87
|
+
const product = simulateHomologousRecombination(
|
|
88
|
+
sequences[0], sequences[1], rois, fragmentOrientations[0] === 'reverse', spacers,
|
|
89
|
+
);
|
|
90
|
+
product.name = 'Homologous recombination product';
|
|
91
|
+
return product;
|
|
92
|
+
},
|
|
93
|
+
buildRequest({ sequences, sequenceIds, rois, fragmentOrientations, spacers, teselaJsonCache, paramsForRequest }) {
|
|
94
|
+
const [pcrTemplateId, homologousRecombinationTargetId] = sequenceIds;
|
|
95
|
+
return {
|
|
96
|
+
endpoint: 'homologous_recombination',
|
|
97
|
+
params: { ...paramsForRequest },
|
|
98
|
+
requestData: {
|
|
99
|
+
pcr_template: {
|
|
100
|
+
sequence: sequences.find((e) => e.id === pcrTemplateId),
|
|
101
|
+
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[pcrTemplateId].size),
|
|
102
|
+
forward_orientation: fragmentOrientations[0] === 'forward',
|
|
103
|
+
},
|
|
104
|
+
homologous_recombination_target: {
|
|
105
|
+
sequence: sequences.find((e) => e.id === homologousRecombinationTargetId),
|
|
106
|
+
location: selectedRegion2SequenceLocation(rois[1], teselaJsonCache[homologousRecombinationTargetId].size),
|
|
107
|
+
},
|
|
108
|
+
spacers,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
gateway_bp: {
|
|
115
|
+
computeProduct({ sequences, rois, fragmentOrientations, spacers, primerDesignSettings }) {
|
|
116
|
+
const product = joinSequencesIntoSingleSequence(
|
|
117
|
+
[sequences[0]], [rois[0].selectionLayer], fragmentOrientations, spacers, false, 'primer tail',
|
|
118
|
+
);
|
|
119
|
+
product.name = 'PCR product';
|
|
120
|
+
const { knownCombination } = primerDesignSettings;
|
|
121
|
+
const leftFeature = {
|
|
122
|
+
start: knownCombination.translationFrame[0],
|
|
123
|
+
end: spacers[0].length - 1,
|
|
124
|
+
type: 'CDS',
|
|
125
|
+
name: 'translation frame',
|
|
126
|
+
strand: 1,
|
|
127
|
+
forward: true,
|
|
128
|
+
};
|
|
129
|
+
const nbAas = Math.floor((spacers[1].length - knownCombination.translationFrame[1]) / 3);
|
|
130
|
+
const rightStart = product.sequence.length - knownCombination.translationFrame[1] - nbAas * 3;
|
|
131
|
+
const rightFeature = {
|
|
132
|
+
start: rightStart,
|
|
133
|
+
end: product.sequence.length - knownCombination.translationFrame[1] - 1,
|
|
134
|
+
type: 'CDS',
|
|
135
|
+
name: 'translation frame',
|
|
136
|
+
strand: 1,
|
|
137
|
+
forward: true,
|
|
138
|
+
};
|
|
139
|
+
product.features.push(leftFeature);
|
|
140
|
+
product.features.push(rightFeature);
|
|
141
|
+
return product;
|
|
142
|
+
},
|
|
143
|
+
buildRequest({ sequences, sequenceIds, rois, fragmentOrientations, spacers, teselaJsonCache, paramsForRequest }) {
|
|
144
|
+
return {
|
|
145
|
+
endpoint: 'simple_pair',
|
|
146
|
+
params: { ...paramsForRequest },
|
|
147
|
+
requestData: {
|
|
148
|
+
pcr_template: {
|
|
149
|
+
sequence: sequences.find((e) => e.id === sequenceIds[0]),
|
|
150
|
+
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[sequenceIds[0]].size),
|
|
151
|
+
forward_orientation: fragmentOrientations[0] === 'forward',
|
|
152
|
+
},
|
|
153
|
+
spacers,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
ebic: {
|
|
160
|
+
computeProduct({ sequences, rois, primerDesignSettings }) {
|
|
161
|
+
return ebicTemplateAnnotation(sequences[0], rois[0].selectionLayer, primerDesignSettings);
|
|
162
|
+
},
|
|
163
|
+
buildRequest({ sequences, rois, templateSequenceIds, teselaJsonCache, paramsForRequest }) {
|
|
164
|
+
return {
|
|
165
|
+
endpoint: 'ebic',
|
|
166
|
+
params: { ...paramsForRequest },
|
|
167
|
+
requestData: {
|
|
168
|
+
template: {
|
|
169
|
+
sequence: sequences.find((e) => e.id === templateSequenceIds[0]),
|
|
170
|
+
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[templateSequenceIds[0]].size),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default designTypeStrategies;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { batch, useDispatch, useStore } from 'react-redux';
|
|
3
|
+
import useBackendRoute from '../../../../../hooks/useBackendRoute';
|
|
4
|
+
import useHttpClient from '../../../../../hooks/useHttpClient';
|
|
5
|
+
import useStoreEditor from '../../../../../hooks/useStoreEditor';
|
|
6
|
+
import error2String from '@opencloning/utils/error2String';
|
|
7
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
8
|
+
import designTypeStrategies from '../designTypeStrategies';
|
|
9
|
+
|
|
10
|
+
const { addPrimersToPCRSource, setMainSequenceId, setCurrentTab } = cloningActions;
|
|
11
|
+
|
|
12
|
+
export default function useDesignPrimers({
|
|
13
|
+
designType, sequenceIds, templateSequenceIds, isAmplified,
|
|
14
|
+
rois, fragmentOrientations, spacers, circularAssembly, primerDesignSettings,
|
|
15
|
+
handleNext, onTabChange,
|
|
16
|
+
}) {
|
|
17
|
+
const [primers, setPrimers] = useState([]);
|
|
18
|
+
const [error, setError] = useState('');
|
|
19
|
+
|
|
20
|
+
const store = useStore();
|
|
21
|
+
const dispatch = useDispatch();
|
|
22
|
+
const backendRoute = useBackendRoute();
|
|
23
|
+
const httpClient = useHttpClient();
|
|
24
|
+
const { updateStoreEditor } = useStoreEditor();
|
|
25
|
+
|
|
26
|
+
const designPrimers = useCallback(async () => {
|
|
27
|
+
fragmentOrientations.forEach((orientation) => {
|
|
28
|
+
if (orientation !== 'forward' && orientation !== 'reverse') {
|
|
29
|
+
throw new Error('Invalid fragment orientation');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { cloning: { sequences, teselaJsonCache, globalPrimerSettings } } = store.getState();
|
|
34
|
+
const paramsForRequest = Object.fromEntries(
|
|
35
|
+
Object.entries(primerDesignSettings)
|
|
36
|
+
.filter(([_, value]) => typeof value !== 'function'),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const strategy = designTypeStrategies[designType];
|
|
40
|
+
const { endpoint, params, requestData } = strategy.buildRequest({
|
|
41
|
+
sequences, sequenceIds, templateSequenceIds, rois, fragmentOrientations,
|
|
42
|
+
spacers, circularAssembly, isAmplified, teselaJsonCache, paramsForRequest,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
requestData.settings = globalPrimerSettings;
|
|
46
|
+
const url = backendRoute(`primer_design/${endpoint}`);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const resp = await httpClient.post(url, requestData, { params });
|
|
50
|
+
setError('');
|
|
51
|
+
setPrimers(resp.data.primers);
|
|
52
|
+
handleNext();
|
|
53
|
+
return false;
|
|
54
|
+
} catch (thrownError) {
|
|
55
|
+
setError(error2String(thrownError));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}, [fragmentOrientations, rois, sequenceIds, templateSequenceIds, designType, circularAssembly, primerDesignSettings, spacers, store, httpClient, backendRoute, handleNext, isAmplified]);
|
|
59
|
+
|
|
60
|
+
const addPrimers = useCallback(() => {
|
|
61
|
+
const pcrSources = store.getState().cloning.sources.filter((source) => source.type === 'PCRSource');
|
|
62
|
+
let usedPCRSources;
|
|
63
|
+
if (designType === 'ebic') {
|
|
64
|
+
usedPCRSources = pcrSources.filter((source) => source.input.some((i) => i.sequence === templateSequenceIds[0]));
|
|
65
|
+
} else {
|
|
66
|
+
const amplifiedTemplateIds = templateSequenceIds.filter((_, i) => isAmplified[i]);
|
|
67
|
+
usedPCRSources = amplifiedTemplateIds.map((id) => pcrSources.find((source) => source.input.some((i) => i.sequence === id)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const validPrimers = primers.filter((p) => p !== null);
|
|
71
|
+
batch(() => {
|
|
72
|
+
usedPCRSources.forEach((pcrSource, index) => {
|
|
73
|
+
dispatch(addPrimersToPCRSource({
|
|
74
|
+
fwdPrimer: validPrimers[index * 2],
|
|
75
|
+
revPrimer: validPrimers[index * 2 + 1],
|
|
76
|
+
sourceId: pcrSource.id,
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
dispatch(setMainSequenceId(null));
|
|
80
|
+
dispatch(setCurrentTab(0));
|
|
81
|
+
});
|
|
82
|
+
setPrimers([]);
|
|
83
|
+
onTabChange(null, 0);
|
|
84
|
+
document.getElementById(`source-${usedPCRSources[0].id}`)?.scrollIntoView();
|
|
85
|
+
updateStoreEditor('mainEditor', null);
|
|
86
|
+
}, [primers, dispatch, onTabChange, updateStoreEditor, designType, templateSequenceIds, isAmplified, store]);
|
|
87
|
+
|
|
88
|
+
return { primers, setPrimers, error, designPrimers, addPrimers };
|
|
89
|
+
}
|
package/src/components/primers/primer_design/SequenceTabComponents/hooks/useRegionSelection.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
function changeValueAtIndex(current, index, newValue) {
|
|
4
|
+
return current.map((_, i) => (i === index ? newValue : current[i]));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function useRegionSelection({ sequenceCount, handleNext }) {
|
|
8
|
+
const [rois, setRois] = useState(Array(sequenceCount).fill(null));
|
|
9
|
+
|
|
10
|
+
const onSelectRegion = useCallback((index, selectedRegion, allowSinglePosition = false) => {
|
|
11
|
+
const { caretPosition } = selectedRegion;
|
|
12
|
+
if (caretPosition === undefined) {
|
|
13
|
+
setRois((c) => changeValueAtIndex(c, index, null));
|
|
14
|
+
return 'You have to select a region in the sequence editor!';
|
|
15
|
+
}
|
|
16
|
+
if (caretPosition === -1) {
|
|
17
|
+
setRois((c) => changeValueAtIndex(c, index, selectedRegion));
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
if (allowSinglePosition) {
|
|
21
|
+
setRois((c) => changeValueAtIndex(c, index, selectedRegion));
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
setRois((c) => changeValueAtIndex(c, index, null));
|
|
25
|
+
return 'Select a region (not a single position) to amplify';
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const handleSelectRegion = useCallback((index, selectedRegion, allowSinglePosition = false) => {
|
|
29
|
+
const regionError = onSelectRegion(index, selectedRegion, allowSinglePosition);
|
|
30
|
+
if (!regionError) {
|
|
31
|
+
handleNext();
|
|
32
|
+
}
|
|
33
|
+
return regionError;
|
|
34
|
+
}, [onSelectRegion, handleNext]);
|
|
35
|
+
|
|
36
|
+
return { rois, handleSelectRegion };
|
|
37
|
+
}
|
package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSequenceProduct.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useStore } from 'react-redux';
|
|
3
|
+
import designTypeStrategies from '../designTypeStrategies';
|
|
4
|
+
|
|
5
|
+
export default function useSequenceProduct({
|
|
6
|
+
rois, spacers, spacersAreValid, fragmentOrientations, circularAssembly,
|
|
7
|
+
designType, primerDesignSettings, sequenceIds, submissionPreventedMessage,
|
|
8
|
+
}) {
|
|
9
|
+
const [sequenceProduct, setSequenceProduct] = React.useState(null);
|
|
10
|
+
const timeoutRef = React.useRef();
|
|
11
|
+
const store = useStore();
|
|
12
|
+
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
clearTimeout(timeoutRef.current);
|
|
15
|
+
|
|
16
|
+
timeoutRef.current = setTimeout(() => {
|
|
17
|
+
let newSequenceProduct = null;
|
|
18
|
+
if (submissionPreventedMessage === '') {
|
|
19
|
+
const { teselaJsonCache } = store.getState().cloning;
|
|
20
|
+
const sequences = sequenceIds.map((id) => teselaJsonCache[id]);
|
|
21
|
+
const strategy = designTypeStrategies[designType];
|
|
22
|
+
if (strategy) {
|
|
23
|
+
newSequenceProduct = strategy.computeProduct({
|
|
24
|
+
sequences, rois, fragmentOrientations, spacers, circularAssembly, primerDesignSettings,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
setSequenceProduct({ ...newSequenceProduct, id: 'opencloning_primer_design_product' });
|
|
29
|
+
}, 300);
|
|
30
|
+
|
|
31
|
+
return () => clearTimeout(timeoutRef.current);
|
|
32
|
+
}, [rois, spacersAreValid, fragmentOrientations, circularAssembly, designType, spacers, primerDesignSettings, sequenceIds, store, submissionPreventedMessage]);
|
|
33
|
+
|
|
34
|
+
return sequenceProduct;
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
|
|
3
|
+
|
|
4
|
+
export default function useSpacers({ initialLength }) {
|
|
5
|
+
const [spacers, setSpacers] = React.useState(Array(initialLength).fill(''));
|
|
6
|
+
|
|
7
|
+
const spacersAreValid = React.useMemo(
|
|
8
|
+
() => spacers.every((spacer) => !stringIsNotDNA(spacer)),
|
|
9
|
+
[spacers],
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
return { spacers, setSpacers, spacersAreValid };
|
|
13
|
+
}
|