@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,164 @@
1
+ import { Alert, Box, FormControl, InputLabel, MenuItem, Select } from '@mui/material';
2
+ import React from 'react';
3
+ import { getReverseComplementSequenceString as reverseComplement } from '@teselagen/sequence-utils';
4
+ import { isEqual } from 'lodash-es';
5
+ import { parseFeatureLocation } from '@teselagen/bio-parsers';
6
+ import { useDispatch, useSelector } from 'react-redux';
7
+ import useGatewaySites from '../../../../hooks/useGatewaySites';
8
+ import useStoreEditor from '../../../../hooks/useStoreEditor';
9
+ import { cloningActions } from '@opencloning/store/cloning';
10
+ import { usePrimerDesign } from './PrimerDesignContext';
11
+ import RequestStatusWrapper from '../../../form/RequestStatusWrapper';
12
+
13
+ const knownCombinations = [
14
+ {
15
+ siteNames: ['attP4', 'attP1'],
16
+ spacers: ['GGGGACAACTTTGTATAGAAAAGTTGNN', reverseComplement('GGGGACTGCTTTTTTGTACAAACTTGN')],
17
+ orientation: [true, true],
18
+ message: 'Primers tails designed based on pDONR™ P4-P1R',
19
+ translationFrame: [4, 6],
20
+ },
21
+ {
22
+ siteNames: ['attP1', 'attP2'],
23
+ spacers: ['GGGGACAAGTTTGTACAAAAAAGCAGGCTNN', reverseComplement('GGGGACCACTTTGTACAAGAAAGCTGGGTN')],
24
+ orientation: [true, false],
25
+ message: 'Primers tails designed based on pDONR™ 221',
26
+ translationFrame: [4, 6],
27
+ },
28
+ {
29
+ siteNames: ['attP2', 'attP3'],
30
+ spacers: ['GGGGACAGCTTTCTTGTACAAAGTGGNN', reverseComplement('GGGGACAACTTTGTATAATAAAGTTGN')],
31
+ orientation: [false, false],
32
+ message: 'Primers tails designed based on pDONR™ P2R-P3',
33
+ translationFrame: [4, 6],
34
+ },
35
+ ];
36
+
37
+ function SiteSelect({ donorSites, site, setSite, label }) {
38
+ return (
39
+ <FormControl sx={{ minWidth: '10em' }}>
40
+ <InputLabel>{label}</InputLabel>
41
+ <Select
42
+ value={site ? `${site.siteName}-${site.location}` : ''}
43
+ onChange={(e) => setSite(
44
+ donorSites.find(({ siteName, location }) => `${siteName}-${location}` === e.target.value),
45
+ )}
46
+ label={label}
47
+ >
48
+ {donorSites.map(({ siteName, location }) => (
49
+ <MenuItem key={`${siteName}-${location}`} value={`${siteName}-${location}`}>
50
+ {siteName}
51
+ {' '}
52
+ {location}
53
+ </MenuItem>
54
+ ))}
55
+ </Select>
56
+ </FormControl>
57
+ );
58
+ }
59
+
60
+ const { setMainSequenceSelection } = cloningActions;
61
+
62
+ function GatewayRoiSelect({ id, greedy = false }) {
63
+ const [leftSite, setLeftSite] = React.useState(null);
64
+ const [rightSite, setRightSite] = React.useState(null);
65
+ const [donorSites, setDonorSites] = React.useState([]);
66
+ const { requestStatus, attemptAgain, sites } = useGatewaySites({ target: id, greedy });
67
+ const { updateStoreEditor } = useStoreEditor();
68
+ const donorVectorSequenceLength = useSelector((state) => state.cloning.teselaJsonCache[id].sequence.length);
69
+ const dispatch = useDispatch();
70
+ const { primerDesignSettings, setSpacers } = usePrimerDesign();
71
+ const [gatewaySelection, setGatewaySelection] = React.useState(null);
72
+ const editorSelection = useSelector((state) => state.cloning.mainSequenceSelection, isEqual);
73
+
74
+ const { knownCombination, setKnownCombination } = primerDesignSettings;
75
+
76
+ const handleKnownCombinationChange = (newKnownCombination, selection) => {
77
+ setGatewaySelection(selection);
78
+ if (newKnownCombination) {
79
+ setKnownCombination(newKnownCombination);
80
+ setSpacers(newKnownCombination.spacers);
81
+ } else {
82
+ setKnownCombination(null);
83
+ setSpacers(['', '']);
84
+ }
85
+ };
86
+ React.useEffect(() => {
87
+ if (requestStatus.status === 'success') {
88
+ setDonorSites(sites.filter(({ siteName }) => siteName.startsWith('attP')));
89
+ }
90
+ }, [sites]);
91
+
92
+ React.useEffect(() => {
93
+ if (gatewaySelection && !isEqual(editorSelection, gatewaySelection)) {
94
+ updateStoreEditor('mainEditor', null, gatewaySelection.selectionLayer);
95
+ dispatch(setMainSequenceSelection(gatewaySelection));
96
+ }
97
+ }, [editorSelection, gatewaySelection]);
98
+
99
+ const checkKnownCombination = React.useCallback((newLeftSite, newRightSite) => {
100
+ if (newLeftSite && newRightSite) {
101
+ const leftSiteLocation = parseFeatureLocation(newLeftSite.location, 0, 0, 0, 1, donorVectorSequenceLength)[0];
102
+ const rightSiteLocation = parseFeatureLocation(newRightSite.location, 0, 0, 0, 1, donorVectorSequenceLength)[0];
103
+ const selectionLayer = { start: leftSiteLocation.start, end: rightSiteLocation.end };
104
+ const selection = { selectionLayer, caretPosition: -1 };
105
+ const siteNames = [newLeftSite.siteName, newRightSite.siteName];
106
+ const orientation = [newLeftSite.location.includes('(+)'), newRightSite.location.includes('(+)')];
107
+ const knownCombinationForward = knownCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNames) && isEqual(knownOrientation, orientation));
108
+ if (knownCombinationForward) {
109
+ handleKnownCombinationChange(knownCombinationForward, selection);
110
+ return;
111
+ }
112
+ const siteNamesReverse = [newRightSite.siteName, newLeftSite.siteName];
113
+ const orientationReverse = [!newRightSite.location.includes('(+)'), !newLeftSite.location.includes('(+)')];
114
+ const knownCombinationReverse = knownCombinations.find(({ siteNames: knownSites, orientation: knownOrientation }) => isEqual(knownSites, siteNamesReverse) && isEqual(knownOrientation, orientationReverse));
115
+ if (knownCombinationReverse) {
116
+ handleKnownCombinationChange(knownCombinationReverse, selection);
117
+ return;
118
+ }
119
+ handleKnownCombinationChange(null, selection);
120
+ }
121
+ }, []);
122
+
123
+ const onSiteSelectLeft = React.useCallback((site) => {
124
+ setLeftSite(site);
125
+ if (rightSite === null || isEqual(rightSite, site)) {
126
+ // Find the first different one
127
+ const differentSite = donorSites.find(({ location }) => location !== site.location);
128
+ setRightSite(differentSite);
129
+ checkKnownCombination(site, differentSite);
130
+ return;
131
+ }
132
+ checkKnownCombination(site, rightSite);
133
+ }, [rightSite, donorSites]);
134
+
135
+ const onSiteSelectRight = React.useCallback((site) => {
136
+ setRightSite(site);
137
+ if (leftSite === null || isEqual(leftSite, site)) {
138
+ // Find the first different one
139
+ const differentSite = donorSites.find(({ location }) => location !== site.location);
140
+ setLeftSite(differentSite);
141
+ checkKnownCombination(differentSite, site);
142
+ return;
143
+ }
144
+
145
+ checkKnownCombination(leftSite, site);
146
+ }, [leftSite, donorSites]);
147
+ return (
148
+ <RequestStatusWrapper requestStatus={requestStatus} retry={attemptAgain}>
149
+ {donorSites.length < 2 && (<Alert severity="error">The sequence must have at least two AttP sites</Alert>)}
150
+ {donorSites.length >= 2 && (
151
+ <>
152
+ <Box sx={{ my: 2, '& > div': { mx: 1 } }}>
153
+ <SiteSelect donorSites={donorSites} site={leftSite} setSite={onSiteSelectLeft} label="Left attP site" />
154
+ <SiteSelect donorSites={donorSites} site={rightSite} setSite={onSiteSelectRight} label="Right attP site" />
155
+ </Box>
156
+ {knownCombination && (<Alert sx={{ width: '80%', margin: 'auto', mb: 2 }} severity="info">{knownCombination.message}</Alert>)}
157
+ {knownCombination === null && (leftSite && rightSite) && (<Alert sx={{ width: '80%', margin: 'auto', mb: 2 }} severity="error">No recommended primer tails found</Alert>)}
158
+ </>
159
+ )}
160
+ </RequestStatusWrapper>
161
+ );
162
+ }
163
+
164
+ export default GatewayRoiSelect;
@@ -0,0 +1,37 @@
1
+ import { FormControlLabel, FormLabel, Radio, RadioGroup } from '@mui/material';
2
+ import React from 'react';
3
+ import { usePrimerDesign } from './PrimerDesignContext';
4
+
5
+ function OrientationPicker({ id, index }) {
6
+ const { designType, fragmentOrientations, handleFragmentOrientationChange, templateSequenceNames } = usePrimerDesign();
7
+ const sequenceName = templateSequenceNames[index];
8
+ let label = sequenceName && sequenceName !== 'name' ? `Seq. ${id} (${sequenceName})` : `Seq. ${id}`;
9
+ if (designType === 'homologous_recombination') {
10
+ label = 'Orientation of insert';
11
+ }
12
+ return (
13
+ <table style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse', margin: '0', padding: '0' }}>
14
+ <tbody>
15
+ <tr style={{ border: 'none' }}>
16
+ <td style={{ width: '40%', verticalAlign: 'middle', textAlign: 'right', paddingRight: '8px', border: 'none', padding: '2px 8px 2px 0' }}>
17
+ <FormLabel id={`fragment-orientation-label-${index}`}>{label}</FormLabel>
18
+ </td>
19
+ <td style={{ width: '60%', verticalAlign: 'middle', textAlign: 'left', border: 'none', padding: '2px 0' }}>
20
+ <RadioGroup
21
+ row
22
+ aria-labelledby={`fragment-orientation-label-${index}`}
23
+ name={`fragment-orientation-${index}`}
24
+ value={fragmentOrientations[index]}
25
+ onChange={(e) => handleFragmentOrientationChange(index, e.target.value)}
26
+ >
27
+ <FormControlLabel value="forward" control={<Radio />} label="Forward" />
28
+ <FormControlLabel value="reverse" control={<Radio />} label="Reverse" />
29
+ </RadioGroup>
30
+ </td>
31
+ </tr>
32
+ </tbody>
33
+ </table>
34
+ );
35
+ }
36
+
37
+ export default OrientationPicker;
@@ -0,0 +1,369 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { batch, useDispatch, useSelector, useStore } from 'react-redux';
3
+ import { updateEditor } from '@teselagen/ove';
4
+ import { isEqual } from 'lodash-es';
5
+ import useBackendRoute from '../../../../hooks/useBackendRoute';
6
+ import { selectedRegion2SequenceLocation } from '@opencloning/utils/selectedRegionUtils';
7
+ import error2String from '@opencloning/utils/error2String';
8
+ import useStoreEditor from '../../../../hooks/useStoreEditor';
9
+ import { cloningActions } from '@opencloning/store/cloning';
10
+ import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
11
+ import { ebicTemplateAnnotation, joinSequencesIntoSingleSequence, simulateHomologousRecombination } from '@opencloning/utils/sequenceManipulation';
12
+ import useHttpClient from '../../../../hooks/useHttpClient';
13
+
14
+ function changeValueAtIndex(current, index, newValue) {
15
+ return current.map((_, i) => (i === index ? newValue : current[i]));
16
+ }
17
+
18
+ const PrimerDesignContext = React.createContext();
19
+
20
+ export function PrimerDesignProvider({ children, designType, sequenceIds, primerDesignSettings, steps }) {
21
+ let templateSequenceIds = sequenceIds;
22
+ if (designType === 'homologous_recombination' || designType === 'gateway_bp') {
23
+ templateSequenceIds = sequenceIds.slice(0, 1);
24
+ }
25
+
26
+ const [primers, setPrimers] = useState([]);
27
+ const [rois, setRois] = useState(Array(sequenceIds.length).fill(null));
28
+ const [error, setError] = useState('');
29
+ const [selectedTab, setSelectedTab] = useState(0);
30
+ const [sequenceProduct, setSequenceProduct] = useState(null);
31
+ const [fragmentOrientations, setFragmentOrientations] = useState(Array(templateSequenceIds.length).fill('forward'));
32
+ const [circularAssembly, setCircularAssembly] = useState(false);
33
+ const [spacers, setSpacers] = useState(Array(templateSequenceIds.length + 1).fill(''));
34
+
35
+ const spacersAreValid = React.useMemo(() => spacers.every((spacer) => !stringIsNotDNA(spacer)), [spacers]);
36
+ const sequenceNames = useSelector((state) => sequenceIds.map((id) => state.cloning.teselaJsonCache[id].name), isEqual);
37
+ const templateSequenceNames = useSelector((state) => templateSequenceIds.map((id) => state.cloning.teselaJsonCache[id].name), isEqual);
38
+ const mainSequenceId = useSelector((state) => state.cloning.mainSequenceId);
39
+
40
+ const store = useStore();
41
+ const backendRoute = useBackendRoute();
42
+ const dispatch = useDispatch();
43
+ const { updateStoreEditor } = useStoreEditor();
44
+ const { setMainSequenceId, addPrimersToPCRSource, setCurrentTab } = cloningActions;
45
+ const httpClient = useHttpClient();
46
+
47
+ const getSubmissionPreventedMessage = () => {
48
+ if (rois.some((region) => region === null)) {
49
+ return 'Not all regions have been selected';
50
+ } if (primerDesignSettings.error) {
51
+ return primerDesignSettings.error;
52
+ } if (spacers.some((spacer) => stringIsNotDNA(spacer))) {
53
+ return 'Spacer sequences not valid';
54
+ }
55
+ return '';
56
+ };
57
+
58
+ const submissionPreventedMessage = getSubmissionPreventedMessage();
59
+
60
+ React.useEffect(() => {
61
+ let newSequenceProduct = null;
62
+ if (submissionPreventedMessage === '') {
63
+ const { teselaJsonCache } = store.getState().cloning;
64
+ const sequences = sequenceIds.map((id) => teselaJsonCache[id]);
65
+ if (designType === 'simple_pair' || designType === 'restriction_ligation') {
66
+ const enzymeSpacers = designType === 'restriction_ligation' ? primerDesignSettings.enzymeSpacers : ['', ''];
67
+ const extendedSpacers = [enzymeSpacers[0] + spacers[0], spacers[1] + enzymeSpacers[1]];
68
+ newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, extendedSpacers, circularAssembly, 'primer tail');
69
+ newSequenceProduct.name = 'PCR product';
70
+ } else if (designType === 'gibson_assembly') {
71
+ newSequenceProduct = joinSequencesIntoSingleSequence(sequences, rois.map((s) => s.selectionLayer), fragmentOrientations, spacers, circularAssembly);
72
+ newSequenceProduct.name = 'Gibson Assembly product';
73
+ } else if (designType === 'homologous_recombination') {
74
+ newSequenceProduct = simulateHomologousRecombination(sequences[0], sequences[1], rois, fragmentOrientations[0] === 'reverse', spacers);
75
+ newSequenceProduct.name = 'Homologous recombination product';
76
+ } else if (designType === 'gateway_bp') {
77
+ newSequenceProduct = joinSequencesIntoSingleSequence([sequences[0]], [rois[0].selectionLayer], fragmentOrientations, spacers, false, 'primer tail');
78
+ newSequenceProduct.name = 'PCR product';
79
+ const { knownCombination } = primerDesignSettings;
80
+ const leftFeature = {
81
+ start: knownCombination.translationFrame[0],
82
+ end: spacers[0].length - 1,
83
+ type: 'CDS',
84
+ name: 'translation frame',
85
+ strand: 1,
86
+ forward: true,
87
+ };
88
+ const nbAas = Math.floor((spacers[1].length - knownCombination.translationFrame[1]) / 3);
89
+ const rightStart = newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - nbAas * 3;
90
+ const rightFeature = {
91
+ start: rightStart,
92
+ end: newSequenceProduct.sequence.length - knownCombination.translationFrame[1] - 1,
93
+ type: 'CDS',
94
+ name: 'translation frame',
95
+ strand: 1,
96
+ forward: true,
97
+ };
98
+ newSequenceProduct.features.push(leftFeature);
99
+ newSequenceProduct.features.push(rightFeature);
100
+ setSequenceProduct(newSequenceProduct);
101
+ } else if (designType === 'ebic') {
102
+ newSequenceProduct = ebicTemplateAnnotation(sequences[0], rois[0].selectionLayer, primerDesignSettings);
103
+ setSequenceProduct(newSequenceProduct);
104
+ }
105
+ }
106
+ setSequenceProduct(newSequenceProduct);
107
+ }, [rois, spacersAreValid, fragmentOrientations, circularAssembly, designType, spacers, primerDesignSettings]);
108
+
109
+ const onCircularAssemblyChange = (event) => {
110
+ setCircularAssembly(event.target.checked);
111
+ if (event.target.checked) {
112
+ // Remove the first spacer
113
+ setSpacers((current) => current.slice(1));
114
+ } else {
115
+ // Add it again
116
+ setSpacers((current) => ['', ...current]);
117
+ }
118
+ };
119
+
120
+ const onSelectRegion = (index, selectedRegion, allowSinglePosition = false) => {
121
+ const { caretPosition } = selectedRegion;
122
+ if (caretPosition === undefined) {
123
+ setRois((c) => changeValueAtIndex(c, index, null));
124
+ return 'You have to select a region in the sequence editor!';
125
+ }
126
+ if (caretPosition === -1) {
127
+ setRois((c) => changeValueAtIndex(c, index, selectedRegion));
128
+ return '';
129
+ }
130
+ if (allowSinglePosition) {
131
+ setRois((c) => changeValueAtIndex(c, index, selectedRegion));
132
+ return '';
133
+ }
134
+ setRois((c) => changeValueAtIndex(c, index, null));
135
+ return 'Select a region (not a single position) to amplify';
136
+ };
137
+
138
+ const onTabChange = (event, newValue) => {
139
+ setSelectedTab(newValue);
140
+ if (newValue < sequenceIds.length) {
141
+ updateStoreEditor('mainEditor', sequenceIds[newValue]);
142
+ dispatch(setMainSequenceId(sequenceIds[newValue]));
143
+ } else if (newValue === sequenceIds.length) {
144
+ updateEditor(store, 'mainEditor', { sequenceData: sequenceProduct || '', selectionLayer: {} });
145
+ } else {
146
+ updateStoreEditor('mainEditor', null);
147
+ }
148
+ };
149
+
150
+ const handleNext = () => {
151
+ onTabChange(null, selectedTab + 1);
152
+ };
153
+
154
+ const handleBack = () => {
155
+ onTabChange(null, selectedTab - 1);
156
+ };
157
+
158
+ const handleSelectRegion = (index, selectedRegion, allowSinglePosition = false) => {
159
+ const regionError = onSelectRegion(index, selectedRegion, allowSinglePosition);
160
+ if (!regionError) {
161
+ handleNext();
162
+ }
163
+ return regionError;
164
+ };
165
+
166
+ const handleFragmentOrientationChange = (index, orientation) => {
167
+ setFragmentOrientations((current) => changeValueAtIndex(current, index, orientation));
168
+ };
169
+
170
+ // Focus on the right sequence when changing tabs
171
+ useEffect(() => {
172
+ // Focus on the correct sequence
173
+ const mainSequenceIndex = sequenceIds.indexOf(mainSequenceId);
174
+ if (mainSequenceIndex !== -1) {
175
+ setSelectedTab(mainSequenceIndex);
176
+ }
177
+ }, [sequenceIds, mainSequenceId]);
178
+
179
+ // Update the sequence product in the editor if in the last tab
180
+ useEffect(() => {
181
+ const timeoutId = setTimeout(() => {
182
+ if (selectedTab === sequenceIds.length) {
183
+ updateEditor(store, 'mainEditor', { sequenceData: sequenceProduct || {} });
184
+ }
185
+ }, 500);
186
+
187
+ return () => clearTimeout(timeoutId);
188
+ }, [sequenceProduct, store]);
189
+
190
+ const designPrimers = async () => {
191
+ // Validate fragmentOrientations
192
+ fragmentOrientations.forEach((orientation) => {
193
+ if (orientation !== 'forward' && orientation !== 'reverse') {
194
+ throw new Error('Invalid fragment orientation');
195
+ }
196
+ });
197
+ const { cloning: { sequences, teselaJsonCache, globalPrimerSettings } } = store.getState();
198
+ let requestData;
199
+ let params;
200
+ let endpoint;
201
+ const paramsForRequest = Object.fromEntries(
202
+ Object.entries(primerDesignSettings)
203
+ .filter(([_, value]) => typeof value !== 'function'),
204
+ );
205
+ if (designType === 'gibson_assembly') {
206
+ params = {
207
+ ...paramsForRequest,
208
+ circular: circularAssembly,
209
+ };
210
+ requestData = {
211
+ pcr_templates: sequenceIds.map((id, index) => ({
212
+ sequence: sequences.find((e) => e.id === id),
213
+ location: selectedRegion2SequenceLocation(rois[index], teselaJsonCache[id].size),
214
+ forward_orientation: fragmentOrientations[index] === 'forward',
215
+ })),
216
+ spacers,
217
+ };
218
+ endpoint = 'gibson_assembly';
219
+ } else if (designType === 'homologous_recombination') {
220
+ const [pcrTemplateId, homologousRecombinationTargetId] = sequenceIds;
221
+ params = {
222
+ ...paramsForRequest,
223
+ };
224
+ requestData = {
225
+ pcr_template: {
226
+ sequence: sequences.find((e) => e.id === pcrTemplateId),
227
+ location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[pcrTemplateId].size),
228
+ forward_orientation: fragmentOrientations[0] === 'forward',
229
+ },
230
+ homologous_recombination_target: {
231
+ sequence: sequences.find((e) => e.id === homologousRecombinationTargetId),
232
+ location: selectedRegion2SequenceLocation(rois[1], teselaJsonCache[homologousRecombinationTargetId].size),
233
+ },
234
+ spacers,
235
+ };
236
+ endpoint = 'homologous_recombination';
237
+ } else if (designType === 'simple_pair' || designType === 'gateway_bp' || designType === 'restriction_ligation') {
238
+ const pcrTemplateId = sequenceIds[0];
239
+ params = {
240
+ ...paramsForRequest,
241
+ };
242
+
243
+ requestData = {
244
+ pcr_template: {
245
+ sequence: sequences.find((e) => e.id === pcrTemplateId),
246
+ location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[pcrTemplateId].size),
247
+ forward_orientation: fragmentOrientations[0] === 'forward',
248
+ },
249
+ spacers,
250
+ };
251
+ endpoint = 'simple_pair';
252
+ } else if (designType === 'ebic') {
253
+ endpoint = 'ebic';
254
+ requestData = {
255
+ template: {
256
+ sequence: sequences.find((e) => e.id === templateSequenceIds[0]),
257
+ location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[templateSequenceIds[0]].size),
258
+ // forward_orientation: fragmentOrientations[0] === 'forward',
259
+ },
260
+ };
261
+ params = {
262
+ ...paramsForRequest,
263
+ };
264
+ }
265
+ requestData.settings = globalPrimerSettings;
266
+ const url = backendRoute(`primer_design/${endpoint}`);
267
+
268
+ try {
269
+ const resp = await httpClient.post(url, requestData, { params });
270
+ setError('');
271
+ const newPrimers = resp.data.primers;
272
+ setPrimers(newPrimers);
273
+ handleNext();
274
+ return false;
275
+ } catch (thrownError) {
276
+ const errorMessage = error2String(thrownError);
277
+ setError(errorMessage);
278
+ return true;
279
+ }
280
+ };
281
+
282
+ const addPrimers = () => {
283
+ const pcrSources = store.getState().cloning.sources.filter((source) => source.type === 'PCRSource');
284
+ let usedPCRSources;
285
+ if (designType === 'ebic') {
286
+ usedPCRSources = pcrSources.filter((source) => source.input.some((i) => i.sequence === templateSequenceIds[0]));
287
+ } else {
288
+ usedPCRSources = templateSequenceIds.map((id) => pcrSources.find((source) => source.input.some((i) => i.sequence === id)));
289
+ }
290
+
291
+ batch(() => {
292
+ usedPCRSources.forEach((pcrSource, index) => {
293
+ dispatch(addPrimersToPCRSource({
294
+ fwdPrimer: primers[index * 2],
295
+ revPrimer: primers[index * 2 + 1],
296
+ sourceId: pcrSource.id,
297
+ }));
298
+ });
299
+ dispatch(setMainSequenceId(null));
300
+ dispatch(setCurrentTab(0));
301
+ });
302
+ setPrimers([]);
303
+ onTabChange(null, 0);
304
+ document.getElementById(`source-${usedPCRSources[0].id}`)?.scrollIntoView();
305
+ updateStoreEditor('mainEditor', null);
306
+ };
307
+
308
+ const value = React.useMemo(() => ({
309
+ primers,
310
+ error,
311
+ rois,
312
+ designPrimers,
313
+ setPrimers,
314
+ selectedTab,
315
+ onTabChange,
316
+ handleNext,
317
+ handleBack,
318
+ handleSelectRegion,
319
+ sequenceIds,
320
+ fragmentOrientations,
321
+ circularAssembly,
322
+ spacers,
323
+ setFragmentOrientations,
324
+ setSpacers,
325
+ handleFragmentOrientationChange,
326
+ sequenceNames,
327
+ primerDesignSettings,
328
+ submissionPreventedMessage,
329
+ addPrimers,
330
+ onCircularAssemblyChange,
331
+ templateSequenceIds,
332
+ templateSequenceNames,
333
+ designType,
334
+ steps,
335
+ }), [
336
+ primers,
337
+ error,
338
+ rois,
339
+ designPrimers,
340
+ setPrimers,
341
+ selectedTab,
342
+ onTabChange,
343
+ handleNext,
344
+ handleBack,
345
+ handleSelectRegion,
346
+ sequenceIds,
347
+ fragmentOrientations,
348
+ circularAssembly,
349
+ spacers,
350
+ setFragmentOrientations,
351
+ setSpacers,
352
+ handleFragmentOrientationChange,
353
+ sequenceNames,
354
+ primerDesignSettings,
355
+ submissionPreventedMessage,
356
+ addPrimers,
357
+ templateSequenceIds,
358
+ designType,
359
+ steps,
360
+ ]);
361
+
362
+ return (
363
+ <PrimerDesignContext.Provider value={value}>
364
+ {children}
365
+ </PrimerDesignContext.Provider>
366
+ );
367
+ }
368
+
369
+ export const usePrimerDesign = () => React.useContext(PrimerDesignContext);
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { PrimerDesignProvider } from './PrimerDesignContext';
3
+ import PrimerDesignForm from './PrimerDesignForm';
4
+ import useEBICPrimerDesignSettings from './useEBICPrimerDesignSettings';
5
+ import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
6
+
7
+ function PrimerDesignEBIC({ pcrSources }) {
8
+ const templateSequenceId = getPcrTemplateSequenceId(pcrSources[0]);
9
+ const sequenceIds = React.useMemo(() => [templateSequenceId], [templateSequenceId]);
10
+ const primerDesignSettings = useEBICPrimerDesignSettings();
11
+
12
+ const steps = React.useMemo(() => [
13
+ { label: 'Region of interest', description: 'Select in the editor the region to be replaced' },
14
+ ], []);
15
+
16
+ return (
17
+ <PrimerDesignProvider designType="ebic" sequenceIds={sequenceIds} primerDesignSettings={primerDesignSettings} steps={steps}>
18
+ <PrimerDesignForm />
19
+ </PrimerDesignProvider>
20
+
21
+ );
22
+ }
23
+
24
+ export default PrimerDesignEBIC;
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import Box from '@mui/material/Box';
3
+ import PrimerDesignStepper from './PrimerDesignStepper';
4
+ import TabPanelSelectRoi from './TabPanelSelectRoi';
5
+ import TabPannelSettings from './TabPannelSettings';
6
+ import TabPanelResults from './TabPanelResults';
7
+ import { usePrimerDesign } from './PrimerDesignContext';
8
+ import TabPanelEBICSettings from './TabPanelEBICSettings';
9
+
10
+ function PrimerDesignForm() {
11
+ const { steps, sequenceIds, designType } = usePrimerDesign();
12
+ return (
13
+ <Box>
14
+ <PrimerDesignStepper />
15
+ {steps.slice(0, sequenceIds.length).map((step, index) => (
16
+ <TabPanelSelectRoi
17
+ key={step.label}
18
+ step={step}
19
+ index={index}
20
+ />
21
+ ))}
22
+ {designType !== 'ebic' && <TabPannelSettings />}
23
+ {designType === 'ebic' && <TabPanelEBICSettings />}
24
+ <TabPanelResults />
25
+ </Box>
26
+ );
27
+ }
28
+
29
+ export default PrimerDesignForm;
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { PrimerDesignProvider } from './PrimerDesignContext';
3
+ import PrimerDesignForm from './PrimerDesignForm';
4
+
5
+ import useGatewayPrimerDesignSettings from './useGatewayPrimerDesignSettings';
6
+ import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
7
+
8
+ function PrimerDesignGatewayBP({ donorVectorId, pcrSource }) {
9
+ const templateSequenceId = getPcrTemplateSequenceId(pcrSource);
10
+ const sequenceIds = React.useMemo(() => [templateSequenceId, donorVectorId], [templateSequenceId, donorVectorId]);
11
+ const steps = React.useMemo(() => [
12
+ { label: 'Amplified region',
13
+ description: `Select the fragment of sequence ${templateSequenceId} to be amplified in the editor and click "Choose region"`,
14
+ inputLabel: `Amplified region (sequence ${templateSequenceId})` },
15
+ { label: 'Replaced region',
16
+ description: 'Select attP sites between which the PCR product will be inserted',
17
+ inputLabel: `Replaced region (sequence ${donorVectorId})`,
18
+ stepCompletionToolTip: 'Select a valid combination of attP sites',
19
+ },
20
+ ], [templateSequenceId, donorVectorId]);
21
+
22
+ const primerDesignSettings = useGatewayPrimerDesignSettings({ homology_length: null, minimal_hybridization_length: 20, target_tm: 55 });
23
+
24
+ return (
25
+ <PrimerDesignProvider
26
+ designType="gateway_bp"
27
+ sequenceIds={sequenceIds}
28
+ primerDesignSettings={primerDesignSettings}
29
+ steps={steps}
30
+ >
31
+ <PrimerDesignForm />
32
+ </PrimerDesignProvider>
33
+ );
34
+ }
35
+
36
+ export default PrimerDesignGatewayBP;