@opencloning/ui 1.5.5 → 1.6.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 +24 -0
- package/package.json +10 -3
- package/src/components/assembler/Assembler.jsx +1 -0
- package/src/components/dummy/index.js +1 -0
- package/src/components/sources/FinishedSource.jsx +46 -1
- package/src/components/sources/RecombinaseList.cy.jsx +103 -0
- package/src/components/sources/RecombinaseList.jsx +131 -0
- package/src/components/sources/Source.jsx +2 -0
- package/src/components/sources/SourceAssembly.jsx +42 -17
- package/src/components/sources/SourceTypeSelector.jsx +1 -0
- package/src/hooks/useDatabase.js +2 -17
- package/src/providers/DatabaseContext.jsx +15 -0
- package/src/version.js +1 -1
- package/src/components/eLabFTW/ELabFTWCategorySelect.cy.jsx +0 -86
- package/src/components/eLabFTW/ELabFTWCategorySelect.jsx +0 -43
- package/src/components/eLabFTW/ELabFTWFileSelect.cy.jsx +0 -43
- package/src/components/eLabFTW/ELabFTWFileSelect.jsx +0 -29
- package/src/components/eLabFTW/ELabFTWResourceSelect.cy.jsx +0 -107
- package/src/components/eLabFTW/ELabFTWResourceSelect.jsx +0 -23
- package/src/components/eLabFTW/GetPrimerComponent.cy.jsx +0 -261
- package/src/components/eLabFTW/GetPrimerComponent.jsx +0 -55
- package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.cy.jsx +0 -184
- package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.jsx +0 -62
- package/src/components/eLabFTW/LoadHistoryComponent.cy.jsx +0 -235
- package/src/components/eLabFTW/LoadHistoryComponent.jsx +0 -51
- package/src/components/eLabFTW/PrimersNotInDatabaseComponent.cy.jsx +0 -159
- package/src/components/eLabFTW/PrimersNotInDatabaseComponent.jsx +0 -54
- package/src/components/eLabFTW/SubmitToDatabaseComponent.cy.jsx +0 -185
- package/src/components/eLabFTW/SubmitToDatabaseComponent.jsx +0 -51
- package/src/components/eLabFTW/common.js +0 -26
- package/src/components/eLabFTW/eLabFTWInterface.js +0 -293
- package/src/components/eLabFTW/eLabFTWInterface.test.js +0 -839
- package/src/components/eLabFTW/envValues.js +0 -7
- package/src/components/eLabFTW/utils.js +0 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#668](https://github.com/manulera/OpenCloning_frontend/pull/668) [`dd0df39`](https://github.com/manulera/OpenCloning_frontend/commit/dd0df39c5fbf65feaab09f90d123e3776efed6bd) Thanks [@manulera](https://github.com/manulera)! - Moved eLabFTW interface into a separate package
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#668](https://github.com/manulera/OpenCloning_frontend/pull/668) [`dd0df39`](https://github.com/manulera/OpenCloning_frontend/commit/dd0df39c5fbf65feaab09f90d123e3776efed6bd) Thanks [@manulera](https://github.com/manulera)! - Added LICENSE to package.json
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [[`dd0df39`](https://github.com/manulera/OpenCloning_frontend/commit/dd0df39c5fbf65feaab09f90d123e3776efed6bd)]:
|
|
14
|
+
- @opencloning/store@1.6.0
|
|
15
|
+
- @opencloning/utils@1.6.0
|
|
16
|
+
|
|
17
|
+
## 1.5.6
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#665](https://github.com/manulera/OpenCloning_frontend/pull/665) [`d3477f1`](https://github.com/manulera/OpenCloning_frontend/commit/d3477f1354b92b61cb8f4299b4a9648b67f26ab0) Thanks [@manulera](https://github.com/manulera)! - Add support for user-defined recombinases
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [[`d3477f1`](https://github.com/manulera/OpenCloning_frontend/commit/d3477f1354b92b61cb8f4299b4a9648b67f26ab0)]:
|
|
24
|
+
- @opencloning/utils@1.5.6
|
|
25
|
+
- @opencloning/store@1.5.6
|
|
26
|
+
|
|
3
27
|
## 1.5.5
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
+
"license": "MIT",
|
|
2
3
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"main": "./src/index.js",
|
|
6
7
|
"scripts": {
|
|
@@ -11,7 +12,13 @@
|
|
|
11
12
|
".": "./src/index.js",
|
|
12
13
|
"./components": "./src/components/index.js",
|
|
13
14
|
"./components/assembler": "./src/components/assembler/index.js",
|
|
15
|
+
"./components/dummy": "./src/components/dummy/index.js",
|
|
16
|
+
"./components/form/GetRequestMultiSelect": "./src/components/form/GetRequestMultiSelect.jsx",
|
|
17
|
+
"./components/form/RetryAlert": "./src/components/form/RetryAlert.jsx",
|
|
18
|
+
"./components/form/RequestStatusWrapper": "./src/components/form/RequestStatusWrapper.jsx",
|
|
19
|
+
"./components/form/PostRequestSelect": "./src/components/form/PostRequestSelect.jsx",
|
|
14
20
|
"./providers/ConfigProvider": "./src/providers/index.js",
|
|
21
|
+
"./providers/DatabaseContext": "./src/providers/DatabaseContext.jsx",
|
|
15
22
|
"./hooks/useConfig": "./src/hooks/useConfig.js",
|
|
16
23
|
"./standalone": "./src/StandAloneOpenCloning.js"
|
|
17
24
|
},
|
|
@@ -26,8 +33,8 @@
|
|
|
26
33
|
"@mui/icons-material": "^5.15.17",
|
|
27
34
|
"@mui/material": "^5.15.17",
|
|
28
35
|
"@mui/x-data-grid": "^8.25.0",
|
|
29
|
-
"@opencloning/store": "1.
|
|
30
|
-
"@opencloning/utils": "1.
|
|
36
|
+
"@opencloning/store": "1.6.0",
|
|
37
|
+
"@opencloning/utils": "1.6.0",
|
|
31
38
|
"@teselagen/bio-parsers": "^0.4.34",
|
|
32
39
|
"@teselagen/ove": "^0.8.34",
|
|
33
40
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './DummyInterface.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useSelector } from 'react-redux';
|
|
3
|
-
import { Alert, Button, Dialog, DialogContent } from '@mui/material';
|
|
3
|
+
import { Alert, Box, Button, Dialog, DialogContent, DialogTitle, Divider, Typography } from '@mui/material';
|
|
4
4
|
import { isEqual } from 'lodash-es';
|
|
5
5
|
import { enzymesInRestrictionEnzymeDigestionSource } from '@opencloning/utils/sourceFunctions';
|
|
6
6
|
import PlannotateAnnotationReport from '../annotation/PlannotateAnnotationReport';
|
|
@@ -163,6 +163,50 @@ function GatewayMessage({ source }) {
|
|
|
163
163
|
return `Gateway ${source.reaction_type} reaction`;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
function RecombinaseMessage({ source }) {
|
|
167
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
168
|
+
const recombinases = source.recombinases || [];
|
|
169
|
+
return (
|
|
170
|
+
<>
|
|
171
|
+
<Box>
|
|
172
|
+
Recombinase reaction
|
|
173
|
+
</Box>
|
|
174
|
+
<Button onClick={() => setDialogOpen(true)}>
|
|
175
|
+
See info
|
|
176
|
+
</Button>
|
|
177
|
+
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
|
178
|
+
<DialogTitle>Recombinase details</DialogTitle>
|
|
179
|
+
<DialogContent dividers>
|
|
180
|
+
{recombinases.map((r, i) => (
|
|
181
|
+
<Box key={`${r.site1}-${r.site2}-${i}`}>
|
|
182
|
+
{i > 0 && <Divider sx={{ my: 2 }} />}
|
|
183
|
+
<Box sx={{ py: 1 }}>
|
|
184
|
+
{r.name && (
|
|
185
|
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
|
186
|
+
{r.name}
|
|
187
|
+
</Typography>
|
|
188
|
+
)}
|
|
189
|
+
<Typography variant="body2" component="div" sx={{ fontFamily: 'monospace', fontSize: '0.9em' }}>
|
|
190
|
+
<Box component="span" sx={{ color: 'text.secondary', fontFamily: 'inherit', fontWeight: 500 }}>
|
|
191
|
+
{r.site1_name || 'site1'}:
|
|
192
|
+
</Box>
|
|
193
|
+
{' '}{r.site1}
|
|
194
|
+
</Typography>
|
|
195
|
+
<Typography variant="body2" component="div" sx={{ fontFamily: 'monospace', fontSize: '0.9em', mt: 0.5 }}>
|
|
196
|
+
<Box component="span" sx={{ color: 'text.secondary', fontFamily: 'inherit', fontWeight: 500 }}>
|
|
197
|
+
{r.site2_name || 'site2'}:
|
|
198
|
+
</Box>
|
|
199
|
+
{' '}{r.site2}
|
|
200
|
+
</Typography>
|
|
201
|
+
</Box>
|
|
202
|
+
</Box>
|
|
203
|
+
))}
|
|
204
|
+
</DialogContent>
|
|
205
|
+
</Dialog>
|
|
206
|
+
</>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
166
210
|
function PlannotateAnnotationMessage({ source }) {
|
|
167
211
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
168
212
|
return (
|
|
@@ -338,6 +382,7 @@ function FinishedSource({ sourceId }) {
|
|
|
338
382
|
case 'OverlapExtensionPCRLigationSource': message = 'Overlap extension PCR ligation'; break;
|
|
339
383
|
case 'InFusionSource': message = 'In-Fusion assembly of fragments'; break;
|
|
340
384
|
case 'CreLoxRecombinationSource': message = 'Cre/Lox recombination'; break;
|
|
385
|
+
case 'RecombinaseSource': message = <RecombinaseMessage source={source} />; break;
|
|
341
386
|
case 'InVivoAssemblySource': message = 'In vivo assembly of fragments'; break;
|
|
342
387
|
case 'RestrictionEnzymeDigestionSource': {
|
|
343
388
|
const uniqueEnzymes = enzymesInRestrictionEnzymeDigestionSource(source);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import RecombinaseList from './RecombinaseList';
|
|
3
|
+
import { clearInputValue, setInputValue } from '../../../../../cypress/e2e/common_functions';
|
|
4
|
+
|
|
5
|
+
const initialRecombinases = [
|
|
6
|
+
{ site1: 'AAaaTTC', site2: 'CCaaGC', site1_name: 'attB', site2_name: 'attP' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const twoRecombinases = [
|
|
10
|
+
...initialRecombinases,
|
|
11
|
+
{ site1: 'ATGCCCTAAaaCT', site2: 'CAaaTTTTTTTCCCT', site1_name: 'attB', site2_name: 'attP' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe('<RecombinaseList />', () => {
|
|
15
|
+
it('enforces site pattern constraints and does not add invalid sites', () => {
|
|
16
|
+
const setRecombinasesSpy = cy.spy().as('setRecombinasesSpy');
|
|
17
|
+
cy.mount(<RecombinaseList recombinases={[]} setRecombinases={setRecombinasesSpy} />);
|
|
18
|
+
|
|
19
|
+
// Invalid: all uppercase (no lowercase region)
|
|
20
|
+
setInputValue('Site 1', 'AAAA', 'body');
|
|
21
|
+
setInputValue('Site 2', 'CCaaGC', 'body');
|
|
22
|
+
cy.get('button').contains('Add recombinase').click();
|
|
23
|
+
cy.get('@setRecombinasesSpy').should('not.have.been.called');
|
|
24
|
+
cy.contains('Sites must match').should('exist');
|
|
25
|
+
|
|
26
|
+
// Invalid: all lowercase
|
|
27
|
+
setInputValue('Site 1', 'aaaa', 'body');
|
|
28
|
+
setInputValue('Site 2', 'CCaaGC', 'body');
|
|
29
|
+
cy.get('button').contains('Add recombinase').click();
|
|
30
|
+
cy.get('@setRecombinasesSpy').should('not.have.been.called');
|
|
31
|
+
|
|
32
|
+
// Invalid: empty site
|
|
33
|
+
clearInputValue('Site 1', 'body');
|
|
34
|
+
setInputValue('Site 2', 'CCaaGC', 'body');
|
|
35
|
+
cy.get('button').contains('Add recombinase').click();
|
|
36
|
+
cy.get('@setRecombinasesSpy').should('not.have.been.called');
|
|
37
|
+
cy.contains('Required').should('exist');
|
|
38
|
+
|
|
39
|
+
// Valid: add with correct pattern
|
|
40
|
+
setInputValue('Site 1', 'AAaaTTC', 'body');
|
|
41
|
+
setInputValue('Site 2', 'CCaaGC', 'body');
|
|
42
|
+
cy.get('button').contains('Add recombinase').click();
|
|
43
|
+
cy.get('@setRecombinasesSpy').should('have.been.calledOnce');
|
|
44
|
+
cy.get('@setRecombinasesSpy').should('have.been.calledWith', [{
|
|
45
|
+
site1: 'AAaaTTC',
|
|
46
|
+
site2: 'CCaaGC',
|
|
47
|
+
site1_name: 'attB',
|
|
48
|
+
site2_name: 'attP',
|
|
49
|
+
}]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('displays initial recombinases and appends when adding (does not overwrite)', () => {
|
|
53
|
+
const setRecombinasesSpy = cy.spy().as('setRecombinasesSpy');
|
|
54
|
+
cy.mount(
|
|
55
|
+
<RecombinaseList recombinases={initialRecombinases} setRecombinases={setRecombinasesSpy} />
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
cy.get('.MuiChip-root').should('have.length', 1);
|
|
59
|
+
cy.get('.MuiChip-root').contains('AAaaTTC');
|
|
60
|
+
|
|
61
|
+
setInputValue('Site 1', 'ATGCCCTAAaaCT', 'body');
|
|
62
|
+
setInputValue('Site 2', 'CAaaTTTTTTTCCCT', 'body');
|
|
63
|
+
cy.get('button').contains('Add recombinase').click();
|
|
64
|
+
|
|
65
|
+
cy.get('@setRecombinasesSpy').should('have.been.calledWith', [
|
|
66
|
+
{ site1: 'AAaaTTC', site2: 'CCaaGC', site1_name: 'attB', site2_name: 'attP' },
|
|
67
|
+
{ site1: 'ATGCCCTAAaaCT', site2: 'CAaaTTTTTTTCCCT', site1_name: 'attB', site2_name: 'attP' },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('calls setRecombinases with filtered array when deleting a chip', () => {
|
|
72
|
+
const setRecombinasesSpy = cy.spy().as('setRecombinasesSpy');
|
|
73
|
+
cy.mount(
|
|
74
|
+
<RecombinaseList recombinases={twoRecombinases} setRecombinases={setRecombinasesSpy} />
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
cy.get('.MuiChip-root').should('have.length', 2);
|
|
78
|
+
cy.get('.MuiChip-root').first().find('.MuiChip-deleteIcon').click();
|
|
79
|
+
cy.get('@setRecombinasesSpy').should('have.been.calledWith', [
|
|
80
|
+
{ site1: 'ATGCCCTAAaaCT', site2: 'CAaaTTTTTTTCCCT', site1_name: 'attB', site2_name: 'attP' },
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('includes optional name and custom site names when provided', () => {
|
|
85
|
+
const setRecombinasesSpy = cy.spy().as('setRecombinasesSpy');
|
|
86
|
+
cy.mount(<RecombinaseList recombinases={[]} setRecombinases={setRecombinasesSpy} />);
|
|
87
|
+
|
|
88
|
+
setInputValue('Name (optional)', 'MyRecombinase', 'body');
|
|
89
|
+
setInputValue('Site 1', 'AAaaTTC', 'body');
|
|
90
|
+
setInputValue('Site 1 name', 'loxP', 'body');
|
|
91
|
+
setInputValue('Site 2', 'CCaaGC', 'body');
|
|
92
|
+
setInputValue('Site 2 name', 'loxR', 'body');
|
|
93
|
+
cy.get('button').contains('Add recombinase').click();
|
|
94
|
+
|
|
95
|
+
cy.get('@setRecombinasesSpy').should('have.been.calledWith', [{
|
|
96
|
+
name: 'MyRecombinase',
|
|
97
|
+
site1: 'AAaaTTC',
|
|
98
|
+
site2: 'CCaaGC',
|
|
99
|
+
site1_name: 'loxP',
|
|
100
|
+
site2_name: 'loxR',
|
|
101
|
+
}]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Button, Chip, FormControl, TextField } from '@mui/material';
|
|
3
|
+
import LabelWithTooltip from '../form/LabelWithTooltip';
|
|
4
|
+
|
|
5
|
+
const SITE_PATTERN = /^[A-Z]+[a-z]+[A-Z]+$/;
|
|
6
|
+
const DEFAULT_SITE1_NAME = 'attB';
|
|
7
|
+
const DEFAULT_SITE2_NAME = 'attP';
|
|
8
|
+
const SITE_PATTERN_HELP = `
|
|
9
|
+
Sites must match: uppercase-lowercase-uppercase (e.g. AAaaTTC, ATGCCCTAAaaCT).
|
|
10
|
+
Lowercase letters represent the homology region where sequences will be joined,
|
|
11
|
+
so the lowercase parts must match in both sites.
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
function RecombinaseList({ recombinases, setRecombinases }) {
|
|
15
|
+
const [name, setName] = React.useState('');
|
|
16
|
+
const [site1, setSite1] = React.useState('');
|
|
17
|
+
const [site2, setSite2] = React.useState('');
|
|
18
|
+
const [site1Name, setSite1Name] = React.useState(DEFAULT_SITE1_NAME);
|
|
19
|
+
const [site2Name, setSite2Name] = React.useState(DEFAULT_SITE2_NAME);
|
|
20
|
+
const [site1Error, setSite1Error] = React.useState('');
|
|
21
|
+
const [site2Error, setSite2Error] = React.useState('');
|
|
22
|
+
|
|
23
|
+
const validateSite = (value) => {
|
|
24
|
+
if (!value.trim()) return 'Required';
|
|
25
|
+
if (!SITE_PATTERN.test(value)) return SITE_PATTERN_HELP;
|
|
26
|
+
return '';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const onAdd = () => {
|
|
30
|
+
const err1 = validateSite(site1);
|
|
31
|
+
const err2 = validateSite(site2);
|
|
32
|
+
setSite1Error(err1);
|
|
33
|
+
setSite2Error(err2);
|
|
34
|
+
if (err1 || err2) return;
|
|
35
|
+
|
|
36
|
+
const rec = {
|
|
37
|
+
site1: site1.trim(),
|
|
38
|
+
site2: site2.trim(),
|
|
39
|
+
site1_name: site1Name.trim() || DEFAULT_SITE1_NAME,
|
|
40
|
+
site2_name: site2Name.trim() || DEFAULT_SITE2_NAME,
|
|
41
|
+
...(name.trim() && { name: name.trim() }),
|
|
42
|
+
};
|
|
43
|
+
setRecombinases([...recombinases, rec]);
|
|
44
|
+
setName('');
|
|
45
|
+
setSite1('');
|
|
46
|
+
setSite2('');
|
|
47
|
+
setSite1Name(DEFAULT_SITE1_NAME);
|
|
48
|
+
setSite2Name(DEFAULT_SITE2_NAME);
|
|
49
|
+
setSite1Error('');
|
|
50
|
+
setSite2Error('');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onRemove = (index) => {
|
|
54
|
+
setRecombinases(recombinases.filter((_, i) => i !== index));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<FormControl fullWidth sx={{ mt: 1 }}>
|
|
60
|
+
<LabelWithTooltip
|
|
61
|
+
label="Recombinase recognition sites"
|
|
62
|
+
tooltip={SITE_PATTERN_HELP}
|
|
63
|
+
/>
|
|
64
|
+
</FormControl>
|
|
65
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
|
|
66
|
+
<TextField
|
|
67
|
+
fullWidth
|
|
68
|
+
label="Name (optional)"
|
|
69
|
+
value={name}
|
|
70
|
+
onChange={(e) => setName(e.target.value)}
|
|
71
|
+
size="small"
|
|
72
|
+
/>
|
|
73
|
+
<TextField
|
|
74
|
+
label="Site 1"
|
|
75
|
+
fullWidth
|
|
76
|
+
value={site1}
|
|
77
|
+
onChange={(e) => { setSite1(e.target.value); setSite1Error(''); }}
|
|
78
|
+
error={Boolean(site1Error)}
|
|
79
|
+
helperText={site1Error}
|
|
80
|
+
size="small"
|
|
81
|
+
placeholder="e.g. AAaaTTC"
|
|
82
|
+
sx={{ '& input': { fontFamily: 'monospace' } }}
|
|
83
|
+
/>
|
|
84
|
+
<TextField
|
|
85
|
+
label="Site 1 name"
|
|
86
|
+
fullWidth
|
|
87
|
+
value={site1Name}
|
|
88
|
+
onChange={(e) => setSite1Name(e.target.value)}
|
|
89
|
+
size="small"
|
|
90
|
+
placeholder={DEFAULT_SITE1_NAME}
|
|
91
|
+
/>
|
|
92
|
+
<TextField
|
|
93
|
+
label="Site 2"
|
|
94
|
+
fullWidth
|
|
95
|
+
value={site2}
|
|
96
|
+
onChange={(e) => { setSite2(e.target.value); setSite2Error(''); }}
|
|
97
|
+
error={Boolean(site2Error)}
|
|
98
|
+
helperText={site2Error}
|
|
99
|
+
size="small"
|
|
100
|
+
placeholder="e.g. CCaaGC"
|
|
101
|
+
sx={{ '& input': { fontFamily: 'monospace' } }}
|
|
102
|
+
/>
|
|
103
|
+
<TextField
|
|
104
|
+
label="Site 2 name"
|
|
105
|
+
fullWidth
|
|
106
|
+
value={site2Name}
|
|
107
|
+
onChange={(e) => setSite2Name(e.target.value)}
|
|
108
|
+
size="small"
|
|
109
|
+
placeholder={DEFAULT_SITE2_NAME}
|
|
110
|
+
/>
|
|
111
|
+
<Button variant="outlined" onClick={onAdd} size="small" sx={{ alignSelf: 'flex-start' }}>
|
|
112
|
+
Add recombinase
|
|
113
|
+
</Button>
|
|
114
|
+
</Box>
|
|
115
|
+
{recombinases.length > 0 && (
|
|
116
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 1 }}>
|
|
117
|
+
{recombinases.map((rec, i) => (
|
|
118
|
+
<Chip
|
|
119
|
+
key={`${rec.site1}-${rec.site2}-${i}`}
|
|
120
|
+
label={rec.name || `${rec.site1} / ${rec.site2}`}
|
|
121
|
+
onDelete={() => onRemove(i)}
|
|
122
|
+
size="small"
|
|
123
|
+
/>
|
|
124
|
+
))}
|
|
125
|
+
</Box>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default React.memo(RecombinaseList);
|
|
@@ -80,6 +80,8 @@ function Source({ sourceId }) {
|
|
|
80
80
|
specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
|
|
81
81
|
case 'CreLoxRecombinationSource':
|
|
82
82
|
specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
|
|
83
|
+
case 'RecombinaseSource':
|
|
84
|
+
specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
|
|
83
85
|
case 'HomologousRecombinationSource':
|
|
84
86
|
specificSource = <SourceHomologousRecombination {...{ source, requestStatus, sendPostRequest }} />; break;
|
|
85
87
|
case 'PCRSource':
|
|
@@ -8,6 +8,7 @@ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
|
8
8
|
import { classNameToEndPointMap } from '@opencloning/utils/sourceFunctions';
|
|
9
9
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
10
10
|
import LabelWithTooltip from '../form/LabelWithTooltip';
|
|
11
|
+
import RecombinaseList from './RecombinaseList';
|
|
11
12
|
|
|
12
13
|
const helpSingleSite = 'Even if input sequences contain multiple att sites '
|
|
13
14
|
+ '(typically 2), a product could be generated where only one site recombines. '
|
|
@@ -25,6 +26,8 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
25
26
|
const [bluntLigation, setBluntLigation] = React.useState(false);
|
|
26
27
|
const [gatewaySettings, setGatewaySettings] = React.useState({ greedy: false, reactionType: null, onlyMultiSite: true });
|
|
27
28
|
const [enzymes, setEnzymes] = React.useState([]);
|
|
29
|
+
const [recombinases, setRecombinases] = React.useState([]);
|
|
30
|
+
const [reverseRecombinase, setReverseRecombinase] = React.useState(false);
|
|
28
31
|
|
|
29
32
|
const dispatch = useDispatch();
|
|
30
33
|
|
|
@@ -45,6 +48,7 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
45
48
|
const preventSubmit = (
|
|
46
49
|
(assemblyType === 'RestrictionAndLigationSource' && enzymes.length === 0)
|
|
47
50
|
|| (assemblyType === 'GatewaySource' && gatewaySettings.reactionType === null)
|
|
51
|
+
|| (assemblyType === 'RecombinaseSource' && recombinases.length === 0)
|
|
48
52
|
|| inputContainsTemplates
|
|
49
53
|
);
|
|
50
54
|
|
|
@@ -90,6 +94,11 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
90
94
|
sendPostRequest({ endpoint: 'gateway', requestData, config, source });
|
|
91
95
|
} else if (assemblyType === 'CreLoxRecombinationSource') {
|
|
92
96
|
sendPostRequest({ endpoint: 'cre_lox_recombination', requestData, source });
|
|
97
|
+
} else if (assemblyType === 'RecombinaseSource') {
|
|
98
|
+
if (recombinases.length === 0) { return; }
|
|
99
|
+
requestData.source.recombinases = recombinases;
|
|
100
|
+
const config = { params: { reverse_recombinase: reverseRecombinase } };
|
|
101
|
+
sendPostRequest({ endpoint: 'recombinase', requestData, config, source });
|
|
93
102
|
} else {
|
|
94
103
|
const config = { params: {
|
|
95
104
|
allow_partial_overlap: allowPartialOverlap,
|
|
@@ -120,22 +129,38 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
120
129
|
</FormControl>
|
|
121
130
|
{ ['GibsonAssemblySource', 'OverlapExtensionPCRLigationSource', 'InFusionSource', 'InVivoAssemblySource'].includes(assemblyType) && (
|
|
122
131
|
// I don't really understand why fullWidth is required here
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
<FormControl fullWidth>
|
|
133
|
+
<TextField
|
|
134
|
+
label="Minimal homology length"
|
|
135
|
+
value={minimalHomology}
|
|
136
|
+
onChange={(e) => { setMinimalHomology(e.target.value); }}
|
|
137
|
+
type="number"
|
|
138
|
+
defaultValue={20}
|
|
139
|
+
InputProps={{
|
|
140
|
+
endAdornment: <InputAdornment position="end">bp</InputAdornment>,
|
|
141
|
+
sx: { '& input': { textAlign: 'center' } },
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
</FormControl>
|
|
136
145
|
)}
|
|
137
146
|
{ (assemblyType === 'RestrictionAndLigationSource') && (
|
|
138
|
-
|
|
147
|
+
<EnzymeMultiSelect setEnzymes={setEnzymes} />
|
|
148
|
+
)}
|
|
149
|
+
{ (assemblyType === 'RecombinaseSource') && (
|
|
150
|
+
<>
|
|
151
|
+
<RecombinaseList recombinases={recombinases} setRecombinases={setRecombinases} />
|
|
152
|
+
<FormControl fullWidth style={{ textAlign: 'left' }}>
|
|
153
|
+
<FormControlLabel
|
|
154
|
+
control={<Checkbox checked={reverseRecombinase} onChange={() => setReverseRecombinase(!reverseRecombinase)} />}
|
|
155
|
+
label={(
|
|
156
|
+
<LabelWithTooltip
|
|
157
|
+
label="Reverse reaction"
|
|
158
|
+
tooltip="Include the reverse reaction of each recombinase (e.g. excision in addition to integration)"
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
/>
|
|
162
|
+
</FormControl>
|
|
163
|
+
</>
|
|
139
164
|
)}
|
|
140
165
|
{ (assemblyType === 'GatewaySource') && (
|
|
141
166
|
<>
|
|
@@ -181,9 +206,9 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
181
206
|
</FormControl>
|
|
182
207
|
)}
|
|
183
208
|
{ (assemblyType === 'LigationSource') && (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
<FormControl fullWidth style={{ textAlign: 'left' }}>
|
|
210
|
+
<FormControlLabel control={<Checkbox checked={bluntLigation} onChange={flipBluntLigation} />} label="Blunt ligation" />
|
|
211
|
+
</FormControl>
|
|
187
212
|
)}
|
|
188
213
|
|
|
189
214
|
{!preventSubmit && (
|
|
@@ -64,6 +64,7 @@ function SourceTypeSelector({ source }) {
|
|
|
64
64
|
options.push(<MenuItem key="InVivoAssemblySource" value="InVivoAssemblySource">In vivo assembly</MenuItem>);
|
|
65
65
|
options.push(<MenuItem key="GatewaySource" value="GatewaySource">Gateway</MenuItem>);
|
|
66
66
|
options.push(<MenuItem key="CreLoxRecombinationSource" value="CreLoxRecombinationSource">Cre/Lox recombination</MenuItem>);
|
|
67
|
+
options.push(<MenuItem key="RecombinaseSource" value="RecombinaseSource">Recombinase</MenuItem>);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// Sort options by text content
|
package/src/hooks/useDatabase.js
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useConfig } from './useConfig';
|
|
3
|
-
import eLabFTWInterface from '../components/eLabFTW/eLabFTWInterface';
|
|
4
|
-
import dummyInterface from '../components/dummy/DummyInterface';
|
|
1
|
+
import { useDatabase } from '../providers/DatabaseContext';
|
|
5
2
|
|
|
6
|
-
export default
|
|
7
|
-
const { database: databaseName } = useConfig();
|
|
8
|
-
|
|
9
|
-
return React.useMemo(() => {
|
|
10
|
-
if (databaseName === 'elabftw') {
|
|
11
|
-
return eLabFTWInterface;
|
|
12
|
-
}
|
|
13
|
-
if (databaseName === 'dummy') {
|
|
14
|
-
return dummyInterface;
|
|
15
|
-
}
|
|
16
|
-
return null;
|
|
17
|
-
}, [databaseName]);
|
|
18
|
-
}
|
|
3
|
+
export default useDatabase;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export const DatabaseContext = createContext(null);
|
|
4
|
+
|
|
5
|
+
export function DatabaseProvider({ value, children }) {
|
|
6
|
+
return (
|
|
7
|
+
<DatabaseContext.Provider value={value}>
|
|
8
|
+
{children}
|
|
9
|
+
</DatabaseContext.Provider>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useDatabase() {
|
|
14
|
+
return useContext(DatabaseContext);
|
|
15
|
+
}
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Version placeholder - replaced at publish time via prepack script
|
|
2
|
-
export const version = "1.
|
|
2
|
+
export const version = "1.6.0";
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import ELabFTWCategorySelect from './ELabFTWCategorySelect';
|
|
3
|
-
import { eLabFTWHttpClient } from './common';
|
|
4
|
-
|
|
5
|
-
describe('<ELabFTWCategorySelect />', () => {
|
|
6
|
-
it('Allows to retry if the request fails', () => {
|
|
7
|
-
cy.stub(eLabFTWHttpClient, 'get').withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).as('eLabFTWHttpClientSpy');
|
|
8
|
-
cy.mount(<ELabFTWCategorySelect fullWidth />);
|
|
9
|
-
cy.get('@eLabFTWHttpClientSpy.all').should('have.callCount', 1);
|
|
10
|
-
cy.get('button').contains('Retry').click();
|
|
11
|
-
cy.get('@eLabFTWHttpClientSpy.all').should('have.callCount', 2);
|
|
12
|
-
});
|
|
13
|
-
it('shows the right options for eLabFTW version 50300', () => {
|
|
14
|
-
const setCategorySpy = cy.spy().as('setCategorySpy');
|
|
15
|
-
cy.stub(eLabFTWHttpClient, 'get').withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
|
|
16
|
-
data: {
|
|
17
|
-
elabftw_version_int: 50300,
|
|
18
|
-
},
|
|
19
|
-
}).withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
|
|
20
|
-
data: [
|
|
21
|
-
{ id: 1, title: 'Category 1' },
|
|
22
|
-
{ id: 2, title: 'Category 2' },
|
|
23
|
-
],
|
|
24
|
-
});
|
|
25
|
-
cy.mount(<ELabFTWCategorySelect fullWidth setCategory={setCategorySpy} />);
|
|
26
|
-
cy.get('.MuiAutocomplete-root').click();
|
|
27
|
-
cy.get('li').contains('Category 1').should('exist');
|
|
28
|
-
cy.get('li').contains('Category 2').should('exist');
|
|
29
|
-
cy.get('li').contains('Category 1').click();
|
|
30
|
-
cy.get('@setCategorySpy').should('have.been.calledWith', { id: 1, title: 'Category 1' });
|
|
31
|
-
});
|
|
32
|
-
it('shows the right options', () => {
|
|
33
|
-
const setCategorySpy = cy.spy().as('setCategorySpy');
|
|
34
|
-
cy.stub(eLabFTWHttpClient, 'get')
|
|
35
|
-
.withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
|
|
36
|
-
data: [
|
|
37
|
-
{ id: 1, title: 'Category 1' },
|
|
38
|
-
{ id: 2, title: 'Category 2' },
|
|
39
|
-
],
|
|
40
|
-
})
|
|
41
|
-
.withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
|
|
42
|
-
data: {
|
|
43
|
-
elabftw_version_int: 50200,
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
cy.mount(<ELabFTWCategorySelect fullWidth setCategory={setCategorySpy} />);
|
|
47
|
-
cy.get('.MuiAutocomplete-root').click();
|
|
48
|
-
cy.get('li').contains('Category 1').should('exist');
|
|
49
|
-
cy.get('li').contains('Category 2').should('exist');
|
|
50
|
-
cy.get('li').contains('Category 1').click();
|
|
51
|
-
cy.get('@setCategorySpy').should('have.been.calledWith', { id: 1, title: 'Category 1' });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('shows empty options if no categories are found', () => {
|
|
55
|
-
cy.mount(<ELabFTWCategorySelect fullWidth />);
|
|
56
|
-
cy.stub(eLabFTWHttpClient, 'get')
|
|
57
|
-
.withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
|
|
58
|
-
data: [],
|
|
59
|
-
})
|
|
60
|
-
.withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
|
|
61
|
-
data: {
|
|
62
|
-
elabftw_version_int: 50200,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
cy.get('.MuiAutocomplete-root').click();
|
|
66
|
-
cy.get('li').should('not.exist');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('shows an error message if the request fails and can retry', () => {
|
|
70
|
-
cy.mount(<ELabFTWCategorySelect fullWidth />);
|
|
71
|
-
cy.stub(eLabFTWHttpClient, 'get')
|
|
72
|
-
.withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } })
|
|
73
|
-
.resolves({
|
|
74
|
-
data: {
|
|
75
|
-
elabftw_version_int: 50300,
|
|
76
|
-
},
|
|
77
|
-
}).as('eLabFTWHttpClientSpyInfo')
|
|
78
|
-
.withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
|
|
79
|
-
.rejects(new Error('Connection error')).as('eLabFTWHttpClientSpy');
|
|
80
|
-
cy.get('.MuiAlert-message').should('contain', 'Could not retrieve categories');
|
|
81
|
-
// Clicking the retry button makes the request again
|
|
82
|
-
cy.get('button').contains('Retry').click();
|
|
83
|
-
cy.get('@eLabFTWHttpClientSpy').should('have.callCount', 2);
|
|
84
|
-
cy.get('@eLabFTWHttpClientSpyInfo').should('have.callCount', 1);
|
|
85
|
-
});
|
|
86
|
-
});
|