@opencloning/ui 1.1.2 → 1.3.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +5 -4
  3. package/src/components/MainSequenceEditor.jsx +2 -0
  4. package/src/components/assembler/Assembler.cy.jsx +364 -0
  5. package/src/components/assembler/Assembler.jsx +298 -206
  6. package/src/components/assembler/AssemblerPart.cy.jsx +52 -0
  7. package/src/components/assembler/AssemblerPart.jsx +51 -79
  8. package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +194 -0
  9. package/src/components/assembler/ExistingSyntaxDialog.jsx +65 -0
  10. package/src/components/assembler/PlasmidSyntaxTable.jsx +83 -0
  11. package/src/components/assembler/assembler_utils.js +134 -0
  12. package/src/components/assembler/assembler_utils.test.js +193 -0
  13. package/src/components/assembler/assembly_component.module.css +1 -1
  14. package/src/components/assembler/graph_utils.js +153 -0
  15. package/src/components/assembler/graph_utils.test.js +239 -0
  16. package/src/components/assembler/index.js +9 -0
  17. package/src/components/assembler/useAssembler.js +59 -22
  18. package/src/components/assembler/useCombinatorialAssembly.js +76 -0
  19. package/src/components/assembler/usePlasmidsLogic.js +82 -0
  20. package/src/components/eLabFTW/utils.js +0 -9
  21. package/src/components/index.js +2 -0
  22. package/src/components/navigation/SelectTemplateDialog.jsx +0 -1
  23. package/src/components/primers/DownloadPrimersButton.jsx +0 -1
  24. package/src/components/primers/PrimerList.jsx +4 -3
  25. package/src/components/primers/import_primers/ImportPrimersButton.jsx +0 -1
  26. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +110 -91
  27. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGatewayBP.jsx +1 -1
  28. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +2 -2
  29. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignHomologousRecombination.jsx +1 -1
  30. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignRestriction.jsx +1 -1
  31. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +1 -1
  32. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +5 -22
  33. package/src/components/primers/primer_design/SequenceTabComponents/TabPannelSettings.jsx +6 -4
  34. package/src/hooks/useStoreEditor.js +4 -0
  35. package/src/version.js +1 -1
  36. package/vitest.config.js +2 -4
  37. package/src/components/DraggableDialogPaper.jsx +0 -16
  38. package/src/components/assembler/AssemblePartWidget.jsx +0 -252
  39. package/src/components/assembler/StopIcon.jsx +0 -34
  40. package/src/components/assembler/assembler_data2.json +0 -50
  41. package/src/components/assembler/moclo.json +0 -110
@@ -2,9 +2,31 @@ import { useCallback } from 'react'
2
2
  import { classNameToEndPointMap } from '@opencloning/utils/sourceFunctions'
3
3
  import useBackendRoute from '../../hooks/useBackendRoute'
4
4
  import useHttpClient from '../../hooks/useHttpClient'
5
- import { arrayCombinations } from '../eLabFTW/utils'
5
+ import { arrayCombinations } from './assembler_utils'
6
6
 
7
7
 
8
+ function formatLoadedFile(plasmid, id) {
9
+ return {
10
+ source: {
11
+ id,
12
+ type: 'UploadedFileSource',
13
+ input: [],
14
+ sequence_file_format: "genbank",
15
+ file_name: plasmid.file_name,
16
+ index_in_file: 0,
17
+ },
18
+ sequence: {
19
+ id,
20
+ type: 'TextFileSequence',
21
+ sequence_file_format: "genbank",
22
+ overhang_crick_3prime: 0,
23
+ overhang_watson_3prime: 0,
24
+ file_content: plasmid.genbankString,
25
+ },
26
+ plasmid: plasmid,
27
+ }
28
+ }
29
+
8
30
  export const useAssembler = () => {
9
31
  const httpClient = useHttpClient();
10
32
  const backendRoute = useBackendRoute();
@@ -12,25 +34,35 @@ export const useAssembler = () => {
12
34
  const requestSources = useCallback(async (assemblerOutput) => {
13
35
  const processedOutput = []
14
36
  for (let pos = 0; pos < assemblerOutput.length; pos++) {
15
- processedOutput.push([])
16
- for (let source of assemblerOutput[pos]) {
37
+ processedOutput.push([])
38
+ for (let plasmid of assemblerOutput[pos]) {
39
+ try {
40
+ if (plasmid.type === 'loadedFile') {
41
+ processedOutput[pos].push(formatLoadedFile(plasmid, pos + 1))
42
+ } else {
43
+ const { source } = plasmid;
17
44
  const url = backendRoute(classNameToEndPointMap[source.type])
18
45
  const {data} = await httpClient.post(url, source)
19
46
  if (data.sources.length !== 1) {
20
- console.error('Expected 1 source, got ' + data.sources.length)
47
+ console.error('Expected 1 source, got ' + data.sources.length)
21
48
  }
22
49
  const thisData = {
23
- source: {...data.sources[0], id: pos + 1},
24
- sequence: {...data.sequences[0], id: pos + 1},
50
+ source: {...data.sources[0], id: pos + 1},
51
+ sequence: {...data.sequences[0], id: pos + 1},
52
+ plasmid: plasmid,
25
53
  }
26
54
  processedOutput[pos].push(thisData)
55
+ }} catch (e) {
56
+ e.plasmid = plasmid;
57
+ throw e;
27
58
  }
59
+ }
28
60
  }
29
61
  return processedOutput
30
- }, [])
62
+ }, [ httpClient, backendRoute ])
31
63
 
32
64
 
33
- const requestAssemblies = useCallback(async (requestedSources) => {
65
+ const requestAssemblies = useCallback(async (requestedSources, enzyme='BsaI') => {
34
66
 
35
67
  const assemblies = arrayCombinations(requestedSources);
36
68
  const output = []
@@ -43,28 +75,33 @@ export const useAssembler = () => {
43
75
  }
44
76
  const requestData = {
45
77
  source: {
46
- type: 'RestrictionAndLigationSource',
47
- restriction_enzymes: ['BsaI'],
48
- id: assembly.length + 1,
78
+ type: 'RestrictionAndLigationSource',
79
+ restriction_enzymes: [enzyme],
80
+ id: assembly.length + 1,
49
81
  },
50
82
  sequences: assembly.map((p) => p.sequence),
51
83
  }
52
- const {data} = await httpClient.post(url, requestData, config)
53
- const thisSource = data.sources[0]
54
- const thisSequence = data.sequences[0]
55
- thisSource.id = assembly.length + 1
56
- thisSequence.id = assembly.length + 1
84
+ try {
85
+ const {data} = await httpClient.post(url, requestData, config)
86
+ const thisSource = data.sources[0]
87
+ const thisSequence = data.sequences[0]
88
+ thisSource.id = assembly.length + 1
89
+ thisSequence.id = assembly.length + 1
57
90
 
58
91
 
59
- const cloningStrategy = {
60
- sources: [thisSource, ...assembly.map((p) => p.source)],
61
- sequences: [thisSequence, ...assembly.map((p) => p.sequence)],
62
- primers: [],
92
+ const cloningStrategy = {
93
+ sources: [thisSource, ...assembly.map((p) => p.source)],
94
+ sequences: [thisSequence, ...assembly.map((p) => p.sequence)],
95
+ primers: [],
96
+ }
97
+ output.push(cloningStrategy)
98
+ } catch (e) {
99
+ e.assembly = assembly;
100
+ throw e;
63
101
  }
64
- output.push(cloningStrategy)
65
102
  }
66
103
  return output;
67
- }, [])
104
+ }, [ httpClient, backendRoute ])
68
105
 
69
106
  return { requestSources, requestAssemblies }
70
107
  }
@@ -0,0 +1,76 @@
1
+ import React from 'react'
2
+ import { arrayCombinations, categoryFilter } from './assembler_utils'
3
+
4
+ function isAssemblyComplete(assembly, categories) {
5
+ const lastPosition = assembly.length - 1
6
+ if (lastPosition === -1) {
7
+ return false
8
+ }
9
+ const lastCategory = categories.find((category) => category.id === assembly[lastPosition].category)
10
+ return lastCategory?.right_overhang === categories[0].left_overhang
11
+ }
12
+
13
+
14
+ export default function useCombinatorialAssembly( { onValueChange, categories, plasmids }) {
15
+
16
+ const [assembly, setAssembly] = React.useState([])
17
+
18
+ const setCategory = React.useCallback((category, index) => {
19
+ onValueChange()
20
+ if (category === '') {
21
+ setAssembly(prev => prev.slice(0, index))
22
+ } else {
23
+ setAssembly(prev => [...prev.slice(0, index), { category, plasmidIds: [] }])
24
+ }
25
+ }, [onValueChange])
26
+
27
+ const setId = React.useCallback((plasmidIds, index) => {
28
+ onValueChange()
29
+ // Handle case where user clears all selections (empty array)
30
+ const value = (!plasmidIds || plasmidIds.length === 0) ? [] : plasmidIds
31
+ setAssembly( prev => prev.map((item, i) => i === index ? { ...item, plasmidIds: value } : item))
32
+ }, [onValueChange])
33
+
34
+ // If the next category of the assembly can only be one, add it to the assembly
35
+ React.useEffect(() => {
36
+ const newAssembly = [...assembly]
37
+ while (true) {
38
+ if (isAssemblyComplete(newAssembly, categories)) {
39
+ break
40
+ }
41
+ let lastPosition = newAssembly.length - 1
42
+ const previousCategoryId = lastPosition === -1 ? null : newAssembly[lastPosition].category
43
+ let nextCategories = categories.filter((category) => categoryFilter(category, categories, previousCategoryId))
44
+ if (nextCategories.length !== 1) {
45
+ break
46
+ } else if (nextCategories.length === 1) {
47
+ newAssembly.push({ category: nextCategories[0].id, plasmidIds: [] })
48
+ }
49
+ }
50
+ if (newAssembly.length !== assembly.length) {
51
+ setAssembly(newAssembly)
52
+ }
53
+ }, [assembly, categories])
54
+
55
+ // If plasmids are removed, remove them from the assembly
56
+ React.useEffect(() => {
57
+ onValueChange()
58
+ setAssembly(prev => {
59
+ const existingPlasmidIds = plasmids.map((plasmid) => plasmid.id)
60
+ return prev.map((item) => ({ ...item, plasmidIds: item.plasmidIds.filter((id) => existingPlasmidIds.includes(id)) }))
61
+ }
62
+ )
63
+ }, [plasmids, onValueChange])
64
+
65
+ // If categories are changed, clear the assembly
66
+ React.useEffect(() => {
67
+ setAssembly([])
68
+ }, [categories])
69
+
70
+ const expandedAssemblies = React.useMemo(() => arrayCombinations(assembly.map(({ plasmidIds }) => plasmidIds)), [assembly])
71
+ const assemblyComplete = isAssemblyComplete(assembly, categories);
72
+ const canBeSubmitted = assemblyComplete && assembly.every((item) => item.plasmidIds.length > 0)
73
+ const currentCategories = React.useMemo(() => assembly.map((item) => item.category), [assembly])
74
+
75
+ return React.useMemo(() => ({ assembly, setCategory, setId, expandedAssemblies, assemblyComplete, canBeSubmitted, currentCategories }), [assembly, setCategory, setId, expandedAssemblies, assemblyComplete, canBeSubmitted, currentCategories])
76
+ }
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { anyToJson } from '@teselagen/bio-parsers';
3
+ import { partsToEdgesGraph } from '@opencloning/ui/components/assembler';
4
+ import { assignSequenceToSyntaxPart } from './assembler_utils';
5
+ import { aliasedEnzymesByName } from '@teselagen/sequence-utils';
6
+
7
+ /**
8
+ * Custom hook that manages plasmids state and logic
9
+ * @param {Object} params - Dependencies from FormDataContext
10
+ * @param {Array} params.parts - Array of parts
11
+ * @param {string} params.assemblyEnzyme - Assembly enzyme name
12
+ * @param {Object} params.overhangNames - Mapping of overhangs to names
13
+ * @returns {Object} - { linkedPlasmids, setLinkedPlasmids, uploadPlasmids }
14
+ */
15
+ export function usePlasmidsLogic({ parts, assemblyEnzyme, overhangNames }) {
16
+ const [linkedPlasmids, setLinkedPlasmidsState] = React.useState([]);
17
+
18
+ const graphForPlasmids = React.useMemo(() => partsToEdgesGraph(parts), [parts]);
19
+ const partDictionary = React.useMemo(() => parts.reduce((acc, part) => {
20
+ acc[`${part.left_overhang}-${part.right_overhang}`] = part;
21
+ return acc;
22
+ }, {}), [parts]);
23
+
24
+ const assignPlasmids = React.useCallback((plasmids) => plasmids.map(plasmid => {
25
+ const enzymes = [aliasedEnzymesByName[assemblyEnzyme.toLowerCase()]];
26
+ const correspondingParts = assignSequenceToSyntaxPart(plasmid, enzymes, graphForPlasmids);
27
+ const correspondingPartsStr = correspondingParts.map(part => `${part.left_overhang}-${part.right_overhang}`);
28
+ const correspondingPartsNames = correspondingParts.map(part => {
29
+ let namePart = '';
30
+ const leftName = overhangNames[part.left_overhang];
31
+ const rightName = overhangNames[part.right_overhang];
32
+ if (leftName || rightName) {
33
+ namePart = `${leftName || part.left_overhang}-${rightName || part.right_overhang}`;
34
+ }
35
+ return namePart;
36
+ });
37
+ return {
38
+ ...plasmid,
39
+ appData: {
40
+ ...plasmid.appData,
41
+ correspondingParts: correspondingPartsStr,
42
+ partInfo: correspondingPartsStr.map(partStr => partDictionary[partStr]),
43
+ correspondingPartsNames: correspondingPartsNames,
44
+ longestFeature: correspondingParts.map(part => part.longestFeature)
45
+ }
46
+ };
47
+ }), [graphForPlasmids, partDictionary, assemblyEnzyme, overhangNames]);
48
+
49
+ // Wrapper for setLinkedPlasmids that automatically assigns plasmids if enzyme is available
50
+ const setLinkedPlasmids = React.useCallback((plasmids) => {
51
+ if (assemblyEnzyme && Array.isArray(plasmids) && plasmids.length > 0) {
52
+ setLinkedPlasmidsState(assignPlasmids(plasmids));
53
+ } else {
54
+ setLinkedPlasmidsState(plasmids);
55
+ }
56
+ }, [assemblyEnzyme, assignPlasmids]);
57
+
58
+ // Update existing plasmids when enzyme or assignment logic changes
59
+ React.useEffect(() => {
60
+ if (assemblyEnzyme) {
61
+ setLinkedPlasmidsState((prevLinkedPlasmids) => {
62
+ if (prevLinkedPlasmids.length > 0) {
63
+ return assignPlasmids(prevLinkedPlasmids);
64
+ }
65
+ return prevLinkedPlasmids;
66
+ });
67
+ }
68
+ }, [assignPlasmids, assemblyEnzyme]); // Note: intentionally not including linkedPlasmids to avoid infinite loop
69
+
70
+ const uploadPlasmids = React.useCallback(async (files) => {
71
+ const plasmids = await Promise.all(files.map(async (file) => {
72
+ const data = await anyToJson(file);
73
+ const sequenceData = data[0].parsedSequence;
74
+ // Force circular
75
+ sequenceData.circular = true;
76
+ return {...sequenceData, appData: { fileName: file.name, correspondingParts: [], partInfo: [], correspondingPartsNames: [] } };
77
+ }));
78
+ setLinkedPlasmids(plasmids);
79
+ }, [setLinkedPlasmids]);
80
+
81
+ return { linkedPlasmids, setLinkedPlasmids, uploadPlasmids };
82
+ }
@@ -28,12 +28,3 @@ export const getFileFromELabFTW = async (itemId, fileInfo) => {
28
28
  throw new Error(`${error2String(e)}`);
29
29
  }
30
30
  };
31
-
32
- export function arrayCombinations(sets) {
33
- if (sets.length === 1) {
34
- return sets[0].map((el) => [el]);
35
- } else
36
- return sets[0].flatMap((val) =>
37
- arrayCombinations(sets.slice(1)).map((c) => [val].concat(c))
38
- );
39
- };
@@ -1,2 +1,4 @@
1
1
  export { default as OpenCloning } from './OpenCloning';
2
2
  export { default as MainAppBar } from './navigation/MainAppBar';
3
+ export { default as useUrlParamsLoader } from '../hooks/useUrlParamsLoader';
4
+ export { default as useInitializeApp } from '../hooks/useInitializeApp';
@@ -11,7 +11,6 @@ function SelectTemplateDialog({ onClose, open }) {
11
11
  const baseUrl = 'https://assets.opencloning.org/OpenCloning-submission';
12
12
  const httpClient = useHttpClient();
13
13
 
14
- // const baseUrl = '';
15
14
  React.useEffect(() => {
16
15
  const fetchData = async () => {
17
16
  try {
@@ -85,7 +85,6 @@ function DownloadPrimersButton({ primers }) {
85
85
  return (
86
86
  <>
87
87
  <Button
88
- variant="contained"
89
88
  onClick={() => setDialogOpen(true)}
90
89
  >
91
90
  Download Primers
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { shallowEqual, useDispatch, useSelector } from 'react-redux';
3
- import { Button } from '@mui/material';
3
+ import { Button, ButtonGroup } from '@mui/material';
4
4
  import PrimerForm from './PrimerForm';
5
5
  import PrimerTableRow from './PrimerTableRow';
6
6
  import './PrimerList.css';
@@ -99,8 +99,8 @@ function PrimerList() {
99
99
  />
100
100
  )) || (
101
101
  <div className="primer-add-container">
102
+ <ButtonGroup>
102
103
  <Button
103
- variant="contained"
104
104
  onClick={switchAddingPrimer}
105
105
  >
106
106
  Add Primer
@@ -109,12 +109,13 @@ function PrimerList() {
109
109
  <DownloadPrimersButton primers={primers} />
110
110
  {database && (
111
111
  <Button
112
- variant="contained"
113
112
  onClick={() => setImportingPrimer(true)}
114
113
  >
115
114
  {`Import from ${database.name}`}
116
115
  </Button>
116
+
117
117
  )}
118
+ </ButtonGroup>
118
119
  </div>
119
120
  )}
120
121
  </div>
@@ -52,7 +52,6 @@ function ImportPrimersButton({ addPrimer }) {
52
52
  <Tooltip arrow title={<span style={{ fontSize: '1.4em' }}>Upload a .csv or .tsv file with headers &apos;name&apos; and &apos;sequence&apos;</span>}>
53
53
  <Button
54
54
  onClick={handleUploadClick}
55
- variant="contained"
56
55
  >
57
56
  Import from file
58
57
  </Button>