@opencloning/ui 1.2.0 → 1.3.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 (31) hide show
  1. package/CHANGELOG.md +27 -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 +251 -0
  8. package/src/components/assembler/ExistingSyntaxDialog.jsx +104 -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
@@ -1,97 +1,69 @@
1
1
  import React from 'react'
2
2
  import styles from './assembly_component.module.css'
3
3
 
4
- import { getAminoAcidFromSequenceTriplet, getComplementSequenceString } from '@teselagen/sequence-utils'
5
4
  import { getSvgByGlyph } from './sbol_visual_glyphs'
5
+ import { partDataToDisplayData } from './assembler_utils'
6
6
 
7
+ export function AssemblerPartCore({ color = 'lightgray', glyph = 'engineered-region' }) {
8
+ return (
9
+ <div data-testid="assembler-part-core" className={styles.boxContainer}>
10
+ <div className={styles.box}>
11
+ <div className={styles.imageContainer} style={{ backgroundColor: color }}>
12
+ <img src={getSvgByGlyph(glyph)} alt={`${glyph}.svg`} />
13
+ </div>
14
+ </div>
15
+ </div>
16
+ )
17
+ }
7
18
 
8
- const defaultData =
9
- {
10
- header: 'Promoter',
11
- body: 'promoter text',
12
- glyph: 'cds-stop',
13
- left_overhang: 'CATG',
14
- right_overhang: 'TATG',
15
- left_inside:'AAAATA',
16
- right_inside:'AATG',
17
- left_codon_start: 2,
18
- right_codon_start: 1,
19
- color: 'greenyellow',
20
- }
19
+ export function DisplayOverhang( { overhang, overhangRc, translation, isRight = false } ) {
20
+ return (
21
+ <div data-testid="display-overhang" className={`${styles.dna} ${styles.overhang} ${isRight ? styles.right : styles.left}`}>
22
+ <div className={styles.top}>{translation}</div>
23
+ <div className={styles.watson}>{overhang}</div>
24
+ <div className={styles.crick}>{overhangRc}</div>
25
+ <div className={styles.bottom}> </div>
26
+ </div>
27
+ )
28
+ }
29
+ export function DisplayInside( { inside, insideRc, translation, isRight = false } ) {
30
+ return (
31
+ <div data-testid="display-inside" className={`${styles.dna} ${styles.inside} ${isRight ? styles.right : styles.left}`}>
32
+ <div className={styles.top}>{translation}</div>
33
+ <div className={styles.watson}>{inside}</div>
34
+ <div className={styles.crick}>{insideRc}</div>
35
+ <div className={styles.bottom}> </div>
36
+ </div>
37
+ )
38
+ }
21
39
 
40
+ export function AssemblerPartContainer({ children }) {
41
+ return (
42
+ <div className={styles.container}>
43
+ {children}
44
+ </div>
45
+ )
46
+ }
22
47
 
23
- function AssemblerPart( { data = defaultData } ) {
24
- const {
25
- left_codon_start: leftCodonStart,
26
- right_codon_start: rightCodonStart,
27
- left_overhang: leftOverhang,
28
- right_overhang: rightOverhang,
29
- left_inside: leftInside,
30
- right_inside: rightInside,
31
- glyph
32
- } = data
33
- const leftOverhangRc = getComplementSequenceString(leftOverhang)
34
- const rightOverhangRc = getComplementSequenceString(rightOverhang)
35
- const leftInsideRc = getComplementSequenceString(leftInside)
36
- const rightInsideRc = getComplementSequenceString(rightInside)
37
- let leftTranslationOverhang = ''
38
- let leftTranslationInside = ''
39
- if (leftCodonStart) {
40
- const triplets = (leftOverhang + leftInside).slice(leftCodonStart - 1).match(/.{3}/g)
41
- const padding = ' '.repeat(leftCodonStart - 1)
42
- const translationLeft = padding + triplets.map(triplet => getAminoAcidFromSequenceTriplet(triplet).threeLettersName.replace('Stop', '***')).join('')
43
- leftTranslationOverhang = translationLeft.slice(0, leftOverhang.length)
44
- leftTranslationInside = translationLeft.slice(leftOverhang.length)
45
- }
46
- let rightTranslationOverhang = ''
47
- let rightTranslationInside = ''
48
- if (rightCodonStart) {
49
- const triplets = (rightInside + rightOverhang).slice(rightCodonStart - 1).match(/.{3}/g)
50
- const padding = ' '.repeat(rightCodonStart - 1)
51
- const translationRight = padding + triplets.map(triplet => getAminoAcidFromSequenceTriplet(triplet).threeLettersName.replace('Stop', '***')).join('')
52
- rightTranslationInside = translationRight.slice(0, rightInside.length)
53
- rightTranslationOverhang = translationRight.slice(rightInside.length)
54
- }
48
+ function AssemblerPart( { data, showRight = true } ) {
49
+ const { left_overhang: leftOverhang, right_overhang: rightOverhang, left_inside: leftInside, right_inside: rightInside, glyph } = data
50
+ const { leftTranslationOverhang, leftTranslationInside, rightTranslationOverhang, rightTranslationInside, leftOverhangRc, rightOverhangRc, leftInsideRc, rightInsideRc } = partDataToDisplayData(data)
55
51
 
56
52
  return (
57
53
 
58
- <div className={styles.container}>
59
- <div className={`${styles.dna} ${styles.overhang} ${styles.left}`}>
60
- <div className={styles.top}>{leftTranslationOverhang}</div>
61
- <div className={styles.watson}>{leftOverhang}</div>
62
- <div className={styles.crick}>{leftOverhangRc}</div>
63
- <div className={styles.bottom}> </div>
64
- </div>
54
+ <AssemblerPartContainer>
55
+ <DisplayOverhang overhang={leftOverhang} overhangRc={leftOverhangRc} translation={leftTranslationOverhang} isRight={false} />
65
56
  {leftInside && (
66
- <div className={`${styles.dna} ${styles.insideLeft}`}>
67
- <div className={styles.top}>{leftTranslationInside}</div>
68
- <div className={styles.watson}>{leftInside}</div>
69
- <div className={styles.crick}>{leftInsideRc}</div>
70
- <div className={styles.bottom}> </div>
71
- </div>
57
+ <DisplayInside inside={leftInside} insideRc={leftInsideRc} translation={leftTranslationInside} isRight={false} />
72
58
  )}
73
- <div className={styles.boxContainer}>
74
- <div className={styles.box}>
75
- <div className={styles.imageContainer} style={{ backgroundColor: data.color || 'lightgray' }}>
76
- <img src={getSvgByGlyph(glyph)} alt={`${glyph}.svg`} />
77
- </div>
78
- </div>
79
- </div>
59
+ <AssemblerPartCore color={data.color || 'lightgray'} glyph={glyph} />
80
60
  {rightInside && (
81
- <div className={`${styles.dna} ${styles.insideRight}`}>
82
- <div className={styles.top}>{rightTranslationInside}</div>
83
- <div className={styles.watson}>{rightInside}</div>
84
- <div className={styles.crick}>{rightInsideRc}</div>
85
- <div className={styles.bottom}> </div>
86
- </div>
61
+ <DisplayInside inside={rightInside} insideRc={rightInsideRc} translation={rightTranslationInside} isRight={true} />
87
62
  )}
88
- <div className={`${styles.dna} ${styles.overhang} ${styles.right}`}>
89
- <div className={styles.top}>{rightTranslationOverhang}</div>
90
- <div className={styles.watson}>{rightOverhang}</div>
91
- <div className={styles.crick}>{rightOverhangRc}</div>
92
- <div className={styles.bottom}> </div>
93
- </div>
94
- </div>
63
+ { showRight && (
64
+ <DisplayOverhang overhang={rightOverhang} overhangRc={rightOverhangRc} translation={rightTranslationOverhang} isRight={true} />
65
+ )}
66
+ </AssemblerPartContainer>
95
67
 
96
68
  )
97
69
  }
@@ -0,0 +1,251 @@
1
+ import React from 'react';
2
+ import ExistingSyntaxDialog from './ExistingSyntaxDialog';
3
+
4
+ const mockSyntaxes = [
5
+ {
6
+ path: 'test-syntax-1',
7
+ name: 'Test Syntax 1',
8
+ description: 'First test syntax',
9
+ },
10
+ {
11
+ path: 'test-syntax-2',
12
+ name: 'Test Syntax 2',
13
+ description: 'Second test syntax',
14
+ },
15
+ ];
16
+
17
+ const mockSyntaxData = {
18
+ name: 'Test Syntax',
19
+ parts: [],
20
+ };
21
+
22
+ const mockPlasmidsData = [
23
+ {
24
+ id: 1,
25
+ name: 'Test Plasmid',
26
+ },
27
+ ];
28
+
29
+ describe('<ExistingSyntaxDialog />', () => {
30
+ beforeEach(() => {
31
+ // Intercept the index.json request
32
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/index.json', {
33
+ statusCode: 200,
34
+ body: mockSyntaxes,
35
+ }).as('getSyntaxes');
36
+ });
37
+
38
+ it('loads and displays syntaxes successfully', () => {
39
+ const onCloseSpy = cy.spy().as('onCloseSpy');
40
+ const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
41
+
42
+ cy.mount(
43
+ <ExistingSyntaxDialog
44
+ onClose={onCloseSpy}
45
+ onSyntaxSelect={onSyntaxSelectSpy}
46
+ />,
47
+ );
48
+
49
+ cy.wait('@getSyntaxes');
50
+ cy.contains('Load an existing syntax').should('exist');
51
+ cy.contains('Test Syntax 1').should('exist');
52
+ cy.contains('First test syntax').should('exist');
53
+ cy.contains('Test Syntax 2').should('exist');
54
+ cy.contains('Second test syntax').should('exist');
55
+ });
56
+
57
+ it('successfully loads syntax and plasmids data when clicking a syntax', () => {
58
+ const onCloseSpy = cy.spy().as('onCloseSpy');
59
+ const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
60
+
61
+ // Intercept syntax.json and plasmids.json requests
62
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-1/syntax.json', {
63
+ statusCode: 200,
64
+ body: mockSyntaxData,
65
+ }).as('getSyntaxData');
66
+
67
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-1/plasmids.json', {
68
+ statusCode: 200,
69
+ body: mockPlasmidsData,
70
+ }).as('getPlasmidsData');
71
+
72
+ cy.mount(
73
+ <ExistingSyntaxDialog
74
+ onClose={onCloseSpy}
75
+ onSyntaxSelect={onSyntaxSelectSpy}
76
+ />,
77
+ );
78
+
79
+ cy.wait('@getSyntaxes');
80
+ cy.contains('Test Syntax 1').click();
81
+
82
+ cy.wait('@getSyntaxData');
83
+ cy.wait('@getPlasmidsData');
84
+
85
+ cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', mockSyntaxData, mockPlasmidsData);
86
+ cy.get('@onCloseSpy').should('have.been.called');
87
+ });
88
+
89
+ it('displays error message when syntax.json request fails', () => {
90
+ const onCloseSpy = cy.spy().as('onCloseSpy');
91
+ const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
92
+
93
+ // Intercept syntax.json with error
94
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-1/syntax.json', {
95
+ statusCode: 500,
96
+ body: { error: 'Internal server error' },
97
+ }).as('getSyntaxDataError');
98
+
99
+ cy.mount(
100
+ <ExistingSyntaxDialog
101
+ onClose={onCloseSpy}
102
+ onSyntaxSelect={onSyntaxSelectSpy}
103
+ />,
104
+ );
105
+
106
+ cy.wait('@getSyntaxes');
107
+ cy.contains('Test Syntax 1').click();
108
+
109
+ cy.wait('@getSyntaxDataError');
110
+
111
+ cy.contains('Failed to load syntax data. Please try again.').should('exist');
112
+ cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
113
+ cy.get('@onCloseSpy').should('not.have.been.called');
114
+ });
115
+
116
+ it('displays error message when plasmids.json request fails', () => {
117
+ const onCloseSpy = cy.spy().as('onCloseSpy');
118
+ const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
119
+
120
+ // Intercept syntax.json successfully
121
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-1/syntax.json', {
122
+ statusCode: 200,
123
+ body: mockSyntaxData,
124
+ }).as('getSyntaxData');
125
+
126
+ // Intercept plasmids.json with error
127
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-1/plasmids.json', {
128
+ statusCode: 500,
129
+ body: { error: 'Internal server error' },
130
+ }).as('getPlasmidsDataError');
131
+
132
+ cy.mount(
133
+ <ExistingSyntaxDialog
134
+ onClose={onCloseSpy}
135
+ onSyntaxSelect={onSyntaxSelectSpy}
136
+ />,
137
+ );
138
+
139
+ cy.wait('@getSyntaxes');
140
+ cy.contains('Test Syntax 1').click();
141
+
142
+ cy.wait('@getSyntaxData');
143
+ cy.wait('@getPlasmidsDataError');
144
+
145
+ cy.contains('Failed to load plasmids data. Please try again.').should('exist');
146
+ cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
147
+ cy.get('@onCloseSpy').should('not.have.been.called');
148
+ });
149
+
150
+ it('clears error message when selecting a different syntax after error', () => {
151
+ const onCloseSpy = cy.spy().as('onCloseSpy');
152
+ const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
153
+
154
+ // First syntax fails
155
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-1/syntax.json', {
156
+ statusCode: 500,
157
+ body: { error: 'Internal server error' },
158
+ }).as('getSyntaxDataError');
159
+
160
+ // Second syntax succeeds
161
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-2/syntax.json', {
162
+ statusCode: 200,
163
+ body: mockSyntaxData,
164
+ }).as('getSyntaxData2');
165
+
166
+ cy.intercept('GET', 'https://assets.opencloning.org/syntaxes/syntaxes/test-syntax-2/plasmids.json', {
167
+ statusCode: 200,
168
+ body: mockPlasmidsData,
169
+ }).as('getPlasmidsData2');
170
+
171
+ cy.mount(
172
+ <ExistingSyntaxDialog
173
+ onClose={onCloseSpy}
174
+ onSyntaxSelect={onSyntaxSelectSpy}
175
+ />,
176
+ );
177
+
178
+ cy.wait('@getSyntaxes');
179
+
180
+ // Click first syntax (fails)
181
+ cy.contains('Test Syntax 1').click();
182
+ cy.wait('@getSyntaxDataError');
183
+ cy.contains('Failed to load syntax data. Please try again.').should('exist');
184
+
185
+ // Click second syntax (succeeds) - error should be cleared
186
+ cy.contains('Test Syntax 2').click();
187
+ cy.wait('@getSyntaxData2');
188
+ cy.wait('@getPlasmidsData2');
189
+
190
+ cy.contains('Failed to load syntax data. Please try again.').should('not.exist');
191
+ cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', mockSyntaxData, mockPlasmidsData);
192
+ cy.get('@onCloseSpy').should('have.been.called');
193
+ });
194
+
195
+ it('successfully uploads syntax from JSON file', () => {
196
+ const onCloseSpy = cy.spy().as('onCloseSpy');
197
+ const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
198
+
199
+ const uploadedSyntaxData = {
200
+ name: 'Uploaded Syntax',
201
+ parts: [
202
+ { id: 1, name: 'Part 1' },
203
+ { id: 2, name: 'Part 2' },
204
+ ],
205
+ };
206
+
207
+ // Create a temporary JSON file
208
+ cy.writeFile('cypress/temp/syntax.json', uploadedSyntaxData);
209
+
210
+ cy.mount(
211
+ <ExistingSyntaxDialog
212
+ onClose={onCloseSpy}
213
+ onSyntaxSelect={onSyntaxSelectSpy}
214
+ />,
215
+ );
216
+
217
+ cy.wait('@getSyntaxes');
218
+ cy.contains('Upload syntax from JSON file').should('exist');
219
+
220
+ // Upload the JSON file
221
+ cy.get('input[type="file"]').selectFile('cypress/temp/syntax.json', { force: true });
222
+
223
+ cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', uploadedSyntaxData, []);
224
+ cy.get('@onCloseSpy').should('have.been.called');
225
+ });
226
+
227
+ it('displays error message when uploaded JSON file is invalid', () => {
228
+ const onCloseSpy = cy.spy().as('onCloseSpy');
229
+ const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
230
+
231
+ // Create a file with invalid JSON
232
+ cy.writeFile('cypress/temp/invalid.json', '{ invalid json }', { encoding: 'utf8' });
233
+
234
+ cy.mount(
235
+ <ExistingSyntaxDialog
236
+ onClose={onCloseSpy}
237
+ onSyntaxSelect={onSyntaxSelectSpy}
238
+ />,
239
+ );
240
+
241
+ cy.wait('@getSyntaxes');
242
+ cy.contains('Upload syntax from JSON file').should('exist');
243
+
244
+ // Upload invalid JSON
245
+ cy.get('input[type="file"]').selectFile('cypress/temp/invalid.json', { force: true });
246
+
247
+ cy.contains(/Failed to parse JSON file/).should('exist');
248
+ cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
249
+ cy.get('@onCloseSpy').should('not.have.been.called');
250
+ });
251
+ });
@@ -0,0 +1,104 @@
1
+ import React from 'react'
2
+ import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box } from '@mui/material'
3
+ import getHttpClient from '@opencloning/utils/getHttpClient';
4
+ import RequestStatusWrapper from '../form/RequestStatusWrapper';
5
+
6
+ const httpClient = getHttpClient();
7
+ const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
8
+ httpClient.defaults.baseURL = baseURL;
9
+
10
+ function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
11
+ const [syntaxes, setSyntaxes] = React.useState([]);
12
+ const [connectAttempt, setConnectAttempt] = React.useState(0);
13
+ const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
14
+ const [loadError, setLoadError] = React.useState(null);
15
+ const fileInputRef = React.useRef(null);
16
+
17
+ React.useEffect(() => {
18
+ setRequestStatus({ status: 'loading' });
19
+ const fetchData = async () => {
20
+ try {
21
+ const { data } = await httpClient.get('index.json');
22
+ setRequestStatus({ status: 'success' });
23
+ setSyntaxes(data);
24
+ } catch {
25
+ setRequestStatus({ status: 'error', message: 'Could not load syntaxes' });
26
+ }
27
+ };
28
+ fetchData();
29
+ }, [connectAttempt]);
30
+
31
+ const onSyntaxClick = React.useCallback(async (syntax) => {
32
+ setLoadError(null);
33
+ let loadingErrorPart = 'syntax'
34
+ try {
35
+ const { data: syntaxData } = await httpClient.get(`${syntax.path}/syntax.json`);
36
+ loadingErrorPart = 'plasmids'
37
+ const { data: plasmidsData } = await httpClient.get(`${syntax.path}/plasmids.json`);
38
+ onSyntaxSelect(syntaxData, plasmidsData);
39
+ onClose();
40
+ } catch {
41
+ setLoadError(`Failed to load ${loadingErrorPart} data. Please try again.`);
42
+ }
43
+ }, [onSyntaxSelect, onClose]);
44
+
45
+ const handleFileUpload = React.useCallback(async (event) => {
46
+ setLoadError(null);
47
+ const file = event.target.files[0];
48
+ if (!file) return;
49
+
50
+ try {
51
+ const text = await file.text();
52
+ const syntaxData = JSON.parse(text);
53
+
54
+ // Uploaded JSON files contain only syntax data, no plasmids
55
+ onSyntaxSelect(syntaxData, []);
56
+ onClose();
57
+ } catch (error) {
58
+ setLoadError(`Failed to parse JSON file: ${error.message}`);
59
+ } finally {
60
+ // Reset file input so the same file can be selected again
61
+ if (fileInputRef.current) {
62
+ fileInputRef.current.value = '';
63
+ }
64
+ }
65
+ }, [onSyntaxSelect, onClose]);
66
+
67
+ return (
68
+ <Dialog open onClose={onClose}>
69
+ <DialogTitle>Load an existing syntax</DialogTitle>
70
+ <DialogContent>
71
+ {loadError && <Alert severity="error" sx={{ mb: 2 }}>{loadError}</Alert>}
72
+ <RequestStatusWrapper requestStatus={requestStatus} retry={() => setConnectAttempt((prev) => prev + 1)}>
73
+ <List>
74
+ {syntaxes.map((syntax) => (
75
+ <ListItem key={syntax.path}>
76
+ <ListItemButton onClick={() => {onSyntaxClick(syntax)}}>
77
+ <ListItemText primary={syntax.name} secondary={syntax.description} />
78
+ </ListItemButton>
79
+ </ListItem>
80
+ ))}
81
+ </List>
82
+ </RequestStatusWrapper>
83
+ <Box sx={{ mb: 2 }}>
84
+ <input
85
+ type="file"
86
+ ref={fileInputRef}
87
+ accept=".json"
88
+ onChange={handleFileUpload}
89
+ style={{ display: 'none' }}
90
+ />
91
+ <Button
92
+ variant="outlined"
93
+ sx={{ display: 'block', mx: 'auto' }}
94
+ onClick={() => fileInputRef.current?.click()}
95
+ >
96
+ Upload syntax from JSON file
97
+ </Button>
98
+ </Box>
99
+ </DialogContent>
100
+ </Dialog>
101
+ )
102
+ }
103
+
104
+ export default ExistingSyntaxDialog
@@ -0,0 +1,83 @@
1
+
2
+ import React from 'react'
3
+ import { Table, TableHead, TableRow, TableCell, TableBody, Typography, Chip, Box } from '@mui/material'
4
+
5
+ function PartChip({ name, overhang}) {
6
+ const label = name ? `${overhang} (${name})` : overhang;
7
+ return (
8
+ <Chip label={label} size="small" sx={{ fontSize: '0.7rem', height: 20, fontFamily: 'monospace' }}
9
+ />
10
+ )
11
+ }
12
+
13
+ function PlasmidRow({ plasmid }) {
14
+ const { name, appData } = plasmid;
15
+ const { fileName, correspondingParts, correspondingPartsNames, partInfo, longestFeature } = appData;
16
+ let sx = undefined;
17
+ let infoStr = '-';
18
+ let longestFeatureStr = '-';
19
+ let noParts = partInfo.length === 0;
20
+ if (partInfo.length === 1) {
21
+ sx = {
22
+ backgroundColor: partInfo[0]?.color,
23
+ }
24
+ infoStr = partInfo[0] ? partInfo[0].name : 'Spans multiple parts';
25
+ longestFeatureStr = longestFeature ? longestFeature[0].name : '-';
26
+ }
27
+ const multipleParts = partInfo.length > 1;
28
+ if (multipleParts) {
29
+ infoStr = 'Contains multiple parts';
30
+ }
31
+
32
+ return (
33
+ <TableRow sx={sx}>
34
+ <TableCell sx={{ maxWidth: 200 }}>
35
+ <Typography variant="body2" noWrap title={name}>
36
+ {name}
37
+ </Typography>
38
+ </TableCell>
39
+ <TableCell sx={{ maxWidth: 200 }}>
40
+ <Typography variant="body2" noWrap title={fileName}>
41
+ {fileName}
42
+ </Typography>
43
+ </TableCell>
44
+ <TableCell sx={multipleParts ? {backgroundColor: 'red'} : null}>
45
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
46
+ {noParts ? '-' : correspondingParts.map((part, idx) => (
47
+ <PartChip key={idx} name={correspondingPartsNames[idx]} overhang={part}
48
+ />
49
+ ))}
50
+ </Box>
51
+ </TableCell>
52
+ <TableCell>
53
+ <Typography variant="body2">{infoStr}</Typography>
54
+ </TableCell>
55
+ <TableCell>
56
+ <Typography variant="body2">{longestFeatureStr}</Typography>
57
+ </TableCell>
58
+ </TableRow>
59
+ )
60
+ }
61
+
62
+ function PlasmidSyntaxTable( { plasmids } ) {
63
+ return (
64
+ <Table stickyHeader size="small">
65
+ <TableHead sx={{ '& th': { fontWeight: 'bold' } }}>
66
+ <TableRow>
67
+ <TableCell>Name</TableCell>
68
+ <TableCell>File Name</TableCell>
69
+ <TableCell>Overhangs</TableCell>
70
+ <TableCell>Part Info</TableCell>
71
+ <TableCell>Longest Feature</TableCell>
72
+ </TableRow>
73
+ </TableHead>
74
+ <TableBody>
75
+ {plasmids.map((plasmid, index) => (
76
+ <PlasmidRow key={index} plasmid={plasmid} />
77
+ ))}
78
+ </TableBody>
79
+ </Table>
80
+ )
81
+ }
82
+
83
+ export default PlasmidSyntaxTable