@opencloning/ui 1.3.1 → 1.3.3
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 +20 -0
- package/package.json +3 -3
- package/src/components/assembler/Assembler.cy.jsx +8 -2
- package/src/components/assembler/Assembler.jsx +19 -4
- package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +14 -6
- package/src/components/assembler/PlasmidSyntaxTable.jsx +1 -1
- package/src/components/assembler/assembler_utils.js +5 -0
- package/src/components/assembler/assembler_utils.test.js +40 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#607](https://github.com/manulera/OpenCloning_frontend/pull/607) [`1e4bfbf`](https://github.com/manulera/OpenCloning_frontend/commit/1e4bfbfb805e841d9a91cf650ebf632ac62b1248) Thanks [@manulera](https://github.com/manulera)! - Syntax validation and fix longestFeature when it's empty
|
|
8
|
+
|
|
9
|
+
- Updated dependencies []:
|
|
10
|
+
- @opencloning/store@1.3.3
|
|
11
|
+
- @opencloning/utils@1.3.3
|
|
12
|
+
|
|
13
|
+
## 1.3.2
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#605](https://github.com/manulera/OpenCloning_frontend/pull/605) [`ff60c18`](https://github.com/manulera/OpenCloning_frontend/commit/ff60c18c1500e8b9046f0810cbd69fe1a65c550c) Thanks [@manulera](https://github.com/manulera)! - Fix how syntaxes are imported to adapt to syntaxes repository change
|
|
18
|
+
|
|
19
|
+
- Updated dependencies []:
|
|
20
|
+
- @opencloning/store@1.3.2
|
|
21
|
+
- @opencloning/utils@1.3.2
|
|
22
|
+
|
|
3
23
|
## 1.3.1
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@emotion/styled": "^11.14.0",
|
|
26
26
|
"@mui/icons-material": "^5.15.17",
|
|
27
27
|
"@mui/material": "^5.15.17",
|
|
28
|
-
"@opencloning/store": "1.3.
|
|
29
|
-
"@opencloning/utils": "1.3.
|
|
28
|
+
"@opencloning/store": "1.3.3",
|
|
29
|
+
"@opencloning/utils": "1.3.3",
|
|
30
30
|
"@teselagen/bio-parsers": "^0.4.32",
|
|
31
31
|
"@teselagen/ove": "^0.8.30",
|
|
32
32
|
"@teselagen/range-utils": "^0.3.13",
|
|
@@ -253,7 +253,10 @@ describe('<UploadPlasmidsButton />', () => {
|
|
|
253
253
|
'cypress/test_files/syntax/pYTK002.gb',
|
|
254
254
|
'cypress/test_files/syntax/moclo_ytk_multi_part.gb',
|
|
255
255
|
'cypress/test_files/syntax/pYTK095.gb',
|
|
256
|
-
'cypress/test_files/sequencing/locus.gb'
|
|
256
|
+
'cypress/test_files/sequencing/locus.gb',
|
|
257
|
+
// This one just to verify that it works with no features
|
|
258
|
+
'cypress/test_files/syntax/pYTK002_no_features.gb'
|
|
259
|
+
],
|
|
257
260
|
{ force: true });
|
|
258
261
|
|
|
259
262
|
// Wait for the dialog to appear (indicating plasmids were processed)
|
|
@@ -287,6 +290,9 @@ describe('<UploadPlasmidsButton />', () => {
|
|
|
287
290
|
cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(3).should('contain', 'Spans multiple parts')
|
|
288
291
|
cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(4).should('contain', 'AmpR')
|
|
289
292
|
|
|
293
|
+
// No features
|
|
294
|
+
cy.get('[data-testid="valid-plasmids-box"] tr').eq(3).find('td').eq(4).should('contain', '-');
|
|
295
|
+
|
|
290
296
|
// Click the import button
|
|
291
297
|
cy.contains('button', 'Import valid plasmids').click();
|
|
292
298
|
|
|
@@ -298,7 +304,7 @@ describe('<UploadPlasmidsButton />', () => {
|
|
|
298
304
|
const firstCall = spy.getCall(0);
|
|
299
305
|
console.log('firstCall', firstCall.args);
|
|
300
306
|
cy.wrap(firstCall.args[0]).should('be.an', 'array');
|
|
301
|
-
cy.wrap(firstCall.args[0]).should('have.length',
|
|
307
|
+
cy.wrap(firstCall.args[0]).should('have.length', 3);
|
|
302
308
|
|
|
303
309
|
const firstPlasmid = firstCall.args[0][0];
|
|
304
310
|
|
|
@@ -16,6 +16,9 @@ import PlasmidSyntaxTable from './PlasmidSyntaxTable';
|
|
|
16
16
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
17
17
|
import error2String from '@opencloning/utils/error2String';
|
|
18
18
|
import { categoryFilter } from './assembler_utils';
|
|
19
|
+
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
20
|
+
import useHttpClient from '../../hooks/useHttpClient';
|
|
21
|
+
import useAlerts from '../../hooks/useAlerts';
|
|
19
22
|
|
|
20
23
|
const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
|
|
21
24
|
|
|
@@ -261,10 +264,22 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
|
|
|
261
264
|
|
|
262
265
|
function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
263
266
|
const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
267
|
+
const httpClient = useHttpClient();
|
|
268
|
+
const backendRoute = useBackendRoute();
|
|
269
|
+
const { addAlert } = useAlerts();
|
|
270
|
+
const onSyntaxSelect = React.useCallback(async (syntax, plasmids) => {
|
|
271
|
+
const url = backendRoute('validate_syntax');
|
|
272
|
+
try {
|
|
273
|
+
await httpClient.post(url, syntax);
|
|
274
|
+
setSyntax(syntax)
|
|
275
|
+
addPlasmids(plasmids)
|
|
276
|
+
} catch (error) {
|
|
277
|
+
addAlert({
|
|
278
|
+
message: error2String(error),
|
|
279
|
+
severity: 'error',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}, [setSyntax, addPlasmids, httpClient, backendRoute, addAlert])
|
|
268
283
|
return <>
|
|
269
284
|
<Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
|
|
270
285
|
{existingSyntaxDialogOpen && <ExistingSyntaxDialog onClose={() => setExistingSyntaxDialogOpen(false)} onSyntaxSelect={onSyntaxSelect}/>}
|
|
@@ -204,8 +204,12 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
204
204
|
],
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
const tempFile = {
|
|
208
|
+
contents: Cypress.Buffer.from(JSON.stringify(uploadedSyntaxData)),
|
|
209
|
+
fileName: 'syntax.json',
|
|
210
|
+
mimeType: 'text/plain',
|
|
211
|
+
lastModified: Date.now(),
|
|
212
|
+
}
|
|
209
213
|
|
|
210
214
|
cy.mount(
|
|
211
215
|
<ExistingSyntaxDialog
|
|
@@ -218,7 +222,7 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
218
222
|
cy.contains('Upload syntax from JSON file').should('exist');
|
|
219
223
|
|
|
220
224
|
// Upload the JSON file
|
|
221
|
-
cy.get('input[type="file"]').selectFile(
|
|
225
|
+
cy.get('input[type="file"]').selectFile(tempFile, { force: true });
|
|
222
226
|
|
|
223
227
|
cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', uploadedSyntaxData, []);
|
|
224
228
|
cy.get('@onCloseSpy').should('have.been.called');
|
|
@@ -228,8 +232,12 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
228
232
|
const onCloseSpy = cy.spy().as('onCloseSpy');
|
|
229
233
|
const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
|
|
230
234
|
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
const invalidFile = {
|
|
236
|
+
contents: Cypress.Buffer.from('{ invalid json }'),
|
|
237
|
+
fileName: 'invalid.json',
|
|
238
|
+
mimeType: 'text/plain',
|
|
239
|
+
lastModified: Date.now(),
|
|
240
|
+
}
|
|
233
241
|
|
|
234
242
|
cy.mount(
|
|
235
243
|
<ExistingSyntaxDialog
|
|
@@ -242,7 +250,7 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
242
250
|
cy.contains('Upload syntax from JSON file').should('exist');
|
|
243
251
|
|
|
244
252
|
// Upload invalid JSON
|
|
245
|
-
cy.get('input[type="file"]').selectFile(
|
|
253
|
+
cy.get('input[type="file"]').selectFile(invalidFile, { force: true });
|
|
246
254
|
|
|
247
255
|
cy.contains(/Failed to parse JSON file/).should('exist');
|
|
248
256
|
cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
|
|
@@ -22,7 +22,7 @@ function PlasmidRow({ plasmid }) {
|
|
|
22
22
|
backgroundColor: partInfo[0]?.color,
|
|
23
23
|
}
|
|
24
24
|
infoStr = partInfo[0] ? partInfo[0].name : 'Spans multiple parts';
|
|
25
|
-
longestFeatureStr = longestFeature ? longestFeature[0].name : '-';
|
|
25
|
+
longestFeatureStr = longestFeature[0] ? longestFeature[0].name : '-';
|
|
26
26
|
}
|
|
27
27
|
const multipleParts = partInfo.length > 1;
|
|
28
28
|
if (multipleParts) {
|
|
@@ -101,6 +101,11 @@ export function getSimplifiedDigestFragments(sequenceData, enzymes) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function assignSequenceToSyntaxPart(sequenceData, enzymes, graph) {
|
|
104
|
+
// Something that is important to understand here is the meaning of forward and reverse.
|
|
105
|
+
// It does not mean whether the overhang is 5' or 3', the value on the top strand is always
|
|
106
|
+
// used, which is convenient for classification within the syntax.
|
|
107
|
+
// Instead, forward means whether the recognition site was forward or reverse when producing that cut.
|
|
108
|
+
// see the test called "shows the meaning of forward and reverse" for more details.
|
|
104
109
|
const simplifiedDigestFragments = getSimplifiedDigestFragments(sequenceData, enzymes);
|
|
105
110
|
const foundParts = [];
|
|
106
111
|
simplifiedDigestFragments
|
|
@@ -28,6 +28,46 @@ describe('reverseComplementSimplifiedDigestFragment', () => {
|
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
it('shows the meaning of forward and reverse', () => {
|
|
32
|
+
const sequence = 'aaGGTCTCaTACTaaa'
|
|
33
|
+
const digestFragments = getDigestFragmentsForRestrictionEnzymes(
|
|
34
|
+
sequence,
|
|
35
|
+
false,
|
|
36
|
+
aliasedEnzymesByName["bsai"],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// This does not denote whether the overhang is 5' or 3',
|
|
40
|
+
// but the orientation of the recognition site.
|
|
41
|
+
expect(digestFragments[0].cut2.overhangBps).toBe('TACT');
|
|
42
|
+
expect(digestFragments[0].cut2.forward).toBe(true);
|
|
43
|
+
expect(digestFragments[1].cut1.overhangBps).toBe('TACT');
|
|
44
|
+
expect(digestFragments[1].cut1.forward).toBe(true);
|
|
45
|
+
|
|
46
|
+
// See how for a fragment with the same overhangs, the forward
|
|
47
|
+
// value is different
|
|
48
|
+
const sequence2 = 'aTACTcGAGACCaaa'
|
|
49
|
+
const digestFragments2 = getDigestFragmentsForRestrictionEnzymes(
|
|
50
|
+
sequence2,
|
|
51
|
+
false,
|
|
52
|
+
aliasedEnzymesByName["bsai"],
|
|
53
|
+
);
|
|
54
|
+
expect(digestFragments2[0].cut2.overhangBps).toBe('TACT');
|
|
55
|
+
expect(digestFragments2[0].cut2.forward).toBe(false);
|
|
56
|
+
expect(digestFragments2[1].cut1.overhangBps).toBe('TACT');
|
|
57
|
+
expect(digestFragments2[1].cut1.forward).toBe(false);
|
|
58
|
+
|
|
59
|
+
// For EcoRI, it's always forward
|
|
60
|
+
const sequenceEcoRI = 'aaaGAATTCaaaGAATTCaaaa'
|
|
61
|
+
const digestFragmentsEcoRI = getDigestFragmentsForRestrictionEnzymes(
|
|
62
|
+
sequenceEcoRI,
|
|
63
|
+
true,
|
|
64
|
+
aliasedEnzymesByName["ecori"],
|
|
65
|
+
);
|
|
66
|
+
expect(digestFragmentsEcoRI[0].cut2.overhangBps).toBe('AATT');
|
|
67
|
+
expect(digestFragmentsEcoRI[0].cut2.forward).toBe(true);
|
|
68
|
+
expect(digestFragmentsEcoRI[0].cut1.overhangBps).toBe('AATT');
|
|
69
|
+
expect(digestFragmentsEcoRI[0].cut1.forward).toBe(true);
|
|
70
|
+
});
|
|
31
71
|
|
|
32
72
|
describe('assignSequenceToSyntaxPart', () => {
|
|
33
73
|
it('works', () => {
|
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.3.
|
|
2
|
+
export const version = "1.3.3";
|