@opencloning/ui 1.7.3 → 1.9.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 +102 -0
- package/package.json +20 -14
- package/src/components/AppAlerts.jsx +7 -7
- package/src/components/CloningHistory.jsx +2 -0
- package/src/components/CloningSpeedDial.jsx +102 -0
- package/src/components/DownloadCloningStrategyDialog.jsx +2 -2
- package/src/components/LoadCloningHistoryWrapper.jsx +2 -2
- package/src/components/MainSequenceEditor.jsx +41 -34
- package/src/components/NetworkNode.jsx +35 -33
- package/src/components/OpenCloning.jsx +4 -3
- package/src/components/SequenceViewer.cy.jsx +27 -0
- package/src/components/SequenceViewer.jsx +211 -0
- package/src/components/annotation/AnnotationChangedAlert.jsx +26 -0
- package/src/components/annotation/useUpdateAnnotationInMainSequence.js +2 -2
- package/src/components/assembler/Assembler.jsx +3 -3
- package/src/components/domesticate/DomesticateDialog.jsx +274 -0
- package/src/components/form/GetRequestMultiSelect.jsx +0 -1
- package/src/components/form/PostRequestSelect.jsx +0 -1
- package/src/components/form/QuerySelect.jsx +128 -0
- package/src/components/form/QueryStatusWrapper.jsx +28 -0
- package/src/components/form/SubmitToDatabaseDialog.jsx +63 -58
- package/src/components/form/TextFieldQueryValidated.jsx +93 -0
- package/src/components/index.js +4 -0
- package/src/components/navigation/FeedbackDialog.jsx +1 -1
- package/src/components/navigation/MainAppBar.jsx +5 -5
- package/src/components/primers/import_primers/ImportPrimersButton.jsx +2 -2
- package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +6 -3
- package/src/components/sources/FinishedSource.jsx +3 -0
- package/src/components/sources/MultipleOutputsSelector.cy.jsx +95 -0
- package/src/components/sources/MultipleOutputsSelector.jsx +32 -19
- package/src/components/sources/RecombinaseList.cy.jsx +1 -1
- package/src/components/sources/RecombinaseList.jsx +1 -1
- package/src/components/sources/SourceAssembly.jsx +36 -2
- package/src/components/sources/SourceRepositoryId.jsx +0 -1
- package/src/components/sources/SourceTypeSelector.jsx +1 -1
- package/src/components/verification/ImportSequencingFilesInput.jsx +17 -0
- package/src/components/verification/LoadFromDatabaseButton.jsx +17 -10
- package/src/components/verification/VerificationFileDialog.jsx +16 -22
- package/src/components/verification/index.js +1 -0
- package/src/config/defaultMainEditorProps.js +1 -0
- package/src/hooks/useAlerts.js +3 -4
- package/src/hooks/useBackendAPI.js +5 -2
- package/src/hooks/useCloningAlerts.js +7 -0
- package/src/hooks/useDebouncedSearchQuery.js +42 -0
- package/src/hooks/useHttpClient.js +7 -5
- package/src/hooks/useLoadDatabaseFile.js +6 -2
- package/src/hooks/useSequencingAlignment.js +26 -0
- package/src/hooks/useSnapgeneHistoryEndpoint.js +3 -3
- package/src/hooks/useStoreEditor.js +10 -68
- package/src/hooks/useUrlParamsLoader.js +2 -2
- package/src/hooks/useValidateState.js +3 -3
- package/src/index.css +3 -11
- package/src/providers/ConfigProvider.jsx +1 -0
- package/src/version.js +1 -1
|
@@ -7,28 +7,45 @@ import OverhangsDisplay from '../OverhangsDisplay';
|
|
|
7
7
|
import SubSequenceDisplayer from './SubSequenceDisplayer';
|
|
8
8
|
import AssemblyPlanDisplayer from './AssemblyPlanDisplayer';
|
|
9
9
|
|
|
10
|
-
function MultipleOutputsSelector({
|
|
10
|
+
function MultipleOutputsSelector({
|
|
11
|
+
sources,
|
|
12
|
+
sequences,
|
|
13
|
+
sourceId,
|
|
14
|
+
onFragmentChosen,
|
|
15
|
+
mockable = {}, // for testing
|
|
16
|
+
}) {
|
|
17
|
+
const [selectedOutput, setSelectedOutput] = React.useState(0);
|
|
18
|
+
const {
|
|
19
|
+
SubSequenceDisplayerComponent = SubSequenceDisplayer,
|
|
20
|
+
AssemblyPlanDisplayerComponent = AssemblyPlanDisplayer,
|
|
21
|
+
CircularOrLinearViewComponent = SimpleCircularOrLinearView,
|
|
22
|
+
OverhangsDisplayComponent = OverhangsDisplay,
|
|
23
|
+
convertSequence = convertToTeselaJson,
|
|
24
|
+
} = mockable;
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => setSelectedOutput(0), [sources]);
|
|
27
|
+
|
|
11
28
|
// If the output is already set or the list of outputs is empty, do not show this element
|
|
12
29
|
if (sources.length === 0) { return null; }
|
|
13
30
|
|
|
14
|
-
// selectedOutput is a local property, until you commit the step by clicking
|
|
15
|
-
const [selectedOutput, setSelectedOutput] = React.useState(0);
|
|
16
|
-
|
|
17
31
|
// Functions called to move between outputs of a restriction reaction
|
|
18
|
-
const incrementSelectedOutput = () => setSelectedOutput(
|
|
19
|
-
(
|
|
20
|
-
);
|
|
21
|
-
const decreaseSelectedOutput = () => setSelectedOutput((
|
|
32
|
+
const incrementSelectedOutput = () => setSelectedOutput((current) => (
|
|
33
|
+
(current + 1) % sources.length
|
|
34
|
+
));
|
|
35
|
+
const decreaseSelectedOutput = () => setSelectedOutput((current) => (
|
|
36
|
+
(current !== 0) ? (current - 1) : sources.length - 1
|
|
37
|
+
));
|
|
22
38
|
|
|
23
39
|
// The function to pick the fragment as the output, and execute the step
|
|
24
40
|
const chooseFragment = (e) => {
|
|
25
41
|
e.preventDefault();
|
|
26
|
-
onFragmentChosen(selectedOutput);
|
|
42
|
+
onFragmentChosen(Math.min(selectedOutput, sources.length - 1));
|
|
27
43
|
};
|
|
28
44
|
|
|
29
45
|
const editorName = `source_editor_${sourceId}`;
|
|
46
|
+
const safeSelectedOutput = Math.min(selectedOutput, sources.length - 1);
|
|
30
47
|
|
|
31
|
-
const seq =
|
|
48
|
+
const seq = convertSequence(sequences[safeSelectedOutput]);
|
|
32
49
|
|
|
33
50
|
return (
|
|
34
51
|
<div className="multiple-output-selector">
|
|
@@ -36,21 +53,17 @@ function MultipleOutputsSelector({ sources, sequences, sourceId, onFragmentChose
|
|
|
36
53
|
<IconButton onClick={decreaseSelectedOutput} type="button" sx={{ height: 'fit-content' }}>
|
|
37
54
|
<ArrowBack />
|
|
38
55
|
</IconButton>
|
|
39
|
-
{
|
|
40
|
-
{' '}
|
|
41
|
-
/
|
|
42
|
-
{' '}
|
|
43
|
-
{sources.length}
|
|
56
|
+
{`${safeSelectedOutput + 1} / ${sources.length}`}
|
|
44
57
|
<IconButton onClick={incrementSelectedOutput} type="button" sx={{ height: 'fit-content' }}>
|
|
45
58
|
<ArrowForward />
|
|
46
59
|
</IconButton>
|
|
47
60
|
</div>
|
|
48
61
|
|
|
49
62
|
<div className="fragment-picker">
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<
|
|
63
|
+
<SubSequenceDisplayerComponent {...{ source: sources[safeSelectedOutput], sourceId }} />
|
|
64
|
+
<AssemblyPlanDisplayerComponent {...{ source: sources[safeSelectedOutput] }} />
|
|
65
|
+
<CircularOrLinearViewComponent {...{ sequenceData: seq, editorName, height: 'auto' }} />
|
|
66
|
+
<OverhangsDisplayComponent {...{ sequenceData: seq, sequence: sequences[safeSelectedOutput] }} />
|
|
54
67
|
</div>
|
|
55
68
|
<form onSubmit={chooseFragment}>
|
|
56
69
|
<Button fullWidth type="submit" variant="contained">Choose product</Button>
|
|
@@ -85,7 +85,7 @@ describe('<RecombinaseList />', () => {
|
|
|
85
85
|
const setRecombinasesSpy = cy.spy().as('setRecombinasesSpy');
|
|
86
86
|
cy.mount(<RecombinaseList recombinases={[]} setRecombinases={setRecombinasesSpy} />);
|
|
87
87
|
|
|
88
|
-
setInputValue('
|
|
88
|
+
setInputValue('Recombinase name (optional)', 'MyRecombinase', 'body');
|
|
89
89
|
setInputValue('Site 1', 'AAaaTTC', 'body');
|
|
90
90
|
setInputValue('Site 1 name', 'loxP', 'body');
|
|
91
91
|
setInputValue('Site 2', 'CCaaGC', 'body');
|
|
@@ -65,7 +65,7 @@ function RecombinaseList({ recombinases, setRecombinases }) {
|
|
|
65
65
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
|
|
66
66
|
<TextField
|
|
67
67
|
fullWidth
|
|
68
|
-
label="
|
|
68
|
+
label="Recombinase name (optional)"
|
|
69
69
|
value={name}
|
|
70
70
|
onChange={(e) => setName(e.target.value)}
|
|
71
71
|
size="small"
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
|
|
3
3
|
import { Checkbox, FormControlLabel, InputLabel, MenuItem, Select, TextField, FormControl, InputAdornment } from '@mui/material';
|
|
4
4
|
import MultipleInputsSelector from './MultipleInputsSelector';
|
|
5
|
-
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
5
|
+
import { anyAssemblyInputIsLinear, getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
6
6
|
import EnzymeMultiSelect from '../form/EnzymeMultiSelect';
|
|
7
7
|
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
8
8
|
import { classNameToEndPointMap } from '@opencloning/utils/sourceFunctions';
|
|
@@ -20,6 +20,11 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
20
20
|
const { id: sourceId, input: sourceInput } = source;
|
|
21
21
|
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
22
22
|
const inputContainsTemplates = inputSequences.some((sequence) => sequence.type === 'TemplateSequence');
|
|
23
|
+
const anyInputIsLinear = useSelector(
|
|
24
|
+
(state) => anyAssemblyInputIsLinear(source, state.cloning.teselaJsonCache),
|
|
25
|
+
);
|
|
26
|
+
// It starts as undefined to avoid flashing the checkbox when the component mounts
|
|
27
|
+
const [inputContainsGenome, setInputContainsGenome] = React.useState(undefined);
|
|
23
28
|
const [minimalHomology, setMinimalHomology] = React.useState(20);
|
|
24
29
|
const [allowPartialOverlap, setAllowPartialOverlap] = React.useState(false);
|
|
25
30
|
const [circularOnly, setCircularOnly] = React.useState(false);
|
|
@@ -43,6 +48,13 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
43
48
|
}
|
|
44
49
|
}, [source]);
|
|
45
50
|
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (inputSequences.length == 1) {
|
|
53
|
+
setInputContainsGenome(false);
|
|
54
|
+
} else {
|
|
55
|
+
setInputContainsGenome(anyInputIsLinear);
|
|
56
|
+
}
|
|
57
|
+
}, [anyInputIsLinear, inputSequences.length]);
|
|
46
58
|
const { updateSource } = cloningActions;
|
|
47
59
|
|
|
48
60
|
const preventSubmit = (
|
|
@@ -97,7 +109,10 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
97
109
|
} else if (assemblyType === 'RecombinaseSource') {
|
|
98
110
|
if (recombinases.length === 0) { return; }
|
|
99
111
|
requestData.source.recombinases = recombinases;
|
|
100
|
-
const config = { params: {
|
|
112
|
+
const config = { params: {
|
|
113
|
+
reverse_recombinase: reverseRecombinase,
|
|
114
|
+
input_contains_genome: inputContainsGenome,
|
|
115
|
+
} };
|
|
101
116
|
sendPostRequest({ endpoint: 'recombinase', requestData, config, source });
|
|
102
117
|
} else {
|
|
103
118
|
const config = { params: {
|
|
@@ -160,6 +175,25 @@ function SourceAssembly({ source, requestStatus, sendPostRequest }) {
|
|
|
160
175
|
)}
|
|
161
176
|
/>
|
|
162
177
|
</FormControl>
|
|
178
|
+
{(anyInputIsLinear && inputSequences.length > 1) && inputContainsGenome !== undefined && (
|
|
179
|
+
<FormControl fullWidth style={{ textAlign: 'left' }}>
|
|
180
|
+
<FormControlLabel
|
|
181
|
+
control={(
|
|
182
|
+
<Checkbox
|
|
183
|
+
data-testid="input-contains-genome-checkbox"
|
|
184
|
+
checked={inputContainsGenome}
|
|
185
|
+
onChange={() => setInputContainsGenome(prev => !prev)}
|
|
186
|
+
/>
|
|
187
|
+
)}
|
|
188
|
+
label={(
|
|
189
|
+
<LabelWithTooltip
|
|
190
|
+
label="Input contains genome"
|
|
191
|
+
tooltip="Select this option if any of the linear input sequences is part of a genome. This will be used to simulate genome integration rather than treating it as a linear DNA sequence"
|
|
192
|
+
/>
|
|
193
|
+
)}
|
|
194
|
+
/>
|
|
195
|
+
</FormControl>
|
|
196
|
+
)}
|
|
163
197
|
</>
|
|
164
198
|
)}
|
|
165
199
|
{ (assemblyType === 'GatewaySource') && (
|
|
@@ -223,7 +223,6 @@ function IndexJsonSelector({
|
|
|
223
223
|
}}
|
|
224
224
|
// Change options only when input changes (not when an option is picked)
|
|
225
225
|
onInputChange={(event, newInputValue, reason) => (reason === 'input') && onInputChange(newInputValue)}
|
|
226
|
-
id="tags-standard"
|
|
227
226
|
options={options}
|
|
228
227
|
noOptionsText={userInput.length < requiredInput ? noOptionsText : 'Nothing found'}
|
|
229
228
|
getOptionLabel={(o) => o.name}
|
|
@@ -44,7 +44,7 @@ function SourceTypeSelector({ source }) {
|
|
|
44
44
|
options.push(<MenuItem key="CopySequence" value="CopySequence">Use an existing sequence</MenuItem>);
|
|
45
45
|
}
|
|
46
46
|
} else {
|
|
47
|
-
// See https://github.com/
|
|
47
|
+
// See https://github.com/OpenCloning/OpenCloning_frontend/issues/101
|
|
48
48
|
if (inputSequences.length < 2) {
|
|
49
49
|
options.push(<MenuItem key="RestrictionEnzymeDigestionSource" value="RestrictionEnzymeDigestionSource">Restriction</MenuItem>);
|
|
50
50
|
options.push(<MenuItem key="PCRSource" value="PCRSource">PCR</MenuItem>);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { sequencingFileExtensions } from '@opencloning/utils/sequencingFileExtensions';
|
|
3
|
+
function ImportSequencingFilesInput({ onFileChange, fileInputRef }) {
|
|
4
|
+
|
|
5
|
+
return (
|
|
6
|
+
<input
|
|
7
|
+
type="file"
|
|
8
|
+
accept={sequencingFileExtensions.map((ext) => `.${ext}`).join(', ')}
|
|
9
|
+
multiple
|
|
10
|
+
onChange={(event) => {onFileChange(Array.from(event.target.files)); fileInputRef.current.value = '';}}
|
|
11
|
+
style={{ display: 'none' }}
|
|
12
|
+
ref={fileInputRef}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default ImportSequencingFilesInput
|
|
@@ -14,11 +14,27 @@ function LoadFromDatabaseButton({ databaseId, onFileChange, setError, existingFi
|
|
|
14
14
|
setSelectedFiles([]);
|
|
15
15
|
}, [open]);
|
|
16
16
|
|
|
17
|
+
const handleSubmit = async () => {
|
|
18
|
+
setOpen(false);
|
|
19
|
+
try {
|
|
20
|
+
const filesToLoad = await Promise.all(selectedFiles.map((file) => files.find((f) => f.name === file).getFile()));
|
|
21
|
+
onFileChange(filesToLoad);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
setError(error.message);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
17
27
|
React.useEffect(() => {
|
|
18
28
|
async function getFiles() {
|
|
19
29
|
setLoading(true);
|
|
20
30
|
const allFiles = await database.getSequencingFiles(databaseId);
|
|
21
|
-
|
|
31
|
+
if (database.autoloadSequencingFiles) {
|
|
32
|
+
const filesToLoad = await Promise.all(allFiles.map((file) => file.getFile()));
|
|
33
|
+
onFileChange(filesToLoad);
|
|
34
|
+
setOpen(false);
|
|
35
|
+
} else {
|
|
36
|
+
setFiles(allFiles.filter((file) => sequencingFileExtensions.includes(file.name.toLowerCase().split('.').pop())));
|
|
37
|
+
}
|
|
22
38
|
setLoading(false);
|
|
23
39
|
}
|
|
24
40
|
if (open) {
|
|
@@ -26,15 +42,6 @@ function LoadFromDatabaseButton({ databaseId, onFileChange, setError, existingFi
|
|
|
26
42
|
}
|
|
27
43
|
}, [open]);
|
|
28
44
|
|
|
29
|
-
const handleSubmit = async () => {
|
|
30
|
-
setOpen(false);
|
|
31
|
-
try {
|
|
32
|
-
const filesToLoad = await Promise.all(selectedFiles.map((file) => files.find((f) => f.name === file).getFile()));
|
|
33
|
-
onFileChange(filesToLoad);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
setError(error.message);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
45
|
|
|
39
46
|
return (
|
|
40
47
|
<>
|
|
@@ -27,6 +27,7 @@ import LoadFromDatabaseButton from './LoadFromDatabaseButton';
|
|
|
27
27
|
import { sequencingFileExtensions } from '@opencloning/utils/sequencingFileExtensions';
|
|
28
28
|
import useHttpClient from '../../hooks/useHttpClient';
|
|
29
29
|
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
30
|
+
import ImportSequencingFilesInput from './ImportSequencingFilesInput';
|
|
30
31
|
|
|
31
32
|
const { addFile, removeFile: removeFileAction, removeFilesAssociatedToSequence, setMainSequenceId, setCurrentTab } = cloningActions;
|
|
32
33
|
|
|
@@ -52,9 +53,9 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
52
53
|
updateStoreEditor('mainEditor', id);
|
|
53
54
|
// TODO: ideally this should be done with a ref
|
|
54
55
|
document.getElementById('opencloning-app-tabs')?.scrollIntoView();
|
|
55
|
-
}, [id]);
|
|
56
|
+
}, [id, dispatch, updateStoreEditor]);
|
|
56
57
|
|
|
57
|
-
const handleFileUpload = async (newFiles) => {
|
|
58
|
+
const handleFileUpload = useCallback(async (newFiles) => {
|
|
58
59
|
// Clear the input
|
|
59
60
|
fileInputRef.current.value = '';
|
|
60
61
|
if (newFiles.some((file) => !sequencingFileExtensions.includes(file.name.toLowerCase().split('.').pop()))) {
|
|
@@ -109,7 +110,7 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
109
110
|
alignments.forEach(({ base64str, file_name }) => { sessionStorage.setItem(`verification-${id}-${file_name}`, base64str); });
|
|
110
111
|
});
|
|
111
112
|
setLoadingMessage('');
|
|
112
|
-
};
|
|
113
|
+
}, [id, dispatch, httpClient, backendRoute, store, sequence]);
|
|
113
114
|
|
|
114
115
|
const onFileChange = useCallback(async (files) => {
|
|
115
116
|
setLoadingMessage('Aligning...');
|
|
@@ -126,13 +127,13 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
126
127
|
const removeFile = useCallback((fileName) => {
|
|
127
128
|
dispatch(removeFileAction({ fileName, sequenceId: id }));
|
|
128
129
|
sessionStorage.removeItem(`verification-${id}-${fileName}`);
|
|
129
|
-
}, [id]);
|
|
130
|
+
}, [id, dispatch]);
|
|
130
131
|
|
|
131
|
-
const handleClickUpload = () => {
|
|
132
|
+
const handleClickUpload = useCallback(() => {
|
|
132
133
|
fileInputRef.current?.click();
|
|
133
|
-
};
|
|
134
|
+
}, [fileInputRef]);
|
|
134
135
|
|
|
135
|
-
const downloadFile = (fileName) => {
|
|
136
|
+
const downloadFile = useCallback((fileName) => {
|
|
136
137
|
const base64Content = sessionStorage.getItem(`verification-${id}-${fileName}`);
|
|
137
138
|
if (!base64Content) {
|
|
138
139
|
setError(`File ${fileName} not found in session storage`);
|
|
@@ -152,7 +153,7 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
152
153
|
a.click();
|
|
153
154
|
document.body.removeChild(a);
|
|
154
155
|
URL.revokeObjectURL(url);
|
|
155
|
-
};
|
|
156
|
+
}, [id]);
|
|
156
157
|
|
|
157
158
|
return (
|
|
158
159
|
<Dialog
|
|
@@ -165,14 +166,7 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
165
166
|
>
|
|
166
167
|
<DialogTitle>Verification files</DialogTitle>
|
|
167
168
|
<DialogContent>
|
|
168
|
-
<
|
|
169
|
-
type="file"
|
|
170
|
-
accept={sequencingFileExtensions.map((ext) => `.${ext}`).join(', ')}
|
|
171
|
-
multiple
|
|
172
|
-
onChange={(event) => onFileChange(Array.from(event.target.files))}
|
|
173
|
-
style={{ display: 'none' }}
|
|
174
|
-
ref={fileInputRef}
|
|
175
|
-
/>
|
|
169
|
+
<ImportSequencingFilesInput onFileChange={onFileChange} fileInputRef={fileInputRef} />
|
|
176
170
|
|
|
177
171
|
{loadingMessage && (
|
|
178
172
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}>
|
|
@@ -229,13 +223,13 @@ export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }
|
|
|
229
223
|
)}
|
|
230
224
|
|
|
231
225
|
{hasSequencingFile && (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
226
|
+
<Button
|
|
227
|
+
variant="contained"
|
|
228
|
+
onClick={() => { setDialogOpen(false); toggleMain(); }}
|
|
229
|
+
color="success"
|
|
230
|
+
>
|
|
237
231
|
See alignments in editor
|
|
238
|
-
|
|
232
|
+
</Button>
|
|
239
233
|
)}
|
|
240
234
|
</Box>
|
|
241
235
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ImportSequencingFilesInput } from './ImportSequencingFilesInput';
|
package/src/hooks/useAlerts.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { useDispatch } from 'react-redux';
|
|
2
|
-
import { cloningActions } from '@opencloning/store/cloning';
|
|
3
2
|
|
|
4
|
-
export default function useAlerts() {
|
|
3
|
+
export default function useAlerts({ actions }) {
|
|
5
4
|
const dispatch = useDispatch();
|
|
6
5
|
|
|
7
6
|
const addAlert = (alert) => {
|
|
8
|
-
dispatch(
|
|
7
|
+
dispatch(actions.addAlert(alert));
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
const removeAlert = (message) => {
|
|
12
|
-
dispatch(
|
|
11
|
+
dispatch(actions.removeAlert(message));
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
return { addAlert, removeAlert };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import error2String from '@opencloning/utils/error2String';
|
|
3
3
|
import useBackendRoute from './useBackendRoute';
|
|
4
|
-
import
|
|
4
|
+
import useCloningAlerts from './useCloningAlerts';
|
|
5
5
|
import useHttpClient from './useHttpClient';
|
|
6
6
|
|
|
7
7
|
export default function useBackendAPI() {
|
|
@@ -10,7 +10,7 @@ export default function useBackendAPI() {
|
|
|
10
10
|
const [sequences, setSequences] = useState([]);
|
|
11
11
|
const backendRoute = useBackendRoute();
|
|
12
12
|
const httpClient = useHttpClient();
|
|
13
|
-
const { addAlert } =
|
|
13
|
+
const { addAlert } = useCloningAlerts();
|
|
14
14
|
|
|
15
15
|
const sendPostRequest = useCallback(async ({ endpoint, requestData, config = {}, source: { output }, modifySource = (s) => s }) => {
|
|
16
16
|
setRequestStatus({ status: 'loading', message: 'loading' });
|
|
@@ -41,6 +41,9 @@ export default function useBackendAPI() {
|
|
|
41
41
|
|
|
42
42
|
setSources(receivedSources); setSequences(resp.data.sequences);
|
|
43
43
|
} catch (error) {
|
|
44
|
+
if (error.response && error.response.headers && error.response.headers['x-warning']) {
|
|
45
|
+
addAlert({ message: error.response.headers['x-warning'], severity: 'warning' });
|
|
46
|
+
}
|
|
44
47
|
setRequestStatus({ status: 'error', message: error2String(error) });
|
|
45
48
|
setSources([]);
|
|
46
49
|
setSequences([]);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function useDebouncedSearchQuery(getQuery, { minChars = 3, debounceMs = 500 } = {}) {
|
|
4
|
+
const [input, setInput] = useState('');
|
|
5
|
+
const [debouncedInput, setDebouncedInput] = useState('');
|
|
6
|
+
|
|
7
|
+
const syncing = input !== debouncedInput;
|
|
8
|
+
|
|
9
|
+
let noOptionsText = 'No results found';
|
|
10
|
+
if (input.length < minChars) {
|
|
11
|
+
noOptionsText = `Type at least ${minChars} characters to search`;
|
|
12
|
+
} else if (syncing) {
|
|
13
|
+
noOptionsText = 'Loading...';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const id = setTimeout(() => setDebouncedInput(input), debounceMs);
|
|
18
|
+
return () => clearTimeout(id);
|
|
19
|
+
}, [input, debounceMs]);
|
|
20
|
+
|
|
21
|
+
const query = React.useMemo(() => {
|
|
22
|
+
return {
|
|
23
|
+
...getQuery(debouncedInput),
|
|
24
|
+
enabled: debouncedInput.length >= minChars
|
|
25
|
+
};
|
|
26
|
+
}, [getQuery, debouncedInput, minChars]);
|
|
27
|
+
|
|
28
|
+
const autocompleteProps = {
|
|
29
|
+
inputValue: input,
|
|
30
|
+
onInputChange: (_event, value, reason) => {
|
|
31
|
+
if (reason === 'input' || reason === 'reset') setInput(value);
|
|
32
|
+
},
|
|
33
|
+
filterOptions: (x) => syncing ? [] : x,
|
|
34
|
+
noOptionsText,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const clearInput = () => {
|
|
38
|
+
setInput('');
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return { query, autocompleteProps, clearInput };
|
|
42
|
+
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import getHttpClient from '@opencloning/utils/getHttpClient';
|
|
2
|
+
import getHttpClient, { getAuthenticatedHttpClient } from '@opencloning/utils/getHttpClient';
|
|
3
3
|
import { useConfig } from './useConfig';
|
|
4
4
|
|
|
5
5
|
export default function useHttpClient() {
|
|
6
|
-
const { backendUrl } = useConfig();
|
|
6
|
+
const { backendUrl, requiresBackendAuth = false } = useConfig();
|
|
7
7
|
|
|
8
8
|
// Memoize the client creation and interceptor setup
|
|
9
9
|
const apiClient = React.useMemo(() => {
|
|
10
|
+
const createHttpClient = requiresBackendAuth ? getAuthenticatedHttpClient : getHttpClient;
|
|
11
|
+
|
|
10
12
|
if (!backendUrl) {
|
|
11
13
|
// Return a client without backend URL if config not loaded yet
|
|
12
|
-
return
|
|
14
|
+
return createHttpClient([]);
|
|
13
15
|
}
|
|
14
|
-
return
|
|
15
|
-
}, [backendUrl]);
|
|
16
|
+
return createHttpClient([backendUrl]);
|
|
17
|
+
}, [backendUrl, requiresBackendAuth]);
|
|
16
18
|
|
|
17
19
|
return apiClient;
|
|
18
20
|
}
|
|
@@ -53,7 +53,7 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
53
53
|
const sequence = cloningStrategy.sequences.find((e) => e.id === cloningSource.id);
|
|
54
54
|
const seq = convertToTeselaJson(sequence);
|
|
55
55
|
const databaseName = await database.getSequenceName(seqDatabaseId);
|
|
56
|
-
if (seq.name !== databaseName) {
|
|
56
|
+
if (databaseName && (seq.name !== databaseName)) {
|
|
57
57
|
seq.name = databaseName;
|
|
58
58
|
const genbank = jsonToGenbank(seq);
|
|
59
59
|
sequence.file_content = genbank;
|
|
@@ -68,11 +68,15 @@ export default function useLoadDatabaseFile({ source, sendPostRequest, setHistor
|
|
|
68
68
|
}
|
|
69
69
|
// This one won't have the source.id deleted
|
|
70
70
|
const prevState = store.getState().cloning;
|
|
71
|
+
const { backendVersion, schemaVersion, frontendVersion } = prevState.appInfo;
|
|
72
|
+
cloningStrategy.backend_version = backendVersion;
|
|
73
|
+
cloningStrategy.schema_version = schemaVersion;
|
|
74
|
+
cloningStrategy.frontend_version = frontendVersion;
|
|
71
75
|
cloningStrategy = await validateState(cloningStrategy);
|
|
72
76
|
|
|
73
77
|
batch(() => {
|
|
74
78
|
// Replace the source with the new one if called from a source
|
|
75
|
-
if (!ancestors) {
|
|
79
|
+
if (!ancestors && source) {
|
|
76
80
|
dispatch(deleteSourceAndItsChildren(source.id));
|
|
77
81
|
}
|
|
78
82
|
const cloningState = store.getState().cloning;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useMutation } from '@tanstack/react-query';
|
|
3
|
+
import { buildAlignmentTrack, buildReferenceTrack, buildAlignmentConfig } from '@opencloning/utils/alignmentUtils';
|
|
4
|
+
import useBackendRoute from './useBackendRoute';
|
|
5
|
+
import useHttpClient from './useHttpClient';
|
|
6
|
+
|
|
7
|
+
export default function useSequencingAlignment({ onError } = {}) {
|
|
8
|
+
const backendRoute = useBackendRoute();
|
|
9
|
+
const httpClient = useHttpClient();
|
|
10
|
+
|
|
11
|
+
const fetchAlignments = useCallback(async ({ sequenceModel, sequenceData, parsedFiles }) => {
|
|
12
|
+
// parsedFiles: [{ fileName, fileContent }] where fileContent is getTeselaJsonFromBase64 output
|
|
13
|
+
const traces = parsedFiles.map((f) => f.fileContent.sequence);
|
|
14
|
+
const resp = await httpClient.post(backendRoute('align_sanger'), { sequence: sequenceModel, traces });
|
|
15
|
+
|
|
16
|
+
const referenceTrack = buildReferenceTrack(sequenceData, resp.data[0]);
|
|
17
|
+
const otherTracks = parsedFiles.map((f, i) => {
|
|
18
|
+
const alignmentFile = { file_name: f.fileName, alignment: [resp.data[0], resp.data[i + 1]] }; // eslint-disable-line camelcase
|
|
19
|
+
return buildAlignmentTrack(f.fileContent, alignmentFile);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return buildAlignmentConfig(sequenceModel.id, sequenceData, [referenceTrack, ...otherTracks]);
|
|
23
|
+
}, [httpClient, backendRoute]);
|
|
24
|
+
|
|
25
|
+
return useMutation({ mutationFn: fetchAlignments, onError });
|
|
26
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import useBackendRoute from './useBackendRoute';
|
|
3
3
|
import useHttpClient from './useHttpClient';
|
|
4
|
-
import
|
|
4
|
+
import useCloningAlerts from './useCloningAlerts';
|
|
5
5
|
import { batch, useDispatch, useStore } from 'react-redux';
|
|
6
6
|
import { mergeStates } from '@opencloning/utils/network';
|
|
7
7
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
@@ -21,7 +21,7 @@ const { setState: setCloningState, deleteSourceAndItsChildren } = cloningActions
|
|
|
21
21
|
export default function useSnapgeneHistoryEndpoint() {
|
|
22
22
|
const backendRoute = useBackendRoute();
|
|
23
23
|
const httpClient = useHttpClient();
|
|
24
|
-
const { addAlert } =
|
|
24
|
+
const { addAlert } = useCloningAlerts();
|
|
25
25
|
const dispatch = useDispatch();
|
|
26
26
|
const store = useStore();
|
|
27
27
|
const url = backendRoute('read_snapgene_history');
|
|
@@ -48,7 +48,7 @@ export default function useSnapgeneHistoryEndpoint() {
|
|
|
48
48
|
}
|
|
49
49
|
if (resp.headers['x-warning']) {
|
|
50
50
|
addAlert({
|
|
51
|
-
message: `SnapGene
|
|
51
|
+
message: `Not all SnapGene history was parsed: ${resp.headers['x-warning']}`,
|
|
52
52
|
severity: 'warning',
|
|
53
53
|
});
|
|
54
54
|
}
|