@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.
- package/CHANGELOG.md +36 -0
- package/package.json +5 -4
- package/src/components/MainSequenceEditor.jsx +2 -0
- package/src/components/assembler/Assembler.cy.jsx +364 -0
- package/src/components/assembler/Assembler.jsx +298 -206
- package/src/components/assembler/AssemblerPart.cy.jsx +52 -0
- package/src/components/assembler/AssemblerPart.jsx +51 -79
- package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +194 -0
- package/src/components/assembler/ExistingSyntaxDialog.jsx +65 -0
- package/src/components/assembler/PlasmidSyntaxTable.jsx +83 -0
- package/src/components/assembler/assembler_utils.js +134 -0
- package/src/components/assembler/assembler_utils.test.js +193 -0
- package/src/components/assembler/assembly_component.module.css +1 -1
- package/src/components/assembler/graph_utils.js +153 -0
- package/src/components/assembler/graph_utils.test.js +239 -0
- package/src/components/assembler/index.js +9 -0
- package/src/components/assembler/useAssembler.js +59 -22
- package/src/components/assembler/useCombinatorialAssembly.js +76 -0
- package/src/components/assembler/usePlasmidsLogic.js +82 -0
- package/src/components/eLabFTW/utils.js +0 -9
- package/src/components/index.js +2 -0
- package/src/components/navigation/SelectTemplateDialog.jsx +0 -1
- package/src/components/primers/DownloadPrimersButton.jsx +0 -1
- package/src/components/primers/PrimerList.jsx +4 -3
- package/src/components/primers/import_primers/ImportPrimersButton.jsx +0 -1
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +110 -91
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGatewayBP.jsx +1 -1
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +2 -2
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignHomologousRecombination.jsx +1 -1
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignRestriction.jsx +1 -1
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +1 -1
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +5 -22
- package/src/components/primers/primer_design/SequenceTabComponents/TabPannelSettings.jsx +6 -4
- package/src/hooks/useStoreEditor.js +4 -0
- package/src/version.js +1 -1
- package/vitest.config.js +2 -4
- package/src/components/DraggableDialogPaper.jsx +0 -16
- package/src/components/assembler/AssemblePartWidget.jsx +0 -252
- package/src/components/assembler/StopIcon.jsx +0 -34
- package/src/components/assembler/assembler_data2.json +0 -50
- 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 =
|
|
24
|
-
const {
|
|
25
|
-
|
|
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
|
-
<
|
|
59
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|