@opencloning/ui 1.2.0 → 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 (31) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +4 -3
  3. package/src/components/assembler/Assembler.cy.jsx +364 -0
  4. package/src/components/assembler/Assembler.jsx +298 -205
  5. package/src/components/assembler/AssemblerPart.cy.jsx +52 -0
  6. package/src/components/assembler/AssemblerPart.jsx +51 -79
  7. package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +194 -0
  8. package/src/components/assembler/ExistingSyntaxDialog.jsx +65 -0
  9. package/src/components/assembler/PlasmidSyntaxTable.jsx +83 -0
  10. package/src/components/assembler/assembler_utils.js +134 -0
  11. package/src/components/assembler/assembler_utils.test.js +193 -0
  12. package/src/components/assembler/assembly_component.module.css +1 -1
  13. package/src/components/assembler/graph_utils.js +153 -0
  14. package/src/components/assembler/graph_utils.test.js +239 -0
  15. package/src/components/assembler/index.js +9 -0
  16. package/src/components/assembler/useAssembler.js +59 -22
  17. package/src/components/assembler/useCombinatorialAssembly.js +76 -0
  18. package/src/components/assembler/usePlasmidsLogic.js +82 -0
  19. package/src/components/eLabFTW/utils.js +0 -9
  20. package/src/components/index.js +2 -0
  21. package/src/components/navigation/SelectTemplateDialog.jsx +0 -1
  22. package/src/components/primers/DownloadPrimersButton.jsx +0 -1
  23. package/src/components/primers/PrimerList.jsx +4 -3
  24. package/src/components/primers/import_primers/ImportPrimersButton.jsx +0 -1
  25. package/src/version.js +1 -1
  26. package/vitest.config.js +2 -4
  27. package/src/components/DraggableDialogPaper.jsx +0 -16
  28. package/src/components/assembler/AssemblePartWidget.jsx +0 -252
  29. package/src/components/assembler/StopIcon.jsx +0 -34
  30. package/src/components/assembler/assembler_data2.json +0 -50
  31. 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>
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.2.0";
2
+ export const version = "1.3.0";
package/vitest.config.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import { defineConfig } from 'vitest/config';
2
2
  import { resolve } from 'path';
3
+ import { testConfig } from '../../vitest.common.config';
3
4
 
4
5
  export default defineConfig({
5
6
  test: {
6
- globals: true,
7
- environment: 'jsdom',
8
- setupFiles: '../../tests/setup.js',
9
- include: ['src/**/*.{test,spec}.{js,jsx}'],
7
+ ...testConfig,
10
8
  },
11
9
  resolve: {
12
10
  alias: {
@@ -1,16 +0,0 @@
1
- import { Paper } from '@mui/material';
2
- import React from 'react';
3
- import Draggable from 'react-draggable';
4
-
5
- function DraggableDialogPaper(props) {
6
- return (
7
- <Draggable
8
- handle="#draggable-dialog-title"
9
- cancel={'[class*="MuiDialogContent-root"]'}
10
- >
11
- <Paper {...props} />
12
- </Draggable>
13
- );
14
- }
15
-
16
- export default DraggableDialogPaper;
@@ -1,252 +0,0 @@
1
- import React from 'react'
2
- import { TextField, FormControl, InputLabel, Select, MenuItem, Box, Grid, Paper, Typography, Table, TableContainer, TableHead, TableBody, TableRow, TableCell, Button } from '@mui/material'
3
- import { ContentCopy as ContentCopyIcon } from '@mui/icons-material'
4
- import AssemblerPart from './AssemblerPart'
5
-
6
- /* eslint-disable camelcase */
7
- const defaultData = {
8
- header: 'Header',
9
- body: 'helper text / body text',
10
- glyph: 'cds-stop',
11
- left_overhang: 'CATG',
12
- right_overhang: 'TATG',
13
- left_inside: 'AAAATA',
14
- right_inside: 'AATG',
15
- left_codon_start: 2,
16
- right_codon_start: 1,
17
- color: 'greenyellow',
18
- }
19
- /* eslint-enable camelcase */
20
-
21
- const glyphOptions = [
22
- 'assembly-scar',
23
- 'cds',
24
- 'cds-stop',
25
- 'chromosomal-locus',
26
- 'engineered-region',
27
- 'five-prime-sticky-restriction-site',
28
- 'origin-of-replication',
29
- 'primer-binding-site',
30
- 'promoter',
31
- 'ribosome-entry-site',
32
- 'specific-recombination-site',
33
- 'terminator',
34
- 'three-prime-sticky-restriction-site',
35
- ]
36
-
37
- function AssemblePartWidget() {
38
- const [formData, setFormData] = React.useState(defaultData)
39
-
40
- const handleChange = (field) => (event) => {
41
- const value = event.target.value
42
- setFormData((prev) => ({
43
- ...prev,
44
- [field]: field === 'left_codon_start' || field === 'right_codon_start'
45
- ? (value === '' ? '' : parseInt(value, 10) || 0)
46
- : value,
47
- }))
48
- }
49
-
50
- const handleCopyRow = async () => {
51
- const keys = Object.keys(formData)
52
- const headers = keys.join('\t')
53
- const values = keys.map((key) => String(formData[key])).join('\t')
54
- const tsvData = `${headers}\n${values}`
55
-
56
- try {
57
- if (window.navigator && window.navigator.clipboard) {
58
- await window.navigator.clipboard.writeText(tsvData)
59
- }
60
- } catch (err) {
61
- // eslint-disable-next-line no-console
62
- console.error('Failed to copy to clipboard:', err)
63
- }
64
- }
65
-
66
- return (
67
- <Box sx={{
68
- p: 1.5,
69
- maxHeight: '100vh',
70
- overflowY: 'auto',
71
- overflowX: 'hidden'
72
- }}>
73
- <Grid container spacing={2}>
74
- <Grid item xs={12} md={6}>
75
- <Paper sx={{ p: 1.5 }}>
76
- <Typography variant="h6" gutterBottom sx={{ mb: 1.5 }}>
77
- Part Configuration
78
- </Typography>
79
- <Box component="form" sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
80
- <TextField
81
- size="small"
82
- label="Header"
83
- value={formData.header}
84
- onChange={handleChange('header')}
85
- fullWidth
86
- />
87
- <TextField
88
- size="small"
89
- label="Body"
90
- value={formData.body}
91
- onChange={handleChange('body')}
92
- fullWidth
93
- multiline
94
- rows={2}
95
- />
96
- <FormControl fullWidth size="small">
97
- <InputLabel id="glyph-select-label">Glyph</InputLabel>
98
- <Select
99
- labelId="glyph-select-label"
100
- value={formData.glyph}
101
- label="Glyph"
102
- onChange={handleChange('glyph')}
103
- >
104
- {glyphOptions.map((option) => (
105
- <MenuItem key={option} value={option}>
106
- {option}
107
- </MenuItem>
108
- ))}
109
- </Select>
110
- </FormControl>
111
- <TextField
112
- size="small"
113
- label="Color"
114
- value={formData.color}
115
- onChange={handleChange('color')}
116
- fullWidth
117
- helperText="CSS color name or hex code"
118
- />
119
- <Typography variant="subtitle2" sx={{ mt: 0.5, mb: 0.5 }}>
120
- Left Side
121
- </Typography>
122
- <TextField
123
- size="small"
124
- label="Left Overhang"
125
- value={formData.left_overhang}
126
- onChange={handleChange('left_overhang')}
127
- error={formData.left_overhang.length !== 4}
128
- helperText={formData.left_overhang.length !== 4 ? 'Must be 4 bases' : ''}
129
- fullWidth
130
- />
131
- <TextField
132
- size="small"
133
- label="Left Inside"
134
- value={formData.left_inside}
135
- onChange={handleChange('left_inside')}
136
- fullWidth
137
- />
138
- <TextField
139
- size="small"
140
- label="Left Codon Start"
141
- type="number"
142
- value={formData.left_codon_start}
143
- onChange={handleChange('left_codon_start')}
144
- fullWidth
145
- inputProps={{ min: 0 }}
146
- helperText="If the left side is translated, where the codon starts"
147
- />
148
- <Typography variant="subtitle2" sx={{ mt: 0.5, mb: 0.5 }}>
149
- Right Side
150
- </Typography>
151
- <TextField
152
- size="small"
153
- label="Right Overhang"
154
- value={formData.right_overhang}
155
- onChange={handleChange('right_overhang')}
156
- fullWidth
157
- />
158
- <TextField
159
- size="small"
160
- label="Right Inside"
161
- value={formData.right_inside}
162
- onChange={handleChange('right_inside')}
163
- fullWidth
164
- />
165
- <TextField
166
- size="small"
167
- label="Right Codon Start"
168
- type="number"
169
- value={formData.right_codon_start}
170
- onChange={handleChange('right_codon_start')}
171
- fullWidth
172
- inputProps={{ min: 0 }}
173
- helperText="If the right side is translated, where the codon starts"
174
- />
175
- </Box>
176
- </Paper>
177
- </Grid>
178
- <Grid item xs={12} md={6}>
179
- <Paper sx={{ p: 1.5 }}>
180
- <Typography variant="h6" gutterBottom sx={{ mb: 1.5 }}>
181
- Preview
182
- </Typography>
183
- <Box sx={{ mt: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
184
- {(formData.header || formData.body) && (
185
- <Box sx={{
186
- textAlign: 'center',
187
- mb: 1.5,
188
- display: 'flex',
189
- flexDirection: 'column',
190
- gap: 0.5
191
- }}>
192
- {formData.header && (
193
- <Typography variant="h6" sx={{ fontWeight: 'bold' }}>
194
- {formData.header}
195
- </Typography>
196
- )}
197
- {formData.body && (
198
- <Typography variant="body2" sx={{ color: 'text.secondary' }}>
199
- {formData.body}
200
- </Typography>
201
- )}
202
- </Box>
203
- )}
204
- <AssemblerPart data={formData} />
205
- </Box>
206
- </Paper>
207
- </Grid>
208
- </Grid>
209
- <Box sx={{ mt: 2 }}>
210
- <Paper sx={{ p: 1.5 }}>
211
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
212
- <Typography variant="h6">
213
- JSON Data
214
- </Typography>
215
- <Button
216
- size="small"
217
- variant="outlined"
218
- startIcon={<ContentCopyIcon />}
219
- onClick={handleCopyRow}
220
- >
221
- Copy Row
222
- </Button>
223
- </Box>
224
- <TableContainer>
225
- <Table size="small" sx={{ '& .MuiTableCell-root': { py: 0.5, px: 1 } }}>
226
- <TableHead>
227
- <TableRow>
228
- {Object.keys(formData).map((key) => (
229
- <TableCell key={key} sx={{ fontWeight: 'bold' }}>
230
- {key}
231
- </TableCell>
232
- ))}
233
- </TableRow>
234
- </TableHead>
235
- <TableBody>
236
- <TableRow>
237
- {Object.keys(formData).map((key) => (
238
- <TableCell key={key}>
239
- {String(formData[key])}
240
- </TableCell>
241
- ))}
242
- </TableRow>
243
- </TableBody>
244
- </Table>
245
- </TableContainer>
246
- </Paper>
247
- </Box>
248
- </Box>
249
- )
250
- }
251
-
252
- export default AssemblePartWidget