@opencloning/ui 1.9.1 → 2.0.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.
- package/CHANGELOG.md +73 -0
- package/package.json +5 -4
- package/src/components/CloningSpeedDial.jsx +3 -5
- package/src/components/EditSequenceNameDialog.jsx +3 -5
- package/src/components/ExternalServicesStatusCheck.jsx +3 -5
- package/src/components/LoadCloningHistoryWrapper.jsx +21 -46
- package/src/components/SequenceViewer.jsx +12 -0
- package/src/components/assembler/Assembler.jsx +4 -6
- package/src/components/assembler/ExistingSyntaxDialog.jsx +2 -2
- package/src/components/assembler/useAssembler.js +6 -8
- package/src/components/domesticate/DomesticateDialog.jsx +3 -5
- package/src/components/form/EnzymeMultiSelect.jsx +3 -5
- package/src/components/form/TextFieldQueryValidated.jsx +22 -4
- package/src/components/navigation/MainAppBar.jsx +26 -20
- package/src/components/navigation/SelectTemplateDialog.jsx +4 -2
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +4 -6
- package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +4 -6
- package/src/components/sources/CollectionSource.jsx +2 -2
- package/src/components/sources/Source.jsx +32 -32
- package/src/components/sources/SourceAnnotation.jsx +2 -2
- package/src/components/sources/SourceAssembly.jsx +7 -7
- package/src/components/sources/SourceDatabase.jsx +2 -2
- package/src/components/sources/SourceFile.jsx +21 -51
- package/src/components/sources/SourceGenomeRegion.cy.jsx +46 -0
- package/src/components/sources/SourceGenomeRegion.jsx +29 -15
- package/src/components/sources/SourceHomologousRecombination.jsx +3 -3
- package/src/components/sources/SourceKnownGenomeRegion.jsx +3 -3
- package/src/components/sources/SourceManuallyTyped.jsx +2 -2
- package/src/components/sources/SourcePCRorHybridization.jsx +3 -3
- package/src/components/sources/SourcePolymeraseExtension.jsx +2 -2
- package/src/components/sources/SourceRepositoryId.jsx +4 -4
- package/src/components/sources/SourceRestriction.jsx +2 -2
- package/src/components/sources/SourceReverseComplement.jsx +2 -2
- package/src/components/sources/SourceServerStaticFile.jsx +3 -3
- package/src/components/verification/VerificationFileDialog.cy.jsx +17 -21
- package/src/components/verification/VerificationFileDialog.jsx +12 -11
- package/src/hooks/index.js +1 -0
- package/src/hooks/useBackendHttpClient.js +17 -0
- package/src/hooks/useCloningHistoryLoader.js +161 -0
- package/src/hooks/useGatewaySites.js +3 -5
- package/src/hooks/useInitializeApp.js +3 -4
- package/src/hooks/useLoadDatabaseFile.js +17 -34
- package/src/hooks/useSequencingAlignment.js +4 -6
- package/src/hooks/useServerStaticFiles.js +2 -2
- package/src/hooks/{useBackendAPI.js → useSourceRequest.js} +6 -8
- package/src/hooks/useStoreEditor.js +2 -1
- package/src/hooks/useUrlParamsLoader.js +29 -23
- package/src/hooks/useValidateState.js +4 -6
- package/src/version.js +1 -1
- package/src/hooks/useBackendRoute.js +0 -23
- package/src/hooks/useHttpClient.js +0 -20
- package/src/hooks/useSnapgeneHistoryEndpoint.js +0 -72
|
@@ -9,7 +9,7 @@ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
|
9
9
|
import SelectPrimerForm from '../primers/SelectPrimerForm';
|
|
10
10
|
|
|
11
11
|
// A component representing the ligation of several fragments
|
|
12
|
-
function SourceHomologousRecombination({ source, requestStatus,
|
|
12
|
+
function SourceHomologousRecombination({ source, requestStatus, sendSourceRequest }) {
|
|
13
13
|
const isCrispr = source.type === 'CRISPRSource';
|
|
14
14
|
const { id: sourceId, input } = source;
|
|
15
15
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), isEqual);
|
|
@@ -33,9 +33,9 @@ function SourceHomologousRecombination({ source, requestStatus, sendPostRequest
|
|
|
33
33
|
const config = { params: { minimal_homology: minimalHomologyRef.current.value } };
|
|
34
34
|
if (isCrispr) {
|
|
35
35
|
requestData.guides = selectedPrimers.map((primerId) => primers.find((p) => p.id === primerId));
|
|
36
|
-
|
|
36
|
+
sendSourceRequest({ endpoint: 'crispr', requestData, config, source });
|
|
37
37
|
} else {
|
|
38
|
-
|
|
38
|
+
sendSourceRequest({ endpoint: 'homologous_recombination', requestData, config, source });
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -4,7 +4,7 @@ import { geneSuggest } from '@opencloning/utils/ncbiRequests';
|
|
|
4
4
|
import { formatBackendPayloadWithGene } from './SourceGenomeRegion';
|
|
5
5
|
import { Box, CircularProgress } from '@mui/material';
|
|
6
6
|
|
|
7
|
-
function SourceKnownGenomeRegion({ source, requestStatus,
|
|
7
|
+
function SourceKnownGenomeRegion({ source, requestStatus, sendSourceRequest }) {
|
|
8
8
|
// A source where we pass a complete GenomeCoordinatesSource, so we just have to make the request
|
|
9
9
|
const [error, setError] = React.useState('');
|
|
10
10
|
const [connectAttempt, setConnectAttempt] = React.useState(0);
|
|
@@ -25,14 +25,14 @@ function SourceKnownGenomeRegion({ source, requestStatus, sendPostRequest }) {
|
|
|
25
25
|
}
|
|
26
26
|
const requestData = formatBackendPayloadWithGene(source.assembly_accession, resp[0], source.padding, source.padding);
|
|
27
27
|
requestData.id = source.id;
|
|
28
|
-
|
|
28
|
+
sendSourceRequest({ endpoint: 'genome_coordinates', requestData, source });
|
|
29
29
|
}
|
|
30
30
|
else {
|
|
31
31
|
const requestData = {
|
|
32
32
|
...source,
|
|
33
33
|
type: 'GenomeCoordinatesSource',
|
|
34
34
|
};
|
|
35
|
-
|
|
35
|
+
sendSourceRequest({ endpoint: 'genome_coordinates', requestData, source });
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
makeRequest();
|
|
@@ -4,7 +4,7 @@ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
|
4
4
|
import ValidatedTextField from '../form/ValidatedTextField';
|
|
5
5
|
import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
|
|
6
6
|
|
|
7
|
-
function SourceManuallyTyped({ source, requestStatus,
|
|
7
|
+
function SourceManuallyTyped({ source, requestStatus, sendSourceRequest }) {
|
|
8
8
|
const { id: sourceId } = source;
|
|
9
9
|
|
|
10
10
|
const [userInput, setUserInput] = React.useState('');
|
|
@@ -48,7 +48,7 @@ function SourceManuallyTyped({ source, requestStatus, sendPostRequest }) {
|
|
|
48
48
|
overhang_watson_3prime: overhangWatson3prime,
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
|
-
|
|
51
|
+
sendSourceRequest({ endpoint: 'manually_typed', requestData, source });
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
|
|
@@ -10,7 +10,7 @@ import { cloningActions } from '@opencloning/store/cloning';
|
|
|
10
10
|
import useStoreEditor from '../../hooks/useStoreEditor';
|
|
11
11
|
import LabelWithTooltip from '../form/LabelWithTooltip';
|
|
12
12
|
|
|
13
|
-
function SourcePCRorHybridization({ source, requestStatus,
|
|
13
|
+
function SourcePCRorHybridization({ source, requestStatus, sendSourceRequest }) {
|
|
14
14
|
// Represents a PCR if inputs != [], else is a oligo hybridization
|
|
15
15
|
|
|
16
16
|
const dispatch = useDispatch();
|
|
@@ -58,14 +58,14 @@ function SourcePCRorHybridization({ source, requestStatus, sendPostRequest }) {
|
|
|
58
58
|
if (!isPcr) {
|
|
59
59
|
requestData.source.input = [forwardPrimerId, reversePrimerId].map((id) => ({ sequence: id }));
|
|
60
60
|
const config = { params: { minimal_annealing: minimalAnnealingRef.current.value } };
|
|
61
|
-
|
|
61
|
+
sendSourceRequest({ endpoint: 'oligonucleotide_hybridization', requestData, config, source });
|
|
62
62
|
} else {
|
|
63
63
|
const config = { params: {
|
|
64
64
|
minimal_annealing: minimalAnnealingRef.current.value,
|
|
65
65
|
allowed_mismatches: allowedMismatchesRef.current.value,
|
|
66
66
|
} };
|
|
67
67
|
requestData.source.add_primer_features = addPrimerFeatures;
|
|
68
|
-
|
|
68
|
+
sendSourceRequest({ endpoint: 'pcr', requestData, config, source });
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
|
|
@@ -4,7 +4,7 @@ import { Alert } from '@mui/material';
|
|
|
4
4
|
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
5
5
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
6
6
|
|
|
7
|
-
function SourcePolymeraseExtension({ source, requestStatus,
|
|
7
|
+
function SourcePolymeraseExtension({ source, requestStatus, sendSourceRequest }) {
|
|
8
8
|
const { id: sourceId } = source;
|
|
9
9
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
10
10
|
const { overhang_crick_3prime, overhang_watson_3prime } = inputSequences[0];
|
|
@@ -16,7 +16,7 @@ function SourcePolymeraseExtension({ source, requestStatus, sendPostRequest }) {
|
|
|
16
16
|
sequences: inputSequences,
|
|
17
17
|
source: { id: sourceId, input: inputSequences.map((e) => ({ sequence: e.id })) },
|
|
18
18
|
};
|
|
19
|
-
|
|
19
|
+
sendSourceRequest({ endpoint: 'polymerase_extension', requestData, source });
|
|
20
20
|
};
|
|
21
21
|
// No need for MultipleOutputsSelector, since there is only one output
|
|
22
22
|
return (
|
|
@@ -3,10 +3,10 @@ import { IconButton, InputAdornment, InputLabel, MenuItem, TextField, FormContro
|
|
|
3
3
|
import { Clear as ClearIcon } from '@mui/icons-material';
|
|
4
4
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
5
5
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
6
|
-
import
|
|
6
|
+
import { getExternalHttpClient } from '@opencloning/utils/httpClients';
|
|
7
7
|
import repositoryMetadata from './repositoryMetadata';
|
|
8
8
|
|
|
9
|
-
const httpClient =
|
|
9
|
+
const httpClient = getExternalHttpClient();
|
|
10
10
|
|
|
11
11
|
function validateRepositoryId(repositoryId, repository) {
|
|
12
12
|
switch (repository) {
|
|
@@ -243,7 +243,7 @@ function IndexJsonSelector({
|
|
|
243
243
|
|
|
244
244
|
// A component providing an interface for the user to type a repository ID
|
|
245
245
|
// and get a sequence
|
|
246
|
-
function SourceRepositoryId({ source, requestStatus,
|
|
246
|
+
function SourceRepositoryId({ source, requestStatus, sendSourceRequest }) {
|
|
247
247
|
const { id: sourceId } = source;
|
|
248
248
|
const [inputValue, setInputValue] = React.useState('');
|
|
249
249
|
const [repositoryType, setRepositoryType] = React.useState(source.type || '');
|
|
@@ -290,7 +290,7 @@ function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {
|
|
|
290
290
|
extra.sequence_file_url = encodeURI(inputValue.url);
|
|
291
291
|
}
|
|
292
292
|
const requestData = { id: sourceId, ...extra, type: repositoryType };
|
|
293
|
-
|
|
293
|
+
sendSourceRequest({ endpoint: `repository_id/${repositoryMeta.slug}`, requestData, source });
|
|
294
294
|
};
|
|
295
295
|
const helperText = error || (repositoryMeta?.example && `Example: ${repositoryMeta.example}`);
|
|
296
296
|
return (
|
|
@@ -7,7 +7,7 @@ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
|
7
7
|
// A component providing an interface for the user to perform a restriction reaction
|
|
8
8
|
// with one or more restriction enzymes, move between output fragments, and eventually
|
|
9
9
|
// select one as an output.
|
|
10
|
-
function SourceRestriction({ source, requestStatus,
|
|
10
|
+
function SourceRestriction({ source, requestStatus, sendSourceRequest }) {
|
|
11
11
|
const { id: sourceId } = source;
|
|
12
12
|
const [enzymes, setEnzymes] = React.useState([]);
|
|
13
13
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
@@ -19,7 +19,7 @@ function SourceRestriction({ source, requestStatus, sendPostRequest }) {
|
|
|
19
19
|
source: { id: sourceId, input: source.input },
|
|
20
20
|
sequences: inputSequences,
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
sendSourceRequest({ endpoint: 'restriction', requestData, source, config: { params: { restriction_enzymes: enzymes } } });
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
return (
|
|
@@ -3,7 +3,7 @@ import { shallowEqual, useSelector } from 'react-redux';
|
|
|
3
3
|
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
4
4
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
5
5
|
|
|
6
|
-
function SourceReverseComplement({ source, requestStatus,
|
|
6
|
+
function SourceReverseComplement({ source, requestStatus, sendSourceRequest }) {
|
|
7
7
|
const { id: sourceId, input } = source;
|
|
8
8
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
9
9
|
const onSubmit = (event) => {
|
|
@@ -13,7 +13,7 @@ function SourceReverseComplement({ source, requestStatus, sendPostRequest }) {
|
|
|
13
13
|
sequences: inputSequences,
|
|
14
14
|
source: { id: sourceId, input },
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
sendSourceRequest({ endpoint: 'reverse_complement', requestData, source });
|
|
17
17
|
};
|
|
18
18
|
// No need for MultipleOutputsSelector, since there is only one output
|
|
19
19
|
return (
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
3
3
|
import { Alert, LinearProgress } from '@mui/material';
|
|
4
4
|
|
|
5
|
-
function SourceServerStaticFile({ source, requestStatus,
|
|
5
|
+
function SourceServerStaticFile({ source, requestStatus, sendSourceRequest }) {
|
|
6
6
|
const onFileSelected = React.useCallback((file) => {
|
|
7
7
|
const requestData = new FormData();
|
|
8
8
|
requestData.append('file', file);
|
|
@@ -11,8 +11,8 @@ function SourceServerStaticFile({ source, requestStatus, sendPostRequest }) {
|
|
|
11
11
|
'content-type': 'multipart/form-data',
|
|
12
12
|
},
|
|
13
13
|
};
|
|
14
|
-
|
|
15
|
-
}, [
|
|
14
|
+
sendSourceRequest({ endpoint: 'read_from_file', requestData, config, source });
|
|
15
|
+
}, [sendSourceRequest, source]);
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<>
|
|
@@ -4,6 +4,11 @@ import store from '@opencloning/store';
|
|
|
4
4
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
5
5
|
import { loadDataAndMount } from '../../../../../cypress/e2e/common_funcions_store';
|
|
6
6
|
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
7
|
+
import {
|
|
8
|
+
clearVerificationFileContents,
|
|
9
|
+
getVerificationFileContent,
|
|
10
|
+
setVerificationFileContent,
|
|
11
|
+
} from '@opencloning/utils/verificationFileStore';
|
|
7
12
|
import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
8
13
|
import { Provider } from 'react-redux';
|
|
9
14
|
|
|
@@ -23,7 +28,7 @@ const base64str = 'aGVsbG8=';
|
|
|
23
28
|
|
|
24
29
|
describe('<VerificationFileDialog />', () => {
|
|
25
30
|
beforeEach(() => {
|
|
26
|
-
|
|
31
|
+
clearVerificationFileContents();
|
|
27
32
|
});
|
|
28
33
|
it('renders and calls setDialogOpen with false when clicking close button', () => {
|
|
29
34
|
// see: https://on.cypress.io/mounting-react
|
|
@@ -51,7 +56,7 @@ describe('<VerificationFileDialog />', () => {
|
|
|
51
56
|
it('displays existing files and can download and delete them', () => {
|
|
52
57
|
store.dispatch(setFiles(dummyFiles));
|
|
53
58
|
dummyFiles.forEach((file) => {
|
|
54
|
-
|
|
59
|
+
setVerificationFileContent(getVerificationFileName(file), base64str);
|
|
55
60
|
});
|
|
56
61
|
cy.mount(
|
|
57
62
|
<Provider store={store}>
|
|
@@ -69,16 +74,11 @@ describe('<VerificationFileDialog />', () => {
|
|
|
69
74
|
|
|
70
75
|
cy.get('[data-testid="DeleteIcon"]').first().click();
|
|
71
76
|
cy.get('table').contains('file2.txt');
|
|
72
|
-
cy.get('table').contains('file1.txt').should('not.exist')
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.should('be.null');
|
|
78
|
-
|
|
79
|
-
cy.window().its('sessionStorage')
|
|
80
|
-
.invoke('getItem', 'verification-1-file2.txt')
|
|
81
|
-
.should('not.be.null');
|
|
77
|
+
cy.get('table').contains('file1.txt').should('not.exist').then(() => {
|
|
78
|
+
// Check that the file is not in the verification file store
|
|
79
|
+
expect(getVerificationFileContent('verification-1-file1.txt')).to.be.null;
|
|
80
|
+
expect(getVerificationFileContent('verification-1-file2.txt')).to.not.be.null;
|
|
81
|
+
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
it('can submit files and aligns them', () => {
|
|
@@ -105,9 +105,8 @@ describe('<VerificationFileDialog />', () => {
|
|
|
105
105
|
cy.expect(file.file_type).to.equal('Sequencing file');
|
|
106
106
|
cy.expect(file.sequence_id).to.equal(2);
|
|
107
107
|
cy.expect(file.alignment).to.have.length(2);
|
|
108
|
-
// The file content is stored in
|
|
109
|
-
cy.
|
|
110
|
-
.invoke('getItem', getVerificationFileName(file))
|
|
108
|
+
// The file content is stored in the verification file store
|
|
109
|
+
cy.wrap(getVerificationFileContent(getVerificationFileName(file)))
|
|
111
110
|
.should('not.be.null', { timeout: 10000 });
|
|
112
111
|
|
|
113
112
|
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
@@ -133,9 +132,8 @@ describe('<VerificationFileDialog />', () => {
|
|
|
133
132
|
cy.expect(file.file_type).to.equal('Sequencing file');
|
|
134
133
|
cy.expect(file.sequence_id).to.equal(2);
|
|
135
134
|
cy.expect(file.alignment).to.deep.equal(['A', 'C']);
|
|
136
|
-
// The file content is stored in
|
|
137
|
-
cy.
|
|
138
|
-
.invoke('getItem', getVerificationFileName(file))
|
|
135
|
+
// The file content is stored in the verification file store
|
|
136
|
+
cy.wrap(getVerificationFileContent(getVerificationFileName(file)))
|
|
139
137
|
.should('not.be.null', { timeout: 10000 });
|
|
140
138
|
});
|
|
141
139
|
});
|
|
@@ -187,9 +185,7 @@ describe('<VerificationFileDialog />', () => {
|
|
|
187
185
|
cy.contains('Request failed');
|
|
188
186
|
// It has not been added
|
|
189
187
|
cy.get('table').contains('BZO902-13409020-13409020.ab1').should('not.exist');
|
|
190
|
-
|
|
191
|
-
.invoke('getItem', 'verification-2-BZO902-13409020-13409020.ab1')
|
|
192
|
-
.should('be.null');
|
|
188
|
+
expect(getVerificationFileContent('verification-2-BZO902-13409020-13409020.ab1')).to.be.null;
|
|
193
189
|
|
|
194
190
|
// Connection error
|
|
195
191
|
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
@@ -21,12 +21,15 @@ import { isEqual } from 'lodash-es';
|
|
|
21
21
|
import { getTeselaJsonFromBase64, file2base64 } from '@opencloning/utils/readNwrite';
|
|
22
22
|
import SequencingFileRow from './SequencingFileRow';
|
|
23
23
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
24
|
-
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
25
24
|
import useStoreEditor from '../../hooks/useStoreEditor';
|
|
26
25
|
import LoadFromDatabaseButton from './LoadFromDatabaseButton';
|
|
27
26
|
import { sequencingFileExtensions } from '@opencloning/utils/sequencingFileExtensions';
|
|
28
|
-
import
|
|
27
|
+
import useBackendHttpClient from '../../hooks/useBackendHttpClient';
|
|
29
28
|
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
29
|
+
import {
|
|
30
|
+
getVerificationFileContent,
|
|
31
|
+
setVerificationFileContent,
|
|
32
|
+
} from '@opencloning/utils/verificationFileStore';
|
|
30
33
|
import ImportSequencingFilesInput from './ImportSequencingFilesInput';
|
|
31
34
|
|
|
32
35
|
const { addFile, removeFile: removeFileAction, removeFilesAssociatedToSequence, setMainSequenceId, setCurrentTab } = cloningActions;
|
|
@@ -43,9 +46,8 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
43
46
|
const dispatch = useDispatch();
|
|
44
47
|
const fileInputRef = useRef(null);
|
|
45
48
|
const { updateStoreEditor } = useStoreEditor();
|
|
46
|
-
const backendRoute = useBackendRoute();
|
|
47
49
|
const store = useStore();
|
|
48
|
-
const httpClient =
|
|
50
|
+
const httpClient = useBackendHttpClient();
|
|
49
51
|
|
|
50
52
|
const toggleMain = React.useCallback(() => {
|
|
51
53
|
dispatch(setMainSequenceId(id));
|
|
@@ -78,7 +80,7 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
78
80
|
const existingSequencingFilesInState = await Promise.all(store.getState().cloning.files
|
|
79
81
|
.filter((f) => f.sequence_id === id && f.file_type === 'Sequencing file')
|
|
80
82
|
.map(async (f) => {
|
|
81
|
-
const base64str =
|
|
83
|
+
const base64str = getVerificationFileContent(getVerificationFileName(f));
|
|
82
84
|
const trace = (await getTeselaJsonFromBase64(base64str, f.file_name)).sequence;
|
|
83
85
|
return { ...f, base64str, trace };
|
|
84
86
|
}));
|
|
@@ -94,7 +96,7 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
94
96
|
const allSequencingFiles = [...existingSequencingFilesInState, ...parsedSequencingFiles];
|
|
95
97
|
const alignments = [];
|
|
96
98
|
const traces = allSequencingFiles.map((f) => f.trace);
|
|
97
|
-
const resp = await httpClient.post(
|
|
99
|
+
const resp = await httpClient.post('align_sanger', { sequence, traces });
|
|
98
100
|
|
|
99
101
|
for (let i = 0; i < allSequencingFiles.length; i++) {
|
|
100
102
|
alignments.push({ ...allSequencingFiles[i], alignment: [resp.data[0], resp.data[i + 1]] });
|
|
@@ -107,10 +109,10 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
107
109
|
const { trace, base64str, ...rest } = alignment;
|
|
108
110
|
dispatch(addFile(rest));
|
|
109
111
|
});
|
|
110
|
-
alignments.forEach(({ base64str, file_name }) => {
|
|
112
|
+
alignments.forEach(({ base64str, file_name }) => { setVerificationFileContent(`verification-${id}-${file_name}`, base64str); });
|
|
111
113
|
});
|
|
112
114
|
setLoadingMessage('');
|
|
113
|
-
}, [id, dispatch, httpClient,
|
|
115
|
+
}, [id, dispatch, httpClient, store, sequence]);
|
|
114
116
|
|
|
115
117
|
const onFileChange = useCallback(async (files) => {
|
|
116
118
|
setLoadingMessage('Aligning...');
|
|
@@ -126,7 +128,6 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
126
128
|
|
|
127
129
|
const removeFile = useCallback((fileName) => {
|
|
128
130
|
dispatch(removeFileAction({ fileName, sequenceId: id }));
|
|
129
|
-
sessionStorage.removeItem(`verification-${id}-${fileName}`);
|
|
130
131
|
}, [id, dispatch]);
|
|
131
132
|
|
|
132
133
|
const handleClickUpload = useCallback(() => {
|
|
@@ -134,9 +135,9 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
134
135
|
}, [fileInputRef]);
|
|
135
136
|
|
|
136
137
|
const downloadFile = useCallback((fileName) => {
|
|
137
|
-
const base64Content =
|
|
138
|
+
const base64Content = getVerificationFileContent(`verification-${id}-${fileName}`);
|
|
138
139
|
if (!base64Content) {
|
|
139
|
-
setError(`File ${fileName} not found in
|
|
140
|
+
setError(`File ${fileName} not found in the verification file store`);
|
|
140
141
|
return;
|
|
141
142
|
}
|
|
142
143
|
const binaryString = atob(base64Content);
|
package/src/hooks/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getBackendHttpClient } from '@opencloning/utils/httpClients';
|
|
3
|
+
import { useConfig } from './useConfig';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Http client for the backend: urls are relative to config.backendUrl
|
|
7
|
+
* (e.g. httpClient.post('validate', body)), and requests can only reach the backend.
|
|
8
|
+
* For external urls use getExternalHttpClient() from @opencloning/utils/httpClients.
|
|
9
|
+
*/
|
|
10
|
+
export default function useBackendHttpClient() {
|
|
11
|
+
const { backendUrl, requiresBackendAuth = false } = useConfig();
|
|
12
|
+
|
|
13
|
+
return React.useMemo(
|
|
14
|
+
() => getBackendHttpClient(backendUrl, { authenticated: requiresBackendAuth }),
|
|
15
|
+
[backendUrl, requiresBackendAuth],
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
loadFilesToVerificationStore,
|
|
8
|
+
parseHistoryFile,
|
|
9
|
+
updateVerificationFileNames,
|
|
10
|
+
} from '@opencloning/utils/readNwrite';
|
|
11
|
+
import useCloningAlerts from './useCloningAlerts';
|
|
12
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
13
|
+
import useValidateState from './useValidateState';
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
setState: setCloningState,
|
|
17
|
+
deleteSourceAndItsChildren,
|
|
18
|
+
} = cloningActions;
|
|
19
|
+
|
|
20
|
+
function makeAlertHandler(addAlert, onError, severity = 'error') {
|
|
21
|
+
return onError || ((message) => addAlert({ message, severity }));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shouldDeleteSourceBeforeApply(mode, source) {
|
|
25
|
+
return source != null && mode === 'merge';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function useCloningHistoryLoader() {
|
|
29
|
+
const dispatch = useDispatch();
|
|
30
|
+
const store = useStore();
|
|
31
|
+
const validateState = useValidateState();
|
|
32
|
+
const { addAlert } = useCloningAlerts();
|
|
33
|
+
const httpClient = useBackendHttpClient();
|
|
34
|
+
const snapgeneUrl = 'read_snapgene_history';
|
|
35
|
+
|
|
36
|
+
const applyCloningStrategy = React.useCallback(async (cloningStrategy, options = {}) => {
|
|
37
|
+
const {
|
|
38
|
+
verificationFiles = [],
|
|
39
|
+
mode,
|
|
40
|
+
source = null,
|
|
41
|
+
onError,
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
const reportError = makeAlertHandler(addAlert, onError);
|
|
45
|
+
const deleteSourceBeforeApply = shouldDeleteSourceBeforeApply(mode, source);
|
|
46
|
+
const sourceId = source?.id ?? null;
|
|
47
|
+
|
|
48
|
+
const prevState = store.getState().cloning;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (deleteSourceBeforeApply) {
|
|
52
|
+
dispatch(deleteSourceAndItsChildren(sourceId));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cloningState = store.getState().cloning;
|
|
56
|
+
let newState;
|
|
57
|
+
let idShift = 0;
|
|
58
|
+
|
|
59
|
+
if (mode === 'replace') {
|
|
60
|
+
newState = cloningStrategy;
|
|
61
|
+
} else if (mode === 'merge') {
|
|
62
|
+
({ mergedState: newState, idShift } = mergeStates(cloningStrategy, cloningState));
|
|
63
|
+
} else if (mode === 'graft') {
|
|
64
|
+
({ mergedState: newState, idShift } = graftState(cloningStrategy, cloningState, sourceId));
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`Unknown apply mode: ${mode}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
batch(() => {
|
|
70
|
+
dispatch(setCloningState(newState));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await loadFilesToVerificationStore(verificationFiles, idShift);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(e);
|
|
76
|
+
dispatch(setCloningState(prevState));
|
|
77
|
+
reportError(e.message);
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
}, [addAlert, dispatch, store]);
|
|
81
|
+
|
|
82
|
+
const loadHistoryFromFile = React.useCallback(async (file, options = {}) => {
|
|
83
|
+
const { onError } = options;
|
|
84
|
+
const reportError = makeAlertHandler(addAlert, onError);
|
|
85
|
+
|
|
86
|
+
let cloningStrategy;
|
|
87
|
+
let verificationFiles;
|
|
88
|
+
try {
|
|
89
|
+
({ cloningStrategy, verificationFiles } = await parseHistoryFile(file));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(e);
|
|
92
|
+
reportError(e.message);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const validatedState = await validateState(cloningStrategy);
|
|
97
|
+
const updatedVerificationFiles = updateVerificationFileNames(
|
|
98
|
+
verificationFiles,
|
|
99
|
+
cloningStrategy.files,
|
|
100
|
+
validatedState.files,
|
|
101
|
+
);
|
|
102
|
+
const canGraft = getGraftSequenceId(validatedState) !== null;
|
|
103
|
+
|
|
104
|
+
const apply = (applyOptions) => applyCloningStrategy(validatedState, {
|
|
105
|
+
verificationFiles: updatedVerificationFiles,
|
|
106
|
+
validate: false,
|
|
107
|
+
...applyOptions,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
cloningStrategy: validatedState,
|
|
112
|
+
verificationFiles: updatedVerificationFiles,
|
|
113
|
+
canGraft,
|
|
114
|
+
replace: (applyOptions = {}) => apply({ mode: 'replace', ...applyOptions }),
|
|
115
|
+
merge: (applyOptions = {}) => apply({ mode: 'merge', ...applyOptions }),
|
|
116
|
+
graft: (applyOptions = {}) => apply({ mode: 'graft', ...applyOptions }),
|
|
117
|
+
};
|
|
118
|
+
}, [addAlert, applyCloningStrategy, validateState]);
|
|
119
|
+
|
|
120
|
+
const loadSnapgeneHistory = React.useCallback(async (file, options = {}) => {
|
|
121
|
+
const { source = null, onError, onWarning } = options;
|
|
122
|
+
const reportWarning = makeAlertHandler(addAlert, onWarning, 'warning');
|
|
123
|
+
|
|
124
|
+
const cloningState = store.getState().cloning;
|
|
125
|
+
const formData = new FormData();
|
|
126
|
+
formData.append('file', file);
|
|
127
|
+
const config = {
|
|
128
|
+
headers: {
|
|
129
|
+
'content-type': 'multipart/form-data',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const resp = await httpClient.post(snapgeneUrl, formData, config);
|
|
135
|
+
const { data: cloningStrategy } = resp;
|
|
136
|
+
|
|
137
|
+
const isEmptyState = getEmptyPlaceholderSource(cloningState.sources) !== null;
|
|
138
|
+
const mode = isEmptyState ? 'replace' : 'merge';
|
|
139
|
+
|
|
140
|
+
if (resp.headers['x-warning']) {
|
|
141
|
+
reportWarning(`Not all SnapGene history was parsed: ${resp.headers['x-warning']}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await applyCloningStrategy(cloningStrategy, {
|
|
145
|
+
mode,
|
|
146
|
+
source,
|
|
147
|
+
onError,
|
|
148
|
+
});
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
reportWarning('Failed to parse the history from the SnapGene file, will only read the sequence.');
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}, [addAlert, applyCloningStrategy, httpClient, snapgeneUrl, store]);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
applyCloningStrategy,
|
|
158
|
+
loadHistoryFromFile,
|
|
159
|
+
loadSnapgeneHistory,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -2,16 +2,14 @@ import React, { useCallback, useState } from 'react';
|
|
|
2
2
|
import { useStore } from 'react-redux';
|
|
3
3
|
import error2String from '@opencloning/utils/error2String';
|
|
4
4
|
import { formatGatewaySites } from '@opencloning/store/cloning_utils';
|
|
5
|
-
import
|
|
6
|
-
import useHttpClient from './useHttpClient';
|
|
5
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
7
6
|
|
|
8
7
|
export default function useGatewaySites({ target, greedy }) {
|
|
9
8
|
const store = useStore();
|
|
10
9
|
const [requestStatus, setRequestStatus] = useState({ status: null, message: '' });
|
|
11
10
|
const [connectAttempt, setConnectAttempt] = useState(0);
|
|
12
11
|
const [sites, setSites] = useState([]);
|
|
13
|
-
const
|
|
14
|
-
const httpClient = useHttpClient();
|
|
12
|
+
const httpClient = useBackendHttpClient();
|
|
15
13
|
|
|
16
14
|
const attemptAgain = useCallback(() => {
|
|
17
15
|
setConnectAttempt((p) => p + 1);
|
|
@@ -20,7 +18,7 @@ export default function useGatewaySites({ target, greedy }) {
|
|
|
20
18
|
React.useEffect(() => {
|
|
21
19
|
async function getSitesInTarget() {
|
|
22
20
|
setRequestStatus({ status: 'loading', message: 'loading' });
|
|
23
|
-
const url =
|
|
21
|
+
const url = 'annotation/get_gateway_sites';
|
|
24
22
|
const state = store.getState();
|
|
25
23
|
const sequence = state.cloning.sequences.find((seq) => seq.id === target);
|
|
26
24
|
try {
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import { clearVerificationFileContents } from '@opencloning/utils/verificationFileStore';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Hook to initialize application-level concerns
|
|
5
|
-
* - Clears
|
|
6
|
+
* - Clears the in-memory verification file store
|
|
6
7
|
*/
|
|
7
8
|
export default function useInitializeApp() {
|
|
8
9
|
|
|
9
10
|
useEffect(() => {
|
|
10
|
-
|
|
11
|
-
// eslint-disable-next-line no-undef
|
|
12
|
-
sessionStorage.clear();
|
|
11
|
+
clearVerificationFileContents();
|
|
13
12
|
}, []);
|
|
14
13
|
}
|
|
15
14
|
|