@opencloning/ui 1.4.5 → 1.4.7
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 +25 -0
- package/package.json +3 -3
- package/src/components/assembler/Assembler.cy.jsx +63 -1
- package/src/components/assembler/Assembler.jsx +61 -26
- package/src/components/assembler/ExistingSyntaxDialog.jsx +6 -2
- package/src/components/assembler/UploadPlasmidsButton.jsx +5 -4
- package/src/components/assembler/assembler_utils.js +45 -0
- package/src/components/assembler/assembler_utils.test.js +56 -2
- package/src/components/assembler/assembly_component.module.css +14 -9
- package/src/components/navigation/SelectTemplateDialog.jsx +2 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.4.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#631](https://github.com/manulera/OpenCloning_frontend/pull/631) [`40dd956`](https://github.com/manulera/OpenCloning_frontend/commit/40dd956d0f6c729ad0690578bf232610a063523f) Thanks [@manulera](https://github.com/manulera)! - Add links to assembler documentation and to create your own syntax in the builder
|
|
8
|
+
|
|
9
|
+
- [#631](https://github.com/manulera/OpenCloning_frontend/pull/631) [`40dd956`](https://github.com/manulera/OpenCloning_frontend/commit/40dd956d0f6c729ad0690578bf232610a063523f) Thanks [@manulera](https://github.com/manulera)! - Add link to documentation for the Assembler, and add deprecation notice for MoClo templates
|
|
10
|
+
|
|
11
|
+
- [#631](https://github.com/manulera/OpenCloning_frontend/pull/631) [`40dd956`](https://github.com/manulera/OpenCloning_frontend/commit/40dd956d0f6c729ad0690578bf232610a063523f) Thanks [@manulera](https://github.com/manulera)! - improve alignment of sequences in the AssemblerPart by improving css
|
|
12
|
+
|
|
13
|
+
- Updated dependencies []:
|
|
14
|
+
- @opencloning/store@1.4.7
|
|
15
|
+
- @opencloning/utils@1.4.7
|
|
16
|
+
|
|
17
|
+
## 1.4.6
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#629](https://github.com/manulera/OpenCloning_frontend/pull/629) [`8820965`](https://github.com/manulera/OpenCloning_frontend/commit/8820965e5a91ec29bb0c9788191123f996a8ecd8) Thanks [@manulera](https://github.com/manulera)! - \* improve category names + clear plasmids when loading new syntax
|
|
22
|
+
- improve handling of files uploaded by user or associated with kit
|
|
23
|
+
- Allow downloading the results from the Assembler.
|
|
24
|
+
- Updated dependencies []:
|
|
25
|
+
- @opencloning/store@1.4.6
|
|
26
|
+
- @opencloning/utils@1.4.6
|
|
27
|
+
|
|
3
28
|
## 1.4.5
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
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.4.
|
|
29
|
-
"@opencloning/utils": "1.4.
|
|
28
|
+
"@opencloning/store": "1.4.7",
|
|
29
|
+
"@opencloning/utils": "1.4.7",
|
|
30
30
|
"@teselagen/bio-parsers": "^0.4.34",
|
|
31
31
|
"@teselagen/ove": "^0.8.34",
|
|
32
32
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
4
4
|
import { AssemblerComponent } from './Assembler';
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
// Test config
|
|
7
8
|
const testConfig = {
|
|
8
9
|
backendUrl: 'http://localhost:8000',
|
|
@@ -33,6 +34,25 @@ const dummyResponse = {
|
|
|
33
34
|
}
|
|
34
35
|
]
|
|
35
36
|
}
|
|
37
|
+
const dummyResponse2 = {
|
|
38
|
+
sources: [{
|
|
39
|
+
...dummyResponse.sources[0],
|
|
40
|
+
id: 2,
|
|
41
|
+
type: 'RestrictionAndLigationSource',
|
|
42
|
+
input: [
|
|
43
|
+
{
|
|
44
|
+
sequence: 1,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
sequence: 2,
|
|
48
|
+
}],
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
sequences: [{
|
|
52
|
+
...dummyResponse.sequences[0],
|
|
53
|
+
id: 2,
|
|
54
|
+
}],
|
|
55
|
+
}
|
|
36
56
|
|
|
37
57
|
// Test data
|
|
38
58
|
const mockPlasmids = [
|
|
@@ -91,12 +111,15 @@ describe('<AssemblerComponent />', () => {
|
|
|
91
111
|
win.localStorage.clear();
|
|
92
112
|
});
|
|
93
113
|
|
|
114
|
+
const addAlertStub = cy.stub().as('addAlert')
|
|
94
115
|
cy.mount(
|
|
95
116
|
<ConfigProvider config={testConfig}>
|
|
96
117
|
<AssemblerComponent
|
|
97
118
|
plasmids={mockPlasmids}
|
|
98
119
|
categories={mockCategories}
|
|
99
120
|
assemblyEnzyme="assembly_enzyme"
|
|
121
|
+
addAlert={addAlertStub}
|
|
122
|
+
appInfo={{}}
|
|
100
123
|
/>
|
|
101
124
|
</ConfigProvider>,
|
|
102
125
|
);
|
|
@@ -203,6 +226,7 @@ describe('<AssemblerComponent />', () => {
|
|
|
203
226
|
// Error should be cleared
|
|
204
227
|
cy.get('.MuiAlert-colorError').should('not.exist');
|
|
205
228
|
});
|
|
229
|
+
|
|
206
230
|
it('works in normal case', () => {
|
|
207
231
|
// Mock successful source fetching
|
|
208
232
|
cy.intercept('POST', 'http://localhost:8000/repository_id*', {
|
|
@@ -217,19 +241,57 @@ describe('<AssemblerComponent />', () => {
|
|
|
217
241
|
expect(req.body.source.restriction_enzymes).to.include('assembly_enzyme');
|
|
218
242
|
req.reply({
|
|
219
243
|
statusCode: 200,
|
|
220
|
-
body:
|
|
244
|
+
body: dummyResponse2,
|
|
221
245
|
});
|
|
222
246
|
}).as('assemblySuccess');
|
|
223
247
|
|
|
224
248
|
// Click submit button
|
|
225
249
|
cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
|
|
226
250
|
cy.wait('@fetchSourceSuccess');
|
|
251
|
+
cy.wait('@assemblySuccess');
|
|
227
252
|
|
|
228
253
|
// Check that the table displays the name
|
|
229
254
|
cy.get('[data-testid="assembler-product-table"]').contains('Category 1').should('exist');
|
|
230
255
|
cy.get('[data-testid="assembler-product-table"]').contains('Category 2').should('exist');
|
|
231
256
|
cy.get('[data-testid="assembler-product-table"]').contains('Test Plasmid 1').should('exist');
|
|
232
257
|
cy.get('[data-testid="assembler-product-table"]').contains('Test Plasmid 2').should('exist');
|
|
258
|
+
|
|
259
|
+
// Stub URL.createObjectURL before clicking (it's called with a Blob, not the filename)
|
|
260
|
+
cy.window().then((win) => {
|
|
261
|
+
cy.stub(win.URL, 'createObjectURL').callsFake((blob) => `blob:mock-${blob?.size ?? 0}`).as('createObjectURL');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Click download assemblies button
|
|
265
|
+
cy.get('[data-testid="assembler-download-assemblies-button"]').click();
|
|
266
|
+
|
|
267
|
+
// Check that the download was triggered (createObjectURL receives the zip Blob)
|
|
268
|
+
|
|
269
|
+
cy.get('@createObjectURL').should((stub) => {
|
|
270
|
+
expect(stub).to.have.been.calledOnce;
|
|
271
|
+
expect(stub.firstCall.args[0]).to.be.instanceOf(Blob);
|
|
272
|
+
expect(stub.firstCall.args[0].type).to.equal('application/zip');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
cy.get('@addAlert').should('have.not.been.called');
|
|
276
|
+
|
|
277
|
+
});
|
|
278
|
+
it('displays error message when downloading assemblies fails', () => {
|
|
279
|
+
|
|
280
|
+
cy.intercept('POST', 'http://localhost:8000/repository_id*', { statusCode: 200, body: dummyResponse }).as('fetchSourceSuccess');
|
|
281
|
+
cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', { statusCode: 200, body: dummyResponse }).as('assemblySuccess');
|
|
282
|
+
|
|
283
|
+
cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
|
|
284
|
+
cy.wait('@fetchSourceSuccess');
|
|
285
|
+
cy.wait('@assemblySuccess');
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
cy.get('[data-testid="assembler-download-assemblies-button"]').click();
|
|
289
|
+
|
|
290
|
+
cy.get('@addAlert').should('have.been.calledOnce');
|
|
291
|
+
cy.get('@addAlert').should((stub) => {
|
|
292
|
+
expect(stub.firstCall.args[0].message).to.include('Error downloading assemblies:');
|
|
293
|
+
expect(stub.firstCall.args[0].severity).to.equal('error');
|
|
294
|
+
});
|
|
233
295
|
});
|
|
234
296
|
});
|
|
235
297
|
|
|
@@ -5,19 +5,20 @@ import {
|
|
|
5
5
|
} from '@mui/material'
|
|
6
6
|
import { Clear as ClearIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
|
7
7
|
import { useAssembler } from './useAssembler';
|
|
8
|
-
import { useDispatch } from 'react-redux';
|
|
8
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
9
9
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
10
10
|
import AssemblerPart from './AssemblerPart';
|
|
11
11
|
|
|
12
12
|
import useCombinatorialAssembly from './useCombinatorialAssembly';
|
|
13
13
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
14
14
|
import error2String from '@opencloning/utils/error2String';
|
|
15
|
-
import { categoryFilter } from './assembler_utils';
|
|
15
|
+
import { categoryFilter, downloadAssemblerFilesAsZip, getFilesToExportFromAssembler } from './assembler_utils';
|
|
16
16
|
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
17
17
|
import useHttpClient from '../../hooks/useHttpClient';
|
|
18
18
|
import useAlerts from '../../hooks/useAlerts';
|
|
19
19
|
import UploadPlasmidsButton from './UploadPlasmidsButton';
|
|
20
20
|
import { useConfig } from '../../providers';
|
|
21
|
+
import { isEqual } from 'lodash-es';
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
|
|
@@ -108,7 +109,7 @@ function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, a
|
|
|
108
109
|
const { key, ...restProps } = props
|
|
109
110
|
const plasmid = plasmids.find((d) => d.id === option)
|
|
110
111
|
return (
|
|
111
|
-
<MenuItem key={key} {...restProps} sx={{ backgroundColor: plasmid.
|
|
112
|
+
<MenuItem key={key} {...restProps} sx={{ backgroundColor: plasmid.userUploaded === true ? '#dcedc8' : undefined }}>
|
|
112
113
|
{formatItemName(plasmid)}
|
|
113
114
|
</MenuItem>
|
|
114
115
|
)}}
|
|
@@ -123,7 +124,7 @@ function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, a
|
|
|
123
124
|
)
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
|
|
127
|
+
export function AssemblerComponent({ plasmids, categories, assemblyEnzyme, addAlert, appInfo }) {
|
|
127
128
|
|
|
128
129
|
const [requestedAssemblies, setRequestedAssemblies] = React.useState([])
|
|
129
130
|
const [errorMessage, setErrorMessage] = React.useState('')
|
|
@@ -137,7 +138,7 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
|
|
|
137
138
|
const { assembly, setCategory, setId, expandedAssemblies, assemblyComplete, canBeSubmitted, currentCategories } = useCombinatorialAssembly({ onValueChange: clearAssemblySelection, categories, plasmids })
|
|
138
139
|
const { requestSources, requestAssemblies } = useAssembler()
|
|
139
140
|
|
|
140
|
-
const onSubmitAssembly = async () => {
|
|
141
|
+
const onSubmitAssembly = React.useCallback(async () => {
|
|
141
142
|
clearAssemblySelection()
|
|
142
143
|
const selectedPlasmids = assembly.map(({ plasmidIds }) => plasmidIds.map((id) => (plasmids.find((item) => item.id === id))))
|
|
143
144
|
|
|
@@ -159,7 +160,20 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
|
|
|
159
160
|
} finally {
|
|
160
161
|
setLoadingMessage(false)
|
|
161
162
|
}
|
|
162
|
-
}
|
|
163
|
+
}, [assemblyEnzyme, assembly, plasmids, requestSources, requestAssemblies, clearAssemblySelection])
|
|
164
|
+
|
|
165
|
+
const onDownloadAssemblies = React.useCallback(async () => {
|
|
166
|
+
try {
|
|
167
|
+
const files = getFilesToExportFromAssembler({requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo})
|
|
168
|
+
await downloadAssemblerFilesAsZip(files);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Error downloading assemblies:', error);
|
|
171
|
+
addAlert({
|
|
172
|
+
message: `Error downloading assemblies: ${error.message}`,
|
|
173
|
+
severity: 'error',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}, [requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo, addAlert])
|
|
163
177
|
|
|
164
178
|
const options = React.useMemo(() => assemblyComplete ? assembly : [...assembly, { category: '', plasmidIds: [] }], [assembly, assemblyComplete])
|
|
165
179
|
|
|
@@ -171,17 +185,26 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
|
|
|
171
185
|
<AssemblerBox key={index} {...{item, index, setCategory, setId, categories, plasmids, assembly}} />
|
|
172
186
|
)}
|
|
173
187
|
</Stack>
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, my: 2 }}>
|
|
189
|
+
{canBeSubmitted && <>
|
|
190
|
+
<Button
|
|
191
|
+
sx={{ p: 2, fontSize: '1.2rem' }}
|
|
192
|
+
variant="contained"
|
|
193
|
+
color="primary"
|
|
194
|
+
data-testid="assembler-submit-button"
|
|
195
|
+
onClick={onSubmitAssembly}
|
|
196
|
+
disabled={Boolean(loadingMessage)}>
|
|
197
|
+
{loadingMessage ? <><CircularProgress /> {loadingMessage}</> : 'Submit'}
|
|
198
|
+
</Button>
|
|
199
|
+
</>}
|
|
200
|
+
{requestedAssemblies.length > 0 && <Button
|
|
201
|
+
color="success"
|
|
177
202
|
variant="contained"
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
onClick={
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
</Button>
|
|
184
|
-
</>}
|
|
203
|
+
data-testid="assembler-download-assemblies-button"
|
|
204
|
+
sx={{ p: 2, fontSize: '1.2rem' }}
|
|
205
|
+
onClick={onDownloadAssemblies}>Download Assemblies
|
|
206
|
+
</Button>}
|
|
207
|
+
</Box>
|
|
185
208
|
{errorMessage && <Alert severity="error" sx={{ my: 2, maxWidth: 300, margin: 'auto', fontSize: '1.2rem' }}>{errorMessage}</Alert>}
|
|
186
209
|
{requestedAssemblies.length > 0 &&
|
|
187
210
|
<AssemblerProductTable {...{requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories}} />
|
|
@@ -196,10 +219,10 @@ function displayNameFromCategory(category) {
|
|
|
196
219
|
if (category.name) {
|
|
197
220
|
name = category.name
|
|
198
221
|
if (category.info)
|
|
199
|
-
name += ` (${category.info})
|
|
222
|
+
name += ` (${category.info})`
|
|
200
223
|
}
|
|
201
224
|
if (category.left_name && category.right_name) {
|
|
202
|
-
name +=
|
|
225
|
+
name += ` (${category.left_name}_${category.right_name})`
|
|
203
226
|
}
|
|
204
227
|
if (name === '') {
|
|
205
228
|
name = category.key
|
|
@@ -238,7 +261,7 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
|
|
|
238
261
|
return newCategories
|
|
239
262
|
}
|
|
240
263
|
|
|
241
|
-
function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
264
|
+
function LoadSyntaxButton({ setSyntax, addPlasmids, clearPlasmids }) {
|
|
242
265
|
const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
|
|
243
266
|
const httpClient = useHttpClient();
|
|
244
267
|
const { staticContentPath } = useConfig();
|
|
@@ -249,6 +272,7 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
|
249
272
|
try {
|
|
250
273
|
await httpClient.post(url, syntax);
|
|
251
274
|
setSyntax(syntax)
|
|
275
|
+
clearPlasmids()
|
|
252
276
|
addPlasmids(plasmids)
|
|
253
277
|
} catch (error) {
|
|
254
278
|
addAlert({
|
|
@@ -256,10 +280,15 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
|
256
280
|
severity: 'error',
|
|
257
281
|
});
|
|
258
282
|
}
|
|
259
|
-
}, [setSyntax, addPlasmids, httpClient, backendRoute, addAlert])
|
|
283
|
+
}, [setSyntax, addPlasmids, clearPlasmids, httpClient, backendRoute, addAlert])
|
|
260
284
|
return <>
|
|
261
285
|
<Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
|
|
262
|
-
{existingSyntaxDialogOpen && <ExistingSyntaxDialog
|
|
286
|
+
{existingSyntaxDialogOpen && <ExistingSyntaxDialog
|
|
287
|
+
staticContentPath={staticContentPath}
|
|
288
|
+
onClose={() => setExistingSyntaxDialogOpen(false)}
|
|
289
|
+
onSyntaxSelect={onSyntaxSelect}
|
|
290
|
+
displayCreateYourOwnMessage={true}
|
|
291
|
+
/>}
|
|
263
292
|
</>
|
|
264
293
|
}
|
|
265
294
|
|
|
@@ -268,6 +297,8 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
|
268
297
|
function Assembler() {
|
|
269
298
|
const [syntax, setSyntax] = React.useState(null);
|
|
270
299
|
const [plasmids, setPlasmids] = React.useState([])
|
|
300
|
+
const { addAlert } = useAlerts();
|
|
301
|
+
const appInfo = useSelector(({ cloning }) => cloning.appInfo, isEqual);
|
|
271
302
|
|
|
272
303
|
const categories = React.useMemo(() => {
|
|
273
304
|
return categoriesFromSyntaxAndPlasmids(syntax, plasmids)
|
|
@@ -280,21 +311,25 @@ function Assembler() {
|
|
|
280
311
|
})
|
|
281
312
|
}, [])
|
|
282
313
|
|
|
314
|
+
const clearLoadedPlasmids = React.useCallback(() => {
|
|
315
|
+
setPlasmids(prev => prev.filter((plasmid) => plasmid.userUploaded !== true))
|
|
316
|
+
}, [])
|
|
317
|
+
|
|
283
318
|
const clearPlasmids = React.useCallback(() => {
|
|
284
|
-
setPlasmids(
|
|
319
|
+
setPlasmids([])
|
|
285
320
|
}, [])
|
|
286
321
|
|
|
287
322
|
return (
|
|
288
323
|
<>
|
|
289
324
|
<Alert severity="warning" sx={{ maxWidth: '400px', margin: 'auto', fontSize: '.9rem', mb: 2 }}>
|
|
290
|
-
The Assembler is experimental. Use with caution.
|
|
325
|
+
The Assembler is experimental. Use with caution. Visit <a href="https://docs.opencloning.org/assembler" target="_blank">the documentation</a> for more information.
|
|
291
326
|
</Alert>
|
|
292
327
|
<ButtonGroup>
|
|
293
|
-
<LoadSyntaxButton setSyntax={setSyntax} addPlasmids={addPlasmids} />
|
|
328
|
+
<LoadSyntaxButton setSyntax={setSyntax} addPlasmids={addPlasmids} clearPlasmids={clearPlasmids} />
|
|
294
329
|
{syntax && <UploadPlasmidsButton addPlasmids={addPlasmids} syntax={syntax} />}
|
|
295
|
-
{syntax && <Button color="error" onClick={
|
|
330
|
+
{syntax && <Button color="error" onClick={clearLoadedPlasmids}>Remove uploaded plasmids</Button>}
|
|
296
331
|
</ButtonGroup>
|
|
297
|
-
{syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories} assemblyEnzyme={syntax.assemblyEnzyme} />}
|
|
332
|
+
{syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories} assemblyEnzyme={syntax.assemblyEnzyme} addAlert={addAlert} appInfo={appInfo} />}
|
|
298
333
|
</>
|
|
299
334
|
)
|
|
300
335
|
}
|
|
@@ -2,10 +2,10 @@ import React from 'react'
|
|
|
2
2
|
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box, ButtonGroup, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'
|
|
3
3
|
import getHttpClient from '@opencloning/utils/getHttpClient';
|
|
4
4
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
5
|
-
import { useConfig } from '../../providers';
|
|
6
5
|
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
7
6
|
import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
|
|
8
7
|
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
|
|
8
|
+
import { IconButton } from '@mui/material';
|
|
9
9
|
|
|
10
10
|
const httpClient = getHttpClient();
|
|
11
11
|
const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
|
|
@@ -71,7 +71,7 @@ function SyntaxListItem({ syntax, onSyntaxClick }) {
|
|
|
71
71
|
)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect }) {
|
|
74
|
+
function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect, displayCreateYourOwnMessage = false }) {
|
|
75
75
|
const [syntaxes, setSyntaxes] = React.useState([]);
|
|
76
76
|
const [connectAttempt, setConnectAttempt] = React.useState(0);
|
|
77
77
|
const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
|
|
@@ -139,6 +139,10 @@ function ExistingSyntaxDialog({ staticContentPath, onClose, onSyntaxSelect }) {
|
|
|
139
139
|
{syntaxes.map((syntax) => (
|
|
140
140
|
<SyntaxListItem key={syntax.path} syntax={syntax} onSyntaxClick={onSyntaxClick} />
|
|
141
141
|
))}
|
|
142
|
+
{displayCreateYourOwnMessage && <ListItem>
|
|
143
|
+
<ListItemText primary="🔎 Can't find your favourite syntax?" secondary="Add it yourself! It's very easy!" />
|
|
144
|
+
<Button type="a" color="success" href="https://syntax.opencloning.org" target="_blank">Create a syntax</Button>
|
|
145
|
+
</ListItem>}
|
|
142
146
|
</List>
|
|
143
147
|
</RequestStatusWrapper>
|
|
144
148
|
<Box sx={{ mb: 2 }}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Button, Dialog, DialogTitle, DialogContent, DialogActions, Alert, Box } from '@mui/material';
|
|
2
|
+
import { Button, Dialog, DialogTitle, DialogContent, DialogActions, Alert, Box, ButtonGroup } from '@mui/material';
|
|
3
3
|
import { useConfig } from '../../providers';
|
|
4
4
|
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
5
5
|
import { usePlasmidsLogic } from './usePlasmidsLogic';
|
|
@@ -26,6 +26,7 @@ function formatPlasmid(sequenceData) {
|
|
|
26
26
|
key: `${left_overhang}-${right_overhang}`,
|
|
27
27
|
sequenceData,
|
|
28
28
|
genbankString: jsonToGenbank(sequenceData),
|
|
29
|
+
userUploaded: true,
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
}
|
|
@@ -94,9 +95,9 @@ function UploadPlasmidsButton({ addPlasmids, syntax }) {
|
|
|
94
95
|
},
|
|
95
96
|
}}
|
|
96
97
|
>
|
|
97
|
-
<DialogActions sx={{ justifyContent: 'center', position: 'sticky', top: 0, zIndex: 99, background: '#fff' }}>
|
|
98
|
-
<Button disabled={validPlasmids.length === 0} variant="contained" color="success" onClick={handleImportValidPlasmids}>Import valid plasmids</Button>
|
|
99
|
-
<Button variant="contained" color="error" onClick={() => setLinkedPlasmids([])}>Cancel</Button>
|
|
98
|
+
<DialogActions sx={{ justifyContent: 'center', position: 'sticky', top: 0, zIndex: 99, background: '#fff', py: 2 }}>
|
|
99
|
+
<Button sx={{ p: 1.5, fontSize: '1rem' }} disabled={validPlasmids.length === 0} variant="contained" color="success" onClick={handleImportValidPlasmids}>Import valid plasmids</Button>
|
|
100
|
+
<Button sx={{ p: 1.5, fontSize: '1rem' }} variant="contained" color="error" onClick={() => setLinkedPlasmids([])}>Cancel</Button>
|
|
100
101
|
</DialogActions>
|
|
101
102
|
{invalidPlasmids.length > 0 && (
|
|
102
103
|
<Box data-testid="invalid-plasmids-box">
|
|
@@ -2,6 +2,9 @@ import { isRangeWithinRange } from '@teselagen/range-utils';
|
|
|
2
2
|
import { getComplementSequenceString, getAminoAcidFromSequenceTriplet, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString } from '@teselagen/sequence-utils';
|
|
3
3
|
import { allSimplePaths } from 'graphology-simple-path';
|
|
4
4
|
import { openCycleAtNode } from './graph_utils';
|
|
5
|
+
import { downloadBlob, formatStateForJsonExport, getZipFileBlob } from '@opencloning/utils/readNwrite';
|
|
6
|
+
import { getGraftSequenceId } from '@opencloning/utils/network';
|
|
7
|
+
import { TextReader} from '@zip.js/zip.js';
|
|
5
8
|
|
|
6
9
|
export function tripletsToTranslation(triplets) {
|
|
7
10
|
if (!triplets) return ''
|
|
@@ -146,3 +149,45 @@ export function categoryFilter(category, categories, previousCategoryId) {
|
|
|
146
149
|
const previousCategory = categories.find((category) => category.id === previousCategoryId)
|
|
147
150
|
return previousCategory?.right_overhang === category.left_overhang
|
|
148
151
|
}
|
|
152
|
+
|
|
153
|
+
export function getFilesToExportFromAssembler({requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo}) {
|
|
154
|
+
const files2Export = [];
|
|
155
|
+
const categoryNames = ['Assembly', ...currentCategories.map(categoryId => categories.find(c => c.id === categoryId).displayName)];
|
|
156
|
+
const assemblyNames = expandedAssemblies.map((assembly, index) => {
|
|
157
|
+
return [index + 1, ...assembly.map(part => plasmids.find(p => p.id === part).plasmid_name)];
|
|
158
|
+
});
|
|
159
|
+
for (const delimiter of ['\t', ',']) {
|
|
160
|
+
const tableHeader = categoryNames.join(delimiter);
|
|
161
|
+
const tableRows = assemblyNames.map(assemblyName => assemblyName.join(delimiter));
|
|
162
|
+
const table = [tableHeader, ...tableRows].join('\n');
|
|
163
|
+
const extension = delimiter === '\t' ? 'tsv' : 'csv';
|
|
164
|
+
files2Export.push({
|
|
165
|
+
name: `assemblies.${extension}`,
|
|
166
|
+
content: table,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < requestedAssemblies.length; i++) {
|
|
171
|
+
const name = `${String(i + 1).padStart(3, '0')}_${assemblyNames[i].slice(1).join('+')}`;
|
|
172
|
+
const requestedAssembly = requestedAssemblies[i];
|
|
173
|
+
const jsonContent = formatStateForJsonExport({...requestedAssembly, appInfo});
|
|
174
|
+
files2Export.push({
|
|
175
|
+
name: `${name}.json`,
|
|
176
|
+
content: JSON.stringify(jsonContent, null, 2),
|
|
177
|
+
});
|
|
178
|
+
const finalSequenceId = getGraftSequenceId(requestedAssembly)
|
|
179
|
+
const finalSequence = requestedAssembly.sequences.find(s => s.id === finalSequenceId)
|
|
180
|
+
files2Export.push({
|
|
181
|
+
name: `${name}.gbk`,
|
|
182
|
+
content: finalSequence.file_content,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return files2Export;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function downloadAssemblerFilesAsZip(files) {
|
|
189
|
+
const files2write = files.map(({name, content}) => ({name, reader: new TextReader(content)}));
|
|
190
|
+
|
|
191
|
+
const blob = await getZipFileBlob(files2write);
|
|
192
|
+
downloadBlob(blob, 'assemblies.zip');
|
|
193
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { aliasedEnzymesByName, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString, getComplementSequenceString, getReverseComplementSequenceAndAnnotations } from "@teselagen/sequence-utils";
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import { assignSequenceToSyntaxPart, simplifyDigestFragment, reverseComplementSimplifiedDigestFragment, tripletsToTranslation, partDataToDisplayData, arrayCombinations,
|
|
3
|
+
import { assignSequenceToSyntaxPart, simplifyDigestFragment, reverseComplementSimplifiedDigestFragment, tripletsToTranslation, partDataToDisplayData, arrayCombinations, getFilesToExportFromAssembler } from "./assembler_utils";
|
|
4
4
|
import { partsToEdgesGraph } from "./graph_utils";
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
|
|
7
7
|
const sequenceBsaI = 'tgggtctcaTACTagagtcacacaggactactaAATGagagacctac';
|
|
8
8
|
const sequenceBsaI2 = 'tgggtctcaAATGagagtcacacaggactactaAGGTagagacctac'
|
|
@@ -257,3 +257,57 @@ describe('arrayCombinations', () => {
|
|
|
257
257
|
expect(arrayCombinations([[1, 2], [3, 4], [5, 6]])).toEqual([[1, 3, 5], [1, 3, 6], [1, 4, 5], [1, 4, 6], [2, 3, 5], [2, 3, 6], [2, 4, 5], [2, 4, 6]]);
|
|
258
258
|
});
|
|
259
259
|
});
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
const goldenGateCloningStrategy = JSON.parse(fs.readFileSync('apps/opencloning/public/examples/golden_gate.json', 'utf8'));
|
|
263
|
+
const gatewayCloningStrategy = JSON.parse(fs.readFileSync('apps/opencloning/public/examples/gateway.json', 'utf8'));
|
|
264
|
+
|
|
265
|
+
const dummyData = {
|
|
266
|
+
requestedAssemblies: [ goldenGateCloningStrategy, gatewayCloningStrategy],
|
|
267
|
+
expandedAssemblies: [[1, 5, 7], [1, 2, 3]],
|
|
268
|
+
plasmids: [
|
|
269
|
+
{id: 1, plasmid_name: 'p1'},
|
|
270
|
+
{id: 2, plasmid_name: 'p2'},
|
|
271
|
+
{id: 3, plasmid_name: 'p3'},
|
|
272
|
+
{id: 4, plasmid_name: 'p4'},
|
|
273
|
+
{id: 5, plasmid_name: 'p5'},
|
|
274
|
+
{id: 6, plasmid_name: 'p6'},
|
|
275
|
+
{id: 7, plasmid_name: 'p7'},
|
|
276
|
+
],
|
|
277
|
+
currentCategories: [1, 2, 3],
|
|
278
|
+
categories: [
|
|
279
|
+
{id: 1, displayName: 'Category 1'},
|
|
280
|
+
{id: 2, displayName: 'Category 2'},
|
|
281
|
+
{id: 3, displayName: 'Category 3'},
|
|
282
|
+
],
|
|
283
|
+
appInfo: {backendVersion: '0.5.1', schemaVersion: '0.4.9', frontendVersion: '__VERSION__'},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
describe('getZipFileFromAssemblies', () => {
|
|
289
|
+
it('returns a zip file', () => {
|
|
290
|
+
const files = getFilesToExportFromAssembler(dummyData);
|
|
291
|
+
expect(files[0].name).toBe('assemblies.tsv');
|
|
292
|
+
expect(files[0].content).toBe('Assembly\tCategory 1\tCategory 2\tCategory 3\n1\tp1\tp5\tp7\n2\tp1\tp2\tp3');
|
|
293
|
+
expect(files[1].name).toBe('assemblies.csv');
|
|
294
|
+
expect(files[1].content).toBe('Assembly,Category 1,Category 2,Category 3\n1,p1,p5,p7\n2,p1,p2,p3');
|
|
295
|
+
|
|
296
|
+
const fileNames = ['001_p1+p5+p7', '002_p1+p2+p3'];
|
|
297
|
+
for (let i = 0; i < 2; i++) {
|
|
298
|
+
const fileIndex1 = i * 2 + 2;
|
|
299
|
+
const fileIndex2 = fileIndex1 + 1;
|
|
300
|
+
expect(files[fileIndex1].name).toBe(`${fileNames[i]}.json`);
|
|
301
|
+
const cloningStrategy = JSON.parse(files[fileIndex1].content);
|
|
302
|
+
expect(cloningStrategy.sequences).toEqual(dummyData.requestedAssemblies[i].sequences);
|
|
303
|
+
expect(cloningStrategy.sources).toEqual(dummyData.requestedAssemblies[i].sources);
|
|
304
|
+
expect(cloningStrategy.primers).toEqual(dummyData.requestedAssemblies[i].primers);
|
|
305
|
+
|
|
306
|
+
expect(files[fileIndex2].name).toBe(`${fileNames[i]}.gbk`);
|
|
307
|
+
const genbankContent = files[fileIndex2].content;
|
|
308
|
+
expect(genbankContent).toBe(dummyData.requestedAssemblies[i].sequences[dummyData.requestedAssemblies[i].sequences.length - 1].file_content);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
})
|
|
313
|
+
})
|
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
min-height: 1em;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
.overhang div {
|
|
18
|
-
padding-left: calc(2px + var(--border-width));
|
|
19
|
-
padding-right: 2px;
|
|
20
|
-
}
|
|
21
17
|
|
|
22
18
|
.container {
|
|
23
19
|
display: flex;
|
|
@@ -57,19 +53,19 @@
|
|
|
57
53
|
.overhang .watson {
|
|
58
54
|
border-left: var(--border-width) var(--border-style) var(--border-color);
|
|
59
55
|
border-bottom: var(--border-width) var(--border-style) var(--border-color);
|
|
60
|
-
padding-left: 2px;
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
.overhang .crick {
|
|
64
59
|
border-right: var(--border-width) var(--border-style) var(--border-color);
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
.
|
|
68
|
-
|
|
62
|
+
.inside .watson {
|
|
63
|
+
padding-bottom: var(--border-width);
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
.
|
|
72
|
-
|
|
66
|
+
.overhang {
|
|
67
|
+
padding-left: var(--border-width);
|
|
68
|
+
padding-right: var(--border-width);
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
.overhang.left .watson {
|
|
@@ -79,3 +75,12 @@
|
|
|
79
75
|
.overhang.right .crick {
|
|
80
76
|
font-weight: bold;
|
|
81
77
|
}
|
|
78
|
+
|
|
79
|
+
.overhang .top {
|
|
80
|
+
padding-left: var(--border-width);
|
|
81
|
+
padding-right: var(--border-width);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.overhang .crick {
|
|
85
|
+
padding-left: var(--border-width);
|
|
86
|
+
}
|
|
@@ -27,6 +27,8 @@ function SelectTemplateDialog({ onClose, open }) {
|
|
|
27
27
|
return (
|
|
28
28
|
<Dialog open={open} onClose={() => onClose('')} className="load-template-dialog">
|
|
29
29
|
<DialogTitle>Load a template</DialogTitle>
|
|
30
|
+
<Alert severity="warning" sx={{ maxWidth: '400px', margin: 'auto', fontSize: '.9rem', mb: 2 }}>
|
|
31
|
+
MoClo templates will soon be replaced by the new Assembler. Go to the Assembler tab to learn more.</Alert>
|
|
30
32
|
{!templates
|
|
31
33
|
&& (
|
|
32
34
|
<DialogContent>
|
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.4.
|
|
2
|
+
export const version = "1.4.7";
|