@opencloning/ui 1.9.1 → 1.9.2
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 +12 -0
- package/package.json +5 -3
- package/src/components/LoadCloningHistoryWrapper.jsx +16 -39
- package/src/components/SequenceViewer.jsx +12 -0
- package/src/components/form/TextFieldQueryValidated.jsx +22 -4
- package/src/components/navigation/MainAppBar.jsx +22 -18
- package/src/components/sources/SourceFile.jsx +19 -49
- package/src/hooks/index.js +2 -0
- package/src/hooks/useCloningHistoryLoader.js +163 -0
- package/src/hooks/useLoadDatabaseFile.js +15 -32
- package/src/hooks/useUrlParamsLoader.js +21 -18
- package/src/version.js +1 -1
- package/src/hooks/useSnapgeneHistoryEndpoint.js +0 -72
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 1.9.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#734](https://github.com/OpenCloning/OpenCloning_frontend/pull/734) [`06bb1a5`](https://github.com/OpenCloning/OpenCloning_frontend/commit/06bb1a513519f598e9ec3b5eec73544e2ed3958e) Thanks [@manulera](https://github.com/manulera)! - Add duplicate feature option to the database sequence viewer
|
|
8
|
+
|
|
9
|
+
- [#736](https://github.com/OpenCloning/OpenCloning_frontend/pull/736) [`bc045f9`](https://github.com/OpenCloning/OpenCloning_frontend/commit/bc045f9e74b4d5b84250dcb5a77b93de99e4d684) Thanks [@manulera](https://github.com/manulera)! - \* Unify load paths under single hook
|
|
10
|
+
- Remove warnings when adding db files to design tab
|
|
11
|
+
- Updated dependencies [[`bc045f9`](https://github.com/OpenCloning/OpenCloning_frontend/commit/bc045f9e74b4d5b84250dcb5a77b93de99e4d684)]:
|
|
12
|
+
- @opencloning/store@1.9.2
|
|
13
|
+
- @opencloning/utils@1.9.2
|
|
14
|
+
|
|
3
15
|
## 1.9.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"license": "MIT",
|
|
3
3
|
"name": "@opencloning/ui",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"scripts": {
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"./hooks/useAlerts": "./src/hooks/useAlerts.js",
|
|
26
26
|
"./hooks/useCloningAlerts": "./src/hooks/useCloningAlerts.js",
|
|
27
27
|
"./hooks/useSequencingAlignment": "./src/hooks/useSequencingAlignment.js",
|
|
28
|
+
"./hooks/useBackendRoute": "./src/hooks/useBackendRoute.js",
|
|
29
|
+
"./hooks/useHttpClient": "./src/hooks/useHttpClient.js",
|
|
28
30
|
"./components/SequenceViewer": "./src/components/SequenceViewer.jsx",
|
|
29
31
|
"./standalone": "./src/StandAloneOpenCloning.js"
|
|
30
32
|
},
|
|
@@ -39,8 +41,8 @@
|
|
|
39
41
|
"@mui/icons-material": "^5.18.0",
|
|
40
42
|
"@mui/material": "^5.18.0",
|
|
41
43
|
"@mui/x-data-grid": "^8.28.5",
|
|
42
|
-
"@opencloning/store": "1.9.
|
|
43
|
-
"@opencloning/utils": "1.9.
|
|
44
|
+
"@opencloning/store": "1.9.2",
|
|
45
|
+
"@opencloning/utils": "1.9.2",
|
|
44
46
|
"@teselagen/bio-parsers": "^0.4.38",
|
|
45
47
|
"@teselagen/ove": "^0.8.42",
|
|
46
48
|
"@teselagen/range-utils": "^0.3.20",
|
|
@@ -2,17 +2,13 @@ import React from 'react';
|
|
|
2
2
|
import { batch, useDispatch, useStore } from 'react-redux';
|
|
3
3
|
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
4
4
|
import useBackendRoute from '../hooks/useBackendRoute';
|
|
5
|
-
import
|
|
5
|
+
import useCloningHistoryLoader from '../hooks/useCloningHistoryLoader';
|
|
6
|
+
import { getEmptyPlaceholderSource } from '@opencloning/store/cloning_utils';
|
|
6
7
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
7
|
-
import { mergeStates } from '@opencloning/utils/network';
|
|
8
|
-
import { loadFilesToSessionStorage, loadHistoryFile, updateVerificationFileNames } from '@opencloning/utils/readNwrite';
|
|
9
8
|
import HistoryLoadedDialog from './HistoryLoadedDialog';
|
|
10
9
|
import useHttpClient from '../hooks/useHttpClient';
|
|
11
|
-
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
12
|
-
import { isEqual } from 'lodash-es';
|
|
13
|
-
import useSnapgeneHistoryEndpoint from '../hooks/useSnapgeneHistoryEndpoint';
|
|
14
10
|
|
|
15
|
-
const {
|
|
11
|
+
const { deleteSourceAndItsChildren, addSourceAndItsOutputSequence } = cloningActions;
|
|
16
12
|
|
|
17
13
|
async function processSequenceFiles(files, backendRoute, httpClient) {
|
|
18
14
|
const allSources = [];
|
|
@@ -63,9 +59,8 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
|
63
59
|
const dispatch = useDispatch();
|
|
64
60
|
const backendRoute = useBackendRoute();
|
|
65
61
|
const store = useStore();
|
|
66
|
-
const validateState = useValidateState();
|
|
67
62
|
const httpClient = useHttpClient();
|
|
68
|
-
const { loadSnapgeneHistory } =
|
|
63
|
+
const { loadHistoryFromFile, loadSnapgeneHistory } = useCloningHistoryLoader();
|
|
69
64
|
const [fileLoaderFunctions, setFileLoaderFunctions] = React.useState(null);
|
|
70
65
|
|
|
71
66
|
React.useEffect(() => {
|
|
@@ -81,34 +76,11 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
|
81
76
|
const isJsonFile = files.length === 1 && files[0].name.endsWith('.json');
|
|
82
77
|
// If the file is a zip or json file, load the history file
|
|
83
78
|
if (isZipFile || isJsonFile) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
({ cloningStrategy, verificationFiles } = await loadHistoryFile(files[0]));
|
|
88
|
-
} catch (e) {
|
|
89
|
-
console.error(e);
|
|
90
|
-
addAlert({ message: e.message, severity: 'error' });
|
|
79
|
+
const prepared = await loadHistoryFromFile(files[0]);
|
|
80
|
+
if (!prepared) {
|
|
91
81
|
return;
|
|
92
82
|
}
|
|
93
83
|
|
|
94
|
-
const validatedState = await validateState(cloningStrategy);
|
|
95
|
-
// Update the verificationFiles names if needed
|
|
96
|
-
const updatedVerificationFiles = updateVerificationFileNames(verificationFiles, cloningStrategy.files, validatedState.files);
|
|
97
|
-
|
|
98
|
-
const updateState = async (newState, idShift) => {
|
|
99
|
-
dispatch(setCloningState(newState));
|
|
100
|
-
await loadFilesToSessionStorage(updatedVerificationFiles, idShift);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const replaceState = async () => {
|
|
104
|
-
await updateState(validatedState, 0);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const addState = async () => {
|
|
108
|
-
const { mergedState, idShift } = mergeStates(validatedState, cloningState);
|
|
109
|
-
await updateState(mergedState, idShift);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
84
|
const errorWrapper = (fn) => async () => {
|
|
113
85
|
try {
|
|
114
86
|
await fn();
|
|
@@ -117,11 +89,15 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
|
117
89
|
addAlert({ message: e.message, severity: 'error' });
|
|
118
90
|
}
|
|
119
91
|
};
|
|
120
|
-
|
|
92
|
+
// If there are no sequences in the cloning state, load the history file
|
|
121
93
|
if (cloningState.sequences.length === 0) {
|
|
122
|
-
errorWrapper(
|
|
94
|
+
errorWrapper(prepared.replace)();
|
|
123
95
|
} else {
|
|
124
|
-
setFileLoaderFunctions({
|
|
96
|
+
setFileLoaderFunctions({
|
|
97
|
+
addState: errorWrapper(prepared.merge),
|
|
98
|
+
replaceState: errorWrapper(prepared.replace),
|
|
99
|
+
clear: () => setFileLoaderFunctions(null),
|
|
100
|
+
});
|
|
125
101
|
}
|
|
126
102
|
// If there are sequences in the cloning state, give the option to merge or replace
|
|
127
103
|
|
|
@@ -144,8 +120,9 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
|
144
120
|
const { sources, sequences, warnings } = await processSequenceFiles(files, backendRoute, httpClient);
|
|
145
121
|
batch(() => {
|
|
146
122
|
// If there is only one source and it is empty, delete it
|
|
147
|
-
|
|
148
|
-
|
|
123
|
+
const emptyPlaceholder = getEmptyPlaceholderSource(cloningState.sources);
|
|
124
|
+
if (emptyPlaceholder) {
|
|
125
|
+
dispatch(deleteSourceAndItsChildren(emptyPlaceholder.id));
|
|
149
126
|
}
|
|
150
127
|
for (let i = 0; i < sources.length; i += 1) {
|
|
151
128
|
dispatch(addSourceAndItsOutputSequence({ source: sources[i], sequence: sequences[i] }));
|
|
@@ -48,6 +48,18 @@ function featureRightClickedOverride(items, { annotation }, props) {
|
|
|
48
48
|
"--",
|
|
49
49
|
"editFeature",
|
|
50
50
|
"deleteFeature",
|
|
51
|
+
{
|
|
52
|
+
text: "Duplicate Feature",
|
|
53
|
+
disabled: props.readOnly,
|
|
54
|
+
onClick: () => {
|
|
55
|
+
const { id, ...rest } = annotation;
|
|
56
|
+
props.upsertFeature({
|
|
57
|
+
...rest,
|
|
58
|
+
// omit id → upsertDeleteActionGenerator assigns a new shortid
|
|
59
|
+
name: annotation.name ? `${annotation.name} (copy)` : undefined,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
},
|
|
51
63
|
"showRemoveDuplicatesDialogFeatures",
|
|
52
64
|
"--",
|
|
53
65
|
"toggleCdsFeatureTranslations",
|
|
@@ -10,6 +10,7 @@ import { TextField } from '@mui/material';
|
|
|
10
10
|
* @param {string} placeholder - Placeholder text
|
|
11
11
|
* @param {Function} getQuery - (value) => ({ queryKey, queryFn, ... }) — returns a query config for the given value (use value in queryKey so each input has its own cache).
|
|
12
12
|
* @param {Function} onChange - Callback when validation succeeds; receives (queryData). Called with null when value is empty or validation fails.
|
|
13
|
+
* @param {Function} onValidationStateChange - Optional callback with ({ isChecking, isValid, hasValue }) when validation lifecycle changes.
|
|
13
14
|
* @param {string} value - Controlled value (optional)
|
|
14
15
|
* @param {number} debounceDelay - Delay in ms before running the query (default: 500)
|
|
15
16
|
* @param {string} successMessage - Optional message to show when validation succeeds (default: ' ')
|
|
@@ -20,6 +21,7 @@ export default function TextFieldQueryValidated({
|
|
|
20
21
|
placeholder,
|
|
21
22
|
getQuery,
|
|
22
23
|
onChange,
|
|
24
|
+
onValidationStateChange,
|
|
23
25
|
debounceDelay = 500,
|
|
24
26
|
successMessage = ' ',
|
|
25
27
|
validatingMessage = 'Validating...',
|
|
@@ -44,24 +46,40 @@ export default function TextFieldQueryValidated({
|
|
|
44
46
|
[getQuery, debouncedValue],
|
|
45
47
|
);
|
|
46
48
|
|
|
47
|
-
const { data: validationError, isLoading, error, isSuccess } = useQuery({
|
|
49
|
+
const { data: validationError, isLoading, isFetching, error, isSuccess } = useQuery({
|
|
48
50
|
...queryConfig,
|
|
49
51
|
enabled: debouncedValue.length > 0 && (queryConfig.enabled !== false),
|
|
50
52
|
});
|
|
51
53
|
|
|
54
|
+
const isDebouncing = localValue !== debouncedValue;
|
|
55
|
+
const isChecking =
|
|
56
|
+
localValue.length > 0 &&
|
|
57
|
+
(isDebouncing || (debouncedValue.length > 0 && (isLoading || isFetching)));
|
|
58
|
+
const isValid =
|
|
59
|
+
localValue.length > 0 &&
|
|
60
|
+
localValue === debouncedValue &&
|
|
61
|
+
isSuccess &&
|
|
62
|
+
!validationError &&
|
|
63
|
+
!error;
|
|
64
|
+
|
|
65
|
+
const hasValue = localValue.length > 0;
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
onValidationStateChange?.({ isChecking, isValid, hasValue });
|
|
69
|
+
}, [isChecking, isValid, hasValue, onValidationStateChange]);
|
|
52
70
|
|
|
53
71
|
useEffect(() => {
|
|
54
72
|
if (debouncedValue.length === 0) {
|
|
55
73
|
onChange('');
|
|
56
74
|
return;
|
|
57
75
|
}
|
|
58
|
-
if (isSuccess && !validationError) {
|
|
76
|
+
if (localValue === debouncedValue && isSuccess && !validationError) {
|
|
59
77
|
onChange(debouncedValue);
|
|
60
78
|
}
|
|
61
79
|
if (error || validationError) {
|
|
62
80
|
onChange('');
|
|
63
81
|
}
|
|
64
|
-
}, [debouncedValue, isSuccess, validationError, error, onChange]);
|
|
82
|
+
}, [localValue, debouncedValue, isSuccess, validationError, error, onChange]);
|
|
65
83
|
|
|
66
84
|
const handleChange = (e) => {
|
|
67
85
|
onChange('');
|
|
@@ -69,7 +87,7 @@ export default function TextFieldQueryValidated({
|
|
|
69
87
|
};
|
|
70
88
|
|
|
71
89
|
let helperText = ' ';
|
|
72
|
-
if (
|
|
90
|
+
if (isChecking) {
|
|
73
91
|
helperText = validatingMessage;
|
|
74
92
|
} else if (validationError) {
|
|
75
93
|
helperText = validationError;
|
|
@@ -4,7 +4,7 @@ import { Menu as MenuIcon } from '@mui/icons-material';
|
|
|
4
4
|
import './MainAppBar.css';
|
|
5
5
|
import { useDispatch } from 'react-redux';
|
|
6
6
|
import ButtonWithMenu from './ButtonWithMenu';
|
|
7
|
-
import { downloadCloningStrategyAsSvg, formatTemplate
|
|
7
|
+
import { downloadCloningStrategyAsSvg, formatTemplate } from '@opencloning/utils/readNwrite';
|
|
8
8
|
import SelectExampleDialog from './SelectExampleDialog';
|
|
9
9
|
import SelectTemplateDialog from './SelectTemplateDialog';
|
|
10
10
|
import FeedbackDialog from './FeedbackDialog';
|
|
@@ -13,11 +13,11 @@ import VersionDialog from './VersionDialog';
|
|
|
13
13
|
import useCloningAlerts from '../../hooks/useCloningAlerts';
|
|
14
14
|
import DownloadCloningStrategyDialog from '../DownloadCloningStrategyDialog';
|
|
15
15
|
import LoadCloningHistoryWrapper from '../LoadCloningHistoryWrapper';
|
|
16
|
-
import
|
|
16
|
+
import useCloningHistoryLoader from '../../hooks/useCloningHistoryLoader';
|
|
17
17
|
import GithubCornerRight from './GithubCornerRight';
|
|
18
18
|
import useHttpClient from '../../hooks/useHttpClient';
|
|
19
19
|
|
|
20
|
-
const { setCurrentTab
|
|
20
|
+
const { setCurrentTab } = cloningActions;
|
|
21
21
|
|
|
22
22
|
function MainAppBar() {
|
|
23
23
|
const [openExampleDialog, setOpenExampleDialog] = React.useState(false);
|
|
@@ -32,7 +32,7 @@ function MainAppBar() {
|
|
|
32
32
|
|
|
33
33
|
const dispatch = useDispatch();
|
|
34
34
|
const { addAlert } = useCloningAlerts();
|
|
35
|
-
const
|
|
35
|
+
const { applyCloningStrategy, loadHistoryFromFile } = useCloningHistoryLoader();
|
|
36
36
|
|
|
37
37
|
// Hidden input field, used to load files.
|
|
38
38
|
const fileInputRef = React.useRef(null);
|
|
@@ -52,26 +52,30 @@ function MainAppBar() {
|
|
|
52
52
|
setOpenExampleDialog(false);
|
|
53
53
|
setOpenTemplateDialog(false);
|
|
54
54
|
if (url) {
|
|
55
|
-
|
|
55
|
+
const fileName = url.split('/').pop();
|
|
56
|
+
let file;
|
|
56
57
|
if (url.endsWith('.zip')) {
|
|
57
|
-
// For zip files, get as blob and process with loadHistoryFile
|
|
58
58
|
const { data: blob } = await httpClient.get(url, { responseType: 'blob' });
|
|
59
|
-
|
|
60
|
-
// eslint-disable-next-line no-undef
|
|
61
|
-
const file = new File([blob], fileName);
|
|
62
|
-
const { cloningStrategy, verificationFiles } = await loadHistoryFile(file);
|
|
63
|
-
data = await validateState(cloningStrategy);
|
|
64
|
-
await loadFilesToSessionStorage(verificationFiles, 0);
|
|
59
|
+
file = new File([blob], fileName);
|
|
65
60
|
} else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
data = await validateState(jsonData);
|
|
61
|
+
const { data: jsonData } = await httpClient.get(url);
|
|
62
|
+
file = new File([JSON.stringify(jsonData)], fileName);
|
|
69
63
|
}
|
|
64
|
+
const prepared = await loadHistoryFromFile(file, {
|
|
65
|
+
onError: (message) => addAlert({ message, severity: 'error' }),
|
|
66
|
+
});
|
|
67
|
+
if (!prepared) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let strategy = prepared.cloningStrategy;
|
|
70
71
|
if (isTemplate) {
|
|
71
|
-
|
|
72
|
+
strategy = formatTemplate(strategy, url);
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
await applyCloningStrategy(strategy, {
|
|
75
|
+
verificationFiles: prepared.verificationFiles,
|
|
76
|
+
mode: 'replace',
|
|
77
|
+
});
|
|
78
|
+
dispatch(setCurrentTab(0));
|
|
75
79
|
}
|
|
76
80
|
};
|
|
77
81
|
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { FormHelperText, Alert, Checkbox, FormControl, FormControlLabel, InputLabel, MenuItem, Select, TextField } from '@mui/material';
|
|
3
|
-
import {
|
|
3
|
+
import { useSelector } from 'react-redux';
|
|
4
4
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
5
5
|
import LabelWithTooltip from '../form/LabelWithTooltip';
|
|
6
|
-
import
|
|
7
|
-
import { loadFilesToSessionStorage, loadHistoryFile, updateVerificationFileNames } from '@opencloning/utils/readNwrite';
|
|
8
|
-
import useValidateState from '../../hooks/useValidateState';
|
|
9
|
-
import { mergeStates, getGraftSequenceId, graftState } from '@opencloning/utils/network';
|
|
10
|
-
import useSnapgeneHistoryEndpoint from '../../hooks/useSnapgeneHistoryEndpoint';
|
|
11
|
-
|
|
12
|
-
const { deleteSourceAndItsChildren, restoreSource, setState: setCloningState } = cloningActions;
|
|
6
|
+
import useCloningHistoryLoader from '../../hooks/useCloningHistoryLoader';
|
|
13
7
|
|
|
14
8
|
const fileFormatToExtension = {
|
|
15
9
|
genbank: 'gb',
|
|
@@ -28,10 +22,7 @@ function SourceFile({ source, requestStatus, sendPostRequest }) {
|
|
|
28
22
|
const hasOutput = useSelector((state) => state.cloning.sequences.some((s) => s.id === source.id));
|
|
29
23
|
// Error message for json only
|
|
30
24
|
const [alert, setAlert] = React.useState(null);
|
|
31
|
-
const
|
|
32
|
-
const validateState = useValidateState();
|
|
33
|
-
const store = useStore();
|
|
34
|
-
const { loadSnapgeneHistory } = useSnapgeneHistoryEndpoint();
|
|
25
|
+
const { loadHistoryFromFile, loadSnapgeneHistory } = useCloningHistoryLoader();
|
|
35
26
|
const onChange = async (event) => {
|
|
36
27
|
setAlert(null);
|
|
37
28
|
const files = Array.from(event.target.files);
|
|
@@ -39,6 +30,7 @@ function SourceFile({ source, requestStatus, sendPostRequest }) {
|
|
|
39
30
|
return;
|
|
40
31
|
}
|
|
41
32
|
event.target.value = null;
|
|
33
|
+
const reportError = (message) => setAlert({ message, severity: 'error' });
|
|
42
34
|
// If the file is a history file, we load it
|
|
43
35
|
if (
|
|
44
36
|
fileFormat === 'json' || fileFormat === 'zip'
|
|
@@ -50,54 +42,32 @@ function SourceFile({ source, requestStatus, sendPostRequest }) {
|
|
|
50
42
|
type: fileFormat === 'json' ? 'application/json' : files[0].type,
|
|
51
43
|
});
|
|
52
44
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
({ cloningStrategy, verificationFiles } = await loadHistoryFile(files[0]));
|
|
57
|
-
} catch (e) {
|
|
58
|
-
console.error(e);
|
|
59
|
-
setAlert({ message: e.message, severity: 'error' });
|
|
45
|
+
|
|
46
|
+
const prepared = await loadHistoryFromFile(files[0], { onError: reportError });
|
|
47
|
+
if (!prepared) {
|
|
60
48
|
return;
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
// Update the verificationFiles names if needed
|
|
65
|
-
const updatedVerificationFiles = updateVerificationFileNames(verificationFiles, cloningStrategy.files, validatedCloningStrategy.files);
|
|
66
|
-
|
|
67
|
-
const canGraft = getGraftSequenceId(validatedCloningStrategy) !== null;
|
|
68
|
-
const graft = hasOutput && canGraft;
|
|
69
|
-
|
|
70
|
-
if (hasOutput && !canGraft) {
|
|
51
|
+
if (hasOutput && !prepared.canGraft) {
|
|
71
52
|
setAlert({ message: 'Cannot graft cloning strategy as it does not converge on a single sequence, you can load it on a source without outputs', severity: 'error' });
|
|
72
53
|
return;
|
|
73
54
|
}
|
|
74
55
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let mergedState;
|
|
83
|
-
let idShift;
|
|
84
|
-
if (graft) {
|
|
85
|
-
({ mergedState, idShift } = graftState(validatedCloningStrategy, cloningState, source.id));
|
|
86
|
-
} else {
|
|
87
|
-
({ mergedState, idShift } = mergeStates(validatedCloningStrategy, cloningState));
|
|
88
|
-
}
|
|
89
|
-
dispatch(setCloningState(mergedState));
|
|
90
|
-
await loadFilesToSessionStorage(updatedVerificationFiles, idShift);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
setAlert({ message: e.message, severity: 'error' });
|
|
93
|
-
dispatch(restoreSource({ ...source, type: 'UploadedFileSource' }));
|
|
56
|
+
const graft = hasOutput && prepared.canGraft;
|
|
57
|
+
try {
|
|
58
|
+
const applyOptions = { source, onError: reportError };
|
|
59
|
+
if (graft) {
|
|
60
|
+
await prepared.graft(applyOptions);
|
|
61
|
+
} else {
|
|
62
|
+
await prepared.merge(applyOptions);
|
|
94
63
|
}
|
|
95
|
-
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Error already reported via onError; state restored in applyCloningStrategy
|
|
66
|
+
}
|
|
96
67
|
return;
|
|
97
68
|
}
|
|
98
69
|
if (fileFormat === 'snapgene' || (fileFormat === '' && files[0].name.endsWith('.dna'))) {
|
|
99
|
-
|
|
100
|
-
const success = await loadSnapgeneHistory(files[0], source.id);
|
|
70
|
+
const success = await loadSnapgeneHistory(files[0], { source });
|
|
101
71
|
if (success) {
|
|
102
72
|
return;
|
|
103
73
|
}
|
package/src/hooks/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { useConfig } from './useConfig.js';
|
|
2
2
|
export { default as useServerStaticFiles, localFilesHttpClient } from './useServerStaticFiles.js';
|
|
3
|
+
export { default as useBackendRoute } from './useBackendRoute.js';
|
|
4
|
+
export { default as useHttpClient } from './useHttpClient.js';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { batch, useDispatch, useStore } from 'react-redux';
|
|
3
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
4
|
+
import { getEmptyPlaceholderSource } from '@opencloning/store/cloning_utils';
|
|
5
|
+
import { graftState, getGraftSequenceId, mergeStates } from '@opencloning/utils/network';
|
|
6
|
+
import {
|
|
7
|
+
loadFilesToSessionStorage,
|
|
8
|
+
parseHistoryFile,
|
|
9
|
+
updateVerificationFileNames,
|
|
10
|
+
} from '@opencloning/utils/readNwrite';
|
|
11
|
+
import useBackendRoute from './useBackendRoute';
|
|
12
|
+
import useCloningAlerts from './useCloningAlerts';
|
|
13
|
+
import useHttpClient from './useHttpClient';
|
|
14
|
+
import useValidateState from './useValidateState';
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
setState: setCloningState,
|
|
18
|
+
deleteSourceAndItsChildren,
|
|
19
|
+
} = cloningActions;
|
|
20
|
+
|
|
21
|
+
function makeAlertHandler(addAlert, onError, severity = 'error') {
|
|
22
|
+
return onError || ((message) => addAlert({ message, severity }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldDeleteSourceBeforeApply(mode, source) {
|
|
26
|
+
return source != null && mode === 'merge';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function useCloningHistoryLoader() {
|
|
30
|
+
const dispatch = useDispatch();
|
|
31
|
+
const store = useStore();
|
|
32
|
+
const validateState = useValidateState();
|
|
33
|
+
const { addAlert } = useCloningAlerts();
|
|
34
|
+
const backendRoute = useBackendRoute();
|
|
35
|
+
const httpClient = useHttpClient();
|
|
36
|
+
const snapgeneUrl = backendRoute('read_snapgene_history');
|
|
37
|
+
|
|
38
|
+
const applyCloningStrategy = React.useCallback(async (cloningStrategy, options = {}) => {
|
|
39
|
+
const {
|
|
40
|
+
verificationFiles = [],
|
|
41
|
+
mode,
|
|
42
|
+
source = null,
|
|
43
|
+
onError,
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
const reportError = makeAlertHandler(addAlert, onError);
|
|
47
|
+
const deleteSourceBeforeApply = shouldDeleteSourceBeforeApply(mode, source);
|
|
48
|
+
const sourceId = source?.id ?? null;
|
|
49
|
+
|
|
50
|
+
const prevState = store.getState().cloning;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (deleteSourceBeforeApply) {
|
|
54
|
+
dispatch(deleteSourceAndItsChildren(sourceId));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cloningState = store.getState().cloning;
|
|
58
|
+
let newState;
|
|
59
|
+
let idShift = 0;
|
|
60
|
+
|
|
61
|
+
if (mode === 'replace') {
|
|
62
|
+
newState = cloningStrategy;
|
|
63
|
+
} else if (mode === 'merge') {
|
|
64
|
+
({ mergedState: newState, idShift } = mergeStates(cloningStrategy, cloningState));
|
|
65
|
+
} else if (mode === 'graft') {
|
|
66
|
+
({ mergedState: newState, idShift } = graftState(cloningStrategy, cloningState, sourceId));
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(`Unknown apply mode: ${mode}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
batch(() => {
|
|
72
|
+
dispatch(setCloningState(newState));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await loadFilesToSessionStorage(verificationFiles, idShift);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.error(e);
|
|
78
|
+
dispatch(setCloningState(prevState));
|
|
79
|
+
reportError(e.message);
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
}, [addAlert, dispatch, store]);
|
|
83
|
+
|
|
84
|
+
const loadHistoryFromFile = React.useCallback(async (file, options = {}) => {
|
|
85
|
+
const { onError } = options;
|
|
86
|
+
const reportError = makeAlertHandler(addAlert, onError);
|
|
87
|
+
|
|
88
|
+
let cloningStrategy;
|
|
89
|
+
let verificationFiles;
|
|
90
|
+
try {
|
|
91
|
+
({ cloningStrategy, verificationFiles } = await parseHistoryFile(file));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(e);
|
|
94
|
+
reportError(e.message);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const validatedState = await validateState(cloningStrategy);
|
|
99
|
+
const updatedVerificationFiles = updateVerificationFileNames(
|
|
100
|
+
verificationFiles,
|
|
101
|
+
cloningStrategy.files,
|
|
102
|
+
validatedState.files,
|
|
103
|
+
);
|
|
104
|
+
const canGraft = getGraftSequenceId(validatedState) !== null;
|
|
105
|
+
|
|
106
|
+
const apply = (applyOptions) => applyCloningStrategy(validatedState, {
|
|
107
|
+
verificationFiles: updatedVerificationFiles,
|
|
108
|
+
validate: false,
|
|
109
|
+
...applyOptions,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
cloningStrategy: validatedState,
|
|
114
|
+
verificationFiles: updatedVerificationFiles,
|
|
115
|
+
canGraft,
|
|
116
|
+
replace: (applyOptions = {}) => apply({ mode: 'replace', ...applyOptions }),
|
|
117
|
+
merge: (applyOptions = {}) => apply({ mode: 'merge', ...applyOptions }),
|
|
118
|
+
graft: (applyOptions = {}) => apply({ mode: 'graft', ...applyOptions }),
|
|
119
|
+
};
|
|
120
|
+
}, [addAlert, applyCloningStrategy, validateState]);
|
|
121
|
+
|
|
122
|
+
const loadSnapgeneHistory = React.useCallback(async (file, options = {}) => {
|
|
123
|
+
const { source = null, onError, onWarning } = options;
|
|
124
|
+
const reportWarning = makeAlertHandler(addAlert, onWarning, 'warning');
|
|
125
|
+
|
|
126
|
+
const cloningState = store.getState().cloning;
|
|
127
|
+
const formData = new FormData();
|
|
128
|
+
formData.append('file', file);
|
|
129
|
+
const config = {
|
|
130
|
+
headers: {
|
|
131
|
+
'content-type': 'multipart/form-data',
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const resp = await httpClient.post(snapgeneUrl, formData, config);
|
|
137
|
+
const { data: cloningStrategy } = resp;
|
|
138
|
+
|
|
139
|
+
const isEmptyState = getEmptyPlaceholderSource(cloningState.sources) !== null;
|
|
140
|
+
const mode = isEmptyState ? 'replace' : 'merge';
|
|
141
|
+
|
|
142
|
+
if (resp.headers['x-warning']) {
|
|
143
|
+
reportWarning(`Not all SnapGene history was parsed: ${resp.headers['x-warning']}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await applyCloningStrategy(cloningStrategy, {
|
|
147
|
+
mode,
|
|
148
|
+
source,
|
|
149
|
+
onError,
|
|
150
|
+
});
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
reportWarning('Failed to parse the history from the SnapGene file, will only read the sequence.');
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}, [addAlert, applyCloningStrategy, httpClient, snapgeneUrl, store]);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
applyCloningStrategy,
|
|
160
|
+
loadHistoryFromFile,
|
|
161
|
+
loadSnapgeneHistory,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
import { batch, useDispatch, useStore } from 'react-redux';
|
|
2
1
|
import { jsonToGenbank } from '@teselagen/bio-parsers';
|
|
3
2
|
import useValidateState from './useValidateState';
|
|
4
|
-
import { convertToTeselaJson,
|
|
3
|
+
import { convertToTeselaJson, parseHistoryFile } from '@opencloning/utils/readNwrite';
|
|
5
4
|
import { getIdsOfSequencesWithoutChildSource } from '@opencloning/store/cloning_utils';
|
|
6
|
-
import
|
|
7
|
-
import { cloningActions } from '@opencloning/store/cloning';
|
|
5
|
+
import useCloningHistoryLoader from './useCloningHistoryLoader';
|
|
8
6
|
import useDatabase from './useDatabase';
|
|
9
|
-
|
|
10
|
-
const { deleteSourceAndItsChildren, setState: setCloningState } = cloningActions;
|
|
7
|
+
import { useStore } from 'react-redux';
|
|
11
8
|
|
|
12
9
|
export default function useLoadDatabaseFile({ source, sendPostRequest, setHistoryFileError }) {
|
|
13
|
-
const dispatch = useDispatch();
|
|
14
10
|
const validateState = useValidateState();
|
|
15
11
|
const store = useStore();
|
|
16
12
|
const database = useDatabase();
|
|
13
|
+
const { applyCloningStrategy } = useCloningHistoryLoader();
|
|
17
14
|
|
|
18
15
|
const loadDatabaseFile = async (file, databaseId, ancestors = false) => {
|
|
19
16
|
if (file.name.endsWith('.json')) {
|
|
20
17
|
let cloningStrategy;
|
|
21
18
|
try {
|
|
22
|
-
({ cloningStrategy } = await
|
|
19
|
+
({ cloningStrategy } = await parseHistoryFile(file));
|
|
23
20
|
// If the cloning strategy should end on a single sequence, set the databaseId for the right source
|
|
24
21
|
const terminalSequences = getIdsOfSequencesWithoutChildSource(cloningStrategy.sources, cloningStrategy.sequences);
|
|
25
22
|
if (terminalSequences.length === 1) {
|
|
@@ -29,7 +26,7 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
29
26
|
// When importing sources that had inputs that we don't want to load, we turn them into database sources
|
|
30
27
|
const allSequenceIds = [...cloningStrategy.sequences.map((e) => e.id), ...cloningStrategy.primers.map((e) => e.id)];
|
|
31
28
|
cloningStrategy.sources = cloningStrategy.sources.map((s) => {
|
|
32
|
-
if (s.input.some(({sequence}) => !allSequenceIds.includes(sequence))) {
|
|
29
|
+
if (s.input.some(({ sequence }) => !allSequenceIds.includes(sequence))) {
|
|
33
30
|
return { id: s.id, type: 'DatabaseSource', input: [], database_id: s.database_id };
|
|
34
31
|
}
|
|
35
32
|
return s;
|
|
@@ -66,7 +63,6 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
66
63
|
setHistoryFileError(e.message);
|
|
67
64
|
return;
|
|
68
65
|
}
|
|
69
|
-
// This one won't have the source.id deleted
|
|
70
66
|
const prevState = store.getState().cloning;
|
|
71
67
|
const { backendVersion, schemaVersion, frontendVersion } = prevState.appInfo;
|
|
72
68
|
cloningStrategy.backend_version = backendVersion;
|
|
@@ -74,28 +70,15 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
74
70
|
cloningStrategy.frontend_version = frontendVersion;
|
|
75
71
|
cloningStrategy = await validateState(cloningStrategy);
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
({ mergedState } = graftState(cloningStrategy, cloningState, source.id));
|
|
87
|
-
} else {
|
|
88
|
-
({ mergedState } = mergeStates(cloningStrategy, cloningState));
|
|
89
|
-
}
|
|
90
|
-
dispatch(setCloningState(mergedState));
|
|
91
|
-
} catch (e) {
|
|
92
|
-
setHistoryFileError(e.message);
|
|
93
|
-
console.error(e);
|
|
94
|
-
if (!ancestors) {
|
|
95
|
-
dispatch(setCloningState(prevState));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
73
|
+
try {
|
|
74
|
+
await applyCloningStrategy(cloningStrategy, {
|
|
75
|
+
mode: ancestors ? 'graft' : 'merge',
|
|
76
|
+
source,
|
|
77
|
+
onError: setHistoryFileError,
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
// Error already reported via onError; state restored in applyCloningStrategy when applicable
|
|
81
|
+
}
|
|
99
82
|
} else {
|
|
100
83
|
const requestData = new FormData();
|
|
101
84
|
requestData.append('file', file);
|
|
@@ -6,10 +6,11 @@ import useLoadDatabaseFile from './useLoadDatabaseFile';
|
|
|
6
6
|
import useCloningAlerts from './useCloningAlerts';
|
|
7
7
|
import useHttpClient from './useHttpClient';
|
|
8
8
|
import useValidateState from './useValidateState';
|
|
9
|
+
import useCloningHistoryLoader from './useCloningHistoryLoader';
|
|
9
10
|
import { formatSequenceLocationString, getUrlParameters } from '@opencloning/utils/other';
|
|
10
|
-
import { formatTemplate
|
|
11
|
+
import { formatTemplate } from '@opencloning/utils/readNwrite';
|
|
11
12
|
|
|
12
|
-
const {
|
|
13
|
+
const { updateSource } = cloningActions;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Hook to load sequences from URL parameters
|
|
@@ -23,6 +24,7 @@ export default function useUrlParamsLoader() {
|
|
|
23
24
|
const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 }, sendPostRequest: null, setHistoryFileError });
|
|
24
25
|
const validateState = useValidateState();
|
|
25
26
|
const httpClient = useHttpClient();
|
|
27
|
+
const { applyCloningStrategy, loadHistoryFromFile } = useCloningHistoryLoader();
|
|
26
28
|
|
|
27
29
|
useEffect(() => {
|
|
28
30
|
async function loadSequenceFromUrlParams() {
|
|
@@ -45,22 +47,24 @@ export default function useUrlParamsLoader() {
|
|
|
45
47
|
} else if (urlParams.source === 'example' && urlParams.example) {
|
|
46
48
|
try {
|
|
47
49
|
const url = `${import.meta.env.BASE_URL}examples/${urlParams.example}`;
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const file = new File([
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
await loadFilesToSessionStorage(verificationFiles, 0);
|
|
50
|
+
const fileName = urlParams.example;
|
|
51
|
+
if (fileName.endsWith('.zip')) {
|
|
52
|
+
const resp = await httpClient.get(url, { responseType: 'blob' });
|
|
53
|
+
if (resp.headers['content-type'] !== 'application/zip') {
|
|
54
|
+
throw new Error('Not found');
|
|
55
|
+
}
|
|
56
|
+
const file = new File([resp.data], fileName);
|
|
57
|
+
const prepared = await loadHistoryFromFile(file, { onError: setHistoryFileError });
|
|
58
|
+
await prepared?.replace();
|
|
58
59
|
} else {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const resp = await httpClient.get(url);
|
|
61
|
+
if (resp.headers['content-type'] !== 'application/json') {
|
|
62
|
+
throw new Error('Not found');
|
|
63
|
+
}
|
|
64
|
+
const file = new File([JSON.stringify(resp.data)], fileName);
|
|
65
|
+
const prepared = await loadHistoryFromFile(file, { onError: setHistoryFileError });
|
|
66
|
+
await prepared?.replace();
|
|
62
67
|
}
|
|
63
|
-
dispatch(setCloningState(data));
|
|
64
68
|
} catch (error) {
|
|
65
69
|
addAlert({
|
|
66
70
|
message: 'Error loading example',
|
|
@@ -76,7 +80,7 @@ export default function useUrlParamsLoader() {
|
|
|
76
80
|
const validatedData = await validateState(data);
|
|
77
81
|
const newState = formatTemplate(validatedData, url);
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
await applyCloningStrategy(newState, { mode: 'replace' });
|
|
80
84
|
} catch (error) {
|
|
81
85
|
addAlert({
|
|
82
86
|
message: 'Error loading template',
|
|
@@ -146,4 +150,3 @@ export default function useUrlParamsLoader() {
|
|
|
146
150
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
151
|
}, []);
|
|
148
152
|
}
|
|
149
|
-
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Version placeholder - replaced at publish time via prepack script
|
|
2
|
-
export const version = "1.9.
|
|
2
|
+
export const version = "1.9.2";
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import useBackendRoute from './useBackendRoute';
|
|
3
|
-
import useHttpClient from './useHttpClient';
|
|
4
|
-
import useCloningAlerts from './useCloningAlerts';
|
|
5
|
-
import { batch, useDispatch, useStore } from 'react-redux';
|
|
6
|
-
import { mergeStates } from '@opencloning/utils/network';
|
|
7
|
-
import { cloningActions } from '@opencloning/store/cloning';
|
|
8
|
-
|
|
9
|
-
const { setState: setCloningState, deleteSourceAndItsChildren } = cloningActions;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Custom React hook that provides a function to load and process a SnapGene .dna file's cloning history
|
|
14
|
-
* via the backend endpoint. If successful, updates the Redux cloning state (merging or replacing as appropriate)
|
|
15
|
-
* and handles warnings. On failure, triggers a user alert.
|
|
16
|
-
*
|
|
17
|
-
* @returns {Object} An object with a single method `loadSnapgeneHistory(file)`:
|
|
18
|
-
* - {Promise<boolean>} loadSnapgeneHistory(file): Attempt to load and merge/replace SnapGene history.
|
|
19
|
-
* Returns true on successfully parsing the SnapGene file, otherwise returns false and triggers a warning alert.
|
|
20
|
-
*/
|
|
21
|
-
export default function useSnapgeneHistoryEndpoint() {
|
|
22
|
-
const backendRoute = useBackendRoute();
|
|
23
|
-
const httpClient = useHttpClient();
|
|
24
|
-
const { addAlert } = useCloningAlerts();
|
|
25
|
-
const dispatch = useDispatch();
|
|
26
|
-
const store = useStore();
|
|
27
|
-
const url = backendRoute('read_snapgene_history');
|
|
28
|
-
|
|
29
|
-
const loadSnapgeneHistory = React.useCallback(async (file, sourceIdToDelete = null) => {
|
|
30
|
-
const cloningState = store.getState().cloning;
|
|
31
|
-
const formData = new FormData();
|
|
32
|
-
formData.append('file', file);
|
|
33
|
-
const config = {
|
|
34
|
-
headers: {
|
|
35
|
-
'content-type': 'multipart/form-data',
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
// If it fails to parse the history return false, true otherwise
|
|
39
|
-
try {
|
|
40
|
-
const resp = await httpClient.post(url, formData, config);
|
|
41
|
-
const { data: cloningStrategy } = resp;
|
|
42
|
-
let mergedState;
|
|
43
|
-
if (!(cloningState.sources.length === 1 && cloningState.sources[0].type === null)) {
|
|
44
|
-
({ mergedState } = mergeStates(cloningStrategy, cloningState));
|
|
45
|
-
} else {
|
|
46
|
-
// If there is only one source and it is empty, replace the whole state
|
|
47
|
-
mergedState = cloningStrategy;
|
|
48
|
-
}
|
|
49
|
-
if (resp.headers['x-warning']) {
|
|
50
|
-
addAlert({
|
|
51
|
-
message: `Not all SnapGene history was parsed: ${resp.headers['x-warning']}`,
|
|
52
|
-
severity: 'warning',
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
batch(() => {
|
|
56
|
-
dispatch(setCloningState(mergedState));
|
|
57
|
-
if (sourceIdToDelete !== null) {
|
|
58
|
-
console.log('deleting source', sourceIdToDelete);
|
|
59
|
-
dispatch(deleteSourceAndItsChildren(sourceIdToDelete));
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
addAlert({
|
|
65
|
-
message: 'Failed to parse the history from the SnapGene file, will only read the sequence.',
|
|
66
|
-
severity: 'warning',
|
|
67
|
-
});
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}, [httpClient, addAlert, dispatch, store, url]);
|
|
71
|
-
return { loadSnapgeneHistory };
|
|
72
|
-
}
|