@opencloning/ui 1.7.3 → 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 +80 -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 +2 -2
- 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/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 +3 -3
- 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,85 @@
|
|
|
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
|
+
|
|
3
83
|
## 1.7.3
|
|
4
84
|
|
|
5
85
|
### 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';
|
|
@@ -59,7 +59,7 @@ async function processSequenceFiles(files, backendRoute, httpClient) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
62
|
-
const { addAlert } =
|
|
62
|
+
const { addAlert } = useCloningAlerts();
|
|
63
63
|
const dispatch = useDispatch();
|
|
64
64
|
const backendRoute = useBackendRoute();
|
|
65
65
|
const store = useStore();
|
|
@@ -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>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useRef, useState } from 'react';
|
|
2
2
|
import { useDispatch, useSelector } from 'react-redux';
|
|
3
3
|
import { Tabs } from '@mui/material';
|
|
4
|
-
import { isEqual } from 'lodash-es';
|
|
5
4
|
import DescriptionEditor from './DescriptionEditor';
|
|
6
5
|
import PrimerList from './primers/PrimerList';
|
|
7
6
|
import SettingsTab from './settings/SettingsTab';
|
|
@@ -14,6 +13,7 @@ import SequenceTab from './SequenceTab';
|
|
|
14
13
|
import AppAlerts from './AppAlerts';
|
|
15
14
|
import Assembler from './assembler/Assembler';
|
|
16
15
|
import { useConfig } from '../hooks/useConfig';
|
|
16
|
+
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
17
17
|
|
|
18
18
|
const { setCurrentTab } = cloningActions;
|
|
19
19
|
|
|
@@ -23,6 +23,7 @@ function OpenCloning() {
|
|
|
23
23
|
const tabPanelsRef = useRef(null);
|
|
24
24
|
const [smallDevice, setSmallDevice] = useState(window.innerWidth < 600);
|
|
25
25
|
const { showAppBar: hasAppBar, enableAssembler } = useConfig();
|
|
26
|
+
const { removeAlert } = useCloningAlerts();
|
|
26
27
|
|
|
27
28
|
React.useEffect(() => {
|
|
28
29
|
const handleResize = () => {
|
|
@@ -44,7 +45,6 @@ function OpenCloning() {
|
|
|
44
45
|
|
|
45
46
|
return (
|
|
46
47
|
<div className="app-container" style={{ height: hasAppBar ? 'calc(100vh - 114px - 10px)' : '100vh' }}>
|
|
47
|
-
<AppAlerts />
|
|
48
48
|
<Tabs
|
|
49
49
|
variant={smallDevice ? 'scrollable' : 'standard'}
|
|
50
50
|
scrollButtons={smallDevice ? 'auto' : false}
|
|
@@ -52,7 +52,7 @@ function OpenCloning() {
|
|
|
52
52
|
centered={!smallDevice}
|
|
53
53
|
value={currentTab}
|
|
54
54
|
onChange={changeTab}
|
|
55
|
-
sx={{ pb:
|
|
55
|
+
sx={{ pb: 2, pt: 1 }}
|
|
56
56
|
id="opencloning-app-tabs"
|
|
57
57
|
>
|
|
58
58
|
<CustomTab label="Cloning" index={0} />
|
|
@@ -64,6 +64,7 @@ function OpenCloning() {
|
|
|
64
64
|
{enableAssembler && <CustomTab label="Assembler" index={6} />}
|
|
65
65
|
</Tabs>
|
|
66
66
|
<div className="tab-panels-container" ref={tabPanelsRef}>
|
|
67
|
+
<AppAlerts reducerName="cloning" removeAlert={removeAlert} />
|
|
67
68
|
<TabPanel index={1} value={currentTab} className="primer-tab-pannel">
|
|
68
69
|
<div className="primer-list-container">
|
|
69
70
|
<PrimerList />
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import SequenceViewer from './SequenceViewer';
|
|
4
|
+
import { convertToTeselaJson } from '@opencloning/utils/readNwrite';
|
|
5
|
+
|
|
6
|
+
describe('<SequenceViewer /> fullscreen', () => {
|
|
7
|
+
it('opens and closes fullscreen', () => {
|
|
8
|
+
cy.readFile('cypress/test_files/sequencing/locus.gb', 'utf8').then((fileContent) => {
|
|
9
|
+
const sequenceModel = { id: 'locus-1', file_content: fileContent };
|
|
10
|
+
const sequenceData = convertToTeselaJson(sequenceModel);
|
|
11
|
+
|
|
12
|
+
cy.mount(<SequenceViewer sequenceData={sequenceData} alignmentData={null} />);
|
|
13
|
+
|
|
14
|
+
cy.contains('2026 bps').should('be.visible');
|
|
15
|
+
cy.get('[aria-label="Fullscreen"]').should('be.visible').click();
|
|
16
|
+
|
|
17
|
+
cy.get('[aria-label="Exit fullscreen"]').should('be.visible');
|
|
18
|
+
cy.get('[aria-label="Fullscreen"]').should('not.exist');
|
|
19
|
+
|
|
20
|
+
cy.get('[aria-label="Exit fullscreen"]').click();
|
|
21
|
+
|
|
22
|
+
cy.get('[aria-label="Exit fullscreen"]').should('not.exist');
|
|
23
|
+
cy.get('[aria-label="Fullscreen"]').should('be.visible');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|