@opencloning/ui 1.9.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/package.json +5 -4
- package/src/components/CloningSpeedDial.jsx +3 -5
- package/src/components/EditSequenceNameDialog.jsx +3 -5
- package/src/components/ExternalServicesStatusCheck.jsx +3 -5
- package/src/components/LoadCloningHistoryWrapper.jsx +21 -46
- package/src/components/SequenceViewer.jsx +12 -0
- package/src/components/assembler/Assembler.jsx +4 -6
- package/src/components/assembler/ExistingSyntaxDialog.jsx +2 -2
- package/src/components/assembler/useAssembler.js +6 -8
- package/src/components/domesticate/DomesticateDialog.jsx +3 -5
- package/src/components/form/EnzymeMultiSelect.jsx +3 -5
- package/src/components/form/TextFieldQueryValidated.jsx +22 -4
- package/src/components/navigation/MainAppBar.jsx +26 -20
- package/src/components/navigation/SelectTemplateDialog.jsx +4 -2
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +4 -6
- package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +4 -6
- package/src/components/sources/CollectionSource.jsx +2 -2
- package/src/components/sources/Source.jsx +32 -32
- package/src/components/sources/SourceAnnotation.jsx +2 -2
- package/src/components/sources/SourceAssembly.jsx +7 -7
- package/src/components/sources/SourceDatabase.jsx +2 -2
- package/src/components/sources/SourceFile.jsx +21 -51
- package/src/components/sources/SourceGenomeRegion.cy.jsx +46 -0
- package/src/components/sources/SourceGenomeRegion.jsx +29 -15
- package/src/components/sources/SourceHomologousRecombination.jsx +3 -3
- package/src/components/sources/SourceKnownGenomeRegion.jsx +3 -3
- package/src/components/sources/SourceManuallyTyped.jsx +2 -2
- package/src/components/sources/SourcePCRorHybridization.jsx +3 -3
- package/src/components/sources/SourcePolymeraseExtension.jsx +2 -2
- package/src/components/sources/SourceRepositoryId.jsx +4 -4
- package/src/components/sources/SourceRestriction.jsx +2 -2
- package/src/components/sources/SourceReverseComplement.jsx +2 -2
- package/src/components/sources/SourceServerStaticFile.jsx +3 -3
- package/src/components/verification/VerificationFileDialog.cy.jsx +17 -21
- package/src/components/verification/VerificationFileDialog.jsx +12 -11
- package/src/hooks/index.js +1 -0
- package/src/hooks/useBackendHttpClient.js +17 -0
- package/src/hooks/useCloningHistoryLoader.js +161 -0
- package/src/hooks/useGatewaySites.js +3 -5
- package/src/hooks/useInitializeApp.js +3 -4
- package/src/hooks/useLoadDatabaseFile.js +17 -34
- package/src/hooks/useSequencingAlignment.js +4 -6
- package/src/hooks/useServerStaticFiles.js +2 -2
- package/src/hooks/{useBackendAPI.js → useSourceRequest.js} +6 -8
- package/src/hooks/useStoreEditor.js +2 -1
- package/src/hooks/useUrlParamsLoader.js +29 -23
- package/src/hooks/useValidateState.js +4 -6
- package/src/version.js +1 -1
- package/src/hooks/useBackendRoute.js +0 -23
- package/src/hooks/useHttpClient.js +0 -20
- package/src/hooks/useSnapgeneHistoryEndpoint.js +0 -72
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
import { batch, useDispatch, useStore } from 'react-redux';
|
|
2
1
|
import { jsonToGenbank } from '@teselagen/bio-parsers';
|
|
3
2
|
import useValidateState from './useValidateState';
|
|
4
|
-
import { convertToTeselaJson,
|
|
3
|
+
import { convertToTeselaJson, parseHistoryFile } from '@opencloning/utils/readNwrite';
|
|
5
4
|
import { getIdsOfSequencesWithoutChildSource } from '@opencloning/store/cloning_utils';
|
|
6
|
-
import
|
|
7
|
-
import { cloningActions } from '@opencloning/store/cloning';
|
|
5
|
+
import useCloningHistoryLoader from './useCloningHistoryLoader';
|
|
8
6
|
import useDatabase from './useDatabase';
|
|
7
|
+
import { useStore } from 'react-redux';
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export default function useLoadDatabaseFile({ source, sendPostRequest, setHistoryFileError }) {
|
|
13
|
-
const dispatch = useDispatch();
|
|
9
|
+
export default function useLoadDatabaseFile({ source, sendSourceRequest, setHistoryFileError }) {
|
|
14
10
|
const validateState = useValidateState();
|
|
15
11
|
const store = useStore();
|
|
16
12
|
const database = useDatabase();
|
|
13
|
+
const { applyCloningStrategy } = useCloningHistoryLoader();
|
|
17
14
|
|
|
18
15
|
const loadDatabaseFile = async (file, databaseId, ancestors = false) => {
|
|
19
16
|
if (file.name.endsWith('.json')) {
|
|
20
17
|
let cloningStrategy;
|
|
21
18
|
try {
|
|
22
|
-
({ cloningStrategy } = await
|
|
19
|
+
({ cloningStrategy } = await parseHistoryFile(file));
|
|
23
20
|
// If the cloning strategy should end on a single sequence, set the databaseId for the right source
|
|
24
21
|
const terminalSequences = getIdsOfSequencesWithoutChildSource(cloningStrategy.sources, cloningStrategy.sequences);
|
|
25
22
|
if (terminalSequences.length === 1) {
|
|
@@ -29,7 +26,7 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
29
26
|
// When importing sources that had inputs that we don't want to load, we turn them into database sources
|
|
30
27
|
const allSequenceIds = [...cloningStrategy.sequences.map((e) => e.id), ...cloningStrategy.primers.map((e) => e.id)];
|
|
31
28
|
cloningStrategy.sources = cloningStrategy.sources.map((s) => {
|
|
32
|
-
if (s.input.some(({sequence}) => !allSequenceIds.includes(sequence))) {
|
|
29
|
+
if (s.input.some(({ sequence }) => !allSequenceIds.includes(sequence))) {
|
|
33
30
|
return { id: s.id, type: 'DatabaseSource', input: [], database_id: s.database_id };
|
|
34
31
|
}
|
|
35
32
|
return s;
|
|
@@ -66,7 +63,6 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
66
63
|
setHistoryFileError(e.message);
|
|
67
64
|
return;
|
|
68
65
|
}
|
|
69
|
-
// This one won't have the source.id deleted
|
|
70
66
|
const prevState = store.getState().cloning;
|
|
71
67
|
const { backendVersion, schemaVersion, frontendVersion } = prevState.appInfo;
|
|
72
68
|
cloningStrategy.backend_version = backendVersion;
|
|
@@ -74,28 +70,15 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
74
70
|
cloningStrategy.frontend_version = frontendVersion;
|
|
75
71
|
cloningStrategy = await validateState(cloningStrategy);
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
({ mergedState } = graftState(cloningStrategy, cloningState, source.id));
|
|
87
|
-
} else {
|
|
88
|
-
({ mergedState } = mergeStates(cloningStrategy, cloningState));
|
|
89
|
-
}
|
|
90
|
-
dispatch(setCloningState(mergedState));
|
|
91
|
-
} catch (e) {
|
|
92
|
-
setHistoryFileError(e.message);
|
|
93
|
-
console.error(e);
|
|
94
|
-
if (!ancestors) {
|
|
95
|
-
dispatch(setCloningState(prevState));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
73
|
+
try {
|
|
74
|
+
await applyCloningStrategy(cloningStrategy, {
|
|
75
|
+
mode: ancestors ? 'graft' : 'merge',
|
|
76
|
+
source,
|
|
77
|
+
onError: setHistoryFileError,
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
// Error already reported via onError; state restored in applyCloningStrategy when applicable
|
|
81
|
+
}
|
|
99
82
|
} else {
|
|
100
83
|
const requestData = new FormData();
|
|
101
84
|
requestData.append('file', file);
|
|
@@ -105,7 +88,7 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
105
88
|
},
|
|
106
89
|
};
|
|
107
90
|
const modifySource = (s) => ({ ...s, database_id: databaseId });
|
|
108
|
-
|
|
91
|
+
sendSourceRequest({ endpoint: 'read_from_file', requestData, config, source, modifySource });
|
|
109
92
|
}
|
|
110
93
|
};
|
|
111
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
|
|
5
|
-
import useHttpClient from './useHttpClient';
|
|
4
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
6
5
|
|
|
7
6
|
export default function useSequencingAlignment({ onError } = {}) {
|
|
8
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
4
|
+
import useBackendHttpClient from './useBackendHttpClient';
|
|
6
5
|
|
|
7
|
-
export default function
|
|
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
|
|
12
|
-
const httpClient = useHttpClient();
|
|
10
|
+
const httpClient = useBackendHttpClient();
|
|
13
11
|
const { addAlert } = useCloningAlerts();
|
|
14
12
|
|
|
15
|
-
const
|
|
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 =
|
|
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,
|
|
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(
|
|
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,12 +4,17 @@ 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';
|
|
8
|
+
import useCloningHistoryLoader from './useCloningHistoryLoader';
|
|
9
9
|
import { formatSequenceLocationString, getUrlParameters } from '@opencloning/utils/other';
|
|
10
|
-
import { formatTemplate
|
|
10
|
+
import { formatTemplate } from '@opencloning/utils/readNwrite';
|
|
11
|
+
import { encodePathSegment } from '@opencloning/utils/urlUtils';
|
|
12
|
+
import { getExternalHttpClient } from '@opencloning/utils/httpClients';
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
// For public urls (not the backend)
|
|
15
|
+
const httpClient = getExternalHttpClient();
|
|
16
|
+
|
|
17
|
+
const { updateSource } = cloningActions;
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* Hook to load sequences from URL parameters
|
|
@@ -20,9 +25,9 @@ export default function useUrlParamsLoader() {
|
|
|
20
25
|
const database = useDatabase();
|
|
21
26
|
const { addAlert } = useCloningAlerts();
|
|
22
27
|
const setHistoryFileError = (e) => addAlert({ message: e, severity: 'error' });
|
|
23
|
-
const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 },
|
|
28
|
+
const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 }, sendSourceRequest: null, setHistoryFileError });
|
|
24
29
|
const validateState = useValidateState();
|
|
25
|
-
const
|
|
30
|
+
const { applyCloningStrategy, loadHistoryFromFile } = useCloningHistoryLoader();
|
|
26
31
|
|
|
27
32
|
useEffect(() => {
|
|
28
33
|
async function loadSequenceFromUrlParams() {
|
|
@@ -44,23 +49,25 @@ export default function useUrlParamsLoader() {
|
|
|
44
49
|
}
|
|
45
50
|
} else if (urlParams.source === 'example' && urlParams.example) {
|
|
46
51
|
try {
|
|
47
|
-
const url = `${import.meta.env.BASE_URL}examples/${urlParams.example}`;
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const file = new File([
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
await loadFilesToSessionStorage(verificationFiles, 0);
|
|
52
|
+
const url = `${import.meta.env.BASE_URL}examples/${encodePathSegment(urlParams.example)}`;
|
|
53
|
+
const fileName = urlParams.example;
|
|
54
|
+
if (fileName.endsWith('.zip')) {
|
|
55
|
+
const resp = await httpClient.get(url, { responseType: 'blob' });
|
|
56
|
+
if (resp.headers['content-type'] !== 'application/zip') {
|
|
57
|
+
throw new Error('Not found');
|
|
58
|
+
}
|
|
59
|
+
const file = new File([resp.data], fileName);
|
|
60
|
+
const prepared = await loadHistoryFromFile(file, { onError: setHistoryFileError });
|
|
61
|
+
await prepared?.replace();
|
|
58
62
|
} else {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
const resp = await httpClient.get(url);
|
|
64
|
+
if (resp.headers['content-type'] !== 'application/json') {
|
|
65
|
+
throw new Error('Not found');
|
|
66
|
+
}
|
|
67
|
+
const file = new File([JSON.stringify(resp.data)], fileName);
|
|
68
|
+
const prepared = await loadHistoryFromFile(file, { onError: setHistoryFileError });
|
|
69
|
+
await prepared?.replace();
|
|
62
70
|
}
|
|
63
|
-
dispatch(setCloningState(data));
|
|
64
71
|
} catch (error) {
|
|
65
72
|
addAlert({
|
|
66
73
|
message: 'Error loading example',
|
|
@@ -71,12 +78,12 @@ export default function useUrlParamsLoader() {
|
|
|
71
78
|
} else if (urlParams.source === 'template' && urlParams.template && urlParams.key) {
|
|
72
79
|
try {
|
|
73
80
|
const baseUrl = 'https://assets.opencloning.org/OpenCloning-submission';
|
|
74
|
-
const url = `${baseUrl}/processed/${urlParams.key}/templates/${urlParams.template}`;
|
|
81
|
+
const url = `${baseUrl}/processed/${encodePathSegment(urlParams.key)}/templates/${encodePathSegment(urlParams.template)}`;
|
|
75
82
|
const { data } = await httpClient.get(url);
|
|
76
83
|
const validatedData = await validateState(data);
|
|
77
84
|
const newState = formatTemplate(validatedData, url);
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
await applyCloningStrategy(newState, { mode: 'replace' });
|
|
80
87
|
} catch (error) {
|
|
81
88
|
addAlert({
|
|
82
89
|
message: 'Error loading template',
|
|
@@ -146,4 +153,3 @@ export default function useUrlParamsLoader() {
|
|
|
146
153
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
154
|
}, []);
|
|
148
155
|
}
|
|
149
|
-
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import useCloningAlerts from './useCloningAlerts';
|
|
3
|
-
import
|
|
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 =
|
|
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(
|
|
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,
|
|
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 = "
|
|
2
|
+
export const version = "2.0.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
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import getHttpClient, { getAuthenticatedHttpClient } from '@opencloning/utils/getHttpClient';
|
|
3
|
-
import { useConfig } from './useConfig';
|
|
4
|
-
|
|
5
|
-
export default function useHttpClient() {
|
|
6
|
-
const { backendUrl, requiresBackendAuth = false } = useConfig();
|
|
7
|
-
|
|
8
|
-
// Memoize the client creation and interceptor setup
|
|
9
|
-
const apiClient = React.useMemo(() => {
|
|
10
|
-
const createHttpClient = requiresBackendAuth ? getAuthenticatedHttpClient : getHttpClient;
|
|
11
|
-
|
|
12
|
-
if (!backendUrl) {
|
|
13
|
-
// Return a client without backend URL if config not loaded yet
|
|
14
|
-
return createHttpClient([]);
|
|
15
|
-
}
|
|
16
|
-
return createHttpClient([backendUrl]);
|
|
17
|
-
}, [backendUrl, requiresBackendAuth]);
|
|
18
|
-
|
|
19
|
-
return apiClient;
|
|
20
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import useBackendRoute from './useBackendRoute';
|
|
3
|
-
import useHttpClient from './useHttpClient';
|
|
4
|
-
import useCloningAlerts from './useCloningAlerts';
|
|
5
|
-
import { batch, useDispatch, useStore } from 'react-redux';
|
|
6
|
-
import { mergeStates } from '@opencloning/utils/network';
|
|
7
|
-
import { cloningActions } from '@opencloning/store/cloning';
|
|
8
|
-
|
|
9
|
-
const { setState: setCloningState, deleteSourceAndItsChildren } = cloningActions;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Custom React hook that provides a function to load and process a SnapGene .dna file's cloning history
|
|
14
|
-
* via the backend endpoint. If successful, updates the Redux cloning state (merging or replacing as appropriate)
|
|
15
|
-
* and handles warnings. On failure, triggers a user alert.
|
|
16
|
-
*
|
|
17
|
-
* @returns {Object} An object with a single method `loadSnapgeneHistory(file)`:
|
|
18
|
-
* - {Promise<boolean>} loadSnapgeneHistory(file): Attempt to load and merge/replace SnapGene history.
|
|
19
|
-
* Returns true on successfully parsing the SnapGene file, otherwise returns false and triggers a warning alert.
|
|
20
|
-
*/
|
|
21
|
-
export default function useSnapgeneHistoryEndpoint() {
|
|
22
|
-
const backendRoute = useBackendRoute();
|
|
23
|
-
const httpClient = useHttpClient();
|
|
24
|
-
const { addAlert } = useCloningAlerts();
|
|
25
|
-
const dispatch = useDispatch();
|
|
26
|
-
const store = useStore();
|
|
27
|
-
const url = backendRoute('read_snapgene_history');
|
|
28
|
-
|
|
29
|
-
const loadSnapgeneHistory = React.useCallback(async (file, sourceIdToDelete = null) => {
|
|
30
|
-
const cloningState = store.getState().cloning;
|
|
31
|
-
const formData = new FormData();
|
|
32
|
-
formData.append('file', file);
|
|
33
|
-
const config = {
|
|
34
|
-
headers: {
|
|
35
|
-
'content-type': 'multipart/form-data',
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
// If it fails to parse the history return false, true otherwise
|
|
39
|
-
try {
|
|
40
|
-
const resp = await httpClient.post(url, formData, config);
|
|
41
|
-
const { data: cloningStrategy } = resp;
|
|
42
|
-
let mergedState;
|
|
43
|
-
if (!(cloningState.sources.length === 1 && cloningState.sources[0].type === null)) {
|
|
44
|
-
({ mergedState } = mergeStates(cloningStrategy, cloningState));
|
|
45
|
-
} else {
|
|
46
|
-
// If there is only one source and it is empty, replace the whole state
|
|
47
|
-
mergedState = cloningStrategy;
|
|
48
|
-
}
|
|
49
|
-
if (resp.headers['x-warning']) {
|
|
50
|
-
addAlert({
|
|
51
|
-
message: `Not all SnapGene history was parsed: ${resp.headers['x-warning']}`,
|
|
52
|
-
severity: 'warning',
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
batch(() => {
|
|
56
|
-
dispatch(setCloningState(mergedState));
|
|
57
|
-
if (sourceIdToDelete !== null) {
|
|
58
|
-
console.log('deleting source', sourceIdToDelete);
|
|
59
|
-
dispatch(deleteSourceAndItsChildren(sourceIdToDelete));
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
addAlert({
|
|
65
|
-
message: 'Failed to parse the history from the SnapGene file, will only read the sequence.',
|
|
66
|
-
severity: 'warning',
|
|
67
|
-
});
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}, [httpClient, addAlert, dispatch, store, url]);
|
|
71
|
-
return { loadSnapgeneHistory };
|
|
72
|
-
}
|