@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.
- package/CHANGELOG.md +18 -0
- package/package.json +37 -0
- package/src/components/AppAlerts.jsx +19 -0
- package/src/components/CloningHistory.jsx +40 -0
- package/src/components/DataModelDisplayer.jsx +30 -0
- package/src/components/DescriptionEditor.jsx +68 -0
- package/src/components/DownloadCloningStrategyDialog.jsx +84 -0
- package/src/components/DownloadSequenceFileDialog.jsx +90 -0
- package/src/components/DragAndDropCloningHistoryWrapper.jsx +39 -0
- package/src/components/DraggableDialogPaper.jsx +16 -0
- package/src/components/EditSequenceNameDialog.jsx +90 -0
- package/src/components/ExternalServicesStatusCheck.jsx +92 -0
- package/src/components/HistoryLoadedDialog.jsx +49 -0
- package/src/components/LoadCloningHistoryWrapper.jsx +166 -0
- package/src/components/MainSequenceCheckBox.jsx +83 -0
- package/src/components/MainSequenceEditor.jsx +165 -0
- package/src/components/NetworkNode.jsx +159 -0
- package/src/components/NetworkTree.css +127 -0
- package/src/components/ObjectTable.jsx +24 -0
- package/src/components/OpenCloning.jsx +102 -0
- package/src/components/OverhangsDisplay.jsx +25 -0
- package/src/components/SequenceEditor.jsx +120 -0
- package/src/components/SequenceTab.jsx +14 -0
- package/src/components/TemplateSequence.jsx +38 -0
- package/src/components/annotation/PlannotateAnnotationReport.jsx +33 -0
- package/src/components/annotation/useUpdateAnnotationInMainSequence.js +39 -0
- package/src/components/assembler/AssemblePartWidget.jsx +252 -0
- package/src/components/assembler/Assembler.jsx +273 -0
- package/src/components/assembler/AssemblerPart.jsx +99 -0
- package/src/components/assembler/StopIcon.jsx +34 -0
- package/src/components/assembler/assembler_data2.json +50 -0
- package/src/components/assembler/assembly_component.module.css +81 -0
- package/src/components/assembler/moclo.json +110 -0
- package/src/components/assembler/sbol_visual_glyphs/LICENSE.html +21 -0
- package/src/components/assembler/sbol_visual_glyphs/assembly-scar.svg +63 -0
- package/src/components/assembler/sbol_visual_glyphs/cds-stop.svg +85 -0
- package/src/components/assembler/sbol_visual_glyphs/cds.svg +60 -0
- package/src/components/assembler/sbol_visual_glyphs/chromosomal-locus.svg +78 -0
- package/src/components/assembler/sbol_visual_glyphs/engineered-region.svg +56 -0
- package/src/components/assembler/sbol_visual_glyphs/five-prime-sticky-restriction-site.svg +56 -0
- package/src/components/assembler/sbol_visual_glyphs/origin-of-replication.svg +57 -0
- package/src/components/assembler/sbol_visual_glyphs/primer-binding-site.svg +59 -0
- package/src/components/assembler/sbol_visual_glyphs/promoter.svg +60 -0
- package/src/components/assembler/sbol_visual_glyphs/ribosome-entry-site.svg +56 -0
- package/src/components/assembler/sbol_visual_glyphs/specific-recombination-site.svg +59 -0
- package/src/components/assembler/sbol_visual_glyphs/terminator.svg +60 -0
- package/src/components/assembler/sbol_visual_glyphs/three-prime-sticky-restriction-site.svg +56 -0
- package/src/components/assembler/sbol_visual_glyphs.js +36 -0
- package/src/components/assembler/useAssembler.js +71 -0
- package/src/components/dummy/DummyInterface.js +41 -0
- package/src/components/dummy/GetSequenceFileAndDatabaseIdComponent.jsx +59 -0
- package/src/components/eLabFTW/ELabFTWCategorySelect.cy.jsx +86 -0
- package/src/components/eLabFTW/ELabFTWCategorySelect.jsx +43 -0
- package/src/components/eLabFTW/ELabFTWFileSelect.cy.jsx +43 -0
- package/src/components/eLabFTW/ELabFTWFileSelect.jsx +29 -0
- package/src/components/eLabFTW/ELabFTWResourceSelect.cy.jsx +107 -0
- package/src/components/eLabFTW/ELabFTWResourceSelect.jsx +23 -0
- package/src/components/eLabFTW/GetPrimerComponent.cy.jsx +261 -0
- package/src/components/eLabFTW/GetPrimerComponent.jsx +55 -0
- package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.cy.jsx +184 -0
- package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.jsx +62 -0
- package/src/components/eLabFTW/LoadHistoryComponent.cy.jsx +235 -0
- package/src/components/eLabFTW/LoadHistoryComponent.jsx +51 -0
- package/src/components/eLabFTW/PrimersNotInDatabaseComponent.cy.jsx +159 -0
- package/src/components/eLabFTW/PrimersNotInDatabaseComponent.jsx +54 -0
- package/src/components/eLabFTW/SubmitToDatabaseComponent.cy.jsx +185 -0
- package/src/components/eLabFTW/SubmitToDatabaseComponent.jsx +51 -0
- package/src/components/eLabFTW/common.js +26 -0
- package/src/components/eLabFTW/eLabFTWInterface.js +294 -0
- package/src/components/eLabFTW/eLabFTWInterface.test.js +839 -0
- package/src/components/eLabFTW/envValues.js +7 -0
- package/src/components/eLabFTW/utils.js +39 -0
- package/src/components/form/CustomFormHelperText.jsx +10 -0
- package/src/components/form/EnzymeMultiSelect.cy.jsx +61 -0
- package/src/components/form/EnzymeMultiSelect.jsx +34 -0
- package/src/components/form/GetRequestMultiSelect.jsx +107 -0
- package/src/components/form/LabelWithTooltip.jsx +16 -0
- package/src/components/form/PostRequestSelect.cy.jsx +70 -0
- package/src/components/form/PostRequestSelect.jsx +86 -0
- package/src/components/form/RequestStatusWrapper.jsx +17 -0
- package/src/components/form/RetryAlert.jsx +20 -0
- package/src/components/form/ServerErrorMessage.jsx +10 -0
- package/src/components/form/SubmitButtonBackendAPI.jsx +15 -0
- package/src/components/form/SubmitToDatabaseDialog.jsx +133 -0
- package/src/components/form/TextFieldValidate.jsx +67 -0
- package/src/components/form/ValidatedTextField.jsx +33 -0
- package/src/components/form/intermediates_disclaimer.svg +181 -0
- package/src/components/navigation/ButtonWithMenu.jsx +43 -0
- package/src/components/navigation/CustomTab.jsx +14 -0
- package/src/components/navigation/FeedbackDialog.jsx +34 -0
- package/src/components/navigation/GithubCornerRight.jsx +29 -0
- package/src/components/navigation/MainAppBar.css +26 -0
- package/src/components/navigation/MainAppBar.jsx +205 -0
- package/src/components/navigation/SelectExampleDialog.jsx +69 -0
- package/src/components/navigation/SelectTemplateDialog.jsx +107 -0
- package/src/components/navigation/TabPanel.jsx +28 -0
- package/src/components/navigation/VersionDialog.jsx +33 -0
- package/src/components/primers/CreatePrimerFromSequenceForm.jsx +42 -0
- package/src/components/primers/DownloadPrimersButton.jsx +104 -0
- package/src/components/primers/PrimerForm.css +14 -0
- package/src/components/primers/PrimerForm.jsx +107 -0
- package/src/components/primers/PrimerList.css +46 -0
- package/src/components/primers/PrimerList.cy.jsx +95 -0
- package/src/components/primers/PrimerList.jsx +126 -0
- package/src/components/primers/PrimerTableRow.cy.jsx +57 -0
- package/src/components/primers/PrimerTableRow.jsx +84 -0
- package/src/components/primers/SelectPrimerForm.jsx +66 -0
- package/src/components/primers/import_primers/ImportPrimersButton.jsx +101 -0
- package/src/components/primers/import_primers/ImportPrimersTable.jsx +51 -0
- package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +107 -0
- package/src/components/primers/import_primers/styles.css +60 -0
- package/src/components/primers/primer_design/SequenceTabComponents/CollapsableLabel.jsx +31 -0
- package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +164 -0
- package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +37 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +369 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignEBIC.jsx +24 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +29 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGatewayBP.jsx +36 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +26 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignHomologousRecombination.jsx +32 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignRestriction.jsx +25 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +25 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignStepper.jsx +53 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +80 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerResultForm.jsx +49 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerSettingsForm.jsx +68 -0
- package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +84 -0
- package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx +85 -0
- package/src/components/primers/primer_design/SequenceTabComponents/StepNavigation.jsx +48 -0
- package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +216 -0
- package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +42 -0
- package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +61 -0
- package/src/components/primers/primer_design/SequenceTabComponents/TabPannelSettings.jsx +59 -0
- package/src/components/primers/primer_design/SequenceTabComponents/primerDesignMinimalValues.json +5 -0
- package/src/components/primers/primer_design/SequenceTabComponents/useEBICPrimerDesignSettings.js +31 -0
- package/src/components/primers/primer_design/SequenceTabComponents/useEnzymePrimerDesignSettings.js +57 -0
- package/src/components/primers/primer_design/SequenceTabComponents/useGatewayPrimerDesignSettings.js +18 -0
- package/src/components/primers/primer_design/SequenceTabComponents/usePrimerDesignSettings.js +31 -0
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +88 -0
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +65 -0
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +84 -0
- package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +74 -0
- package/src/components/primers/primer_design/common/NoAttPSitesError.jsx +31 -0
- package/src/components/primers/primer_details/PCRTable.cy.jsx +51 -0
- package/src/components/primers/primer_details/PCRTable.jsx +35 -0
- package/src/components/primers/primer_details/Primer3Figure.jsx +25 -0
- package/src/components/primers/primer_details/PrimerDetailsTds.jsx +39 -0
- package/src/components/primers/primer_details/PrimerInfoIcon.cy.jsx +137 -0
- package/src/components/primers/primer_details/PrimerInfoIcon.jsx +132 -0
- package/src/components/primers/primer_details/TableSection.jsx +17 -0
- package/src/components/primers/primer_details/primerDetailsFormatting.js +3 -0
- package/src/components/primers/primer_details/useMultiplePrimerDetails.js +29 -0
- package/src/components/primers/primer_details/usePCRDetails.js +47 -0
- package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +49 -0
- package/src/components/primers/primer_details/useSinglePrimerSequenceDetails.js +25 -0
- package/src/components/primers/primersToTabularFile.js +49 -0
- package/src/components/primers/primersToTabularFile.test.js +108 -0
- package/src/components/settings/SettingsTab.cy.jsx +267 -0
- package/src/components/settings/SettingsTab.jsx +170 -0
- package/src/components/sources/AssemblyPlanDisplayer.cy.jsx +22 -0
- package/src/components/sources/AssemblyPlanDisplayer.jsx +27 -0
- package/src/components/sources/CollectionSource.jsx +97 -0
- package/src/components/sources/FinishedSource.jsx +397 -0
- package/src/components/sources/KnownSourceErrors.jsx +50 -0
- package/src/components/sources/MultipleInputsSelector.jsx +63 -0
- package/src/components/sources/MultipleOutputsSelector.jsx +63 -0
- package/src/components/sources/NewSourceBox.jsx +37 -0
- package/src/components/sources/PCRUnitForm.jsx +102 -0
- package/src/components/sources/SingleInputSelector.jsx +36 -0
- package/src/components/sources/Source.jsx +125 -0
- package/src/components/sources/SourceAnnotation.jsx +44 -0
- package/src/components/sources/SourceAssembly.jsx +201 -0
- package/src/components/sources/SourceBox.css +18 -0
- package/src/components/sources/SourceBox.jsx +60 -0
- package/src/components/sources/SourceCopySequence.jsx +38 -0
- package/src/components/sources/SourceDatabase.jsx +28 -0
- package/src/components/sources/SourceFile.jsx +188 -0
- package/src/components/sources/SourceGenomeRegion.cy.jsx +131 -0
- package/src/components/sources/SourceGenomeRegion.jsx +486 -0
- package/src/components/sources/SourceHomologousRecombination.jsx +125 -0
- package/src/components/sources/SourceKnownGenomeRegion.jsx +60 -0
- package/src/components/sources/SourceManuallyTyped.jsx +116 -0
- package/src/components/sources/SourcePCRorHybridization.jsx +165 -0
- package/src/components/sources/SourcePolymeraseExtension.jsx +44 -0
- package/src/components/sources/SourceRepositoryId.jsx +409 -0
- package/src/components/sources/SourceRestriction.jsx +41 -0
- package/src/components/sources/SourceReverseComplement.jsx +33 -0
- package/src/components/sources/SourceTypeSelector.jsx +94 -0
- package/src/components/sources/SubSequenceDisplayer.jsx +70 -0
- package/src/components/sources/VerifyDeleteDialog.jsx +23 -0
- package/src/components/sources/repositoryMetadata.js +14 -0
- package/src/components/verification/LoadFromDatabaseButton.jsx +90 -0
- package/src/components/verification/SequencingFileRow.jsx +34 -0
- package/src/components/verification/VerificationFileDialog.cy.jsx +176 -0
- package/src/components/verification/VerificationFileDialog.jsx +248 -0
- package/src/config/defaultMainEditorProps.js +44 -0
- package/src/hooks/useAlerts.js +16 -0
- package/src/hooks/useBackendAPI.js +51 -0
- package/src/hooks/useBackendRoute.js +22 -0
- package/src/hooks/useDatabase.js +18 -0
- package/src/hooks/useDragAndDropFile.js +31 -0
- package/src/hooks/useGatewaySites.js +40 -0
- package/src/hooks/useHttpClient.js +12 -0
- package/src/hooks/useLoadDatabaseFile.js +108 -0
- package/src/hooks/useStoreEditor.js +101 -0
- package/src/hooks/useValidateState.js +43 -0
- 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
|
+
});
|