@opencloning/ui 1.4.11 → 1.5.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +3 -3
  3. package/src/components/assembler/Assembler.cy.jsx +1 -1
  4. package/src/components/assembler/Assembler.jsx +4 -4
  5. package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +2 -9
  6. package/src/components/assembler/ExistingSyntaxDialog.jsx +4 -5
  7. package/src/components/assembler/UploadPlasmidsButton.cy.jsx +32 -2
  8. package/src/components/assembler/assembler_utils.js +21 -10
  9. package/src/components/assembler/assembler_utils.test.js +29 -0
  10. package/src/components/assembler/useAssembler.js +2 -2
  11. package/src/components/assembler/usePlasmidsLogic.js +8 -8
  12. package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +4 -30
  13. package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +2 -1
  14. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +55 -313
  15. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +2 -2
  16. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +14 -5
  17. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +12 -17
  18. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +8 -4
  19. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +1 -59
  20. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +2 -3
  21. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +18 -8
  22. package/src/components/primers/primer_design/SequenceTabComponents/{TabPannelSettings.jsx → TabPanelSettings.jsx} +4 -22
  23. package/src/components/primers/primer_design/SequenceTabComponents/designTypeStrategies.js +178 -0
  24. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +89 -0
  25. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useRegionSelection.js +37 -0
  26. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSequenceProduct.js +35 -0
  27. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useSpacers.js +13 -0
  28. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useTabNavigation.js +40 -0
  29. package/src/components/primers/primer_design/SequenceTabComponents/utils/getSequenceLabel.js +7 -0
  30. package/src/components/primers/primer_design/SequenceTabComponents/utils/knownGatewayCombinations.js +27 -0
  31. package/src/components/primers/primer_design/SequenceTabComponents/utils/trimPadding.js +58 -0
  32. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +11 -17
  33. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.cy.jsx +131 -0
  34. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +143 -24
  35. package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +11 -15
  36. package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +10 -16
  37. package/src/components/primers/primer_design/SourceComponents/useNavigateAfterPrimerDesign.js +23 -0
  38. package/src/version.js +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#651](https://github.com/manulera/OpenCloning_frontend/pull/651) [`b5c89e2`](https://github.com/manulera/OpenCloning_frontend/commit/b5c89e23aa1065319cb07a9ac48a26693dd0a21a) Thanks [@manulera](https://github.com/manulera)! - Big refactor of Primer designer for better maintainability
8
+
9
+ - [#651](https://github.com/manulera/OpenCloning_frontend/pull/651) [`b5c89e2`](https://github.com/manulera/OpenCloning_frontend/commit/b5c89e23aa1065319cb07a9ac48a26693dd0a21a) Thanks [@manulera](https://github.com/manulera)! - Allow primer design of Gibson and similar assembly methods to include fragments that are not amplified. Similar to what the NEBuilder planner does, where not all products are amplified. Perhaps in the future it would be useful to also allow the option to restore the restriction sites on the edge.
10
+
11
+ - Updated dependencies [[`b5c89e2`](https://github.com/manulera/OpenCloning_frontend/commit/b5c89e23aa1065319cb07a9ac48a26693dd0a21a)]:
12
+ - @opencloning/store@1.5.1
13
+ - @opencloning/utils@1.5.1
14
+
15
+ ## 1.5.0
16
+
17
+ ### Minor Changes
18
+
19
+ - [#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
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [[`800ebcd`](https://github.com/manulera/OpenCloning_frontend/commit/800ebcd4ed610ad89d538e1d59314977aae40583)]:
24
+ - @opencloning/utils@1.5.0
25
+ - @opencloning/store@1.5.0
26
+
3
27
  ## 1.4.11
4
28
 
5
29
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.4.11",
3
+ "version": "1.5.1",
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.11",
29
- "@opencloning/utils": "1.4.11",
28
+ "@opencloning/store": "1.5.1",
29
+ "@opencloning/utils": "1.5.1",
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', () => {
@@ -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) => {
@@ -1,6 +1,5 @@
1
1
  import { Alert, Box, FormControl, InputLabel, MenuItem, Select } from '@mui/material';
2
2
  import React from 'react';
3
- import { getReverseComplementSequenceString as reverseComplement } from '@teselagen/sequence-utils';
4
3
  import { isEqual } from 'lodash-es';
5
4
  import { parseFeatureLocation } from '@teselagen/bio-parsers';
6
5
  import { useDispatch, useSelector } from 'react-redux';
@@ -9,30 +8,9 @@ import useStoreEditor from '../../../../hooks/useStoreEditor';
9
8
  import { cloningActions } from '@opencloning/store/cloning';
10
9
  import { usePrimerDesign } from './PrimerDesignContext';
11
10
  import RequestStatusWrapper from '../../../form/RequestStatusWrapper';
11
+ import knownGatewayCombinations from './utils/knownGatewayCombinations';
12
12
 
13
- const knownCombinations = [
14
- {
15
- siteNames: ['attP4', 'attP1'],
16
- spacers: ['GGGGACAACTTTGTATAGAAAAGTTGNN', reverseComplement('GGGGACTGCTTTTTTGTACAAACTTGN')],
17
- orientation: [true, true],
18
- message: 'Primers tails designed based on pDONR™ P4-P1R',
19
- translationFrame: [4, 6],
20
- },
21
- {
22
- siteNames: ['attP1', 'attP2'],
23
- spacers: ['GGGGACAAGTTTGTACAAAAAAGCAGGCTNN', reverseComplement('GGGGACCACTTTGTACAAGAAAGCTGGGTN')],
24
- orientation: [true, false],
25
- message: 'Primers tails designed based on pDONR™ 221',
26
- translationFrame: [4, 6],
27
- },
28
- {
29
- siteNames: ['attP2', 'attP3'],
30
- spacers: ['GGGGACAGCTTTCTTGTACAAAGTGGNN', reverseComplement('GGGGACAACTTTGTATAATAAAGTTGN')],
31
- orientation: [false, false],
32
- message: 'Primers tails designed based on pDONR™ P2R-P3',
33
- translationFrame: [4, 6],
34
- },
35
- ];
13
+ const { setMainSequenceSelection } = cloningActions;
36
14
 
37
15
  function SiteSelect({ donorSites, site, setSite, label }) {
38
16
  return (
@@ -57,8 +35,6 @@ function SiteSelect({ donorSites, site, setSite, label }) {
57
35
  );
58
36
  }
59
37
 
60
- const { setMainSequenceSelection } = cloningActions;
61
-
62
38
  function GatewayRoiSelect({ id, greedy = false }) {
63
39
  const [leftSite, setLeftSite] = React.useState(null);
64
40
  const [rightSite, setRightSite] = React.useState(null);
@@ -104,14 +80,14 @@ function GatewayRoiSelect({ id, greedy = false }) {
104
80
  const selection = { selectionLayer, caretPosition: -1 };
105
81
  const siteNames = [newLeftSite.siteName, newRightSite.siteName];
106
82
  const orientation = [newLeftSite.location.includes('(+)'), newRightSite.location.includes('(+)')];
107
- const knownCombinationForward = knownCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNames) && isEqual(knownOrientation, orientation));
83
+ const knownCombinationForward = knownGatewayCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNames) && isEqual(knownOrientation, orientation));
108
84
  if (knownCombinationForward) {
109
85
  handleKnownCombinationChange(knownCombinationForward, selection);
110
86
  return;
111
87
  }
112
88
  const siteNamesReverse = [newRightSite.siteName, newLeftSite.siteName];
113
89
  const orientationReverse = [!newRightSite.location.includes('(+)'), !newLeftSite.location.includes('(+)')];
114
- const knownCombinationReverse = knownCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNamesReverse) && isEqual(knownOrientation, orientationReverse));
90
+ const knownCombinationReverse = knownGatewayCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNamesReverse) && isEqual(knownOrientation, orientationReverse));
115
91
  if (knownCombinationReverse) {
116
92
  handleKnownCombinationChange(knownCombinationReverse, selection);
117
93
  return;
@@ -123,7 +99,6 @@ function GatewayRoiSelect({ id, greedy = false }) {
123
99
  const onSiteSelectLeft = React.useCallback((site) => {
124
100
  setLeftSite(site);
125
101
  if (rightSite === null || isEqual(rightSite, site)) {
126
- // Find the first different one
127
102
  const differentSite = donorSites.find(({ location }) => location !== site.location);
128
103
  setRightSite(differentSite);
129
104
  checkKnownCombination(site, differentSite);
@@ -135,7 +110,6 @@ function GatewayRoiSelect({ id, greedy = false }) {
135
110
  const onSiteSelectRight = React.useCallback((site) => {
136
111
  setRightSite(site);
137
112
  if (leftSite === null || isEqual(leftSite, site)) {
138
- // Find the first different one
139
113
  const differentSite = donorSites.find(({ location }) => location !== site.location);
140
114
  setLeftSite(differentSite);
141
115
  checkKnownCombination(differentSite, site);
@@ -1,11 +1,12 @@
1
1
  import { FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';
2
2
  import React from 'react';
3
3
  import { usePrimerDesign } from './PrimerDesignContext';
4
+ import { getSequenceLabel } from './utils/getSequenceLabel';
4
5
 
5
6
  function OrientationPicker({ id, index }) {
6
7
  const { designType, fragmentOrientations, handleFragmentOrientationChange, templateSequenceNames } = usePrimerDesign();
7
8
  const sequenceName = templateSequenceNames[index];
8
- let label = sequenceName && sequenceName !== 'name' ? `Seq. ${id} (${sequenceName})` : `Seq. ${id}`;
9
+ let label = getSequenceLabel(id, sequenceName);
9
10
  if (designType === 'homologous_recombination') {
10
11
  label = 'Orientation of insert';
11
12
  }