@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,69 @@
|
|
|
1
|
+
import { Dialog, DialogTitle, List, ListItem, ListItemButton, ListItemText } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
const examples = [
|
|
5
|
+
{
|
|
6
|
+
title: 'Arabidopsis CRISPR-HDR genome editing',
|
|
7
|
+
link: 'arabidopsis_CRISPR_HDR.zip',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
title: 'Gibson assembly',
|
|
11
|
+
link: 'gibson_assembly.json',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
title: 'Golden gate assembly',
|
|
15
|
+
link: 'golden_gate.json',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: 'Integration of cassette by homologous recombination',
|
|
19
|
+
link: 'homologous_recombination.json',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: 'Restriction + ligation assembly (v1)',
|
|
23
|
+
link: 'restriction_ligation_assembly.json',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'Restriction + ligation assembly (v2)',
|
|
27
|
+
link: 'restriction_then_ligation.json',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'Templateless PCR',
|
|
31
|
+
link: 'templateless_PCR.json',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: 'CRISPR HDR',
|
|
35
|
+
link: 'crispr_hdr.json',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'Gateway cloning',
|
|
39
|
+
link: 'gateway.json',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: 'Annotate features with pLannotate',
|
|
43
|
+
link: 'plannotate.json',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: 'Cre/Lox recombination',
|
|
47
|
+
link: 'cre_lox_recombination.json',
|
|
48
|
+
},
|
|
49
|
+
].sort((a, b) => a.title.localeCompare(b.title));
|
|
50
|
+
|
|
51
|
+
function SelectExampleDialog({ onClose, open }) {
|
|
52
|
+
return (
|
|
53
|
+
<Dialog open={open} onClose={() => onClose('')} className="load-example-dialog">
|
|
54
|
+
<DialogTitle>Load an example</DialogTitle>
|
|
55
|
+
<List>
|
|
56
|
+
{
|
|
57
|
+
examples.map((example) => (
|
|
58
|
+
<ListItem key={example.link} className="load-example-item">
|
|
59
|
+
<ListItemButton onClick={() => onClose(`${import.meta.env.BASE_URL}examples/${example.link}`)}><ListItemText>{example.title}</ListItemText></ListItemButton>
|
|
60
|
+
</ListItem>
|
|
61
|
+
))
|
|
62
|
+
}
|
|
63
|
+
</List>
|
|
64
|
+
|
|
65
|
+
</Dialog>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default SelectExampleDialog;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Accordion, AccordionDetails, AccordionSummary, Alert, Button, CircularProgress, Dialog, DialogContent, DialogTitle, IconButton, List, ListItem, ListItemButton, ListItemText } from '@mui/material';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
5
|
+
import useHttpClient from '../../hooks/useHttpClient';
|
|
6
|
+
|
|
7
|
+
function SelectTemplateDialog({ onClose, open }) {
|
|
8
|
+
const [templates, setTemplates] = React.useState(null);
|
|
9
|
+
const [connectAttempt, setConnectAttemp] = React.useState(0);
|
|
10
|
+
const [error, setError] = React.useState(null);
|
|
11
|
+
const baseUrl = 'https://assets.opencloning.org/OpenCloning-submission';
|
|
12
|
+
const httpClient = useHttpClient();
|
|
13
|
+
|
|
14
|
+
// const baseUrl = '';
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
const fetchData = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const resp = await httpClient.get(`${baseUrl}/index.json`);
|
|
19
|
+
setTemplates(resp.data);
|
|
20
|
+
setError('');
|
|
21
|
+
} catch {
|
|
22
|
+
setTemplates(null);
|
|
23
|
+
setError('Failed to load templates from GitHub');
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
fetchData();
|
|
27
|
+
}, [connectAttempt]);
|
|
28
|
+
return (
|
|
29
|
+
<Dialog open={open} onClose={() => onClose('')} className="load-template-dialog">
|
|
30
|
+
<DialogTitle>Load a template</DialogTitle>
|
|
31
|
+
{!templates
|
|
32
|
+
&& (
|
|
33
|
+
<DialogContent>
|
|
34
|
+
{error ? (
|
|
35
|
+
<Alert
|
|
36
|
+
severity="error"
|
|
37
|
+
action={(
|
|
38
|
+
<Button color="inherit" size="small" onClick={() => { setError(''); setConnectAttemp((p) => p + 1); }}>
|
|
39
|
+
Retry
|
|
40
|
+
</Button>
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{error}
|
|
44
|
+
</Alert>
|
|
45
|
+
) : (
|
|
46
|
+
<>
|
|
47
|
+
Loading templates...
|
|
48
|
+
{' '}
|
|
49
|
+
<CircularProgress className="loading-progress" color="inherit" size="2em" />
|
|
50
|
+
</>
|
|
51
|
+
)}
|
|
52
|
+
</DialogContent>
|
|
53
|
+
)}
|
|
54
|
+
<List>
|
|
55
|
+
{
|
|
56
|
+
templates && Object.keys(templates).map((key) => {
|
|
57
|
+
const { kit, assemblies } = templates[key];
|
|
58
|
+
return (
|
|
59
|
+
<Accordion key={key} className="load-template-item">
|
|
60
|
+
<AccordionSummary
|
|
61
|
+
expandIcon={<ExpandMoreIcon />}
|
|
62
|
+
aria-controls={`${key}-content`}
|
|
63
|
+
id={`${key}-header`}
|
|
64
|
+
>
|
|
65
|
+
<ListItemText sx={{ my: 0 }} primary={kit.title} secondary={kit.description} />
|
|
66
|
+
</AccordionSummary>
|
|
67
|
+
<AccordionDetails sx={{ py: 0 }}>
|
|
68
|
+
<List sx={{ py: 0, my: 0 }}>
|
|
69
|
+
{
|
|
70
|
+
assemblies.map((assembly) => (
|
|
71
|
+
<ListItem sx={{ px: 0, pt: 0 }} key={assembly.template_file} className="load-template-item">
|
|
72
|
+
<ListItemButton sx={{ py: 0 }} onClick={() => onClose(`${baseUrl}/processed/${key}/templates/${assembly.template_file}`, true)}>
|
|
73
|
+
<ListItemText primary={assembly.title} secondary={assembly.description} />
|
|
74
|
+
</ListItemButton>
|
|
75
|
+
</ListItem>
|
|
76
|
+
))
|
|
77
|
+
}
|
|
78
|
+
</List>
|
|
79
|
+
</AccordionDetails>
|
|
80
|
+
</Accordion>
|
|
81
|
+
|
|
82
|
+
);
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
<ListItem key="blah" className="load-template-item">
|
|
86
|
+
|
|
87
|
+
<ListItemText
|
|
88
|
+
primary="🔎 Can't find your favourite kit?"
|
|
89
|
+
secondary={(
|
|
90
|
+
<>
|
|
91
|
+
Create it from an Addgene kit.
|
|
92
|
+
<br />
|
|
93
|
+
It's very easy!
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
97
|
+
<IconButton edge="end" aria-label="submit-button">
|
|
98
|
+
<Button className="button-hyperlink" variant="contained" color="success" href="https://github.com/OpenCloning/OpenCloning-submission/blob/master/docs/index.md" target="_blank" rel="noopener noreferrer">Create templates</Button>
|
|
99
|
+
</IconButton>
|
|
100
|
+
|
|
101
|
+
</ListItem>
|
|
102
|
+
</List>
|
|
103
|
+
</Dialog>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default SelectTemplateDialog;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const TabPanel = React.forwardRef(({ children, value, index, ...other }, ref) => {
|
|
4
|
+
const style = value === index ? {
|
|
5
|
+
visibility: 'visible',
|
|
6
|
+
|
|
7
|
+
} : {
|
|
8
|
+
visibility: 'hidden',
|
|
9
|
+
position: 'absolute',
|
|
10
|
+
width: '1px',
|
|
11
|
+
height: '0px',
|
|
12
|
+
overflow: 'hidden',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
role="tabpanel"
|
|
18
|
+
style={style}
|
|
19
|
+
id={`tab-panel-${index}`}
|
|
20
|
+
aria-labelledby={`tab-${index}`}
|
|
21
|
+
{...other}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export default TabPanel;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Dialog, DialogContent, DialogTitle, List, ListItem, ListItemText } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useSelector } from 'react-redux';
|
|
4
|
+
import { isEqual } from 'lodash-es';
|
|
5
|
+
|
|
6
|
+
function VersionDialog({ open, setOpen }) {
|
|
7
|
+
const { backendVersion, schemaVersion, frontendVersion } = useSelector(({ cloning }) => cloning.appInfo, isEqual);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Dialog
|
|
11
|
+
open={open}
|
|
12
|
+
onClose={() => setOpen(false)}
|
|
13
|
+
className="version-dialog"
|
|
14
|
+
>
|
|
15
|
+
<DialogTitle sx={{ textAlign: 'center' }}> App version </DialogTitle>
|
|
16
|
+
<DialogContent>
|
|
17
|
+
<List>
|
|
18
|
+
<ListItem fullWidth>
|
|
19
|
+
<ListItemText primary="Frontend" secondary={frontendVersion || 'N.A.'} />
|
|
20
|
+
</ListItem>
|
|
21
|
+
<ListItem fullWidth>
|
|
22
|
+
<ListItemText primary="Backend" secondary={backendVersion || 'N.A.'} />
|
|
23
|
+
</ListItem>
|
|
24
|
+
<ListItem fullWidth>
|
|
25
|
+
<ListItemText primary="Schema" secondary={schemaVersion || 'N.A.'} />
|
|
26
|
+
</ListItem>
|
|
27
|
+
</List>
|
|
28
|
+
</DialogContent>
|
|
29
|
+
</Dialog>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default VersionDialog;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FormControl, TextField } from '@mui/material';
|
|
3
|
+
import './PrimerForm.css';
|
|
4
|
+
import './PrimerList.css';
|
|
5
|
+
import CustomFormHelperText from '../form/CustomFormHelperText';
|
|
6
|
+
|
|
7
|
+
function CreatePrimerFromSequenceForm({
|
|
8
|
+
primer: { name, sequence }, setName, setSequence, existingPrimerNames,
|
|
9
|
+
}) {
|
|
10
|
+
let nameError = '';
|
|
11
|
+
if (existingPrimerNames.includes(name)) {
|
|
12
|
+
nameError = 'Name exists';
|
|
13
|
+
}
|
|
14
|
+
if (name === '') {
|
|
15
|
+
nameError = 'Name is required';
|
|
16
|
+
}
|
|
17
|
+
return (
|
|
18
|
+
<div className="primer-result-form">
|
|
19
|
+
<FormControl sx={{ m: 1, pb: 2, display: { width: '30%' } }}>
|
|
20
|
+
<TextField
|
|
21
|
+
label="Name"
|
|
22
|
+
value={name}
|
|
23
|
+
onChange={(e) => setName(e.target.value)}
|
|
24
|
+
error={nameError !== ''}
|
|
25
|
+
FormHelperTextProps={{ component: 'div' }}
|
|
26
|
+
helperText={<CustomFormHelperText>{nameError}</CustomFormHelperText>}
|
|
27
|
+
/>
|
|
28
|
+
</FormControl>
|
|
29
|
+
<FormControl sx={{ m: 1, pb: 2, display: { width: '60%' } }}>
|
|
30
|
+
<TextField
|
|
31
|
+
label="Sequence"
|
|
32
|
+
value={sequence}
|
|
33
|
+
onChange={(e) => setSequence(e.target.value)}
|
|
34
|
+
inputProps={{ id: 'sequence' }}
|
|
35
|
+
FormHelperTextProps={{ component: 'div' }}
|
|
36
|
+
/>
|
|
37
|
+
</FormControl>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default CreatePrimerFromSequenceForm;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormLabel, RadioGroup, FormControlLabel, Radio, TextField } from '@mui/material';
|
|
4
|
+
import { useSelector } from 'react-redux';
|
|
5
|
+
import primersToTabularFile from './primersToTabularFile';
|
|
6
|
+
|
|
7
|
+
import { downloadTextFile } from '@opencloning/utils/readNwrite';
|
|
8
|
+
import { usePCRDetails } from './primer_details/usePCRDetails';
|
|
9
|
+
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
10
|
+
import useMultiplePrimerDetails from './primer_details/useMultiplePrimerDetails';
|
|
11
|
+
import { isCompleteOligoHybridizationSource, isCompletePCRSource } from '@opencloning/store/cloning_utils';
|
|
12
|
+
|
|
13
|
+
function DownloadPrimersDialog({ primers, open, onClose }) {
|
|
14
|
+
const pcrSourceIds = useSelector((state) => state.cloning.sources
|
|
15
|
+
.filter((source) => isCompletePCRSource(source) || isCompleteOligoHybridizationSource(source))
|
|
16
|
+
.map((source) => source.id));
|
|
17
|
+
const [fileName, setFileName] = React.useState('primers');
|
|
18
|
+
const [extension, setExtension] = React.useState('.csv');
|
|
19
|
+
|
|
20
|
+
const { pcrDetails, retryGetPCRDetails, requestStatus: pcrDetailsRequestStatus } = usePCRDetails(pcrSourceIds);
|
|
21
|
+
const { primerDetails, retryGetPrimerDetails, requestStatus: primerDetailsRequestStatus } = useMultiplePrimerDetails(primers);
|
|
22
|
+
|
|
23
|
+
const handleDownload = () => {
|
|
24
|
+
const fullFileName = fileName + extension;
|
|
25
|
+
const separator = extension === '.csv' ? ',' : '\t';
|
|
26
|
+
const fileContent = primersToTabularFile(primerDetails, pcrDetails, separator);
|
|
27
|
+
|
|
28
|
+
downloadTextFile(fileContent, fullFileName);
|
|
29
|
+
onClose();
|
|
30
|
+
};
|
|
31
|
+
return (
|
|
32
|
+
<Dialog
|
|
33
|
+
open={open}
|
|
34
|
+
onClose={onClose}
|
|
35
|
+
PaperProps={{
|
|
36
|
+
component: 'form',
|
|
37
|
+
onSubmit: (event) => {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
handleDownload();
|
|
40
|
+
},
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<DialogTitle>Save Primers to File</DialogTitle>
|
|
44
|
+
<DialogContent>
|
|
45
|
+
<FormControl fullWidth>
|
|
46
|
+
<TextField
|
|
47
|
+
autoFocus
|
|
48
|
+
required
|
|
49
|
+
id="file_name"
|
|
50
|
+
label="File name"
|
|
51
|
+
variant="standard"
|
|
52
|
+
value={fileName}
|
|
53
|
+
onChange={(e) => setFileName(e.target.value)}
|
|
54
|
+
sx={{ mb: 2 }}
|
|
55
|
+
/>
|
|
56
|
+
<FormLabel id="save-primers-radio-group-label">File format</FormLabel>
|
|
57
|
+
<RadioGroup
|
|
58
|
+
aria-labelledby="save-primers-radio-group-label"
|
|
59
|
+
value={extension}
|
|
60
|
+
variant="standard"
|
|
61
|
+
onChange={(e) => setExtension(e.target.value)}
|
|
62
|
+
>
|
|
63
|
+
<FormControlLabel value=".csv" control={<Radio />} label="csv" />
|
|
64
|
+
<FormControlLabel value=".tsv" control={<Radio />} label="tsv" />
|
|
65
|
+
</RadioGroup>
|
|
66
|
+
</FormControl>
|
|
67
|
+
</DialogContent>
|
|
68
|
+
<DialogActions>
|
|
69
|
+
<RequestStatusWrapper requestStatus={primerDetailsRequestStatus} retry={() => { retryGetPrimerDetails(); retryGetPCRDetails(); }}>
|
|
70
|
+
<RequestStatusWrapper requestStatus={pcrDetailsRequestStatus} retry={retryGetPCRDetails}>
|
|
71
|
+
<Button onClick={onClose}>
|
|
72
|
+
Cancel
|
|
73
|
+
</Button>
|
|
74
|
+
<Button type="submit">Save file</Button>
|
|
75
|
+
</RequestStatusWrapper>
|
|
76
|
+
</RequestStatusWrapper>
|
|
77
|
+
</DialogActions>
|
|
78
|
+
</Dialog>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function DownloadPrimersButton({ primers }) {
|
|
83
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<Button
|
|
88
|
+
variant="contained"
|
|
89
|
+
onClick={() => setDialogOpen(true)}
|
|
90
|
+
>
|
|
91
|
+
Download Primers
|
|
92
|
+
</Button>
|
|
93
|
+
{dialogOpen && (
|
|
94
|
+
<DownloadPrimersDialog
|
|
95
|
+
primers={primers}
|
|
96
|
+
open={dialogOpen}
|
|
97
|
+
onClose={() => setDialogOpen(false)}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default DownloadPrimersButton;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
3
|
+
import CancelIcon from '@mui/icons-material/Cancel';
|
|
4
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
5
|
+
import { IconButton } from '@mui/material';
|
|
6
|
+
import ValidatedTextField from '../form/ValidatedTextField';
|
|
7
|
+
|
|
8
|
+
import './PrimerForm.css';
|
|
9
|
+
import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
|
|
10
|
+
|
|
11
|
+
function PrimerForm({
|
|
12
|
+
primer = { name: '', sequence: '', id: null }, submitPrimer, cancelForm, existingNames, disabledSequenceText = '',
|
|
13
|
+
}) {
|
|
14
|
+
const [errorStatus, setErrorStatus] = React.useState(primer.id ? { name: false, sequence: false } : { name: true, sequence: true });
|
|
15
|
+
const [touched, setTouched] = React.useState(false);
|
|
16
|
+
const [submissionAttempted, setSubmissionAttempted] = React.useState(false);
|
|
17
|
+
const [name, setName] = React.useState(primer.name);
|
|
18
|
+
const [sequence, setSequence] = React.useState(primer.sequence);
|
|
19
|
+
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
setName(primer.name);
|
|
22
|
+
setSequence(primer.sequence);
|
|
23
|
+
}, [primer.name, primer.sequence]);
|
|
24
|
+
|
|
25
|
+
const updateValidationStatus = (fieldName, valid) => {
|
|
26
|
+
setTouched(true);
|
|
27
|
+
// Update the validation status for the given field (taken from the ref.id)
|
|
28
|
+
setErrorStatus((prevErrorStatus) => ({
|
|
29
|
+
...prevErrorStatus,
|
|
30
|
+
[fieldName]: valid,
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
const submissionAllowed = Object.values(errorStatus).every((error) => !error);
|
|
34
|
+
|
|
35
|
+
const onSubmit = (e) => {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
setSubmissionAttempted(true);
|
|
38
|
+
if (submissionAllowed) {
|
|
39
|
+
// Id is null in the case of adding new primer
|
|
40
|
+
const { id } = primer;
|
|
41
|
+
submitPrimer({ id, name, sequence });
|
|
42
|
+
cancelForm();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onSequenceChange = (event) => {
|
|
47
|
+
// Remove all non-letter characters on paste
|
|
48
|
+
if (event.nativeEvent.inputType === 'insertFromPaste') {
|
|
49
|
+
setSequence(event.target.value.replace(/[^a-zA-Z]/g, ''));
|
|
50
|
+
} else {
|
|
51
|
+
setSequence(event.target.value);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const sequenceErrorChecker = (s) => (stringIsNotDNA(s) ? { error: true, helperText: 'Invalid DNA sequence' } : { error: false, helperText: '' });
|
|
56
|
+
const nameErrorChecker = (s) => (existingNames.includes(s) ? { error: true, helperText: 'Name exists' } : { error: false, helperText: '' });
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<form className="primer-row" onSubmit={onSubmit} style={{ margin: 'auto' }}>
|
|
60
|
+
<ValidatedTextField
|
|
61
|
+
id="name"
|
|
62
|
+
label="Name"
|
|
63
|
+
value={name}
|
|
64
|
+
onInputChange={(e) => setName(e.target.value)}
|
|
65
|
+
sx={{ m: 1, display: { width: '20%' } }}
|
|
66
|
+
submissionAttempted={submissionAttempted}
|
|
67
|
+
required
|
|
68
|
+
errorChecker={nameErrorChecker}
|
|
69
|
+
updateValidationStatus={updateValidationStatus}
|
|
70
|
+
floatingHelperText
|
|
71
|
+
/>
|
|
72
|
+
<ValidatedTextField
|
|
73
|
+
id="sequence"
|
|
74
|
+
label="Sequence"
|
|
75
|
+
value={sequence}
|
|
76
|
+
onInputChange={onSequenceChange}
|
|
77
|
+
sx={{ m: 1, display: { width: '60%' } }}
|
|
78
|
+
className="sequence"
|
|
79
|
+
submissionAttempted={submissionAttempted}
|
|
80
|
+
required
|
|
81
|
+
errorChecker={sequenceErrorChecker}
|
|
82
|
+
updateValidationStatus={updateValidationStatus}
|
|
83
|
+
floatingHelperText
|
|
84
|
+
disabled={disabledSequenceText !== ''}
|
|
85
|
+
initialHelperText={disabledSequenceText}
|
|
86
|
+
/>
|
|
87
|
+
{touched
|
|
88
|
+
&& (
|
|
89
|
+
<IconButton type="submit" sx={{ height: 'fit-content' }}>
|
|
90
|
+
<Tooltip title={submissionAllowed ? 'Save changes' : 'Incorrect values'} arrow placement="top">
|
|
91
|
+
<CheckCircleIcon size={25} className={submissionAllowed ? '' : 'form-invalid'} color={submissionAllowed ? 'success' : 'grey'} />
|
|
92
|
+
</Tooltip>
|
|
93
|
+
</IconButton>
|
|
94
|
+
)}
|
|
95
|
+
{cancelForm !== null && (
|
|
96
|
+
<IconButton onClick={cancelForm} type="button" sx={{ height: 'fit-content' }}>
|
|
97
|
+
<Tooltip title="Discard changes" arrow placement="top">
|
|
98
|
+
<CancelIcon color="error" />
|
|
99
|
+
</Tooltip>
|
|
100
|
+
</IconButton>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
</form>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default PrimerForm;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
table {
|
|
2
|
+
width: auto;
|
|
3
|
+
border-collapse: collapse;
|
|
4
|
+
width: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.primer-table-container {
|
|
8
|
+
width: 60%;
|
|
9
|
+
margin: auto;
|
|
10
|
+
margin-bottom: 10px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.primer-form-container {
|
|
14
|
+
width: 60%;
|
|
15
|
+
margin: auto;
|
|
16
|
+
margin-bottom: 10px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
table th:first-child, td.icons {
|
|
20
|
+
width: 15%;
|
|
21
|
+
/* Prevent icons from appear as column is sequence is very long */
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
th, td {
|
|
26
|
+
border-bottom: 1px solid #ddd;
|
|
27
|
+
padding: 8px;
|
|
28
|
+
text-align: left;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
th {
|
|
32
|
+
background-color: #f0f0f0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
tr:hover {
|
|
37
|
+
background-color: #f0f0f0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
div.primer-table-container {
|
|
41
|
+
overflow-x: scroll;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sequence, #sequence {
|
|
45
|
+
font-family: monospace;
|
|
46
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PrimerList from './PrimerList';
|
|
3
|
+
import store from '@opencloning/store';
|
|
4
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
5
|
+
import { Provider } from 'react-redux';
|
|
6
|
+
|
|
7
|
+
const { setConfig, setPrimers, setGlobalPrimerSettings } = cloningActions;
|
|
8
|
+
|
|
9
|
+
const mockReply = {
|
|
10
|
+
statusCode: 200, body: {
|
|
11
|
+
melting_temperature: 60, gc_content: .5, homodimer: {
|
|
12
|
+
melting_temperature: 0,
|
|
13
|
+
deltaG: 0,
|
|
14
|
+
figure: "dummy_figure"
|
|
15
|
+
},
|
|
16
|
+
hairpin: {
|
|
17
|
+
melting_temperature: 0,
|
|
18
|
+
deltaG: 0,
|
|
19
|
+
figure: "dummy_figure"
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('PrimerList', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
|
|
27
|
+
});
|
|
28
|
+
it('displays the right information', () => {
|
|
29
|
+
store.dispatch(setPrimers([
|
|
30
|
+
{ id: 1, name: 'P1', sequence: 'TCATTAAAGTTAACG' },
|
|
31
|
+
]));
|
|
32
|
+
|
|
33
|
+
cy.mount(
|
|
34
|
+
<Provider store={store}>
|
|
35
|
+
<PrimerList />
|
|
36
|
+
</Provider>
|
|
37
|
+
);
|
|
38
|
+
cy.get('td.name').contains('P1');
|
|
39
|
+
cy.get('td.length').contains('15');
|
|
40
|
+
cy.get('td.gc-content').contains('27');
|
|
41
|
+
cy.get('td.melting-temperature').contains('37.5');
|
|
42
|
+
cy.get('td.sequence').contains('TCATTAAAGTTAACG');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('caches primer details across re-renders and re-renders on global settings change', () => {
|
|
46
|
+
let calls = 0;
|
|
47
|
+
store.dispatch(setPrimers([
|
|
48
|
+
{ id: 1, name: 'P1', sequence: 'AAA' },
|
|
49
|
+
]));
|
|
50
|
+
cy.intercept('POST', 'http://127.0.0.1:8000/primer_details*', (req) => {
|
|
51
|
+
calls += 1;
|
|
52
|
+
const respReply = calls === 1 ? mockReply : {
|
|
53
|
+
statusCode: 200, body: {
|
|
54
|
+
...mockReply.body,
|
|
55
|
+
melting_temperature: calls === 1 ? 60 : 70,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
expect(req.body).to.deep.equal({
|
|
59
|
+
sequence: 'AAA',
|
|
60
|
+
settings: {
|
|
61
|
+
primer_dna_conc: calls === 1 ? 50 : 100,
|
|
62
|
+
primer_salt_monovalent: 50,
|
|
63
|
+
primer_salt_divalent: 1.5,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
req.reply(respReply);
|
|
67
|
+
}).as('primerDetails');
|
|
68
|
+
|
|
69
|
+
// First mount triggers two network calls (one per unique primer sequence)
|
|
70
|
+
cy.mount(
|
|
71
|
+
<Provider store={store}>
|
|
72
|
+
<PrimerList />
|
|
73
|
+
</Provider>)
|
|
74
|
+
cy.contains('Loading...').should('not.exist');
|
|
75
|
+
cy.wait('@primerDetails');
|
|
76
|
+
cy.then(() => {
|
|
77
|
+
expect(calls).to.equal(1);
|
|
78
|
+
cy.mount(
|
|
79
|
+
<Provider store={store}>
|
|
80
|
+
<PrimerList />
|
|
81
|
+
</Provider>)
|
|
82
|
+
cy.then(() => {
|
|
83
|
+
expect(calls).to.equal(1);
|
|
84
|
+
});
|
|
85
|
+
store.dispatch(setGlobalPrimerSettings({ primer_dna_conc: 100 }))
|
|
86
|
+
cy.wait('@primerDetails');
|
|
87
|
+
cy.then(() => {
|
|
88
|
+
expect(calls).to.equal(2);
|
|
89
|
+
cy.get('td.melting-temperature').contains('70');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
});
|
|
95
|
+
});
|