@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/package.json +5 -4
  3. package/src/components/CloningSpeedDial.jsx +3 -5
  4. package/src/components/EditSequenceNameDialog.jsx +3 -5
  5. package/src/components/ExternalServicesStatusCheck.jsx +3 -5
  6. package/src/components/LoadCloningHistoryWrapper.jsx +21 -46
  7. package/src/components/SequenceViewer.jsx +12 -0
  8. package/src/components/assembler/Assembler.jsx +4 -6
  9. package/src/components/assembler/ExistingSyntaxDialog.jsx +2 -2
  10. package/src/components/assembler/useAssembler.js +6 -8
  11. package/src/components/domesticate/DomesticateDialog.jsx +3 -5
  12. package/src/components/form/EnzymeMultiSelect.jsx +3 -5
  13. package/src/components/form/TextFieldQueryValidated.jsx +22 -4
  14. package/src/components/navigation/MainAppBar.jsx +26 -20
  15. package/src/components/navigation/SelectTemplateDialog.jsx +4 -2
  16. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +4 -6
  17. package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +4 -6
  18. package/src/components/sources/CollectionSource.jsx +2 -2
  19. package/src/components/sources/Source.jsx +32 -32
  20. package/src/components/sources/SourceAnnotation.jsx +2 -2
  21. package/src/components/sources/SourceAssembly.jsx +7 -7
  22. package/src/components/sources/SourceDatabase.jsx +2 -2
  23. package/src/components/sources/SourceFile.jsx +21 -51
  24. package/src/components/sources/SourceGenomeRegion.cy.jsx +46 -0
  25. package/src/components/sources/SourceGenomeRegion.jsx +29 -15
  26. package/src/components/sources/SourceHomologousRecombination.jsx +3 -3
  27. package/src/components/sources/SourceKnownGenomeRegion.jsx +3 -3
  28. package/src/components/sources/SourceManuallyTyped.jsx +2 -2
  29. package/src/components/sources/SourcePCRorHybridization.jsx +3 -3
  30. package/src/components/sources/SourcePolymeraseExtension.jsx +2 -2
  31. package/src/components/sources/SourceRepositoryId.jsx +4 -4
  32. package/src/components/sources/SourceRestriction.jsx +2 -2
  33. package/src/components/sources/SourceReverseComplement.jsx +2 -2
  34. package/src/components/sources/SourceServerStaticFile.jsx +3 -3
  35. package/src/components/verification/VerificationFileDialog.cy.jsx +17 -21
  36. package/src/components/verification/VerificationFileDialog.jsx +12 -11
  37. package/src/hooks/index.js +1 -0
  38. package/src/hooks/useBackendHttpClient.js +17 -0
  39. package/src/hooks/useCloningHistoryLoader.js +161 -0
  40. package/src/hooks/useGatewaySites.js +3 -5
  41. package/src/hooks/useInitializeApp.js +3 -4
  42. package/src/hooks/useLoadDatabaseFile.js +17 -34
  43. package/src/hooks/useSequencingAlignment.js +4 -6
  44. package/src/hooks/useServerStaticFiles.js +2 -2
  45. package/src/hooks/{useBackendAPI.js → useSourceRequest.js} +6 -8
  46. package/src/hooks/useStoreEditor.js +2 -1
  47. package/src/hooks/useUrlParamsLoader.js +29 -23
  48. package/src/hooks/useValidateState.js +4 -6
  49. package/src/version.js +1 -1
  50. package/src/hooks/useBackendRoute.js +0 -23
  51. package/src/hooks/useHttpClient.js +0 -20
  52. 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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'crispr', requestData, config, source });
36
+ sendSourceRequest({ endpoint: 'crispr', requestData, config, source });
37
37
  } else {
38
- sendPostRequest({ endpoint: 'homologous_recombination', requestData, config, source });
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
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
- sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'manually_typed', requestData, source });
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'oligonucleotide_hybridization', requestData, config, source });
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
- sendPostRequest({ endpoint: 'pcr', requestData, config, source });
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'polymerase_extension', requestData, source });
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 getHttpClient from '@opencloning/utils/getHttpClient';
6
+ import { getExternalHttpClient } from '@opencloning/utils/httpClients';
7
7
  import repositoryMetadata from './repositoryMetadata';
8
8
 
9
- const httpClient = getHttpClient();
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: `repository_id/${repositoryMeta.slug}`, requestData, source });
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'restriction', requestData, source, config: { params: { restriction_enzymes: enzymes } } });
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'reverse_complement', requestData, source });
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, sendPostRequest }) {
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
- sendPostRequest({ endpoint: 'read_from_file', requestData, config, source });
15
- }, [sendPostRequest, source]);
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
- sessionStorage.clear();
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
- sessionStorage.setItem(getVerificationFileName(file), base64str);
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
- // Check that the file is not in sessionStorage
75
- cy.window().its('sessionStorage')
76
- .invoke('getItem', 'verification-1-file1.txt')
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 sessionStorage
109
- cy.window().its('sessionStorage')
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 sessionStorage
137
- cy.window().its('sessionStorage')
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
- cy.window().its('sessionStorage')
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 useHttpClient from '../../hooks/useHttpClient';
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 = useHttpClient();
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 = sessionStorage.getItem(getVerificationFileName(f));
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(backendRoute('align_sanger'), { sequence, traces });
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 }) => { sessionStorage.setItem(`verification-${id}-${file_name}`, base64str); });
112
+ alignments.forEach(({ base64str, file_name }) => { setVerificationFileContent(`verification-${id}-${file_name}`, base64str); });
111
113
  });
112
114
  setLoadingMessage('');
113
- }, [id, dispatch, httpClient, backendRoute, store, sequence]);
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 = sessionStorage.getItem(`verification-${id}-${fileName}`);
138
+ const base64Content = getVerificationFileContent(`verification-${id}-${fileName}`);
138
139
  if (!base64Content) {
139
- setError(`File ${fileName} not found in session storage`);
140
+ setError(`File ${fileName} not found in the verification file store`);
140
141
  return;
141
142
  }
142
143
  const binaryString = atob(base64Content);
@@ -1,2 +1,3 @@
1
1
  export { useConfig } from './useConfig.js';
2
2
  export { default as useServerStaticFiles, localFilesHttpClient } from './useServerStaticFiles.js';
3
+ export { default as useBackendHttpClient } from './useBackendHttpClient.js';
@@ -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 useBackendRoute from './useBackendRoute';
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 backendRoute = useBackendRoute();
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 = backendRoute('annotation/get_gateway_sites');
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 session storage
6
+ * - Clears the in-memory verification file store
6
7
  */
7
8
  export default function useInitializeApp() {
8
9
 
9
10
  useEffect(() => {
10
- // Clear session storage
11
- // eslint-disable-next-line no-undef
12
- sessionStorage.clear();
11
+ clearVerificationFileContents();
13
12
  }, []);
14
13
  }
15
14