@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,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'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;
|