@opencloning/ui 1.3.1 → 1.3.3

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.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#607](https://github.com/manulera/OpenCloning_frontend/pull/607) [`1e4bfbf`](https://github.com/manulera/OpenCloning_frontend/commit/1e4bfbfb805e841d9a91cf650ebf632ac62b1248) Thanks [@manulera](https://github.com/manulera)! - Syntax validation and fix longestFeature when it's empty
8
+
9
+ - Updated dependencies []:
10
+ - @opencloning/store@1.3.3
11
+ - @opencloning/utils@1.3.3
12
+
13
+ ## 1.3.2
14
+
15
+ ### Patch Changes
16
+
17
+ - [#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
18
+
19
+ - Updated dependencies []:
20
+ - @opencloning/store@1.3.2
21
+ - @opencloning/utils@1.3.2
22
+
3
23
  ## 1.3.1
4
24
 
5
25
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
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.1",
29
- "@opencloning/utils": "1.3.1",
28
+ "@opencloning/store": "1.3.3",
29
+ "@opencloning/utils": "1.3.3",
30
30
  "@teselagen/bio-parsers": "^0.4.32",
31
31
  "@teselagen/ove": "^0.8.30",
32
32
  "@teselagen/range-utils": "^0.3.13",
@@ -253,7 +253,10 @@ describe('<UploadPlasmidsButton />', () => {
253
253
  'cypress/test_files/syntax/pYTK002.gb',
254
254
  'cypress/test_files/syntax/moclo_ytk_multi_part.gb',
255
255
  'cypress/test_files/syntax/pYTK095.gb',
256
- 'cypress/test_files/sequencing/locus.gb'],
256
+ 'cypress/test_files/sequencing/locus.gb',
257
+ // This one just to verify that it works with no features
258
+ 'cypress/test_files/syntax/pYTK002_no_features.gb'
259
+ ],
257
260
  { force: true });
258
261
 
259
262
  // Wait for the dialog to appear (indicating plasmids were processed)
@@ -287,6 +290,9 @@ describe('<UploadPlasmidsButton />', () => {
287
290
  cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(3).should('contain', 'Spans multiple parts')
288
291
  cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(4).should('contain', 'AmpR')
289
292
 
293
+ // No features
294
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(3).find('td').eq(4).should('contain', '-');
295
+
290
296
  // Click the import button
291
297
  cy.contains('button', 'Import valid plasmids').click();
292
298
 
@@ -298,7 +304,7 @@ describe('<UploadPlasmidsButton />', () => {
298
304
  const firstCall = spy.getCall(0);
299
305
  console.log('firstCall', firstCall.args);
300
306
  cy.wrap(firstCall.args[0]).should('be.an', 'array');
301
- cy.wrap(firstCall.args[0]).should('have.length', 2);
307
+ cy.wrap(firstCall.args[0]).should('have.length', 3);
302
308
 
303
309
  const firstPlasmid = firstCall.args[0][0];
304
310
 
@@ -16,6 +16,9 @@ import PlasmidSyntaxTable from './PlasmidSyntaxTable';
16
16
  import ExistingSyntaxDialog from './ExistingSyntaxDialog';
17
17
  import error2String from '@opencloning/utils/error2String';
18
18
  import { categoryFilter } from './assembler_utils';
19
+ import useBackendRoute from '../../hooks/useBackendRoute';
20
+ import useHttpClient from '../../hooks/useHttpClient';
21
+ import useAlerts from '../../hooks/useAlerts';
19
22
 
20
23
  const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
21
24
 
@@ -261,10 +264,22 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
261
264
 
262
265
  function LoadSyntaxButton({ setSyntax, addPlasmids }) {
263
266
  const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
264
- const onSyntaxSelect = React.useCallback((syntax, plasmids) => {
265
- setSyntax(syntax)
266
- addPlasmids(plasmids.filter((plasmid) => plasmid.appData.correspondingParts.length === 1).map(formatPlasmid))
267
- }, [setSyntax, addPlasmids])
267
+ const httpClient = useHttpClient();
268
+ const backendRoute = useBackendRoute();
269
+ const { addAlert } = useAlerts();
270
+ const onSyntaxSelect = React.useCallback(async (syntax, plasmids) => {
271
+ const url = backendRoute('validate_syntax');
272
+ try {
273
+ await httpClient.post(url, syntax);
274
+ setSyntax(syntax)
275
+ addPlasmids(plasmids)
276
+ } catch (error) {
277
+ addAlert({
278
+ message: error2String(error),
279
+ severity: 'error',
280
+ });
281
+ }
282
+ }, [setSyntax, addPlasmids, httpClient, backendRoute, addAlert])
268
283
  return <>
269
284
  <Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
270
285
  {existingSyntaxDialogOpen && <ExistingSyntaxDialog onClose={() => setExistingSyntaxDialogOpen(false)} onSyntaxSelect={onSyntaxSelect}/>}
@@ -204,8 +204,12 @@ describe('<ExistingSyntaxDialog />', () => {
204
204
  ],
205
205
  };
206
206
 
207
- // Create a temporary JSON file
208
- cy.writeFile('cypress/temp/syntax.json', uploadedSyntaxData);
207
+ const tempFile = {
208
+ contents: Cypress.Buffer.from(JSON.stringify(uploadedSyntaxData)),
209
+ fileName: 'syntax.json',
210
+ mimeType: 'text/plain',
211
+ lastModified: Date.now(),
212
+ }
209
213
 
210
214
  cy.mount(
211
215
  <ExistingSyntaxDialog
@@ -218,7 +222,7 @@ describe('<ExistingSyntaxDialog />', () => {
218
222
  cy.contains('Upload syntax from JSON file').should('exist');
219
223
 
220
224
  // Upload the JSON file
221
- cy.get('input[type="file"]').selectFile('cypress/temp/syntax.json', { force: true });
225
+ cy.get('input[type="file"]').selectFile(tempFile, { force: true });
222
226
 
223
227
  cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', uploadedSyntaxData, []);
224
228
  cy.get('@onCloseSpy').should('have.been.called');
@@ -228,8 +232,12 @@ describe('<ExistingSyntaxDialog />', () => {
228
232
  const onCloseSpy = cy.spy().as('onCloseSpy');
229
233
  const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
230
234
 
231
- // Create a file with invalid JSON
232
- cy.writeFile('cypress/temp/invalid.json', '{ invalid json }', { encoding: 'utf8' });
235
+ const invalidFile = {
236
+ contents: Cypress.Buffer.from('{ invalid json }'),
237
+ fileName: 'invalid.json',
238
+ mimeType: 'text/plain',
239
+ lastModified: Date.now(),
240
+ }
233
241
 
234
242
  cy.mount(
235
243
  <ExistingSyntaxDialog
@@ -242,7 +250,7 @@ describe('<ExistingSyntaxDialog />', () => {
242
250
  cy.contains('Upload syntax from JSON file').should('exist');
243
251
 
244
252
  // Upload invalid JSON
245
- cy.get('input[type="file"]').selectFile('cypress/temp/invalid.json', { force: true });
253
+ cy.get('input[type="file"]').selectFile(invalidFile, { force: true });
246
254
 
247
255
  cy.contains(/Failed to parse JSON file/).should('exist');
248
256
  cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
@@ -22,7 +22,7 @@ function PlasmidRow({ plasmid }) {
22
22
  backgroundColor: partInfo[0]?.color,
23
23
  }
24
24
  infoStr = partInfo[0] ? partInfo[0].name : 'Spans multiple parts';
25
- longestFeatureStr = longestFeature ? longestFeature[0].name : '-';
25
+ longestFeatureStr = longestFeature[0] ? longestFeature[0].name : '-';
26
26
  }
27
27
  const multipleParts = partInfo.length > 1;
28
28
  if (multipleParts) {
@@ -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.1";
2
+ export const version = "1.3.3";