@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 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.0",
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.0",
29
- "@opencloning/utils": "1.3.0",
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.filter((plasmid) => plasmid.appData.correspondingParts.length === 1).map(formatPlasmid))
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.0";
2
+ export const version = "1.3.2";