@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
@@ -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,194 @@
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
+ });
@@ -0,0 +1,65 @@
1
+ import React from 'react'
2
+ import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert } 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
+
16
+ React.useEffect(() => {
17
+ setRequestStatus({ status: 'loading' });
18
+ const fetchData = async () => {
19
+ try {
20
+ const { data } = await httpClient.get('index.json');
21
+ setRequestStatus({ status: 'success' });
22
+ setSyntaxes(data);
23
+ } catch {
24
+ setRequestStatus({ status: 'error', message: 'Could not load syntaxes' });
25
+ }
26
+ };
27
+ fetchData();
28
+ }, [connectAttempt]);
29
+
30
+ const onSyntaxClick = React.useCallback(async (syntax) => {
31
+ setLoadError(null);
32
+ let loadingErrorPart = 'syntax'
33
+ try {
34
+ const { data: syntaxData } = await httpClient.get(`${syntax.path}/syntax.json`);
35
+ loadingErrorPart = 'plasmids'
36
+ const { data: plasmidsData } = await httpClient.get(`${syntax.path}/plasmids.json`);
37
+ onSyntaxSelect(syntaxData, plasmidsData);
38
+ onClose();
39
+ } catch {
40
+ setLoadError(`Failed to load ${loadingErrorPart} data. Please try again.`);
41
+ }
42
+ }, [onSyntaxSelect, onClose]);
43
+
44
+ return (
45
+ <Dialog open onClose={onClose}>
46
+ <DialogTitle>Load an existing syntax</DialogTitle>
47
+ <DialogContent>
48
+ <RequestStatusWrapper requestStatus={requestStatus} retry={() => setConnectAttempt((prev) => prev + 1)}>
49
+ {loadError && <Alert severity="error" sx={{ mb: 2 }}>{loadError}</Alert>}
50
+ <List>
51
+ {syntaxes.map((syntax) => (
52
+ <ListItem key={syntax.path}>
53
+ <ListItemButton onClick={() => {onSyntaxClick(syntax)}}>
54
+ <ListItemText primary={syntax.name} secondary={syntax.description} />
55
+ </ListItemButton>
56
+ </ListItem>
57
+ ))}
58
+ </List>
59
+ </RequestStatusWrapper>
60
+ </DialogContent>
61
+ </Dialog>
62
+ )
63
+ }
64
+
65
+ 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
@@ -0,0 +1,134 @@
1
+ import { isRangeWithinRange } from '@teselagen/range-utils';
2
+ import { getComplementSequenceString, getAminoAcidFromSequenceTriplet, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString } from '@teselagen/sequence-utils';
3
+ import { allSimplePaths } from 'graphology-simple-path';
4
+
5
+ export function tripletsToTranslation(triplets) {
6
+ if (!triplets) return ''
7
+ return triplets.map(triplet =>
8
+ /[^ACGT]/i.test(triplet) ? ' - ' :
9
+ getAminoAcidFromSequenceTriplet(triplet).threeLettersName.replace('Stop', '***')
10
+ ).join('')
11
+ }
12
+
13
+ export function partDataToDisplayData(data) {
14
+ const {
15
+ left_codon_start: leftCodonStart,
16
+ right_codon_start: rightCodonStart,
17
+ left_overhang: leftOverhang,
18
+ right_overhang: rightOverhang,
19
+ left_inside: leftInside,
20
+ right_inside: rightInside,
21
+ glyph
22
+ } = data
23
+ const leftOverhangRc = getComplementSequenceString(leftOverhang)
24
+ const rightOverhangRc = getComplementSequenceString(rightOverhang)
25
+ const leftInsideRc = getComplementSequenceString(leftInside)
26
+ const rightInsideRc = getComplementSequenceString(rightInside)
27
+ let leftTranslationOverhang = ''
28
+ let leftTranslationInside = ''
29
+ if (leftCodonStart) {
30
+ const triplets = (leftOverhang + leftInside).slice(leftCodonStart - 1).match(/.{3}/g)
31
+ const padding = ' '.repeat(leftCodonStart - 1)
32
+ const translationLeft = padding + tripletsToTranslation(triplets)
33
+ leftTranslationOverhang = translationLeft.slice(0, leftOverhang.length)
34
+ leftTranslationInside = translationLeft.slice(leftOverhang.length)
35
+ }
36
+ let rightTranslationOverhang = ''
37
+ let rightTranslationInside = ''
38
+ if (rightCodonStart) {
39
+ const triplets = (rightInside + rightOverhang).slice(rightCodonStart - 1).match(/.{3}/g)
40
+ const padding = ' '.repeat(rightCodonStart - 1)
41
+ const translationRight = padding + tripletsToTranslation(triplets)
42
+ rightTranslationInside = translationRight.slice(0, rightInside.length)
43
+ rightTranslationOverhang = translationRight.slice(rightInside.length)
44
+ }
45
+ return {
46
+ leftTranslationOverhang,
47
+ leftTranslationInside,
48
+ rightTranslationOverhang,
49
+ rightTranslationInside,
50
+ leftOverhangRc,
51
+ rightOverhangRc,
52
+ leftInsideRc,
53
+ rightInsideRc,
54
+ }
55
+ }
56
+
57
+
58
+ export function simplifyDigestFragment({cut1, cut2}) {
59
+ return {
60
+ left: {ovhg: cut1.overhangBps.toUpperCase(), forward: cut1.forward},
61
+ right: {ovhg: cut2.overhangBps.toUpperCase(), forward: cut2.forward},
62
+ };
63
+ };
64
+
65
+ export function reverseComplementSimplifiedDigestFragment({left, right, longestFeature}) {
66
+ return {
67
+ left: {ovhg: getReverseComplementSequenceString(right.ovhg), forward: !right.forward},
68
+ right: {ovhg: getReverseComplementSequenceString(left.ovhg), forward: !left.forward},
69
+ longestFeature
70
+ };
71
+ }
72
+
73
+ export function longestFeatureInDigestFragment(digestFragment, sequenceData) {
74
+ const {cut1, cut2} = digestFragment;
75
+ const leftEdge = cut1.overhangSize >=0 ? cut1.topSnipPosition : cut1.bottomSnipPosition;
76
+ const rightEdge = cut2.overhangSize >=0 ? cut2.bottomSnipPosition : cut2.topSnipPosition;
77
+ if (!sequenceData.features || sequenceData.features.length === 0) return null;
78
+ const featuresInside = sequenceData.features.filter(feature => isRangeWithinRange(feature, {start: leftEdge, end: rightEdge}, sequenceData.length));
79
+ return featuresInside.reduce((longest, feature) => {
80
+ if (!longest) return feature;
81
+ return feature.end - feature.start > longest.end - longest.start ? feature : longest;
82
+ }, null);
83
+ }
84
+
85
+ export function getSimplifiedDigestFragments(sequenceData, enzymes) {
86
+ const { sequence, circular } = sequenceData;
87
+
88
+ const digestFragments = getDigestFragmentsForRestrictionEnzymes(
89
+ sequence,
90
+ circular,
91
+ enzymes,
92
+ );
93
+
94
+ const longestFeatures = digestFragments.map(fragment => longestFeatureInDigestFragment(fragment, sequenceData));
95
+ const simplifiedDigestFragments = digestFragments.map(simplifyDigestFragment);
96
+ simplifiedDigestFragments.forEach((fragment, index) => {
97
+ fragment.longestFeature = longestFeatures[index];
98
+ });
99
+ const simplifiedDigestFragmentsRc = simplifiedDigestFragments.map(reverseComplementSimplifiedDigestFragment);
100
+ return simplifiedDigestFragments.concat(simplifiedDigestFragmentsRc);
101
+ }
102
+
103
+ export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
104
+ const simplifiedDigestFragments = getSimplifiedDigestFragments(sequenceData, enzymes);
105
+ const foundParts = [];
106
+ simplifiedDigestFragments
107
+ .filter(f => f.left.forward && !f.right.forward && graph.hasNode(f.left.ovhg) && graph.hasNode(f.right.ovhg))
108
+ .forEach(fragment => {
109
+ const paths = allSimplePaths(graph, fragment.left.ovhg, fragment.right.ovhg);
110
+ if (paths.length > 0) {
111
+ foundParts.push({left_overhang: fragment.left.ovhg, right_overhang: fragment.right.ovhg, longestFeature: fragment.longestFeature});
112
+ }
113
+ });
114
+ return foundParts;
115
+ }
116
+
117
+ export function arrayCombinations(sets) {
118
+ if (sets.length === 0) {
119
+ return null;
120
+ } else if (sets.length === 1) {
121
+ return sets[0].map((el) => [el]);
122
+ } else
123
+ return sets[0].flatMap((val) =>
124
+ arrayCombinations(sets.slice(1)).map((c) => [val].concat(c))
125
+ );
126
+ };
127
+
128
+ export function categoryFilter(category, categories, previousCategoryId) {
129
+ if (previousCategoryId === null) {
130
+ return category.left_overhang === categories[0].left_overhang
131
+ }
132
+ const previousCategory = categories.find((category) => category.id === previousCategoryId)
133
+ return previousCategory?.right_overhang === category.left_overhang
134
+ }