@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,126 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
|
3
|
+
import Button from '@mui/material/Button';
|
|
4
|
+
import PrimerForm from './PrimerForm';
|
|
5
|
+
import PrimerTableRow from './PrimerTableRow';
|
|
6
|
+
import './PrimerList.css';
|
|
7
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
8
|
+
import ImportPrimersButton from './import_primers/ImportPrimersButton';
|
|
9
|
+
import PrimerDatabaseImportForm from './import_primers/PrimerDatabaseImportForm';
|
|
10
|
+
import { getUsedPrimerIds, isCompleteOligoHybridizationSource, isCompletePCRSource } from '@opencloning/store/cloning_utils';
|
|
11
|
+
import useDatabase from '../../hooks/useDatabase';
|
|
12
|
+
import DownloadPrimersButton from './DownloadPrimersButton';
|
|
13
|
+
import useMultiplePrimerDetails from './primer_details/useMultiplePrimerDetails';
|
|
14
|
+
import { usePCRDetails } from './primer_details/usePCRDetails';
|
|
15
|
+
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
16
|
+
|
|
17
|
+
function PrimerList() {
|
|
18
|
+
const primers = useSelector((state) => state.cloning.primers, shallowEqual);
|
|
19
|
+
const { deletePrimer: deleteAction, addPrimer: addAction, editPrimer: editAction } = cloningActions;
|
|
20
|
+
const database = useDatabase();
|
|
21
|
+
const dispatch = useDispatch();
|
|
22
|
+
const deletePrimer = (id) => dispatch(deleteAction(id));
|
|
23
|
+
const addPrimer = (newPrimer) => dispatch(addAction(newPrimer));
|
|
24
|
+
const editPrimer = (editedPrimer) => dispatch(editAction(editedPrimer));
|
|
25
|
+
const [addingPrimer, setAddingPrimer] = React.useState(false);
|
|
26
|
+
const [editingPrimerId, setEditingPrimerId] = React.useState(null);
|
|
27
|
+
const [importingPrimer, setImportingPrimer] = React.useState(false);
|
|
28
|
+
const onEditClick = (id) => {
|
|
29
|
+
setEditingPrimerId(id);
|
|
30
|
+
setAddingPrimer(false);
|
|
31
|
+
};
|
|
32
|
+
const editingPrimer = primers.find((p) => p.id === editingPrimerId);
|
|
33
|
+
const switchAddingPrimer = () => setAddingPrimer(!addingPrimer);
|
|
34
|
+
// We don't allow used primers to be deleted
|
|
35
|
+
const primerIdsInUse = useSelector(
|
|
36
|
+
(state) => getUsedPrimerIds(state.cloning.sources),
|
|
37
|
+
shallowEqual,
|
|
38
|
+
);
|
|
39
|
+
const pcrSourceIds = useSelector((state) => state.cloning.sources
|
|
40
|
+
.filter((source) => isCompletePCRSource(source) || isCompleteOligoHybridizationSource(source))
|
|
41
|
+
.map((source) => source.id));
|
|
42
|
+
const { primerDetails, retryGetPrimerDetails, requestStatus: primerDetailsRequestStatus } = useMultiplePrimerDetails(primers);
|
|
43
|
+
const { pcrDetails, retryGetPCRDetails, requestStatus: pcrDetailsRequestStatus } = usePCRDetails(pcrSourceIds);
|
|
44
|
+
|
|
45
|
+
const details = primerDetails.length > 0 ? primerDetails : primers.map((p) => ({ ...p, length: p.sequence.length }));
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<div className="primer-table-container">
|
|
49
|
+
<RequestStatusWrapper requestStatus={primerDetailsRequestStatus} retry={() => { retryGetPrimerDetails(); retryGetPCRDetails(); }}>
|
|
50
|
+
<RequestStatusWrapper requestStatus={pcrDetailsRequestStatus} retry={retryGetPCRDetails} />
|
|
51
|
+
</RequestStatusWrapper>
|
|
52
|
+
<table>
|
|
53
|
+
<thead>
|
|
54
|
+
<tr style={{ whiteSpace: 'nowrap' }}>
|
|
55
|
+
<th> </th>
|
|
56
|
+
<th>Name</th>
|
|
57
|
+
<th>Length</th>
|
|
58
|
+
<th>Tm</th>
|
|
59
|
+
<th>GC%</th>
|
|
60
|
+
<th>Sequence</th>
|
|
61
|
+
</tr>
|
|
62
|
+
</thead>
|
|
63
|
+
<tbody>
|
|
64
|
+
{details.filter((primer) => primer.id !== editingPrimerId).map((primer) => (
|
|
65
|
+
<PrimerTableRow
|
|
66
|
+
key={primer.id}
|
|
67
|
+
primerDetails={primer}
|
|
68
|
+
pcrDetails={pcrDetails}
|
|
69
|
+
deletePrimer={deletePrimer}
|
|
70
|
+
canBeDeleted={!primerIdsInUse.includes(primer.id)}
|
|
71
|
+
onEditClick={onEditClick}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</tbody>
|
|
75
|
+
</table>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="primer-form-container">
|
|
78
|
+
{(editingPrimerId && (
|
|
79
|
+
<PrimerForm
|
|
80
|
+
key="primer-edit"
|
|
81
|
+
submitPrimer={editPrimer}
|
|
82
|
+
cancelForm={() => setEditingPrimerId(null)}
|
|
83
|
+
existingNames={primers.filter((p) => p.name !== editingPrimer.name).map((p) => p.name)}
|
|
84
|
+
disabledSequenceText={primerIdsInUse.includes(editingPrimerId) ? 'Cannot edit sequence in use' : ''}
|
|
85
|
+
primer={editingPrimer}
|
|
86
|
+
/>
|
|
87
|
+
)) || (addingPrimer && (
|
|
88
|
+
<PrimerForm
|
|
89
|
+
key="primer-add"
|
|
90
|
+
submitPrimer={addPrimer}
|
|
91
|
+
cancelForm={switchAddingPrimer}
|
|
92
|
+
existingNames={primers.map((p) => p.name)}
|
|
93
|
+
/>
|
|
94
|
+
)) || (importingPrimer && (
|
|
95
|
+
<PrimerDatabaseImportForm
|
|
96
|
+
submitPrimer={addPrimer}
|
|
97
|
+
cancelForm={() => setImportingPrimer(false)}
|
|
98
|
+
existingNames={primers.map((p) => p.name)}
|
|
99
|
+
/>
|
|
100
|
+
)) || (
|
|
101
|
+
<div className="primer-add-container">
|
|
102
|
+
<Button
|
|
103
|
+
variant="contained"
|
|
104
|
+
onClick={switchAddingPrimer}
|
|
105
|
+
>
|
|
106
|
+
Add Primer
|
|
107
|
+
</Button>
|
|
108
|
+
<ImportPrimersButton addPrimer={addPrimer} />
|
|
109
|
+
<DownloadPrimersButton primers={primers} />
|
|
110
|
+
{database && (
|
|
111
|
+
<Button
|
|
112
|
+
variant="contained"
|
|
113
|
+
onClick={() => setImportingPrimer(true)}
|
|
114
|
+
>
|
|
115
|
+
{`Import from ${database.name}`}
|
|
116
|
+
</Button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default PrimerList;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PrimerTableRow from './PrimerTableRow';
|
|
3
|
+
import { mockPrimerDetails, mockPCRDetails, mockPrimer } from '../../../../../tests/mockPrimerDetailsData';
|
|
4
|
+
|
|
5
|
+
describe('<PrimerTableRow />', () => {
|
|
6
|
+
it('displays the right information with PCR details', () => {
|
|
7
|
+
cy.mount(
|
|
8
|
+
<PrimerTableRow
|
|
9
|
+
primerDetails={mockPrimerDetails}
|
|
10
|
+
pcrDetails={mockPCRDetails}
|
|
11
|
+
/>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
cy.get('.name').should('contain', 'Test Primer');
|
|
15
|
+
cy.get('.melting-temperature').should('contain', '56.7 (60)');
|
|
16
|
+
cy.get('.gc-content').should('contain', '48 (50)');
|
|
17
|
+
cy.get('.length').should('contain', '21 (8)');
|
|
18
|
+
cy.get('.sequence').should('contain', 'ACGTACGT');
|
|
19
|
+
});
|
|
20
|
+
it('displays the right information without PCR details', () => {
|
|
21
|
+
cy.mount(
|
|
22
|
+
<PrimerTableRow
|
|
23
|
+
primerDetails={mockPrimerDetails}
|
|
24
|
+
pcrDetails={[]}
|
|
25
|
+
/>,
|
|
26
|
+
);
|
|
27
|
+
cy.get('.melting-temperature').should('contain', '60');
|
|
28
|
+
cy.get('.gc-content').should('contain', '50');
|
|
29
|
+
cy.get('.length').should('contain', '8');
|
|
30
|
+
});
|
|
31
|
+
it('shows skeletons when info missing', () => {
|
|
32
|
+
cy.mount(
|
|
33
|
+
<PrimerTableRow
|
|
34
|
+
primerDetails={mockPrimer}
|
|
35
|
+
pcrDetails={[]}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
cy.get('.melting-temperature .MuiSkeleton-root').should('exist');
|
|
39
|
+
cy.get('.gc-content .MuiSkeleton-root').should('exist');
|
|
40
|
+
});
|
|
41
|
+
it('shows zero values', () => {
|
|
42
|
+
// There is a handful of places where we test that values are not undefined,
|
|
43
|
+
// but if we mistakenly test for bool and the value is 0, nothing will show.
|
|
44
|
+
// This test is to make sure that we are testing for undefined instead of bool.
|
|
45
|
+
cy.mount(
|
|
46
|
+
<PrimerTableRow
|
|
47
|
+
primerDetails={{ ...mockPrimerDetails, melting_temperature: 0, gc_content: 0, length: 0 }}
|
|
48
|
+
pcrDetails={[
|
|
49
|
+
{ ...mockPCRDetails[0], fwdPrimer: { ...mockPCRDetails[0].fwdPrimer, melting_temperature: 0, gc_content: 0, length: 0 } },
|
|
50
|
+
]}
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
cy.get('.melting-temperature').should('contain', '0 (0)');
|
|
54
|
+
cy.get('.gc-content').should('contain', '0 (0)');
|
|
55
|
+
cy.get('.length').should('contain', '0 (0)');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { IconButton, Tooltip } from '@mui/material';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
4
|
+
import EditIcon from '@mui/icons-material/Edit';
|
|
5
|
+
import SaveIcon from '@mui/icons-material/Save';
|
|
6
|
+
import ClearIcon from '@mui/icons-material/Clear';
|
|
7
|
+
import SubmitToDatabaseDialog from '../form/SubmitToDatabaseDialog';
|
|
8
|
+
import useDatabase from '../../hooks/useDatabase';
|
|
9
|
+
import PrimerDetailsTds from './primer_details/PrimerDetailsTds';
|
|
10
|
+
import PrimerInfoIcon from './primer_details/PrimerInfoIcon';
|
|
11
|
+
|
|
12
|
+
function PrimerTableRow({ primerDetails, deletePrimer, canBeDeleted, onEditClick, pcrDetails }) {
|
|
13
|
+
const [saveToDatabaseDialogOpen, setSaveToDatabaseDialogOpen] = useState(false);
|
|
14
|
+
|
|
15
|
+
const database = useDatabase();
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
if (!primerDetails.database_id) {
|
|
19
|
+
setSaveToDatabaseDialogOpen(false);
|
|
20
|
+
}
|
|
21
|
+
}, [primerDetails.database_id]);
|
|
22
|
+
|
|
23
|
+
const isSavedToDatabase = database && Boolean(primerDetails.database_id);
|
|
24
|
+
|
|
25
|
+
let deleteMessage;
|
|
26
|
+
if (!canBeDeleted) {
|
|
27
|
+
deleteMessage = isSavedToDatabase ? 'Cannot remove primer in use from session' : 'Cannot delete primer in use';
|
|
28
|
+
} else {
|
|
29
|
+
deleteMessage = isSavedToDatabase ? 'Remove from session' : 'Delete';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<tr>
|
|
34
|
+
<td className="icons">
|
|
35
|
+
<Tooltip arrow title={deleteMessage} placement="top">
|
|
36
|
+
<IconButton onClick={() => (canBeDeleted && deletePrimer(primerDetails.id))}>
|
|
37
|
+
{isSavedToDatabase ? <ClearIcon /> : <DeleteIcon />}
|
|
38
|
+
</IconButton>
|
|
39
|
+
</Tooltip>
|
|
40
|
+
<Tooltip arrow title={isSavedToDatabase ? `Stored in ${database.name}` : 'Edit'} placement="top">
|
|
41
|
+
{isSavedToDatabase ? (
|
|
42
|
+
<IconButton
|
|
43
|
+
onClick={() => window.open(database.getPrimerLink(primerDetails.database_id), '_blank')}
|
|
44
|
+
sx={{ cursor: 'pointer' }}
|
|
45
|
+
>
|
|
46
|
+
<database.DatabaseIcon color="success" />
|
|
47
|
+
</IconButton>
|
|
48
|
+
) : (
|
|
49
|
+
<IconButton onClick={() => (onEditClick(primerDetails.id))}>
|
|
50
|
+
<EditIcon />
|
|
51
|
+
</IconButton>
|
|
52
|
+
)}
|
|
53
|
+
</Tooltip>
|
|
54
|
+
<PrimerInfoIcon primerDetails={primerDetails} pcrDetails={pcrDetails} />
|
|
55
|
+
{database && !primerDetails.database_id && (
|
|
56
|
+
<>
|
|
57
|
+
<Tooltip arrow title={`Save to ${database.name}`} placement="top">
|
|
58
|
+
<IconButton onClick={() => setSaveToDatabaseDialogOpen(true)}>
|
|
59
|
+
<database.SubmitIcon />
|
|
60
|
+
</IconButton>
|
|
61
|
+
</Tooltip>
|
|
62
|
+
{saveToDatabaseDialogOpen && (
|
|
63
|
+
<SubmitToDatabaseDialog
|
|
64
|
+
id={primerDetails.id}
|
|
65
|
+
dialogOpen={saveToDatabaseDialogOpen}
|
|
66
|
+
setDialogOpen={setSaveToDatabaseDialogOpen}
|
|
67
|
+
resourceType="primer"
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
</td>
|
|
73
|
+
<td className="name">{primerDetails.name}</td>
|
|
74
|
+
<PrimerDetailsTds
|
|
75
|
+
primerId={primerDetails.id}
|
|
76
|
+
primerDetails={primerDetails}
|
|
77
|
+
pcrDetails={pcrDetails}
|
|
78
|
+
/>
|
|
79
|
+
<td className="sequence">{primerDetails.sequence}</td>
|
|
80
|
+
</tr>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default PrimerTableRow;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FormControl, InputLabel, Select, MenuItem, Box, Chip } from '@mui/material';
|
|
3
|
+
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
|
4
|
+
|
|
5
|
+
function chipRenderer(selected, primers) {
|
|
6
|
+
return (
|
|
7
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
|
8
|
+
{selected.map((id) => (
|
|
9
|
+
<Chip key={id} label={primers.find((p) => p.id === id).name} />
|
|
10
|
+
))}
|
|
11
|
+
</Box>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function SelectPrimerForm({ primers, selected, onChange, goToPrimerTab, label, multiple = false }) {
|
|
16
|
+
const [open, setOpen] = React.useState(false);
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
// If the primer that was selected is deleted
|
|
20
|
+
const primerIds = primers.map(({ id }) => id);
|
|
21
|
+
if (!multiple) {
|
|
22
|
+
if (selected && !primerIds.includes(selected)) {
|
|
23
|
+
onChange(null);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
onChange(selected.filter((id) => primerIds.includes(id)));
|
|
27
|
+
}
|
|
28
|
+
}, [primers]);
|
|
29
|
+
|
|
30
|
+
const onChangeHandleCreatePrimer = (value) => {
|
|
31
|
+
if (multiple) {
|
|
32
|
+
onChange(value.filter((id) => id !== ""));
|
|
33
|
+
} else {
|
|
34
|
+
onChange(value);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleCreatePrimerClick = () => {
|
|
39
|
+
setOpen(false);
|
|
40
|
+
goToPrimerTab();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<FormControl fullWidth>
|
|
45
|
+
<InputLabel>{label}</InputLabel>
|
|
46
|
+
<Select
|
|
47
|
+
multiple={multiple}
|
|
48
|
+
value={selected}
|
|
49
|
+
onChange={(e) => onChangeHandleCreatePrimer(e.target.value)}
|
|
50
|
+
onClose={() => setOpen(false)}
|
|
51
|
+
onOpen={() => setOpen(true)}
|
|
52
|
+
open={open}
|
|
53
|
+
label={label}
|
|
54
|
+
renderValue={multiple ? (s) => chipRenderer(s, primers) : undefined}
|
|
55
|
+
>
|
|
56
|
+
<MenuItem onClick={handleCreatePrimerClick} value="">
|
|
57
|
+
<AddCircleIcon color="success" />
|
|
58
|
+
<em style={{ marginLeft: 8 }}>Create primer</em>
|
|
59
|
+
</MenuItem>
|
|
60
|
+
{primers.map(({ name, id }) => (<MenuItem key={id} value={id}>{name}</MenuItem>))}
|
|
61
|
+
</Select>
|
|
62
|
+
</FormControl>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default SelectPrimerForm;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { shallowEqual, useSelector } from 'react-redux';
|
|
3
|
+
import Button from '@mui/material/Button';
|
|
4
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
5
|
+
import Modal from '@mui/material/Modal';
|
|
6
|
+
import Box from '@mui/material/Box';
|
|
7
|
+
import useAlerts from '../../../hooks/useAlerts';
|
|
8
|
+
import './styles.css';
|
|
9
|
+
|
|
10
|
+
import { primersFromTextFile } from '@opencloning/utils/fileParsers';
|
|
11
|
+
import PrimersImportTable from './ImportPrimersTable';
|
|
12
|
+
|
|
13
|
+
function ImportPrimersButton({ addPrimer }) {
|
|
14
|
+
const { addAlert } = useAlerts();
|
|
15
|
+
const existingNames = useSelector((state) => state.cloning.primers.map((p) => p.name), shallowEqual);
|
|
16
|
+
|
|
17
|
+
// Ref to the hidden file input element
|
|
18
|
+
const hiddenFileInput = React.useRef(null);
|
|
19
|
+
const [openModal, setOpenModal] = useState(false);
|
|
20
|
+
const [importedPrimers, setImportedPrimers] = useState([]);
|
|
21
|
+
const importButtonDisabled = importedPrimers.every((primer) => primer.error !== '');
|
|
22
|
+
|
|
23
|
+
// Function to simulate click on hidden file input
|
|
24
|
+
const handleUploadClick = () => {
|
|
25
|
+
hiddenFileInput.current.click();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Function to handle file selection
|
|
29
|
+
const handleFileUpload = async (event) => {
|
|
30
|
+
const fileUploaded = event.target.files[0];
|
|
31
|
+
try {
|
|
32
|
+
const uploadedPrimers = await primersFromTextFile(fileUploaded, existingNames);
|
|
33
|
+
setImportedPrimers(uploadedPrimers);
|
|
34
|
+
setOpenModal(true);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
addAlert({
|
|
37
|
+
message: error.message,
|
|
38
|
+
severity: 'error',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
event.target.value = null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleImportClick = () => {
|
|
45
|
+
importedPrimers.forEach((primer) => {
|
|
46
|
+
if (primer.error === '') {
|
|
47
|
+
addPrimer({ name: primer.name, sequence: primer.sequence });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
setOpenModal(false);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<Tooltip arrow title={<span style={{ fontSize: '1.4em' }}>Upload a .csv or .tsv file with headers 'name' and 'sequence'</span>}>
|
|
56
|
+
<Button
|
|
57
|
+
onClick={handleUploadClick}
|
|
58
|
+
variant="contained"
|
|
59
|
+
>
|
|
60
|
+
Import from file
|
|
61
|
+
</Button>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
|
|
64
|
+
<input
|
|
65
|
+
style={{ display: 'none' }}
|
|
66
|
+
type="file"
|
|
67
|
+
accept=".csv,.tsv"
|
|
68
|
+
ref={hiddenFileInput}
|
|
69
|
+
onChange={handleFileUpload}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<Modal open={openModal} onClose={() => setOpenModal(false)}>
|
|
73
|
+
<Box className="import-primers-modal-content">
|
|
74
|
+
<h2 style={{ textAlign: 'center' }}>Import Primers Preview</h2>
|
|
75
|
+
|
|
76
|
+
<PrimersImportTable importedPrimers={importedPrimers} />
|
|
77
|
+
|
|
78
|
+
<Box className="import-primers-modal-buttons">
|
|
79
|
+
<Button
|
|
80
|
+
variant="outlined"
|
|
81
|
+
color="error"
|
|
82
|
+
onClick={() => setOpenModal(false)}
|
|
83
|
+
>
|
|
84
|
+
Cancel
|
|
85
|
+
</Button>
|
|
86
|
+
<Button
|
|
87
|
+
variant="contained"
|
|
88
|
+
color="primary"
|
|
89
|
+
disabled={importButtonDisabled}
|
|
90
|
+
onClick={handleImportClick}
|
|
91
|
+
>
|
|
92
|
+
Import Valid Primers
|
|
93
|
+
</Button>
|
|
94
|
+
</Box>
|
|
95
|
+
</Box>
|
|
96
|
+
</Modal>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default ImportPrimersButton;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Table, TableCell, TableRow, TableHead, TableBody } from '@mui/material';
|
|
3
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
4
|
+
import CancelIcon from '@mui/icons-material/Cancel';
|
|
5
|
+
import WarningIcon from '@mui/icons-material/Warning';
|
|
6
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
7
|
+
|
|
8
|
+
function CustomTableRow({ primer }) {
|
|
9
|
+
const { name, sequence, error } = primer;
|
|
10
|
+
|
|
11
|
+
let msg = 'Valid primer';
|
|
12
|
+
let icon = <CheckCircleIcon color="success" />;
|
|
13
|
+
if (error === 'invalid') {
|
|
14
|
+
msg = 'Invalid DNA sequence';
|
|
15
|
+
icon = <CancelIcon color="error" />;
|
|
16
|
+
} else if (error === 'existing') {
|
|
17
|
+
msg = 'Primer already exists';
|
|
18
|
+
icon = <WarningIcon color="warning" />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<TableRow>
|
|
23
|
+
<TableCell align="center">
|
|
24
|
+
<Tooltip title={msg} placement="top">{icon}</Tooltip>
|
|
25
|
+
</TableCell>
|
|
26
|
+
<TableCell>{name}</TableCell>
|
|
27
|
+
<TableCell className="sequence-cell">{sequence}</TableCell>
|
|
28
|
+
</TableRow>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function PrimersImportTable({ importedPrimers }) {
|
|
33
|
+
return (
|
|
34
|
+
<Table className="primers-table">
|
|
35
|
+
<TableHead>
|
|
36
|
+
<TableRow>
|
|
37
|
+
<TableCell align="center">Status</TableCell>
|
|
38
|
+
<TableCell>Name</TableCell>
|
|
39
|
+
<TableCell>Sequence</TableCell>
|
|
40
|
+
</TableRow>
|
|
41
|
+
</TableHead>
|
|
42
|
+
<TableBody>
|
|
43
|
+
{importedPrimers.map((primer, index) => (
|
|
44
|
+
<CustomTableRow key={index} primer={primer} />
|
|
45
|
+
))}
|
|
46
|
+
</TableBody>
|
|
47
|
+
</Table>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default PrimersImportTable;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { shallowEqual, useSelector } from 'react-redux';
|
|
3
|
+
import Alert from '@mui/material/Alert';
|
|
4
|
+
import { Button, TextField } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
import useDatabase from '../../../hooks/useDatabase';
|
|
7
|
+
import { stringIsNotDNA } from '@opencloning/store/cloning_utils';
|
|
8
|
+
|
|
9
|
+
function PrimerDatabaseImportForm({ submitPrimer, cancelForm }) {
|
|
10
|
+
const existingNames = useSelector((state) => state.cloning.primers.map((p) => p.name), shallowEqual);
|
|
11
|
+
const [primer, setPrimer] = useState(null);
|
|
12
|
+
const [error, setError] = useState('');
|
|
13
|
+
const database = useDatabase();
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (!primer) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!primer.name) {
|
|
20
|
+
setError('Primer does not have a name');
|
|
21
|
+
} else if (!primer.sequence) {
|
|
22
|
+
setError('Primer does not have a sequence');
|
|
23
|
+
} else if (stringIsNotDNA(primer.sequence)) {
|
|
24
|
+
setError('Sequence contains invalid characters');
|
|
25
|
+
} else if (existingNames.includes(primer.name)) {
|
|
26
|
+
setError(`A primer with name "${primer.name}" already exists`);
|
|
27
|
+
} else {
|
|
28
|
+
setError('');
|
|
29
|
+
}
|
|
30
|
+
}, [primer]);
|
|
31
|
+
|
|
32
|
+
const onSubmit = (e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
if (primer) {
|
|
35
|
+
submitPrimer(primer);
|
|
36
|
+
setPrimer(null);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<form className="primer-row" onSubmit={onSubmit}>
|
|
42
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1em', width: '80%', margin: 'auto' }}>
|
|
43
|
+
<div style={{ display: 'flex', gap: '2em', alignItems: 'center', justifyContent: 'center' }}>
|
|
44
|
+
<database.GetPrimerComponent
|
|
45
|
+
primer={primer}
|
|
46
|
+
setPrimer={setPrimer}
|
|
47
|
+
setError={setError}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
{primer && (
|
|
51
|
+
<div style={{ display: 'flex', gap: '1em', alignItems: 'center' }}>
|
|
52
|
+
<TextField
|
|
53
|
+
label="Name"
|
|
54
|
+
value={primer.name}
|
|
55
|
+
className="name"
|
|
56
|
+
disabled
|
|
57
|
+
sx={{ width: '30%' }}
|
|
58
|
+
/>
|
|
59
|
+
<TextField
|
|
60
|
+
label="Sequence"
|
|
61
|
+
value={primer.sequence}
|
|
62
|
+
className="sequence"
|
|
63
|
+
disabled
|
|
64
|
+
fullWidth
|
|
65
|
+
InputProps={{
|
|
66
|
+
sx: {
|
|
67
|
+
fontFamily: 'monospace',
|
|
68
|
+
},
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
{error && (
|
|
74
|
+
<Alert severity="error" sx={{ mt: 1 }}>
|
|
75
|
+
{error}
|
|
76
|
+
</Alert>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<div style={{ display: 'flex', gap: '1em', justifyContent: 'center' }}>
|
|
80
|
+
<Button
|
|
81
|
+
variant="outlined"
|
|
82
|
+
color="error"
|
|
83
|
+
onClick={cancelForm}
|
|
84
|
+
>
|
|
85
|
+
Cancel
|
|
86
|
+
</Button>
|
|
87
|
+
<Button
|
|
88
|
+
variant="contained"
|
|
89
|
+
color="primary"
|
|
90
|
+
type="submit"
|
|
91
|
+
disabled={!primer || error}
|
|
92
|
+
>
|
|
93
|
+
Import Primer
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{primer && (
|
|
100
|
+
<div style={{ display: 'flex', alignItems: 'center', marginTop: '1em' }} />
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
</form>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default PrimerDatabaseImportForm;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.import-primers-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 10px;
|
|
5
|
+
margin-top: 10px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.import-primers-modal-content {
|
|
9
|
+
position: absolute;
|
|
10
|
+
top: 50%;
|
|
11
|
+
left: 50%;
|
|
12
|
+
transform: translate(-50%, -50%);
|
|
13
|
+
width: 80%;
|
|
14
|
+
max-width: 800px;
|
|
15
|
+
background-color: #fff;
|
|
16
|
+
box-shadow: 24;
|
|
17
|
+
padding: 32px;
|
|
18
|
+
border-radius: 16px;
|
|
19
|
+
max-height: 80vh;
|
|
20
|
+
overflow: auto;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.import-primers-modal-buttons {
|
|
24
|
+
display: flex;
|
|
25
|
+
justify-content: flex-end;
|
|
26
|
+
gap: 1em;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.primers-table {
|
|
30
|
+
width: 100%;
|
|
31
|
+
margin-bottom: 24px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.primers-table th {
|
|
35
|
+
font-weight: bold;
|
|
36
|
+
font-size: 1.2em;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* This makes the two first columns narrow and the sequence column wider */
|
|
40
|
+
.primers-table td:first-child {
|
|
41
|
+
width: 1%;
|
|
42
|
+
white-space: nowrap;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.primers-table td:nth-child(2) {
|
|
46
|
+
width: 1%;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.sequence-cell {
|
|
51
|
+
font-family: monospace;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.primer-add-container {
|
|
55
|
+
display: flex;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: 2em;
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, FormLabel, IconButton, Collapse } from '@mui/material';
|
|
3
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
4
|
+
|
|
5
|
+
function CollapsableLabel({ label, className, children, open = false }) {
|
|
6
|
+
const [show, setShow] = React.useState(open);
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
setShow(open);
|
|
10
|
+
}, [open]);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Box className={className}>
|
|
14
|
+
<Box sx={{ mt: 1 }}>
|
|
15
|
+
<FormLabel>{label}</FormLabel>
|
|
16
|
+
<IconButton
|
|
17
|
+
onClick={() => setShow(!show)}
|
|
18
|
+
aria-expanded={show}
|
|
19
|
+
aria-label="show more"
|
|
20
|
+
>
|
|
21
|
+
<ExpandMoreIcon />
|
|
22
|
+
</IconButton>
|
|
23
|
+
</Box>
|
|
24
|
+
<Collapse in={show}>
|
|
25
|
+
{children}
|
|
26
|
+
</Collapse>
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default CollapsableLabel;
|