@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,65 @@
1
+ import { Button, FormControl } from '@mui/material';
2
+ import React from 'react';
3
+ import { batch, useDispatch } from 'react-redux';
4
+ import MultipleInputsSelector from '../../../sources/MultipleInputsSelector';
5
+ import { cloningActions } from '@opencloning/store/cloning';
6
+ import useStoreEditor from '../../../../hooks/useStoreEditor';
7
+ import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
8
+
9
+ function PrimerDesignGibsonAssembly({ source, assemblyType }) {
10
+ const [targets, setTargets] = React.useState(source.input.map(({ sequence }) => sequence));
11
+ const inputSequenceId = getPcrTemplateSequenceId(source);
12
+
13
+ const onInputChange = (newInputSequenceIds) => {
14
+ // Prevent unsetting the input of the source
15
+ if (!newInputSequenceIds.includes(inputSequenceId)) {
16
+ setTargets( (prev) => [...prev, ...newInputSequenceIds]);
17
+ } else {
18
+ setTargets(newInputSequenceIds);
19
+ }
20
+ };
21
+
22
+ const { updateStoreEditor } = useStoreEditor();
23
+ const { addPCRsAndSubsequentSourcesForAssembly, setCurrentTab, setMainSequenceId } = cloningActions;
24
+ const dispatch = useDispatch();
25
+ const onSubmit = (event) => {
26
+ event.preventDefault();
27
+ const newSequence = {
28
+ type: 'TemplateSequence',
29
+ primer_design: 'gibson_assembly',
30
+ circular: false,
31
+ };
32
+
33
+ batch(() => {
34
+ // Slice from the second on
35
+ const newPCRTemplates = targets.slice(1);
36
+ dispatch(addPCRsAndSubsequentSourcesForAssembly({ sourceId: source.id, newSequence, templateIds: newPCRTemplates, sourceType: assemblyType }));
37
+ dispatch(setMainSequenceId(inputSequenceId));
38
+ updateStoreEditor('mainEditor', inputSequenceId);
39
+ dispatch(setCurrentTab(3));
40
+ // Scroll to the top of the page after 300ms
41
+ setTimeout(() => {
42
+ document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
43
+ }, 300);
44
+ });
45
+ };
46
+
47
+ return (
48
+ <form onSubmit={onSubmit}>
49
+ <FormControl fullWidth>
50
+ <MultipleInputsSelector
51
+ inputSequenceIds={targets}
52
+ label="Input sequences (in order)"
53
+ onChange={onInputChange}
54
+ />
55
+ </FormControl>
56
+
57
+ <Button type="submit" variant="contained" color="success">
58
+ Design primers
59
+ </Button>
60
+
61
+ </form>
62
+ );
63
+ }
64
+
65
+ export default PrimerDesignGibsonAssembly;
@@ -0,0 +1,84 @@
1
+ import { Alert, Button, FormControl } from '@mui/material';
2
+ import React from 'react';
3
+ import { batch, useDispatch } from 'react-redux';
4
+ import SingleInputSelector from '../../../sources/SingleInputSelector';
5
+ import { cloningActions } from '@opencloning/store/cloning';
6
+ import useStoreEditor from '../../../../hooks/useStoreEditor';
7
+ import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
8
+
9
+ function PrimerDesignHomologousRecombination({ source, primerDesignType }) {
10
+ const [target, setTarget] = React.useState('');
11
+
12
+ const { updateStoreEditor } = useStoreEditor();
13
+ const { addTemplateChildAndSubsequentSource, setCurrentTab, setMainSequenceId } = cloningActions;
14
+ const dispatch = useDispatch();
15
+ const inputSequenceId = getPcrTemplateSequenceId(source);
16
+ const onSubmit = (event) => {
17
+ event.preventDefault();
18
+ const newSource = {
19
+ input: [{ sequence: Number(target) }],
20
+ type: primerDesignType === 'homologous_recombination' ? 'HomologousRecombinationSource' : 'CRISPRSource',
21
+ };
22
+ const newSequence = {
23
+ type: 'TemplateSequence',
24
+ primer_design: 'homologous_recombination',
25
+ circular: false,
26
+ };
27
+
28
+ batch(() => {
29
+ dispatch(addTemplateChildAndSubsequentSource({ newSource, newSequence, sourceId: source.id }));
30
+ dispatch(setMainSequenceId(inputSequenceId));
31
+ updateStoreEditor('mainEditor', inputSequenceId);
32
+ dispatch(setCurrentTab(3));
33
+ // Scroll to the top of the page after 300ms
34
+ setTimeout(() => {
35
+ document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
36
+ }, 300);
37
+ });
38
+ };
39
+ return (
40
+ <form onSubmit={onSubmit}>
41
+ <Alert severity="info" icon={false} sx={{ textAlign: 'left' }}>
42
+ <p style={{ marginBottom: 4 }}>
43
+ Use this to design
44
+ {' '}
45
+ <strong>primers with homology arms</strong>
46
+ {' '}
47
+ to amplify a fragment of sequence
48
+ {' '}
49
+ {inputSequenceId}
50
+ {' '}
51
+ and insert it into a
52
+ {' '}
53
+ <strong>target sequence</strong>
54
+ {' '}
55
+ via
56
+ {' '}
57
+ {primerDesignType === 'homologous_recombination' ? 'homologous recombination' : 'CRISPR cut + homologous repair'}
58
+ .
59
+ </p>
60
+ <p>
61
+ If you haven&apos;t, import a
62
+ {' '}
63
+ <strong>target sequence</strong>
64
+ , then select it below.
65
+ </p>
66
+ </Alert>
67
+ <FormControl fullWidth>
68
+ <SingleInputSelector
69
+ label="Target sequence"
70
+ selectedId={target}
71
+ onChange={(e) => setTarget(e.target.value)}
72
+ inputSequenceIds={[]}
73
+ />
74
+ </FormControl>
75
+ {target && (
76
+ <Button type="submit" variant="contained" color="success">
77
+ Design primers
78
+ </Button>
79
+ )}
80
+ </form>
81
+ );
82
+ }
83
+
84
+ export default PrimerDesignHomologousRecombination;
@@ -0,0 +1,74 @@
1
+ import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
2
+ import React from 'react';
3
+
4
+ import { batch, useDispatch } from 'react-redux';
5
+ import PrimerDesignHomologousRecombination from './PrimerDesignHomologousRecombination';
6
+ import PrimerDesignGibsonAssembly from './PrimerDesignGibsonAssembly';
7
+ import { cloningActions } from '@opencloning/store/cloning';
8
+ import useStoreEditor from '../../../../hooks/useStoreEditor';
9
+ import PrimerDesignGatewayBP from './PrimerDesignGatewayBP';
10
+ import { getPcrTemplateSequenceId } from '@opencloning/store/cloning_utils';
11
+
12
+ function PrimerDesignSourceForm({ source }) {
13
+ const [primerDesignType, setPrimerDesignType] = React.useState('');
14
+ const { updateStoreEditor } = useStoreEditor();
15
+ const { addPCRsAndSubsequentSourcesForAssembly, setMainSequenceId, setCurrentTab } = cloningActions;
16
+ const dispatch = useDispatch();
17
+ const inputSequenceId = getPcrTemplateSequenceId(source);
18
+ React.useEffect(() => {
19
+ // Here the user does not have to select anything else
20
+ if (primerDesignType === 'restriction_ligation' || primerDesignType === 'simple_pair') {
21
+ const newSequence = {
22
+ type: 'TemplateSequence',
23
+ primer_design: primerDesignType,
24
+ circular: false,
25
+ };
26
+
27
+ batch(() => {
28
+ dispatch(addPCRsAndSubsequentSourcesForAssembly({ sourceId: source.id, newSequence, templateIds: [], sourceType: null }));
29
+ dispatch(setMainSequenceId(inputSequenceId));
30
+ updateStoreEditor('mainEditor', inputSequenceId);
31
+ dispatch(setCurrentTab(3));
32
+ // Scroll to the top of the page after 300ms
33
+ setTimeout(() => {
34
+ document.querySelector('.tab-panels-container')?.scrollTo({ top: 0, behavior: 'instant' });
35
+ }, 300);
36
+ });
37
+ }
38
+ }, [primerDesignType]);
39
+
40
+ return (
41
+ <>
42
+ <FormControl fullWidth>
43
+ <InputLabel id="select-primer-design-type-label">Purpose of primers</InputLabel>
44
+ <Select
45
+ id="select-primer-design-type"
46
+ value={primerDesignType}
47
+ onChange={(event) => setPrimerDesignType(event.target.value)}
48
+ label="Purpose of primers"
49
+ >
50
+ <MenuItem value="simple_pair">Normal PCR</MenuItem>
51
+ <MenuItem value="homologous_recombination">Homologous Recombination</MenuItem>
52
+ <MenuItem value="crispr">CRISPR</MenuItem>
53
+ <MenuItem value="GibsonAssemblySource">Gibson Assembly</MenuItem>
54
+ <MenuItem value="InFusionSource">In-Fusion</MenuItem>
55
+ <MenuItem value="InVivoAssemblySource">In vivo Assembly</MenuItem>
56
+ <MenuItem value="restriction_ligation">Restriction and Ligation</MenuItem>
57
+ <MenuItem value="gateway_bp">Gateway BP reaction</MenuItem>
58
+ </Select>
59
+ </FormControl>
60
+ {['homologous_recombination', 'crispr'].includes(primerDesignType)
61
+ && (
62
+ <PrimerDesignHomologousRecombination source={source} primerDesignType={primerDesignType} />
63
+ )}
64
+ {['GibsonAssemblySource', 'InFusionSource', 'InVivoAssemblySource'].includes(primerDesignType) && (
65
+ <PrimerDesignGibsonAssembly source={source} assemblyType={primerDesignType} />
66
+ )}
67
+ {primerDesignType === 'gateway_bp' && (
68
+ <PrimerDesignGatewayBP source={source} />
69
+ )}
70
+ </>
71
+ );
72
+ }
73
+
74
+ export default PrimerDesignSourceForm;
@@ -0,0 +1,31 @@
1
+ import { Alert } from '@mui/material';
2
+ import React from 'react';
3
+
4
+ function NoAttPSitesError({ sites }) {
5
+ return (
6
+ <Alert severity="error" icon={false}>
7
+ <p>
8
+ At least two attP sites are required.
9
+ </p>
10
+ {sites.length === 0 && <p>No att sites of any type were found.</p>}
11
+ {sites.length > 0 && (
12
+ <>
13
+ <p style={{ marginBottom: '10px' }}>
14
+ We found the following sites:
15
+ </p>
16
+
17
+ {sites.map((site) => (
18
+ <div key={site.siteName}>
19
+ {site.siteName}
20
+ {' '}
21
+ {site.location}
22
+ </div>
23
+ ))}
24
+
25
+ </>
26
+ )}
27
+ </Alert>
28
+ );
29
+ }
30
+
31
+ export default NoAttPSitesError;
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { mount } from '@cypress/react';
3
+ import PCRTable from './PCRTable';
4
+ import { mockPCRDetails } from '../../../../../../tests/mockPrimerDetailsData';
5
+
6
+ describe('PCRTable', () => {
7
+ it('renders PCR details correctly', () => {
8
+ mount(<PCRTable pcrDetail={mockPCRDetails[0]} />);
9
+ // PCR 3 Section Header
10
+ cy.get('.pcr-table tr').eq(0).should('contain', 'PCR 3');
11
+
12
+ // Primer Names Row
13
+ cy.contains('.pcr-table tr', 'Primer names').within(() => {
14
+ cy.get('td').eq(1).should('contain', 'fwd');
15
+ cy.get('td').eq(2).should('contain', 'rvs');
16
+ });
17
+
18
+ // Binding Information
19
+ cy.contains('.pcr-table tr', 'Binding length').within(() => {
20
+ cy.get('td').eq(1).should('contain', '21');
21
+ cy.get('td').eq(2).should('contain', '20');
22
+ });
23
+
24
+ cy.contains('.pcr-table tr', 'Tm (binding)').within(() => {
25
+ cy.get('td').eq(1).should('contain', '56.7 °C');
26
+ cy.get('td').eq(2).should('contain', '50.9 °C');
27
+ });
28
+
29
+ cy.contains('.pcr-table tr', 'GC% (binding)').within(() => {
30
+ cy.get('td').eq(1).should('contain', '48%');
31
+ cy.get('td').eq(2).should('contain', '40%');
32
+ });
33
+
34
+ // Temperature Difference
35
+ cy.contains('.pcr-table tr', 'Tm difference').within(() => {
36
+ cy.get('td').should('contain', '5.8 °C');
37
+ });
38
+
39
+ // Heterodimer Information
40
+ cy.contains('.pcr-table tr', 'Tm (heterodimer)').within(() => {
41
+ cy.get('td').should('contain', '20.5 °C');
42
+ });
43
+
44
+ cy.contains('.pcr-table tr', 'ΔG (heterodimer)').within(() => {
45
+ cy.get('td').should('contain', '-5276 kcal/mol');
46
+ });
47
+
48
+ // Verify heterodimer figure exists
49
+ cy.contains('SEQ').should('exist');
50
+ });
51
+ });
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { Table, TableBody } from '@mui/material';
3
+ import { formatGcContent, formatMeltingTemperature, formatDeltaG } from './primerDetailsFormatting';
4
+ import Primer3Figure from './Primer3Figure';
5
+ import TableSection from './TableSection';
6
+
7
+ export default function PCRTable({ pcrDetail }) {
8
+ const { sourceId, sourceType, fwdPrimer, rvsPrimer, heterodimer } = pcrDetail;
9
+ const name = (sourceType === 'PCRSource') ? 'PCR' : 'Oligonucleotide hybridization';
10
+
11
+ return (
12
+ <Table size="small" className="pcr-table">
13
+ <TableBody />
14
+
15
+ <TableSection
16
+ title={`${name} ${sourceId}`}
17
+ values={[
18
+ ['Primer names', fwdPrimer.name, rvsPrimer.name],
19
+ ['Binding length', fwdPrimer.length, rvsPrimer.length],
20
+ ['Tm (binding)', `${formatMeltingTemperature(fwdPrimer.melting_temperature)} °C`, `${formatMeltingTemperature(rvsPrimer.melting_temperature)} °C`],
21
+ ['GC% (binding)', `${formatGcContent(fwdPrimer.gc_content)}%`, `${formatGcContent(rvsPrimer.gc_content)}%`],
22
+ ['Tm difference', `${formatMeltingTemperature(fwdPrimer.melting_temperature - rvsPrimer.melting_temperature)} °C`],
23
+ ]}
24
+ />
25
+ {heterodimer && (
26
+ <>
27
+ <TableSection
28
+ values={[['Tm (heterodimer)', `${formatMeltingTemperature(heterodimer.melting_temperature)} °C`], ['ΔG (heterodimer)', `${formatDeltaG(heterodimer.deltaG)} kcal/mol`]]}
29
+ />
30
+ <Primer3Figure figure={heterodimer.figure} />
31
+ </>
32
+ )}
33
+ </Table>
34
+ );
35
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { TableCell, TableRow } from '@mui/material';
3
+
4
+ export default function Primer3Figure({ figure }) {
5
+ // Remove all trailing spaces
6
+ const trimmedRows = figure.split('\n').map((row) => row.trim());
7
+ const longestRow = Math.max(...trimmedRows.map((row) => row.length));
8
+ return (
9
+ <TableRow>
10
+ <TableCell colSpan={3} sx={{ padding: 0, margin: 0 }}>
11
+ <code style={{ width: '100%',
12
+ whiteSpace: 'pre',
13
+ fontFamily: 'monospace',
14
+ display: 'block',
15
+ maxWidth: '100%',
16
+ overflow: 'auto',
17
+ margin: 0,
18
+ fontSize: `min(calc(100% * 80 / ${longestRow}), 1rem)` }}
19
+ >
20
+ {trimmedRows.join('\n')}
21
+ </code>
22
+ </TableCell>
23
+ </TableRow>
24
+ );
25
+ }
@@ -0,0 +1,39 @@
1
+ import { Skeleton } from '@mui/material';
2
+ import React from 'react';
3
+ import { formatGcContent, formatMeltingTemperature } from './primerDetailsFormatting';
4
+
5
+ function PrimerDetailsTds({ primerDetails, pcrDetails }) {
6
+ // A primer could be involved in multiple PCR reactions, so we pick the one in which it has the longest
7
+ // annealing length for display in the table.
8
+ const pcrDetail = pcrDetails.find(({ fwdPrimer, rvsPrimer }) => fwdPrimer.id === primerDetails.id || rvsPrimer.id === primerDetails.id);
9
+
10
+ const loadingOrErrorComponent = <Skeleton variant="text" height={20} />;
11
+ let meltingTemperature = formatMeltingTemperature(primerDetails.melting_temperature);
12
+ let gcContent = formatGcContent(primerDetails.gc_content);
13
+ let { length } = primerDetails;
14
+ if (pcrDetail) {
15
+ const pcrPrimerDetail = primerDetails.id === pcrDetail.fwdPrimer.id ? pcrDetail.fwdPrimer : pcrDetail.rvsPrimer;
16
+ // We pick the pair with the highest binding length for display here
17
+ meltingTemperature = `${formatMeltingTemperature(pcrPrimerDetail.melting_temperature)} (${meltingTemperature})`;
18
+ gcContent = `${formatGcContent(pcrPrimerDetail.gc_content)} (${gcContent})`;
19
+ length = `${pcrPrimerDetail.length} (${length})`;
20
+ }
21
+ return (
22
+ <>
23
+ <td style={{ whiteSpace: 'nowrap' }} className="length">{length}</td>
24
+ {primerDetails.gc_content !== undefined ? (
25
+ <>
26
+ <td style={{ whiteSpace: 'nowrap' }} className="melting-temperature">{meltingTemperature}</td>
27
+ <td style={{ whiteSpace: 'nowrap' }} className="gc-content">{gcContent}</td>
28
+ </>
29
+ ) : (
30
+ <>
31
+ <td style={{ whiteSpace: 'nowrap' }} className="melting-temperature">{loadingOrErrorComponent}</td>
32
+ <td style={{ whiteSpace: 'nowrap' }} className="gc-content">{loadingOrErrorComponent}</td>
33
+ </>
34
+ )}
35
+ </>
36
+ );
37
+ }
38
+
39
+ export default PrimerDetailsTds;
@@ -0,0 +1,137 @@
1
+ import React from 'react';
2
+ import { mount } from '@cypress/react';
3
+ import PrimerInfoIcon, { PrimerInfoDialog } from './PrimerInfoIcon';
4
+ import { mockPCRDetails, mockPrimerDetails, mockPrimer } from '../../../../../../tests/mockPrimerDetailsData';
5
+
6
+ const emptyPCRDetails = [];
7
+
8
+ describe('PrimerInfoIcon Component', () => {
9
+ it('renders the info icon when no warnings', () => {
10
+ mount(
11
+ <PrimerInfoIcon
12
+ primerDetails={mockPrimerDetails}
13
+ pcrDetails={emptyPCRDetails}
14
+ />,
15
+ );
16
+
17
+ cy.get('svg[data-testid="InfoIcon"]').should('exist');
18
+ // Mouseover shows tooltip
19
+ cy.get('svg[data-testid="InfoIcon"]').trigger('mouseover');
20
+ cy.get('div[role="tooltip"]').should('exist');
21
+ cy.get('div[role="tooltip"]').should('contain', 'Primer details');
22
+ cy.get('svg[data-testid="InfoIcon"]').click();
23
+ cy.get('div[role="dialog"]').should('exist');
24
+ });
25
+ it('renders the warning icon when warnings', () => {
26
+ mount(
27
+ <PrimerInfoIcon
28
+ primerDetails={{ ...mockPrimerDetails, gc_content: 0.0 }}
29
+ pcrDetails={emptyPCRDetails}
30
+ />,
31
+ );
32
+
33
+ cy.get('svg[data-testid="WarningIcon"]').should('exist');
34
+ // Mouseover shows tooltip
35
+ cy.get('svg[data-testid="WarningIcon"]').trigger('mouseover');
36
+ cy.get('div[role="tooltip"]').should('exist');
37
+ cy.get('div[role="tooltip"]').should('contain', 'GC content');
38
+ cy.get('svg[data-testid="WarningIcon"]').click();
39
+ cy.get('div[role="dialog"]').should('exist');
40
+ });
41
+ it('handles connection errors', () => {
42
+ mount(
43
+ <PrimerInfoIcon
44
+ primerDetails={{ ...mockPrimer }}
45
+ pcrDetails={emptyPCRDetails}
46
+ />,
47
+ );
48
+ cy.get('span').trigger('mouseover');
49
+ cy.get('div[role="tooltip"]').should('exist');
50
+ cy.get('div[role="tooltip"]').should('contain', 'Primer details not available');
51
+ });
52
+ });
53
+
54
+ describe('PrimerInfoDialog Component', () => {
55
+ it('Without PCR details', () => {
56
+ mount(
57
+ <PrimerInfoDialog
58
+ primerDetails={mockPrimerDetails}
59
+ pcrDetails={emptyPCRDetails}
60
+ open
61
+ onClose={() => {}}
62
+ />,
63
+ );
64
+ cy.get('div[role="dialog"]').should('exist');
65
+ cy.get('tr').eq(0).should('contain', 'Full sequence');
66
+ cy.get('tr').eq(1).find('td').eq(0)
67
+ .should('contain', 'Name');
68
+ cy.get('tr').eq(1).find('td').eq(1)
69
+ .should('contain', 'Test Primer');
70
+
71
+ // Full sequence section
72
+ cy.get('tr').eq(2).find('td').eq(0)
73
+ .should('contain', 'Length');
74
+ cy.get('tr').eq(2).find('td').eq(1)
75
+ .should('contain', '8');
76
+ cy.get('tr').eq(3).find('td').eq(0)
77
+ .should('contain', 'Tm (full sequence)');
78
+ cy.get('tr').eq(3).find('td').eq(1)
79
+ .should('contain', '60 °C');
80
+ cy.get('tr').eq(4).find('td').eq(0)
81
+ .should('contain', 'GC% (full sequence)');
82
+ cy.get('tr').eq(4).find('td').eq(1)
83
+ .should('contain', '50%');
84
+
85
+ // Homodimer section
86
+ cy.get('tr').eq(5).should('contain', 'Homodimer');
87
+ cy.get('tr').eq(6).find('td').eq(0)
88
+ .should('contain', 'Tm (homodimer)');
89
+ cy.get('tr').eq(6).find('td').eq(1)
90
+ .should('contain', '60 °C');
91
+ cy.get('tr').eq(7).find('td').eq(0)
92
+ .should('contain', 'ΔG (homodimer)');
93
+ cy.get('tr').eq(7).find('td').eq(1)
94
+ .should('contain', '-100 kcal/mol');
95
+
96
+ // Hairpin section
97
+ cy.get('tr').eq(9).should('contain', 'Hairpin');
98
+ cy.get('tr').eq(10).find('td').eq(0)
99
+ .should('contain', 'Tm (hairpin)');
100
+ cy.get('tr').eq(10).find('td').eq(1)
101
+ .should('contain', '60 °C');
102
+ cy.get('tr').eq(11).find('td').eq(0)
103
+ .should('contain', 'ΔG (hairpin)');
104
+ cy.get('tr').eq(11).find('td').eq(1)
105
+ .should('contain', '-100 kcal/mol');
106
+ cy.get('.pcr-table').should('not.exist');
107
+ });
108
+
109
+ it('with PCR details', () => {
110
+ mount(
111
+ <PrimerInfoDialog
112
+ primerDetails={mockPrimerDetails}
113
+ pcrDetails={mockPCRDetails}
114
+ open
115
+ onClose={() => {}}
116
+ />,
117
+ );
118
+ cy.get('.pcr-table').should('exist');
119
+ });
120
+ it('with multiple PCR details', () => {
121
+ const mockPCRDetailsWithMultipleSources = [
122
+ { ...mockPCRDetails[0], sourceId: 1 },
123
+ { ...mockPCRDetails[0], sourceId: 2 },
124
+ ];
125
+ mount(
126
+ <PrimerInfoDialog
127
+ primerDetails={mockPrimerDetails}
128
+ pcrDetails={mockPCRDetailsWithMultipleSources}
129
+ open
130
+ onClose={() => {}}
131
+ />,
132
+ );
133
+ cy.get('.pcr-table').should('have.length', 2);
134
+ cy.get('.pcr-table').eq(0).should('contain', 'PCR 1');
135
+ cy.get('.pcr-table').eq(1).should('contain', 'PCR 2');
136
+ });
137
+ });
@@ -0,0 +1,132 @@
1
+ import { Dialog, DialogContent, IconButton, Tooltip, Table, TableBody } from '@mui/material';
2
+ import React from 'react';
3
+ import InfoIcon from '@mui/icons-material/Info';
4
+ import WarningIcon from '@mui/icons-material/Warning';
5
+ import { formatGcContent, formatMeltingTemperature, formatDeltaG } from './primerDetailsFormatting';
6
+ import Primer3Figure from './Primer3Figure';
7
+ import TableSection from './TableSection';
8
+ import PCRTable from './PCRTable';
9
+
10
+ export function PrimerInfoDialog({ primerDetails, open, onClose, pcrDetails }) {
11
+ const relatedPcrDetails = pcrDetails.filter(({ fwdPrimer, rvsPrimer }) => fwdPrimer.id === primerDetails.id || rvsPrimer.id === primerDetails.id);
12
+ return (
13
+ <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
14
+ <DialogContent>
15
+
16
+ <Table size="small">
17
+ <TableBody>
18
+ <TableSection
19
+ title="Full sequence"
20
+ values={[
21
+ ['Name', primerDetails.name],
22
+ ['Length', primerDetails.length],
23
+ ['Tm (full sequence)', `${formatMeltingTemperature(primerDetails.melting_temperature)} °C`],
24
+ ['GC% (full sequence)', `${formatGcContent(primerDetails.gc_content)}%`],
25
+ ]}
26
+ />
27
+ {/* TODO: Warning if length > 60 */}
28
+ {primerDetails.homodimer && (
29
+ <>
30
+ <TableSection
31
+ title="Homodimer"
32
+ values={[
33
+ ['Tm (homodimer)', `${formatMeltingTemperature(primerDetails.homodimer.melting_temperature)} °C`],
34
+ ['ΔG (homodimer)', `${formatDeltaG(primerDetails.homodimer.deltaG)} kcal/mol`],
35
+ ]}
36
+ />
37
+ <Primer3Figure figure={primerDetails.homodimer.figure} />
38
+ </>
39
+ )}
40
+ {primerDetails.hairpin && (
41
+ <>
42
+ <TableSection
43
+ title="Hairpin"
44
+ values={[
45
+ ['Tm (hairpin)', `${formatMeltingTemperature(primerDetails.hairpin.melting_temperature)} °C`],
46
+ ['ΔG (hairpin)', `${formatDeltaG(primerDetails.hairpin.deltaG)} kcal/mol`],
47
+ ]}
48
+ />
49
+ <Primer3Figure figure={primerDetails.hairpin.figure} />
50
+ </>
51
+ )}
52
+ </TableBody>
53
+ </Table>
54
+ {relatedPcrDetails.map((pcrDetail) => (
55
+ <PCRTable
56
+ key={pcrDetail.sourceId}
57
+ pcrDetail={pcrDetail}
58
+ />
59
+ ))}
60
+
61
+ </DialogContent>
62
+ </Dialog>
63
+ );
64
+ }
65
+
66
+ const primerProblematicValues = (primerDetails, pcrDetails) => {
67
+ const pcrDetail = pcrDetails.find(({ fwdPrimer, rvsPrimer }) => fwdPrimer.id === primerDetails.id || rvsPrimer.id === primerDetails.id);
68
+ const problematicValues = {
69
+ homodimer: '',
70
+ hairpin: '',
71
+ gcContent: '',
72
+ meltingTemperature: '',
73
+ };
74
+ let gcContent = primerDetails.gc_content;
75
+ let meltingTemperature = primerDetails.melting_temperature;
76
+ if (pcrDetail) {
77
+ const pcrPrimer = primerDetails.id === pcrDetail.fwdPrimer.id ? pcrDetail.fwdPrimer : pcrDetail.rvsPrimer;
78
+ meltingTemperature = pcrPrimer.melting_temperature;
79
+ gcContent = pcrPrimer.gc_content;
80
+ }
81
+
82
+ if (gcContent < 0.30 || gcContent > 0.70) {
83
+ problematicValues.gcContent = 'GC content is outside the optimal range';
84
+ }
85
+ if (meltingTemperature < 50 || meltingTemperature > 70) {
86
+ problematicValues.meltingTemperature = 'Melting temperature is outside the optimal range';
87
+ }
88
+
89
+ return problematicValues;
90
+ if (primerDetails.homodimer && primerDetails.homodimer.deltaG < -8000) {
91
+ problematicValues.homodimer = 'May form homodimers';
92
+ }
93
+ if (primerDetails.hairpin && primerDetails.hairpin.deltaG < -8000) {
94
+ problematicValues.hairpin = 'May form hairpins';
95
+ }
96
+
97
+ return problematicValues;
98
+ };
99
+
100
+ const primerWarning = (problematicValues) => {
101
+ const field = Object.keys(problematicValues).find((key) => problematicValues[key] !== '');
102
+ return field ? problematicValues[field] : '';
103
+ };
104
+
105
+ function PrimerInfoIcon({ primerDetails, pcrDetails }) {
106
+ const [open, setOpen] = React.useState(false);
107
+ const handleOpen = () => setOpen(true);
108
+ const handleClose = () => setOpen(false);
109
+ const warning = primerWarning(primerProblematicValues(primerDetails, pcrDetails));
110
+ let tooltipTitle = 'Primer details';
111
+ const disabled = primerDetails.melting_temperature === undefined;
112
+ if (disabled) {
113
+ tooltipTitle = 'Primer details not available';
114
+ } else if (warning !== '') {
115
+ tooltipTitle = warning;
116
+ }
117
+ return (
118
+ <>
119
+ <Tooltip title={tooltipTitle} placement="top" arrow>
120
+ {/* This span is necessary to work when the button is disabled */}
121
+ <span>
122
+ <IconButton disabled={disabled} onClick={handleOpen}>
123
+ {warning === '' ? <InfoIcon /> : <WarningIcon color="warning" />}
124
+ </IconButton>
125
+ </span>
126
+ </Tooltip>
127
+ {open && <PrimerInfoDialog primerDetails={primerDetails} open={open} onClose={handleClose} pcrDetails={pcrDetails} />}
128
+ </>
129
+ );
130
+ }
131
+
132
+ export default PrimerInfoIcon;