@opencloning/ui 1.7.2 → 1.8.1
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 +90 -0
- package/package.json +20 -14
- package/src/components/AppAlerts.jsx +7 -7
- package/src/components/CloningHistory.jsx +2 -0
- package/src/components/CloningSpeedDial.jsx +102 -0
- package/src/components/DownloadCloningStrategyDialog.jsx +2 -2
- package/src/components/LoadCloningHistoryWrapper.jsx +29 -22
- package/src/components/MainSequenceEditor.jsx +41 -34
- package/src/components/NetworkNode.jsx +35 -33
- package/src/components/OpenCloning.jsx +4 -3
- package/src/components/SequenceViewer.cy.jsx +27 -0
- package/src/components/SequenceViewer.jsx +211 -0
- package/src/components/annotation/AnnotationChangedAlert.jsx +26 -0
- package/src/components/annotation/useUpdateAnnotationInMainSequence.js +2 -2
- package/src/components/assembler/Assembler.jsx +3 -3
- package/src/components/domesticate/DomesticateDialog.jsx +274 -0
- package/src/components/form/GetRequestMultiSelect.jsx +0 -1
- package/src/components/form/PostRequestSelect.jsx +0 -1
- package/src/components/form/QuerySelect.jsx +128 -0
- package/src/components/form/QueryStatusWrapper.jsx +31 -0
- package/src/components/form/SubmitToDatabaseDialog.jsx +63 -58
- package/src/components/form/TextFieldQueryValidated.jsx +93 -0
- package/src/components/index.js +4 -0
- package/src/components/navigation/FeedbackDialog.jsx +1 -1
- package/src/components/navigation/MainAppBar.jsx +5 -5
- package/src/components/primers/import_primers/ImportPrimersButton.jsx +2 -2
- package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +6 -3
- package/src/components/sources/FinishedSource.jsx +3 -0
- package/src/components/sources/MultipleOutputsSelector.cy.jsx +95 -0
- package/src/components/sources/MultipleOutputsSelector.jsx +32 -19
- package/src/components/sources/RecombinaseList.cy.jsx +1 -1
- package/src/components/sources/RecombinaseList.jsx +1 -1
- package/src/components/sources/SourceAssembly.jsx +36 -2
- package/src/components/sources/SourceFile.jsx +9 -1
- package/src/components/sources/SourceRepositoryId.jsx +0 -1
- package/src/components/sources/SourceTypeSelector.jsx +1 -1
- package/src/components/verification/ImportSequencingFilesInput.jsx +17 -0
- package/src/components/verification/LoadFromDatabaseButton.jsx +17 -10
- package/src/components/verification/VerificationFileDialog.jsx +16 -22
- package/src/components/verification/index.js +1 -0
- package/src/config/defaultMainEditorProps.js +1 -0
- package/src/hooks/useAlerts.js +3 -4
- package/src/hooks/useBackendAPI.js +5 -2
- package/src/hooks/useCloningAlerts.js +7 -0
- package/src/hooks/useDebouncedSearchQuery.js +42 -0
- package/src/hooks/useLoadDatabaseFile.js +6 -2
- package/src/hooks/useSequencingAlignment.js +26 -0
- package/src/hooks/useSnapgeneHistoryEndpoint.js +72 -0
- package/src/hooks/useStoreEditor.js +10 -68
- package/src/hooks/useUrlParamsLoader.js +2 -2
- package/src/hooks/useValidateState.js +3 -3
- package/src/index.css +3 -11
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,95 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.8.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#699](https://github.com/OpenCloning/OpenCloning_frontend/pull/699) [`8e841bd`](https://github.com/OpenCloning/OpenCloning_frontend/commit/8e841bdee4797df61bcf8d1972e3c3581d24fbfb) Thanks [@manulera](https://github.com/manulera)! - Improve snapgene warning
|
|
8
|
+
|
|
9
|
+
- Updated dependencies []:
|
|
10
|
+
- @opencloning/store@1.8.1
|
|
11
|
+
- @opencloning/utils@1.8.1
|
|
12
|
+
|
|
13
|
+
## 1.8.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- [#694](https://github.com/OpenCloning/OpenCloning_frontend/pull/694) [`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57) Thanks [@manulera](https://github.com/manulera)! - \* Rename `useAlerts` to `useCloningAlerts`, separate alerts in cloning and opencloningdb apps.
|
|
18
|
+
- Add `react-query` to avoid managing requests with `useState`/`useEffect`. Going forward, prefer `useMutation` or `useQuery` over manual `useState`/`useEffect` patterns, we will replace them over time.
|
|
19
|
+
- Prevent importing the same primer from the database twice (remove it from options in `PrimerDatabaseImportForm`).
|
|
20
|
+
- Add `Imported from unknown database` option in `FinishedSource` component, not to fail when `DatabaseSource` is present, but no database is set up.
|
|
21
|
+
- Improve placement of app alerts to be within the `OpenCloning` component, not app-wide.
|
|
22
|
+
- Fix hiding ancestors.
|
|
23
|
+
- Add standalone `SequenceViewer` component.
|
|
24
|
+
- Refactor alignment functionality into a separate hook for better reusability into `useSequencingAlignment.js`.
|
|
25
|
+
- Add support for loading
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [[`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57), [`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57)]:
|
|
30
|
+
- @opencloning/store@1.8.0
|
|
31
|
+
- @opencloning/utils@1.8.0
|
|
32
|
+
|
|
33
|
+
## 1.7.8
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Updated dependencies []:
|
|
38
|
+
- @opencloning/store@1.7.8
|
|
39
|
+
- @opencloning/utils@1.7.8
|
|
40
|
+
|
|
41
|
+
## 1.7.7
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- [#689](https://github.com/manulera/OpenCloning_frontend/pull/689) [`c5d9309`](https://github.com/manulera/OpenCloning_frontend/commit/c5d93099583fdfafafd40e9924d2216b596e0cec) Thanks [@manulera](https://github.com/manulera)! - Added experimental feature for domestication that relies on API calls to goldenbraidpro
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [[`c5d9309`](https://github.com/manulera/OpenCloning_frontend/commit/c5d93099583fdfafafd40e9924d2216b596e0cec)]:
|
|
48
|
+
- @opencloning/utils@1.7.7
|
|
49
|
+
- @opencloning/store@1.7.7
|
|
50
|
+
|
|
51
|
+
## 1.7.6
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- [#686](https://github.com/manulera/OpenCloning_frontend/pull/686) [`c81b179`](https://github.com/manulera/OpenCloning_frontend/commit/c81b179b4fece50271ea214cab1f8a356e36811f) Thanks [@manulera](https://github.com/manulera)! - useBackendAPI now shows x-warning messages when the backend returns a failed request with that header. This is used and tested for partial restriction-ligation.
|
|
56
|
+
|
|
57
|
+
- [#686](https://github.com/manulera/OpenCloning_frontend/pull/686) [`c81b179`](https://github.com/manulera/OpenCloning_frontend/commit/c81b179b4fece50271ea214cab1f8a356e36811f) Thanks [@manulera](https://github.com/manulera)! - Fix MultipleOutputsSelector to reset to position 0 when sources change. This was giving an error before when number of sources chaged if the position was higher than the length of the new source array, as it was trying to index the array beyond its length.
|
|
58
|
+
|
|
59
|
+
- Updated dependencies []:
|
|
60
|
+
- @opencloning/store@1.7.6
|
|
61
|
+
- @opencloning/utils@1.7.6
|
|
62
|
+
|
|
63
|
+
## 1.7.5
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- [#684](https://github.com/manulera/OpenCloning_frontend/pull/684) [`034c738`](https://github.com/manulera/OpenCloning_frontend/commit/034c73840dc183b6b3584365c1db899d2f0c548b) Thanks [@manulera](https://github.com/manulera)! - Allow recombinase genome setting
|
|
68
|
+
|
|
69
|
+
- Updated dependencies [[`034c738`](https://github.com/manulera/OpenCloning_frontend/commit/034c73840dc183b6b3584365c1db899d2f0c548b)]:
|
|
70
|
+
- @opencloning/store@1.7.5
|
|
71
|
+
- @opencloning/utils@1.7.5
|
|
72
|
+
|
|
73
|
+
## 1.7.4
|
|
74
|
+
|
|
75
|
+
### Patch Changes
|
|
76
|
+
|
|
77
|
+
- [#682](https://github.com/manulera/OpenCloning_frontend/pull/682) [`04703a9`](https://github.com/manulera/OpenCloning_frontend/commit/04703a9269a5d4cc4bdde478c1ec61e5c73b0d2a) Thanks [@manulera](https://github.com/manulera)! - Fix bug when copying pcr products
|
|
78
|
+
|
|
79
|
+
- Updated dependencies [[`04703a9`](https://github.com/manulera/OpenCloning_frontend/commit/04703a9269a5d4cc4bdde478c1ec61e5c73b0d2a)]:
|
|
80
|
+
- @opencloning/utils@1.7.4
|
|
81
|
+
- @opencloning/store@1.7.4
|
|
82
|
+
|
|
83
|
+
## 1.7.3
|
|
84
|
+
|
|
85
|
+
### Patch Changes
|
|
86
|
+
|
|
87
|
+
- [#677](https://github.com/manulera/OpenCloning_frontend/pull/677) [`04827d6`](https://github.com/manulera/OpenCloning_frontend/commit/04827d6184c5a1276814f60ef3c3fee2c832a6fd) Thanks [@manulera](https://github.com/manulera)! - Now loads history from Snapgene files
|
|
88
|
+
|
|
89
|
+
- Updated dependencies []:
|
|
90
|
+
- @opencloning/store@1.7.3
|
|
91
|
+
- @opencloning/utils@1.7.3
|
|
92
|
+
|
|
3
93
|
## 1.7.2
|
|
4
94
|
|
|
5
95
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"license": "MIT",
|
|
3
3
|
"name": "@opencloning/ui",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"scripts": {
|
|
@@ -17,32 +17,38 @@
|
|
|
17
17
|
"./components/form/RetryAlert": "./src/components/form/RetryAlert.jsx",
|
|
18
18
|
"./components/form/RequestStatusWrapper": "./src/components/form/RequestStatusWrapper.jsx",
|
|
19
19
|
"./components/form/PostRequestSelect": "./src/components/form/PostRequestSelect.jsx",
|
|
20
|
+
"./components/verification": "./src/components/verification/index.js",
|
|
20
21
|
"./providers/ConfigProvider": "./src/providers/index.js",
|
|
21
22
|
"./providers/DatabaseContext": "./src/providers/DatabaseContext.jsx",
|
|
22
23
|
"./hooks": "./src/hooks/index.js",
|
|
24
|
+
"./hooks/useLoadDatabaseFile": "./src/hooks/useLoadDatabaseFile.js",
|
|
25
|
+
"./hooks/useAlerts": "./src/hooks/useAlerts.js",
|
|
26
|
+
"./hooks/useCloningAlerts": "./src/hooks/useCloningAlerts.js",
|
|
27
|
+
"./hooks/useSequencingAlignment": "./src/hooks/useSequencingAlignment.js",
|
|
28
|
+
"./components/SequenceViewer": "./src/components/SequenceViewer.jsx",
|
|
23
29
|
"./standalone": "./src/StandAloneOpenCloning.js"
|
|
24
30
|
},
|
|
25
31
|
"repository": {
|
|
26
32
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/
|
|
33
|
+
"url": "https://github.com/OpenCloning/OpenCloning_frontend.git",
|
|
28
34
|
"directory": "packages/ui"
|
|
29
35
|
},
|
|
30
36
|
"dependencies": {
|
|
31
37
|
"@emotion/react": "^11.14.0",
|
|
32
|
-
"@emotion/styled": "^11.14.
|
|
33
|
-
"@mui/icons-material": "^5.
|
|
34
|
-
"@mui/material": "^5.
|
|
35
|
-
"@mui/x-data-grid": "^8.
|
|
36
|
-
"@opencloning/store": "1.
|
|
37
|
-
"@opencloning/utils": "1.
|
|
38
|
-
"@teselagen/bio-parsers": "^0.4.
|
|
39
|
-
"@teselagen/ove": "^0.8.
|
|
38
|
+
"@emotion/styled": "^11.14.1",
|
|
39
|
+
"@mui/icons-material": "^5.18.0",
|
|
40
|
+
"@mui/material": "^5.18.0",
|
|
41
|
+
"@mui/x-data-grid": "^8.28.5",
|
|
42
|
+
"@opencloning/store": "1.8.1",
|
|
43
|
+
"@opencloning/utils": "1.8.1",
|
|
44
|
+
"@teselagen/bio-parsers": "^0.4.38",
|
|
45
|
+
"@teselagen/ove": "^0.8.42",
|
|
40
46
|
"@teselagen/range-utils": "^0.3.20",
|
|
41
47
|
"@teselagen/sequence-utils": "^0.3.42",
|
|
42
|
-
"@zip.js/zip.js": "^2.
|
|
43
|
-
"axios": "^1.
|
|
44
|
-
"lodash-es": "^4.
|
|
45
|
-
"react-draggable": "^4.
|
|
48
|
+
"@zip.js/zip.js": "^2.8.26",
|
|
49
|
+
"axios": "^1.16.0",
|
|
50
|
+
"lodash-es": "^4.18.1",
|
|
51
|
+
"react-draggable": "^4.5.0"
|
|
46
52
|
},
|
|
47
53
|
"peerDependencies": {
|
|
48
54
|
"react": "^18.3.1",
|
|
@@ -2,16 +2,16 @@ import { Alert } from '@mui/material';
|
|
|
2
2
|
import { isEqual } from 'lodash-es';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { useSelector } from 'react-redux';
|
|
5
|
-
import useAlerts from '../hooks/useAlerts';
|
|
6
5
|
import ExternalServicesStatusCheck from './ExternalServicesStatusCheck';
|
|
7
6
|
|
|
8
|
-
function AppAlerts() {
|
|
9
|
-
const { alerts } = useSelector((state) => state
|
|
10
|
-
const { removeAlert } = useAlerts();
|
|
7
|
+
function AppAlerts( { reducerName, removeAlert } ) {
|
|
8
|
+
const { alerts } = useSelector((state) => state[reducerName], isEqual);
|
|
11
9
|
return (
|
|
12
|
-
<div
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
<div className="app-alerts-container" style={{ position: 'absolute' }}>
|
|
11
|
+
<div id="global-error-message-wrapper">
|
|
12
|
+
{alerts.map((alert, index) => (<Alert key={index} severity={alert.severity} onClose={() => { removeAlert(alert.message); }}>{alert.message}</Alert>))}
|
|
13
|
+
<ExternalServicesStatusCheck />
|
|
14
|
+
</div>
|
|
15
15
|
</div>
|
|
16
16
|
);
|
|
17
17
|
}
|
|
@@ -5,6 +5,7 @@ import { getSortedSourceIds } from '@opencloning/utils/network';
|
|
|
5
5
|
import NetWorkNode from './NetworkNode';
|
|
6
6
|
import NewSourceBox from './sources/NewSourceBox';
|
|
7
7
|
import DragAndDropCloningHistoryWrapper from './DragAndDropCloningHistoryWrapper';
|
|
8
|
+
import CloningSpeedDial from './CloningSpeedDial';
|
|
8
9
|
|
|
9
10
|
function CloningHistory() {
|
|
10
11
|
const startingSourceIds = useSelector(
|
|
@@ -19,6 +20,7 @@ function CloningHistory() {
|
|
|
19
20
|
);
|
|
20
21
|
return (
|
|
21
22
|
<DragAndDropCloningHistoryWrapper>
|
|
23
|
+
<CloningSpeedDial />
|
|
22
24
|
<div className="tf-tree tf-ancestor-tree">
|
|
23
25
|
<div>
|
|
24
26
|
<ul>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, SpeedDial, SpeedDialAction } from '@mui/material'
|
|
3
|
+
import { Build as BuildIcon, Sync as SyncIcon } from '@mui/icons-material'
|
|
4
|
+
import useDatabase from '../hooks/useDatabase';
|
|
5
|
+
import { useDispatch, useStore } from 'react-redux';
|
|
6
|
+
import { getSequencesNotInDatabase } from '@opencloning/utils/network';
|
|
7
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
8
|
+
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
9
|
+
import { cloneDeep } from 'lodash-es';
|
|
10
|
+
import useBackendRoute from '../hooks/useBackendRoute';
|
|
11
|
+
import useHttpClient from '../hooks/useHttpClient';
|
|
12
|
+
|
|
13
|
+
const { setState: setCloningState } = cloningActions;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function useLocateSequencesInDatabaseAction() {
|
|
17
|
+
const database = useDatabase();
|
|
18
|
+
const store = useStore();
|
|
19
|
+
const dispatch = useDispatch();
|
|
20
|
+
const backendRoute = useBackendRoute();
|
|
21
|
+
const httpClient = useHttpClient();
|
|
22
|
+
const url = backendRoute('normalize_cloning_strategy');
|
|
23
|
+
|
|
24
|
+
const { addAlert } = useCloningAlerts();
|
|
25
|
+
const main = React.useCallback(async () => {
|
|
26
|
+
const { sequences: seqState, sources: srcState, primers, description } = store.getState().cloning;
|
|
27
|
+
let sequences = cloneDeep(seqState);
|
|
28
|
+
let sources = cloneDeep(srcState);
|
|
29
|
+
|
|
30
|
+
const sequencesNotInDb = getSequencesNotInDatabase(sources, sequences);
|
|
31
|
+
if (sequencesNotInDb.length === 0) {
|
|
32
|
+
addAlert({
|
|
33
|
+
message: 'All sequences are already in the database',
|
|
34
|
+
severity: 'success',
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const promises = sequencesNotInDb.map((sequence) => database.locateSequenceInDatabase(sequence));
|
|
39
|
+
const results = await Promise.all(promises);
|
|
40
|
+
if (!results.some((result) => result.length > 0)) {
|
|
41
|
+
addAlert({
|
|
42
|
+
message: 'No sequences located in database',
|
|
43
|
+
severity: 'success',
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < results.length; i++) {
|
|
49
|
+
if (results[i].length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const localId = sequencesNotInDb[i].id;
|
|
53
|
+
const newSequence = { ...results[i][0].sequence, id: localId };
|
|
54
|
+
const newSource = { type: 'DatabaseSource', database_id: results[i][0].sequence_ref.id, input: [], id: localId };
|
|
55
|
+
sources = sources.map((s) => s.id === localId ? newSource : s);
|
|
56
|
+
sequences = sequences.map((s) => s.id === localId ? newSequence : s);
|
|
57
|
+
addAlert({
|
|
58
|
+
message: `Sequence ${localId} located in database`,
|
|
59
|
+
severity: 'success',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const { data: newCloningStrategy } = await httpClient.post(url, { sequences, sources, primers, description });
|
|
63
|
+
dispatch(setCloningState(newCloningStrategy));
|
|
64
|
+
|
|
65
|
+
// while (handledSequenceIds.size < sequences.length || iter < sequences.length) {
|
|
66
|
+
// iter++;
|
|
67
|
+
// const sequenceIdsThatAreInput = new Set(getSequenceIdsThatAreInput(sources));
|
|
68
|
+
|
|
69
|
+
// if (!sequenceIdsThatAreInput.includes(sequence.id)) {
|
|
70
|
+
// const newSource = {
|
|
71
|
+
// input: [],
|
|
72
|
+
// type: 'DatabaseSource',
|
|
73
|
+
// database_id: result[0].sequence_ref.id,
|
|
74
|
+
// };
|
|
75
|
+
// const newSequence = result[0].sequence
|
|
76
|
+
// batch(() => {
|
|
77
|
+
// dispatch(deleteSourceAndItsChildren(sequence.id));
|
|
78
|
+
// dispatch(addSourceAndItsOutputSequence({ source: newSource, sequence: newSequence }));
|
|
79
|
+
// });
|
|
80
|
+
// }
|
|
81
|
+
// console.log(result);
|
|
82
|
+
// }
|
|
83
|
+
}, [database, store, dispatch, addAlert, httpClient, url]);
|
|
84
|
+
return main;
|
|
85
|
+
}
|
|
86
|
+
function CloningSpeedDial() {
|
|
87
|
+
const database = useDatabase();
|
|
88
|
+
const locateSequencesInDatabaseAction = useLocateSequencesInDatabaseAction();
|
|
89
|
+
if (!database || !database.locateSequenceInDatabase) return null;
|
|
90
|
+
return (
|
|
91
|
+
// <Box sx={{ position: 'absolute', bottom: 0, right: 0 }}>
|
|
92
|
+
<Box sx={{ position: 'fixed', bottom: 10, left: 10, zIndex: 1000 }} data-testid="cloning-speed-dial">
|
|
93
|
+
<SpeedDial icon={<BuildIcon />} ariaLabel="Cloning tools" direction="up" >
|
|
94
|
+
{ database && Boolean(database.locateSequenceInDatabase) && <SpeedDialAction icon={<SyncIcon />} tooltipTitle="Synchronize sequences with database" onClick={locateSequencesInDatabaseAction} /> }
|
|
95
|
+
</SpeedDial>
|
|
96
|
+
|
|
97
|
+
</Box>
|
|
98
|
+
// </Box>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default CloningSpeedDial
|
|
@@ -2,7 +2,7 @@ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl,
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { useSelector, useStore } from 'react-redux';
|
|
4
4
|
import { downloadStateAsJson, downloadStateAsZip } from '@opencloning/utils/readNwrite';
|
|
5
|
-
import
|
|
5
|
+
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
function DownloadCloningStrategyDialog({ open, setOpen }) {
|
|
@@ -11,7 +11,7 @@ function DownloadCloningStrategyDialog({ open, setOpen }) {
|
|
|
11
11
|
const hasVerificationFiles = useSelector(({ cloning }) => cloning.files.length > 0);
|
|
12
12
|
|
|
13
13
|
const store = useStore();
|
|
14
|
-
const { addAlert } =
|
|
14
|
+
const { addAlert } = useCloningAlerts();
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
17
|
<Dialog
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { batch, useDispatch, useStore } from 'react-redux';
|
|
3
|
-
import
|
|
3
|
+
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
4
4
|
import useBackendRoute from '../hooks/useBackendRoute';
|
|
5
5
|
import useValidateState from '../hooks/useValidateState';
|
|
6
6
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
@@ -10,6 +10,7 @@ import HistoryLoadedDialog from './HistoryLoadedDialog';
|
|
|
10
10
|
import useHttpClient from '../hooks/useHttpClient';
|
|
11
11
|
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
12
12
|
import { isEqual } from 'lodash-es';
|
|
13
|
+
import useSnapgeneHistoryEndpoint from '../hooks/useSnapgeneHistoryEndpoint';
|
|
13
14
|
|
|
14
15
|
const { setState: setCloningState, deleteSourceAndItsChildren, addSourceAndItsOutputSequence } = cloningActions;
|
|
15
16
|
|
|
@@ -58,13 +59,13 @@ async function processSequenceFiles(files, backendRoute, httpClient) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
61
|
-
const { addAlert } =
|
|
62
|
+
const { addAlert } = useCloningAlerts();
|
|
62
63
|
const dispatch = useDispatch();
|
|
63
64
|
const backendRoute = useBackendRoute();
|
|
64
65
|
const store = useStore();
|
|
65
66
|
const validateState = useValidateState();
|
|
66
67
|
const httpClient = useHttpClient();
|
|
67
|
-
|
|
68
|
+
const { loadSnapgeneHistory } = useSnapgeneHistoryEndpoint();
|
|
68
69
|
const [fileLoaderFunctions, setFileLoaderFunctions] = React.useState(null);
|
|
69
70
|
|
|
70
71
|
React.useEffect(() => {
|
|
@@ -124,34 +125,40 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
|
124
125
|
}
|
|
125
126
|
// If there are sequences in the cloning state, give the option to merge or replace
|
|
126
127
|
|
|
128
|
+
return;
|
|
127
129
|
// You cannot drop a zip or json file and a sequence file at the same time
|
|
128
130
|
} else if (files.some((file) => file.name.endsWith('.json') || file.name.endsWith('.zip'))) {
|
|
129
131
|
addAlert({
|
|
130
132
|
message: 'Drop either a single JSON/zip file or multiple sequence files. Not both.',
|
|
131
133
|
severity: 'error',
|
|
132
134
|
});
|
|
135
|
+
return;
|
|
133
136
|
// Process a bunch of sequence files
|
|
134
|
-
} else {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// If there is only one source and it is empty, delete it
|
|
139
|
-
if (cloningState.sources.length === 1 && cloningState.sources[0].type === null) {
|
|
140
|
-
dispatch(deleteSourceAndItsChildren(cloningState.sources[0].id));
|
|
141
|
-
}
|
|
142
|
-
for (let i = 0; i < sources.length; i += 1) {
|
|
143
|
-
dispatch(addSourceAndItsOutputSequence({ source: sources[i], sequence: sequences[i] }));
|
|
144
|
-
}
|
|
145
|
-
warnings.forEach((warning) => addAlert({ message: warning, severity: 'warning' }));
|
|
146
|
-
});
|
|
147
|
-
} catch (e) {
|
|
148
|
-
console.error(e);
|
|
149
|
-
addAlert({
|
|
150
|
-
message: e.message,
|
|
151
|
-
severity: 'error',
|
|
152
|
-
});
|
|
137
|
+
} else if (files.length === 1 && files[0].name.endsWith('.dna')) {
|
|
138
|
+
const success = await loadSnapgeneHistory(files[0]);
|
|
139
|
+
if (success) {
|
|
140
|
+
return;
|
|
153
141
|
}
|
|
154
142
|
}
|
|
143
|
+
try {
|
|
144
|
+
const { sources, sequences, warnings } = await processSequenceFiles(files, backendRoute, httpClient);
|
|
145
|
+
batch(() => {
|
|
146
|
+
// If there is only one source and it is empty, delete it
|
|
147
|
+
if (cloningState.sources.length === 1 && cloningState.sources[0].type === null) {
|
|
148
|
+
dispatch(deleteSourceAndItsChildren(cloningState.sources[0].id));
|
|
149
|
+
}
|
|
150
|
+
for (let i = 0; i < sources.length; i += 1) {
|
|
151
|
+
dispatch(addSourceAndItsOutputSequence({ source: sources[i], sequence: sequences[i] }));
|
|
152
|
+
}
|
|
153
|
+
warnings.forEach((warning) => addAlert({ message: warning, severity: 'warning' }));
|
|
154
|
+
});
|
|
155
|
+
} catch (e) {
|
|
156
|
+
console.error(e);
|
|
157
|
+
addAlert({
|
|
158
|
+
message: e.message,
|
|
159
|
+
severity: 'error',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
155
162
|
};
|
|
156
163
|
processFileSubmission();
|
|
157
164
|
}, [fileList]);
|
|
@@ -4,11 +4,13 @@ import { useDispatch, useStore } from 'react-redux';
|
|
|
4
4
|
import { getReverseComplementSequenceString, getSequenceDataBetweenRange } from '@teselagen/sequence-utils';
|
|
5
5
|
import defaultMainEditorProps from '../config/defaultMainEditorProps';
|
|
6
6
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
7
|
-
import
|
|
7
|
+
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
8
8
|
import { Alert, Button } from '@mui/material';
|
|
9
9
|
import { useSelector } from 'react-redux';
|
|
10
10
|
import useUpdateAnnotationInMainSequence from './annotation/useUpdateAnnotationInMainSequence';
|
|
11
11
|
import useStoreEditor from '../hooks/useStoreEditor';
|
|
12
|
+
import DomesticateDialog from './domesticate/DomesticateDialog';
|
|
13
|
+
import AnnotationChangedAlert from './annotation/AnnotationChangedAlert';
|
|
12
14
|
|
|
13
15
|
const { setMainSequenceSelection, addPrimer } = cloningActions;
|
|
14
16
|
|
|
@@ -41,23 +43,9 @@ function primerRightClickedOverride(items, { annotation }, props) {
|
|
|
41
43
|
];
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
function featureRightClickedOverride(items, { annotation }, props) {
|
|
45
|
-
return [
|
|
46
|
-
...regionRightClickedOverride(items, { annotation }, props),
|
|
47
|
-
"--",
|
|
48
|
-
"editFeature",
|
|
49
|
-
"deleteFeature",
|
|
50
|
-
"showRemoveDuplicatesDialogFeatures",
|
|
51
|
-
"--",
|
|
52
|
-
"toggleCdsFeatureTranslations",
|
|
53
|
-
"viewFeatureProperties",
|
|
54
|
-
];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
46
|
function MainSequenceEditor() {
|
|
59
47
|
const dispatch = useDispatch();
|
|
60
|
-
const { addAlert } =
|
|
48
|
+
const { addAlert } = useCloningAlerts();
|
|
61
49
|
const { updateStoreEditor } = useStoreEditor();
|
|
62
50
|
const updateAnnotationInMainSequence = useUpdateAnnotationInMainSequence();
|
|
63
51
|
|
|
@@ -67,7 +55,7 @@ function MainSequenceEditor() {
|
|
|
67
55
|
if (!history) return false;
|
|
68
56
|
const sequenceId = state.VectorEditor.mainEditor?.sequenceData?.id;
|
|
69
57
|
if (sequenceId === 'opencloning_primer_design_product') return false;
|
|
70
|
-
return state.cloning.mainSequenceId && Object.keys(history).length > 0 && history.
|
|
58
|
+
return state.cloning.mainSequenceId && Object.keys(history).length > 0 && history.past.length !== 0;
|
|
71
59
|
}
|
|
72
60
|
);
|
|
73
61
|
|
|
@@ -76,6 +64,30 @@ function MainSequenceEditor() {
|
|
|
76
64
|
const mainSequenceId = useSelector((state) => state.cloning.mainSequenceId);
|
|
77
65
|
const topDivRef = React.useRef(null);
|
|
78
66
|
|
|
67
|
+
const [domesticateDialogOpen, setDomesticateDialogOpen] = React.useState(false);
|
|
68
|
+
const [domesticateContext, setDomesticateContext] = React.useState(null);
|
|
69
|
+
|
|
70
|
+
const featureRightClickedOverride = React.useCallback((items, { annotation }, props) => {
|
|
71
|
+
return [
|
|
72
|
+
...regionRightClickedOverride(items, { annotation }, props),
|
|
73
|
+
"--",
|
|
74
|
+
"editFeature",
|
|
75
|
+
"deleteFeature",
|
|
76
|
+
"showRemoveDuplicatesDialogFeatures",
|
|
77
|
+
"--",
|
|
78
|
+
"toggleCdsFeatureTranslations",
|
|
79
|
+
"viewFeatureProperties",
|
|
80
|
+
"--",
|
|
81
|
+
{
|
|
82
|
+
text: 'Domesticate (experimental)',
|
|
83
|
+
onClick: () => {
|
|
84
|
+
setDomesticateContext({ sequenceData: props.sequenceData, annotation });
|
|
85
|
+
setDomesticateDialogOpen(true);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
79
91
|
React.useEffect(() => {
|
|
80
92
|
if (annotationChanged) {
|
|
81
93
|
setTimeout(() => {
|
|
@@ -142,25 +154,20 @@ function MainSequenceEditor() {
|
|
|
142
154
|
return (
|
|
143
155
|
<div style={{ textAlign: 'left' }} ref={topDivRef}>
|
|
144
156
|
{annotationChanged &&
|
|
145
|
-
<
|
|
146
|
-
style={{maxWidth: '500px', margin: '10px auto'}}
|
|
147
|
-
severity="info"
|
|
148
|
-
data-testid="annotation-changed-alert"
|
|
149
|
-
action={
|
|
150
|
-
<>
|
|
151
|
-
<Button color="primary" onClick={updateAnnotationInMainSequence}>
|
|
152
|
-
Save
|
|
153
|
-
</Button>
|
|
154
|
-
<Button color="secondary" onClick={onAnnotationCancel}>
|
|
155
|
-
Cancel
|
|
156
|
-
</Button>
|
|
157
|
-
</>
|
|
158
|
-
}
|
|
159
|
-
>
|
|
160
|
-
<strong>Annotation Changed</strong>
|
|
161
|
-
</Alert>
|
|
157
|
+
<AnnotationChangedAlert onSave={updateAnnotationInMainSequence} onCancel={onAnnotationCancel} />
|
|
162
158
|
}
|
|
163
159
|
<Editor {...{ editorName, ...defaultMainEditorProps, ...extraProp, height: '800' }} />
|
|
160
|
+
{domesticateDialogOpen &&
|
|
161
|
+
<DomesticateDialog
|
|
162
|
+
open={domesticateDialogOpen}
|
|
163
|
+
onClose={() => {
|
|
164
|
+
setDomesticateDialogOpen(false);
|
|
165
|
+
setDomesticateContext(null);
|
|
166
|
+
}}
|
|
167
|
+
initialContext={domesticateContext}
|
|
168
|
+
mainSequenceId={mainSequenceId}
|
|
169
|
+
/>
|
|
170
|
+
}
|
|
164
171
|
</div>
|
|
165
172
|
);
|
|
166
173
|
}
|
|
@@ -17,9 +17,7 @@ import useDatabase from '../hooks/useDatabase';
|
|
|
17
17
|
|
|
18
18
|
const { addToSourcesWithHiddenAncestors, removeFromSourcesWithHiddenAncestors, addSequenceInBetween } = cloningActions;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
const database = useDatabase();
|
|
22
|
-
const isSavedToDatabase = database && useSelector((state) => Boolean(getSourceDatabaseId(state.cloning.sources, sequenceId)));
|
|
20
|
+
function SequenceContent({ sequenceId, sequenceIsTemplate, isSavedToDatabase }) {
|
|
23
21
|
return (
|
|
24
22
|
<span className="tf-nc" style={{ borderColor: isSavedToDatabase ? 'green' : 'default' }}>
|
|
25
23
|
<span className="node-text">
|
|
@@ -35,15 +33,18 @@ const SequenceContent = React.memo(({ sequenceId, sequenceIsTemplate }) => {
|
|
|
35
33
|
</span>
|
|
36
34
|
</span>
|
|
37
35
|
);
|
|
38
|
-
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const MemoizedSequenceContent = React.memo(SequenceContent);
|
|
39
39
|
|
|
40
|
-
function SequenceWrapper({ children, sequenceId, sequenceIsTemplate }) {
|
|
40
|
+
function SequenceWrapper({ children, sequenceId, sequenceIsTemplate, isSavedToDatabase }) {
|
|
41
41
|
if (sequenceId === null) {
|
|
42
42
|
return children;
|
|
43
43
|
}
|
|
44
|
+
const className = 'sequence-node' + (isSavedToDatabase ? ' in-database' : '');
|
|
44
45
|
return (
|
|
45
|
-
<li key={sequenceId} id={`sequence-${sequenceId}`} className=
|
|
46
|
-
<
|
|
46
|
+
<li key={sequenceId} id={`sequence-${sequenceId}`} className={className}>
|
|
47
|
+
<MemoizedSequenceContent {...{ sequenceId, sequenceIsTemplate, isSavedToDatabase }} />
|
|
47
48
|
<ul>
|
|
48
49
|
{children}
|
|
49
50
|
</ul>
|
|
@@ -94,15 +95,16 @@ function NetWorkNode({ sourceId }) {
|
|
|
94
95
|
if (tooltipRef.current) {
|
|
95
96
|
tooltipRef.current.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
|
|
96
97
|
}
|
|
97
|
-
}, [ancestorsHidden, sourceId, sequenceId]);
|
|
98
|
+
}, [ancestorsHidden, sourceId, sequenceId, dispatch]);
|
|
98
99
|
|
|
99
100
|
const Icon = ancestorsHidden ? VisibilityIcon : VisibilityOffIcon;
|
|
100
101
|
const visibilityIconToolTip = ancestorsHidden ? 'Show ancestors' : 'Hide ancestors';
|
|
101
102
|
const isSavedToDatabase = database && hasDatabaseId;
|
|
103
|
+
const className = 'source-node' + (isSavedToDatabase ? ' in-database' : '');
|
|
102
104
|
|
|
103
105
|
return (
|
|
104
|
-
<SequenceWrapper {...{ sequenceId, sequenceIsTemplate }}>
|
|
105
|
-
<li id={`source-${sourceId}`} className={
|
|
106
|
+
<SequenceWrapper {...{ sequenceId, sequenceIsTemplate, isSavedToDatabase }}>
|
|
107
|
+
<li id={`source-${sourceId}`} className={className} style={{ marginBottom: ancestorsHidden ? '30px' : undefined }}>
|
|
106
108
|
<Box component="span" className="tf-nc" style={{ borderColor: isSavedToDatabase ? 'green' : 'default' }}>
|
|
107
109
|
<span className="node-text">
|
|
108
110
|
<SourceBox {...{ sourceId }}>
|
|
@@ -116,36 +118,36 @@ function NetWorkNode({ sourceId }) {
|
|
|
116
118
|
{sourceId}
|
|
117
119
|
</div>
|
|
118
120
|
{ (!sourceIsTemplate && sourceInput.length > 0 && sequenceId) && (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
<div className="before-node before-node-visibility">
|
|
122
|
+
<Tooltip
|
|
123
|
+
arrow
|
|
124
|
+
title={visibilityIconToolTip}
|
|
125
|
+
placement="left"
|
|
126
|
+
>
|
|
127
|
+
<div ref={tooltipRef}>
|
|
128
|
+
<Icon onClick={onVisibilityClick} style={{ color: 'grey' }} />
|
|
129
|
+
</div>
|
|
130
|
+
</Tooltip>
|
|
131
|
+
</div>
|
|
130
132
|
)}
|
|
131
133
|
{ (sourceIsTemplate && sourceInput.length > 0)
|
|
132
134
|
&& (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
<div className="before-node before-node-sequence-in-between">
|
|
136
|
+
<Tooltip arrow title="Add sequence in between" placement={sourceInput.length > 1 ? 'top' : 'left'}>
|
|
137
|
+
<div>
|
|
138
|
+
<AddCircleIcon onClick={() => { dispatch(addSequenceInBetween(sourceId)); }} color="success" />
|
|
139
|
+
</div>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
</div>
|
|
140
142
|
)}
|
|
141
143
|
</span>
|
|
142
144
|
</Box>
|
|
143
145
|
{parentSourceIds.length > 0 && (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
<ul style={ancestorsHidden ? { visibility: 'hidden', height: 0, overflow: 'hidden' } : undefined}>
|
|
147
|
+
{parentSourceIds.map((id) => (
|
|
148
|
+
<MemoizedNetWorkNode sourceId={id} key={`node-${id}`} />
|
|
149
|
+
))}
|
|
150
|
+
</ul>
|
|
149
151
|
)}
|
|
150
152
|
</li>
|
|
151
153
|
</SequenceWrapper>
|