@opencloning/ui 1.5.1 → 1.5.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 +25 -0
- package/package.json +4 -3
- package/src/components/assembler/Assembler.cy.jsx +67 -0
- package/src/components/assembler/Assembler.jsx +184 -46
- package/src/components/assembler/SyntaxOverviewTable.jsx +103 -0
- package/src/components/assembler/assembler_utils.js +36 -10
- package/src/components/assembler/assembler_utils.test.js +39 -11
- package/src/components/assembler/index.js +2 -0
- package/src/components/assembler/useAssembler.js +1 -0
- package/src/components/form/EditTextDialog.jsx +48 -0
- package/src/components/form/ServerStaticFileSelect.jsx +0 -1
- package/src/components/index.js +1 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.5.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#655](https://github.com/manulera/OpenCloning_frontend/pull/655) [`b34e83f`](https://github.com/manulera/OpenCloning_frontend/commit/b34e83f0b8813de7744d3d71ad779be0df2f75cf) Thanks [@manulera](https://github.com/manulera)! - Changes to Assembler naming:
|
|
8
|
+
|
|
9
|
+
- Handle slashes in downloaded file names (were being turned into subfolders)
|
|
10
|
+
- Allow users to change names of outputs in assembler
|
|
11
|
+
|
|
12
|
+
- [#655](https://github.com/manulera/OpenCloning_frontend/pull/655) [`b34e83f`](https://github.com/manulera/OpenCloning_frontend/commit/b34e83f0b8813de7744d3d71ad779be0df2f75cf) Thanks [@manulera](https://github.com/manulera)! - In Assembler tab, allow users to see an overview of the Syntax
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [[`b34e83f`](https://github.com/manulera/OpenCloning_frontend/commit/b34e83f0b8813de7744d3d71ad779be0df2f75cf)]:
|
|
15
|
+
- @opencloning/utils@1.5.3
|
|
16
|
+
- @opencloning/store@1.5.3
|
|
17
|
+
|
|
18
|
+
## 1.5.2
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- [#653](https://github.com/manulera/OpenCloning_frontend/pull/653) [`e529568`](https://github.com/manulera/OpenCloning_frontend/commit/e5295684d62b25954e6095fc63fa7c9e65551fb6) Thanks [@manulera](https://github.com/manulera)! - Assembler - pass sort_by_recognition_sites=true to restriction & ligation endpoint
|
|
23
|
+
|
|
24
|
+
- Updated dependencies []:
|
|
25
|
+
- @opencloning/store@1.5.2
|
|
26
|
+
- @opencloning/utils@1.5.2
|
|
27
|
+
|
|
3
28
|
## 1.5.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"@emotion/styled": "^11.14.0",
|
|
26
26
|
"@mui/icons-material": "^5.15.17",
|
|
27
27
|
"@mui/material": "^5.15.17",
|
|
28
|
-
"@
|
|
29
|
-
"@opencloning/
|
|
28
|
+
"@mui/x-data-grid": "^8.25.0",
|
|
29
|
+
"@opencloning/store": "1.5.3",
|
|
30
|
+
"@opencloning/utils": "1.5.3",
|
|
30
31
|
"@teselagen/bio-parsers": "^0.4.34",
|
|
31
32
|
"@teselagen/ove": "^0.8.34",
|
|
32
33
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -84,6 +84,20 @@ const mockPlasmids = [
|
|
|
84
84
|
repository_id: '67890',
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
id: 3,
|
|
89
|
+
plasmid_name: 'Test Plasmid 3',
|
|
90
|
+
left_overhang: 'CCCT',
|
|
91
|
+
right_overhang: 'AACG',
|
|
92
|
+
key: 'CCCT-AACG',
|
|
93
|
+
type: 'AddgeneIdSource',
|
|
94
|
+
source: {
|
|
95
|
+
id: 3,
|
|
96
|
+
type: 'AddgeneIdSource',
|
|
97
|
+
input: [],
|
|
98
|
+
repository_id: '11111',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
87
101
|
];
|
|
88
102
|
|
|
89
103
|
const mockCategories = [
|
|
@@ -239,6 +253,9 @@ describe('<AssemblerComponent />', () => {
|
|
|
239
253
|
expect(req.body).to.have.property('source');
|
|
240
254
|
expect(req.body.source).to.have.property('restriction_enzymes');
|
|
241
255
|
expect(req.body.source.restriction_enzymes).to.include('assembly_enzyme');
|
|
256
|
+
// Check that the value of the sort_by_recognition_sites was set in the request query
|
|
257
|
+
expect(req.query).to.have.property('sort_by_recognition_sites');
|
|
258
|
+
expect(req.query.sort_by_recognition_sites).to.equal('true');
|
|
242
259
|
req.reply({
|
|
243
260
|
statusCode: 200,
|
|
244
261
|
body: dummyResponse2,
|
|
@@ -293,5 +310,55 @@ describe('<AssemblerComponent />', () => {
|
|
|
293
310
|
expect(stub.firstCall.args[0].severity).to.equal('error');
|
|
294
311
|
});
|
|
295
312
|
});
|
|
313
|
+
|
|
314
|
+
it('disables download when output names have duplicates', () => {
|
|
315
|
+
cy.get('[data-testid="plasmid-select"]').first().within(() => { cy.get('input').click(); });
|
|
316
|
+
cy.get('li').contains('Test Plasmid 3').click();
|
|
317
|
+
cy.intercept('POST', 'http://localhost:8000/repository_id*', { statusCode: 200, body: dummyResponse }).as('fetchSourceSuccess');
|
|
318
|
+
cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', { statusCode: 200, body: dummyResponse2 }).as('assemblySuccess');
|
|
319
|
+
|
|
320
|
+
cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
|
|
321
|
+
cy.wait('@fetchSourceSuccess');
|
|
322
|
+
cy.wait('@assemblySuccess');
|
|
323
|
+
|
|
324
|
+
cy.get('[data-testid="assembler-product-table"]').should('exist');
|
|
325
|
+
cy.get('[data-testid="assembler-download-assemblies-button"]').should('not.be.disabled');
|
|
326
|
+
|
|
327
|
+
cy.get('[data-testid="assembler-product-table-edit-button"]').first().click();
|
|
328
|
+
cy.get('[role="dialog"]').find('input').clear().type('duplicate_name');
|
|
329
|
+
cy.get('[role="dialog"]').contains('Save').click();
|
|
330
|
+
|
|
331
|
+
cy.get('[data-testid="assembler-product-table-edit-button"]').last().click();
|
|
332
|
+
cy.get('[role="dialog"]').find('input').clear().type('duplicate_name');
|
|
333
|
+
cy.get('[role="dialog"]').contains('Save').click();
|
|
334
|
+
|
|
335
|
+
cy.get('[data-testid="assembler-download-assemblies-button"]').should('be.disabled');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('disables download when output name is empty', () => {
|
|
339
|
+
cy.intercept('POST', 'http://localhost:8000/repository_id*', { statusCode: 200, body: dummyResponse }).as('fetchSourceSuccess');
|
|
340
|
+
cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', { statusCode: 200, body: dummyResponse2 }).as('assemblySuccess');
|
|
341
|
+
|
|
342
|
+
cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
|
|
343
|
+
cy.wait('@fetchSourceSuccess');
|
|
344
|
+
cy.wait('@assemblySuccess');
|
|
345
|
+
|
|
346
|
+
cy.get('[data-testid="assembler-product-table-edit-button"]').first().click();
|
|
347
|
+
cy.get('[role="dialog"]').find('input').clear();
|
|
348
|
+
cy.get('[role="dialog"]').contains('Save').click();
|
|
349
|
+
|
|
350
|
+
cy.get('[data-testid="assembler-download-assemblies-button"]').should('be.disabled');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('enables download when output names are valid and unique', () => {
|
|
354
|
+
cy.intercept('POST', 'http://localhost:8000/repository_id*', { statusCode: 200, body: dummyResponse }).as('fetchSourceSuccess');
|
|
355
|
+
cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', { statusCode: 200, body: dummyResponse2 }).as('assemblySuccess');
|
|
356
|
+
|
|
357
|
+
cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
|
|
358
|
+
cy.wait('@fetchSourceSuccess');
|
|
359
|
+
cy.wait('@assemblySuccess');
|
|
360
|
+
|
|
361
|
+
cy.get('[data-testid="assembler-download-assemblies-button"]').should('not.be.disabled');
|
|
362
|
+
});
|
|
296
363
|
});
|
|
297
364
|
|
|
@@ -1,74 +1,155 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import {
|
|
3
|
-
Alert, Autocomplete, Box, Button, CircularProgress, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, Stack,
|
|
4
|
-
|
|
3
|
+
Alert, Autocomplete, Box, Button, CircularProgress, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, Stack, TextField, ButtonGroup,
|
|
4
|
+
DialogTitle,
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
FormControlLabel,
|
|
8
|
+
Switch
|
|
5
9
|
} from '@mui/material'
|
|
6
|
-
import { Clear as ClearIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
|
10
|
+
import { Clear as ClearIcon, Edit as EditIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
|
11
|
+
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid';
|
|
7
12
|
import { useAssembler } from './useAssembler';
|
|
8
13
|
import { useDispatch, useSelector } from 'react-redux';
|
|
9
14
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
10
15
|
import AssemblerPart from './AssemblerPart';
|
|
16
|
+
import EditTextDialog from '../form/EditTextDialog';
|
|
11
17
|
|
|
12
18
|
import useCombinatorialAssembly from './useCombinatorialAssembly';
|
|
13
19
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
14
20
|
import error2String from '@opencloning/utils/error2String';
|
|
15
|
-
import { categoryFilter, downloadAssemblerFilesAsZip, getFilesToExportFromAssembler } from './assembler_utils';
|
|
21
|
+
import { categoryFilter, downloadAssemblerFilesAsZip, getFilesToExportFromAssembler, getDefaultAssemblyOutputName, MAX_OUTPUT_NAME_LENGTH, sanitizeOutputName } from './assembler_utils';
|
|
16
22
|
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
17
23
|
import useHttpClient from '../../hooks/useHttpClient';
|
|
18
24
|
import useAlerts from '../../hooks/useAlerts';
|
|
19
25
|
import UploadPlasmidsButton from './UploadPlasmidsButton';
|
|
20
26
|
import { useConfig } from '../../providers';
|
|
21
27
|
import { isEqual } from 'lodash-es';
|
|
28
|
+
import SyntaxOverviewTable from './SyntaxOverviewTable';
|
|
29
|
+
import { graphToMSA, partsToGraph } from './graph_utils';
|
|
22
30
|
|
|
23
31
|
|
|
24
32
|
const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
|
|
25
33
|
|
|
26
|
-
|
|
27
34
|
function formatItemName(item) {
|
|
28
35
|
// Fallback in case the item is not found (while updating list)
|
|
29
36
|
return item ? `${item.plasmid_name}` : '-'
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
function
|
|
39
|
+
function isRowInvalid(rowIndex, assemblyOutputNames) {
|
|
40
|
+
const name = assemblyOutputNames[rowIndex] ?? '';
|
|
41
|
+
const trimmed = name.trim();
|
|
42
|
+
const isEmpty = trimmed.length === 0;
|
|
43
|
+
const isTooLong = name.length > MAX_OUTPUT_NAME_LENGTH;
|
|
44
|
+
const isDuplicate = assemblyOutputNames.filter((n) => n === name).length > 1;
|
|
45
|
+
return isEmpty || isTooLong || isDuplicate;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function AssemblerProductTable({
|
|
49
|
+
requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories,
|
|
50
|
+
assemblyOutputNames, onOutputNameChange,
|
|
51
|
+
}) {
|
|
52
|
+
const dispatch = useDispatch();
|
|
53
|
+
const [editDialog, setEditDialog] = React.useState({ open: false, rowIndex: null, value: '' });
|
|
33
54
|
|
|
34
|
-
const dispatch = useDispatch()
|
|
35
55
|
const handleViewAssembly = (index) => {
|
|
36
|
-
const newState = requestedAssemblies[index]
|
|
37
|
-
dispatch(setCloningState(newState))
|
|
38
|
-
dispatch(setCurrentTabAction(0))
|
|
39
|
-
}
|
|
56
|
+
const newState = requestedAssemblies[index];
|
|
57
|
+
dispatch(setCloningState(newState));
|
|
58
|
+
dispatch(setCurrentTabAction(0));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleEdit = (rowIndex) => () => {
|
|
62
|
+
setEditDialog({ open: true, rowIndex, value: assemblyOutputNames[rowIndex] ?? '' });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleEditSave = (newValue) => {
|
|
66
|
+
if (editDialog.rowIndex !== null) {
|
|
67
|
+
onOutputNameChange(editDialog.rowIndex, sanitizeOutputName(newValue));
|
|
68
|
+
}
|
|
69
|
+
setEditDialog({ open: false, rowIndex: null, value: '' });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const rows = expandedAssemblies.map((parts, rowIndex) => ({
|
|
73
|
+
id: rowIndex,
|
|
74
|
+
outputName: assemblyOutputNames[rowIndex] ?? '',
|
|
75
|
+
rowIndex,
|
|
76
|
+
parts,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const columns = [
|
|
80
|
+
{
|
|
81
|
+
field: 'actions',
|
|
82
|
+
type: 'actions',
|
|
83
|
+
headerName: '',
|
|
84
|
+
width: 60,
|
|
85
|
+
getActions: (params) => [
|
|
86
|
+
<GridActionsCellItem
|
|
87
|
+
key="view"
|
|
88
|
+
icon={<VisibilityIcon />}
|
|
89
|
+
label="View"
|
|
90
|
+
onClick={() => handleViewAssembly(params.row.rowIndex)}
|
|
91
|
+
data-testid="assembler-product-table-view-button"
|
|
92
|
+
/>,
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
field: 'outputName',
|
|
97
|
+
headerName: 'Output name',
|
|
98
|
+
flex: 1,
|
|
99
|
+
minWidth: 200,
|
|
100
|
+
renderCell: (params) => (
|
|
101
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
|
|
102
|
+
<Box sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
103
|
+
{params.value || 'Click to edit...'}
|
|
104
|
+
</Box>
|
|
105
|
+
<IconButton size="small" onClick={(e) => { e.stopPropagation(); handleEdit(params.row.rowIndex)(); }} data-testid="assembler-product-table-edit-button">
|
|
106
|
+
<EditIcon fontSize="small" />
|
|
107
|
+
</IconButton>
|
|
108
|
+
</Box>
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
...currentCategories.map((categoryId, idx) => {
|
|
112
|
+
const category = categories.find((c) => c.id === categoryId);
|
|
113
|
+
return {
|
|
114
|
+
field: `category_${categoryId}`,
|
|
115
|
+
headerName: category?.displayName ?? '',
|
|
116
|
+
flex: 1,
|
|
117
|
+
valueGetter: (value, row) => formatItemName(plasmids.find((d) => d.id === row.parts[idx])),
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
];
|
|
121
|
+
|
|
40
122
|
return (
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
123
|
+
<Box data-testid="assembler-product-table" sx={{ '& .MuiDataGrid-cell': { fontSize: '1.2rem' }, '& .MuiDataGrid-columnHeader': { fontSize: '1.2rem', fontWeight: 'bold' } }}>
|
|
124
|
+
<DataGrid
|
|
125
|
+
rows={rows}
|
|
126
|
+
columns={columns}
|
|
127
|
+
getRowClassName={(params) => (isRowInvalid(params.row.rowIndex, assemblyOutputNames) ? 'error-row' : '')}
|
|
128
|
+
density="compact"
|
|
129
|
+
disableRowSelectionOnClick
|
|
130
|
+
disableColumnSorting
|
|
131
|
+
disableColumnFilter
|
|
132
|
+
disableColumnMenu
|
|
133
|
+
hideFooter
|
|
134
|
+
autoHeight
|
|
135
|
+
sx={{
|
|
136
|
+
'& .error-row': {
|
|
137
|
+
backgroundColor: 'rgba(255, 0, 0, 0.15)',
|
|
138
|
+
'&:hover': { backgroundColor: 'rgba(255, 0, 0, 0.25)' },
|
|
139
|
+
},
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
<EditTextDialog
|
|
143
|
+
open={editDialog.open}
|
|
144
|
+
value={editDialog.value}
|
|
145
|
+
onClose={() => setEditDialog({ open: false, rowIndex: null, value: '' })}
|
|
146
|
+
onSave={handleEditSave}
|
|
147
|
+
title="Edit Output Name"
|
|
148
|
+
placeholder="Enter output name..."
|
|
149
|
+
maxLength={MAX_OUTPUT_NAME_LENGTH}
|
|
150
|
+
/>
|
|
151
|
+
</Box>
|
|
152
|
+
);
|
|
72
153
|
}
|
|
73
154
|
|
|
74
155
|
function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, assembly }) {
|
|
@@ -132,17 +213,32 @@ function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, a
|
|
|
132
213
|
export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addAlert, appInfo }) {
|
|
133
214
|
|
|
134
215
|
const [requestedAssemblies, setRequestedAssemblies] = React.useState([])
|
|
216
|
+
const [assemblyOutputNames, setAssemblyOutputNames] = React.useState([])
|
|
135
217
|
const [errorMessage, setErrorMessage] = React.useState('')
|
|
136
218
|
const [loadingMessage, setLoadingMessage] = React.useState('')
|
|
137
219
|
|
|
138
220
|
const clearAssemblySelection = React.useCallback(() => {
|
|
139
221
|
setRequestedAssemblies([])
|
|
222
|
+
setAssemblyOutputNames([])
|
|
140
223
|
setErrorMessage('')
|
|
141
224
|
}, [])
|
|
142
225
|
|
|
143
226
|
const { assembly, setCategory, setId, expandedAssemblies, assemblyComplete, canBeSubmitted, currentCategories } = useCombinatorialAssembly({ onValueChange: clearAssemblySelection, categories, plasmids })
|
|
144
227
|
const { requestSources, requestAssemblies } = useAssembler()
|
|
145
228
|
|
|
229
|
+
const onOutputNameChange = React.useCallback((index, name) => {
|
|
230
|
+
setAssemblyOutputNames((prev) => {
|
|
231
|
+
const next = [...prev];
|
|
232
|
+
next[index] = name;
|
|
233
|
+
return next;
|
|
234
|
+
});
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
const namesAreUnique = new Set(assemblyOutputNames).size === assemblyOutputNames.length;
|
|
238
|
+
const namesNonEmpty = assemblyOutputNames.length > 0 && assemblyOutputNames.every((n) => n.trim().length > 0);
|
|
239
|
+
const namesNotTooLong = assemblyOutputNames.every((n) => n.length <= MAX_OUTPUT_NAME_LENGTH);
|
|
240
|
+
const canDownload = requestedAssemblies.length > 0 && namesAreUnique && namesNonEmpty && namesNotTooLong;
|
|
241
|
+
|
|
146
242
|
const onSubmitAssembly = React.useCallback(async () => {
|
|
147
243
|
clearAssemblySelection()
|
|
148
244
|
const selectedPlasmids = assembly.map(({ plasmidIds }) => plasmidIds.map((id) => (plasmids.find((item) => item.id === id))))
|
|
@@ -155,6 +251,11 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
155
251
|
setLoadingMessage('Assembling...')
|
|
156
252
|
const assemblies = await requestAssemblies(resp, assemblyEnzymes)
|
|
157
253
|
setRequestedAssemblies(assemblies)
|
|
254
|
+
if (expandedAssemblies && expandedAssemblies.length > 0) {
|
|
255
|
+
setAssemblyOutputNames(
|
|
256
|
+
expandedAssemblies.map((_, i) => getDefaultAssemblyOutputName(i, expandedAssemblies, plasmids)),
|
|
257
|
+
)
|
|
258
|
+
}
|
|
158
259
|
} catch (e) {
|
|
159
260
|
if (e.assembly) {
|
|
160
261
|
errorMessage = (<><div style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>{error2String(e)}</div><div>Error assembling {e.assembly.map((p) => formatItemName(p.plasmid)).join(', ')}</div></>)
|
|
@@ -169,7 +270,9 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
169
270
|
|
|
170
271
|
const onDownloadAssemblies = React.useCallback(async () => {
|
|
171
272
|
try {
|
|
172
|
-
const files = getFilesToExportFromAssembler({
|
|
273
|
+
const files = getFilesToExportFromAssembler({
|
|
274
|
+
requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo, outputNames: assemblyOutputNames,
|
|
275
|
+
});
|
|
173
276
|
await downloadAssemblerFilesAsZip(files);
|
|
174
277
|
} catch (error) {
|
|
175
278
|
console.error('Error downloading assemblies:', error);
|
|
@@ -178,7 +281,7 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
178
281
|
severity: 'error',
|
|
179
282
|
});
|
|
180
283
|
}
|
|
181
|
-
}, [requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo, addAlert])
|
|
284
|
+
}, [requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo, addAlert, assemblyOutputNames])
|
|
182
285
|
|
|
183
286
|
const options = React.useMemo(() => assemblyComplete ? assembly : [...assembly, { category: '', plasmidIds: [] }], [assembly, assemblyComplete])
|
|
184
287
|
|
|
@@ -207,12 +310,17 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
207
310
|
variant="contained"
|
|
208
311
|
data-testid="assembler-download-assemblies-button"
|
|
209
312
|
sx={{ p: 2, fontSize: '1.2rem' }}
|
|
210
|
-
onClick={onDownloadAssemblies}
|
|
313
|
+
onClick={onDownloadAssemblies}
|
|
314
|
+
disabled={!canDownload}
|
|
315
|
+
title={!canDownload ? 'Output names must be unique, non-empty, and at most 255 characters' : ''}>Download Assemblies
|
|
211
316
|
</Button>}
|
|
212
317
|
</Box>
|
|
213
318
|
{errorMessage && <Alert severity="error" sx={{ my: 2, maxWidth: 300, margin: 'auto', fontSize: '1.2rem' }}>{errorMessage}</Alert>}
|
|
214
319
|
{requestedAssemblies.length > 0 &&
|
|
215
|
-
<AssemblerProductTable {...{
|
|
320
|
+
<AssemblerProductTable {...{
|
|
321
|
+
requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories,
|
|
322
|
+
assemblyOutputNames, onOutputNameChange,
|
|
323
|
+
}} />
|
|
216
324
|
}
|
|
217
325
|
|
|
218
326
|
</Box >
|
|
@@ -298,6 +406,35 @@ function LoadSyntaxButton({ setSyntax, addPlasmids, clearPlasmids }) {
|
|
|
298
406
|
}
|
|
299
407
|
|
|
300
408
|
|
|
409
|
+
function SyntaxOverviewButton({ syntax }) {
|
|
410
|
+
const [open, setOpen] = React.useState(false)
|
|
411
|
+
const [mode, setMode] = React.useState('detailed')
|
|
412
|
+
const msa = React.useMemo(() => syntax ? graphToMSA(partsToGraph(syntax.parts)) : [], [syntax])
|
|
413
|
+
return <>
|
|
414
|
+
<Button color="success" onClick={() => setOpen(true)} data-testid="assembler-syntax-overview-button">Syntax Overview</Button>
|
|
415
|
+
{open && <Dialog
|
|
416
|
+
open={open}
|
|
417
|
+
onClose={() => setOpen(false)}
|
|
418
|
+
fullWidth
|
|
419
|
+
maxWidth="xl"
|
|
420
|
+
PaperProps={{ sx: { height: '90vh' } }}
|
|
421
|
+
>
|
|
422
|
+
<DialogTitle>Syntax overview</DialogTitle>
|
|
423
|
+
<DialogContent>
|
|
424
|
+
<FormControlLabel
|
|
425
|
+
control={
|
|
426
|
+
<Switch
|
|
427
|
+
checked={mode === 'detailed'}
|
|
428
|
+
onChange={(e) => setMode(e.target.checked ? 'detailed' : 'compact')}
|
|
429
|
+
/>
|
|
430
|
+
}
|
|
431
|
+
label={mode === 'compact' ? 'Compact' : 'Detailed'}
|
|
432
|
+
/>
|
|
433
|
+
<SyntaxOverviewTable msa={msa} mode={mode} parts={syntax.parts} />
|
|
434
|
+
</DialogContent>
|
|
435
|
+
</Dialog>}
|
|
436
|
+
</>
|
|
437
|
+
}
|
|
301
438
|
|
|
302
439
|
function Assembler() {
|
|
303
440
|
const [syntax, setSyntax] = React.useState(null);
|
|
@@ -331,6 +468,7 @@ function Assembler() {
|
|
|
331
468
|
</Alert>
|
|
332
469
|
<ButtonGroup>
|
|
333
470
|
<LoadSyntaxButton setSyntax={setSyntax} addPlasmids={addPlasmids} clearPlasmids={clearPlasmids} />
|
|
471
|
+
{syntax && <SyntaxOverviewButton syntax={syntax} />}
|
|
334
472
|
{syntax && <UploadPlasmidsButton addPlasmids={addPlasmids} syntax={syntax} />}
|
|
335
473
|
{syntax && <Button color="error" onClick={clearLoadedPlasmids}>Remove uploaded plasmids</Button>}
|
|
336
474
|
</ButtonGroup>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Typography, Table, TableBody, TableRow, TableCell } from '@mui/material';
|
|
3
|
+
import { partDataToDisplayData } from './assembler_utils';
|
|
4
|
+
import { AssemblerPartContainer, AssemblerPartCore, DisplayInside, DisplayOverhang } from './AssemblerPart';
|
|
5
|
+
import AssemblerPart from './AssemblerPart';
|
|
6
|
+
import { GRAPH_SPACER } from './graph_utils';
|
|
7
|
+
|
|
8
|
+
function OverhangRow({ row, parts, mode = 'detailed' }) {
|
|
9
|
+
|
|
10
|
+
const rows2iterate = [...row];
|
|
11
|
+
const actualRows =[];
|
|
12
|
+
|
|
13
|
+
let currentCell = [rows2iterate.shift(), 1];
|
|
14
|
+
while (currentCell[0] !== undefined) {
|
|
15
|
+
if (rows2iterate[0] === GRAPH_SPACER) {
|
|
16
|
+
currentCell[1]++;
|
|
17
|
+
rows2iterate.shift();
|
|
18
|
+
} else {
|
|
19
|
+
actualRows.push(currentCell);
|
|
20
|
+
currentCell = [rows2iterate.shift(), 1];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
actualRows.forEach(cell => {
|
|
24
|
+
const [leftOverhang, rightOverhang] = cell[0].split('-');
|
|
25
|
+
const data = parts.find(part => part.left_overhang === leftOverhang && part.right_overhang === rightOverhang) ||
|
|
26
|
+
{
|
|
27
|
+
left_overhang: leftOverhang,
|
|
28
|
+
right_overhang: rightOverhang,
|
|
29
|
+
left_inside: '',
|
|
30
|
+
right_inside: '',
|
|
31
|
+
left_codon_start: 0,
|
|
32
|
+
right_codon_start: 0,
|
|
33
|
+
color: 'lightgray',
|
|
34
|
+
glyph: 'engineered-region',
|
|
35
|
+
}
|
|
36
|
+
cell.push(data);
|
|
37
|
+
});
|
|
38
|
+
return (
|
|
39
|
+
<TableRow >
|
|
40
|
+
{actualRows.flatMap(
|
|
41
|
+
(cell, index) => {
|
|
42
|
+
const showRight = index === actualRows.length - 1;
|
|
43
|
+
|
|
44
|
+
const colSpan = (cell[1]-1)*2 + 1;
|
|
45
|
+
const { left_overhang, right_overhang, left_inside, color, glyph } = cell[2];
|
|
46
|
+
const { leftTranslationOverhang, leftTranslationInside, leftOverhangRc, rightOverhangRc, leftInsideRc } = partDataToDisplayData(cell[2]);
|
|
47
|
+
if (mode === 'compact') {
|
|
48
|
+
return <React.Fragment key={`row-${index}-compact`}>
|
|
49
|
+
<TableCell sx={{padding: 0, borderRight: 'solid 1px gray'}} >
|
|
50
|
+
<AssemblerPartContainer>
|
|
51
|
+
<DisplayOverhang overhang={left_overhang} overhangRc={leftOverhangRc} translation={leftTranslationOverhang} isRight={false} />
|
|
52
|
+
{left_inside && <DisplayInside inside={left_inside} insideRc={leftInsideRc} translation={leftTranslationInside} isRight={false} />}
|
|
53
|
+
</AssemblerPartContainer>
|
|
54
|
+
</TableCell>
|
|
55
|
+
<TableCell sx={{ padding: 0, textAlign: "center", borderRight: 'solid 1px gray' }} colSpan={colSpan}>
|
|
56
|
+
<AssemblerPartContainer>
|
|
57
|
+
<AssemblerPartCore color={color} glyph={glyph} />
|
|
58
|
+
</AssemblerPartContainer>
|
|
59
|
+
</TableCell>
|
|
60
|
+
{showRight && (
|
|
61
|
+
<TableCell
|
|
62
|
+
key={index}
|
|
63
|
+
sx={{ padding: 0, borderRight: 'solid 1px gray' }}
|
|
64
|
+
>
|
|
65
|
+
<AssemblerPartContainer>
|
|
66
|
+
<DisplayOverhang overhang={right_overhang} overhangRc={rightOverhangRc} isRight={true} />
|
|
67
|
+
</AssemblerPartContainer>
|
|
68
|
+
</TableCell>
|
|
69
|
+
)}
|
|
70
|
+
</React.Fragment>
|
|
71
|
+
}
|
|
72
|
+
if (mode === 'detailed') {
|
|
73
|
+
return (
|
|
74
|
+
<TableCell colSpan={cell[1]} key={`row-${index}-detailed`} sx={{ border: 1, py: 0 }}>
|
|
75
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', flexDirection: 'column', alignItems: 'center' }}>
|
|
76
|
+
<Typography variant="h6" >{cell[2].name}</Typography>
|
|
77
|
+
<Typography variant="body2" >{cell[2].info}</Typography>
|
|
78
|
+
<AssemblerPart data={cell[2]} />
|
|
79
|
+
</Box>
|
|
80
|
+
</TableCell>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
)}
|
|
85
|
+
</TableRow>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function SyntaxOverviewTable({ msa, mode, parts }) {
|
|
90
|
+
return (
|
|
91
|
+
<Box sx={{ overflowY: 'auto', overflowX: 'auto' }}>
|
|
92
|
+
<Table data-testid="overhangs-preview-table" sx={{ borderCollapse: 'separate', borderSpacing: 0 }}>
|
|
93
|
+
<TableBody>
|
|
94
|
+
{msa.map((row, index) => (
|
|
95
|
+
<OverhangRow key={index} row={row} mode={mode} parts={parts} />
|
|
96
|
+
))}
|
|
97
|
+
</TableBody>
|
|
98
|
+
</Table>
|
|
99
|
+
</Box>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default SyntaxOverviewTable
|
|
@@ -5,6 +5,9 @@ import { openCycleAtNode } from './graph_utils';
|
|
|
5
5
|
import { downloadBlob, formatStateForJsonExport, getZipFileBlob } from '@opencloning/utils/readNwrite';
|
|
6
6
|
import { getGraftSequenceId } from '@opencloning/utils/network';
|
|
7
7
|
import { TextReader} from '@zip.js/zip.js';
|
|
8
|
+
import { editGenbankSequenceNameFromTextContent } from '@opencloning/utils';
|
|
9
|
+
|
|
10
|
+
export const MAX_OUTPUT_NAME_LENGTH = 250;
|
|
8
11
|
|
|
9
12
|
export function tripletsToTranslation(triplets) {
|
|
10
13
|
if (!triplets) return ''
|
|
@@ -161,11 +164,33 @@ export function categoryFilter(category, categories, previousCategoryId) {
|
|
|
161
164
|
return previousCategory?.right_overhang === category.left_overhang
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
export function
|
|
167
|
+
export function getDefaultAssemblyOutputName(assemblyIndex, expandedAssemblies, plasmids) {
|
|
168
|
+
const assembly = expandedAssemblies[assemblyIndex];
|
|
169
|
+
const plasmidNames = assembly.map(part => plasmids.find(p => p.id === part)?.plasmid_name ?? '');
|
|
170
|
+
let name = `${String(assemblyIndex + 1).padStart(3, '0')}_${plasmidNames.join('+')}`.replaceAll('/', '_').replaceAll(' ', '_');
|
|
171
|
+
if (name.length > MAX_OUTPUT_NAME_LENGTH) {
|
|
172
|
+
name = `${String(assemblyIndex + 1).padStart(3, '0')}_construct`;
|
|
173
|
+
}
|
|
174
|
+
return name;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function sanitizeOutputName(name) {
|
|
178
|
+
let sanitized = name.replaceAll('/', '_').replaceAll(' ', '_');
|
|
179
|
+
if (sanitized.length > MAX_OUTPUT_NAME_LENGTH) {
|
|
180
|
+
sanitized = sanitized.slice(0, MAX_OUTPUT_NAME_LENGTH);
|
|
181
|
+
}
|
|
182
|
+
return sanitized;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getFilesToExportFromAssembler({requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo, outputNames}) {
|
|
186
|
+
if (outputNames.length !== requestedAssemblies.length) {
|
|
187
|
+
throw new Error('outputNames must be the same length as requestedAssemblies');
|
|
188
|
+
}
|
|
165
189
|
const files2Export = [];
|
|
166
|
-
const categoryNames = ['Assembly', ...currentCategories.map(categoryId => categories.find(c => c.id === categoryId).displayName)];
|
|
190
|
+
const categoryNames = ['Assembly', 'Name', ...currentCategories.map(categoryId => categories.find(c => c.id === categoryId).displayName)];
|
|
167
191
|
const assemblyNames = expandedAssemblies.map((assembly, index) => {
|
|
168
|
-
|
|
192
|
+
const plasmidNames = assembly.map(part => plasmids.find(p => p.id === part).plasmid_name);
|
|
193
|
+
return [index + 1, outputNames[index], ...plasmidNames];
|
|
169
194
|
});
|
|
170
195
|
for (const delimiter of ['\t', ',']) {
|
|
171
196
|
const tableHeader = categoryNames.join(delimiter);
|
|
@@ -179,18 +204,19 @@ export function getFilesToExportFromAssembler({requestedAssemblies, expandedAsse
|
|
|
179
204
|
}
|
|
180
205
|
|
|
181
206
|
for (let i = 0; i < requestedAssemblies.length; i++) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
name = `${String(i + 1).padStart(3, '0')}_construct`;
|
|
185
|
-
}
|
|
186
|
-
const requestedAssembly = requestedAssemblies[i];
|
|
207
|
+
const name = sanitizeOutputName(outputNames[i]);
|
|
208
|
+
const requestedAssembly = JSON.parse(JSON.stringify(requestedAssemblies[i]));
|
|
187
209
|
const jsonContent = formatStateForJsonExport({...requestedAssembly, appInfo});
|
|
210
|
+
const finalSequenceId = getGraftSequenceId(requestedAssembly)
|
|
211
|
+
const finalSequence = requestedAssembly.sequences.find(s => s.id === finalSequenceId)
|
|
212
|
+
finalSequence.file_content = editGenbankSequenceNameFromTextContent(finalSequence.file_content, name);
|
|
213
|
+
const finalSource = requestedAssembly.sources.find(s => s.id === finalSequenceId)
|
|
214
|
+
finalSource.output_name = name;
|
|
188
215
|
files2Export.push({
|
|
189
216
|
name: `${name}.json`,
|
|
190
217
|
content: JSON.stringify(jsonContent, null, 2),
|
|
191
218
|
});
|
|
192
|
-
|
|
193
|
-
const finalSequence = requestedAssembly.sequences.find(s => s.id === finalSequenceId)
|
|
219
|
+
|
|
194
220
|
files2Export.push({
|
|
195
221
|
name: `${name}.gbk`,
|
|
196
222
|
content: finalSequence.file_content,
|
|
@@ -1,6 +1,6 @@
|
|
|
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, getFilesToExportFromAssembler } from "./assembler_utils";
|
|
3
|
+
import { assignSequenceToSyntaxPart, simplifyDigestFragment, reverseComplementSimplifiedDigestFragment, tripletsToTranslation, partDataToDisplayData, arrayCombinations, getFilesToExportFromAssembler, getDefaultAssemblyOutputName } from "./assembler_utils";
|
|
4
4
|
import { partsToEdgesGraph } from "./graph_utils";
|
|
5
5
|
|
|
6
6
|
|
|
@@ -314,13 +314,25 @@ const dummyData = {
|
|
|
314
314
|
|
|
315
315
|
|
|
316
316
|
|
|
317
|
+
describe('getDefaultAssemblyOutputName', () => {
|
|
318
|
+
it('returns default name from plasmid names', () => {
|
|
319
|
+
expect(getDefaultAssemblyOutputName(0, dummyData.expandedAssemblies, dummyData.plasmids)).toBe('001_p1+p5+p7');
|
|
320
|
+
expect(getDefaultAssemblyOutputName(1, dummyData.expandedAssemblies, dummyData.plasmids)).toBe('002_p1+p2+p3');
|
|
321
|
+
});
|
|
322
|
+
it('returns _construct suffix when name is too long', () => {
|
|
323
|
+
const longPlasmids = dummyData.plasmids.map(plasmid => ({ ...plasmid, plasmid_name: 'p1'.repeat(100) }));
|
|
324
|
+
expect(getDefaultAssemblyOutputName(0, dummyData.expandedAssemblies, longPlasmids)).toBe('001_construct');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
317
328
|
describe('getFilesToExportFromAssembler', () => {
|
|
318
329
|
it('returns the correct files', () => {
|
|
319
|
-
const
|
|
330
|
+
const outputNames = dummyData.expandedAssemblies.map((_, i) => getDefaultAssemblyOutputName(i, dummyData.expandedAssemblies, dummyData.plasmids));
|
|
331
|
+
const files = getFilesToExportFromAssembler({ ...dummyData, outputNames });
|
|
320
332
|
expect(files[0].name).toBe('assemblies.tsv');
|
|
321
|
-
expect(files[0].content).toBe('Assembly\tCategory 1\tCategory 2\tCategory 3\n1\tp1\tp5\tp7\n2\tp1\tp2\tp3');
|
|
333
|
+
expect(files[0].content).toBe('Assembly\tName\tCategory 1\tCategory 2\tCategory 3\n1\t001_p1+p5+p7\tp1\tp5\tp7\n2\t002_p1+p2+p3\tp1\tp2\tp3');
|
|
322
334
|
expect(files[1].name).toBe('assemblies.csv');
|
|
323
|
-
expect(files[1].content).toBe('Assembly,Category 1,Category 2,Category 3\n1,p1,p5,p7\n2,p1,p2,p3');
|
|
335
|
+
expect(files[1].content).toBe('Assembly,Name,Category 1,Category 2,Category 3\n1,001_p1+p5+p7,p1,p5,p7\n2,002_p1+p2+p3,p1,p2,p3');
|
|
324
336
|
|
|
325
337
|
const fileNames = ['001_p1+p5+p7', '002_p1+p2+p3'];
|
|
326
338
|
for (let i = 0; i < 2; i++) {
|
|
@@ -328,13 +340,20 @@ describe('getFilesToExportFromAssembler', () => {
|
|
|
328
340
|
const fileIndex2 = fileIndex1 + 1;
|
|
329
341
|
expect(files[fileIndex1].name).toBe(`${fileNames[i]}.json`);
|
|
330
342
|
const cloningStrategy = JSON.parse(files[fileIndex1].content);
|
|
331
|
-
|
|
332
|
-
expect(cloningStrategy.
|
|
343
|
+
// Messed up by change of name, not worth checking
|
|
344
|
+
// expect(cloningStrategy.sequences).toEqual(dummyData.requestedAssemblies[i].sequences);
|
|
345
|
+
// expect(cloningStrategy.sources).toEqual(dummyData.requestedAssemblies[i].sources);
|
|
333
346
|
expect(cloningStrategy.primers).toEqual(dummyData.requestedAssemblies[i].primers);
|
|
347
|
+
expect(cloningStrategy.sources[cloningStrategy.sources.length - 1].output_name).toBe(fileNames[i]);
|
|
348
|
+
const firstline = cloningStrategy.sequences[cloningStrategy.sequences.length - 1].file_content.split('\n')[0];
|
|
349
|
+
expect(firstline).toContain(fileNames[i]);
|
|
334
350
|
|
|
335
351
|
expect(files[fileIndex2].name).toBe(`${fileNames[i]}.gbk`);
|
|
336
352
|
const genbankContent = files[fileIndex2].content;
|
|
337
|
-
|
|
353
|
+
const expectedFileContent = dummyData.requestedAssemblies[i].sequences[dummyData.requestedAssemblies[i].sequences.length - 1].file_content
|
|
354
|
+
expect(genbankContent).toBe(expectedFileContent.replaceAll('final_product', fileNames[i]).replaceAll('expression_clone_lacZ', fileNames[i]));
|
|
355
|
+
const firstline2 = genbankContent.split('\n')[0];
|
|
356
|
+
expect(firstline2).toContain(fileNames[i]);
|
|
338
357
|
}
|
|
339
358
|
|
|
340
359
|
|
|
@@ -343,11 +362,20 @@ describe('getFilesToExportFromAssembler', () => {
|
|
|
343
362
|
const dummyData2 = {
|
|
344
363
|
...dummyData,
|
|
345
364
|
plasmids: dummyData.plasmids.map(plasmid => ({...plasmid, plasmid_name: 'p1'.repeat(100)})),
|
|
346
|
-
}
|
|
347
|
-
const
|
|
365
|
+
};
|
|
366
|
+
const outputNames = dummyData2.expandedAssemblies.map((_, i) => getDefaultAssemblyOutputName(i, dummyData2.expandedAssemblies, dummyData2.plasmids));
|
|
367
|
+
const files = getFilesToExportFromAssembler({ ...dummyData2, outputNames });
|
|
348
368
|
expect(files[2].name).toBe('001_construct.json');
|
|
349
369
|
expect(files[3].name).toBe('001_construct.gbk');
|
|
350
370
|
expect(files[4].name).toBe('002_construct.json');
|
|
351
371
|
expect(files[5].name).toBe('002_construct.gbk');
|
|
352
|
-
})
|
|
353
|
-
|
|
372
|
+
});
|
|
373
|
+
it('uses custom outputNames when provided', () => {
|
|
374
|
+
const customNames = ['my_assembly_1', 'my_assembly_2'];
|
|
375
|
+
const files = getFilesToExportFromAssembler({ ...dummyData, outputNames: customNames });
|
|
376
|
+
expect(files[2].name).toBe('my_assembly_1.json');
|
|
377
|
+
expect(files[3].name).toBe('my_assembly_1.gbk');
|
|
378
|
+
expect(files[4].name).toBe('my_assembly_2.json');
|
|
379
|
+
expect(files[5].name).toBe('my_assembly_2.gbk');
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { default as EditTextDialog } from '../form/EditTextDialog';
|
|
1
2
|
export { default as AssemblerPart } from './AssemblerPart';
|
|
2
3
|
export { AssemblerPartCore } from './AssemblerPart';
|
|
3
4
|
export { getSvgByGlyph } from './sbol_visual_glyphs';
|
|
@@ -7,3 +8,4 @@ export { partsToGraph, graphToMSA, graphHasCycle, partsToEdgesGraph, GRAPH_SPACE
|
|
|
7
8
|
export { usePlasmidsLogic } from './usePlasmidsLogic.js';
|
|
8
9
|
export { default as PlasmidSyntaxTable } from './PlasmidSyntaxTable.jsx';
|
|
9
10
|
export { default as ExistingSyntaxDialog } from './ExistingSyntaxDialog.jsx';
|
|
11
|
+
export { default as SyntaxOverviewTable } from './SyntaxOverviewTable.jsx';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Alert } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
export default function EditTextDialog({
|
|
5
|
+
open,
|
|
6
|
+
value,
|
|
7
|
+
onClose,
|
|
8
|
+
onSave,
|
|
9
|
+
title = 'Edit',
|
|
10
|
+
placeholder = 'Enter text...',
|
|
11
|
+
multiline = false,
|
|
12
|
+
maxLength,
|
|
13
|
+
}) {
|
|
14
|
+
const [tempValue, setTempValue] = React.useState(value || '');
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
if (open) setTempValue(value || '');
|
|
18
|
+
}, [open, value]);
|
|
19
|
+
|
|
20
|
+
const handleChange = (e) => {
|
|
21
|
+
const newValue = multiline ? e.target.value.replace(/\n/g, '') : e.target.value;
|
|
22
|
+
setTempValue(maxLength ? newValue.slice(0, maxLength) : newValue);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
|
27
|
+
<DialogTitle>{title}</DialogTitle>
|
|
28
|
+
<DialogContent>
|
|
29
|
+
{maxLength && <Alert severity="info" sx={{ mb: 2 }}>Maximum input length is {maxLength} characters</Alert>}
|
|
30
|
+
<TextField
|
|
31
|
+
autoFocus
|
|
32
|
+
multiline={multiline}
|
|
33
|
+
rows={multiline ? 6 : undefined}
|
|
34
|
+
fullWidth
|
|
35
|
+
value={tempValue}
|
|
36
|
+
onChange={handleChange}
|
|
37
|
+
placeholder={placeholder}
|
|
38
|
+
inputProps={maxLength ? { maxLength } : undefined}
|
|
39
|
+
sx={{ mt: 1 }}
|
|
40
|
+
/>
|
|
41
|
+
</DialogContent>
|
|
42
|
+
<DialogActions>
|
|
43
|
+
<Button onClick={onClose}>Cancel</Button>
|
|
44
|
+
<Button onClick={() => onSave(tempValue)} variant="contained">Save</Button>
|
|
45
|
+
</DialogActions>
|
|
46
|
+
</Dialog>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -75,7 +75,6 @@ function ServerStaticFileSelect({ onFileSelected, multiple = false, type = 'sequ
|
|
|
75
75
|
const label = type === 'sequence' ? 'Sequence' : 'Syntax';
|
|
76
76
|
|
|
77
77
|
const onOptionsChange = React.useCallback((event, value) => {
|
|
78
|
-
console.log('onOptionsChange', value);
|
|
79
78
|
if (multiple && type === 'sequence') {
|
|
80
79
|
if (value.includes('__all__')) {
|
|
81
80
|
const allSequences = options.filter((option) => option !== '__all__');
|
package/src/components/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as OpenCloning } from './OpenCloning';
|
|
2
2
|
export { default as MainAppBar } from './navigation/MainAppBar';
|
|
3
|
+
export { default as EditTextDialog } from './form/EditTextDialog';
|
|
3
4
|
export { default as useUrlParamsLoader } from '../hooks/useUrlParamsLoader';
|
|
4
5
|
export { default as useInitializeApp } from '../hooks/useInitializeApp';
|
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.5.
|
|
2
|
+
export const version = "1.5.3";
|