@opencloning/ui 1.3.0 → 1.3.2
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/Assembler.jsx +1 -1
- package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +57 -0
- package/src/components/assembler/ExistingSyntaxDialog.jsx +41 -2
- package/src/components/assembler/assembler_utils.js +5 -0
- package/src/components/assembler/assembler_utils.test.js +40 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#605](https://github.com/manulera/OpenCloning_frontend/pull/605) [`ff60c18`](https://github.com/manulera/OpenCloning_frontend/commit/ff60c18c1500e8b9046f0810cbd69fe1a65c550c) Thanks [@manulera](https://github.com/manulera)! - Fix how syntaxes are imported to adapt to syntaxes repository change
|
|
8
|
+
|
|
9
|
+
- Updated dependencies []:
|
|
10
|
+
- @opencloning/store@1.3.2
|
|
11
|
+
- @opencloning/utils@1.3.2
|
|
12
|
+
|
|
13
|
+
## 1.3.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#602](https://github.com/manulera/OpenCloning_frontend/pull/602) [`b9b821d`](https://github.com/manulera/OpenCloning_frontend/commit/b9b821d562417b85b69dbf53ddaac324474d4e6b) Thanks [@manulera](https://github.com/manulera)! - Allow users to submit their own syntax from JSON file. Not validated yet so wrong syntaxes will trigger an error.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies []:
|
|
20
|
+
- @opencloning/store@1.3.1
|
|
21
|
+
- @opencloning/utils@1.3.1
|
|
22
|
+
|
|
3
23
|
## 1.3.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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.3.
|
|
29
|
-
"@opencloning/utils": "1.3.
|
|
28
|
+
"@opencloning/store": "1.3.2",
|
|
29
|
+
"@opencloning/utils": "1.3.2",
|
|
30
30
|
"@teselagen/bio-parsers": "^0.4.32",
|
|
31
31
|
"@teselagen/ove": "^0.8.30",
|
|
32
32
|
"@teselagen/range-utils": "^0.3.13",
|
|
@@ -263,7 +263,7 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
|
263
263
|
const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
|
|
264
264
|
const onSyntaxSelect = React.useCallback((syntax, plasmids) => {
|
|
265
265
|
setSyntax(syntax)
|
|
266
|
-
addPlasmids(plasmids
|
|
266
|
+
addPlasmids(plasmids)
|
|
267
267
|
}, [setSyntax, addPlasmids])
|
|
268
268
|
return <>
|
|
269
269
|
<Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
|
|
@@ -191,4 +191,61 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
191
191
|
cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', mockSyntaxData, mockPlasmidsData);
|
|
192
192
|
cy.get('@onCloseSpy').should('have.been.called');
|
|
193
193
|
});
|
|
194
|
+
|
|
195
|
+
it('successfully uploads syntax from JSON file', () => {
|
|
196
|
+
const onCloseSpy = cy.spy().as('onCloseSpy');
|
|
197
|
+
const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
|
|
198
|
+
|
|
199
|
+
const uploadedSyntaxData = {
|
|
200
|
+
name: 'Uploaded Syntax',
|
|
201
|
+
parts: [
|
|
202
|
+
{ id: 1, name: 'Part 1' },
|
|
203
|
+
{ id: 2, name: 'Part 2' },
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Create a temporary JSON file
|
|
208
|
+
cy.writeFile('cypress/temp/syntax.json', uploadedSyntaxData);
|
|
209
|
+
|
|
210
|
+
cy.mount(
|
|
211
|
+
<ExistingSyntaxDialog
|
|
212
|
+
onClose={onCloseSpy}
|
|
213
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
214
|
+
/>,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
cy.wait('@getSyntaxes');
|
|
218
|
+
cy.contains('Upload syntax from JSON file').should('exist');
|
|
219
|
+
|
|
220
|
+
// Upload the JSON file
|
|
221
|
+
cy.get('input[type="file"]').selectFile('cypress/temp/syntax.json', { force: true });
|
|
222
|
+
|
|
223
|
+
cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', uploadedSyntaxData, []);
|
|
224
|
+
cy.get('@onCloseSpy').should('have.been.called');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('displays error message when uploaded JSON file is invalid', () => {
|
|
228
|
+
const onCloseSpy = cy.spy().as('onCloseSpy');
|
|
229
|
+
const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
|
|
230
|
+
|
|
231
|
+
// Create a file with invalid JSON
|
|
232
|
+
cy.writeFile('cypress/temp/invalid.json', '{ invalid json }', { encoding: 'utf8' });
|
|
233
|
+
|
|
234
|
+
cy.mount(
|
|
235
|
+
<ExistingSyntaxDialog
|
|
236
|
+
onClose={onCloseSpy}
|
|
237
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
238
|
+
/>,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
cy.wait('@getSyntaxes');
|
|
242
|
+
cy.contains('Upload syntax from JSON file').should('exist');
|
|
243
|
+
|
|
244
|
+
// Upload invalid JSON
|
|
245
|
+
cy.get('input[type="file"]').selectFile('cypress/temp/invalid.json', { force: true });
|
|
246
|
+
|
|
247
|
+
cy.contains(/Failed to parse JSON file/).should('exist');
|
|
248
|
+
cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
|
|
249
|
+
cy.get('@onCloseSpy').should('not.have.been.called');
|
|
250
|
+
});
|
|
194
251
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert } from '@mui/material'
|
|
2
|
+
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box } from '@mui/material'
|
|
3
3
|
import getHttpClient from '@opencloning/utils/getHttpClient';
|
|
4
4
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
5
5
|
|
|
@@ -12,6 +12,7 @@ function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
|
12
12
|
const [connectAttempt, setConnectAttempt] = React.useState(0);
|
|
13
13
|
const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
|
|
14
14
|
const [loadError, setLoadError] = React.useState(null);
|
|
15
|
+
const fileInputRef = React.useRef(null);
|
|
15
16
|
|
|
16
17
|
React.useEffect(() => {
|
|
17
18
|
setRequestStatus({ status: 'loading' });
|
|
@@ -41,12 +42,34 @@ function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
|
41
42
|
}
|
|
42
43
|
}, [onSyntaxSelect, onClose]);
|
|
43
44
|
|
|
45
|
+
const handleFileUpload = React.useCallback(async (event) => {
|
|
46
|
+
setLoadError(null);
|
|
47
|
+
const file = event.target.files[0];
|
|
48
|
+
if (!file) return;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const text = await file.text();
|
|
52
|
+
const syntaxData = JSON.parse(text);
|
|
53
|
+
|
|
54
|
+
// Uploaded JSON files contain only syntax data, no plasmids
|
|
55
|
+
onSyntaxSelect(syntaxData, []);
|
|
56
|
+
onClose();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
setLoadError(`Failed to parse JSON file: ${error.message}`);
|
|
59
|
+
} finally {
|
|
60
|
+
// Reset file input so the same file can be selected again
|
|
61
|
+
if (fileInputRef.current) {
|
|
62
|
+
fileInputRef.current.value = '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [onSyntaxSelect, onClose]);
|
|
66
|
+
|
|
44
67
|
return (
|
|
45
68
|
<Dialog open onClose={onClose}>
|
|
46
69
|
<DialogTitle>Load an existing syntax</DialogTitle>
|
|
47
70
|
<DialogContent>
|
|
71
|
+
{loadError && <Alert severity="error" sx={{ mb: 2 }}>{loadError}</Alert>}
|
|
48
72
|
<RequestStatusWrapper requestStatus={requestStatus} retry={() => setConnectAttempt((prev) => prev + 1)}>
|
|
49
|
-
{loadError && <Alert severity="error" sx={{ mb: 2 }}>{loadError}</Alert>}
|
|
50
73
|
<List>
|
|
51
74
|
{syntaxes.map((syntax) => (
|
|
52
75
|
<ListItem key={syntax.path}>
|
|
@@ -57,6 +80,22 @@ function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
|
57
80
|
))}
|
|
58
81
|
</List>
|
|
59
82
|
</RequestStatusWrapper>
|
|
83
|
+
<Box sx={{ mb: 2 }}>
|
|
84
|
+
<input
|
|
85
|
+
type="file"
|
|
86
|
+
ref={fileInputRef}
|
|
87
|
+
accept=".json"
|
|
88
|
+
onChange={handleFileUpload}
|
|
89
|
+
style={{ display: 'none' }}
|
|
90
|
+
/>
|
|
91
|
+
<Button
|
|
92
|
+
variant="outlined"
|
|
93
|
+
sx={{ display: 'block', mx: 'auto' }}
|
|
94
|
+
onClick={() => fileInputRef.current?.click()}
|
|
95
|
+
>
|
|
96
|
+
Upload syntax from JSON file
|
|
97
|
+
</Button>
|
|
98
|
+
</Box>
|
|
60
99
|
</DialogContent>
|
|
61
100
|
</Dialog>
|
|
62
101
|
)
|
|
@@ -101,6 +101,11 @@ export function getSimplifiedDigestFragments(sequenceData, enzymes) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
|
|
104
|
+
// Something that is important to understand here is the meaning of forward and reverse.
|
|
105
|
+
// It does not mean whether the overhang is 5' or 3', the value on the top strand is always
|
|
106
|
+
// used, which is convenient for classification within the syntax.
|
|
107
|
+
// Instead, forward means whether the recognition site was forward or reverse when producing that cut.
|
|
108
|
+
// see the test called "shows the meaning of forward and reverse" for more details.
|
|
104
109
|
const simplifiedDigestFragments = getSimplifiedDigestFragments(sequenceData, enzymes);
|
|
105
110
|
const foundParts = [];
|
|
106
111
|
simplifiedDigestFragments
|
|
@@ -28,6 +28,46 @@ describe('reverseComplementSimplifiedDigestFragment', () => {
|
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
it('shows the meaning of forward and reverse', () => {
|
|
32
|
+
const sequence = 'aaGGTCTCaTACTaaa'
|
|
33
|
+
const digestFragments = getDigestFragmentsForRestrictionEnzymes(
|
|
34
|
+
sequence,
|
|
35
|
+
false,
|
|
36
|
+
aliasedEnzymesByName["bsai"],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// This does not denote whether the overhang is 5' or 3',
|
|
40
|
+
// but the orientation of the recognition site.
|
|
41
|
+
expect(digestFragments[0].cut2.overhangBps).toBe('TACT');
|
|
42
|
+
expect(digestFragments[0].cut2.forward).toBe(true);
|
|
43
|
+
expect(digestFragments[1].cut1.overhangBps).toBe('TACT');
|
|
44
|
+
expect(digestFragments[1].cut1.forward).toBe(true);
|
|
45
|
+
|
|
46
|
+
// See how for a fragment with the same overhangs, the forward
|
|
47
|
+
// value is different
|
|
48
|
+
const sequence2 = 'aTACTcGAGACCaaa'
|
|
49
|
+
const digestFragments2 = getDigestFragmentsForRestrictionEnzymes(
|
|
50
|
+
sequence2,
|
|
51
|
+
false,
|
|
52
|
+
aliasedEnzymesByName["bsai"],
|
|
53
|
+
);
|
|
54
|
+
expect(digestFragments2[0].cut2.overhangBps).toBe('TACT');
|
|
55
|
+
expect(digestFragments2[0].cut2.forward).toBe(false);
|
|
56
|
+
expect(digestFragments2[1].cut1.overhangBps).toBe('TACT');
|
|
57
|
+
expect(digestFragments2[1].cut1.forward).toBe(false);
|
|
58
|
+
|
|
59
|
+
// For EcoRI, it's always forward
|
|
60
|
+
const sequenceEcoRI = 'aaaGAATTCaaaGAATTCaaaa'
|
|
61
|
+
const digestFragmentsEcoRI = getDigestFragmentsForRestrictionEnzymes(
|
|
62
|
+
sequenceEcoRI,
|
|
63
|
+
true,
|
|
64
|
+
aliasedEnzymesByName["ecori"],
|
|
65
|
+
);
|
|
66
|
+
expect(digestFragmentsEcoRI[0].cut2.overhangBps).toBe('AATT');
|
|
67
|
+
expect(digestFragmentsEcoRI[0].cut2.forward).toBe(true);
|
|
68
|
+
expect(digestFragmentsEcoRI[0].cut1.overhangBps).toBe('AATT');
|
|
69
|
+
expect(digestFragmentsEcoRI[0].cut1.forward).toBe(true);
|
|
70
|
+
});
|
|
31
71
|
|
|
32
72
|
describe('assignSequenceToSyntaxPart', () => {
|
|
33
73
|
it('works', () => {
|
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.3.
|
|
2
|
+
export const version = "1.3.2";
|