@opencloning/ui 1.4.3 → 1.4.5
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 +20 -0
- package/package.json +3 -3
- package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +51 -0
- package/src/components/assembler/ExistingSyntaxDialog.jsx +49 -10
- package/src/components/assembler/assembler_utils.js +10 -1
- package/src/components/assembler/assembler_utils.test.js +28 -2
- package/src/components/assembler/graph_utils.js +5 -4
- package/src/components/assembler/graph_utils.test.js +9 -9
- package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx +3 -3
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.4.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#627](https://github.com/manulera/OpenCloning_frontend/pull/627) [`74e5365`](https://github.com/manulera/OpenCloning_frontend/commit/74e53653a27c2dc64f19afd984981c0fb70fc747) Thanks [@manulera](https://github.com/manulera)! - Allow selecting sub-assemblies from a kit
|
|
8
|
+
|
|
9
|
+
- Updated dependencies []:
|
|
10
|
+
- @opencloning/store@1.4.5
|
|
11
|
+
- @opencloning/utils@1.4.5
|
|
12
|
+
|
|
13
|
+
## 1.4.4
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#624](https://github.com/manulera/OpenCloning_frontend/pull/624) [`c333aa3`](https://github.com/manulera/OpenCloning_frontend/commit/c333aa3f5eda8ab7a65799d589ba6e9b376e2d24) Thanks [@manulera](https://github.com/manulera)! - Assigning plasmids to categories handles palindromic parts
|
|
18
|
+
|
|
19
|
+
- Updated dependencies []:
|
|
20
|
+
- @opencloning/store@1.4.4
|
|
21
|
+
- @opencloning/utils@1.4.4
|
|
22
|
+
|
|
3
23
|
## 1.4.3
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@emotion/styled": "^11.14.0",
|
|
26
26
|
"@mui/icons-material": "^5.15.17",
|
|
27
27
|
"@mui/material": "^5.15.17",
|
|
28
|
-
"@opencloning/store": "1.4.
|
|
29
|
-
"@opencloning/utils": "1.4.
|
|
28
|
+
"@opencloning/store": "1.4.5",
|
|
29
|
+
"@opencloning/utils": "1.4.5",
|
|
30
30
|
"@teselagen/bio-parsers": "^0.4.34",
|
|
31
31
|
"@teselagen/ove": "^0.8.34",
|
|
32
32
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -21,6 +21,23 @@ const mockSyntaxes = [
|
|
|
21
21
|
name: 'Test Syntax 2',
|
|
22
22
|
description: 'Second test syntax',
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
path: 'test-syntax-3',
|
|
26
|
+
name: 'Test Syntax 3',
|
|
27
|
+
description: 'Third test syntax',
|
|
28
|
+
syntaxes: [
|
|
29
|
+
{
|
|
30
|
+
path: 'test-syntax-3-1.json',
|
|
31
|
+
name: 'Test Syntax 3-1',
|
|
32
|
+
description: 'Third test syntax 1',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
path: 'test-syntax-3-2.json',
|
|
36
|
+
name: 'Test Syntax 3-2',
|
|
37
|
+
description: 'Third test syntax 2',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
24
41
|
];
|
|
25
42
|
|
|
26
43
|
const mockSyntaxData = {
|
|
@@ -265,4 +282,38 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
265
282
|
cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
|
|
266
283
|
cy.get('@onCloseSpy').should('not.have.been.called');
|
|
267
284
|
});
|
|
285
|
+
|
|
286
|
+
it('works with multiple syntaxes', () => {
|
|
287
|
+
const onCloseSpy = cy.spy().as('onCloseSpy');
|
|
288
|
+
const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
|
|
289
|
+
|
|
290
|
+
cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-3/test-syntax-3-2.json', {
|
|
291
|
+
statusCode: 200,
|
|
292
|
+
body: mockSyntaxData,
|
|
293
|
+
}).as('getSyntaxData');
|
|
294
|
+
|
|
295
|
+
cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-3/plasmids_test-syntax-3-2.json', {
|
|
296
|
+
statusCode: 200,
|
|
297
|
+
body: mockPlasmidsData,
|
|
298
|
+
}).as('getPlasmidsData');
|
|
299
|
+
|
|
300
|
+
cy.mount(
|
|
301
|
+
<ExistingSyntaxDialog
|
|
302
|
+
onClose={onCloseSpy}
|
|
303
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
304
|
+
/>,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
cy.wait('@getSyntaxes');
|
|
308
|
+
cy.contains('Test Syntax 3').click();
|
|
309
|
+
cy.contains('Test Syntax 3-1').should('exist');
|
|
310
|
+
cy.contains('Test Syntax 3-2').should('exist');
|
|
311
|
+
cy.contains('Third test syntax 1').should('exist');
|
|
312
|
+
cy.contains('Third test syntax 2').should('exist');
|
|
313
|
+
cy.contains('Test Syntax 3-2').click();
|
|
314
|
+
cy.wait('@getSyntaxData');
|
|
315
|
+
cy.wait('@getPlasmidsData');
|
|
316
|
+
cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', mockSyntaxData, mockPlasmidsData);
|
|
317
|
+
cy.get('@onCloseSpy').should('have.been.called');
|
|
318
|
+
});
|
|
268
319
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box, ButtonGroup } from '@mui/material'
|
|
2
|
+
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box, ButtonGroup, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'
|
|
3
3
|
import getHttpClient from '@opencloning/utils/getHttpClient';
|
|
4
4
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
5
5
|
import { useConfig } from '../../providers';
|
|
6
6
|
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
7
7
|
import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
|
|
8
|
+
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
|
|
8
9
|
|
|
9
10
|
const httpClient = getHttpClient();
|
|
10
11
|
const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
|
|
@@ -28,6 +29,48 @@ function LocalSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
|
28
29
|
)
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function SyntaxListItem({ syntax, onSyntaxClick }) {
|
|
33
|
+
if (syntax.syntaxes === undefined) {
|
|
34
|
+
const syntaxPath = syntax.path + '/syntax.json';
|
|
35
|
+
const plasmidsPath = syntax.path + '/plasmids.json';
|
|
36
|
+
return (
|
|
37
|
+
<ListItem>
|
|
38
|
+
<ListItemButton onClick={() => {onSyntaxClick(syntaxPath, plasmidsPath)}}>
|
|
39
|
+
<ListItemText primary={syntax.name} secondary={syntax.description} />
|
|
40
|
+
</ListItemButton>
|
|
41
|
+
</ListItem>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onOptionClick = (index) => {
|
|
46
|
+
const syntaxPath = `${syntax.path}/${syntax.syntaxes[index].path}`;
|
|
47
|
+
const plasmidsPath = `${syntax.path}/plasmids_${syntax.syntaxes[index].path}`;
|
|
48
|
+
onSyntaxClick(syntaxPath, plasmidsPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<ListItem>
|
|
53
|
+
<Accordion>
|
|
54
|
+
<AccordionSummary sx={{ my: 0, py: 0 }} expandIcon={<ExpandMoreIcon />}>
|
|
55
|
+
<ListItemText sx={{ my: 0, py: 0 }} primary={syntax.name} secondary={syntax.description} />
|
|
56
|
+
</AccordionSummary>
|
|
57
|
+
<AccordionDetails>
|
|
58
|
+
<List sx={{ py: 0, my: 0 }}>
|
|
59
|
+
{syntax.syntaxes.map((syntax, index) => (
|
|
60
|
+
<ListItem key={syntax.path} sx={{ my: 0, py: 0 }}>
|
|
61
|
+
<ListItemButton sx={{ py: 0 }} onClick={() => onOptionClick(index)}>
|
|
62
|
+
<ListItemText primary={syntax.name} secondary={syntax.description} />
|
|
63
|
+
</ListItemButton>
|
|
64
|
+
</ListItem>
|
|
65
|
+
))}
|
|
66
|
+
</List>
|
|
67
|
+
</AccordionDetails>
|
|
68
|
+
</Accordion>
|
|
69
|
+
</ListItem>
|
|
70
|
+
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
31
74
|
function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect }) {
|
|
32
75
|
const [syntaxes, setSyntaxes] = React.useState([]);
|
|
33
76
|
const [connectAttempt, setConnectAttempt] = React.useState(0);
|
|
@@ -42,7 +85,7 @@ function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect }) {
|
|
|
42
85
|
try {
|
|
43
86
|
const { data } = await httpClient.get('index.json');
|
|
44
87
|
setRequestStatus({ status: 'success' });
|
|
45
|
-
setSyntaxes(data);
|
|
88
|
+
setSyntaxes(data.sort((a, b) => a.name.localeCompare(b.name)));
|
|
46
89
|
} catch {
|
|
47
90
|
setRequestStatus({ status: 'error', message: 'Could not load syntaxes' });
|
|
48
91
|
}
|
|
@@ -50,13 +93,13 @@ function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect }) {
|
|
|
50
93
|
fetchData();
|
|
51
94
|
}, [connectAttempt]);
|
|
52
95
|
|
|
53
|
-
const onSyntaxClick = React.useCallback(async (
|
|
96
|
+
const onSyntaxClick = React.useCallback(async (syntaxPath, plasmidsPath) => {
|
|
54
97
|
setLoadError(null);
|
|
55
98
|
let loadingErrorPart = 'syntax'
|
|
56
99
|
try {
|
|
57
|
-
const { data: syntaxData } = await httpClient.get(
|
|
100
|
+
const { data: syntaxData } = await httpClient.get(syntaxPath);
|
|
58
101
|
loadingErrorPart = 'plasmids'
|
|
59
|
-
const { data: plasmidsData } = await httpClient.get(
|
|
102
|
+
const { data: plasmidsData } = await httpClient.get(plasmidsPath);
|
|
60
103
|
onSyntaxSelect(syntaxData, plasmidsData);
|
|
61
104
|
onClose();
|
|
62
105
|
} catch {
|
|
@@ -94,11 +137,7 @@ function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect }) {
|
|
|
94
137
|
<RequestStatusWrapper requestStatus={requestStatus} retry={() => setConnectAttempt((prev) => prev + 1)}>
|
|
95
138
|
<List>
|
|
96
139
|
{syntaxes.map((syntax) => (
|
|
97
|
-
<
|
|
98
|
-
<ListItemButton onClick={() => {onSyntaxClick(syntax)}}>
|
|
99
|
-
<ListItemText primary={syntax.name} secondary={syntax.description} />
|
|
100
|
-
</ListItemButton>
|
|
101
|
-
</ListItem>
|
|
140
|
+
<SyntaxListItem key={syntax.path} syntax={syntax} onSyntaxClick={onSyntaxClick} />
|
|
102
141
|
))}
|
|
103
142
|
</List>
|
|
104
143
|
</RequestStatusWrapper>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isRangeWithinRange } from '@teselagen/range-utils';
|
|
2
2
|
import { getComplementSequenceString, getAminoAcidFromSequenceTriplet, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString } from '@teselagen/sequence-utils';
|
|
3
3
|
import { allSimplePaths } from 'graphology-simple-path';
|
|
4
|
+
import { openCycleAtNode } from './graph_utils';
|
|
4
5
|
|
|
5
6
|
export function tripletsToTranslation(triplets) {
|
|
6
7
|
if (!triplets) return ''
|
|
@@ -100,6 +101,13 @@ export function getSimplifiedDigestFragments(sequenceData, enzymes) {
|
|
|
100
101
|
return simplifiedDigestFragments.concat(simplifiedDigestFragmentsRc);
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
export function isFragmentPalindromic(fragment) {
|
|
105
|
+
return (
|
|
106
|
+
(fragment.left.ovhg === getReverseComplementSequenceString(fragment.left.ovhg)) &&
|
|
107
|
+
(fragment.right.ovhg === getReverseComplementSequenceString(fragment.right.ovhg))
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
|
|
104
112
|
// Something that is important to understand here is the meaning of forward and reverse.
|
|
105
113
|
// It does not mean whether the overhang is 5' or 3', the value on the top strand is always
|
|
@@ -111,7 +119,8 @@ export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
|
|
|
111
119
|
simplifiedDigestFragments
|
|
112
120
|
.filter(f => f.left.forward && !f.right.forward && graph.hasNode(f.left.ovhg) && graph.hasNode(f.right.ovhg))
|
|
113
121
|
.forEach(fragment => {
|
|
114
|
-
const
|
|
122
|
+
const graphForPaths = isFragmentPalindromic(fragment) ? openCycleAtNode(graph, graph.nodes()[0]) : graph;
|
|
123
|
+
const paths = allSimplePaths(graphForPaths, fragment.left.ovhg, fragment.right.ovhg);
|
|
115
124
|
if (paths.length > 0) {
|
|
116
125
|
foundParts.push({left_overhang: fragment.left.ovhg, right_overhang: fragment.right.ovhg, longestFeature: fragment.longestFeature});
|
|
117
126
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { aliasedEnzymesByName, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString, getComplementSequenceString } from "@teselagen/sequence-utils";
|
|
2
|
-
import
|
|
1
|
+
import { aliasedEnzymesByName, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString, getComplementSequenceString, getReverseComplementSequenceAndAnnotations } from "@teselagen/sequence-utils";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { assignSequenceToSyntaxPart, simplifyDigestFragment, reverseComplementSimplifiedDigestFragment, tripletsToTranslation, partDataToDisplayData, arrayCombinations, getSimplifiedDigestFragments } from "./assembler_utils";
|
|
3
4
|
import { partsToEdgesGraph } from "./graph_utils";
|
|
5
|
+
import { genbankToJson } from '@teselagen/bio-parsers';
|
|
4
6
|
|
|
5
7
|
const sequenceBsaI = 'tgggtctcaTACTagagtcacacaggactactaAATGagagacctac';
|
|
6
8
|
const sequenceBsaI2 = 'tgggtctcaAATGagagtcacacaggactactaAGGTagagacctac'
|
|
@@ -28,6 +30,30 @@ describe('reverseComplementSimplifiedDigestFragment', () => {
|
|
|
28
30
|
});
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
it('handles palindromic fragments', () => {
|
|
36
|
+
|
|
37
|
+
const parts = [
|
|
38
|
+
{left_overhang: 'AATT', right_overhang: 'AGCT'}, // This part is palindromic, should only be picked up in AATT-AGCT, not AGCT-AATT
|
|
39
|
+
{left_overhang: 'AGCT', right_overhang: 'GGAG'},
|
|
40
|
+
{left_overhang: 'GGAG', right_overhang: 'AATT'},
|
|
41
|
+
]
|
|
42
|
+
// Read file
|
|
43
|
+
const sequence = 'tgggtctcaAATTagagtcacacaggactactaAGCTagagacctac'
|
|
44
|
+
const seqData = { sequence, circular: true };
|
|
45
|
+
const result = assignSequenceToSyntaxPart(seqData, [aliasedEnzymesByName["bsai"]], partsToEdgesGraph(parts));
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual([{left_overhang: 'AATT', right_overhang: 'AGCT', longestFeature: null}]);
|
|
48
|
+
|
|
49
|
+
const seqDataRc = getReverseComplementSequenceAndAnnotations(seqData);
|
|
50
|
+
const resultRc = assignSequenceToSyntaxPart(seqDataRc, [aliasedEnzymesByName["bsai"]], partsToEdgesGraph(parts));
|
|
51
|
+
|
|
52
|
+
expect(resultRc).toEqual([{left_overhang: 'AATT', right_overhang: 'AGCT', longestFeature: null}]);
|
|
53
|
+
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
|
|
31
57
|
it('shows the meaning of forward and reverse', () => {
|
|
32
58
|
const sequence = 'aaGGTCTCaTACTaaa'
|
|
33
59
|
const digestFragments = getDigestFragmentsForRestrictionEnzymes(
|
|
@@ -40,9 +40,11 @@ export function partsToEdgesGraph(parts) {
|
|
|
40
40
|
|
|
41
41
|
// Break cycles by removing incoming edges to a node
|
|
42
42
|
export function openCycleAtNode(graph, cutNode) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const newGraph = graph.copy();
|
|
44
|
+
for (const edge of newGraph.inEdges(cutNode)) {
|
|
45
|
+
newGraph.dropEdge(edge);
|
|
45
46
|
}
|
|
47
|
+
return newGraph;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
// Convert DAG to MSA-like matrix (rows = paths, columns = topological generations)
|
|
@@ -142,8 +144,7 @@ function minimumCoveringRows(msa) {
|
|
|
142
144
|
|
|
143
145
|
export function graphToMSA(graph) {
|
|
144
146
|
if (graph.nodes().length === 0) return [];
|
|
145
|
-
const newGraph = graph.
|
|
146
|
-
openCycleAtNode(newGraph, newGraph.nodes()[0]);
|
|
147
|
+
const newGraph = openCycleAtNode(graph, graph.nodes()[0]);
|
|
147
148
|
return minimumCoveringRows(dagToMSA(newGraph));
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -114,9 +114,9 @@ describe('openCycleAtNode', () => {
|
|
|
114
114
|
graph.addEdge('C', 'B'); // Creates cycle
|
|
115
115
|
|
|
116
116
|
expect(graph.inDegree('B')).toBe(2);
|
|
117
|
-
openCycleAtNode(graph, 'B');
|
|
118
|
-
expect(
|
|
119
|
-
expect(
|
|
117
|
+
const newGraph = openCycleAtNode(graph, 'B');
|
|
118
|
+
expect(newGraph.inDegree('B')).toBe(0);
|
|
119
|
+
expect(newGraph.outDegree('B')).toBe(1);
|
|
120
120
|
|
|
121
121
|
});
|
|
122
122
|
|
|
@@ -127,9 +127,9 @@ describe('openCycleAtNode', () => {
|
|
|
127
127
|
graph.addEdge('A', 'B');
|
|
128
128
|
graph.addEdge('B', 'A'); // Cycle
|
|
129
129
|
|
|
130
|
-
openCycleAtNode(graph, 'A');
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
130
|
+
const newGraph = openCycleAtNode(graph, 'A');
|
|
131
|
+
expect(newGraph.outDegree('A')).toBe(1);
|
|
132
|
+
expect(newGraph.hasEdge('A', 'B')).toBe(true);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
it('handles node with no incoming edges', () => {
|
|
@@ -138,9 +138,9 @@ describe('openCycleAtNode', () => {
|
|
|
138
138
|
graph.addNode('B');
|
|
139
139
|
graph.addEdge('A', 'B');
|
|
140
140
|
|
|
141
|
-
openCycleAtNode(graph, 'A');
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
141
|
+
const newGraph = openCycleAtNode(graph, 'A');
|
|
142
|
+
expect(newGraph.inDegree('A')).toBe(0);
|
|
143
|
+
expect(newGraph.hasEdge('A', 'B')).toBe(true);
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|
package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx
CHANGED
|
@@ -6,7 +6,7 @@ import { updateEditor } from '@teselagen/ove';
|
|
|
6
6
|
import EnzymeMultiSelect from '../../../form/EnzymeMultiSelect';
|
|
7
7
|
import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
|
|
8
8
|
import { usePrimerDesign } from './PrimerDesignContext';
|
|
9
|
-
import {
|
|
9
|
+
import { isEnzymePalindromic } from '@opencloning/utils/enzyme_utils';
|
|
10
10
|
|
|
11
11
|
function RestrictionSpacerForm() {
|
|
12
12
|
const { primerDesignSettings } = usePrimerDesign();
|
|
@@ -39,7 +39,7 @@ function RestrictionSpacerForm() {
|
|
|
39
39
|
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
|
|
40
40
|
<EnzymeMultiSelect value={leftEnzyme} setEnzymes={(v) => updateEnzymeSettings({ left_enzyme: v })} label="Left enzyme" multiple={false} />
|
|
41
41
|
</FormControl>
|
|
42
|
-
{leftEnzyme && !
|
|
42
|
+
{leftEnzyme && !isEnzymePalindromic(leftEnzyme) && (
|
|
43
43
|
<FormControlLabel
|
|
44
44
|
control={<Checkbox checked={leftEnzymeInverted} onChange={(e) => updateEnzymeSettings({ left_enzyme_inverted: e.target.checked })} />}
|
|
45
45
|
label="Invert site"
|
|
@@ -50,7 +50,7 @@ function RestrictionSpacerForm() {
|
|
|
50
50
|
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
|
|
51
51
|
<EnzymeMultiSelect value={rightEnzyme} setEnzymes={(v) => updateEnzymeSettings({ right_enzyme: v })} label="Right enzyme" multiple={false} />
|
|
52
52
|
</FormControl>
|
|
53
|
-
{rightEnzyme && !
|
|
53
|
+
{rightEnzyme && !isEnzymePalindromic(rightEnzyme) && (
|
|
54
54
|
<FormControlLabel
|
|
55
55
|
control={<Checkbox checked={rightEnzymeInverted} onChange={(e) => updateEnzymeSettings({ right_enzyme_inverted: e.target.checked })} />}
|
|
56
56
|
label="Invert site"
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Version placeholder - replaced at publish time via prepack script
|
|
2
|
-
export const version = "1.4.
|
|
2
|
+
export const version = "1.4.5";
|