@opencloning/ui 1.7.3 → 1.9.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/package.json +20 -14
  3. package/src/components/AppAlerts.jsx +7 -7
  4. package/src/components/CloningHistory.jsx +2 -0
  5. package/src/components/CloningSpeedDial.jsx +102 -0
  6. package/src/components/DownloadCloningStrategyDialog.jsx +2 -2
  7. package/src/components/LoadCloningHistoryWrapper.jsx +2 -2
  8. package/src/components/MainSequenceEditor.jsx +41 -34
  9. package/src/components/NetworkNode.jsx +35 -33
  10. package/src/components/OpenCloning.jsx +4 -3
  11. package/src/components/SequenceViewer.cy.jsx +27 -0
  12. package/src/components/SequenceViewer.jsx +211 -0
  13. package/src/components/annotation/AnnotationChangedAlert.jsx +26 -0
  14. package/src/components/annotation/useUpdateAnnotationInMainSequence.js +2 -2
  15. package/src/components/assembler/Assembler.jsx +3 -3
  16. package/src/components/domesticate/DomesticateDialog.jsx +274 -0
  17. package/src/components/form/GetRequestMultiSelect.jsx +0 -1
  18. package/src/components/form/PostRequestSelect.jsx +0 -1
  19. package/src/components/form/QuerySelect.jsx +128 -0
  20. package/src/components/form/QueryStatusWrapper.jsx +28 -0
  21. package/src/components/form/SubmitToDatabaseDialog.jsx +63 -58
  22. package/src/components/form/TextFieldQueryValidated.jsx +93 -0
  23. package/src/components/index.js +4 -0
  24. package/src/components/navigation/FeedbackDialog.jsx +1 -1
  25. package/src/components/navigation/MainAppBar.jsx +5 -5
  26. package/src/components/primers/import_primers/ImportPrimersButton.jsx +2 -2
  27. package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +6 -3
  28. package/src/components/sources/FinishedSource.jsx +3 -0
  29. package/src/components/sources/MultipleOutputsSelector.cy.jsx +95 -0
  30. package/src/components/sources/MultipleOutputsSelector.jsx +32 -19
  31. package/src/components/sources/RecombinaseList.cy.jsx +1 -1
  32. package/src/components/sources/RecombinaseList.jsx +1 -1
  33. package/src/components/sources/SourceAssembly.jsx +36 -2
  34. package/src/components/sources/SourceRepositoryId.jsx +0 -1
  35. package/src/components/sources/SourceTypeSelector.jsx +1 -1
  36. package/src/components/verification/ImportSequencingFilesInput.jsx +17 -0
  37. package/src/components/verification/LoadFromDatabaseButton.jsx +17 -10
  38. package/src/components/verification/VerificationFileDialog.jsx +16 -22
  39. package/src/components/verification/index.js +1 -0
  40. package/src/config/defaultMainEditorProps.js +1 -0
  41. package/src/hooks/useAlerts.js +3 -4
  42. package/src/hooks/useBackendAPI.js +5 -2
  43. package/src/hooks/useCloningAlerts.js +7 -0
  44. package/src/hooks/useDebouncedSearchQuery.js +42 -0
  45. package/src/hooks/useHttpClient.js +7 -5
  46. package/src/hooks/useLoadDatabaseFile.js +6 -2
  47. package/src/hooks/useSequencingAlignment.js +26 -0
  48. package/src/hooks/useSnapgeneHistoryEndpoint.js +3 -3
  49. package/src/hooks/useStoreEditor.js +10 -68
  50. package/src/hooks/useUrlParamsLoader.js +2 -2
  51. package/src/hooks/useValidateState.js +3 -3
  52. package/src/index.css +3 -11
  53. package/src/providers/ConfigProvider.jsx +1 -0
  54. package/src/version.js +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 1.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#711](https://github.com/OpenCloning/OpenCloning_frontend/pull/711) [`6813620`](https://github.com/OpenCloning/OpenCloning_frontend/commit/6813620f4a47c44857045866d667474efbf9a64c) Thanks [@manulera](https://github.com/manulera)! - Use authentication for cloning endpoints in opencloning-db
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`6813620`](https://github.com/OpenCloning/OpenCloning_frontend/commit/6813620f4a47c44857045866d667474efbf9a64c)]:
12
+ - @opencloning/utils@1.9.0
13
+ - @opencloning/store@1.9.0
14
+
15
+ ## 1.8.2
16
+
17
+ ### Patch Changes
18
+
19
+ - [#709](https://github.com/OpenCloning/OpenCloning_frontend/pull/709) [`08144c0`](https://github.com/OpenCloning/OpenCloning_frontend/commit/08144c085cb5da37e902edc1a3b9c593c58ab680) Thanks [@manulera](https://github.com/manulera)! - Fix QueryStatusWrapper and its effect on SequenceDetailPage
20
+
21
+ - Updated dependencies []:
22
+ - @opencloning/store@1.8.2
23
+ - @opencloning/utils@1.8.2
24
+
25
+ ## 1.8.1
26
+
27
+ ### Patch Changes
28
+
29
+ - [#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
30
+
31
+ - Updated dependencies []:
32
+ - @opencloning/store@1.8.1
33
+ - @opencloning/utils@1.8.1
34
+
35
+ ## 1.8.0
36
+
37
+ ### Minor Changes
38
+
39
+ - [#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.
40
+ - 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.
41
+ - Prevent importing the same primer from the database twice (remove it from options in `PrimerDatabaseImportForm`).
42
+ - Add `Imported from unknown database` option in `FinishedSource` component, not to fail when `DatabaseSource` is present, but no database is set up.
43
+ - Improve placement of app alerts to be within the `OpenCloning` component, not app-wide.
44
+ - Fix hiding ancestors.
45
+ - Add standalone `SequenceViewer` component.
46
+ - Refactor alignment functionality into a separate hook for better reusability into `useSequencingAlignment.js`.
47
+ - Add support for loading
48
+
49
+ ### Patch Changes
50
+
51
+ - Updated dependencies [[`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57), [`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57)]:
52
+ - @opencloning/store@1.8.0
53
+ - @opencloning/utils@1.8.0
54
+
55
+ ## 1.7.8
56
+
57
+ ### Patch Changes
58
+
59
+ - Updated dependencies []:
60
+ - @opencloning/store@1.7.8
61
+ - @opencloning/utils@1.7.8
62
+
63
+ ## 1.7.7
64
+
65
+ ### Patch Changes
66
+
67
+ - [#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
68
+
69
+ - Updated dependencies [[`c5d9309`](https://github.com/manulera/OpenCloning_frontend/commit/c5d93099583fdfafafd40e9924d2216b596e0cec)]:
70
+ - @opencloning/utils@1.7.7
71
+ - @opencloning/store@1.7.7
72
+
73
+ ## 1.7.6
74
+
75
+ ### Patch Changes
76
+
77
+ - [#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.
78
+
79
+ - [#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.
80
+
81
+ - Updated dependencies []:
82
+ - @opencloning/store@1.7.6
83
+ - @opencloning/utils@1.7.6
84
+
85
+ ## 1.7.5
86
+
87
+ ### Patch Changes
88
+
89
+ - [#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
90
+
91
+ - Updated dependencies [[`034c738`](https://github.com/manulera/OpenCloning_frontend/commit/034c73840dc183b6b3584365c1db899d2f0c548b)]:
92
+ - @opencloning/store@1.7.5
93
+ - @opencloning/utils@1.7.5
94
+
95
+ ## 1.7.4
96
+
97
+ ### Patch Changes
98
+
99
+ - [#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
100
+
101
+ - Updated dependencies [[`04703a9`](https://github.com/manulera/OpenCloning_frontend/commit/04703a9269a5d4cc4bdde478c1ec61e5c73b0d2a)]:
102
+ - @opencloning/utils@1.7.4
103
+ - @opencloning/store@1.7.4
104
+
3
105
  ## 1.7.3
4
106
 
5
107
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "license": "MIT",
3
3
  "name": "@opencloning/ui",
4
- "version": "1.7.3",
4
+ "version": "1.9.0",
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/manulera/OpenCloning_frontend.git",
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.0",
33
- "@mui/icons-material": "^5.15.17",
34
- "@mui/material": "^5.15.17",
35
- "@mui/x-data-grid": "^8.25.0",
36
- "@opencloning/store": "1.7.3",
37
- "@opencloning/utils": "1.7.3",
38
- "@teselagen/bio-parsers": "^0.4.34",
39
- "@teselagen/ove": "^0.8.34",
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.9.0",
43
+ "@opencloning/utils": "1.9.0",
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.7.62",
43
- "axios": "^1.12.2",
44
- "lodash-es": "^4.17.23",
45
- "react-draggable": "^4.4.6"
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.cloning, isEqual);
10
- const { removeAlert } = useAlerts();
7
+ function AppAlerts( { reducerName, removeAlert } ) {
8
+ const { alerts } = useSelector((state) => state[reducerName], isEqual);
11
9
  return (
12
- <div id="global-error-message-wrapper">
13
- {alerts.map((alert, index) => (<Alert key={index} severity={alert.severity} onClose={() => { removeAlert(alert.message); }}>{alert.message}</Alert>))}
14
- <ExternalServicesStatusCheck />
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 useAlerts from '../hooks/useAlerts';
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 } = useAlerts();
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 useAlerts from '../hooks/useAlerts';
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 } = useAlerts();
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 useAlerts from '../hooks/useAlerts';
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 } = useAlerts();
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.future.length === 0;
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
- <Alert
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
- const SequenceContent = React.memo(({ sequenceId, sequenceIsTemplate }) => {
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="sequence-node">
46
- <SequenceContent {...{ sequenceId, sequenceIsTemplate }} />
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={`source-node ${ancestorsHidden ? 'hidden-ancestors' : ''}`}>
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
- <div className="before-node before-node-visibility">
120
- <Tooltip
121
- arrow
122
- title={visibilityIconToolTip}
123
- placement="left"
124
- >
125
- <div ref={tooltipRef}>
126
- <Icon onClick={onVisibilityClick} style={{ color: 'grey' }} />
127
- </div>
128
- </Tooltip>
129
- </div>
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
- <div className="before-node before-node-sequence-in-between">
134
- <Tooltip arrow title="Add sequence in between" placement={sourceInput.length > 1 ? 'top' : 'left'}>
135
- <div>
136
- <AddCircleIcon onClick={() => { dispatch(addSequenceInBetween(sourceId)); }} color="success" />
137
- </div>
138
- </Tooltip>
139
- </div>
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
- <ul className={ancestorsHidden ? 'hidden-ancestors' : ''}>
145
- {parentSourceIds.map((id) => (
146
- <MemoizedNetWorkNode sourceId={id} key={`node-${id}`} />
147
- ))}
148
- </ul>
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: 3, pt: 1 }}
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
+