@opencloning/ui 1.4.11 → 1.5.0
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/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/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#648](https://github.com/manulera/OpenCloning_frontend/pull/648) [`800ebcd`](https://github.com/manulera/OpenCloning_frontend/commit/800ebcd4ed610ad89d538e1d59314977aae40583) Thanks [@manulera](https://github.com/manulera)! - Multiple enzymes can be used in the Assembler
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`800ebcd`](https://github.com/manulera/OpenCloning_frontend/commit/800ebcd4ed610ad89d538e1d59314977aae40583)]:
|
|
12
|
+
- @opencloning/utils@1.5.0
|
|
13
|
+
- @opencloning/store@1.5.0
|
|
14
|
+
|
|
3
15
|
## 1.4.11
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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.
|
|
29
|
-
"@opencloning/utils": "1.
|
|
28
|
+
"@opencloning/store": "1.5.0",
|
|
29
|
+
"@opencloning/utils": "1.5.0",
|
|
30
30
|
"@teselagen/bio-parsers": "^0.4.34",
|
|
31
31
|
"@teselagen/ove": "^0.8.34",
|
|
32
32
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -117,7 +117,7 @@ describe('<AssemblerComponent />', () => {
|
|
|
117
117
|
<AssemblerComponent
|
|
118
118
|
plasmids={mockPlasmids}
|
|
119
119
|
categories={mockCategories}
|
|
120
|
-
|
|
120
|
+
assemblyEnzymes={['assembly_enzyme']}
|
|
121
121
|
addAlert={addAlertStub}
|
|
122
122
|
appInfo={{}}
|
|
123
123
|
/>
|
|
@@ -129,7 +129,7 @@ function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, a
|
|
|
129
129
|
)
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
export function AssemblerComponent({ plasmids, categories,
|
|
132
|
+
export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addAlert, appInfo }) {
|
|
133
133
|
|
|
134
134
|
const [requestedAssemblies, setRequestedAssemblies] = React.useState([])
|
|
135
135
|
const [errorMessage, setErrorMessage] = React.useState('')
|
|
@@ -153,7 +153,7 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme, addAl
|
|
|
153
153
|
const resp = await requestSources(selectedPlasmids)
|
|
154
154
|
errorMessage = 'Error assembling sequences'
|
|
155
155
|
setLoadingMessage('Assembling...')
|
|
156
|
-
const assemblies = await requestAssemblies(resp,
|
|
156
|
+
const assemblies = await requestAssemblies(resp, assemblyEnzymes)
|
|
157
157
|
setRequestedAssemblies(assemblies)
|
|
158
158
|
} catch (e) {
|
|
159
159
|
if (e.assembly) {
|
|
@@ -165,7 +165,7 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme, addAl
|
|
|
165
165
|
} finally {
|
|
166
166
|
setLoadingMessage(false)
|
|
167
167
|
}
|
|
168
|
-
}, [
|
|
168
|
+
}, [assemblyEnzymes, assembly, plasmids, requestSources, requestAssemblies, clearAssemblySelection])
|
|
169
169
|
|
|
170
170
|
const onDownloadAssemblies = React.useCallback(async () => {
|
|
171
171
|
try {
|
|
@@ -334,7 +334,7 @@ function Assembler() {
|
|
|
334
334
|
{syntax && <UploadPlasmidsButton addPlasmids={addPlasmids} syntax={syntax} />}
|
|
335
335
|
{syntax && <Button color="error" onClick={clearLoadedPlasmids}>Remove uploaded plasmids</Button>}
|
|
336
336
|
</ButtonGroup>
|
|
337
|
-
{syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories}
|
|
337
|
+
{syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories} assemblyEnzymes={syntax.assemblyEnzymes} addAlert={addAlert} appInfo={appInfo} />}
|
|
338
338
|
</>
|
|
339
339
|
)
|
|
340
340
|
}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
3
3
|
|
|
4
|
-
// Test config
|
|
5
|
-
const testConfig = {
|
|
6
|
-
backendUrl: 'http://localhost:8000',
|
|
7
|
-
showAppBar: false,
|
|
8
|
-
noExternalRequests: false,
|
|
9
|
-
enableAssembler: true,
|
|
10
|
-
enablePlannotate: false,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
4
|
const mockSyntaxes = [
|
|
14
5
|
{
|
|
15
6
|
path: 'test-syntax-1',
|
|
@@ -43,6 +34,7 @@ const mockSyntaxes = [
|
|
|
43
34
|
const mockSyntaxData = {
|
|
44
35
|
name: 'Test Syntax',
|
|
45
36
|
parts: [],
|
|
37
|
+
assemblyEnzymes: ['BsmBI'],
|
|
46
38
|
};
|
|
47
39
|
|
|
48
40
|
const mockPlasmidsData = [
|
|
@@ -228,6 +220,7 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
228
220
|
{ id: 1, name: 'Part 1' },
|
|
229
221
|
{ id: 2, name: 'Part 2' },
|
|
230
222
|
],
|
|
223
|
+
assemblyEnzymes: ['BsmBI'],
|
|
231
224
|
};
|
|
232
225
|
|
|
233
226
|
const tempFile = {
|
|
@@ -4,8 +4,8 @@ import getHttpClient from '@opencloning/utils/getHttpClient';
|
|
|
4
4
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
5
5
|
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
6
6
|
import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
|
|
7
|
+
import { normalizeSyntaxEnzymes } from '@opencloning/utils/normalizeSyntax';
|
|
7
8
|
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
|
|
8
|
-
import { IconButton } from '@mui/material';
|
|
9
9
|
|
|
10
10
|
const httpClient = getHttpClient();
|
|
11
11
|
const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
|
|
@@ -15,7 +15,7 @@ function LocalSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
|
15
15
|
|
|
16
16
|
const onFileSelected = React.useCallback(async (file) => {
|
|
17
17
|
const text = await readSubmittedTextFile(file);
|
|
18
|
-
const syntaxData = JSON.parse(text);
|
|
18
|
+
const syntaxData = normalizeSyntaxEnzymes(JSON.parse(text));
|
|
19
19
|
onSyntaxSelect(syntaxData, []);
|
|
20
20
|
onClose();
|
|
21
21
|
}, [onSyntaxSelect, onClose]);
|
|
@@ -100,7 +100,7 @@ function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect, disp
|
|
|
100
100
|
const { data: syntaxData } = await httpClient.get(syntaxPath);
|
|
101
101
|
loadingErrorPart = 'plasmids'
|
|
102
102
|
const { data: plasmidsData } = await httpClient.get(plasmidsPath);
|
|
103
|
-
onSyntaxSelect(syntaxData, plasmidsData);
|
|
103
|
+
onSyntaxSelect(normalizeSyntaxEnzymes(syntaxData), plasmidsData);
|
|
104
104
|
onClose();
|
|
105
105
|
} catch {
|
|
106
106
|
setLoadError(`Failed to load ${loadingErrorPart} data. Please try again.`);
|
|
@@ -114,9 +114,8 @@ function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect, disp
|
|
|
114
114
|
|
|
115
115
|
try {
|
|
116
116
|
const text = await file.text();
|
|
117
|
-
const syntaxData = JSON.parse(text);
|
|
117
|
+
const syntaxData = normalizeSyntaxEnzymes(JSON.parse(text));
|
|
118
118
|
|
|
119
|
-
// Uploaded JSON files contain only syntax data, no plasmids
|
|
120
119
|
onSyntaxSelect(syntaxData, []);
|
|
121
120
|
onClose();
|
|
122
121
|
} catch (error) {
|
|
@@ -3,13 +3,43 @@ import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
|
3
3
|
import { localFilesHttpClient } from '@opencloning/ui/hooks/useServerStaticFiles';
|
|
4
4
|
import UploadPlasmidsButton from './UploadPlasmidsButton';
|
|
5
5
|
import mocloSyntax from '../../../../../cypress/test_files/syntax/moclo_syntax.json';
|
|
6
|
-
import { dummyIndex } from '../form/ServerStaticFileSelect.cy.jsx';
|
|
7
6
|
|
|
8
7
|
mocloSyntax.overhangNames = {
|
|
9
8
|
...mocloSyntax.overhangNames,
|
|
10
9
|
CCCT: 'CCCT_overhang',
|
|
11
10
|
AACG: 'AACG_overhang',
|
|
12
11
|
};
|
|
12
|
+
mocloSyntax.assemblyEnzymes = ['BsaI'];
|
|
13
|
+
|
|
14
|
+
export const dummyIndex = {
|
|
15
|
+
sequences: [
|
|
16
|
+
{
|
|
17
|
+
name: 'Example sequence 1',
|
|
18
|
+
path: 'example.fa',
|
|
19
|
+
categories: ['Test category'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'Example sequence 2',
|
|
23
|
+
path: 'example2.gb',
|
|
24
|
+
categories: ['Test category2'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Example sequence 3',
|
|
28
|
+
path: 'example3.fa',
|
|
29
|
+
categories: ['Test category'],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
syntaxes: [
|
|
33
|
+
{
|
|
34
|
+
name: 'Example syntax 1',
|
|
35
|
+
path: 'example.json',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Example syntax 2',
|
|
39
|
+
path: 'example2.json',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
13
43
|
|
|
14
44
|
// Test config
|
|
15
45
|
const testConfig = {
|
|
@@ -20,7 +50,7 @@ const testConfig = {
|
|
|
20
50
|
enablePlannotate: false,
|
|
21
51
|
};
|
|
22
52
|
|
|
23
|
-
describe('<UploadPlasmidsButton />', () => {
|
|
53
|
+
describe.only('<UploadPlasmidsButton />', () => {
|
|
24
54
|
beforeEach(() => {
|
|
25
55
|
cy.window().then((win) => {
|
|
26
56
|
win.localStorage.clear();
|
|
@@ -117,17 +117,28 @@ export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
|
|
|
117
117
|
// used, which is convenient for classification within the syntax.
|
|
118
118
|
// Instead, forward means whether the recognition site was forward or reverse when producing that cut.
|
|
119
119
|
// see the test called "shows the meaning of forward and reverse" for more details.
|
|
120
|
-
const simplifiedDigestFragments = getSimplifiedDigestFragments(sequenceData, enzymes);
|
|
121
120
|
const foundParts = [];
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
|
|
122
|
+
// Enzymes are processed in order, so the first enzyme that finds a part is used.
|
|
123
|
+
// This is for syntaxes like GoldenBraid, that in the omega 1 assembly uses BsmBI
|
|
124
|
+
// for the backbone, and BtgZI for the parts.
|
|
125
|
+
// The order of the enzymes therefore matters, and in that case we put BsmBI first,
|
|
126
|
+
// because in principle domesticated parts should not have BsmBI recognition sites.
|
|
127
|
+
for (const enzyme of enzymes) {
|
|
128
|
+
const simplifiedDigestFragments = getSimplifiedDigestFragments(sequenceData, [enzyme]);
|
|
129
|
+
simplifiedDigestFragments
|
|
130
|
+
.filter(f => f.left.forward && !f.right.forward && graph.hasNode(f.left.ovhg) && graph.hasNode(f.right.ovhg))
|
|
131
|
+
.forEach(fragment => {
|
|
132
|
+
const graphForPaths = isFragmentPalindromic(fragment) ? openCycleAtNode(graph, graph.nodes()[0]) : graph;
|
|
133
|
+
const paths = allSimplePaths(graphForPaths, fragment.left.ovhg, fragment.right.ovhg);
|
|
134
|
+
if (paths.length > 0) {
|
|
135
|
+
foundParts.push({left_overhang: fragment.left.ovhg, right_overhang: fragment.right.ovhg, longestFeature: fragment.longestFeature});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
if (foundParts.length > 0) {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
131
142
|
return foundParts;
|
|
132
143
|
}
|
|
133
144
|
|
|
@@ -150,6 +150,35 @@ describe('assignSequenceToSyntaxPart', () => {
|
|
|
150
150
|
expect(type).toBe('misc_feature');
|
|
151
151
|
expect(name).toBe('feature1');
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
it('works with multiple enzymes', () => {
|
|
155
|
+
const enzymes = [aliasedEnzymesByName["bsmbi"], aliasedEnzymesByName["bsai"]];
|
|
156
|
+
const parts = [{left_overhang: 'TACT', right_overhang: 'AATG'}, {left_overhang: 'AATG', right_overhang: 'AGGT'}];
|
|
157
|
+
const graph = partsToEdgesGraph(parts);
|
|
158
|
+
// Works when both sites are of the same enzyme
|
|
159
|
+
const sequences = [
|
|
160
|
+
{ sequence: 'AAggtctcaTACTagagtcacacaggactactaAATGagagaccAA', circular: true },
|
|
161
|
+
{ sequence: 'AAcgtctcaTACTagagtcacacaggactactaAATGagagacgAA', circular: true },
|
|
162
|
+
];
|
|
163
|
+
for (const sequenceData of sequences) {
|
|
164
|
+
const result = assignSequenceToSyntaxPart(sequenceData, enzymes, graph);
|
|
165
|
+
expect(result).toEqual([{left_overhang: 'TACT', right_overhang: 'AATG', longestFeature: null}]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Does not work when the sites are of different enzymes
|
|
169
|
+
const sequenceData2 = { sequence: 'AAcgtctcaTACTagagtcacacaggactactaAATGagagaccAA', circular: true };
|
|
170
|
+
const result2 = assignSequenceToSyntaxPart(sequenceData2, enzymes, graph);
|
|
171
|
+
expect(result2).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('Does not assign a plasmid with a single cut', () => {
|
|
175
|
+
const parts = [{left_overhang: 'TACT', right_overhang: 'AATG'}, {left_overhang: 'AATG', right_overhang: 'AGGT'}];
|
|
176
|
+
const graph = partsToEdgesGraph(parts);
|
|
177
|
+
const enzymes = [aliasedEnzymesByName["bsai"]];
|
|
178
|
+
const sequenceData = { sequence: 'tgggtctcaTACTagagtc', circular: true };
|
|
179
|
+
const result = assignSequenceToSyntaxPart(sequenceData, enzymes, graph);
|
|
180
|
+
expect(result).toEqual([]);
|
|
181
|
+
})
|
|
153
182
|
});
|
|
154
183
|
|
|
155
184
|
describe('tripletsToTranslation', () => {
|
|
@@ -62,7 +62,7 @@ export const useAssembler = () => {
|
|
|
62
62
|
}, [ httpClient, backendRoute ])
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
const requestAssemblies = useCallback(async (requestedSources,
|
|
65
|
+
const requestAssemblies = useCallback(async (requestedSources, enzymes=['BsaI']) => {
|
|
66
66
|
|
|
67
67
|
const assemblies = arrayCombinations(requestedSources);
|
|
68
68
|
const output = []
|
|
@@ -76,7 +76,7 @@ export const useAssembler = () => {
|
|
|
76
76
|
const requestData = {
|
|
77
77
|
source: {
|
|
78
78
|
type: 'RestrictionAndLigationSource',
|
|
79
|
-
restriction_enzymes:
|
|
79
|
+
restriction_enzymes: enzymes,
|
|
80
80
|
id: assembly.length + 1,
|
|
81
81
|
},
|
|
82
82
|
sequences: assembly.map((p) => p.sequence),
|
|
@@ -8,11 +8,11 @@ import { aliasedEnzymesByName } from '@teselagen/sequence-utils';
|
|
|
8
8
|
* Custom hook that manages plasmids state and logic
|
|
9
9
|
* @param {Object} params - Dependencies from FormDataContext
|
|
10
10
|
* @param {Array} params.parts - Array of parts
|
|
11
|
-
* @param {string} params.
|
|
11
|
+
* @param {string[]} params.assemblyEnzymes - Assembly enzyme names
|
|
12
12
|
* @param {Object} params.overhangNames - Mapping of overhangs to names
|
|
13
13
|
* @returns {Object} - { linkedPlasmids, setLinkedPlasmids, uploadPlasmids }
|
|
14
14
|
*/
|
|
15
|
-
export function usePlasmidsLogic({ parts,
|
|
15
|
+
export function usePlasmidsLogic({ parts, assemblyEnzymes, overhangNames }) {
|
|
16
16
|
const [linkedPlasmids, setLinkedPlasmidsState] = React.useState([]);
|
|
17
17
|
|
|
18
18
|
const graphForPlasmids = React.useMemo(() => partsToEdgesGraph(parts), [parts]);
|
|
@@ -22,7 +22,7 @@ export function usePlasmidsLogic({ parts, assemblyEnzyme, overhangNames }) {
|
|
|
22
22
|
}, {}), [parts]);
|
|
23
23
|
|
|
24
24
|
const assignPlasmids = React.useCallback((plasmids) => plasmids.map(plasmid => {
|
|
25
|
-
const enzymes =
|
|
25
|
+
const enzymes = assemblyEnzymes.map(name => aliasedEnzymesByName[name.toLowerCase()]);
|
|
26
26
|
const correspondingParts = assignSequenceToSyntaxPart(plasmid, enzymes, graphForPlasmids);
|
|
27
27
|
const correspondingPartsStr = correspondingParts.map(part => `${part.left_overhang}-${part.right_overhang}`);
|
|
28
28
|
const correspondingPartsNames = correspondingParts.map(part => {
|
|
@@ -44,20 +44,20 @@ export function usePlasmidsLogic({ parts, assemblyEnzyme, overhangNames }) {
|
|
|
44
44
|
longestFeature: correspondingParts.map(part => part.longestFeature)
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
-
}), [graphForPlasmids, partDictionary,
|
|
47
|
+
}), [graphForPlasmids, partDictionary, assemblyEnzymes, overhangNames]);
|
|
48
48
|
|
|
49
49
|
// Wrapper for setLinkedPlasmids that automatically assigns plasmids if enzyme is available
|
|
50
50
|
const setLinkedPlasmids = React.useCallback((plasmids) => {
|
|
51
|
-
if (
|
|
51
|
+
if (assemblyEnzymes.length > 0 && Array.isArray(plasmids) && plasmids.length > 0) {
|
|
52
52
|
setLinkedPlasmidsState(assignPlasmids(plasmids));
|
|
53
53
|
} else {
|
|
54
54
|
setLinkedPlasmidsState(plasmids);
|
|
55
55
|
}
|
|
56
|
-
}, [
|
|
56
|
+
}, [assemblyEnzymes, assignPlasmids]);
|
|
57
57
|
|
|
58
58
|
// Update existing plasmids when enzyme or assignment logic changes
|
|
59
59
|
React.useEffect(() => {
|
|
60
|
-
if (
|
|
60
|
+
if (assemblyEnzymes.length > 0) {
|
|
61
61
|
setLinkedPlasmidsState((prevLinkedPlasmids) => {
|
|
62
62
|
if (prevLinkedPlasmids.length > 0) {
|
|
63
63
|
return assignPlasmids(prevLinkedPlasmids);
|
|
@@ -65,7 +65,7 @@ export function usePlasmidsLogic({ parts, assemblyEnzyme, overhangNames }) {
|
|
|
65
65
|
return prevLinkedPlasmids;
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
|
-
}, [assignPlasmids,
|
|
68
|
+
}, [assignPlasmids, assemblyEnzymes]); // Note: intentionally not including linkedPlasmids to avoid infinite loop
|
|
69
69
|
|
|
70
70
|
const uploadPlasmids = React.useCallback(async (files) => {
|
|
71
71
|
const plasmids = await Promise.all(files.map(async (file) => {
|
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.
|
|
2
|
+
export const version = "1.5.0";
|