@opencloning/ui 1.7.3 → 1.8.1

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 +80 -0
  2. package/package.json +20 -14
  3. package/src/components/AppAlerts.jsx +7 -7
  4. package/src/components/CloningHistory.jsx +2 -0
  5. package/src/components/CloningSpeedDial.jsx +102 -0
  6. package/src/components/DownloadCloningStrategyDialog.jsx +2 -2
  7. package/src/components/LoadCloningHistoryWrapper.jsx +2 -2
  8. package/src/components/MainSequenceEditor.jsx +41 -34
  9. package/src/components/NetworkNode.jsx +35 -33
  10. package/src/components/OpenCloning.jsx +4 -3
  11. package/src/components/SequenceViewer.cy.jsx +27 -0
  12. package/src/components/SequenceViewer.jsx +211 -0
  13. package/src/components/annotation/AnnotationChangedAlert.jsx +26 -0
  14. package/src/components/annotation/useUpdateAnnotationInMainSequence.js +2 -2
  15. package/src/components/assembler/Assembler.jsx +3 -3
  16. package/src/components/domesticate/DomesticateDialog.jsx +274 -0
  17. package/src/components/form/GetRequestMultiSelect.jsx +0 -1
  18. package/src/components/form/PostRequestSelect.jsx +0 -1
  19. package/src/components/form/QuerySelect.jsx +128 -0
  20. package/src/components/form/QueryStatusWrapper.jsx +31 -0
  21. package/src/components/form/SubmitToDatabaseDialog.jsx +63 -58
  22. package/src/components/form/TextFieldQueryValidated.jsx +93 -0
  23. package/src/components/index.js +4 -0
  24. package/src/components/navigation/FeedbackDialog.jsx +1 -1
  25. package/src/components/navigation/MainAppBar.jsx +5 -5
  26. package/src/components/primers/import_primers/ImportPrimersButton.jsx +2 -2
  27. package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +6 -3
  28. package/src/components/sources/FinishedSource.jsx +3 -0
  29. package/src/components/sources/MultipleOutputsSelector.cy.jsx +95 -0
  30. package/src/components/sources/MultipleOutputsSelector.jsx +32 -19
  31. package/src/components/sources/RecombinaseList.cy.jsx +1 -1
  32. package/src/components/sources/RecombinaseList.jsx +1 -1
  33. package/src/components/sources/SourceAssembly.jsx +36 -2
  34. package/src/components/sources/SourceRepositoryId.jsx +0 -1
  35. package/src/components/sources/SourceTypeSelector.jsx +1 -1
  36. package/src/components/verification/ImportSequencingFilesInput.jsx +17 -0
  37. package/src/components/verification/LoadFromDatabaseButton.jsx +17 -10
  38. package/src/components/verification/VerificationFileDialog.jsx +16 -22
  39. package/src/components/verification/index.js +1 -0
  40. package/src/config/defaultMainEditorProps.js +1 -0
  41. package/src/hooks/useAlerts.js +3 -4
  42. package/src/hooks/useBackendAPI.js +5 -2
  43. package/src/hooks/useCloningAlerts.js +7 -0
  44. package/src/hooks/useDebouncedSearchQuery.js +42 -0
  45. package/src/hooks/useLoadDatabaseFile.js +6 -2
  46. package/src/hooks/useSequencingAlignment.js +26 -0
  47. package/src/hooks/useSnapgeneHistoryEndpoint.js +3 -3
  48. package/src/hooks/useStoreEditor.js +10 -68
  49. package/src/hooks/useUrlParamsLoader.js +2 -2
  50. package/src/hooks/useValidateState.js +3 -3
  51. package/src/index.css +3 -11
  52. 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({ sources, sequences, sourceId, onFragmentChosen }) {
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
- (selectedOutput + 1) % sources.length,
20
- );
21
- const decreaseSelectedOutput = () => setSelectedOutput((selectedOutput !== 0) ? (selectedOutput - 1) : sources.length - 1);
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 = convertToTeselaJson(sequences[selectedOutput]);
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
- {selectedOutput + 1}
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
- <SubSequenceDisplayer {...{ source: sources[selectedOutput], sourceId }} />
51
- <AssemblyPlanDisplayer {...{ source: sources[selectedOutput] }} />
52
- <SimpleCircularOrLinearView {...{ sequenceData: seq, editorName, height: 'auto' }} />
53
- <OverhangsDisplay {...{ sequenceData: seq, sequence: sequences[selectedOutput] }} />
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('Name (optional)', 'MyRecombinase', 'body');
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="Name (optional)"
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: { reverse_recombinase: reverseRecombinase } };
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/manulera/OpenCloning_frontend/issues/101
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
- setFiles(allFiles.filter((file) => sequencingFileExtensions.includes(file.name.toLowerCase().split('.').pop())));
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
- <input
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
- <Button
233
- variant="contained"
234
- onClick={() => { setDialogOpen(false); toggleMain(); }}
235
- color="success"
236
- >
226
+ <Button
227
+ variant="contained"
228
+ onClick={() => { setDialogOpen(false); toggleMain(); }}
229
+ color="success"
230
+ >
237
231
  See alignments in editor
238
- </Button>
232
+ </Button>
239
233
  )}
240
234
  </Box>
241
235
 
@@ -0,0 +1 @@
1
+ export { default as ImportSequencingFilesInput } from './ImportSequencingFilesInput';
@@ -2,6 +2,7 @@ export default {
2
2
  isFullscreen: false,
3
3
  readOnly: false,
4
4
  disableBpEditing: true,
5
+ hideSingleImport: true,
5
6
  annotationVisibility: { reverseSequence: true, cutsites: false },
6
7
  ToolBarProps: {
7
8
  toolList: [
@@ -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(cloningActions.addAlert(alert));
7
+ dispatch(actions.addAlert(alert));
9
8
  };
10
9
 
11
10
  const removeAlert = (message) => {
12
- dispatch(cloningActions.removeAlert(message));
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 useAlerts from './useAlerts';
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 } = useAlerts();
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,7 @@
1
+
2
+ import { cloningActions } from '@opencloning/store/cloning';
3
+ import useAlerts from './useAlerts';
4
+
5
+ export default function useCloningAlerts() {
6
+ return useAlerts({ actions: cloningActions });
7
+ }
@@ -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
+ }
@@ -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 useAlerts from './useAlerts';
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 } = useAlerts();
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 parsing warning: ${resp.headers['x-warning']}`,
51
+ message: `Not all SnapGene history was parsed: ${resp.headers['x-warning']}`,
52
52
  severity: 'warning',
53
53
  });
54
54
  }
@@ -2,8 +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 { findRotation, syncChromatogramDataWithAlignment } from '@opencloning/utils/sequenceManipulation';
6
- import { getReverseComplementSequenceAndAnnotations, getReverseComplementSequenceString, rotateSequenceDataToPosition } from '@teselagen/sequence-utils';
5
+ import { buildAlignmentTrack, buildReferenceTrack, buildAlignmentConfig, removePanelFromShown, updatePanelsShownWithAlignment } from '@opencloning/utils/alignmentUtils';
7
6
 
8
7
  export default function useStoreEditor() {
9
8
  const store = useStore();
@@ -27,74 +26,17 @@ export default function useStoreEditor() {
27
26
  const pcrPrimers = getPCRPrimers(cloning, id);
28
27
  const alignmentFiles = cloning.files.filter((e) => e.sequence_id === id && e.file_type === 'Sequencing file');
29
28
  let { panelsShown } = store.getState().VectorEditor.mainEditor;
30
- panelsShown = [[...panelsShown[0].filter((p) => p.id !== 'simpleAlignment')]]
29
+ panelsShown = removePanelFromShown(panelsShown, 'simpleAlignment');
31
30
 
32
31
  if (alignmentFiles.length > 0) {
33
- addAlignment(store, {
34
- id: 'simpleAlignment',
35
- alignmentType: 'Sequencing alignment',
36
- name: `Seq. ${id}`,
37
- // set the visibilities of the annotations you'd like to see
38
- alignmentAnnotationVisibility: {
39
- features: true,
40
- parts: true,
41
- translations: true,
42
- },
43
- alignmentTracks: [
44
- {
45
- sequenceData,
46
- alignmentData: {
47
- // the alignmentData just needs the sequence < TODO this has to be changed to be the largest ---
48
- sequence: alignmentFiles[0].alignment[0],
49
- name: sequenceData.name
50
- },
51
- },
52
- ...await Promise.all(alignmentFiles.map(async (aln) => {
53
- const fileContent = await getTeselaJsonFromBase64(sessionStorage.getItem(`verification-${id}-${aln.file_name}`), aln.file_name);
54
- let chromatogramData = fileContent.chromatogramData;
55
- let alignmentSequenceData = {
56
- name: aln.file_name,
57
- sequence: aln.alignment[1].replaceAll('-', ''),
58
- };
59
- if (chromatogramData) {
60
- chromatogramData = syncChromatogramDataWithAlignment(chromatogramData, aln.alignment[1]);
61
- }
62
- if (fileContent.features && fileContent.features.length > 0) {
63
- alignmentSequenceData = fileContent;
64
- const alignmentSequence = aln.alignment[1].replaceAll('-', '');
65
- let rotation = findRotation(fileContent.sequence, alignmentSequence);
66
- // If the rotation is -1, it may be reverse complemented
67
- const reverseComplemented = rotation === -1;
68
- if (reverseComplemented) {
69
- rotation = findRotation(fileContent.sequence, getReverseComplementSequenceString(alignmentSequence));
70
- alignmentSequenceData = getReverseComplementSequenceAndAnnotations(fileContent);
71
- }
72
- if (rotation !== -1 && rotation !== 0) {
73
- rotation = reverseComplemented ? fileContent.sequence.length - rotation : rotation;
74
- alignmentSequenceData = rotateSequenceDataToPosition(alignmentSequenceData, rotation);
75
- }
76
- }
77
- return {
78
- sequenceData: {...alignmentSequenceData, name: aln.file_name},
79
- alignmentData: {
80
- sequence: aln.alignment[1],
81
- name: aln.file_name
82
- },
83
- chromatogramData,
84
- };
85
- })),
86
- ],
87
- });
88
- panelsShown = [[
89
- ...panelsShown[0].filter((p) => p.id !== 'simpleAlignment'),
90
- {
91
- id: 'simpleAlignment',
92
- type: 'alignment',
93
- name: 'Alignments',
94
- active: true,
95
- isFullscreen: false,
96
- },
97
- ]];
32
+ const referenceTrack = buildReferenceTrack(sequenceData, alignmentFiles[0].alignment[0]);
33
+ const otherTracks = await Promise.all(alignmentFiles.map(async (aln) => {
34
+ const fileContent = await getTeselaJsonFromBase64(sessionStorage.getItem(`verification-${id}-${aln.file_name}`), aln.file_name);
35
+ return buildAlignmentTrack(fileContent, aln);
36
+ }));
37
+ const alignmentConfig = buildAlignmentConfig(id, sequenceData, [referenceTrack, ...otherTracks]);
38
+ addAlignment(store, alignmentConfig);
39
+ panelsShown = updatePanelsShownWithAlignment(panelsShown);
98
40
  }
99
41
  sequenceData.primers = sequenceData.primers.concat([...pcrPrimers]);
100
42
  updateEditor(store, editorName, { sequenceData, selectionLayer, panelsShown, sequenceDataHistory: {} });
@@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux';
3
3
  import { cloningActions } from '@opencloning/store/cloning';
4
4
  import useDatabase from './useDatabase';
5
5
  import useLoadDatabaseFile from './useLoadDatabaseFile';
6
- import useAlerts from './useAlerts';
6
+ import useCloningAlerts from './useCloningAlerts';
7
7
  import useHttpClient from './useHttpClient';
8
8
  import useValidateState from './useValidateState';
9
9
  import { formatSequenceLocationString, getUrlParameters } from '@opencloning/utils/other';
@@ -18,7 +18,7 @@ const { setState: setCloningState, updateSource } = cloningActions;
18
18
  export default function useUrlParamsLoader() {
19
19
  const dispatch = useDispatch();
20
20
  const database = useDatabase();
21
- const { addAlert } = useAlerts();
21
+ const { addAlert } = useCloningAlerts();
22
22
  const setHistoryFileError = (e) => addAlert({ message: e, severity: 'error' });
23
23
  const { loadDatabaseFile } = useLoadDatabaseFile({ source: { id: 1 }, sendPostRequest: null, setHistoryFileError });
24
24
  const validateState = useValidateState();