@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +90 -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 +29 -22
  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 +31 -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/SourceFile.jsx +9 -1
  35. package/src/components/sources/SourceRepositoryId.jsx +0 -1
  36. package/src/components/sources/SourceTypeSelector.jsx +1 -1
  37. package/src/components/verification/ImportSequencingFilesInput.jsx +17 -0
  38. package/src/components/verification/LoadFromDatabaseButton.jsx +17 -10
  39. package/src/components/verification/VerificationFileDialog.jsx +16 -22
  40. package/src/components/verification/index.js +1 -0
  41. package/src/config/defaultMainEditorProps.js +1 -0
  42. package/src/hooks/useAlerts.js +3 -4
  43. package/src/hooks/useBackendAPI.js +5 -2
  44. package/src/hooks/useCloningAlerts.js +7 -0
  45. package/src/hooks/useDebouncedSearchQuery.js +42 -0
  46. package/src/hooks/useLoadDatabaseFile.js +6 -2
  47. package/src/hooks/useSequencingAlignment.js +26 -0
  48. package/src/hooks/useSnapgeneHistoryEndpoint.js +72 -0
  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/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.7.2",
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/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.2",
37
- "@opencloning/utils": "1.7.2",
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.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.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';
@@ -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 } = useAlerts();
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
- try {
136
- const { sources, sequences, warnings } = await processSequenceFiles(files, backendRoute, httpClient);
137
- batch(() => {
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 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>