@opencloning/ui 1.4.5 → 1.4.6
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 +11 -0
- package/package.json +3 -3
- package/src/components/assembler/Assembler.cy.jsx +63 -1
- package/src/components/assembler/Assembler.jsx +54 -24
- package/src/components/assembler/ExistingSyntaxDialog.jsx +0 -1
- package/src/components/assembler/UploadPlasmidsButton.jsx +1 -0
- package/src/components/assembler/assembler_utils.js +45 -0
- package/src/components/assembler/assembler_utils.test.js +56 -2
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.4.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#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
|
|
8
|
+
- improve handling of files uploaded by user or associated with kit
|
|
9
|
+
- Allow downloading the results from the Assembler.
|
|
10
|
+
- Updated dependencies []:
|
|
11
|
+
- @opencloning/store@1.4.6
|
|
12
|
+
- @opencloning/utils@1.4.6
|
|
13
|
+
|
|
3
14
|
## 1.4.5
|
|
4
15
|
|
|
5
16
|
### 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.6",
|
|
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.6",
|
|
29
|
+
"@opencloning/utils": "1.4.6",
|
|
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,7 +280,7 @@ 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
286
|
{existingSyntaxDialogOpen && <ExistingSyntaxDialog staticContentPath={staticContentPath} onClose={() => setExistingSyntaxDialogOpen(false)} onSyntaxSelect={onSyntaxSelect}/>}
|
|
@@ -268,6 +292,8 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
|
268
292
|
function Assembler() {
|
|
269
293
|
const [syntax, setSyntax] = React.useState(null);
|
|
270
294
|
const [plasmids, setPlasmids] = React.useState([])
|
|
295
|
+
const { addAlert } = useAlerts();
|
|
296
|
+
const appInfo = useSelector(({ cloning }) => cloning.appInfo, isEqual);
|
|
271
297
|
|
|
272
298
|
const categories = React.useMemo(() => {
|
|
273
299
|
return categoriesFromSyntaxAndPlasmids(syntax, plasmids)
|
|
@@ -280,8 +306,12 @@ function Assembler() {
|
|
|
280
306
|
})
|
|
281
307
|
}, [])
|
|
282
308
|
|
|
309
|
+
const clearLoadedPlasmids = React.useCallback(() => {
|
|
310
|
+
setPlasmids(prev => prev.filter((plasmid) => plasmid.userUploaded !== true))
|
|
311
|
+
}, [])
|
|
312
|
+
|
|
283
313
|
const clearPlasmids = React.useCallback(() => {
|
|
284
|
-
setPlasmids(
|
|
314
|
+
setPlasmids([])
|
|
285
315
|
}, [])
|
|
286
316
|
|
|
287
317
|
return (
|
|
@@ -290,11 +320,11 @@ function Assembler() {
|
|
|
290
320
|
The Assembler is experimental. Use with caution.
|
|
291
321
|
</Alert>
|
|
292
322
|
<ButtonGroup>
|
|
293
|
-
<LoadSyntaxButton setSyntax={setSyntax} addPlasmids={addPlasmids} />
|
|
323
|
+
<LoadSyntaxButton setSyntax={setSyntax} addPlasmids={addPlasmids} clearPlasmids={clearPlasmids} />
|
|
294
324
|
{syntax && <UploadPlasmidsButton addPlasmids={addPlasmids} syntax={syntax} />}
|
|
295
|
-
{syntax && <Button color="error" onClick={
|
|
325
|
+
{syntax && <Button color="error" onClick={clearLoadedPlasmids}>Remove uploaded plasmids</Button>}
|
|
296
326
|
</ButtonGroup>
|
|
297
|
-
{syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories} assemblyEnzyme={syntax.assemblyEnzyme} />}
|
|
327
|
+
{syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories} assemblyEnzyme={syntax.assemblyEnzyme} addAlert={addAlert} appInfo={appInfo} />}
|
|
298
328
|
</>
|
|
299
329
|
)
|
|
300
330
|
}
|
|
@@ -2,7 +2,6 @@ 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';
|
|
@@ -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
|
+
})
|
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.6";
|