@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#778](https://github.com/OpenCloning/OpenCloning_frontend/pull/778) [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394) Thanks [@manulera](https://github.com/manulera)! - Only send the auth token to the backend, and rename the http client APIs.
8
+
9
+ - `@opencloning/utils`: `@opencloning/utils/httpClients` exports `getBackendHttpClient(backendUrl, { authenticated })`, a client that can only reach the backend (`allowAbsoluteUrls: false`) and is the only one carrying the auth token, and `getExternalHttpClient()` for whitelisted external urls. Replaces `@opencloning/utils/getHttpClient` (`getHttpClient`, `getAuthenticatedHttpClient`). `attachAuthInterceptors` refuses clients that could send the token elsewhere.
10
+ - `@opencloning/ui` (breaking): `useHttpClient` is renamed `useBackendHttpClient` and takes urls relative to `backendUrl`; `useBackendRoute` is removed; `useBackendAPI` is renamed `useSourceRequest` and `sendPostRequest` is renamed `sendSourceRequest`.
11
+ - `@opencloning/opencloningdb`, `@opencloning/opencloning-elabftw`: clients can no longer be pointed at other hosts.
12
+
13
+ ### Patch Changes
14
+
15
+ - [#778](https://github.com/OpenCloning/OpenCloning_frontend/pull/778) [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394) Thanks [@manulera](https://github.com/manulera)! - Encode values interpolated into request URL paths to prevent client-side path traversal. A sample UID such as `../workspaces/1/users/5` could make a user's own request hit a different endpoint, e.g. "delete UID" removing a workspace member.
16
+
17
+ - `@opencloning/utils`: new `encodePathSegment` (`@opencloning/utils/urlUtils`), which applies `encodeURIComponent` and rejects `.` and `..`.
18
+ - `@opencloning/opencloningdb`: all path parameters in `endpoints.js` are encoded.
19
+ - `@opencloning/opencloning-elabftw`: eLabFTW item and upload paths are encoded.
20
+ - `@opencloning/ui`: the `example`, `key` and `template` URL parameters are encoded when loading examples and templates.
21
+
22
+ - Updated dependencies [[`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394), [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394), [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394)]:
23
+ - @opencloning/utils@2.0.0
24
+ - @opencloning/store@2.0.0
25
+
26
+ ## 1.10.0
27
+
28
+ ### Minor Changes
29
+
30
+ - [#773](https://github.com/OpenCloning/OpenCloning_frontend/pull/773) [`f7f376e`](https://github.com/OpenCloning/OpenCloning_frontend/commit/f7f376ee351d9b2da52413700836dc55ba54b5ea) Thanks [@manulera](https://github.com/manulera)! - Handle failed NCBI request to `dataset_report` in `SourceGenomeRegion`.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies []:
35
+ - @opencloning/store@1.10.0
36
+ - @opencloning/utils@1.10.0
37
+
38
+ ## 1.9.5
39
+
40
+ ### Patch Changes
41
+
42
+ - Updated dependencies [[`73363e7`](https://github.com/OpenCloning/OpenCloning_frontend/commit/73363e7a7f7e494e20a54bd87e5dba97f8a9197e)]:
43
+ - @opencloning/utils@1.9.5
44
+ - @opencloning/store@1.9.5
45
+
46
+ ## 1.9.4
47
+
48
+ ### Patch Changes
49
+
50
+ - Updated dependencies []:
51
+ - @opencloning/store@1.9.4
52
+ - @opencloning/utils@1.9.4
53
+
54
+ ## 1.9.3
55
+
56
+ ### Patch Changes
57
+
58
+ - [#762](https://github.com/OpenCloning/OpenCloning_frontend/pull/762) [`df69a1b`](https://github.com/OpenCloning/OpenCloning_frontend/commit/df69a1bfb3b3128e93d89eef48a29f81a54ebb82) Thanks [@manulera](https://github.com/manulera)! - Fix QuotaExceededError when adding more than ~10 AB1/sequencing files (OpenCloning/OpenCloning#82). The raw contents of the verification files used to be persisted in Web Storage (localStorage, later sessionStorage), which has a quota of ~5 MB, so submitting around 10+ AB1 files failed with `QuotaExceededError` and the files/alignments disappeared. The contents are now kept in an in-memory store for the duration of the tab session, which has no quota.
59
+
60
+ - Updated dependencies [[`df69a1b`](https://github.com/OpenCloning/OpenCloning_frontend/commit/df69a1bfb3b3128e93d89eef48a29f81a54ebb82)]:
61
+ - @opencloning/utils@1.9.3
62
+ - @opencloning/store@1.9.3
63
+
64
+ ## 1.9.2
65
+
66
+ ### Patch Changes
67
+
68
+ - [#734](https://github.com/OpenCloning/OpenCloning_frontend/pull/734) [`06bb1a5`](https://github.com/OpenCloning/OpenCloning_frontend/commit/06bb1a513519f598e9ec3b5eec73544e2ed3958e) Thanks [@manulera](https://github.com/manulera)! - Add duplicate feature option to the database sequence viewer
69
+
70
+ - [#736](https://github.com/OpenCloning/OpenCloning_frontend/pull/736) [`bc045f9`](https://github.com/OpenCloning/OpenCloning_frontend/commit/bc045f9e74b4d5b84250dcb5a77b93de99e4d684) Thanks [@manulera](https://github.com/manulera)! - \* Unify load paths under single hook
71
+ - Remove warnings when adding db files to design tab
72
+ - Updated dependencies [[`bc045f9`](https://github.com/OpenCloning/OpenCloning_frontend/commit/bc045f9e74b4d5b84250dcb5a77b93de99e4d684)]:
73
+ - @opencloning/store@1.9.2
74
+ - @opencloning/utils@1.9.2
75
+
3
76
  ## 1.9.1
4
77
 
5
78
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "license": "MIT",
3
3
  "name": "@opencloning/ui",
4
- "version": "1.9.1",
4
+ "version": "2.0.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "scripts": {
@@ -25,6 +25,7 @@
25
25
  "./hooks/useAlerts": "./src/hooks/useAlerts.js",
26
26
  "./hooks/useCloningAlerts": "./src/hooks/useCloningAlerts.js",
27
27
  "./hooks/useSequencingAlignment": "./src/hooks/useSequencingAlignment.js",
28
+ "./hooks/useBackendHttpClient": "./src/hooks/useBackendHttpClient.js",
28
29
  "./components/SequenceViewer": "./src/components/SequenceViewer.jsx",
29
30
  "./standalone": "./src/StandAloneOpenCloning.js"
30
31
  },
@@ -39,14 +40,14 @@
39
40
  "@mui/icons-material": "^5.18.0",
40
41
  "@mui/material": "^5.18.0",
41
42
  "@mui/x-data-grid": "^8.28.5",
42
- "@opencloning/store": "1.9.1",
43
- "@opencloning/utils": "1.9.1",
43
+ "@opencloning/store": "2.0.0",
44
+ "@opencloning/utils": "2.0.0",
44
45
  "@teselagen/bio-parsers": "^0.4.38",
45
46
  "@teselagen/ove": "^0.8.42",
46
47
  "@teselagen/range-utils": "^0.3.20",
47
48
  "@teselagen/sequence-utils": "^0.3.42",
48
49
  "@zip.js/zip.js": "^2.8.26",
49
- "axios": "^1.16.0",
50
+ "axios": "^1.20.0",
50
51
  "lodash-es": "^4.18.1",
51
52
  "react-draggable": "^4.5.0"
52
53
  },
@@ -7,8 +7,7 @@ import { getSequencesNotInDatabase } from '@opencloning/utils/network';
7
7
  import { cloningActions } from '@opencloning/store/cloning';
8
8
  import useCloningAlerts from '../hooks/useCloningAlerts';
9
9
  import { cloneDeep } from 'lodash-es';
10
- import useBackendRoute from '../hooks/useBackendRoute';
11
- import useHttpClient from '../hooks/useHttpClient';
10
+ import useBackendHttpClient from '../hooks/useBackendHttpClient';
12
11
 
13
12
  const { setState: setCloningState } = cloningActions;
14
13
 
@@ -17,9 +16,8 @@ function useLocateSequencesInDatabaseAction() {
17
16
  const database = useDatabase();
18
17
  const store = useStore();
19
18
  const dispatch = useDispatch();
20
- const backendRoute = useBackendRoute();
21
- const httpClient = useHttpClient();
22
- const url = backendRoute('normalize_cloning_strategy');
19
+ const httpClient = useBackendHttpClient();
20
+ const url = 'normalize_cloning_strategy';
23
21
 
24
22
  const { addAlert } = useCloningAlerts();
25
23
  const main = React.useCallback(async () => {
@@ -4,16 +4,14 @@ import { useDispatch, useSelector, useStore } from 'react-redux';
4
4
  import { isEqual } from 'lodash-es';
5
5
  import { cloningActions } from '@opencloning/store/cloning';
6
6
  import error2String from '@opencloning/utils/error2String';
7
- import useBackendRoute from '../hooks/useBackendRoute';
8
- import useHttpClient from '../hooks/useHttpClient';
7
+ import useBackendHttpClient from '../hooks/useBackendHttpClient';
9
8
 
10
9
  function EditSequenceNameDialog({ id, dialogOpen, setDialogOpen }) {
11
10
  const [name, setName] = React.useState('');
12
11
  const [originalName, setOriginalName] = React.useState('');
13
12
  const [error, setError] = React.useState('');
14
13
  const store = useStore();
15
- const backendRoute = useBackendRoute();
16
- const httpClient = useHttpClient();
14
+ const httpClient = useBackendHttpClient();
17
15
 
18
16
  const { updateSequenceAndItsSource } = cloningActions;
19
17
  const dispatch = useDispatch();
@@ -23,7 +21,7 @@ function EditSequenceNameDialog({ id, dialogOpen, setDialogOpen }) {
23
21
  const source = sources.find((s) => s.id === id);
24
22
  const sequence = sequences.find((s) => s.id === id);
25
23
  setError('');
26
- const url = backendRoute('rename_sequence');
24
+ const url = 'rename_sequence';
27
25
  try {
28
26
  const { data: newSequence } = await httpClient.post(url, sequence, { params: { name } });
29
27
  const newSource = { ...source, output_name: newName };
@@ -1,8 +1,7 @@
1
1
  import { Alert, Button, CircularProgress } from '@mui/material';
2
2
  import React from 'react';
3
3
  import { useDispatch } from 'react-redux';
4
- import useBackendRoute from '../hooks/useBackendRoute';
5
- import useHttpClient from '../hooks/useHttpClient';
4
+ import useBackendHttpClient from '../hooks/useBackendHttpClient';
6
5
  import { cloningActions } from '@opencloning/store/cloning';
7
6
  import { version } from '../index';
8
7
 
@@ -14,8 +13,7 @@ function ExternalServicesStatusCheck() {
14
13
  const [loading, setLoading] = React.useState(false);
15
14
  const [successMessage, setSuccessMessage] = React.useState('');
16
15
  const dispatch = useDispatch();
17
- const backendRoute = useBackendRoute();
18
- const httpClient = useHttpClient();
16
+ const httpClient = useBackendHttpClient();
19
17
  React.useEffect(() => {
20
18
  dispatch(updateAppInfo({ frontendVersion: version }));
21
19
  setLoading(true);
@@ -23,7 +21,7 @@ function ExternalServicesStatusCheck() {
23
21
  const services = [
24
22
  {
25
23
  message: 'Backend server is down',
26
- url: backendRoute('version'),
24
+ url: 'version',
27
25
  check: (resp) => {
28
26
  if (resp.status === 200) {
29
27
  const { backend_version: backendVersion, schema_version: schemaVersion } = resp.data;
@@ -1,20 +1,15 @@
1
1
  import React from 'react';
2
2
  import { batch, useDispatch, useStore } from 'react-redux';
3
3
  import useCloningAlerts from '../hooks/useCloningAlerts';
4
- import useBackendRoute from '../hooks/useBackendRoute';
5
- import useValidateState from '../hooks/useValidateState';
4
+ import useCloningHistoryLoader from '../hooks/useCloningHistoryLoader';
5
+ import { getEmptyPlaceholderSource } from '@opencloning/store/cloning_utils';
6
6
  import { cloningActions } from '@opencloning/store/cloning';
7
- import { mergeStates } from '@opencloning/utils/network';
8
- import { loadFilesToSessionStorage, loadHistoryFile, updateVerificationFileNames } from '@opencloning/utils/readNwrite';
9
7
  import HistoryLoadedDialog from './HistoryLoadedDialog';
10
- import useHttpClient from '../hooks/useHttpClient';
11
- import { getVerificationFileName } from '@opencloning/utils/readNwrite';
12
- import { isEqual } from 'lodash-es';
13
- import useSnapgeneHistoryEndpoint from '../hooks/useSnapgeneHistoryEndpoint';
8
+ import useBackendHttpClient from '../hooks/useBackendHttpClient';
14
9
 
15
- const { setState: setCloningState, deleteSourceAndItsChildren, addSourceAndItsOutputSequence } = cloningActions;
10
+ const { deleteSourceAndItsChildren, addSourceAndItsOutputSequence } = cloningActions;
16
11
 
17
- async function processSequenceFiles(files, backendRoute, httpClient) {
12
+ async function processSequenceFiles(files, httpClient) {
18
13
  const allSources = [];
19
14
  const allSequences = [];
20
15
  const allWarnings = [];
@@ -31,7 +26,7 @@ async function processSequenceFiles(files, backendRoute, httpClient) {
31
26
  'content-type': 'multipart/form-data',
32
27
  },
33
28
  };
34
- const url = backendRoute('read_from_file');
29
+ const url = 'read_from_file';
35
30
  try {
36
31
  const { data: { sources, sequences }, headers } = await httpClient.post(url, formData, config);
37
32
  // If there are warnings, add them to the list of warnings
@@ -61,11 +56,9 @@ async function processSequenceFiles(files, backendRoute, httpClient) {
61
56
  function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
62
57
  const { addAlert } = useCloningAlerts();
63
58
  const dispatch = useDispatch();
64
- const backendRoute = useBackendRoute();
65
59
  const store = useStore();
66
- const validateState = useValidateState();
67
- const httpClient = useHttpClient();
68
- const { loadSnapgeneHistory } = useSnapgeneHistoryEndpoint();
60
+ const httpClient = useBackendHttpClient();
61
+ const { loadHistoryFromFile, loadSnapgeneHistory } = useCloningHistoryLoader();
69
62
  const [fileLoaderFunctions, setFileLoaderFunctions] = React.useState(null);
70
63
 
71
64
  React.useEffect(() => {
@@ -81,34 +74,11 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
81
74
  const isJsonFile = files.length === 1 && files[0].name.endsWith('.json');
82
75
  // If the file is a zip or json file, load the history file
83
76
  if (isZipFile || isJsonFile) {
84
- let cloningStrategy;
85
- let verificationFiles;
86
- try {
87
- ({ cloningStrategy, verificationFiles } = await loadHistoryFile(files[0]));
88
- } catch (e) {
89
- console.error(e);
90
- addAlert({ message: e.message, severity: 'error' });
77
+ const prepared = await loadHistoryFromFile(files[0]);
78
+ if (!prepared) {
91
79
  return;
92
80
  }
93
81
 
94
- const validatedState = await validateState(cloningStrategy);
95
- // Update the verificationFiles names if needed
96
- const updatedVerificationFiles = updateVerificationFileNames(verificationFiles, cloningStrategy.files, validatedState.files);
97
-
98
- const updateState = async (newState, idShift) => {
99
- dispatch(setCloningState(newState));
100
- await loadFilesToSessionStorage(updatedVerificationFiles, idShift);
101
- };
102
-
103
- const replaceState = async () => {
104
- await updateState(validatedState, 0);
105
- };
106
-
107
- const addState = async () => {
108
- const { mergedState, idShift } = mergeStates(validatedState, cloningState);
109
- await updateState(mergedState, idShift);
110
- };
111
-
112
82
  const errorWrapper = (fn) => async () => {
113
83
  try {
114
84
  await fn();
@@ -117,11 +87,15 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
117
87
  addAlert({ message: e.message, severity: 'error' });
118
88
  }
119
89
  };
120
- // If there are no sequences in the cloning state, load the history file
90
+ // If there are no sequences in the cloning state, load the history file
121
91
  if (cloningState.sequences.length === 0) {
122
- errorWrapper(replaceState)();
92
+ errorWrapper(prepared.replace)();
123
93
  } else {
124
- setFileLoaderFunctions({ addState: errorWrapper(addState), replaceState: errorWrapper(replaceState), clear: () => setFileLoaderFunctions(null) });
94
+ setFileLoaderFunctions({
95
+ addState: errorWrapper(prepared.merge),
96
+ replaceState: errorWrapper(prepared.replace),
97
+ clear: () => setFileLoaderFunctions(null),
98
+ });
125
99
  }
126
100
  // If there are sequences in the cloning state, give the option to merge or replace
127
101
 
@@ -141,11 +115,12 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
141
115
  }
142
116
  }
143
117
  try {
144
- const { sources, sequences, warnings } = await processSequenceFiles(files, backendRoute, httpClient);
118
+ const { sources, sequences, warnings } = await processSequenceFiles(files, httpClient);
145
119
  batch(() => {
146
120
  // If there is only one source and it is empty, delete it
147
- if (cloningState.sources.length === 1 && cloningState.sources[0].type === null) {
148
- dispatch(deleteSourceAndItsChildren(cloningState.sources[0].id));
121
+ const emptyPlaceholder = getEmptyPlaceholderSource(cloningState.sources);
122
+ if (emptyPlaceholder) {
123
+ dispatch(deleteSourceAndItsChildren(emptyPlaceholder.id));
149
124
  }
150
125
  for (let i = 0; i < sources.length; i += 1) {
151
126
  dispatch(addSourceAndItsOutputSequence({ source: sources[i], sequence: sequences[i] }));
@@ -48,6 +48,18 @@ function featureRightClickedOverride(items, { annotation }, props) {
48
48
  "--",
49
49
  "editFeature",
50
50
  "deleteFeature",
51
+ {
52
+ text: "Duplicate Feature",
53
+ disabled: props.readOnly,
54
+ onClick: () => {
55
+ const { id, ...rest } = annotation;
56
+ props.upsertFeature({
57
+ ...rest,
58
+ // omit id → upsertDeleteActionGenerator assigns a new shortid
59
+ name: annotation.name ? `${annotation.name} (copy)` : undefined,
60
+ });
61
+ },
62
+ },
51
63
  "showRemoveDuplicatesDialogFeatures",
52
64
  "--",
53
65
  "toggleCdsFeatureTranslations",
@@ -19,8 +19,7 @@ import useCombinatorialAssembly from './useCombinatorialAssembly';
19
19
  import ExistingSyntaxDialog from './ExistingSyntaxDialog';
20
20
  import error2String from '@opencloning/utils/error2String';
21
21
  import { categoryFilter, downloadAssemblerFilesAsZip, getFilesToExportFromAssembler, getDefaultAssemblyOutputName, MAX_OUTPUT_NAME_LENGTH, sanitizeOutputName } from './assembler_utils';
22
- import useBackendRoute from '../../hooks/useBackendRoute';
23
- import useHttpClient from '../../hooks/useHttpClient';
22
+ import useBackendHttpClient from '../../hooks/useBackendHttpClient';
24
23
  import useCloningAlerts from '../../hooks/useCloningAlerts';
25
24
  import UploadPlasmidsButton from './UploadPlasmidsButton';
26
25
  import { useConfig } from '../../providers';
@@ -376,12 +375,11 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
376
375
 
377
376
  function LoadSyntaxButton({ setSyntax, addPlasmids, clearPlasmids }) {
378
377
  const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
379
- const httpClient = useHttpClient();
378
+ const httpClient = useBackendHttpClient();
380
379
  const { staticContentPath } = useConfig();
381
- const backendRoute = useBackendRoute();
382
380
  const { addAlert } = useCloningAlerts();
383
381
  const onSyntaxSelect = React.useCallback(async (syntax, plasmids) => {
384
- const url = backendRoute('validate_syntax');
382
+ const url = 'validate_syntax';
385
383
  try {
386
384
  await httpClient.post(url, syntax);
387
385
  setSyntax(syntax)
@@ -393,7 +391,7 @@ function LoadSyntaxButton({ setSyntax, addPlasmids, clearPlasmids }) {
393
391
  severity: 'error',
394
392
  });
395
393
  }
396
- }, [setSyntax, addPlasmids, clearPlasmids, httpClient, backendRoute, addAlert])
394
+ }, [setSyntax, addPlasmids, clearPlasmids, httpClient, addAlert])
397
395
  return <>
398
396
  <Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
399
397
  {existingSyntaxDialogOpen && <ExistingSyntaxDialog
@@ -1,13 +1,13 @@
1
1
  import React from 'react'
2
2
  import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box, ButtonGroup, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'
3
- import getHttpClient from '@opencloning/utils/getHttpClient';
3
+ import { getExternalHttpClient } from '@opencloning/utils/httpClients';
4
4
  import RequestStatusWrapper from '../form/RequestStatusWrapper';
5
5
  import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
6
6
  import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
7
7
  import { normalizeSyntaxEnzymes } from '@opencloning/utils/normalizeSyntax';
8
8
  import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
9
9
 
10
- const httpClient = getHttpClient();
10
+ const httpClient = getExternalHttpClient();
11
11
  const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
12
12
  httpClient.defaults.baseURL = baseURL;
13
13
 
@@ -1,7 +1,6 @@
1
1
  import { useCallback } from 'react'
2
2
  import { classNameToEndPointMap } from '@opencloning/utils/sourceFunctions'
3
- import useBackendRoute from '../../hooks/useBackendRoute'
4
- import useHttpClient from '../../hooks/useHttpClient'
3
+ import useBackendHttpClient from '../../hooks/useBackendHttpClient'
5
4
  import { arrayCombinations } from './assembler_utils'
6
5
 
7
6
 
@@ -28,8 +27,7 @@ function formatLoadedFile(plasmid, id) {
28
27
  }
29
28
 
30
29
  export const useAssembler = () => {
31
- const httpClient = useHttpClient();
32
- const backendRoute = useBackendRoute();
30
+ const httpClient = useBackendHttpClient();
33
31
 
34
32
  const requestSources = useCallback(async (assemblerOutput) => {
35
33
  const processedOutput = []
@@ -41,7 +39,7 @@ export const useAssembler = () => {
41
39
  processedOutput[pos].push(formatLoadedFile(plasmid, pos + 1))
42
40
  } else {
43
41
  const { source } = plasmid;
44
- const url = backendRoute(classNameToEndPointMap[source.type])
42
+ const url = classNameToEndPointMap[source.type]
45
43
  const {data} = await httpClient.post(url, source)
46
44
  if (data.sources.length !== 1) {
47
45
  console.error('Expected 1 source, got ' + data.sources.length)
@@ -59,7 +57,7 @@ export const useAssembler = () => {
59
57
  }
60
58
  }
61
59
  return processedOutput
62
- }, [ httpClient, backendRoute ])
60
+ }, [ httpClient ])
63
61
 
64
62
 
65
63
  const requestAssemblies = useCallback(async (requestedSources, enzymes=['BsaI']) => {
@@ -67,7 +65,7 @@ export const useAssembler = () => {
67
65
  const assemblies = arrayCombinations(requestedSources);
68
66
  const output = []
69
67
  for (let assembly of assemblies) {
70
- const url = backendRoute('restriction_and_ligation')
68
+ const url = 'restriction_and_ligation'
71
69
  const config = {
72
70
  params: {
73
71
  circular_only: true,
@@ -102,7 +100,7 @@ export const useAssembler = () => {
102
100
  }
103
101
  }
104
102
  return output;
105
- }, [ httpClient, backendRoute ])
103
+ }, [ httpClient ])
106
104
 
107
105
  return { requestSources, requestAssemblies }
108
106
  }
@@ -19,8 +19,7 @@ import {
19
19
  TextField,
20
20
  } from '@mui/material';
21
21
  import error2String from '@opencloning/utils/error2String';
22
- import useBackendRoute from '../../hooks/useBackendRoute';
23
- import useHttpClient from '../../hooks/useHttpClient';
22
+ import useBackendHttpClient from '../../hooks/useBackendHttpClient';
24
23
  import { featureToGenbankLocationString, mergeStates } from '@opencloning/utils';
25
24
  import { useDispatch, useStore } from 'react-redux';
26
25
  import { cloningActions } from '@opencloning/store/cloning';
@@ -77,8 +76,7 @@ function categoryPrefixSuffixValid(category, prefix, suffix) {
77
76
  }
78
77
 
79
78
  export default function DomesticateDialog({ open, onClose, initialContext, mainSequenceId }) {
80
- const httpClient = useHttpClient();
81
- const backendRoute = useBackendRoute();
79
+ const httpClient = useBackendHttpClient();
82
80
  const store = useStore();
83
81
 
84
82
  const location = React.useMemo(() => featureToGenbankLocationString(initialContext.annotation), [initialContext]);
@@ -131,7 +129,7 @@ export default function DomesticateDialog({ open, onClose, initialContext, mainS
131
129
  enzymes,
132
130
  };
133
131
  try {
134
- const url = backendRoute('batch_cloning/domesticate');
132
+ const url = 'batch_cloning/domesticate';
135
133
  const { data } = await httpClient.post(url, body);
136
134
 
137
135
  const { mergedState, idShift } = mergeStates(data, store.getState().cloning);
@@ -1,18 +1,16 @@
1
1
  import * as React from 'react';
2
2
  import GetRequestMultiSelect from './GetRequestMultiSelect';
3
- import useBackendRoute from '../../hooks/useBackendRoute';
4
- import useHttpClient from '../../hooks/useHttpClient';
3
+ import useBackendHttpClient from '../../hooks/useBackendHttpClient';
5
4
 
6
5
  function EnzymeMultiSelect({ setEnzymes, label = 'Enzymes used', multiple = true }) {
7
- const backendRoute = useBackendRoute();
8
- const url = backendRoute('restriction_enzyme_list');
6
+ const url = 'restriction_enzyme_list';
9
7
  const getOptionsFromResponse = React.useCallback((data) => data.enzyme_names, []);
10
8
  const messages = React.useMemo(() => ({
11
9
  loadingMessage: 'retrieving enzymes...',
12
10
  errorMessage: 'Could not retrieve enzymes from server',
13
11
  }), []);
14
12
  const onChange = React.useCallback((value) => setEnzymes(value), [setEnzymes]);
15
- const httpClient = useHttpClient();
13
+ const httpClient = useBackendHttpClient();
16
14
 
17
15
  return (
18
16
 
@@ -10,6 +10,7 @@ import { TextField } from '@mui/material';
10
10
  * @param {string} placeholder - Placeholder text
11
11
  * @param {Function} getQuery - (value) => ({ queryKey, queryFn, ... }) — returns a query config for the given value (use value in queryKey so each input has its own cache).
12
12
  * @param {Function} onChange - Callback when validation succeeds; receives (queryData). Called with null when value is empty or validation fails.
13
+ * @param {Function} onValidationStateChange - Optional callback with ({ isChecking, isValid, hasValue }) when validation lifecycle changes.
13
14
  * @param {string} value - Controlled value (optional)
14
15
  * @param {number} debounceDelay - Delay in ms before running the query (default: 500)
15
16
  * @param {string} successMessage - Optional message to show when validation succeeds (default: ' ')
@@ -20,6 +21,7 @@ export default function TextFieldQueryValidated({
20
21
  placeholder,
21
22
  getQuery,
22
23
  onChange,
24
+ onValidationStateChange,
23
25
  debounceDelay = 500,
24
26
  successMessage = ' ',
25
27
  validatingMessage = 'Validating...',
@@ -44,24 +46,40 @@ export default function TextFieldQueryValidated({
44
46
  [getQuery, debouncedValue],
45
47
  );
46
48
 
47
- const { data: validationError, isLoading, error, isSuccess } = useQuery({
49
+ const { data: validationError, isLoading, isFetching, error, isSuccess } = useQuery({
48
50
  ...queryConfig,
49
51
  enabled: debouncedValue.length > 0 && (queryConfig.enabled !== false),
50
52
  });
51
53
 
54
+ const isDebouncing = localValue !== debouncedValue;
55
+ const isChecking =
56
+ localValue.length > 0 &&
57
+ (isDebouncing || (debouncedValue.length > 0 && (isLoading || isFetching)));
58
+ const isValid =
59
+ localValue.length > 0 &&
60
+ localValue === debouncedValue &&
61
+ isSuccess &&
62
+ !validationError &&
63
+ !error;
64
+
65
+ const hasValue = localValue.length > 0;
66
+
67
+ useEffect(() => {
68
+ onValidationStateChange?.({ isChecking, isValid, hasValue });
69
+ }, [isChecking, isValid, hasValue, onValidationStateChange]);
52
70
 
53
71
  useEffect(() => {
54
72
  if (debouncedValue.length === 0) {
55
73
  onChange('');
56
74
  return;
57
75
  }
58
- if (isSuccess && !validationError) {
76
+ if (localValue === debouncedValue && isSuccess && !validationError) {
59
77
  onChange(debouncedValue);
60
78
  }
61
79
  if (error || validationError) {
62
80
  onChange('');
63
81
  }
64
- }, [debouncedValue, isSuccess, validationError, error, onChange]);
82
+ }, [localValue, debouncedValue, isSuccess, validationError, error, onChange]);
65
83
 
66
84
  const handleChange = (e) => {
67
85
  onChange('');
@@ -69,7 +87,7 @@ export default function TextFieldQueryValidated({
69
87
  };
70
88
 
71
89
  let helperText = ' ';
72
- if (isLoading) {
90
+ if (isChecking) {
73
91
  helperText = validatingMessage;
74
92
  } else if (validationError) {
75
93
  helperText = validationError;
@@ -4,7 +4,7 @@ import { Menu as MenuIcon } from '@mui/icons-material';
4
4
  import './MainAppBar.css';
5
5
  import { useDispatch } from 'react-redux';
6
6
  import ButtonWithMenu from './ButtonWithMenu';
7
- import { downloadCloningStrategyAsSvg, formatTemplate, loadHistoryFile, loadFilesToSessionStorage } from '@opencloning/utils/readNwrite';
7
+ import { downloadCloningStrategyAsSvg, formatTemplate } from '@opencloning/utils/readNwrite';
8
8
  import SelectExampleDialog from './SelectExampleDialog';
9
9
  import SelectTemplateDialog from './SelectTemplateDialog';
10
10
  import FeedbackDialog from './FeedbackDialog';
@@ -13,11 +13,14 @@ import VersionDialog from './VersionDialog';
13
13
  import useCloningAlerts from '../../hooks/useCloningAlerts';
14
14
  import DownloadCloningStrategyDialog from '../DownloadCloningStrategyDialog';
15
15
  import LoadCloningHistoryWrapper from '../LoadCloningHistoryWrapper';
16
- import useValidateState from '../../hooks/useValidateState';
16
+ import useCloningHistoryLoader from '../../hooks/useCloningHistoryLoader';
17
17
  import GithubCornerRight from './GithubCornerRight';
18
- import useHttpClient from '../../hooks/useHttpClient';
18
+ import { getExternalHttpClient } from '@opencloning/utils/httpClients';
19
19
 
20
- const { setCurrentTab, setState: setCloningState } = cloningActions;
20
+ // External urls (not the backend): no auth token, only whitelisted urls
21
+ const httpClient = getExternalHttpClient();
22
+
23
+ const { setCurrentTab } = cloningActions;
21
24
 
22
25
  function MainAppBar() {
23
26
  const [openExampleDialog, setOpenExampleDialog] = React.useState(false);
@@ -28,11 +31,10 @@ function MainAppBar() {
28
31
  const [openVersionDialog, setOpenVersionDialog] = React.useState(false);
29
32
  const [anchorEl, setAnchorEl] = React.useState(null);
30
33
  const openMenu = Boolean(anchorEl);
31
- const httpClient = useHttpClient();
32
34
 
33
35
  const dispatch = useDispatch();
34
36
  const { addAlert } = useCloningAlerts();
35
- const validateState = useValidateState();
37
+ const { applyCloningStrategy, loadHistoryFromFile } = useCloningHistoryLoader();
36
38
 
37
39
  // Hidden input field, used to load files.
38
40
  const fileInputRef = React.useRef(null);
@@ -52,26 +54,30 @@ function MainAppBar() {
52
54
  setOpenExampleDialog(false);
53
55
  setOpenTemplateDialog(false);
54
56
  if (url) {
55
- let data;
57
+ const fileName = url.split('/').pop();
58
+ let file;
56
59
  if (url.endsWith('.zip')) {
57
- // For zip files, get as blob and process with loadHistoryFile
58
60
  const { data: blob } = await httpClient.get(url, { responseType: 'blob' });
59
- const fileName = url.split('/').pop();
60
- // eslint-disable-next-line no-undef
61
- const file = new File([blob], fileName);
62
- const { cloningStrategy, verificationFiles } = await loadHistoryFile(file);
63
- data = await validateState(cloningStrategy);
64
- await loadFilesToSessionStorage(verificationFiles, 0);
61
+ file = new File([blob], fileName);
65
62
  } else {
66
- // For JSON files, get as JSON
67
- let { data: jsonData } = await httpClient.get(url);
68
- data = await validateState(jsonData);
63
+ const { data: jsonData } = await httpClient.get(url);
64
+ file = new File([JSON.stringify(jsonData)], fileName);
65
+ }
66
+ const prepared = await loadHistoryFromFile(file, {
67
+ onError: (message) => addAlert({ message, severity: 'error' }),
68
+ });
69
+ if (!prepared) {
70
+ return;
69
71
  }
72
+ let strategy = prepared.cloningStrategy;
70
73
  if (isTemplate) {
71
- data = formatTemplate(data, url);
74
+ strategy = formatTemplate(strategy, url);
72
75
  }
73
- dispatch(setCloningState(data));
74
- dispatch(setCurrentTab(0))
76
+ await applyCloningStrategy(strategy, {
77
+ verificationFiles: prepared.verificationFiles,
78
+ mode: 'replace',
79
+ });
80
+ dispatch(setCurrentTab(0));
75
81
  }
76
82
  };
77
83