@opencloning/ui 1.9.2 → 2.1.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 (56) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/package.json +5 -6
  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 +5 -7
  7. package/src/components/assembler/Assembler.cy.jsx +184 -0
  8. package/src/components/assembler/Assembler.jsx +57 -11
  9. package/src/components/assembler/ExistingSyntaxDialog.jsx +2 -2
  10. package/src/components/assembler/UploadPlasmidsButton.cy.jsx +88 -6
  11. package/src/components/assembler/UploadPlasmidsButton.jsx +137 -73
  12. package/src/components/assembler/assembler_utils.js +25 -12
  13. package/src/components/assembler/assembler_utils.test.js +40 -1
  14. package/src/components/assembler/useAssembler.js +24 -8
  15. package/src/components/assembler/usePlasmidsLogic.js +20 -2
  16. package/src/components/domesticate/DomesticateDialog.jsx +4 -5
  17. package/src/components/dummy/DummyInterface.js +6 -0
  18. package/src/components/form/EnzymeMultiSelect.jsx +3 -5
  19. package/src/components/navigation/MainAppBar.jsx +4 -2
  20. package/src/components/navigation/SelectTemplateDialog.jsx +4 -2
  21. package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +4 -6
  22. package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +4 -6
  23. package/src/components/sources/CollectionSource.jsx +2 -2
  24. package/src/components/sources/Source.jsx +32 -32
  25. package/src/components/sources/SourceAnnotation.jsx +2 -2
  26. package/src/components/sources/SourceAssembly.jsx +7 -7
  27. package/src/components/sources/SourceDatabase.jsx +2 -2
  28. package/src/components/sources/SourceFile.jsx +2 -2
  29. package/src/components/sources/SourceGenomeRegion.cy.jsx +46 -0
  30. package/src/components/sources/SourceGenomeRegion.jsx +29 -15
  31. package/src/components/sources/SourceHomologousRecombination.jsx +3 -3
  32. package/src/components/sources/SourceKnownGenomeRegion.jsx +3 -3
  33. package/src/components/sources/SourceManuallyTyped.jsx +2 -2
  34. package/src/components/sources/SourcePCRorHybridization.jsx +3 -3
  35. package/src/components/sources/SourcePolymeraseExtension.jsx +2 -2
  36. package/src/components/sources/SourceRepositoryId.jsx +4 -4
  37. package/src/components/sources/SourceRestriction.jsx +2 -2
  38. package/src/components/sources/SourceReverseComplement.jsx +2 -2
  39. package/src/components/sources/SourceServerStaticFile.jsx +3 -3
  40. package/src/components/verification/VerificationFileDialog.cy.jsx +17 -21
  41. package/src/components/verification/VerificationFileDialog.jsx +12 -11
  42. package/src/hooks/index.js +1 -2
  43. package/src/hooks/useBackendHttpClient.js +17 -0
  44. package/src/hooks/useCloningHistoryLoader.js +5 -7
  45. package/src/hooks/useGatewaySites.js +3 -5
  46. package/src/hooks/useInitializeApp.js +3 -4
  47. package/src/hooks/useLoadDatabaseFile.js +2 -2
  48. package/src/hooks/useSequencingAlignment.js +4 -6
  49. package/src/hooks/useServerStaticFiles.js +2 -2
  50. package/src/hooks/{useBackendAPI.js → useSourceRequest.js} +6 -8
  51. package/src/hooks/useStoreEditor.js +2 -1
  52. package/src/hooks/useUrlParamsLoader.js +8 -5
  53. package/src/hooks/useValidateState.js +4 -6
  54. package/src/version.js +1 -1
  55. package/src/hooks/useBackendRoute.js +0 -23
  56. package/src/hooks/useHttpClient.js +0 -20
@@ -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,4 +1,3 @@
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';
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
+ }
@@ -4,13 +4,12 @@ import { cloningActions } from '@opencloning/store/cloning';
4
4
  import { getEmptyPlaceholderSource } from '@opencloning/store/cloning_utils';
5
5
  import { graftState, getGraftSequenceId, mergeStates } from '@opencloning/utils/network';
6
6
  import {
7
- loadFilesToSessionStorage,
7
+ loadFilesToVerificationStore,
8
8
  parseHistoryFile,
9
9
  updateVerificationFileNames,
10
10
  } from '@opencloning/utils/readNwrite';
11
- import useBackendRoute from './useBackendRoute';
12
11
  import useCloningAlerts from './useCloningAlerts';
13
- import useHttpClient from './useHttpClient';
12
+ import useBackendHttpClient from './useBackendHttpClient';
14
13
  import useValidateState from './useValidateState';
15
14
 
16
15
  const {
@@ -31,9 +30,8 @@ export default function useCloningHistoryLoader() {
31
30
  const store = useStore();
32
31
  const validateState = useValidateState();
33
32
  const { addAlert } = useCloningAlerts();
34
- const backendRoute = useBackendRoute();
35
- const httpClient = useHttpClient();
36
- const snapgeneUrl = backendRoute('read_snapgene_history');
33
+ const httpClient = useBackendHttpClient();
34
+ const snapgeneUrl = 'read_snapgene_history';
37
35
 
38
36
  const applyCloningStrategy = React.useCallback(async (cloningStrategy, options = {}) => {
39
37
  const {
@@ -72,7 +70,7 @@ export default function useCloningHistoryLoader() {
72
70
  dispatch(setCloningState(newState));
73
71
  });
74
72
 
75
- await loadFilesToSessionStorage(verificationFiles, idShift);
73
+ await loadFilesToVerificationStore(verificationFiles, idShift);
76
74
  } catch (e) {
77
75
  console.error(e);
78
76
  dispatch(setCloningState(prevState));
@@ -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
 
@@ -6,7 +6,7 @@ import useCloningHistoryLoader from './useCloningHistoryLoader';
6
6
  import useDatabase from './useDatabase';
7
7
  import { useStore } from 'react-redux';
8
8
 
9
- export default function useLoadDatabaseFile({ source, sendPostRequest, setHistoryFileError }) {
9
+ export default function useLoadDatabaseFile({ source, sendSourceRequest, setHistoryFileError }) {
10
10
  const validateState = useValidateState();
11
11
  const store = useStore();
12
12
  const database = useDatabase();
@@ -88,7 +88,7 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
88
88
  },
89
89
  };
90
90
  const modifySource = (s) => ({ ...s, database_id: databaseId });
91
- sendPostRequest({ endpoint: 'read_from_file', requestData, config, source, modifySource });
91
+ sendSourceRequest({ endpoint: 'read_from_file', requestData, config, source, modifySource });
92
92
  }
93
93
  };
94
94
  return { loadDatabaseFile };
@@ -1,17 +1,15 @@
1
1
  import { useCallback } from 'react';
2
2
  import { useMutation } from '@tanstack/react-query';
3
3
  import { buildAlignmentTrack, buildReferenceTrack, buildAlignmentConfig } from '@opencloning/utils/alignmentUtils';
4
- import useBackendRoute from './useBackendRoute';
5
- import useHttpClient from './useHttpClient';
4
+ import useBackendHttpClient from './useBackendHttpClient';
6
5
 
7
6
  export default function useSequencingAlignment({ onError } = {}) {
8
- const backendRoute = useBackendRoute();
9
- const httpClient = useHttpClient();
7
+ const httpClient = useBackendHttpClient();
10
8
 
11
9
  const fetchAlignments = useCallback(async ({ sequenceModel, sequenceData, parsedFiles }) => {
12
10
  // parsedFiles: [{ fileName, fileContent }] where fileContent is getTeselaJsonFromBase64 output
13
11
  const traces = parsedFiles.map((f) => f.fileContent.sequence);
14
- const resp = await httpClient.post(backendRoute('align_sanger'), { sequence: sequenceModel, traces });
12
+ const resp = await httpClient.post('align_sanger', { sequence: sequenceModel, traces });
15
13
 
16
14
  const referenceTrack = buildReferenceTrack(sequenceData, resp.data[0]);
17
15
  const otherTracks = parsedFiles.map((f, i) => {
@@ -20,7 +18,7 @@ export default function useSequencingAlignment({ onError } = {}) {
20
18
  });
21
19
 
22
20
  return buildAlignmentConfig(sequenceModel.id, sequenceData, [referenceTrack, ...otherTracks]);
23
- }, [httpClient, backendRoute]);
21
+ }, [httpClient]);
24
22
 
25
23
  return useMutation({ mutationFn: fetchAlignments, onError });
26
24
  }
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- import getHttpClient from '@opencloning/utils/getHttpClient';
2
+ import { getExternalHttpClient } from '@opencloning/utils/httpClients';
3
3
  import { useConfig } from '../providers';
4
4
  import useRequestForEffect from './useRequestForEffect';
5
5
 
6
6
 
7
- export const localFilesHttpClient = getHttpClient([]);
7
+ export const localFilesHttpClient = getExternalHttpClient();
8
8
 
9
9
  function sequencesToCategories(sequences) {
10
10
  const allCategories = sequences
@@ -1,22 +1,20 @@
1
1
  import { useState, useCallback } from 'react';
2
2
  import error2String from '@opencloning/utils/error2String';
3
- import useBackendRoute from './useBackendRoute';
4
3
  import useCloningAlerts from './useCloningAlerts';
5
- import useHttpClient from './useHttpClient';
4
+ import useBackendHttpClient from './useBackendHttpClient';
6
5
 
7
- export default function useBackendAPI() {
6
+ export default function useSourceRequest() {
8
7
  const [requestStatus, setRequestStatus] = useState({ status: null, message: '' });
9
8
  const [sources, setSources] = useState([]);
10
9
  const [sequences, setSequences] = useState([]);
11
- const backendRoute = useBackendRoute();
12
- const httpClient = useHttpClient();
10
+ const httpClient = useBackendHttpClient();
13
11
  const { addAlert } = useCloningAlerts();
14
12
 
15
- const sendPostRequest = useCallback(async ({ endpoint, requestData, config = {}, source: { output }, modifySource = (s) => s }) => {
13
+ const sendSourceRequest = useCallback(async ({ endpoint, requestData, config = {}, source: { output }, modifySource = (s) => s }) => {
16
14
  setRequestStatus({ status: 'loading', message: 'loading' });
17
15
 
18
16
  // Url built like this in case trailing slash
19
- const url = backendRoute(endpoint);
17
+ const url = endpoint;
20
18
  // paramsSerializer: { indexes: null } is to correctly serialize arrays in the URL
21
19
  const fullConfig = { ...config, paramsSerializer: { indexes: null } };
22
20
  try {
@@ -50,5 +48,5 @@ export default function useBackendAPI() {
50
48
  }
51
49
  }, []);
52
50
 
53
- return { requestStatus, sources, sequences, sendPostRequest };
51
+ return { requestStatus, sources, sequences, sendSourceRequest };
54
52
  }
@@ -2,6 +2,7 @@ import { useStore } from 'react-redux';
2
2
  import { updateEditor, addAlignment } from '@teselagen/ove';
3
3
  import { getPCRPrimers } from '@opencloning/store/cloning_utils';
4
4
  import { getTeselaJsonFromBase64 } from '@opencloning/utils/readNwrite';
5
+ import { getVerificationFileContent } from '@opencloning/utils/verificationFileStore';
5
6
  import { buildAlignmentTrack, buildReferenceTrack, buildAlignmentConfig, removePanelFromShown, updatePanelsShownWithAlignment } from '@opencloning/utils/alignmentUtils';
6
7
 
7
8
  export default function useStoreEditor() {
@@ -31,7 +32,7 @@ export default function useStoreEditor() {
31
32
  if (alignmentFiles.length > 0) {
32
33
  const referenceTrack = buildReferenceTrack(sequenceData, alignmentFiles[0].alignment[0]);
33
34
  const otherTracks = await Promise.all(alignmentFiles.map(async (aln) => {
34
- const fileContent = await getTeselaJsonFromBase64(sessionStorage.getItem(`verification-${id}-${aln.file_name}`), aln.file_name);
35
+ const fileContent = await getTeselaJsonFromBase64(getVerificationFileContent(`verification-${id}-${aln.file_name}`), aln.file_name);
35
36
  return buildAlignmentTrack(fileContent, aln);
36
37
  }));
37
38
  const alignmentConfig = buildAlignmentConfig(id, sequenceData, [referenceTrack, ...otherTracks]);
@@ -4,11 +4,15 @@ import { cloningActions } from '@opencloning/store/cloning';
4
4
  import useDatabase from './useDatabase';
5
5
  import useLoadDatabaseFile from './useLoadDatabaseFile';
6
6
  import useCloningAlerts from './useCloningAlerts';
7
- import useHttpClient from './useHttpClient';
8
7
  import useValidateState from './useValidateState';
9
8
  import useCloningHistoryLoader from './useCloningHistoryLoader';
10
9
  import { formatSequenceLocationString, getUrlParameters } from '@opencloning/utils/other';
11
10
  import { formatTemplate } from '@opencloning/utils/readNwrite';
11
+ import { encodePathSegment } from '@opencloning/utils/urlUtils';
12
+ import { getExternalHttpClient } from '@opencloning/utils/httpClients';
13
+
14
+ // For public urls (not the backend)
15
+ const httpClient = getExternalHttpClient();
12
16
 
13
17
  const { updateSource } = cloningActions;
14
18
 
@@ -21,9 +25,8 @@ export default function useUrlParamsLoader() {
21
25
  const database = useDatabase();
22
26
  const { addAlert } = useCloningAlerts();
23
27
  const setHistoryFileError = (e) => addAlert({ message: e, severity: 'error' });
24
- const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 }, sendPostRequest: null, setHistoryFileError });
28
+ const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 }, sendSourceRequest: null, setHistoryFileError });
25
29
  const validateState = useValidateState();
26
- const httpClient = useHttpClient();
27
30
  const { applyCloningStrategy, loadHistoryFromFile } = useCloningHistoryLoader();
28
31
 
29
32
  useEffect(() => {
@@ -46,7 +49,7 @@ export default function useUrlParamsLoader() {
46
49
  }
47
50
  } else if (urlParams.source === 'example' && urlParams.example) {
48
51
  try {
49
- const url = `${import.meta.env.BASE_URL}examples/${urlParams.example}`;
52
+ const url = `${import.meta.env.BASE_URL}examples/${encodePathSegment(urlParams.example)}`;
50
53
  const fileName = urlParams.example;
51
54
  if (fileName.endsWith('.zip')) {
52
55
  const resp = await httpClient.get(url, { responseType: 'blob' });
@@ -75,7 +78,7 @@ export default function useUrlParamsLoader() {
75
78
  } else if (urlParams.source === 'template' && urlParams.template && urlParams.key) {
76
79
  try {
77
80
  const baseUrl = 'https://assets.opencloning.org/OpenCloning-submission';
78
- const url = `${baseUrl}/processed/${urlParams.key}/templates/${urlParams.template}`;
81
+ const url = `${baseUrl}/processed/${encodePathSegment(urlParams.key)}/templates/${encodePathSegment(urlParams.template)}`;
79
82
  const { data } = await httpClient.get(url);
80
83
  const validatedData = await validateState(data);
81
84
  const newState = formatTemplate(validatedData, url);
@@ -1,18 +1,16 @@
1
1
  import React from 'react';
2
2
  import useCloningAlerts from './useCloningAlerts';
3
- import useBackendRoute from './useBackendRoute';
4
- import useHttpClient from './useHttpClient';
3
+ import useBackendHttpClient from './useBackendHttpClient';
5
4
 
6
5
  export default function useValidateState() {
7
- const backendRoute = useBackendRoute();
8
6
  const { addAlert } = useCloningAlerts();
9
- const httpClient = useHttpClient();
7
+ const httpClient = useBackendHttpClient();
10
8
 
11
9
  const validateState = React.useCallback(async (newState) => {
12
10
  // Returns null if the state is valid, otherwise the new updated state
13
11
  // It also adds alerts
14
12
  try {
15
- const response = await httpClient.post(backendRoute('validate'), newState);
13
+ const response = await httpClient.post('validate', newState);
16
14
  if (response.data !== null) {
17
15
  response.headers['x-warning'].split(';').forEach((warning) => {
18
16
  addAlert({
@@ -37,7 +35,7 @@ export default function useValidateState() {
37
35
  }
38
36
  return newState;
39
37
  }
40
- }, [addAlert, backendRoute, httpClient]);
38
+ }, [addAlert, httpClient]);
41
39
 
42
40
  return validateState;
43
41
  }
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";
2
+ export const version = "2.1.0";
@@ -1,23 +0,0 @@
1
- import { useConfig } from './useConfig';
2
-
3
- export default function useBackendRoute() {
4
- const { backendUrl: configBackendUrl } = useConfig();
5
-
6
- if (!configBackendUrl) {
7
- return () => {};
8
- }
9
- const backendUrl = configBackendUrl.endsWith('/') ? configBackendUrl : `${configBackendUrl}/`;
10
-
11
- return function backendRoute(path) {
12
- if (!backendUrl) {
13
- throw new Error('Backend URL not set');
14
- }
15
- // console.log(new URL('/api', window.location.origin).href);
16
- // console.log(new URL(import.meta.env.VITE_REACT_APP_BACKEND_URL, window.location.origin).href);
17
- // This handles both the case where the backend url is absolute and the case where it is relative
18
- const backendRoot = new URL(backendUrl, window.location.origin).href;
19
- // Remove trailing slash from path
20
- const sanitizedPath = path.endsWith('/') ? path.slice(0, -1) : path;
21
- return new URL(sanitizedPath, backendRoot).href;
22
- };
23
- }