@opencloning/ui 1.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 (207) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +37 -0
  3. package/src/components/AppAlerts.jsx +19 -0
  4. package/src/components/CloningHistory.jsx +40 -0
  5. package/src/components/DataModelDisplayer.jsx +30 -0
  6. package/src/components/DescriptionEditor.jsx +68 -0
  7. package/src/components/DownloadCloningStrategyDialog.jsx +84 -0
  8. package/src/components/DownloadSequenceFileDialog.jsx +90 -0
  9. package/src/components/DragAndDropCloningHistoryWrapper.jsx +39 -0
  10. package/src/components/DraggableDialogPaper.jsx +16 -0
  11. package/src/components/EditSequenceNameDialog.jsx +90 -0
  12. package/src/components/ExternalServicesStatusCheck.jsx +92 -0
  13. package/src/components/HistoryLoadedDialog.jsx +49 -0
  14. package/src/components/LoadCloningHistoryWrapper.jsx +166 -0
  15. package/src/components/MainSequenceCheckBox.jsx +83 -0
  16. package/src/components/MainSequenceEditor.jsx +165 -0
  17. package/src/components/NetworkNode.jsx +159 -0
  18. package/src/components/NetworkTree.css +127 -0
  19. package/src/components/ObjectTable.jsx +24 -0
  20. package/src/components/OpenCloning.jsx +102 -0
  21. package/src/components/OverhangsDisplay.jsx +25 -0
  22. package/src/components/SequenceEditor.jsx +120 -0
  23. package/src/components/SequenceTab.jsx +14 -0
  24. package/src/components/TemplateSequence.jsx +38 -0
  25. package/src/components/annotation/PlannotateAnnotationReport.jsx +33 -0
  26. package/src/components/annotation/useUpdateAnnotationInMainSequence.js +39 -0
  27. package/src/components/assembler/AssemblePartWidget.jsx +252 -0
  28. package/src/components/assembler/Assembler.jsx +273 -0
  29. package/src/components/assembler/AssemblerPart.jsx +99 -0
  30. package/src/components/assembler/StopIcon.jsx +34 -0
  31. package/src/components/assembler/assembler_data2.json +50 -0
  32. package/src/components/assembler/assembly_component.module.css +81 -0
  33. package/src/components/assembler/moclo.json +110 -0
  34. package/src/components/assembler/sbol_visual_glyphs/LICENSE.html +21 -0
  35. package/src/components/assembler/sbol_visual_glyphs/assembly-scar.svg +63 -0
  36. package/src/components/assembler/sbol_visual_glyphs/cds-stop.svg +85 -0
  37. package/src/components/assembler/sbol_visual_glyphs/cds.svg +60 -0
  38. package/src/components/assembler/sbol_visual_glyphs/chromosomal-locus.svg +78 -0
  39. package/src/components/assembler/sbol_visual_glyphs/engineered-region.svg +56 -0
  40. package/src/components/assembler/sbol_visual_glyphs/five-prime-sticky-restriction-site.svg +56 -0
  41. package/src/components/assembler/sbol_visual_glyphs/origin-of-replication.svg +57 -0
  42. package/src/components/assembler/sbol_visual_glyphs/primer-binding-site.svg +59 -0
  43. package/src/components/assembler/sbol_visual_glyphs/promoter.svg +60 -0
  44. package/src/components/assembler/sbol_visual_glyphs/ribosome-entry-site.svg +56 -0
  45. package/src/components/assembler/sbol_visual_glyphs/specific-recombination-site.svg +59 -0
  46. package/src/components/assembler/sbol_visual_glyphs/terminator.svg +60 -0
  47. package/src/components/assembler/sbol_visual_glyphs/three-prime-sticky-restriction-site.svg +56 -0
  48. package/src/components/assembler/sbol_visual_glyphs.js +36 -0
  49. package/src/components/assembler/useAssembler.js +71 -0
  50. package/src/components/dummy/DummyInterface.js +41 -0
  51. package/src/components/dummy/GetSequenceFileAndDatabaseIdComponent.jsx +59 -0
  52. package/src/components/eLabFTW/ELabFTWCategorySelect.cy.jsx +86 -0
  53. package/src/components/eLabFTW/ELabFTWCategorySelect.jsx +43 -0
  54. package/src/components/eLabFTW/ELabFTWFileSelect.cy.jsx +43 -0
  55. package/src/components/eLabFTW/ELabFTWFileSelect.jsx +29 -0
  56. package/src/components/eLabFTW/ELabFTWResourceSelect.cy.jsx +107 -0
  57. package/src/components/eLabFTW/ELabFTWResourceSelect.jsx +23 -0
  58. package/src/components/eLabFTW/GetPrimerComponent.cy.jsx +261 -0
  59. package/src/components/eLabFTW/GetPrimerComponent.jsx +55 -0
  60. package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.cy.jsx +184 -0
  61. package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.jsx +62 -0
  62. package/src/components/eLabFTW/LoadHistoryComponent.cy.jsx +235 -0
  63. package/src/components/eLabFTW/LoadHistoryComponent.jsx +51 -0
  64. package/src/components/eLabFTW/PrimersNotInDatabaseComponent.cy.jsx +159 -0
  65. package/src/components/eLabFTW/PrimersNotInDatabaseComponent.jsx +54 -0
  66. package/src/components/eLabFTW/SubmitToDatabaseComponent.cy.jsx +185 -0
  67. package/src/components/eLabFTW/SubmitToDatabaseComponent.jsx +51 -0
  68. package/src/components/eLabFTW/common.js +26 -0
  69. package/src/components/eLabFTW/eLabFTWInterface.js +294 -0
  70. package/src/components/eLabFTW/eLabFTWInterface.test.js +839 -0
  71. package/src/components/eLabFTW/envValues.js +7 -0
  72. package/src/components/eLabFTW/utils.js +39 -0
  73. package/src/components/form/CustomFormHelperText.jsx +10 -0
  74. package/src/components/form/EnzymeMultiSelect.cy.jsx +61 -0
  75. package/src/components/form/EnzymeMultiSelect.jsx +34 -0
  76. package/src/components/form/GetRequestMultiSelect.jsx +107 -0
  77. package/src/components/form/LabelWithTooltip.jsx +16 -0
  78. package/src/components/form/PostRequestSelect.cy.jsx +70 -0
  79. package/src/components/form/PostRequestSelect.jsx +86 -0
  80. package/src/components/form/RequestStatusWrapper.jsx +17 -0
  81. package/src/components/form/RetryAlert.jsx +20 -0
  82. package/src/components/form/ServerErrorMessage.jsx +10 -0
  83. package/src/components/form/SubmitButtonBackendAPI.jsx +15 -0
  84. package/src/components/form/SubmitToDatabaseDialog.jsx +133 -0
  85. package/src/components/form/TextFieldValidate.jsx +67 -0
  86. package/src/components/form/ValidatedTextField.jsx +33 -0
  87. package/src/components/form/intermediates_disclaimer.svg +181 -0
  88. package/src/components/navigation/ButtonWithMenu.jsx +43 -0
  89. package/src/components/navigation/CustomTab.jsx +14 -0
  90. package/src/components/navigation/FeedbackDialog.jsx +34 -0
  91. package/src/components/navigation/GithubCornerRight.jsx +29 -0
  92. package/src/components/navigation/MainAppBar.css +26 -0
  93. package/src/components/navigation/MainAppBar.jsx +205 -0
  94. package/src/components/navigation/SelectExampleDialog.jsx +69 -0
  95. package/src/components/navigation/SelectTemplateDialog.jsx +107 -0
  96. package/src/components/navigation/TabPanel.jsx +28 -0
  97. package/src/components/navigation/VersionDialog.jsx +33 -0
  98. package/src/components/primers/CreatePrimerFromSequenceForm.jsx +42 -0
  99. package/src/components/primers/DownloadPrimersButton.jsx +104 -0
  100. package/src/components/primers/PrimerForm.css +14 -0
  101. package/src/components/primers/PrimerForm.jsx +107 -0
  102. package/src/components/primers/PrimerList.css +46 -0
  103. package/src/components/primers/PrimerList.cy.jsx +95 -0
  104. package/src/components/primers/PrimerList.jsx +126 -0
  105. package/src/components/primers/PrimerTableRow.cy.jsx +57 -0
  106. package/src/components/primers/PrimerTableRow.jsx +84 -0
  107. package/src/components/primers/SelectPrimerForm.jsx +66 -0
  108. package/src/components/primers/import_primers/ImportPrimersButton.jsx +101 -0
  109. package/src/components/primers/import_primers/ImportPrimersTable.jsx +51 -0
  110. package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +107 -0
  111. package/src/components/primers/import_primers/styles.css +60 -0
  112. package/src/components/primers/primer_design/SequenceTabComponents/CollapsableLabel.jsx +31 -0
  113. package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +164 -0
  114. package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +37 -0
  115. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +369 -0
  116. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignEBIC.jsx +24 -0
  117. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +29 -0
  118. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGatewayBP.jsx +36 -0
  119. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +26 -0
  120. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignHomologousRecombination.jsx +32 -0
  121. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignRestriction.jsx +25 -0
  122. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +25 -0
  123. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignStepper.jsx +53 -0
  124. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +80 -0
  125. package/src/components/primers/primer_design/SequenceTabComponents/PrimerResultForm.jsx +49 -0
  126. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSettingsForm.jsx +68 -0
  127. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +84 -0
  128. package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx +85 -0
  129. package/src/components/primers/primer_design/SequenceTabComponents/StepNavigation.jsx +48 -0
  130. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +216 -0
  131. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +42 -0
  132. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +61 -0
  133. package/src/components/primers/primer_design/SequenceTabComponents/TabPannelSettings.jsx +59 -0
  134. package/src/components/primers/primer_design/SequenceTabComponents/primerDesignMinimalValues.json +5 -0
  135. package/src/components/primers/primer_design/SequenceTabComponents/useEBICPrimerDesignSettings.js +31 -0
  136. package/src/components/primers/primer_design/SequenceTabComponents/useEnzymePrimerDesignSettings.js +57 -0
  137. package/src/components/primers/primer_design/SequenceTabComponents/useGatewayPrimerDesignSettings.js +18 -0
  138. package/src/components/primers/primer_design/SequenceTabComponents/usePrimerDesignSettings.js +31 -0
  139. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +88 -0
  140. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +65 -0
  141. package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +84 -0
  142. package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +74 -0
  143. package/src/components/primers/primer_design/common/NoAttPSitesError.jsx +31 -0
  144. package/src/components/primers/primer_details/PCRTable.cy.jsx +51 -0
  145. package/src/components/primers/primer_details/PCRTable.jsx +35 -0
  146. package/src/components/primers/primer_details/Primer3Figure.jsx +25 -0
  147. package/src/components/primers/primer_details/PrimerDetailsTds.jsx +39 -0
  148. package/src/components/primers/primer_details/PrimerInfoIcon.cy.jsx +137 -0
  149. package/src/components/primers/primer_details/PrimerInfoIcon.jsx +132 -0
  150. package/src/components/primers/primer_details/TableSection.jsx +17 -0
  151. package/src/components/primers/primer_details/primerDetailsFormatting.js +3 -0
  152. package/src/components/primers/primer_details/useMultiplePrimerDetails.js +29 -0
  153. package/src/components/primers/primer_details/usePCRDetails.js +47 -0
  154. package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +49 -0
  155. package/src/components/primers/primer_details/useSinglePrimerSequenceDetails.js +25 -0
  156. package/src/components/primers/primersToTabularFile.js +49 -0
  157. package/src/components/primers/primersToTabularFile.test.js +108 -0
  158. package/src/components/settings/SettingsTab.cy.jsx +267 -0
  159. package/src/components/settings/SettingsTab.jsx +170 -0
  160. package/src/components/sources/AssemblyPlanDisplayer.cy.jsx +22 -0
  161. package/src/components/sources/AssemblyPlanDisplayer.jsx +27 -0
  162. package/src/components/sources/CollectionSource.jsx +97 -0
  163. package/src/components/sources/FinishedSource.jsx +397 -0
  164. package/src/components/sources/KnownSourceErrors.jsx +50 -0
  165. package/src/components/sources/MultipleInputsSelector.jsx +63 -0
  166. package/src/components/sources/MultipleOutputsSelector.jsx +63 -0
  167. package/src/components/sources/NewSourceBox.jsx +37 -0
  168. package/src/components/sources/PCRUnitForm.jsx +102 -0
  169. package/src/components/sources/SingleInputSelector.jsx +36 -0
  170. package/src/components/sources/Source.jsx +125 -0
  171. package/src/components/sources/SourceAnnotation.jsx +44 -0
  172. package/src/components/sources/SourceAssembly.jsx +201 -0
  173. package/src/components/sources/SourceBox.css +18 -0
  174. package/src/components/sources/SourceBox.jsx +60 -0
  175. package/src/components/sources/SourceCopySequence.jsx +38 -0
  176. package/src/components/sources/SourceDatabase.jsx +28 -0
  177. package/src/components/sources/SourceFile.jsx +188 -0
  178. package/src/components/sources/SourceGenomeRegion.cy.jsx +131 -0
  179. package/src/components/sources/SourceGenomeRegion.jsx +486 -0
  180. package/src/components/sources/SourceHomologousRecombination.jsx +125 -0
  181. package/src/components/sources/SourceKnownGenomeRegion.jsx +60 -0
  182. package/src/components/sources/SourceManuallyTyped.jsx +116 -0
  183. package/src/components/sources/SourcePCRorHybridization.jsx +165 -0
  184. package/src/components/sources/SourcePolymeraseExtension.jsx +44 -0
  185. package/src/components/sources/SourceRepositoryId.jsx +409 -0
  186. package/src/components/sources/SourceRestriction.jsx +41 -0
  187. package/src/components/sources/SourceReverseComplement.jsx +33 -0
  188. package/src/components/sources/SourceTypeSelector.jsx +94 -0
  189. package/src/components/sources/SubSequenceDisplayer.jsx +70 -0
  190. package/src/components/sources/VerifyDeleteDialog.jsx +23 -0
  191. package/src/components/sources/repositoryMetadata.js +14 -0
  192. package/src/components/verification/LoadFromDatabaseButton.jsx +90 -0
  193. package/src/components/verification/SequencingFileRow.jsx +34 -0
  194. package/src/components/verification/VerificationFileDialog.cy.jsx +176 -0
  195. package/src/components/verification/VerificationFileDialog.jsx +248 -0
  196. package/src/config/defaultMainEditorProps.js +44 -0
  197. package/src/hooks/useAlerts.js +16 -0
  198. package/src/hooks/useBackendAPI.js +51 -0
  199. package/src/hooks/useBackendRoute.js +22 -0
  200. package/src/hooks/useDatabase.js +18 -0
  201. package/src/hooks/useDragAndDropFile.js +31 -0
  202. package/src/hooks/useGatewaySites.js +40 -0
  203. package/src/hooks/useHttpClient.js +12 -0
  204. package/src/hooks/useLoadDatabaseFile.js +108 -0
  205. package/src/hooks/useStoreEditor.js +101 -0
  206. package/src/hooks/useValidateState.js +43 -0
  207. package/vitest.config.js +18 -0
@@ -0,0 +1,486 @@
1
+ import React from 'react';
2
+ import InputLabel from '@mui/material/InputLabel';
3
+ import MenuItem from '@mui/material/MenuItem';
4
+ import TextField from '@mui/material/TextField';
5
+ import FormControl from '@mui/material/FormControl';
6
+ import Select from '@mui/material/Select';
7
+ import { Alert, Box, FormHelperText, FormLabel } from '@mui/material';
8
+ import PostRequestSelect from '../form/PostRequestSelect';
9
+ import { getReferenceAssemblyId, taxonSuggest, geneSuggest, getInfoFromAssemblyId, getInfoFromSequenceAccession } from '@opencloning/utils/ncbiRequests';
10
+ import TextFieldValidate from '../form/TextFieldValidate';
11
+ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
12
+ import GetRequestMultiSelect from '../form/GetRequestMultiSelect';
13
+ import useHttpClient from '../../hooks/useHttpClient';
14
+ import { formatSequenceLocationString } from '@opencloning/utils/other';
15
+
16
+ function getGeneCoordsInfo(gene) {
17
+ const { range: geneRange, accession_version: accessionVersion } = gene.annotation.genomic_regions[0].gene_range;
18
+ const { begin: start, end, orientation } = geneRange[0];
19
+ const strand = orientation === 'plus' ? 1 : -1;
20
+ return { accessionVersion, start: Number(start), end: Number(end), strand };
21
+ }
22
+
23
+ function formatGeneCoords(gene) {
24
+ const { accessionVersion, start, end, strand } = getGeneCoordsInfo(gene);
25
+ return `${accessionVersion} (${start}..${end}${strand === -1 ? ', complement' : ''})`;
26
+ }
27
+
28
+
29
+ export function formatBackendPayloadWithGene(assemblyId, gene, shiftUpstream, shiftDownstream) {
30
+ const { accessionVersion, start, end, strand } = getGeneCoordsInfo(gene);
31
+ const shiftedStart = start - (strand === 1 ? shiftUpstream : shiftDownstream);
32
+ const shiftedEnd = end + (strand === 1 ? shiftDownstream : shiftUpstream);
33
+
34
+ return {
35
+ repository_id: accessionVersion,
36
+ assembly_accession: assemblyId,
37
+ locus_tag: gene.annotation.locus_tag ? gene.annotation.locus_tag : null,
38
+ gene_id: gene.annotation.gene_id ? gene.annotation.gene_id : null,
39
+ coordinates: formatSequenceLocationString(shiftedStart, shiftedEnd, strand),
40
+ };
41
+ }
42
+
43
+ function SpeciesPicker({ setSpecies, setAssemblyId }) {
44
+ const speciesPostRequestSettings = React.useMemo(() => ({
45
+ setValue: (v) => {
46
+ if (v === null) {
47
+ setSpecies(null);
48
+ setAssemblyId('');
49
+ return;
50
+ }
51
+ getReferenceAssemblyId(v.tax_id).then((response) => {
52
+ // Set the species
53
+ setSpecies(v);
54
+ // Set the assemblyId
55
+ setAssemblyId(response === null ? '' : response);
56
+ });
57
+ },
58
+ getOptions: taxonSuggest,
59
+ getOptionLabel: (option) => (option ? `${option.sci_name} - ${option.tax_id}` : ''),
60
+ isOptionEqualToValue: (option, value) => option.tax_id === value.tax_id,
61
+ textLabel: 'Species / taxon ID',
62
+ }), []);
63
+ return (<PostRequestSelect {...speciesPostRequestSettings} fullWidth />);
64
+ }
65
+
66
+ function SequenceAccessionPicker({ assemblyAccession, setSequenceAccession }) {
67
+ const httpClient = useHttpClient();
68
+
69
+ const url = `https://api.ncbi.nlm.nih.gov/datasets/v2alpha/genome/accession/${assemblyAccession}/sequence_reports`
70
+ return (
71
+ <FormControl fullWidth>
72
+ <GetRequestMultiSelect
73
+ getOptionsFromResponse={(data) => data.reports}
74
+ httpClient={httpClient}
75
+ url={url}
76
+ label="Chromosome"
77
+ messages={{ loadingMessage: 'Loading chromosomes...', errorMessage: 'Could not load chromosomes' }}
78
+ onChange={(value, options) => { setSequenceAccession(options.find((o) => `${o.chr_name} - ${o.refseq_accession}` === value).refseq_accession)}}
79
+ getOptionLabel={({ chr_name, refseq_accession }) => `${chr_name} - ${refseq_accession}`}
80
+ multiple={false}
81
+ autoComplete={false}
82
+ />
83
+ </FormControl>
84
+ );
85
+ }
86
+
87
+ // Extra component to be used in SourceGenomeRegion
88
+ function SourceGenomeRegionLocusOnReference({ source, requestStatus, sendPostRequest }) {
89
+ const { id: sourceId } = source;
90
+ const [gene, setGene] = React.useState(null);
91
+ const [species, setSpecies] = React.useState(null);
92
+ const [assemblyId, setAssemblyId] = React.useState('');
93
+ const upstreamBasesRef = React.useRef(null);
94
+ const downstreamBasesRef = React.useRef(null);
95
+
96
+ const onSubmit = (event) => {
97
+ event.preventDefault();
98
+ const requestData = formatBackendPayloadWithGene(assemblyId, gene, Number(upstreamBasesRef.current.value), Number(downstreamBasesRef.current.value));
99
+ requestData.id = sourceId;
100
+ sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
101
+ };
102
+
103
+ // Reset gene when species changes
104
+ React.useEffect(() => {
105
+ setGene(null);
106
+ }, [species]);
107
+
108
+ return (
109
+ <form onSubmit={onSubmit}>
110
+ <SpeciesPicker {...{ setSpecies, setAssemblyId }} />
111
+ {assemblyId && (
112
+ <>
113
+ <KnownAssemblyField assemblyId={assemblyId} />
114
+ <SourceGenomeRegionSelectGene {...{ gene, upstreamBasesRef, downstreamBasesRef, setGene, assemblyId }} />
115
+ {gene && (
116
+ <SubmitButtonBackendAPI
117
+ requestStatus={requestStatus}
118
+ {...(import.meta.env.VITE_UMAMI_WEBSITE_ID && { "data-umami-event": "submit-genome-region-locus-reference" })}
119
+ >Submit</SubmitButtonBackendAPI>
120
+ )}
121
+ </>
122
+ )}
123
+ { (species && assemblyId === '') && (
124
+ <Alert sx={{ alignItems: 'center' }} severity="error">
125
+ The selected species does not have a reference assembly.
126
+ </Alert>
127
+ )}
128
+ </form>
129
+ );
130
+ }
131
+
132
+ function KnownSpeciesField({ species }) {
133
+ return (
134
+ <FormControl fullWidth>
135
+ <TextField
136
+ label="Species"
137
+ value={`${species.organism_name} - ${species.tax_id}`}
138
+ disabled
139
+ />
140
+ </FormControl>
141
+ );
142
+ }
143
+
144
+ function KnownAssemblyField({ assemblyId }) {
145
+ return (
146
+ <FormControl fullWidth>
147
+ <TextField
148
+ label="Assembly ID"
149
+ value={assemblyId}
150
+ disabled
151
+ />
152
+ </FormControl>
153
+ );
154
+ }
155
+
156
+ function AssemblyIdSelector({ setAssemblyId, setHasAnnotation = () => {}, onAssemblyIdChange = () => {} }) {
157
+ const [exactMatch, setExactMatch] = React.useState(false);
158
+ const [newerAssembly, setNewerAssembly] = React.useState(false);
159
+ const [species, setSpecies] = React.useState(null);
160
+ const [pairedAccessionWithAnnotation, setPairedAccessionWithAnnotation] = React.useState('');
161
+
162
+ const onChange = async (userInput, resp) => {
163
+ setPairedAccessionWithAnnotation('');
164
+ setSpecies(resp === null ? null : resp.species);
165
+ if (resp === null) {
166
+ setAssemblyId('');
167
+ setExactMatch(true);
168
+ } else if (resp.exactMatch) {
169
+ setAssemblyId(userInput);
170
+ setExactMatch(true);
171
+ } else {
172
+ setAssemblyId(resp.newerAssembly);
173
+ setExactMatch(false);
174
+ }
175
+
176
+ setHasAnnotation(resp !== null && resp.hasAnnotation);
177
+ setNewerAssembly(resp !== null && resp.newerAssembly);
178
+ onAssemblyIdChange();
179
+ if (resp !== null && !resp.hasAnnotation && resp.pairedAccession) {
180
+ const pairedAccessionInfo = await getInfoFromAssemblyId(resp.pairedAccession);
181
+ if (pairedAccessionInfo !== null && pairedAccessionInfo.hasAnnotation) {
182
+ setPairedAccessionWithAnnotation(resp.pairedAccession);
183
+ }
184
+ }
185
+ };
186
+
187
+ return (
188
+ <>
189
+ <TextFieldValidate onChange={onChange} getterFunction={getInfoFromAssemblyId} label="Assembly ID" defaultHelperText="Example ID: GCA_000002945.3" />
190
+ {newerAssembly && (
191
+ <Alert severity="warning">
192
+ {!exactMatch ? 'Using assembly ID' : 'Newer assembly exists:'}
193
+ {' '}
194
+ <a href={`https://www.ncbi.nlm.nih.gov/datasets/genome/${newerAssembly}`} target="_blank" rel="noopener noreferrer">{newerAssembly}</a>
195
+ </Alert>
196
+ )}
197
+ {pairedAccessionWithAnnotation && (
198
+ <Alert severity="warning">
199
+ Equivalent assembly <a href={`https://www.ncbi.nlm.nih.gov/datasets/genome/${pairedAccessionWithAnnotation}`} target="_blank" rel="noopener noreferrer">{pairedAccessionWithAnnotation}</a> has annotation.
200
+ </Alert>
201
+ )}
202
+ {species && <KnownSpeciesField species={species} />}
203
+ </>
204
+ );
205
+ }
206
+
207
+ // Extra component to be used in SourceGenomeRegion
208
+ function SourceGenomeRegionLocusOnOther({ source, requestStatus, sendPostRequest }) {
209
+ const { id: sourceId } = source;
210
+ const [gene, setGene] = React.useState(null);
211
+ const [assemblyId, setAssemblyId] = React.useState('');
212
+ const [hasAnnotation, setHasAnnotation] = React.useState(false);
213
+ const upstreamBasesRef = React.useRef(null);
214
+ const downstreamBasesRef = React.useRef(null);
215
+
216
+ const onSubmit = (event) => {
217
+ event.preventDefault();
218
+ const requestData = formatBackendPayloadWithGene(assemblyId, gene, Number(upstreamBasesRef.current.value), Number(downstreamBasesRef.current.value));
219
+ requestData.id = sourceId;
220
+ sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
221
+ };
222
+
223
+ return (
224
+ <form onSubmit={onSubmit}>
225
+ <AssemblyIdSelector {...{ setAssemblyId, setHasAnnotation, onAssemblyIdChange: () => setGene(null) }} />
226
+ {assemblyId && hasAnnotation && (
227
+ <>
228
+ <SourceGenomeRegionSelectGene {...{ gene, upstreamBasesRef, downstreamBasesRef, setGene, assemblyId }} />
229
+ {gene && (
230
+ <SubmitButtonBackendAPI
231
+ requestStatus={requestStatus}
232
+ {...(import.meta.env.VITE_UMAMI_WEBSITE_ID && { "data-umami-event": "submit-genome-region-locus-other" })}
233
+ >Submit</SubmitButtonBackendAPI>
234
+ )}
235
+ </>
236
+ )}
237
+ {assemblyId && !hasAnnotation && (<Alert severity="error">The selected assembly has no gene annotations</Alert>)}
238
+ </form>
239
+ );
240
+ }
241
+
242
+ // Extra component to be used in SourceGenomeRegion
243
+ function SourceGenomeRegionCustomCoordinates({ source, requestStatus, sendPostRequest, selectionMode }) {
244
+ // https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?dbfrom=nuccore&db=assembly&id=CM041205.1&idtype=acc
245
+ const { id: sourceId } = source;
246
+ const [species, setSpecies] = React.useState(null);
247
+ const [sequenceAccession, setSequenceAccession] = React.useState('');
248
+ const [assemblyId, setAssemblyId] = React.useState('');
249
+ const noError = { start: null, end: null, strand: null };
250
+ const [formError, setFormError] = React.useState({ ...noError });
251
+ // I don't manage to use refs for the Select component
252
+ const [coords, setCoords] = React.useState({ start: '', end: '', strand: '' });
253
+
254
+ React.useEffect(() => {
255
+ // Clear the form when the selection mode changes
256
+ setSpecies(null);
257
+ setSequenceAccession('');
258
+ setAssemblyId('');
259
+ setCoords({ start: '', end: '', strand: '' });
260
+ }, [selectionMode]);
261
+
262
+ const onSubmit = (event) => {
263
+ event.preventDefault();
264
+ if (coords.start === '') {
265
+ setFormError({ ...noError, start: 'Field required' });
266
+ return;
267
+ }
268
+ if (coords.end === '') {
269
+ setFormError({ ...noError, end: 'Field required' });
270
+ return;
271
+ }
272
+ if (coords.strand === '') {
273
+ setFormError({ ...noError, strand: 'Field required' });
274
+ return;
275
+ }
276
+ // Start must be greater than zero
277
+ if (Number(coords.start) < 1) {
278
+ setFormError({ ...noError, start: 'Start must be greater than zero' });
279
+ return;
280
+ }
281
+ if (Number(coords.start) >= Number(coords.end)) {
282
+ setFormError({ ...noError, end: 'End must be greater than start' });
283
+ return;
284
+ }
285
+
286
+ setFormError({ ...noError });
287
+ const requestData = {
288
+ id: sourceId,
289
+ repository_id: sequenceAccession,
290
+ assembly_accession: assemblyId || null,
291
+ coordinates: formatSequenceLocationString(coords.start, coords.end, coords.strand === 'plus' ? 1 : -1),
292
+ };
293
+ sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
294
+ };
295
+
296
+ const onSequenceAccessionChange = async (userInput, resp) => {
297
+ setFormError({ ...noError });
298
+ setCoords({ start: '', end: '', strand: '' });
299
+ if (resp === null) {
300
+ setSpecies(null);
301
+ setSequenceAccession('');
302
+ return;
303
+ }
304
+ setSpecies(resp.species || null);
305
+ setSequenceAccession(resp.sequenceAccessionStandard);
306
+ };
307
+
308
+ return (
309
+ <form onSubmit={onSubmit}>
310
+ {(selectionMode === 'custom_sequence_accession') && (
311
+ (
312
+ <>
313
+ <TextFieldValidate onChange={onSequenceAccessionChange} getterFunction={getInfoFromSequenceAccession} label="Sequence accession" defaultHelperText="Example ID: NC_003424.3" />
314
+ {species && <KnownSpeciesField species={species} />}
315
+ </>
316
+ )
317
+ )}
318
+ {(selectionMode === 'custom_reference') && (
319
+ <>
320
+ {assemblyId && <KnownAssemblyField assemblyId={assemblyId} />}
321
+ <SpeciesPicker {...{ setSpecies, setAssemblyId }} />
322
+ </>
323
+ )}
324
+ {(selectionMode === 'custom_other') && (
325
+ <AssemblyIdSelector {...{ setAssemblyId, onAssemblyIdChange: () => { setFormError({ ...noError }); setSequenceAccession(''); } }} />
326
+ )}
327
+ {assemblyId && ['custom_reference', 'custom_other'].includes(selectionMode) && (
328
+ <SequenceAccessionPicker {...{ assemblyAccession: assemblyId, setSequenceAccession }} />
329
+ )}
330
+ {sequenceAccession && (
331
+ <>
332
+ <Box component="fieldset" sx={{ p: 1, mb: 1 }} style={{ borderRadius: '.5em', boxShadow: null }}>
333
+ <legend><FormLabel>Sequence coordinates</FormLabel></legend>
334
+ <FormControl fullWidth>
335
+ <TextField
336
+ fullWidth
337
+ label="Start"
338
+ value={coords.start}
339
+ onChange={(event) => setCoords((prev) => ({ ...prev, start: event.target.value }))}
340
+ type="number"
341
+ error={formError.start !== null}
342
+ helperText={formError.start}
343
+ />
344
+ </FormControl>
345
+ <FormControl fullWidth>
346
+ <TextField
347
+ fullWidth
348
+ label="End"
349
+ value={coords.end}
350
+ onChange={(event) => setCoords((prev) => ({ ...prev, end: event.target.value }))}
351
+ type="number"
352
+ error={formError.end !== null}
353
+ helperText={formError.end}
354
+ />
355
+ </FormControl>
356
+ <FormControl fullWidth>
357
+ <InputLabel error={formError.strand !== null} id={`selection-mode-${sourceId}-strand-label`}>Strand</InputLabel>
358
+ <Select
359
+ labelId={`selection-mode-${sourceId}-strand-label`}
360
+ label="Strand"
361
+ value={coords.strand}
362
+ onChange={(event) => setCoords((prev) => ({ ...prev, strand: event.target.value }))}
363
+ error={formError.strand !== null}
364
+ >
365
+ <MenuItem value="plus">plus</MenuItem>
366
+ <MenuItem value="minus">minus</MenuItem>
367
+ </Select>
368
+ <FormHelperText error={formError.strand !== null}>{formError.strand}</FormHelperText>
369
+ </FormControl>
370
+ </Box>
371
+ <SubmitButtonBackendAPI
372
+ requestStatus={requestStatus}
373
+ {...(import.meta.env.VITE_UMAMI_WEBSITE_ID && { "data-umami-event": "submit-genome-region-custom-coordinates" })}
374
+ >Submit</SubmitButtonBackendAPI>
375
+ </>
376
+ )}
377
+ </form>
378
+ );
379
+ }
380
+
381
+ function SourceGenomeRegionSelectGene({ gene, upstreamBasesRef, downstreamBasesRef, setGene, assemblyId }) {
382
+ const [error, setError] = React.useState('');
383
+ const genePostRequestSettings = React.useMemo(() => ({
384
+ setValue: setGene,
385
+ getOptions: async (userInput) => {
386
+ try {
387
+ // We await the response to catch the error
388
+ setError('');
389
+ return await geneSuggest(assemblyId, userInput);
390
+ } catch (e) {
391
+ // Connection error
392
+ if (e.code === 'ERR_NETWORK' || e.response === undefined) {
393
+ setError('Connection error');
394
+ return [];
395
+ }
396
+ // Bad request
397
+ if (e.response?.status >= 400 && e.response?.status < 500) {
398
+ setError(e.response.data.message);
399
+ return [];
400
+ }
401
+ // Server error
402
+ if (e.response?.status >= 500) {
403
+ setError('NCBI server error');
404
+ return [];
405
+ }
406
+ // Here we are assuming that the assemblyId has been validated
407
+ setError('The assembly has no gene annotations');
408
+ return [];
409
+ }
410
+ },
411
+ getOptionLabel: ({ annotation }) => (annotation ? `${annotation.symbol} ${annotation.locus_tag === undefined ? '' : annotation.locus_tag} ${annotation.name}` : ''),
412
+ isOptionEqualToValue: (option, value) => option.locus_tag === value.locus_tag,
413
+ textLabel: 'Gene',
414
+ disableFiltering: true,
415
+ }), [setGene, assemblyId]);
416
+
417
+ return (
418
+ <>
419
+ <PostRequestSelect {...genePostRequestSettings} fullWidth />
420
+ {error && (<Alert severity="error">{error}</Alert>)}
421
+ {gene && (
422
+ <>
423
+ <FormControl fullWidth>
424
+ <TextField
425
+ label="Gene coordinates"
426
+ value={formatGeneCoords(gene)}
427
+ disabled
428
+ />
429
+ </FormControl>
430
+ <FormControl fullWidth>
431
+ <TextField
432
+ fullWidth
433
+ label="Upstream bases"
434
+ inputRef={upstreamBasesRef}
435
+ type="number"
436
+ defaultValue={1000}
437
+ />
438
+ </FormControl>
439
+ <FormControl fullWidth>
440
+ <TextField
441
+ fullWidth
442
+ label="Downstream bases"
443
+ inputRef={downstreamBasesRef}
444
+ type="number"
445
+ defaultValue={1000}
446
+ />
447
+ </FormControl>
448
+ </>
449
+ )}
450
+ </>
451
+ );
452
+ }
453
+
454
+ function SourceGenomeRegion({ source, requestStatus, sendPostRequest }) {
455
+ const { id: sourceId } = source;
456
+ const [selectionMode, setSelectionMode] = React.useState('');
457
+ const changeSelectionMode = (event) => { setSelectionMode(event.target.value); };
458
+
459
+ return (
460
+ <>
461
+ <form>
462
+ <FormControl fullWidth>
463
+ <InputLabel id={`selection-mode-${sourceId}-label`}>Type of region</InputLabel>
464
+ <Select
465
+ value={selectionMode}
466
+ onChange={changeSelectionMode}
467
+ labelId={`selection-mode-${sourceId}-label`}
468
+ label="Type of region"
469
+ >
470
+ <MenuItem value="reference_genome">Locus in reference genome</MenuItem>
471
+ <MenuItem value="other_assembly">Locus in other assembly</MenuItem>
472
+ <MenuItem value="custom_reference">Custom coordinates in reference genome</MenuItem>
473
+ <MenuItem value="custom_other">Custom coordinates in other assembly</MenuItem>
474
+ <MenuItem value="custom_sequence_accession">Custom coordinates in sequence accession</MenuItem>
475
+ </Select>
476
+ </FormControl>
477
+ </form>
478
+ {selectionMode === 'reference_genome' && (<SourceGenomeRegionLocusOnReference {...{ source, requestStatus, sendPostRequest }} />)}
479
+ {selectionMode === 'other_assembly' && (<SourceGenomeRegionLocusOnOther {...{ source, requestStatus, sendPostRequest }} />)}
480
+ {selectionMode.startsWith('custom') && (<SourceGenomeRegionCustomCoordinates {...{ source, requestStatus, sendPostRequest, selectionMode }} />)}
481
+
482
+ </>
483
+ );
484
+ }
485
+
486
+ export { SourceGenomeRegion, AssemblyIdSelector, SpeciesPicker, SequenceAccessionPicker };
@@ -0,0 +1,125 @@
1
+ import React from 'react';
2
+ import { useDispatch, useSelector } from 'react-redux';
3
+ import { FormControl, InputAdornment, TextField } from '@mui/material';
4
+ import { isEqual } from 'lodash-es';
5
+ import SingleInputSelector from './SingleInputSelector';
6
+ import { cloningActions } from '@opencloning/store/cloning';
7
+ import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
8
+ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
9
+ import SelectPrimerForm from '../primers/SelectPrimerForm';
10
+
11
+ // A component representing the ligation of several fragments
12
+ function SourceHomologousRecombination({ source, requestStatus, sendPostRequest }) {
13
+ const isCrispr = source.type === 'CRISPRSource';
14
+ const { id: sourceId, input } = source;
15
+ const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), isEqual);
16
+ const inputsAreNotTemplates = inputSequences.every((sequence) => sequence.type !== 'TemplateSequence');
17
+ const [template, setTemplate] = React.useState(input.length > 0 ? input[0].sequence : null);
18
+ const [insert, setInsert] = React.useState(input.length > 1 ? input[1].sequence : null);
19
+ const [selectedPrimers, setSelectedPrimers] = React.useState([]);
20
+ const { updateSource, setCurrentTab } = cloningActions;
21
+ const dispatch = useDispatch();
22
+
23
+ const primers = useSelector((state) => state.cloning.primers, isEqual);
24
+
25
+ const allowSubmit = (template !== null && insert !== null) && (isCrispr ? selectedPrimers.length > 0 : true) && inputsAreNotTemplates;
26
+ const minimalHomologyRef = React.useRef(null);
27
+ const onSubmit = (event) => {
28
+ event.preventDefault();
29
+ const requestData = {
30
+ source: { id: sourceId, input, output_name: source.output_name },
31
+ sequences: inputSequences,
32
+ };
33
+ const config = { params: { minimal_homology: minimalHomologyRef.current.value } };
34
+ if (isCrispr) {
35
+ requestData.guides = selectedPrimers.map((primerId) => primers.find((p) => p.id === primerId));
36
+ sendPostRequest({ endpoint: 'crispr', requestData, config, source });
37
+ } else {
38
+ sendPostRequest({ endpoint: 'homologous_recombination', requestData, config, source });
39
+ }
40
+ };
41
+
42
+ const onTemplateChange = (event) => {
43
+ setTemplate(Number(event.target.value));
44
+ const newInput = [Number(event.target.value)];
45
+ if (insert) {
46
+ newInput.push(insert);
47
+ }
48
+ dispatch(updateSource({ id: sourceId, input: newInput.map((id) => ({ sequence: id })) }));
49
+ };
50
+
51
+ const onInsertChange = (event) => {
52
+ if (event.target.value === '') {
53
+ setInsert(null);
54
+ dispatch(updateSource({ id: sourceId, input: [{ sequence: template }] }));
55
+ } else {
56
+ setInsert(Number(event.target.value));
57
+ dispatch(updateSource({ id: sourceId, input: [{ sequence: template }, { sequence: Number(event.target.value) }] }));
58
+ }
59
+ };
60
+
61
+ const goToPrimerTab = () => {
62
+ dispatch(setCurrentTab(1));
63
+ };
64
+
65
+ return (
66
+ <div className="homologous-recombination">
67
+ <form onSubmit={onSubmit}>
68
+ <FormControl fullWidth>
69
+ <SingleInputSelector
70
+ label="Template sequence"
71
+ {...{ selectedId: template,
72
+ onChange: onTemplateChange,
73
+ inputSequenceIds: [...new Set(input.map(({sequence}) => sequence))].filter(
74
+ (id) => id !== insert,
75
+ ) }}
76
+ />
77
+ </FormControl>
78
+ <FormControl fullWidth>
79
+ <SingleInputSelector
80
+ label="Insert sequence"
81
+ allowUnset
82
+ {...{ selectedId: insert,
83
+ onChange: onInsertChange,
84
+ inputSequenceIds: [...new Set(input.map(({sequence}) => sequence))].filter(
85
+ (id) => id !== template,
86
+ ) }}
87
+ />
88
+ </FormControl>
89
+ <FormControl fullWidth>
90
+ <TextField
91
+ label="Minimal homology length"
92
+ inputRef={minimalHomologyRef}
93
+ type="number"
94
+ defaultValue={40}
95
+ InputProps={{
96
+ endAdornment: <InputAdornment position="end">bp</InputAdornment>,
97
+ sx: { '& input': { textAlign: 'center' } },
98
+ }}
99
+ />
100
+ </FormControl>
101
+ {isCrispr && (
102
+ <SelectPrimerForm
103
+ primers={primers}
104
+ selected={selectedPrimers}
105
+ onChange={setSelectedPrimers}
106
+ label="Select gRNAs (from primers)"
107
+ goToPrimerTab={goToPrimerTab}
108
+ multiple
109
+ />
110
+ )}
111
+ { allowSubmit && (
112
+ <SubmitButtonBackendAPI
113
+ requestStatus={requestStatus}
114
+ color="primary"
115
+ {...(import.meta.env.VITE_UMAMI_WEBSITE_ID && { "data-umami-event": isCrispr ? "submit-crispr" : "submit-homologous-recombination" })}
116
+ >
117
+ {isCrispr ? 'Perform CRISPR' : 'Recombine'}
118
+ </SubmitButtonBackendAPI>
119
+ )}
120
+ </form>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ export default SourceHomologousRecombination;
@@ -0,0 +1,60 @@
1
+ import React from 'react'
2
+ import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
3
+ import { geneSuggest } from '@opencloning/utils/ncbiRequests';
4
+ import { formatBackendPayloadWithGene } from './SourceGenomeRegion';
5
+ import { Box, CircularProgress } from '@mui/material';
6
+
7
+ function SourceKnownGenomeRegion({ source, requestStatus, sendPostRequest }) {
8
+ // A source where we pass a complete GenomeCoordinatesSource, so we just have to make the request
9
+ const [error, setError] = React.useState('');
10
+ const [connectAttempt, setConnectAttempt] = React.useState(0);
11
+ React.useEffect(() => {
12
+ async function makeRequest() {
13
+ setError(false);
14
+ if (source.locus_tag && source.assembly_accession) {
15
+ let resp;
16
+ try {
17
+ resp = await geneSuggest(source.assembly_accession, source.locus_tag)
18
+ } catch (e) {
19
+ setError(true);
20
+ return;
21
+ }
22
+ if (resp.length === 0) {
23
+ setError(true);
24
+ return;
25
+ }
26
+ const requestData = formatBackendPayloadWithGene(source.assembly_accession, resp[0], source.padding, source.padding);
27
+ requestData.id = source.id;
28
+ sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
29
+ }
30
+ else {
31
+ const requestData = {
32
+ ...source,
33
+ type: 'GenomeCoordinatesSource',
34
+ };
35
+ sendPostRequest({ endpoint: 'genome_coordinates', requestData, source });
36
+ }
37
+ }
38
+ makeRequest();
39
+ }, [connectAttempt]);
40
+
41
+ if (requestStatus.status === 'error' || error) {
42
+
43
+ return (
44
+ <div>
45
+ <div style={{ marginBottom: '1em', marginTop: '2em' }}>Could not retrieve genome sequence.</div>
46
+ <SubmitButtonBackendAPI requestStatus={requestStatus} onClick={() => setConnectAttempt(connectAttempt + 1)}>Retry</SubmitButtonBackendAPI>
47
+ </div>
48
+ )
49
+ }
50
+ return (
51
+ <div style={{ marginBottom: '1em', marginTop: '2em', textAlign: 'center' }}>
52
+ <div>Loading genome sequence...</div>
53
+ <Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
54
+ <CircularProgress />
55
+ </Box>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ export default SourceKnownGenomeRegion;