@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
@@ -2,14 +2,16 @@ import { Accordion, AccordionDetails, AccordionSummary, Alert, Button, CircularP
2
2
 
3
3
  import React from 'react';
4
4
  import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
5
- import useHttpClient from '../../hooks/useHttpClient';
5
+ import { getExternalHttpClient } from '@opencloning/utils/httpClients';
6
+
7
+ // External urls (not the backend): no auth token, only whitelisted urls
8
+ const httpClient = getExternalHttpClient();
6
9
 
7
10
  function SelectTemplateDialog({ onClose, open }) {
8
11
  const [templates, setTemplates] = React.useState(null);
9
12
  const [connectAttempt, setConnectAttemp] = React.useState(0);
10
13
  const [error, setError] = React.useState(null);
11
14
  const baseUrl = 'https://assets.opencloning.org/OpenCloning-submission';
12
- const httpClient = useHttpClient();
13
15
 
14
16
  React.useEffect(() => {
15
17
  const fetchData = async () => {
@@ -1,7 +1,6 @@
1
1
  import { useState, useCallback } from 'react';
2
2
  import { batch, useDispatch, useStore } from 'react-redux';
3
- import useBackendRoute from '../../../../../hooks/useBackendRoute';
4
- import useHttpClient from '../../../../../hooks/useHttpClient';
3
+ import useBackendHttpClient from '../../../../../hooks/useBackendHttpClient';
5
4
  import useStoreEditor from '../../../../../hooks/useStoreEditor';
6
5
  import error2String from '@opencloning/utils/error2String';
7
6
  import { cloningActions } from '@opencloning/store/cloning';
@@ -19,8 +18,7 @@ export default function useDesignPrimers({
19
18
 
20
19
  const store = useStore();
21
20
  const dispatch = useDispatch();
22
- const backendRoute = useBackendRoute();
23
- const httpClient = useHttpClient();
21
+ const httpClient = useBackendHttpClient();
24
22
  const { updateStoreEditor } = useStoreEditor();
25
23
 
26
24
  const designPrimers = useCallback(async () => {
@@ -43,7 +41,7 @@ export default function useDesignPrimers({
43
41
  });
44
42
 
45
43
  requestData.settings = globalPrimerSettings;
46
- const url = backendRoute(`primer_design/${endpoint}`);
44
+ const url = `primer_design/${endpoint}`;
47
45
 
48
46
  try {
49
47
  const resp = await httpClient.post(url, requestData, { params });
@@ -55,7 +53,7 @@ export default function useDesignPrimers({
55
53
  setError(error2String(thrownError));
56
54
  return true;
57
55
  }
58
- }, [fragmentOrientations, rois, sequenceIds, templateSequenceIds, designType, circularAssembly, primerDesignSettings, spacers, store, httpClient, backendRoute, handleNext, isAmplified]);
56
+ }, [fragmentOrientations, rois, sequenceIds, templateSequenceIds, designType, circularAssembly, primerDesignSettings, spacers, store, httpClient, handleNext, isAmplified]);
59
57
 
60
58
  const addPrimers = useCallback(() => {
61
59
  const pcrSources = store.getState().cloning.sources.filter((source) => source.type === 'PCRSource');
@@ -1,6 +1,5 @@
1
1
  import React, { useCallback } from 'react';
2
- import useBackendRoute from '../../../hooks/useBackendRoute';
3
- import useHttpClient from '../../../hooks/useHttpClient';
2
+ import useBackendHttpClient from '../../../hooks/useBackendHttpClient';
4
3
  import { isEqual } from 'lodash-es';
5
4
  import { useSelector } from 'react-redux';
6
5
 
@@ -16,15 +15,14 @@ export function makeHeterodimerCacheKey(sequence1, sequence2, globalPrimerSettin
16
15
  return `${pairKey}::${settingsKey}`;
17
16
  }
18
17
  export function usePrimerDetailsEndpoints() {
19
- const backendRoute = useBackendRoute();
20
- const httpClient = useHttpClient();
18
+ const httpClient = useBackendHttpClient();
21
19
  const globalPrimerSettings = useSelector((state) => state.cloning.globalPrimerSettings, isEqual);
22
20
 
23
21
  // Caches are keyed by both the input sequence(s) and the current globalPrimerSettings,
24
22
  // so we don't need to clear them on settings change.
25
23
 
26
- const url = backendRoute('primer_details');
27
- const heterodimerUrl = backendRoute('primer_heterodimer');
24
+ const url = 'primer_details';
25
+ const heterodimerUrl = 'primer_heterodimer';
28
26
 
29
27
  const getPrimerDetails = useCallback(async (sequence) => {
30
28
  const cacheKey = makePrimerDetailsCacheKey(sequence, globalPrimerSettings);
@@ -7,7 +7,7 @@ import { classNameToEndPointMap } from '@opencloning/utils/sourceFunctions';
7
7
  import ObjectTable from '../ObjectTable';
8
8
  import { cloningActions } from '@opencloning/store/cloning';
9
9
 
10
- function CollectionSource({ source, requestStatus, sendPostRequest }) {
10
+ function CollectionSource({ source, requestStatus, sendSourceRequest }) {
11
11
  const { id: sourceId, options, image: imageInfo, title, description } = source;
12
12
  const [image, imageWidth] = imageInfo || [null, null];
13
13
  const [selectedOption, setSelectedOption] = React.useState(null);
@@ -34,7 +34,7 @@ function CollectionSource({ source, requestStatus, sendPostRequest }) {
34
34
  const reverseOligo = primers.find((primer) => primer.id === selectedSource.input[1].sequence);
35
35
  requestData = { source: { id: sourceId, ...selectedSource }, primers: [forwardOligo, reverseOligo] };
36
36
  }
37
- sendPostRequest({ endpoint, requestData, source, modifySource });
37
+ sendSourceRequest({ endpoint, requestData, source, modifySource });
38
38
  };
39
39
 
40
40
  const onChange = (event) => {
@@ -14,7 +14,7 @@ import SourceAnnotation from './SourceAnnotation';
14
14
  import SourceDatabase from './SourceDatabase';
15
15
  import SourcePolymeraseExtension from './SourcePolymeraseExtension';
16
16
  import CollectionSource from './CollectionSource';
17
- import useBackendAPI from '../../hooks/useBackendAPI';
17
+ import useSourceRequest from '../../hooks/useSourceRequest';
18
18
  import MultipleOutputsSelector from './MultipleOutputsSelector';
19
19
  import { cloningActions } from '@opencloning/store/cloning';
20
20
  import SourceCopySequence from './SourceCopySequence';
@@ -31,7 +31,7 @@ function Source({ sourceId }) {
31
31
  const { type: sourceType } = source;
32
32
  let specificSource = null;
33
33
  const templateOnlySources = ['CollectionSource', 'KnownGenomeCoordinatesSource'];
34
- const { requestStatus, sendPostRequest, sources, sequences } = useBackendAPI();
34
+ const { requestStatus, sendSourceRequest, sources, sequences } = useSourceRequest();
35
35
  const { addSequenceAndUpdateItsSource, updateSequenceAndItsSource } = cloningActions;
36
36
  const [chosenFragment, setChosenFragment] = React.useState(null);
37
37
  const dispatch = useDispatch();
@@ -49,67 +49,67 @@ function Source({ sourceId }) {
49
49
  switch (sourceType) {
50
50
  /* eslint-disable */
51
51
  case 'UploadedFileSource':
52
- specificSource = <SourceFile {...{ source, requestStatus, sendPostRequest }} />; break;
52
+ specificSource = <SourceFile {...{ source, requestStatus, sendSourceRequest }} />; break;
53
53
  case 'LocalFileSource':
54
- specificSource = <SourceServerStaticFile {...{ source, requestStatus, sendPostRequest }} />; break;
54
+ specificSource = <SourceServerStaticFile {...{ source, requestStatus, sendSourceRequest }} />; break;
55
55
  case 'RestrictionEnzymeDigestionSource':
56
- specificSource = <SourceRestriction {...{ source, requestStatus, sendPostRequest }} />; break;
56
+ specificSource = <SourceRestriction {...{ source, requestStatus, sendSourceRequest }} />; break;
57
57
  case 'RepositoryIdSource':
58
- specificSource = <SourceRepositoryId {...{ source, requestStatus, sendPostRequest }} />; break;
58
+ specificSource = <SourceRepositoryId {...{ source, requestStatus, sendSourceRequest }} />; break;
59
59
  case 'AddgeneIdSource':
60
- specificSource = <SourceRepositoryId {...{ source, requestStatus, sendPostRequest }} />; break;
60
+ specificSource = <SourceRepositoryId {...{ source, requestStatus, sendSourceRequest }} />; break;
61
61
  case 'SnapGenePlasmidSource':
62
- specificSource = <SourceRepositoryId {...{ source, requestStatus, sendPostRequest }} />; break;
62
+ specificSource = <SourceRepositoryId {...{ source, requestStatus, sendSourceRequest }} />; break;
63
63
  case 'EuroscarfSource':
64
- specificSource = <SourceRepositoryId {...{ source, requestStatus, sendPostRequest }} />; break;
64
+ specificSource = <SourceRepositoryId {...{ source, requestStatus, sendSourceRequest }} />; break;
65
65
  case 'WekWikGeneIdSource':
66
- specificSource = <SourceRepositoryId {...{ source, requestStatus, sendPostRequest }} />; break;
66
+ specificSource = <SourceRepositoryId {...{ source, requestStatus, sendSourceRequest }} />; break;
67
67
  case 'OpenDNACollectionsSource':
68
- specificSource = <SourceRepositoryId {...{ source, requestStatus, sendPostRequest }} />; break;
68
+ specificSource = <SourceRepositoryId {...{ source, requestStatus, sendSourceRequest }} />; break;
69
69
  case 'LigationSource':
70
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
70
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
71
71
  case 'GibsonAssemblySource':
72
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
72
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
73
73
  case 'OverlapExtensionPCRLigationSource':
74
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
74
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
75
75
  case 'InFusionSource':
76
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
76
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
77
77
  case 'InVivoAssemblySource':
78
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
78
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
79
79
  case 'GatewaySource':
80
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
80
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
81
81
  case 'CreLoxRecombinationSource':
82
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
82
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
83
83
  case 'RecombinaseSource':
84
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
84
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
85
85
  case 'HomologousRecombinationSource':
86
- specificSource = <SourceHomologousRecombination {...{ source, requestStatus, sendPostRequest }} />; break;
86
+ specificSource = <SourceHomologousRecombination {...{ source, requestStatus, sendSourceRequest }} />; break;
87
87
  case 'PCRSource':
88
- specificSource = <SourcePCRorHybridization {...{ source, requestStatus, sendPostRequest }} />; break;
88
+ specificSource = <SourcePCRorHybridization {...{ source, requestStatus, sendSourceRequest }} />; break;
89
89
  case 'RestrictionAndLigationSource':
90
- specificSource = <SourceAssembly {...{ source, requestStatus, sendPostRequest }} />; break;
90
+ specificSource = <SourceAssembly {...{ source, requestStatus, sendSourceRequest }} />; break;
91
91
  case 'GenomeCoordinatesSource':
92
- specificSource = <SourceGenomeRegion {...{ source, requestStatus, sendPostRequest }} />; break;
92
+ specificSource = <SourceGenomeRegion {...{ source, requestStatus, sendSourceRequest }} />; break;
93
93
  case 'KnownGenomeCoordinatesSource':
94
- specificSource = <SourceKnownGenomeRegion {...{ source, requestStatus, sendPostRequest }} />; break;
94
+ specificSource = <SourceKnownGenomeRegion {...{ source, requestStatus, sendSourceRequest }} />; break;
95
95
  case 'ManuallyTypedSource':
96
- specificSource = <SourceManuallyTyped {...{ source, requestStatus, sendPostRequest }} />; break;
96
+ specificSource = <SourceManuallyTyped {...{ source, requestStatus, sendSourceRequest }} />; break;
97
97
  case 'CRISPRSource':
98
- specificSource = <SourceHomologousRecombination {...{ source, requestStatus, sendPostRequest }} />; break;
98
+ specificSource = <SourceHomologousRecombination {...{ source, requestStatus, sendSourceRequest }} />; break;
99
99
  case 'OligoHybridizationSource':
100
- specificSource = <SourcePCRorHybridization {...{ source, requestStatus, sendPostRequest }} />; break;
100
+ specificSource = <SourcePCRorHybridization {...{ source, requestStatus, sendSourceRequest }} />; break;
101
101
  case 'PolymeraseExtensionSource':
102
- specificSource = <SourcePolymeraseExtension {...{ source, requestStatus, sendPostRequest }} />; break;
102
+ specificSource = <SourcePolymeraseExtension {...{ source, requestStatus, sendSourceRequest }} />; break;
103
103
  case 'DatabaseSource':
104
- specificSource = <SourceDatabase {...{ source, requestStatus, sendPostRequest }} />; break;
104
+ specificSource = <SourceDatabase {...{ source, requestStatus, sendSourceRequest }} />; break;
105
105
  case 'CollectionSource':
106
- specificSource = <CollectionSource {...{ source, requestStatus, sendPostRequest }} />; break;
106
+ specificSource = <CollectionSource {...{ source, requestStatus, sendSourceRequest }} />; break;
107
107
  case 'CopySequence':
108
108
  specificSource = <SourceCopySequence {...{ source }} />; break;
109
109
  case 'AnnotationSource':
110
- specificSource = <SourceAnnotation {...{ source, requestStatus, sendPostRequest }} />; break;
110
+ specificSource = <SourceAnnotation {...{ source, requestStatus, sendSourceRequest }} />; break;
111
111
  case 'ReverseComplementSource':
112
- specificSource = <SourceReverseComplement {...{ source, requestStatus, sendPostRequest }} />; break;
112
+ specificSource = <SourceReverseComplement {...{ source, requestStatus, sendSourceRequest }} />; break;
113
113
  default:
114
114
  break;
115
115
  /* eslint-enable */
@@ -5,7 +5,7 @@ import { isEqual } from 'lodash-es';
5
5
  import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
6
6
  import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
7
7
 
8
- function SourceAnnotation({ source, requestStatus, sendPostRequest }) {
8
+ function SourceAnnotation({ source, requestStatus, sendSourceRequest }) {
9
9
  const [annotationTool, setAnnotationTool] = React.useState('plannotate');
10
10
 
11
11
  const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, source.id), isEqual);
@@ -16,7 +16,7 @@ function SourceAnnotation({ source, requestStatus, sendPostRequest }) {
16
16
  sequence: inputSequences[0],
17
17
  source: { id: source.id, input: inputSequences.map((e) => ({ sequence: e.id })), annotation_tool: annotationTool },
18
18
  };
19
- sendPostRequest({ endpoint: 'annotate/plannotate', requestData, source });
19
+ sendSourceRequest({ endpoint: 'annotate/plannotate', requestData, source });
20
20
  };
21
21
 
22
22
  return (
@@ -15,7 +15,7 @@ const helpSingleSite = 'Even if input sequences contain multiple att sites '
15
15
  + 'Select this option to get those products.';
16
16
 
17
17
  // A component representing the ligation or gibson assembly of several fragments
18
- function SourceAssembly({ source, requestStatus, sendPostRequest }) {
18
+ function SourceAssembly({ source, requestStatus, sendSourceRequest }) {
19
19
  const assemblyType = source.type;
20
20
  const { id: sourceId, input: sourceInput } = source;
21
21
  const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
@@ -91,21 +91,21 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
91
91
  } };
92
92
  // To instantiate the correct class on the backend
93
93
  requestData.source.type = assemblyType;
94
- sendPostRequest({ endpoint: 'gibson_assembly', requestData, config, source });
94
+ sendSourceRequest({ endpoint: 'gibson_assembly', requestData, config, source });
95
95
  } else if (assemblyType === 'RestrictionAndLigationSource') {
96
96
  if (enzymes.length === 0) { return; }
97
97
  requestData.source.restriction_enzymes = enzymes;
98
98
  const config = { params: {
99
99
  circular_only: circularOnly,
100
100
  } };
101
- sendPostRequest({ endpoint: 'restriction_and_ligation', requestData, config, source });
101
+ sendSourceRequest({ endpoint: 'restriction_and_ligation', requestData, config, source });
102
102
  } else if (assemblyType === 'GatewaySource') {
103
103
  requestData.source.greedy = gatewaySettings.greedy;
104
104
  requestData.source.reaction_type = gatewaySettings.reactionType;
105
105
  const config = { params: { circular_only: circularOnly, only_multi_site: gatewaySettings.onlyMultiSite } };
106
- sendPostRequest({ endpoint: 'gateway', requestData, config, source });
106
+ sendSourceRequest({ endpoint: 'gateway', requestData, config, source });
107
107
  } else if (assemblyType === 'CreLoxRecombinationSource') {
108
- sendPostRequest({ endpoint: 'cre_lox_recombination', requestData, source });
108
+ sendSourceRequest({ endpoint: 'cre_lox_recombination', requestData, source });
109
109
  } else if (assemblyType === 'RecombinaseSource') {
110
110
  if (recombinases.length === 0) { return; }
111
111
  requestData.source.recombinases = recombinases;
@@ -113,14 +113,14 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
113
113
  reverse_recombinase: reverseRecombinase,
114
114
  input_contains_genome: inputContainsGenome,
115
115
  } };
116
- sendPostRequest({ endpoint: 'recombinase', requestData, config, source });
116
+ sendSourceRequest({ endpoint: 'recombinase', requestData, config, source });
117
117
  } else {
118
118
  const config = { params: {
119
119
  allow_partial_overlap: allowPartialOverlap,
120
120
  circular_only: circularOnly,
121
121
  blunt: bluntLigation,
122
122
  } };
123
- sendPostRequest({ endpoint: classNameToEndPointMap[assemblyType], requestData, config, source });
123
+ sendSourceRequest({ endpoint: classNameToEndPointMap[assemblyType], requestData, config, source });
124
124
  }
125
125
  };
126
126
 
@@ -4,12 +4,12 @@ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
4
4
  import useDatabase from '../../hooks/useDatabase';
5
5
  import useLoadDatabaseFile from '../../hooks/useLoadDatabaseFile';
6
6
 
7
- function SourceDatabase({ source, requestStatus, sendPostRequest }) {
7
+ function SourceDatabase({ source, requestStatus, sendSourceRequest }) {
8
8
  const [file, setFile] = React.useState(null);
9
9
  const [databaseId, setDatabaseId] = React.useState(null);
10
10
  const database = useDatabase();
11
11
  const [historyFileError, setHistoryFileError] = React.useState(null);
12
- const { loadDatabaseFile } = useLoadDatabaseFile({ source, sendPostRequest, setHistoryFileError });
12
+ const { loadDatabaseFile } = useLoadDatabaseFile({ source, sendSourceRequest, setHistoryFileError });
13
13
 
14
14
  const onSubmit = async (e) => {
15
15
  e.preventDefault();
@@ -1,15 +1,9 @@
1
1
  import React from 'react';
2
2
  import { FormHelperText, Alert, Checkbox, FormControl, FormControlLabel, InputLabel, MenuItem, Select, TextField } from '@mui/material';
3
- import { useDispatch, batch, useStore, useSelector } from 'react-redux';
3
+ import { useSelector } from 'react-redux';
4
4
  import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
5
5
  import LabelWithTooltip from '../form/LabelWithTooltip';
6
- import { cloningActions } from '@opencloning/store/cloning';
7
- import { loadFilesToSessionStorage, loadHistoryFile, updateVerificationFileNames } from '@opencloning/utils/readNwrite';
8
- import useValidateState from '../../hooks/useValidateState';
9
- import { mergeStates, getGraftSequenceId, graftState } from '@opencloning/utils/network';
10
- import useSnapgeneHistoryEndpoint from '../../hooks/useSnapgeneHistoryEndpoint';
11
-
12
- const { deleteSourceAndItsChildren, restoreSource, setState: setCloningState } = cloningActions;
6
+ import useCloningHistoryLoader from '../../hooks/useCloningHistoryLoader';
13
7
 
14
8
  const fileFormatToExtension = {
15
9
  genbank: 'gb',
@@ -20,7 +14,7 @@ const fileFormatToExtension = {
20
14
  };
21
15
 
22
16
  // A component providing an interface to import a file
23
- function SourceFile({ source, requestStatus, sendPostRequest }) {
17
+ function SourceFile({ source, requestStatus, sendSourceRequest }) {
24
18
  const [circularize, setCircularize] = React.useState(false);
25
19
  const [showCoordinates, setShowCoordinates] = React.useState(false);
26
20
  const [coordinates, setCoordinates] = React.useState({ start: null, end: null });
@@ -28,10 +22,7 @@ function SourceFile({ source, requestStatus, sendPostRequest }) {
28
22
  const hasOutput = useSelector((state) => state.cloning.sequences.some((s) => s.id === source.id));
29
23
  // Error message for json only
30
24
  const [alert, setAlert] = React.useState(null);
31
- const dispatch = useDispatch();
32
- const validateState = useValidateState();
33
- const store = useStore();
34
- const { loadSnapgeneHistory } = useSnapgeneHistoryEndpoint();
25
+ const { loadHistoryFromFile, loadSnapgeneHistory } = useCloningHistoryLoader();
35
26
  const onChange = async (event) => {
36
27
  setAlert(null);
37
28
  const files = Array.from(event.target.files);
@@ -39,6 +30,7 @@ function SourceFile({ source, requestStatus, sendPostRequest }) {
39
30
  return;
40
31
  }
41
32
  event.target.value = null;
33
+ const reportError = (message) => setAlert({ message, severity: 'error' });
42
34
  // If the file is a history file, we load it
43
35
  if (
44
36
  fileFormat === 'json' || fileFormat === 'zip'
@@ -50,54 +42,32 @@ function SourceFile({ source, requestStatus, sendPostRequest }) {
50
42
  type: fileFormat === 'json' ? 'application/json' : files[0].type,
51
43
  });
52
44
  }
53
- let cloningStrategy;
54
- let verificationFiles;
55
- try {
56
- ({ cloningStrategy, verificationFiles } = await loadHistoryFile(files[0]));
57
- } catch (e) {
58
- console.error(e);
59
- setAlert({ message: e.message, severity: 'error' });
45
+
46
+ const prepared = await loadHistoryFromFile(files[0], { onError: reportError });
47
+ if (!prepared) {
60
48
  return;
61
49
  }
62
50
 
63
- const validatedCloningStrategy = await validateState(cloningStrategy);
64
- // Update the verificationFiles names if needed
65
- const updatedVerificationFiles = updateVerificationFileNames(verificationFiles, cloningStrategy.files, validatedCloningStrategy.files);
66
-
67
- const canGraft = getGraftSequenceId(validatedCloningStrategy) !== null;
68
- const graft = hasOutput && canGraft;
69
-
70
- if (hasOutput && !canGraft) {
51
+ if (hasOutput && !prepared.canGraft) {
71
52
  setAlert({ message: 'Cannot graft cloning strategy as it does not converge on a single sequence, you can load it on a source without outputs', severity: 'error' });
72
53
  return;
73
54
  }
74
55
 
75
- batch(async () => {
76
- if (!graft) {
77
- // Replace the source with the new one
78
- dispatch(deleteSourceAndItsChildren(source.id));
79
- }
80
- try {
81
- const cloningState = store.getState().cloning;
82
- let mergedState;
83
- let idShift;
84
- if (graft) {
85
- ({ mergedState, idShift } = graftState(validatedCloningStrategy, cloningState, source.id));
86
- } else {
87
- ({ mergedState, idShift } = mergeStates(validatedCloningStrategy, cloningState));
88
- }
89
- dispatch(setCloningState(mergedState));
90
- await loadFilesToSessionStorage(updatedVerificationFiles, idShift);
91
- } catch (e) {
92
- setAlert({ message: e.message, severity: 'error' });
93
- dispatch(restoreSource({ ...source, type: 'UploadedFileSource' }));
56
+ const graft = hasOutput && prepared.canGraft;
57
+ try {
58
+ const applyOptions = { source, onError: reportError };
59
+ if (graft) {
60
+ await prepared.graft(applyOptions);
61
+ } else {
62
+ await prepared.merge(applyOptions);
94
63
  }
95
- });
64
+ } catch {
65
+ // Error already reported via onError; state restored in applyCloningStrategy
66
+ }
96
67
  return;
97
68
  }
98
69
  if (fileFormat === 'snapgene' || (fileFormat === '' && files[0].name.endsWith('.dna'))) {
99
- console.log('loading snapgene history');
100
- const success = await loadSnapgeneHistory(files[0], source.id);
70
+ const success = await loadSnapgeneHistory(files[0], { source });
101
71
  if (success) {
102
72
  return;
103
73
  }
@@ -110,7 +80,7 @@ function SourceFile({ source, requestStatus, sendPostRequest }) {
110
80
  },
111
81
  params: { circularize, sequence_file_format: fileFormat || null, ...coordinates },
112
82
  };
113
- sendPostRequest({ endpoint: 'read_from_file', requestData, config, source });
83
+ sendSourceRequest({ endpoint: 'read_from_file', requestData, config, source });
114
84
  };
115
85
 
116
86
  const updateCoordinates = (value, isStart) => {
@@ -62,6 +62,52 @@ describe('<AssemblyIdSelector />', () => {
62
62
  cy.wait('@getPairedAssemblyInfo');
63
63
  cy.contains('Equivalent assembly GCF_000002945.3 has annotation').should('exist');
64
64
  });
65
+ it('shows a warning when the paired accession lookup fails', () => {
66
+ const setAssemblyId = cy.spy().as('setAssemblyId');
67
+ const setHasAnnotation = cy.spy().as('setHasAnnotation');
68
+ const onAssemblyIdChange = cy.spy().as('onAssemblyIdChange');
69
+
70
+ cy.intercept('GET', 'https://api.ncbi.nlm.nih.gov/datasets/v2alpha/genome/accession/GCA_000002945.3/dataset_report*', {
71
+ statusCode: 200,
72
+ body: {
73
+ reports: [{
74
+ accession: 'GCA_000002945.3',
75
+ organism: {
76
+ tax_id: 559292,
77
+ organism_name: 'Saccharomyces cerevisiae'
78
+ },
79
+ annotation_info: undefined,
80
+ paired_accession: 'GCF_000002945.3',
81
+ assembly_info: {
82
+ assembly_status: 'current'
83
+ }
84
+ }]
85
+ }
86
+ }).as('getAssemblyInfo');
87
+
88
+ cy.intercept('GET', 'https://api.ncbi.nlm.nih.gov/datasets/v2alpha/genome/accession/GCF_000002945.3/dataset_report*', {
89
+ statusCode: 500,
90
+ body: {}
91
+ }).as('getPairedAssemblyInfo');
92
+
93
+ cy.mount(
94
+ <ConfigProvider config={config}>
95
+ <AssemblyIdSelector
96
+ setAssemblyId={setAssemblyId}
97
+ setHasAnnotation={setHasAnnotation}
98
+ onAssemblyIdChange={onAssemblyIdChange}
99
+ />
100
+ </ConfigProvider>
101
+ );
102
+ cy.get('input').type('GCA_000002945.3');
103
+ cy.wait('@getAssemblyInfo');
104
+ cy.wait('@getPairedAssemblyInfo');
105
+
106
+ cy.contains('Could not check whether an equivalent assembly has annotation:').should('exist');
107
+ cy.contains('Equivalent assembly GCF_000002945.3 has annotation').should('not.exist');
108
+ cy.get('label').contains('Species').siblings('div').children('input')
109
+ .should('have.value', 'Saccharomyces cerevisiae - 559292');
110
+ });
65
111
  it('handles NCBI being down displaying the right error', () => {
66
112
  cy.intercept('GET', 'https://api.ncbi.nlm.nih.gov/datasets/v2alpha/genome/accession/GCA_000002945.3/dataset_report*', {
67
113
  statusCode: 500,
@@ -5,8 +5,11 @@ import { getReferenceAssemblyId, taxonSuggest, geneSuggest, getInfoFromAssemblyI
5
5
  import TextFieldValidate from '../form/TextFieldValidate';
6
6
  import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
7
7
  import GetRequestMultiSelect from '../form/GetRequestMultiSelect';
8
- import useHttpClient from '../../hooks/useHttpClient';
9
8
  import { formatSequenceLocationString } from '@opencloning/utils/other';
9
+ import { getExternalHttpClient } from '@opencloning/utils/httpClients';
10
+
11
+ // External urls (not the backend): no auth token, only whitelisted urls
12
+ const httpClient = getExternalHttpClient();
10
13
 
11
14
  function getGeneCoordsInfo(gene) {
12
15
  const { range: geneRange, accession_version: accessionVersion } = gene.annotation.genomic_regions[0].gene_range;
@@ -59,7 +62,6 @@ function SpeciesPicker({ setSpecies, setAssemblyId }) {
59
62
  }
60
63
 
61
64
  function SequenceAccessionPicker({ assemblyAccession, setSequenceAccession }) {
62
- const httpClient = useHttpClient();
63
65
 
64
66
  const url = `https://api.ncbi.nlm.nih.gov/datasets/v2alpha/genome/accession/${assemblyAccession}/sequence_reports`
65
67
  return (
@@ -80,7 +82,7 @@ function SequenceAccessionPicker({ assemblyAccession, setSequenceAccession }) {
80
82
  }
81
83
 
82
84
  // Extra component to be used in SourceGenomeRegion
83
- function SourceGenomeRegionLocusOnReference({ source, requestStatus, sendPostRequest }) {
85
+ function SourceGenomeRegionLocusOnReference({ source, requestStatus, sendSourceRequest }) {
84
86
  const { id: sourceId } = source;
85
87
  const [gene, setGene] = React.useState(null);
86
88
  const [species, setSpecies] = React.useState(null);
@@ -92,7 +94,7 @@ function SourceGenomeRegionLocusOnReference({ source, requestStatus, sendPostReq
92
94
  event.preventDefault();
93
95
  const requestData = formatBackendPayloadWithGene(assemblyId, gene, Number(upstreamBasesRef.current.value), Number(downstreamBasesRef.current.value));
94
96
  requestData.id = sourceId;
95
- sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
97
+ sendSourceRequest({ endpoint: 'genome_coordinates', requestData, source });
96
98
  };
97
99
 
98
100
  // Reset gene when species changes
@@ -153,9 +155,11 @@ function AssemblyIdSelector({ setAssemblyId, setHasAnnotation = () => {}, onAsse
153
155
  const [newerAssembly, setNewerAssembly] = React.useState(false);
154
156
  const [species, setSpecies] = React.useState(null);
155
157
  const [pairedAccessionWithAnnotation, setPairedAccessionWithAnnotation] = React.useState('');
158
+ const [pairedAccessionLookupFailed, setPairedAccessionLookupFailed] = React.useState(false);
156
159
 
157
160
  const onChange = async (userInput, resp) => {
158
161
  setPairedAccessionWithAnnotation('');
162
+ setPairedAccessionLookupFailed(false);
159
163
  setSpecies(resp === null ? null : resp.species);
160
164
  if (resp === null) {
161
165
  setAssemblyId('');
@@ -172,9 +176,14 @@ function AssemblyIdSelector({ setAssemblyId, setHasAnnotation = () => {}, onAsse
172
176
  setNewerAssembly(resp !== null && resp.newerAssembly);
173
177
  onAssemblyIdChange();
174
178
  if (resp !== null && !resp.hasAnnotation && resp.pairedAccession) {
175
- const pairedAccessionInfo = await getInfoFromAssemblyId(resp.pairedAccession);
176
- if (pairedAccessionInfo !== null && pairedAccessionInfo.hasAnnotation) {
177
- setPairedAccessionWithAnnotation(resp.pairedAccession);
179
+ try {
180
+ const pairedAccessionInfo = await getInfoFromAssemblyId(resp.pairedAccession);
181
+ if (pairedAccessionInfo !== null && pairedAccessionInfo.hasAnnotation) {
182
+ setPairedAccessionWithAnnotation(resp.pairedAccession);
183
+ }
184
+ } catch (e) {
185
+ console.error(e);
186
+ setPairedAccessionLookupFailed(true);
178
187
  }
179
188
  }
180
189
  };
@@ -194,13 +203,18 @@ function AssemblyIdSelector({ setAssemblyId, setHasAnnotation = () => {}, onAsse
194
203
  Equivalent assembly <a href={`https://www.ncbi.nlm.nih.gov/datasets/genome/${pairedAccessionWithAnnotation}`} target="_blank" rel="noopener noreferrer">{pairedAccessionWithAnnotation}</a> has annotation.
195
204
  </Alert>
196
205
  )}
206
+ {pairedAccessionLookupFailed && (
207
+ <Alert severity="warning">
208
+ Could not check whether an equivalent assembly has annotation: Try deleting and retyping the assembly ID.
209
+ </Alert>
210
+ )}
197
211
  {species && <KnownSpeciesField species={species} />}
198
212
  </>
199
213
  );
200
214
  }
201
215
 
202
216
  // Extra component to be used in SourceGenomeRegion
203
- function SourceGenomeRegionLocusOnOther({ source, requestStatus, sendPostRequest }) {
217
+ function SourceGenomeRegionLocusOnOther({ source, requestStatus, sendSourceRequest }) {
204
218
  const { id: sourceId } = source;
205
219
  const [gene, setGene] = React.useState(null);
206
220
  const [assemblyId, setAssemblyId] = React.useState('');
@@ -212,7 +226,7 @@ function SourceGenomeRegionLocusOnOther({ source, requestStatus, sendPostRequest
212
226
  event.preventDefault();
213
227
  const requestData = formatBackendPayloadWithGene(assemblyId, gene, Number(upstreamBasesRef.current.value), Number(downstreamBasesRef.current.value));
214
228
  requestData.id = sourceId;
215
- sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
229
+ sendSourceRequest({ endpoint: 'genome_coordinates', requestData, source });
216
230
  };
217
231
 
218
232
  return (
@@ -235,7 +249,7 @@ function SourceGenomeRegionLocusOnOther({ source, requestStatus, sendPostRequest
235
249
  }
236
250
 
237
251
  // Extra component to be used in SourceGenomeRegion
238
- function SourceGenomeRegionCustomCoordinates({ source, requestStatus, sendPostRequest, selectionMode }) {
252
+ function SourceGenomeRegionCustomCoordinates({ source, requestStatus, sendSourceRequest, selectionMode }) {
239
253
  // https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?dbfrom=nuccore&db=assembly&id=CM041205.1&idtype=acc
240
254
  const { id: sourceId } = source;
241
255
  const [species, setSpecies] = React.useState(null);
@@ -285,7 +299,7 @@ function SourceGenomeRegionCustomCoordinates({ source, requestStatus, sendPostRe
285
299
  assembly_accession: assemblyId || null,
286
300
  coordinates: formatSequenceLocationString(coords.start, coords.end, coords.strand === 'plus' ? 1 : -1),
287
301
  };
288
- sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
302
+ sendSourceRequest({ endpoint: 'genome_coordinates', requestData, source });
289
303
  };
290
304
 
291
305
  const onSequenceAccessionChange = async (userInput, resp) => {
@@ -446,7 +460,7 @@ function SourceGenomeRegionSelectGene({ gene, upstreamBasesRef, downstreamBasesR
446
460
  );
447
461
  }
448
462
 
449
- function SourceGenomeRegion({ source, requestStatus, sendPostRequest }) {
463
+ function SourceGenomeRegion({ source, requestStatus, sendSourceRequest }) {
450
464
  const { id: sourceId } = source;
451
465
  const [selectionMode, setSelectionMode] = React.useState('');
452
466
  const changeSelectionMode = (event) => { setSelectionMode(event.target.value); };
@@ -470,9 +484,9 @@ function SourceGenomeRegion({ source, requestStatus, sendPostRequest }) {
470
484
  </Select>
471
485
  </FormControl>
472
486
  </form>
473
- {selectionMode === 'reference_genome' && (<SourceGenomeRegionLocusOnReference {...{ source, requestStatus, sendPostRequest }} />)}
474
- {selectionMode === 'other_assembly' && (<SourceGenomeRegionLocusOnOther {...{ source, requestStatus, sendPostRequest }} />)}
475
- {selectionMode.startsWith('custom') && (<SourceGenomeRegionCustomCoordinates {...{ source, requestStatus, sendPostRequest, selectionMode }} />)}
487
+ {selectionMode === 'reference_genome' && (<SourceGenomeRegionLocusOnReference {...{ source, requestStatus, sendSourceRequest }} />)}
488
+ {selectionMode === 'other_assembly' && (<SourceGenomeRegionLocusOnOther {...{ source, requestStatus, sendSourceRequest }} />)}
489
+ {selectionMode.startsWith('custom') && (<SourceGenomeRegionCustomCoordinates {...{ source, requestStatus, sendSourceRequest, selectionMode }} />)}
476
490
 
477
491
  </>
478
492
  );