@opencloning/ui 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/package.json +8 -8
- package/src/components/assembler/Assembler.cy.jsx +1 -137
- package/src/components/assembler/Assembler.jsx +23 -87
- package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +66 -34
- package/src/components/assembler/ExistingSyntaxDialog.jsx +39 -8
- package/src/components/assembler/PlasmidSyntaxTable.jsx +1 -1
- package/src/components/assembler/UploadPlasmidsButton.cy.jsx +239 -0
- package/src/components/assembler/UploadPlasmidsButton.jsx +121 -0
- package/src/components/assembler/usePlasmidsLogic.js +9 -5
- package/src/components/form/ServerStaticFileSelect.cy.jsx +357 -0
- package/src/components/form/ServerStaticFileSelect.jsx +120 -0
- package/src/components/sources/Source.jsx +3 -0
- package/src/components/sources/SourceServerStaticFile.jsx +26 -0
- package/src/components/sources/SourceTypeSelector.jsx +4 -1
- package/src/hooks/useRequestForEffect.js +28 -0
- package/src/hooks/useServerStaticFiles.js +56 -0
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#611](https://github.com/manulera/OpenCloning_frontend/pull/611) [`74a58c4`](https://github.com/manulera/OpenCloning_frontend/commit/74a58c463a32bf6443eaf062094dc4aac5c76b5a) Thanks [@manulera](https://github.com/manulera)! - Adds functionality to load files (sequences and syntaxes) from a local public folder, providing an alternative to uploading files manually. The implementation enables users to pre-configure collections of sequences and syntaxes that can be easily selected through a UI.
|
|
8
|
+
|
|
9
|
+
**Changes:**
|
|
10
|
+
|
|
11
|
+
- Added `useRequestForEffect` hook for managing async requests with retry capability
|
|
12
|
+
- Added `useServerStaticFiles` hook for fetching and managing local file collections
|
|
13
|
+
- Added `ServerStaticFileSelect` component for selecting files from the local collection with category filtering
|
|
14
|
+
- Added `SourceServerStaticFile` component and integrated it into the source selection flow
|
|
15
|
+
- Added local file loading capability to the assembler's plasmid uploader and syntax loader
|
|
16
|
+
- Configured build system to copy example collection folder to public directory during development
|
|
17
|
+
- Added comprehensive test coverage for all new components and functionality
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies []:
|
|
22
|
+
- @opencloning/store@1.4.0
|
|
23
|
+
- @opencloning/utils@1.4.0
|
|
24
|
+
|
|
25
|
+
## 1.3.3
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- [#607](https://github.com/manulera/OpenCloning_frontend/pull/607) [`1e4bfbf`](https://github.com/manulera/OpenCloning_frontend/commit/1e4bfbfb805e841d9a91cf650ebf632ac62b1248) Thanks [@manulera](https://github.com/manulera)! - Syntax validation and fix longestFeature when it's empty
|
|
30
|
+
|
|
31
|
+
- Updated dependencies []:
|
|
32
|
+
- @opencloning/store@1.3.3
|
|
33
|
+
- @opencloning/utils@1.3.3
|
|
34
|
+
|
|
3
35
|
## 1.3.2
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloning/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,15 +25,15 @@
|
|
|
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.
|
|
29
|
-
"@opencloning/utils": "1.
|
|
30
|
-
"@teselagen/bio-parsers": "^0.4.
|
|
31
|
-
"@teselagen/ove": "^0.8.
|
|
32
|
-
"@teselagen/range-utils": "^0.3.
|
|
33
|
-
"@teselagen/sequence-utils": "^0.3.
|
|
28
|
+
"@opencloning/store": "1.4.0",
|
|
29
|
+
"@opencloning/utils": "1.4.0",
|
|
30
|
+
"@teselagen/bio-parsers": "^0.4.34",
|
|
31
|
+
"@teselagen/ove": "^0.8.34",
|
|
32
|
+
"@teselagen/range-utils": "^0.3.20",
|
|
33
|
+
"@teselagen/sequence-utils": "^0.3.42",
|
|
34
34
|
"@zip.js/zip.js": "^2.7.62",
|
|
35
35
|
"axios": "^1.12.2",
|
|
36
|
-
"lodash-es": "^4.17.
|
|
36
|
+
"lodash-es": "^4.17.23",
|
|
37
37
|
"react-draggable": "^4.4.6"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
/* eslint-disable camelcase */
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
4
|
-
import { AssemblerComponent
|
|
5
|
-
import mocloSyntax from '../../../../../cypress/test_files/syntax/moclo_syntax.json';
|
|
6
|
-
|
|
7
|
-
mocloSyntax.overhangNames = {
|
|
8
|
-
...mocloSyntax.overhangNames,
|
|
9
|
-
CCCT: 'CCCT_overhang',
|
|
10
|
-
AACG: 'AACG_overhang',
|
|
11
|
-
};
|
|
4
|
+
import { AssemblerComponent } from './Assembler';
|
|
12
5
|
|
|
13
6
|
// Test config
|
|
14
7
|
const testConfig = {
|
|
@@ -233,132 +226,3 @@ describe('<AssemblerComponent />', () => {
|
|
|
233
226
|
});
|
|
234
227
|
});
|
|
235
228
|
|
|
236
|
-
describe('<UploadPlasmidsButton />', () => {
|
|
237
|
-
beforeEach(() => {
|
|
238
|
-
cy.window().then((win) => {
|
|
239
|
-
win.localStorage.clear();
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('calls addPlasmids with correctly formatted valid plasmid', () => {
|
|
244
|
-
const addPlasmidsSpy = cy.spy().as('addPlasmidsSpy');
|
|
245
|
-
|
|
246
|
-
cy.mount(
|
|
247
|
-
<ConfigProvider config={testConfig}>
|
|
248
|
-
<UploadPlasmidsButton addPlasmids={addPlasmidsSpy} syntax={mocloSyntax} />
|
|
249
|
-
</ConfigProvider>,
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
|
|
253
|
-
'cypress/test_files/syntax/pYTK002.gb',
|
|
254
|
-
'cypress/test_files/syntax/moclo_ytk_multi_part.gb',
|
|
255
|
-
'cypress/test_files/syntax/pYTK095.gb',
|
|
256
|
-
'cypress/test_files/sequencing/locus.gb'],
|
|
257
|
-
{ force: true });
|
|
258
|
-
|
|
259
|
-
// Wait for the dialog to appear (indicating plasmids were processed)
|
|
260
|
-
cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
|
|
261
|
-
|
|
262
|
-
cy.get('@addPlasmidsSpy').should('not.have.been.called');
|
|
263
|
-
|
|
264
|
-
cy.get('[data-testid="invalid-plasmids-box"]').contains('Invalid Plasmids').should('exist');
|
|
265
|
-
cy.get('[data-testid="valid-plasmids-box"]').contains('Valid Plasmids').should('exist');
|
|
266
|
-
|
|
267
|
-
cy.get('[data-testid="invalid-plasmids-box"]').contains('pYTK057')
|
|
268
|
-
cy.get('[data-testid="invalid-plasmids-box"]').contains('moclo_ytk_multi_part.gb')
|
|
269
|
-
cy.get('[data-testid="invalid-plasmids-box"] .MuiChip-label').contains('ATCC-TGGC')
|
|
270
|
-
cy.get('[data-testid="invalid-plasmids-box"] .MuiChip-label').contains('CCCT-AACG (CCCT_overhang-AACG_overhang)')
|
|
271
|
-
cy.get('[data-testid="invalid-plasmids-box"]').contains('Contains multiple parts')
|
|
272
|
-
cy.get('[data-testid="invalid-plasmids-box"]').contains('locus.gb')
|
|
273
|
-
|
|
274
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(0).should('contain', 'pYTK002')
|
|
275
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(1).should('contain', 'pYTK002.gb')
|
|
276
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(2).should('contain', 'CCCT-AACG (CCCT_overhang-AACG_overhang)')
|
|
277
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(3).should('contain', '1')
|
|
278
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(4).should('contain', 'ConS')
|
|
279
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).then(($el) => {
|
|
280
|
-
const bgColor = window.getComputedStyle($el[0]).backgroundColor;
|
|
281
|
-
cy.wrap(bgColor).should('equal', 'rgb(132, 197, 222)');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(0).should('contain', 'pYTK095')
|
|
285
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(1).should('contain', 'pYTK095.gb')
|
|
286
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(2).should('contain', 'TACA-CCCT (TACA-CCCT_overhang)')
|
|
287
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(3).should('contain', 'Spans multiple parts')
|
|
288
|
-
cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(4).should('contain', 'AmpR')
|
|
289
|
-
|
|
290
|
-
// Click the import button
|
|
291
|
-
cy.contains('button', 'Import valid plasmids').click();
|
|
292
|
-
|
|
293
|
-
// Verify addPlasmids was called
|
|
294
|
-
cy.get('@addPlasmidsSpy').should('have.been.called');
|
|
295
|
-
|
|
296
|
-
// Verify it was called with an array and check structure
|
|
297
|
-
cy.get('@addPlasmidsSpy').then((spy) => {
|
|
298
|
-
const firstCall = spy.getCall(0);
|
|
299
|
-
console.log('firstCall', firstCall.args);
|
|
300
|
-
cy.wrap(firstCall.args[0]).should('be.an', 'array');
|
|
301
|
-
cy.wrap(firstCall.args[0]).should('have.length', 2);
|
|
302
|
-
|
|
303
|
-
const firstPlasmid = firstCall.args[0][0];
|
|
304
|
-
|
|
305
|
-
cy.wrap(firstPlasmid.file_name).should('equal', 'pYTK002.gb');
|
|
306
|
-
cy.wrap(firstPlasmid.plasmid_name).should('equal', 'pYTK002.gb (ConS)');
|
|
307
|
-
cy.wrap(firstPlasmid.left_overhang).should('equal', 'CCCT');
|
|
308
|
-
cy.wrap(firstPlasmid.right_overhang).should('equal', 'AACG');
|
|
309
|
-
cy.wrap(firstPlasmid.key).should('equal', 'CCCT-AACG');
|
|
310
|
-
|
|
311
|
-
const {appData} = firstPlasmid.sequenceData;
|
|
312
|
-
|
|
313
|
-
cy.wrap(appData.fileName).should('equal', 'pYTK002.gb');
|
|
314
|
-
cy.wrap(appData.correspondingParts).should('deep.equal', ['CCCT-AACG']);
|
|
315
|
-
cy.wrap(appData.correspondingPartsNames).should('deep.equal', ["CCCT_overhang-AACG_overhang"]);
|
|
316
|
-
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
it('does not allow to submit when all plasmids are invalid', () => {
|
|
322
|
-
cy.mount(
|
|
323
|
-
<ConfigProvider config={testConfig}>
|
|
324
|
-
<UploadPlasmidsButton addPlasmids={() => {}} syntax={mocloSyntax} />
|
|
325
|
-
</ConfigProvider>,
|
|
326
|
-
);
|
|
327
|
-
cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
|
|
328
|
-
'cypress/test_files/sequencing/locus.gb'],
|
|
329
|
-
{ force: true });
|
|
330
|
-
|
|
331
|
-
// Wait for the dialog to appear (indicating plasmids were processed)
|
|
332
|
-
cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
|
|
333
|
-
|
|
334
|
-
cy.get('[data-testid="invalid-plasmids-box"]').contains('Invalid Plasmids').should('exist');
|
|
335
|
-
cy.get('[data-testid="valid-plasmids-box"]').should('not.exist');
|
|
336
|
-
|
|
337
|
-
cy.get('button').contains('Import valid plasmids').should('be.disabled');
|
|
338
|
-
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('cancelling does not call addPlasmids', () => {
|
|
342
|
-
const addPlasmidsSpy = cy.spy().as('addPlasmidsSpy');
|
|
343
|
-
cy.mount(
|
|
344
|
-
<ConfigProvider config={testConfig}>
|
|
345
|
-
<UploadPlasmidsButton addPlasmids={addPlasmidsSpy} syntax={mocloSyntax} />
|
|
346
|
-
</ConfigProvider>,
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
|
|
350
|
-
'cypress/test_files/syntax/pYTK002.gb',
|
|
351
|
-
'cypress/test_files/syntax/moclo_ytk_multi_part.gb',
|
|
352
|
-
'cypress/test_files/syntax/pYTK095.gb',
|
|
353
|
-
'cypress/test_files/sequencing/locus.gb'],
|
|
354
|
-
{ force: true });
|
|
355
|
-
|
|
356
|
-
cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
|
|
357
|
-
|
|
358
|
-
cy.get('button').contains('Cancel').click();
|
|
359
|
-
|
|
360
|
-
cy.get('@addPlasmidsSpy').should('not.have.been.called');
|
|
361
|
-
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import {
|
|
3
|
-
Alert, Autocomplete, Box, Button, CircularProgress,
|
|
4
|
-
DialogActions, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, Stack, Table,
|
|
3
|
+
Alert, Autocomplete, Box, Button, CircularProgress, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, Stack, Table,
|
|
5
4
|
TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, ButtonGroup
|
|
6
5
|
} from '@mui/material'
|
|
7
6
|
import { Clear as ClearIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
|
@@ -9,39 +8,18 @@ import { useAssembler } from './useAssembler';
|
|
|
9
8
|
import { useDispatch } from 'react-redux';
|
|
10
9
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
11
10
|
import AssemblerPart from './AssemblerPart';
|
|
12
|
-
|
|
11
|
+
|
|
13
12
|
import useCombinatorialAssembly from './useCombinatorialAssembly';
|
|
14
|
-
import { usePlasmidsLogic } from './usePlasmidsLogic';
|
|
15
|
-
import PlasmidSyntaxTable from './PlasmidSyntaxTable';
|
|
16
13
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
17
14
|
import error2String from '@opencloning/utils/error2String';
|
|
18
15
|
import { categoryFilter } from './assembler_utils';
|
|
16
|
+
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
17
|
+
import useHttpClient from '../../hooks/useHttpClient';
|
|
18
|
+
import useAlerts from '../../hooks/useAlerts';
|
|
19
|
+
import UploadPlasmidsButton from './UploadPlasmidsButton';
|
|
19
20
|
|
|
20
|
-
const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
|
|
21
|
-
|
|
22
|
-
function formatPlasmid(sequenceData) {
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
const { fileName, correspondingParts, longestFeature } = appData;
|
|
26
|
-
const [left_overhang, right_overhang] = correspondingParts[0].split('-');
|
|
27
|
-
|
|
28
|
-
let plasmidName = fileName;
|
|
29
|
-
if (longestFeature[0]?.name) {
|
|
30
|
-
plasmidName += ` (${longestFeature[0].name})`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
type: 'loadedFile',
|
|
35
|
-
plasmid_name: plasmidName,
|
|
36
|
-
file_name: fileName,
|
|
37
|
-
left_overhang,
|
|
38
|
-
right_overhang,
|
|
39
|
-
key: `${left_overhang}-${right_overhang}`,
|
|
40
|
-
sequenceData,
|
|
41
|
-
genbankString: jsonToGenbank(sequenceData),
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
}
|
|
22
|
+
const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
|
|
45
23
|
|
|
46
24
|
|
|
47
25
|
function formatItemName(item) {
|
|
@@ -261,71 +239,29 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
|
|
|
261
239
|
|
|
262
240
|
function LoadSyntaxButton({ setSyntax, addPlasmids }) {
|
|
263
241
|
const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
242
|
+
const httpClient = useHttpClient();
|
|
243
|
+
const backendRoute = useBackendRoute();
|
|
244
|
+
const { addAlert } = useAlerts();
|
|
245
|
+
const onSyntaxSelect = React.useCallback(async (syntax, plasmids) => {
|
|
246
|
+
const url = backendRoute('validate_syntax');
|
|
247
|
+
try {
|
|
248
|
+
await httpClient.post(url, syntax);
|
|
249
|
+
setSyntax(syntax)
|
|
250
|
+
addPlasmids(plasmids)
|
|
251
|
+
} catch (error) {
|
|
252
|
+
addAlert({
|
|
253
|
+
message: error2String(error),
|
|
254
|
+
severity: 'error',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}, [setSyntax, addPlasmids, httpClient, backendRoute, addAlert])
|
|
268
258
|
return <>
|
|
269
259
|
<Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
|
|
270
260
|
{existingSyntaxDialogOpen && <ExistingSyntaxDialog onClose={() => setExistingSyntaxDialogOpen(false)} onSyntaxSelect={onSyntaxSelect}/>}
|
|
271
261
|
</>
|
|
272
262
|
}
|
|
273
263
|
|
|
274
|
-
export function UploadPlasmidsButton({ addPlasmids, syntax }) {
|
|
275
|
-
const { uploadPlasmids, linkedPlasmids, setLinkedPlasmids } = usePlasmidsLogic(syntax)
|
|
276
|
-
const validPlasmids = React.useMemo(() => linkedPlasmids.filter((plasmid) => plasmid.appData.correspondingParts.length === 1), [linkedPlasmids])
|
|
277
|
-
const invalidPlasmids = React.useMemo(() => linkedPlasmids.filter((plasmid) => plasmid.appData.correspondingParts.length !== 1), [linkedPlasmids])
|
|
278
|
-
const fileInputRef = React.useRef(null)
|
|
279
|
-
|
|
280
|
-
const handleFileChange = (event) => {
|
|
281
|
-
uploadPlasmids(Array.from(event.target.files))
|
|
282
|
-
fileInputRef.current.value = ''
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const handleImportValidPlasmids = React.useCallback(() => {
|
|
286
|
-
addPlasmids(validPlasmids.map(formatPlasmid))
|
|
287
|
-
setLinkedPlasmids([])
|
|
288
|
-
}, [addPlasmids, validPlasmids, setLinkedPlasmids])
|
|
289
264
|
|
|
290
|
-
return (<>
|
|
291
|
-
<Button color="primary" onClick={() => fileInputRef.current.click()}>
|
|
292
|
-
Add Plasmids
|
|
293
|
-
</Button>
|
|
294
|
-
<input multiple type="file" ref={fileInputRef} style={{ display: 'none' }} onChange={handleFileChange} accept=".gbk,.gb,.fasta,.fa,.dna" />
|
|
295
|
-
<Dialog
|
|
296
|
-
maxWidth="lg"
|
|
297
|
-
fullWidth
|
|
298
|
-
open={invalidPlasmids.length > 0 || validPlasmids.length > 0}
|
|
299
|
-
onClose={() => setLinkedPlasmids([])}
|
|
300
|
-
PaperProps={{
|
|
301
|
-
style: {
|
|
302
|
-
maxHeight: '80vh',
|
|
303
|
-
},
|
|
304
|
-
}}
|
|
305
|
-
>
|
|
306
|
-
<DialogActions sx={{ justifyContent: 'center', position: 'sticky', top: 0, zIndex: 99, background: '#fff' }}>
|
|
307
|
-
<Button disabled={validPlasmids.length === 0} variant="contained" color="success" onClick={handleImportValidPlasmids}>Import valid plasmids</Button>
|
|
308
|
-
<Button variant="contained" color="error" onClick={() => setLinkedPlasmids([])}>Cancel</Button>
|
|
309
|
-
</DialogActions>
|
|
310
|
-
{invalidPlasmids.length > 0 && (
|
|
311
|
-
<Box data-testid="invalid-plasmids-box">
|
|
312
|
-
<DialogTitle>Invalid Plasmids</DialogTitle>
|
|
313
|
-
<DialogContent>
|
|
314
|
-
<PlasmidSyntaxTable plasmids={invalidPlasmids} />
|
|
315
|
-
</DialogContent>
|
|
316
|
-
</Box>
|
|
317
|
-
)}
|
|
318
|
-
{validPlasmids.length > 0 && (
|
|
319
|
-
<Box data-testid="valid-plasmids-box">
|
|
320
|
-
<DialogTitle>Valid Plasmids</DialogTitle>
|
|
321
|
-
<DialogContent>
|
|
322
|
-
<PlasmidSyntaxTable plasmids={validPlasmids} />
|
|
323
|
-
</DialogContent>
|
|
324
|
-
</Box>
|
|
325
|
-
)}
|
|
326
|
-
</Dialog>
|
|
327
|
-
</>)
|
|
328
|
-
}
|
|
329
265
|
|
|
330
266
|
function Assembler() {
|
|
331
267
|
const [syntax, setSyntax] = React.useState(null);
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
2
3
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
3
4
|
|
|
5
|
+
// Test config
|
|
6
|
+
const testConfig = {
|
|
7
|
+
backendUrl: 'http://localhost:8000',
|
|
8
|
+
showAppBar: false,
|
|
9
|
+
noExternalRequests: false,
|
|
10
|
+
enableAssembler: true,
|
|
11
|
+
enablePlannotate: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
4
14
|
const mockSyntaxes = [
|
|
5
15
|
{
|
|
6
16
|
path: 'test-syntax-1',
|
|
@@ -40,10 +50,12 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
40
50
|
const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
|
|
41
51
|
|
|
42
52
|
cy.mount(
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
<ConfigProvider config={testConfig}>
|
|
54
|
+
<ExistingSyntaxDialog
|
|
55
|
+
onClose={onCloseSpy}
|
|
56
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
57
|
+
/>
|
|
58
|
+
</ConfigProvider>,
|
|
47
59
|
);
|
|
48
60
|
|
|
49
61
|
cy.wait('@getSyntaxes');
|
|
@@ -70,10 +82,12 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
70
82
|
}).as('getPlasmidsData');
|
|
71
83
|
|
|
72
84
|
cy.mount(
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
<ConfigProvider config={testConfig}>
|
|
86
|
+
<ExistingSyntaxDialog
|
|
87
|
+
onClose={onCloseSpy}
|
|
88
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
89
|
+
/>
|
|
90
|
+
</ConfigProvider>,
|
|
77
91
|
);
|
|
78
92
|
|
|
79
93
|
cy.wait('@getSyntaxes');
|
|
@@ -97,10 +111,12 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
97
111
|
}).as('getSyntaxDataError');
|
|
98
112
|
|
|
99
113
|
cy.mount(
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
<ConfigProvider config={testConfig}>
|
|
115
|
+
<ExistingSyntaxDialog
|
|
116
|
+
onClose={onCloseSpy}
|
|
117
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
118
|
+
/>
|
|
119
|
+
</ConfigProvider>,
|
|
104
120
|
);
|
|
105
121
|
|
|
106
122
|
cy.wait('@getSyntaxes');
|
|
@@ -130,10 +146,12 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
130
146
|
}).as('getPlasmidsDataError');
|
|
131
147
|
|
|
132
148
|
cy.mount(
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
<ConfigProvider config={testConfig}>
|
|
150
|
+
<ExistingSyntaxDialog
|
|
151
|
+
onClose={onCloseSpy}
|
|
152
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
153
|
+
/>
|
|
154
|
+
</ConfigProvider>,
|
|
137
155
|
);
|
|
138
156
|
|
|
139
157
|
cy.wait('@getSyntaxes');
|
|
@@ -169,10 +187,12 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
169
187
|
}).as('getPlasmidsData2');
|
|
170
188
|
|
|
171
189
|
cy.mount(
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
<ConfigProvider config={testConfig}>
|
|
191
|
+
<ExistingSyntaxDialog
|
|
192
|
+
onClose={onCloseSpy}
|
|
193
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
194
|
+
/>
|
|
195
|
+
</ConfigProvider>,
|
|
176
196
|
);
|
|
177
197
|
|
|
178
198
|
cy.wait('@getSyntaxes');
|
|
@@ -204,21 +224,27 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
204
224
|
],
|
|
205
225
|
};
|
|
206
226
|
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
const tempFile = {
|
|
228
|
+
contents: Cypress.Buffer.from(JSON.stringify(uploadedSyntaxData)),
|
|
229
|
+
fileName: 'syntax.json',
|
|
230
|
+
mimeType: 'text/plain',
|
|
231
|
+
lastModified: Date.now(),
|
|
232
|
+
}
|
|
209
233
|
|
|
210
234
|
cy.mount(
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
<ConfigProvider config={testConfig}>
|
|
236
|
+
<ExistingSyntaxDialog
|
|
237
|
+
onClose={onCloseSpy}
|
|
238
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
239
|
+
/>
|
|
240
|
+
</ConfigProvider>,
|
|
215
241
|
);
|
|
216
242
|
|
|
217
243
|
cy.wait('@getSyntaxes');
|
|
218
244
|
cy.contains('Upload syntax from JSON file').should('exist');
|
|
219
245
|
|
|
220
246
|
// Upload the JSON file
|
|
221
|
-
cy.get('input[type="file"]').selectFile(
|
|
247
|
+
cy.get('input[type="file"]').selectFile(tempFile, { force: true });
|
|
222
248
|
|
|
223
249
|
cy.get('@onSyntaxSelectSpy').should('have.been.calledWith', uploadedSyntaxData, []);
|
|
224
250
|
cy.get('@onCloseSpy').should('have.been.called');
|
|
@@ -228,21 +254,27 @@ describe('<ExistingSyntaxDialog />', () => {
|
|
|
228
254
|
const onCloseSpy = cy.spy().as('onCloseSpy');
|
|
229
255
|
const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
|
|
230
256
|
|
|
231
|
-
|
|
232
|
-
|
|
257
|
+
const invalidFile = {
|
|
258
|
+
contents: Cypress.Buffer.from('{ invalid json }'),
|
|
259
|
+
fileName: 'invalid.json',
|
|
260
|
+
mimeType: 'text/plain',
|
|
261
|
+
lastModified: Date.now(),
|
|
262
|
+
}
|
|
233
263
|
|
|
234
264
|
cy.mount(
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
265
|
+
<ConfigProvider config={testConfig}>
|
|
266
|
+
<ExistingSyntaxDialog
|
|
267
|
+
onClose={onCloseSpy}
|
|
268
|
+
onSyntaxSelect={onSyntaxSelectSpy}
|
|
269
|
+
/>
|
|
270
|
+
</ConfigProvider>,
|
|
239
271
|
);
|
|
240
272
|
|
|
241
273
|
cy.wait('@getSyntaxes');
|
|
242
274
|
cy.contains('Upload syntax from JSON file').should('exist');
|
|
243
275
|
|
|
244
276
|
// Upload invalid JSON
|
|
245
|
-
cy.get('input[type="file"]').selectFile(
|
|
277
|
+
cy.get('input[type="file"]').selectFile(invalidFile, { force: true });
|
|
246
278
|
|
|
247
279
|
cy.contains(/Failed to parse JSON file/).should('exist');
|
|
248
280
|
cy.get('@onSyntaxSelectSpy').should('not.have.been.called');
|
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box } from '@mui/material'
|
|
2
|
+
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box, ButtonGroup } from '@mui/material'
|
|
3
3
|
import getHttpClient from '@opencloning/utils/getHttpClient';
|
|
4
4
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
5
|
+
import { useConfig } from '../../providers';
|
|
6
|
+
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
7
|
+
import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
|
|
5
8
|
|
|
6
9
|
const httpClient = getHttpClient();
|
|
7
10
|
const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
|
|
8
11
|
httpClient.defaults.baseURL = baseURL;
|
|
9
12
|
|
|
13
|
+
function LocalSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
14
|
+
|
|
15
|
+
const onFileSelected = React.useCallback(async (file) => {
|
|
16
|
+
const text = await readSubmittedTextFile(file);
|
|
17
|
+
const syntaxData = JSON.parse(text);
|
|
18
|
+
onSyntaxSelect(syntaxData, []);
|
|
19
|
+
onClose();
|
|
20
|
+
}, [onSyntaxSelect, onClose]);
|
|
21
|
+
return (
|
|
22
|
+
<Dialog data-testid="local-syntax-dialog" open onClose={onClose}>
|
|
23
|
+
<DialogTitle>Load syntax from local server</DialogTitle>
|
|
24
|
+
<DialogContent sx={{ minWidth: '400px' }}>
|
|
25
|
+
<ServerStaticFileSelect onFileSelected={onFileSelected} type="syntax" />
|
|
26
|
+
</DialogContent>
|
|
27
|
+
</Dialog>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
11
32
|
const [syntaxes, setSyntaxes] = React.useState([]);
|
|
12
33
|
const [connectAttempt, setConnectAttempt] = React.useState(0);
|
|
13
34
|
const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
|
|
14
35
|
const [loadError, setLoadError] = React.useState(null);
|
|
15
36
|
const fileInputRef = React.useRef(null);
|
|
37
|
+
const { staticContentPath } = useConfig();
|
|
38
|
+
const [localDialogOpen, setLocalDialogOpen] = React.useState(false);
|
|
16
39
|
|
|
17
40
|
React.useEffect(() => {
|
|
18
41
|
setRequestStatus({ status: 'loading' });
|
|
@@ -88,13 +111,21 @@ function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
|
|
|
88
111
|
onChange={handleFileUpload}
|
|
89
112
|
style={{ display: 'none' }}
|
|
90
113
|
/>
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
<ButtonGroup sx={{ display: 'flex', justifyContent: 'center' }}>
|
|
115
|
+
<Button
|
|
116
|
+
onClick={() => fileInputRef.current?.click()}
|
|
117
|
+
>
|
|
118
|
+
Upload syntax from JSON file
|
|
119
|
+
</Button>
|
|
120
|
+
{staticContentPath && (
|
|
121
|
+
<Button
|
|
122
|
+
onClick={() => setLocalDialogOpen(true)}
|
|
123
|
+
>
|
|
124
|
+
Load syntax from local server
|
|
125
|
+
</Button>
|
|
126
|
+
)}
|
|
127
|
+
</ButtonGroup>
|
|
128
|
+
{localDialogOpen && <LocalSyntaxDialog onClose={() => {setLocalDialogOpen(false); onClose()}} onSyntaxSelect={onSyntaxSelect} />}
|
|
98
129
|
</Box>
|
|
99
130
|
</DialogContent>
|
|
100
131
|
</Dialog>
|
|
@@ -22,7 +22,7 @@ function PlasmidRow({ plasmid }) {
|
|
|
22
22
|
backgroundColor: partInfo[0]?.color,
|
|
23
23
|
}
|
|
24
24
|
infoStr = partInfo[0] ? partInfo[0].name : 'Spans multiple parts';
|
|
25
|
-
longestFeatureStr = longestFeature ? longestFeature[0].name : '-';
|
|
25
|
+
longestFeatureStr = longestFeature[0] ? longestFeature[0].name : '-';
|
|
26
26
|
}
|
|
27
27
|
const multipleParts = partInfo.length > 1;
|
|
28
28
|
if (multipleParts) {
|