@opencloning/ui 1.4.10 → 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 CHANGED
@@ -1,5 +1,27 @@
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
+
15
+ ## 1.4.11
16
+
17
+ ### Patch Changes
18
+
19
+ - [#644](https://github.com/manulera/OpenCloning_frontend/pull/644) [`27744a3`](https://github.com/manulera/OpenCloning_frontend/commit/27744a3e5cebd6d41cb11a76f3d4d367f5a4edc8) Thanks [@manulera](https://github.com/manulera)! - Fix graphToMSA
20
+
21
+ - Updated dependencies []:
22
+ - @opencloning/store@1.4.11
23
+ - @opencloning/utils@1.4.11
24
+
3
25
  ## 1.4.10
4
26
 
5
27
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.4.10",
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.4.10",
29
- "@opencloning/utils": "1.4.10",
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
- assemblyEnzyme="assembly_enzyme"
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, assemblyEnzyme, addAlert, appInfo }) {
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, assemblyEnzyme)
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
- }, [assemblyEnzyme, assembly, plasmids, requestSources, requestAssemblies, clearAssemblySelection])
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} assemblyEnzyme={syntax.assemblyEnzyme} addAlert={addAlert} appInfo={appInfo} />}
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
- simplifiedDigestFragments
123
- .filter(f => f.left.forward && !f.right.forward && graph.hasNode(f.left.ovhg) && graph.hasNode(f.right.ovhg))
124
- .forEach(fragment => {
125
- const graphForPaths = isFragmentPalindromic(fragment) ? openCycleAtNode(graph, graph.nodes()[0]) : graph;
126
- const paths = allSimplePaths(graphForPaths, fragment.left.ovhg, fragment.right.ovhg);
127
- if (paths.length > 0) {
128
- foundParts.push({left_overhang: fragment.left.ovhg, right_overhang: fragment.right.ovhg, longestFeature: fragment.longestFeature});
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', () => {
@@ -144,7 +144,12 @@ function minimumCoveringRows(msa) {
144
144
 
145
145
  export function graphToMSA(graph) {
146
146
  if (graph.nodes().length === 0) return [];
147
- const newGraph = openCycleAtNode(graph, graph.nodes()[0]);
147
+ const firstNodeLeftOverhang = graph.nodes()[0].split('-')[0];
148
+ const nodesToRemove = graph.nodes().filter(node => node.split('-')[0] === firstNodeLeftOverhang);
149
+ let newGraph = graph;
150
+ for (const node of nodesToRemove) {
151
+ newGraph = openCycleAtNode(newGraph, node);
152
+ }
148
153
  return minimumCoveringRows(dagToMSA(newGraph));
149
154
  }
150
155
 
@@ -62,7 +62,7 @@ export const useAssembler = () => {
62
62
  }, [ httpClient, backendRoute ])
63
63
 
64
64
 
65
- const requestAssemblies = useCallback(async (requestedSources, enzyme='BsaI') => {
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: [enzyme],
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.assemblyEnzyme - Assembly enzyme name
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, assemblyEnzyme, overhangNames }) {
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 = [aliasedEnzymesByName[assemblyEnzyme.toLowerCase()]];
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, assemblyEnzyme, overhangNames]);
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 (assemblyEnzyme && Array.isArray(plasmids) && plasmids.length > 0) {
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
- }, [assemblyEnzyme, assignPlasmids]);
56
+ }, [assemblyEnzymes, assignPlasmids]);
57
57
 
58
58
  // Update existing plasmids when enzyme or assignment logic changes
59
59
  React.useEffect(() => {
60
- if (assemblyEnzyme) {
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, assemblyEnzyme]); // Note: intentionally not including linkedPlasmids to avoid infinite loop
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.4.10";
2
+ export const version = "1.5.0";