@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,17 @@
1
+ import React from 'react';
2
+ import { TableCell, TableRow } from '@mui/material';
3
+
4
+ export default function TableSection({ title, values }) {
5
+ return (
6
+ <>
7
+ {title && <TableRow><TableCell sx={{ fontWeight: 'bold', textAlign: 'center', fontSize: '1.2rem' }} colSpan={3}>{title}</TableCell></TableRow>}
8
+ {values.map((value) => (
9
+ <TableRow key={value[0]}>
10
+ <TableCell width="50%" sx={{ fontWeight: 'bold', textAlign: 'right' }}>{value[0]}</TableCell>
11
+ <TableCell colSpan={value[2] ? 1 : 2} sx={{ width: '1px', whiteSpace: 'nowrap' }}>{value[1]}</TableCell>
12
+ {value[2] && <TableCell>{value[2]}</TableCell>}
13
+ </TableRow>
14
+ ))}
15
+ </>
16
+ );
17
+ }
@@ -0,0 +1,3 @@
1
+ export const formatGcContent = (gcContent) => (gcContent !== undefined ? Number((gcContent * 100).toFixed(0)) : '');
2
+ export const formatMeltingTemperature = (meltingTemperature) => (meltingTemperature !== undefined ? Number(meltingTemperature.toFixed(1)) : '');
3
+ export const formatDeltaG = (deltaG) => (deltaG !== undefined ? Number(deltaG.toFixed(0)) : '');
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { usePrimerDetailsEndpoints } from './usePrimerDetailsEndpoints';
3
+
4
+ export default function useMultiplePrimerDetails(primers) {
5
+ const [primerDetails, setPrimerDetails] = React.useState([]);
6
+ const [connectionAttempt, setConnectionAttempt] = React.useState(0);
7
+ const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
8
+
9
+ const retryGetPrimerDetails = () => setConnectionAttempt((prev) => prev + 1);
10
+
11
+ const { getPrimerDetails } = usePrimerDetailsEndpoints();
12
+ React.useEffect(() => {
13
+ const fetchPrimerDetails = async () => {
14
+ try {
15
+ setRequestStatus({ status: 'loading' });
16
+ const details1 = await Promise.all(primers.map((primer) => getPrimerDetails(primer.sequence)));
17
+ const details2 = details1.map((detail, index) => ({ ...detail, ...primers[index] }));
18
+ setPrimerDetails(details2);
19
+ setRequestStatus({ status: 'success' });
20
+ } catch (error) {
21
+ console.error(error);
22
+ setRequestStatus({ status: 'error', message: `Error fetching primer details: ${error.message}` });
23
+ }
24
+ };
25
+ fetchPrimerDetails();
26
+ }, [primers, connectionAttempt, getPrimerDetails]);
27
+
28
+ return { primerDetails, retryGetPrimerDetails, requestStatus };
29
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { isEqual } from 'lodash-es';
3
+ import { useSelector } from 'react-redux';
4
+ import { getPrimerBindingInfoFromSource } from '@opencloning/store/cloning_utils';
5
+ import { usePrimerDetailsEndpoints } from './usePrimerDetailsEndpoints';
6
+
7
+ export function usePCRDetails(sourceIds) {
8
+ const [pcrDetails, setPcrDetails] = React.useState([]);
9
+ const [requestStatus, setRequestStatus] = React.useState({ status: 'loading', message: '' });
10
+ const [connectionAttempt, setConnectionAttempt] = React.useState(0);
11
+ const retryGetPCRDetails = () => setConnectionAttempt((prev) => prev + 1);
12
+
13
+ const { getPrimerDetails, getHeterodimerDetails } = usePrimerDetailsEndpoints();
14
+ const bindingInfos = useSelector((state) => {
15
+ const { primers, sources, teselaJsonCache } = state.cloning;
16
+
17
+ return sourceIds.map((sourceId) => {
18
+ const source = sources.find((s) => s.id === sourceId);
19
+ const sequenceLength = source.type === 'PCRSource' && teselaJsonCache[source.input[1].sequence].size;
20
+ return getPrimerBindingInfoFromSource(primers, source, sequenceLength);
21
+ });
22
+ }, isEqual);
23
+
24
+ React.useEffect(() => {
25
+ const fetchPrimerDetails = async (bindingInfo) => {
26
+ let fwdPrimer = await getPrimerDetails(bindingInfo.fwdPrimer.sequence.slice(-bindingInfo.fwdLength));
27
+ fwdPrimer = { ...fwdPrimer, ...bindingInfo.fwdPrimer };
28
+ let rvsPrimer = await getPrimerDetails(bindingInfo.rvsPrimer.sequence.slice(-bindingInfo.rvsLength));
29
+ rvsPrimer = { ...rvsPrimer, ...bindingInfo.rvsPrimer };
30
+ const heterodimer = await getHeterodimerDetails(bindingInfo.fwdPrimer.sequence, bindingInfo.rvsPrimer.sequence);
31
+ return { sourceId: bindingInfo.sourceId, sourceType: bindingInfo.sourceType, fwdPrimer, rvsPrimer, heterodimer };
32
+ };
33
+ const getAllDetails = async () => {
34
+ setRequestStatus({ status: 'loading', message: 'loading' });
35
+ try {
36
+ const details = await Promise.all(bindingInfos.map(fetchPrimerDetails));
37
+ setPcrDetails(details);
38
+ setRequestStatus({ status: 'success', message: '' });
39
+ } catch (error) {
40
+ setRequestStatus({ status: 'error', message: `Error fetching PCR details: ${error.message}` });
41
+ }
42
+ };
43
+ getAllDetails();
44
+ }, [bindingInfos, connectionAttempt, getPrimerDetails, getHeterodimerDetails]);
45
+
46
+ return { pcrDetails, retryGetPCRDetails, requestStatus };
47
+ }
@@ -0,0 +1,49 @@
1
+ import React, { useCallback } from 'react';
2
+ import useBackendRoute from '../../../hooks/useBackendRoute';
3
+ import useHttpClient from '../../../hooks/useHttpClient';
4
+ import { isEqual } from 'lodash-es';
5
+ import { useSelector } from 'react-redux';
6
+
7
+ const primerDetailsCache = new Map();
8
+ const heterodimerDetailsCache = new Map();
9
+ export function makePrimerDetailsCacheKey(sequence, globalPrimerSettings) {
10
+ const settingsKey = JSON.stringify(globalPrimerSettings);
11
+ return `${String(sequence)}::${settingsKey}`;
12
+ }
13
+ export function makeHeterodimerCacheKey(sequence1, sequence2, globalPrimerSettings) {
14
+ const pairKey = [sequence1, sequence2].map(String).sort().join(',');
15
+ const settingsKey = JSON.stringify(globalPrimerSettings);
16
+ return `${pairKey}::${settingsKey}`;
17
+ }
18
+ export function usePrimerDetailsEndpoints() {
19
+ const backendRoute = useBackendRoute();
20
+ const httpClient = useHttpClient();
21
+ const globalPrimerSettings = useSelector((state) => state.cloning.globalPrimerSettings, isEqual);
22
+
23
+ // Caches are keyed by both the input sequence(s) and the current globalPrimerSettings,
24
+ // so we don't need to clear them on settings change.
25
+
26
+ const url = backendRoute('primer_details');
27
+ const heterodimerUrl = backendRoute('primer_heterodimer');
28
+
29
+ const getPrimerDetails = useCallback(async (sequence) => {
30
+ const cacheKey = makePrimerDetailsCacheKey(sequence, globalPrimerSettings);
31
+ if (!primerDetailsCache.has(cacheKey)) {
32
+ const { data } = await httpClient.post(url, { sequence, settings: globalPrimerSettings });
33
+ data.length = sequence.length;
34
+ primerDetailsCache.set(cacheKey, data);
35
+ }
36
+ return primerDetailsCache.get(cacheKey);
37
+ }, [url, httpClient, globalPrimerSettings]);
38
+
39
+ const getHeterodimerDetails = useCallback(async (sequence1, sequence2) => {
40
+ const cacheKey = makeHeterodimerCacheKey(sequence1, sequence2, globalPrimerSettings);
41
+ if (!heterodimerDetailsCache.has(cacheKey)) {
42
+ const { data } = await httpClient.post(heterodimerUrl, { sequence1, sequence2, settings: globalPrimerSettings });
43
+ heterodimerDetailsCache.set(cacheKey, data);
44
+ }
45
+ return heterodimerDetailsCache.get(cacheKey);
46
+ }, [heterodimerUrl, httpClient, globalPrimerSettings]);
47
+
48
+ return { getPrimerDetails, getHeterodimerDetails };
49
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { usePrimerDetailsEndpoints } from './usePrimerDetailsEndpoints';
3
+
4
+ export function useSinglePrimerSequenceDetails(sequence) {
5
+ const [primerDetails, setPrimerDetails] = React.useState({ status: 'loading' });
6
+ const { getPrimerDetails } = usePrimerDetailsEndpoints();
7
+ const [connectionAttempt, setConnectionAttempt] = React.useState(0);
8
+
9
+ const retryGetPrimerDetails = () => setConnectionAttempt((prev) => prev + 1);
10
+
11
+ React.useEffect(() => {
12
+ const fetchPrimerDetails = async () => {
13
+ try {
14
+ const details = await getPrimerDetails(sequence);
15
+ setPrimerDetails({ status: 'success', ...details });
16
+ } catch (error) {
17
+ console.error(error);
18
+ setPrimerDetails({ status: 'error', error });
19
+ }
20
+ };
21
+ fetchPrimerDetails();
22
+ }, [sequence, connectionAttempt]);
23
+
24
+ return { primerDetails, retryGetPrimerDetails };
25
+ }
@@ -0,0 +1,49 @@
1
+ import { formatDeltaG, formatGcContent, formatMeltingTemperature } from './primer_details/primerDetailsFormatting';
2
+
3
+ export default function primersToTabularFile(primerDetails, pcrDetails, separator) {
4
+ if (primerDetails.length === 0) {
5
+ return '';
6
+ }
7
+ const rowsAsObjects = primerDetails.map((primer) => {
8
+ const pcrDetail = pcrDetails.find(({ fwdPrimer, rvsPrimer }) => fwdPrimer.id === primer.id || rvsPrimer.id === primer.id);
9
+ const pcrPart = {
10
+ pcr_source_id: '',
11
+ binding_length: '',
12
+ binding_melting_temperature: '',
13
+ binding_gc_content: '',
14
+ heterodimer_melting_temperature: '',
15
+ heterodimer_deltaG: '',
16
+ };
17
+ if (pcrDetail) {
18
+ const pcrPrimer = primer.id === pcrDetail.fwdPrimer.id ? pcrDetail.fwdPrimer : pcrDetail.rvsPrimer;
19
+ pcrPart.pcr_source_id = pcrDetail.sourceId;
20
+ pcrPart.binding_length = pcrPrimer.length;
21
+ pcrPart.binding_melting_temperature = formatMeltingTemperature(pcrPrimer.melting_temperature);
22
+ pcrPart.binding_gc_content = formatGcContent(pcrPrimer.gc_content);
23
+ if (pcrDetail.heterodimer) {
24
+ pcrPart.heterodimer_melting_temperature = formatMeltingTemperature(pcrDetail.heterodimer.melting_temperature);
25
+ pcrPart.heterodimer_deltaG = formatDeltaG(pcrDetail.heterodimer.deltaG);
26
+ }
27
+ }
28
+ return {
29
+ id: primer.id,
30
+ name: primer.name,
31
+ sequence: primer.sequence,
32
+ length: primer.length,
33
+ melting_temperature: formatMeltingTemperature(primer.melting_temperature),
34
+ gc_content: formatGcContent(primer.gc_content),
35
+ homodimer_melting_temperature: formatMeltingTemperature(primer.homodimer?.melting_temperature),
36
+ homodimer_deltaG: formatDeltaG(primer.homodimer?.deltaG),
37
+ hairpin_melting_temperature: formatMeltingTemperature(primer.hairpin?.melting_temperature),
38
+ hairpin_deltaG: formatDeltaG(primer.hairpin?.deltaG),
39
+ ...pcrPart,
40
+ };
41
+ });
42
+
43
+ const headers = Object.keys(rowsAsObjects[0]);
44
+ // We don't use Object.values(row) because otherwise undefined values are not included
45
+ const rows = rowsAsObjects.map((row) => Object.keys(row).map((key) => row[key]).join(separator));
46
+ const out = [headers.join(separator), ...rows].join('\n');
47
+
48
+ return out;
49
+ }
@@ -0,0 +1,108 @@
1
+ import primersToTabularFile from './primersToTabularFile';
2
+ import { mockPCRDetails } from '../../../../../tests/mockPrimerDetailsData';
3
+
4
+ const headers = [
5
+ 'id',
6
+ 'name',
7
+ 'sequence',
8
+ 'length',
9
+ 'melting_temperature',
10
+ 'gc_content',
11
+ 'homodimer_melting_temperature',
12
+ 'homodimer_deltaG',
13
+ 'hairpin_melting_temperature',
14
+ 'hairpin_deltaG',
15
+ 'pcr_source_id',
16
+ 'binding_length',
17
+ 'binding_melting_temperature',
18
+ 'binding_gc_content',
19
+ 'heterodimer_melting_temperature',
20
+ 'heterodimer_deltaG',
21
+ ];
22
+
23
+ const DATABASE_ID = 7;
24
+ const HOMODIMER_MELTING_TEMPERATURE = 51.58;
25
+ const HOMODIMER_DELTA_G = -100.01;
26
+ const HAIRPIN_MELTING_TEMPERATURE = 52.01;
27
+ const HAIRPIN_DELTA_G = -101.02;
28
+ const MELTING_TEMPERATURE = 50.02;
29
+ const GC_CONTENT = 0.438;
30
+
31
+ describe('primersToTabularFile', () => {
32
+ it('converts primers to a tabular file format', () => {
33
+ const primers = [
34
+ { id: 1,
35
+ name: 'Primer1',
36
+ sequence: 'ATCG',
37
+ length: 4,
38
+ database_id: DATABASE_ID,
39
+ melting_temperature: MELTING_TEMPERATURE,
40
+ gc_content: GC_CONTENT,
41
+ homodimer: {
42
+ melting_temperature: HOMODIMER_MELTING_TEMPERATURE,
43
+ deltaG: HOMODIMER_DELTA_G,
44
+ },
45
+ hairpin: {
46
+ melting_temperature: HAIRPIN_MELTING_TEMPERATURE,
47
+ deltaG: HAIRPIN_DELTA_G,
48
+ },
49
+ },
50
+ { id: 6, name: 'Primer2', sequence: 'GCTA', database_id: null },
51
+ ];
52
+
53
+ [',', '\t'].forEach((separator) => {
54
+ const result = primersToTabularFile(primers, mockPCRDetails, separator);
55
+
56
+ const expectedOutput = `${headers.join(separator)}\n${
57
+ '1,Primer1,ATCG,4,50,44,51.6,-100,52,-101,3,21,56.7,48,20.5,-5276\n'.replaceAll(',', separator)
58
+ }${`6,Primer2,GCTA${separator.repeat(headers.length - 3)}`.replaceAll(',', separator)}`;
59
+
60
+ expect(result).toBe(expectedOutput);
61
+ });
62
+ });
63
+
64
+ it('correctly handles zero values', () => {
65
+ const primers = [
66
+ { id: 1,
67
+ name: 'Primer1',
68
+ sequence: 'ATCG',
69
+ length: 0,
70
+ database_id: 0,
71
+ melting_temperature: 0,
72
+ gc_content: 0,
73
+ homodimer: {
74
+ melting_temperature: 0,
75
+ deltaG: 0,
76
+ },
77
+ hairpin: {
78
+ melting_temperature: 0,
79
+ deltaG: 0,
80
+ },
81
+ },
82
+ ];
83
+ const mockPCRDetailsZero = [{
84
+ ...mockPCRDetails[0],
85
+ heterodimer: {
86
+ melting_temperature: 0,
87
+ deltaG: 0,
88
+ },
89
+ fwdPrimer: {
90
+ ...mockPCRDetails[0].fwdPrimer,
91
+ melting_temperature: 0,
92
+ gc_content: 0,
93
+ length: 0,
94
+ },
95
+ }];
96
+
97
+ const result = primersToTabularFile(primers, mockPCRDetailsZero, ',');
98
+
99
+ const expectedOutput = `${headers.join(',')}\n`
100
+ + '1,Primer1,ATCG,0,0,0,0,0,0,0,3,0,0,0,0,0';
101
+
102
+ expect(result).toBe(expectedOutput);
103
+ });
104
+ it('returns empty string if no primers', () => {
105
+ const result = primersToTabularFile([], mockPCRDetails, ',');
106
+ expect(result).toBe('');
107
+ });
108
+ });
@@ -0,0 +1,267 @@
1
+ import React from 'react';
2
+ import SettingsTab from './SettingsTab';
3
+ import store from '@opencloning/store';
4
+ import { cloningActions } from '@opencloning/store/cloning';
5
+ import { Provider } from 'react-redux';
6
+ import { setInputValue, checkInputValue } from '../../../../../cypress/e2e/common_functions';
7
+
8
+ const { setGlobalPrimerSettings } = cloningActions;
9
+
10
+ // Field labels for maintainable selectors
11
+ const FIELD_LABELS = {
12
+ PRIMER_DNA_CONC: 'Primer DNA concentration',
13
+ MONOVALENT_IONS: 'Monovalent ions',
14
+ DIVALENT_IONS: 'Divalent ions',
15
+ };
16
+
17
+ describe('SettingsTab', () => {
18
+ beforeEach(() => {
19
+ // Reset store to initial state before each test
20
+ store.dispatch(setGlobalPrimerSettings({
21
+ primer_dna_conc: 50,
22
+ primer_salt_monovalent: 50,
23
+ primer_salt_divalent: 1.5,
24
+ }));
25
+ });
26
+
27
+ it('displays current global primer settings', () => {
28
+ cy.mount(
29
+ <Provider store={store}>
30
+ <SettingsTab />
31
+ </Provider>
32
+ );
33
+
34
+ // Check that the current values are displayed using helper functions
35
+ checkInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '50');
36
+ checkInputValue(FIELD_LABELS.MONOVALENT_IONS, '50');
37
+ checkInputValue(FIELD_LABELS.DIVALENT_IONS, '1.5');
38
+ });
39
+
40
+ it('allows editing when Edit button is clicked', () => {
41
+ cy.mount(
42
+ <Provider store={store}>
43
+ <SettingsTab />
44
+ </Provider>
45
+ );
46
+
47
+ // Initially, fields should be disabled
48
+ cy.contains('label', FIELD_LABELS.PRIMER_DNA_CONC)
49
+ .parent()
50
+ .find('input')
51
+ .should('be.disabled');
52
+
53
+ cy.contains('label', FIELD_LABELS.MONOVALENT_IONS)
54
+ .parent()
55
+ .find('input')
56
+ .should('be.disabled');
57
+
58
+ cy.contains('label', FIELD_LABELS.DIVALENT_IONS)
59
+ .parent()
60
+ .find('input')
61
+ .should('be.disabled');
62
+
63
+ // Click Edit button
64
+ cy.contains('Edit').click();
65
+
66
+ // Fields should now be enabled
67
+ cy.contains('label', FIELD_LABELS.PRIMER_DNA_CONC)
68
+ .parent()
69
+ .find('input')
70
+ .should('not.be.disabled');
71
+
72
+ cy.contains('label', FIELD_LABELS.MONOVALENT_IONS)
73
+ .parent()
74
+ .find('input')
75
+ .should('not.be.disabled');
76
+
77
+ cy.contains('label', FIELD_LABELS.DIVALENT_IONS)
78
+ .parent()
79
+ .find('input')
80
+ .should('not.be.disabled');
81
+
82
+ // Save and Cancel buttons should be visible
83
+ cy.contains('Save').should('be.visible');
84
+ cy.contains('Cancel').should('be.visible');
85
+ });
86
+
87
+ it('validates input values and shows error messages for invalid values', () => {
88
+ cy.mount(
89
+ <Provider store={store}>
90
+ <SettingsTab />
91
+ </Provider>
92
+ );
93
+
94
+ // Enter edit mode
95
+ cy.contains('Edit').click();
96
+
97
+ // Test invalid values (0 or negative) using helper functions
98
+ setInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '0');
99
+ setInputValue(FIELD_LABELS.MONOVALENT_IONS, '-5');
100
+ setInputValue(FIELD_LABELS.DIVALENT_IONS, '0');
101
+
102
+ // Check error messages
103
+ cy.get('p').filter(':contains("Must be greater than 0")').should('have.length', 3);
104
+
105
+ // Save button should be disabled
106
+ cy.contains('Save').should('be.disabled');
107
+
108
+ // Check that fields show error state
109
+ cy.contains('label', FIELD_LABELS.PRIMER_DNA_CONC)
110
+ .parent()
111
+ .find('input')
112
+ .should('have.attr', 'aria-invalid', 'true');
113
+ });
114
+
115
+ it('accepts sensible values and enables save button', () => {
116
+ cy.mount(
117
+ <Provider store={store}>
118
+ <SettingsTab />
119
+ </Provider>
120
+ );
121
+
122
+ // Enter edit mode
123
+ cy.contains('Edit').click();
124
+
125
+ // Enter valid values using helper functions
126
+ setInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '100');
127
+ setInputValue(FIELD_LABELS.MONOVALENT_IONS, '75');
128
+ setInputValue(FIELD_LABELS.DIVALENT_IONS, '2.5');
129
+
130
+ // No error messages should be visible
131
+ cy.contains('Must be greater than 0').should('not.exist');
132
+
133
+ // Save button should be enabled
134
+ cy.contains('Save').should('not.be.disabled');
135
+ });
136
+
137
+ it('updates store state when valid values are saved', () => {
138
+ cy.mount(
139
+ <Provider store={store}>
140
+ <SettingsTab />
141
+ </Provider>
142
+ );
143
+
144
+ // Enter edit mode
145
+ cy.contains('Edit').click();
146
+
147
+ // Enter new values using helper functions
148
+ setInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '100');
149
+ setInputValue(FIELD_LABELS.MONOVALENT_IONS, '75');
150
+ setInputValue(FIELD_LABELS.DIVALENT_IONS, '2.5');
151
+
152
+ // Save the changes
153
+ cy.contains('Save').click();
154
+
155
+ // Verify the store state was updated
156
+ cy.then(() => {
157
+ const state = store.getState();
158
+ expect(state.cloning.globalPrimerSettings).to.deep.equal({
159
+ primer_dna_conc: 100,
160
+ primer_salt_monovalent: 75,
161
+ primer_salt_divalent: 2.5,
162
+ });
163
+ });
164
+
165
+ // Verify the UI shows the updated values using helper functions
166
+ checkInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '100');
167
+ checkInputValue(FIELD_LABELS.MONOVALENT_IONS, '75');
168
+ checkInputValue(FIELD_LABELS.DIVALENT_IONS, '2.5');
169
+ });
170
+
171
+ it('cancels changes and reverts to original values', () => {
172
+ cy.mount(
173
+ <Provider store={store}>
174
+ <SettingsTab />
175
+ </Provider>
176
+ );
177
+
178
+ // Enter edit mode
179
+ cy.contains('Edit').click();
180
+
181
+ // Enter new values using helper functions
182
+ setInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '999');
183
+ setInputValue(FIELD_LABELS.MONOVALENT_IONS, '888');
184
+ setInputValue(FIELD_LABELS.DIVALENT_IONS, '777');
185
+
186
+ // Cancel the changes
187
+ cy.contains('Cancel').click();
188
+
189
+ // Verify the values reverted to original using helper functions
190
+ checkInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '50');
191
+ checkInputValue(FIELD_LABELS.MONOVALENT_IONS, '50');
192
+ checkInputValue(FIELD_LABELS.DIVALENT_IONS, '1.5');
193
+
194
+ // Verify the store state was not changed
195
+ cy.then(() => {
196
+ const state = store.getState();
197
+ expect(state.cloning.globalPrimerSettings).to.deep.equal({
198
+ primer_dna_conc: 50,
199
+ primer_salt_monovalent: 50,
200
+ primer_salt_divalent: 1.5,
201
+ });
202
+ });
203
+ });
204
+
205
+ it('handles decimal values correctly', () => {
206
+ cy.mount(
207
+ <Provider store={store}>
208
+ <SettingsTab />
209
+ </Provider>
210
+ );
211
+
212
+ // Enter edit mode
213
+ cy.contains('Edit').click();
214
+
215
+ // Enter decimal values using helper functions
216
+ setInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '25.5');
217
+ setInputValue(FIELD_LABELS.MONOVALENT_IONS, '37.2');
218
+ setInputValue(FIELD_LABELS.DIVALENT_IONS, '0.8');
219
+
220
+ // Save the changes
221
+ cy.contains('Save').click();
222
+
223
+ // Verify the store state was updated with decimal values
224
+ cy.then(() => {
225
+ const state = store.getState();
226
+ expect(state.cloning.globalPrimerSettings).to.deep.equal({
227
+ primer_dna_conc: 25.5,
228
+ primer_salt_monovalent: 37.2,
229
+ primer_salt_divalent: 0.8,
230
+ });
231
+ });
232
+ });
233
+
234
+ it('validates edge case values', () => {
235
+ cy.mount(
236
+ <Provider store={store}>
237
+ <SettingsTab />
238
+ </Provider>
239
+ );
240
+
241
+ // Enter edit mode
242
+ cy.contains('Edit').click();
243
+
244
+ // Test very small positive values (should be valid) using helper functions
245
+ setInputValue(FIELD_LABELS.PRIMER_DNA_CONC, '0.1');
246
+ setInputValue(FIELD_LABELS.MONOVALENT_IONS, '0.01');
247
+ setInputValue(FIELD_LABELS.DIVALENT_IONS, '0.001');
248
+
249
+ // No error messages should be visible
250
+ cy.contains('Must be greater than 0').should('not.exist');
251
+
252
+ // Save button should be enabled
253
+ cy.contains('Save').should('not.be.disabled');
254
+
255
+ // Save and verify
256
+ cy.contains('Save').click();
257
+
258
+ cy.then(() => {
259
+ const state = store.getState();
260
+ expect(state.cloning.globalPrimerSettings).to.deep.equal({
261
+ primer_dna_conc: 0.1,
262
+ primer_salt_monovalent: 0.01,
263
+ primer_salt_divalent: 0.001,
264
+ });
265
+ });
266
+ });
267
+ });