@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,409 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import IconButton from '@mui/material/IconButton';
|
|
3
|
+
import InputAdornment from '@mui/material/InputAdornment';
|
|
4
|
+
import ClearIcon from '@mui/icons-material/Clear';
|
|
5
|
+
import InputLabel from '@mui/material/InputLabel';
|
|
6
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
7
|
+
import TextField from '@mui/material/TextField';
|
|
8
|
+
import FormControl from '@mui/material/FormControl';
|
|
9
|
+
import Select from '@mui/material/Select';
|
|
10
|
+
import { Alert, Autocomplete, Table, TableBody, TableCell, TableRow } from '@mui/material';
|
|
11
|
+
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
12
|
+
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
13
|
+
import getHttpClient from '@opencloning/utils/getHttpClient';
|
|
14
|
+
import repositoryMetadata from './repositoryMetadata';
|
|
15
|
+
|
|
16
|
+
const httpClient = getHttpClient();
|
|
17
|
+
|
|
18
|
+
function validateRepositoryId(repositoryId, repository) {
|
|
19
|
+
switch (repository) {
|
|
20
|
+
case 'AddgeneIdSource':
|
|
21
|
+
if (!repositoryId.match(/^\d+/)) {
|
|
22
|
+
return 'Addgene IDs must be numbers (e.g. 39296)';
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
case 'BenchlingUrlSource':
|
|
26
|
+
if (!repositoryId.match(/^https:\/\/benchling\.com\/.+\/edit$/)) {
|
|
27
|
+
return 'Use a Benchling URL like https://benchling.com/siverson/f/lib_B94YxDHhQh-cidar-moclo-library/seq_dh1FrJTc-b0015_dh/edit';
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case 'EuroscarfSource':
|
|
31
|
+
if (!repositoryId.match(/^P\d+$/)) {
|
|
32
|
+
return 'Euroscarf IDs must be P followed by numbers (e.g. P30174)';
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 'WekWikGeneIdSource':
|
|
36
|
+
if (!repositoryId.match(/^\d+$/)) {
|
|
37
|
+
return 'WeKwikGene IDs must be numbers (e.g. 0000304)';
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
const snapgeneCheckOption = (option, inputValue) => option.name.toLowerCase().includes(inputValue.toLowerCase());
|
|
49
|
+
const snapgeneFormatOption = (option, plasmidSet, plasmidSetName) => ({ name: option.name, path: `${plasmidSet}/${option.subpath}`, plasmidSetName, plasmidSet });
|
|
50
|
+
const snapgeneGetOptions = (data, inputValue) => Object.entries(data)
|
|
51
|
+
.flatMap(([plasmidSet, category]) => category.plasmids
|
|
52
|
+
.filter((option) => snapgeneCheckOption(option, inputValue))
|
|
53
|
+
.map((option) => snapgeneFormatOption(option, plasmidSet, data[plasmidSet].name)));
|
|
54
|
+
function SnapgeneSuccessComponent({ option }) {
|
|
55
|
+
return (
|
|
56
|
+
<Alert severity="info" sx={{ mb: 1 }}>
|
|
57
|
+
Plasmid
|
|
58
|
+
{' '}
|
|
59
|
+
<a href={`https://www.snapgene.com/plasmids/${option.path}`} target="_blank" rel="noopener noreferrer">{option.name}</a>
|
|
60
|
+
{' '}
|
|
61
|
+
from set
|
|
62
|
+
{' '}
|
|
63
|
+
<a href={`https://www.snapgene.com/plasmids/${option.plasmidSet}`} target="_blank" rel="noopener noreferrer">{option.plasmidSetName}</a>
|
|
64
|
+
</Alert>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const iGEMGetOptions = (plasmids, inputValue) => plasmids.map((p) => ({
|
|
69
|
+
name: `${p['Short Desc / Name']} / ${p['Part Name']} / ${p['Plasmid Backbone']}`,
|
|
70
|
+
url: `https://assets.opencloning.org/annotated-igem-distribution/results/plasmids/${p['Index ID']}.gb`,
|
|
71
|
+
table_name: p['Short Desc / Name'],
|
|
72
|
+
part_name: p['Part Name'],
|
|
73
|
+
part_url: p['Part URL'],
|
|
74
|
+
backbone: p['Plasmid Backbone'],
|
|
75
|
+
})).filter((p) => p.name.toLowerCase().includes(inputValue.toLowerCase()));
|
|
76
|
+
|
|
77
|
+
function iGEMSuccessComponent({ option }) {
|
|
78
|
+
return (
|
|
79
|
+
<Alert severity="info" sx={{ mb: 1 }}>
|
|
80
|
+
{'Plasmid '}
|
|
81
|
+
<a href={option.url} target="_blank" rel="noopener noreferrer">{option.table_name}</a>
|
|
82
|
+
{' containing part '}
|
|
83
|
+
<a href={option.part_url} target="_blank" rel="noopener noreferrer">{option.part_name}</a>
|
|
84
|
+
{` in backbone ${option.backbone} from `}
|
|
85
|
+
<a href="https://airtable.com/appgWgf6EPX5gpnNU/shrb0c8oYTgpZDRgH/tblNqHsHbNNQP2HCX" target="_blank" rel="noopener noreferrer">2024 iGEM Distribution</a>
|
|
86
|
+
</Alert>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sevaGetOptions = (data, inputValue) => data.map((p) => {
|
|
91
|
+
const info = [p.Resistance, p.ORI, p.Cargo, p.Gadget, p.FunctionType].filter((i) => i !== '').join('/');
|
|
92
|
+
return {
|
|
93
|
+
name: `${p.Name} (${info})`,
|
|
94
|
+
plasmid_name: p.Name,
|
|
95
|
+
data: p,
|
|
96
|
+
};
|
|
97
|
+
}).filter((p) => p.name.toLowerCase().includes(inputValue.toLowerCase()));
|
|
98
|
+
|
|
99
|
+
function SEVASuccessComponent({ option }) {
|
|
100
|
+
return (
|
|
101
|
+
<Alert severity="info" sx={{ mb: 1 }} icon={false}>
|
|
102
|
+
<Table size="small">
|
|
103
|
+
<TableBody>
|
|
104
|
+
{Object.entries(option.data)
|
|
105
|
+
.filter(([key]) => ['Name', 'Resistance', 'ORI', 'Cargo', 'Gadget', 'FunctionType'].includes(key))
|
|
106
|
+
.map(([key, value]) => (
|
|
107
|
+
<TableRow key={key}>
|
|
108
|
+
<TableCell>{key}</TableCell>
|
|
109
|
+
<TableCell>{value || 'N/A'}</TableCell>
|
|
110
|
+
</TableRow>
|
|
111
|
+
))}
|
|
112
|
+
</TableBody>
|
|
113
|
+
</Table>
|
|
114
|
+
</Alert>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const openDNACollectionsGetOptions = (data, inputValue, groupedBy) => data.map((p) => ({
|
|
119
|
+
name: `${p.id}` + (p.plasmid_name ? ` - ${p.plasmid_name}` : ''),
|
|
120
|
+
plasmid_name: p.plasmid_name,
|
|
121
|
+
plasmid_id: p.id,
|
|
122
|
+
collection: p.collection,
|
|
123
|
+
url: `https://assets.opencloning.org/open-dna-collections/${p.path}`,
|
|
124
|
+
})).filter((p) => p.name.toLowerCase().includes(inputValue.toLowerCase()) && (groupedBy ? p.collection === groupedBy : true));
|
|
125
|
+
|
|
126
|
+
function OpenDNACollectionsSuccessComponent({ option }) {
|
|
127
|
+
return (
|
|
128
|
+
<Alert severity="info" sx={{ mb: 1 }}>
|
|
129
|
+
Plasmid <a href={option.url} target="_blank" rel="noopener noreferrer">{option.name}</a> {' '}
|
|
130
|
+
from collection <a href={`https://github.com/Reclone-org/open-dna-collections/tree/main/${option.collection}`} target="_blank" rel="noopener noreferrer">{option.collection}</a>
|
|
131
|
+
</Alert>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function IndexJsonSelector({
|
|
136
|
+
url,
|
|
137
|
+
setInputValue,
|
|
138
|
+
getOptions,
|
|
139
|
+
noOptionsText,
|
|
140
|
+
inputLabel,
|
|
141
|
+
SuccessComponent,
|
|
142
|
+
responseProcessCallback = (resp) => resp.data,
|
|
143
|
+
requiredInput = 3,
|
|
144
|
+
groupField = null, // You can pass the name of the field to group by
|
|
145
|
+
}) {
|
|
146
|
+
const [userInput, setUserInput] = React.useState('');
|
|
147
|
+
const [data, setData] = React.useState(null);
|
|
148
|
+
const [options, setOptions] = React.useState([]);
|
|
149
|
+
const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
|
|
150
|
+
const [retry, setRetry] = React.useState(0);
|
|
151
|
+
const [groups, setGroups] = React.useState([]);
|
|
152
|
+
const [groupedBy, setGroupedBy] = React.useState(null);
|
|
153
|
+
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
const fetchOptions = async () => {
|
|
156
|
+
setRequestStatus({ status: 'loading' });
|
|
157
|
+
try {
|
|
158
|
+
const resp = await httpClient.get(url);
|
|
159
|
+
setData(responseProcessCallback(resp));
|
|
160
|
+
if (requiredInput === 0) {
|
|
161
|
+
setOptions(getOptions(resp.data, ''));
|
|
162
|
+
}
|
|
163
|
+
if (groupField) {
|
|
164
|
+
setGroups([...new Set(resp.data.map((p) => p[groupField]))].sort());
|
|
165
|
+
}
|
|
166
|
+
setRequestStatus({ status: 'success' });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
setRequestStatus({ status: 'error', message: error.message });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
fetchOptions();
|
|
172
|
+
}, [retry]);
|
|
173
|
+
|
|
174
|
+
const onInputChange = (newInputValue) => {
|
|
175
|
+
if (newInputValue === undefined) {
|
|
176
|
+
// When clearing the input via x button
|
|
177
|
+
setUserInput('');
|
|
178
|
+
if (requiredInput === 0) {
|
|
179
|
+
setOptions(getOptions(data, '', groupedBy));
|
|
180
|
+
} else {
|
|
181
|
+
setOptions([]);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
setUserInput(newInputValue);
|
|
186
|
+
if (newInputValue.length < requiredInput) {
|
|
187
|
+
setOptions([]);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setOptions(getOptions(data, newInputValue, groupedBy));
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const onGroupChange = (newGroup) => {
|
|
195
|
+
setUserInput('');
|
|
196
|
+
setInputValue('')
|
|
197
|
+
setGroupedBy(newGroup);
|
|
198
|
+
setOptions(getOptions(data, '', newGroup))
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const selectedOption = options.find((option) => option.name === userInput);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<RequestStatusWrapper requestStatus={requestStatus} retry={() => setRetry(retry + 1)}>
|
|
205
|
+
{groupField && (
|
|
206
|
+
<FormControl fullWidth>
|
|
207
|
+
<InputLabel>{groupField.charAt(0).toUpperCase() + groupField.slice(1).replace(/([A-Z])/g, ' $1')}</InputLabel>
|
|
208
|
+
<Select
|
|
209
|
+
endAdornment={groupedBy && (<InputAdornment position="end"><IconButton onClick={() => onGroupChange(null)}><ClearIcon /></IconButton></InputAdornment>)}
|
|
210
|
+
value={groupedBy}
|
|
211
|
+
onChange={(e) => onGroupChange(e.target.value)}
|
|
212
|
+
// Capitalize the first letter and add a space before each capital letter
|
|
213
|
+
label={groupField.charAt(0).toUpperCase() + groupField.slice(1).replace(/([A-Z])/g, ' $1')}
|
|
214
|
+
>
|
|
215
|
+
{groups.map((group) => (
|
|
216
|
+
<MenuItem value={group}>{group}</MenuItem>
|
|
217
|
+
))}
|
|
218
|
+
</Select>
|
|
219
|
+
</FormControl>
|
|
220
|
+
)}
|
|
221
|
+
<FormControl fullWidth>
|
|
222
|
+
<Autocomplete
|
|
223
|
+
onChange={(event, value) => {
|
|
224
|
+
onInputChange(value?.name);
|
|
225
|
+
if (value) {
|
|
226
|
+
setInputValue(value);
|
|
227
|
+
} else {
|
|
228
|
+
setInputValue('');
|
|
229
|
+
}
|
|
230
|
+
}}
|
|
231
|
+
// Change options only when input changes (not when an option is picked)
|
|
232
|
+
onInputChange={(event, newInputValue, reason) => (reason === 'input') && onInputChange(newInputValue)}
|
|
233
|
+
id="tags-standard"
|
|
234
|
+
options={options}
|
|
235
|
+
noOptionsText={userInput.length < requiredInput ? noOptionsText : 'Nothing found'}
|
|
236
|
+
getOptionLabel={(o) => o.name}
|
|
237
|
+
isOptionEqualToValue={(o1, o2) => o1.subpath === o2.subpath}
|
|
238
|
+
inputValue={userInput}
|
|
239
|
+
renderInput={(params) => (
|
|
240
|
+
<TextField
|
|
241
|
+
{...params}
|
|
242
|
+
label={inputLabel}
|
|
243
|
+
/>
|
|
244
|
+
)}
|
|
245
|
+
/>
|
|
246
|
+
</FormControl>
|
|
247
|
+
{selectedOption && <SuccessComponent option={selectedOption} />}
|
|
248
|
+
</RequestStatusWrapper>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// A component providing an interface for the user to type a repository ID
|
|
253
|
+
// and get a sequence
|
|
254
|
+
function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {
|
|
255
|
+
const { id: sourceId } = source;
|
|
256
|
+
const [inputValue, setInputValue] = React.useState('');
|
|
257
|
+
const [repositoryType, setRepositoryType] = React.useState(source.type || '');
|
|
258
|
+
const [error, setError] = React.useState('');
|
|
259
|
+
|
|
260
|
+
const repositoryMeta = repositoryMetadata[repositoryType] || {};
|
|
261
|
+
|
|
262
|
+
React.useEffect(() => {
|
|
263
|
+
setRepositoryType(source.type || '');
|
|
264
|
+
}, [source.type]);
|
|
265
|
+
|
|
266
|
+
React.useEffect(() => {
|
|
267
|
+
setInputValue('');
|
|
268
|
+
setError('');
|
|
269
|
+
}, [repositoryType]);
|
|
270
|
+
|
|
271
|
+
React.useEffect(() => {
|
|
272
|
+
if (inputValue) {
|
|
273
|
+
setError(validateRepositoryId(inputValue, repositoryType));
|
|
274
|
+
} else {
|
|
275
|
+
setError('');
|
|
276
|
+
}
|
|
277
|
+
}, [inputValue, repositoryType]);
|
|
278
|
+
|
|
279
|
+
const onSubmit = (event) => {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
const extra = { repository_id: inputValue };
|
|
282
|
+
if (repositoryType === 'BenchlingUrlSource') {
|
|
283
|
+
// Remove /edit from the end of the URL and add .gb
|
|
284
|
+
extra.repository_id = inputValue.replace(/\/edit$/, '.gb');
|
|
285
|
+
}
|
|
286
|
+
if (repositoryType === 'SnapGenePlasmidSource') {
|
|
287
|
+
extra.repository_id = inputValue.path;
|
|
288
|
+
}
|
|
289
|
+
if (repositoryType === 'IGEMSource') {
|
|
290
|
+
extra.repository_id = `${inputValue.part_name}-${inputValue.backbone}`;
|
|
291
|
+
extra.sequence_file_url = inputValue.url;
|
|
292
|
+
}
|
|
293
|
+
if (repositoryType === 'SEVASource') {
|
|
294
|
+
extra.repository_id = inputValue.plasmid_name;
|
|
295
|
+
}
|
|
296
|
+
if (repositoryType === 'OpenDNACollectionsSource') {
|
|
297
|
+
extra.repository_id = inputValue.collection + '/' + inputValue.plasmid_id;
|
|
298
|
+
extra.sequence_file_url = encodeURI(inputValue.url);
|
|
299
|
+
}
|
|
300
|
+
const requestData = { id: sourceId, ...extra, type: repositoryType };
|
|
301
|
+
sendPostRequest({ endpoint: `repository_id/${repositoryMeta.slug}`, requestData, source });
|
|
302
|
+
};
|
|
303
|
+
const helperText = error || (repositoryMeta?.example && `Example: ${repositoryMeta.example}`);
|
|
304
|
+
return (
|
|
305
|
+
<>
|
|
306
|
+
<FormControl fullWidth>
|
|
307
|
+
<InputLabel id={`select-repository-${sourceId}-label`}>Select repository</InputLabel>
|
|
308
|
+
<Select
|
|
309
|
+
value={repositoryType}
|
|
310
|
+
onChange={(event) => setRepositoryType(event.target.value)}
|
|
311
|
+
labelId={`select-repository-${sourceId}-label`}
|
|
312
|
+
label="Select repository"
|
|
313
|
+
>
|
|
314
|
+
<MenuItem value="AddgeneIdSource">Addgene</MenuItem>
|
|
315
|
+
<MenuItem value="BenchlingUrlSource">Benchling</MenuItem>
|
|
316
|
+
<MenuItem value="EuroscarfSource">Euroscarf</MenuItem>
|
|
317
|
+
<MenuItem value="NCBISequenceSource">GenBank</MenuItem>
|
|
318
|
+
<MenuItem value="IGEMSource">iGEM</MenuItem>
|
|
319
|
+
<MenuItem value="OpenDNACollectionsSource">Open DNA Collections</MenuItem>
|
|
320
|
+
<MenuItem value="SEVASource">SEVA Plasmids</MenuItem>
|
|
321
|
+
<MenuItem value="SnapGenePlasmidSource">SnapGene</MenuItem>
|
|
322
|
+
<MenuItem value="WekWikGeneIdSource">WeKwikGene</MenuItem>
|
|
323
|
+
</Select>
|
|
324
|
+
</FormControl>
|
|
325
|
+
{repositoryType && repositoryType !== 'RepositoryIdSource' && (
|
|
326
|
+
<form onSubmit={onSubmit}>
|
|
327
|
+
{!['SnapGenePlasmidSource', 'IGEMSource', 'SEVASource', 'OpenDNACollectionsSource'].includes(repositoryType) && (
|
|
328
|
+
<>
|
|
329
|
+
<FormControl fullWidth>
|
|
330
|
+
<TextField
|
|
331
|
+
label={repositoryMeta.inputLabel}
|
|
332
|
+
id={`repository-id-${sourceId}`}
|
|
333
|
+
value={inputValue}
|
|
334
|
+
onChange={(event) => setInputValue(event.target.value)}
|
|
335
|
+
helperText={helperText}
|
|
336
|
+
error={error !== ''}
|
|
337
|
+
/>
|
|
338
|
+
</FormControl>
|
|
339
|
+
{/* Extra info for benchling case */}
|
|
340
|
+
{repositoryType === 'BenchlingUrlSource' && (
|
|
341
|
+
<Alert severity="info" sx={{ mb: 1 }}>
|
|
342
|
+
The sequence must be publicly accessible. Use the URL from a sequence editor page (ending in "/edit"), like
|
|
343
|
+
{' '}
|
|
344
|
+
<a target="_blank" rel="noopener noreferrer" href="https://benchling.com/siverson/f/lib_B94YxDHhQh-cidar-moclo-library/seq_dh1FrJTc-b0015_dh/edit">this example</a>
|
|
345
|
+
.
|
|
346
|
+
</Alert>
|
|
347
|
+
)}
|
|
348
|
+
</>
|
|
349
|
+
)}
|
|
350
|
+
{repositoryType === 'SnapGenePlasmidSource'
|
|
351
|
+
&& (
|
|
352
|
+
<IndexJsonSelector
|
|
353
|
+
url="https://assets.opencloning.org/SnapGene_crawler/index.json"
|
|
354
|
+
setInputValue={setInputValue}
|
|
355
|
+
getOptions={snapgeneGetOptions}
|
|
356
|
+
noOptionsText="Type at least 3 characters to search, see SnapGene plasmids for options"
|
|
357
|
+
inputLabel="Plasmid name"
|
|
358
|
+
SuccessComponent={SnapgeneSuccessComponent}
|
|
359
|
+
requiredInput={3}
|
|
360
|
+
/>
|
|
361
|
+
)}
|
|
362
|
+
{repositoryType === 'IGEMSource' && (
|
|
363
|
+
<IndexJsonSelector
|
|
364
|
+
url="https://assets.opencloning.org/annotated-igem-distribution/results/index.json"
|
|
365
|
+
setInputValue={setInputValue}
|
|
366
|
+
getOptions={iGEMGetOptions}
|
|
367
|
+
noOptionsText=""
|
|
368
|
+
inputLabel="Plasmid name"
|
|
369
|
+
SuccessComponent={iGEMSuccessComponent}
|
|
370
|
+
requiredInput={0}
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
373
|
+
{repositoryType === 'SEVASource' && (
|
|
374
|
+
<IndexJsonSelector
|
|
375
|
+
url="https://assets.opencloning.org/seva_plasmids_index/index.json"
|
|
376
|
+
setInputValue={setInputValue}
|
|
377
|
+
getOptions={sevaGetOptions}
|
|
378
|
+
noOptionsText="Type at least 3 characters to search"
|
|
379
|
+
inputLabel="Plasmid name"
|
|
380
|
+
SuccessComponent={SEVASuccessComponent}
|
|
381
|
+
requiredInput={3}
|
|
382
|
+
/>
|
|
383
|
+
)}
|
|
384
|
+
{repositoryType === 'OpenDNACollectionsSource' && (
|
|
385
|
+
<IndexJsonSelector
|
|
386
|
+
url="https://assets.opencloning.org/open-dna-collections/scripts/index.json"
|
|
387
|
+
setInputValue={setInputValue}
|
|
388
|
+
getOptions={openDNACollectionsGetOptions}
|
|
389
|
+
noOptionsText=""
|
|
390
|
+
inputLabel="Plasmid name"
|
|
391
|
+
SuccessComponent={OpenDNACollectionsSuccessComponent}
|
|
392
|
+
requiredInput={0}
|
|
393
|
+
groupField="collection"
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
396
|
+
{inputValue && !error && (
|
|
397
|
+
<SubmitButtonBackendAPI
|
|
398
|
+
requestStatus={requestStatus}
|
|
399
|
+
{...(import.meta.env.VITE_UMAMI_WEBSITE_ID && { "data-umami-event": "submit-repository-id", "data-umami-event-repository": `${repositoryMeta.slug}` })}
|
|
400
|
+
>Submit</SubmitButtonBackendAPI>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
</form>
|
|
404
|
+
)}
|
|
405
|
+
</>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export default SourceRepositoryId;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallowEqual, useSelector } from 'react-redux';
|
|
3
|
+
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
4
|
+
import EnzymeMultiSelect from '../form/EnzymeMultiSelect';
|
|
5
|
+
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
6
|
+
|
|
7
|
+
// A component providing an interface for the user to perform a restriction reaction
|
|
8
|
+
// with one or more restriction enzymes, move between output fragments, and eventually
|
|
9
|
+
// select one as an output.
|
|
10
|
+
function SourceRestriction({ source, requestStatus, sendPostRequest }) {
|
|
11
|
+
const { id: sourceId } = source;
|
|
12
|
+
const [enzymes, setEnzymes] = React.useState([]);
|
|
13
|
+
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
14
|
+
|
|
15
|
+
const onSubmit = (e) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
if (enzymes.length === 0) { return; }
|
|
18
|
+
const requestData = {
|
|
19
|
+
source: { id: sourceId, input: source.input },
|
|
20
|
+
sequences: inputSequences,
|
|
21
|
+
};
|
|
22
|
+
sendPostRequest({ endpoint: 'restriction', requestData, source, config: { params: { restriction_enzymes: enzymes } } });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="restriction">
|
|
27
|
+
<form onSubmit={onSubmit}>
|
|
28
|
+
<EnzymeMultiSelect setEnzymes={setEnzymes} />
|
|
29
|
+
{(enzymes.length > 0) && (
|
|
30
|
+
<SubmitButtonBackendAPI
|
|
31
|
+
requestStatus={requestStatus}
|
|
32
|
+
color="success"
|
|
33
|
+
{...(import.meta.env.VITE_UMAMI_WEBSITE_ID && { "data-umami-event": "submit-restriction" })}
|
|
34
|
+
>Perform restriction</SubmitButtonBackendAPI>
|
|
35
|
+
)}
|
|
36
|
+
</form>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default SourceRestriction;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallowEqual, useSelector } from 'react-redux';
|
|
3
|
+
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
4
|
+
import SubmitButtonBackendAPI from '../form/SubmitButtonBackendAPI';
|
|
5
|
+
|
|
6
|
+
function SourceReverseComplement({ source, requestStatus, sendPostRequest }) {
|
|
7
|
+
const { id: sourceId, input } = source;
|
|
8
|
+
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
9
|
+
const onSubmit = (event) => {
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
|
|
12
|
+
const requestData = {
|
|
13
|
+
sequences: inputSequences,
|
|
14
|
+
source: { id: sourceId, input },
|
|
15
|
+
};
|
|
16
|
+
sendPostRequest({ endpoint: 'reverse_complement', requestData, source });
|
|
17
|
+
};
|
|
18
|
+
// No need for MultipleOutputsSelector, since there is only one output
|
|
19
|
+
return (
|
|
20
|
+
<div className="ReverseComplementSource">
|
|
21
|
+
<form onSubmit={onSubmit}>
|
|
22
|
+
<SubmitButtonBackendAPI
|
|
23
|
+
requestStatus={requestStatus}
|
|
24
|
+
{...(import.meta.env.VITE_UMAMI_WEBSITE_ID && { "data-umami-event": "submit-reverse-complement" })}
|
|
25
|
+
>
|
|
26
|
+
Reverse complement
|
|
27
|
+
</SubmitButtonBackendAPI>
|
|
28
|
+
</form>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default SourceReverseComplement;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
|
3
|
+
import InputLabel from '@mui/material/InputLabel';
|
|
4
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
5
|
+
import FormControl from '@mui/material/FormControl';
|
|
6
|
+
import Select from '@mui/material/Select';
|
|
7
|
+
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
8
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
9
|
+
import useDatabase from '../../hooks/useDatabase';
|
|
10
|
+
|
|
11
|
+
const { replaceSource } = cloningActions;
|
|
12
|
+
|
|
13
|
+
function SourceTypeSelector({ source }) {
|
|
14
|
+
const { id: sourceId, type: sourceType } = source;
|
|
15
|
+
const dispatch = useDispatch();
|
|
16
|
+
const database = useDatabase();
|
|
17
|
+
const sourceIsPrimerDesign = useSelector((state) => Boolean(state.cloning.sequences.find((e) => e.id === source.id)?.primer_design));
|
|
18
|
+
const noExternalRequests = useSelector((state) => state.cloning.config.noExternalRequests);
|
|
19
|
+
const enablePlannotate = useSelector((state) => state.cloning.config.enablePlannotate);
|
|
20
|
+
|
|
21
|
+
const onChange = (event) => {
|
|
22
|
+
// Clear the source other than these fields
|
|
23
|
+
dispatch(replaceSource({
|
|
24
|
+
id: sourceId,
|
|
25
|
+
type: event.target.value,
|
|
26
|
+
input: source.input
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
29
|
+
const inputSequences = useSelector((state) => getInputSequencesFromSourceId(state, sourceId), shallowEqual);
|
|
30
|
+
const sequencesExist = useSelector((state) => state.cloning.sequences.length > 0, shallowEqual);
|
|
31
|
+
const options = [];
|
|
32
|
+
if (inputSequences.length === 0) {
|
|
33
|
+
options.push(<MenuItem key="UploadedFileSource" value="UploadedFileSource">Submit file</MenuItem>);
|
|
34
|
+
if (!noExternalRequests) {
|
|
35
|
+
options.push(<MenuItem key="RepositoryIdSource" value="RepositoryIdSource">Repository</MenuItem>);
|
|
36
|
+
options.push(<MenuItem key="GenomeCoordinatesSource" value="GenomeCoordinatesSource">Genome region</MenuItem>);
|
|
37
|
+
}
|
|
38
|
+
options.push(<MenuItem key="ManuallyTypedSource" value="ManuallyTypedSource">Enter manually</MenuItem>);
|
|
39
|
+
options.push(<MenuItem key="OligoHybridizationSource" value="OligoHybridizationSource">Oligonucleotide hybridization</MenuItem>);
|
|
40
|
+
if (database) {
|
|
41
|
+
options.push(<MenuItem key="DatabaseSource" value="DatabaseSource">{`Import from ${database.name}`}</MenuItem>);
|
|
42
|
+
}
|
|
43
|
+
if (sequencesExist) {
|
|
44
|
+
options.push(<MenuItem key="CopySequence" value="CopySequence">Use an existing sequence</MenuItem>);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// See https://github.com/manulera/OpenCloning_frontend/issues/101
|
|
48
|
+
if (inputSequences.length < 2) {
|
|
49
|
+
options.push(<MenuItem key="RestrictionEnzymeDigestionSource" value="RestrictionEnzymeDigestionSource">Restriction</MenuItem>);
|
|
50
|
+
options.push(<MenuItem key="PCRSource" value="PCRSource">PCR</MenuItem>);
|
|
51
|
+
options.push(<MenuItem key="PolymeraseExtensionSource" value="PolymeraseExtensionSource">Polymerase extension</MenuItem>);
|
|
52
|
+
options.push(<MenuItem key="ReverseComplementSource" value="ReverseComplementSource">Reverse complement</MenuItem>);
|
|
53
|
+
if (enablePlannotate) {
|
|
54
|
+
options.push(<MenuItem key="AnnotationSource" value="AnnotationSource">Annotate features</MenuItem>);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
options.push(<MenuItem key="LigationSource" value="LigationSource">Ligation (sticky / blunt)</MenuItem>);
|
|
58
|
+
options.push(<MenuItem key="GibsonAssemblySource" value="GibsonAssemblySource">Gibson assembly</MenuItem>);
|
|
59
|
+
options.push(<MenuItem key="HomologousRecombinationSource" value="HomologousRecombinationSource">Homologous recombination</MenuItem>);
|
|
60
|
+
options.push(<MenuItem key="CRISPRSource" value="CRISPRSource">CRISPR</MenuItem>);
|
|
61
|
+
options.push(<MenuItem key="RestrictionAndLigationSource" value="RestrictionAndLigationSource">Restriction + ligation / Golden Gate</MenuItem>);
|
|
62
|
+
options.push(<MenuItem key="OverlapExtensionPCRLigationSource" value="OverlapExtensionPCRLigationSource">Join overlap extension PCR fragments</MenuItem>);
|
|
63
|
+
options.push(<MenuItem key="InFusionSource" value="InFusionSource">In-Fusion</MenuItem>);
|
|
64
|
+
options.push(<MenuItem key="InVivoAssemblySource" value="InVivoAssemblySource">In vivo assembly</MenuItem>);
|
|
65
|
+
options.push(<MenuItem key="GatewaySource" value="GatewaySource">Gateway</MenuItem>);
|
|
66
|
+
options.push(<MenuItem key="CreLoxRecombinationSource" value="CreLoxRecombinationSource">Cre/Lox recombination</MenuItem>);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Sort options by text content
|
|
70
|
+
options.sort((a, b) => a.props.children.localeCompare(b.props.children));
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
{!sourceType && (<h2 className="empty-source-title">{inputSequences.length === 0 ? 'Import a sequence' : 'Use this sequence'}</h2>)}
|
|
75
|
+
<FormControl fullWidth>
|
|
76
|
+
<InputLabel id={`select-source-${sourceId}-label`}>Source type</InputLabel>
|
|
77
|
+
<Select
|
|
78
|
+
value={sourceType || ''}
|
|
79
|
+
onChange={onChange}
|
|
80
|
+
labelId={`select-source-${sourceId}-label`}
|
|
81
|
+
// Note how you have to set the label in two places
|
|
82
|
+
// see https://stackoverflow.com/questions/67064682/material-ui-outlined-select-label-is-not-rendering-properly
|
|
83
|
+
label="Source type"
|
|
84
|
+
disabled={sourceIsPrimerDesign}
|
|
85
|
+
>
|
|
86
|
+
{options}
|
|
87
|
+
</Select>
|
|
88
|
+
</FormControl>
|
|
89
|
+
</>
|
|
90
|
+
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default SourceTypeSelector;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SimpleCircularOrLinearView } from '@teselagen/ove';
|
|
3
|
+
import { useSelector } from 'react-redux';
|
|
4
|
+
import { reversePositionInRange } from '@teselagen/range-utils';
|
|
5
|
+
import { isEqual } from 'lodash-es';
|
|
6
|
+
import { parseFeatureLocation } from '@teselagen/bio-parsers';
|
|
7
|
+
import { getInputSequencesFromSourceId } from '@opencloning/store/cloning_utils';
|
|
8
|
+
|
|
9
|
+
function getCutParameters(seq, cut, isLeft) {
|
|
10
|
+
if (cut === null) {
|
|
11
|
+
return isLeft ? [0, 0, 0] : [seq.size, seq.size, 0];
|
|
12
|
+
}
|
|
13
|
+
const { cut_watson: watson, overhang: ovhg } = cut;
|
|
14
|
+
const crick = (watson - ovhg) % seq.size;
|
|
15
|
+
return [watson, crick, ovhg];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function SubSequenceDisplayer({
|
|
19
|
+
source, sourceId,
|
|
20
|
+
}) {
|
|
21
|
+
if (!['PCRSource', 'RestrictionEnzymeDigestionSource'].includes(source.type)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const inputSequenceIds = useSelector((state) => getInputSequencesFromSourceId(state, sourceId).map(({ id }) => id), isEqual);
|
|
25
|
+
const seq = useSelector((state) => state.cloning.teselaJsonCache[inputSequenceIds[0]], isEqual);
|
|
26
|
+
|
|
27
|
+
const editorName = `subsequence_editor_${sourceId}`;
|
|
28
|
+
let selectionLayer = null;
|
|
29
|
+
|
|
30
|
+
if (['PCRSource'].includes(source.type)) {
|
|
31
|
+
const leftLocation = parseFeatureLocation(source.input[1].left_location, 0, 0, 1, seq.length)[0];
|
|
32
|
+
const rightLocation = parseFeatureLocation(source.input[1].right_location, 0, 0, 1, seq.length)[0];
|
|
33
|
+
// Special case for the whole sequence amplification
|
|
34
|
+
if (isEqual(leftLocation, rightLocation)) {
|
|
35
|
+
selectionLayer = {
|
|
36
|
+
start: 0,
|
|
37
|
+
end: seq.size - 1,
|
|
38
|
+
};
|
|
39
|
+
} else if (!source.input[1].reverse_complemented) {
|
|
40
|
+
selectionLayer = {
|
|
41
|
+
start: leftLocation.start,
|
|
42
|
+
end: rightLocation.end,
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
selectionLayer = {
|
|
46
|
+
end: reversePositionInRange(leftLocation.start, seq.size),
|
|
47
|
+
start: reversePositionInRange(rightLocation.end, seq.size),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (['RestrictionEnzymeDigestionSource'].includes(source.type)) {
|
|
52
|
+
// The edges have the form (watson_pos, ovhg)
|
|
53
|
+
|
|
54
|
+
const [leftWatson, leftCrick, leftOvhg] = getCutParameters(seq, source.left_edge, true);
|
|
55
|
+
const [rightWatson, rightCrick, RightOvhg] = getCutParameters(seq, source.right_edge, false);
|
|
56
|
+
|
|
57
|
+
selectionLayer = {
|
|
58
|
+
start: leftOvhg > 0 ? leftCrick : leftWatson,
|
|
59
|
+
end: RightOvhg > 0 ? rightWatson : rightCrick,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="multiple-output-selector">
|
|
65
|
+
<SimpleCircularOrLinearView {...{ sequenceData: seq, editorName, selectionLayer, caretPosition: null, height: 'auto' }} />
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default SubSequenceDisplayer;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Button, Dialog, DialogActions, DialogTitle } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
function VerifyDeleteDialog({ dialogOpen, setDialogOpen, onClickDelete }) {
|
|
5
|
+
return (
|
|
6
|
+
<Dialog
|
|
7
|
+
open={dialogOpen}
|
|
8
|
+
onClose={() => setDialogOpen(false)}
|
|
9
|
+
className="verify-delete-dialog"
|
|
10
|
+
>
|
|
11
|
+
<DialogTitle>
|
|
12
|
+
Delete this source and all its children?
|
|
13
|
+
|
|
14
|
+
</DialogTitle>
|
|
15
|
+
<DialogActions>
|
|
16
|
+
<Button color="error" onClick={onClickDelete}>Delete</Button>
|
|
17
|
+
<Button onClick={() => { setDialogOpen(false); }}>Cancel</Button>
|
|
18
|
+
</DialogActions>
|
|
19
|
+
</Dialog>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default VerifyDeleteDialog;
|