@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,14 @@
|
|
|
1
|
+
const repositoryMetadata = {
|
|
2
|
+
AddgeneIdSource: { slug: 'addgene', label: 'Addgene', inputLabel: 'Addgene ID', example: '39296' },
|
|
3
|
+
BenchlingUrlSource: { slug: 'benchling', label: 'Benchling', inputLabel: 'Benchling URL' },
|
|
4
|
+
EuroscarfSource: { slug: 'euroscarf', label: 'Euroscarf', inputLabel: 'Euroscarf ID', example: 'P30174' },
|
|
5
|
+
NCBISequenceSource: { slug: 'genbank', label: 'GenBank', inputLabel: 'GenBank ID', example: 'NM_001018957.2' },
|
|
6
|
+
IGEMSource: { slug: 'igem', label: 'iGEM' },
|
|
7
|
+
OpenDNACollectionsSource: { slug: 'open_dna_collections', label: 'Open DNA Collections' },
|
|
8
|
+
SEVASource: { slug: 'seva', label: 'SEVA Plasmids' },
|
|
9
|
+
SnapGenePlasmidSource: { slug: 'snapgene', label: 'SnapGene' },
|
|
10
|
+
WekWikGeneIdSource: { slug: 'wekwikgene', label: 'WeKwikGene', inputLabel: 'WeKwikGene ID', example: '0000304' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default repositoryMetadata;
|
|
14
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControlLabel } from '@mui/material';
|
|
3
|
+
import useDatabase from '../../hooks/useDatabase';
|
|
4
|
+
import { sequencingFileExtensions } from '@opencloning/utils/sequencingFileExtensions';
|
|
5
|
+
|
|
6
|
+
function LoadFromDatabaseButton({ databaseId, onFileChange, setError, existingFileNames }) {
|
|
7
|
+
const database = useDatabase();
|
|
8
|
+
const [open, setOpen] = React.useState(false);
|
|
9
|
+
const [files, setFiles] = React.useState([]);
|
|
10
|
+
const [selectedFiles, setSelectedFiles] = React.useState([]);
|
|
11
|
+
const [loading, setLoading] = React.useState(false);
|
|
12
|
+
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
setSelectedFiles([]);
|
|
15
|
+
}, [open]);
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
async function getFiles() {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
const allFiles = await database.getSequencingFiles(databaseId);
|
|
21
|
+
setFiles(allFiles.filter((file) => sequencingFileExtensions.includes(file.name.toLowerCase().split('.').pop())));
|
|
22
|
+
setLoading(false);
|
|
23
|
+
}
|
|
24
|
+
if (open) {
|
|
25
|
+
getFiles();
|
|
26
|
+
}
|
|
27
|
+
}, [open]);
|
|
28
|
+
|
|
29
|
+
const handleSubmit = async () => {
|
|
30
|
+
setOpen(false);
|
|
31
|
+
try {
|
|
32
|
+
const filesToLoad = await Promise.all(selectedFiles.map((file) => files.find((f) => f.name === file).getFile()));
|
|
33
|
+
onFileChange(filesToLoad);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
setError(error.message);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Button
|
|
42
|
+
variant="contained"
|
|
43
|
+
onClick={() => setOpen(true)}
|
|
44
|
+
>
|
|
45
|
+
{`Load from ${database.name}`}
|
|
46
|
+
</Button>
|
|
47
|
+
<Dialog open={open} onClose={() => setOpen(false)}>
|
|
48
|
+
<DialogTitle>Select sequencing files to load</DialogTitle>
|
|
49
|
+
<DialogContent>
|
|
50
|
+
{loading && <CircularProgress />}
|
|
51
|
+
{!loading && files.length > 0 && (
|
|
52
|
+
<div>
|
|
53
|
+
{files.map((file, index) => (
|
|
54
|
+
<div key={file.name}>
|
|
55
|
+
<FormControlLabel
|
|
56
|
+
control={(
|
|
57
|
+
<Checkbox
|
|
58
|
+
disabled={existingFileNames.includes(file.name)}
|
|
59
|
+
checked={selectedFiles.includes(file.name) || existingFileNames.includes(file.name)}
|
|
60
|
+
onChange={(e) => {
|
|
61
|
+
if (e.target.checked) {
|
|
62
|
+
setSelectedFiles([...selectedFiles, file.name]);
|
|
63
|
+
} else {
|
|
64
|
+
setSelectedFiles(selectedFiles.filter((f) => f !== file.name));
|
|
65
|
+
}
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
label={file.name}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{!loading && files.length === 0 && (
|
|
76
|
+
<DialogContentText>
|
|
77
|
+
No sequencing files found in the database.
|
|
78
|
+
</DialogContentText>
|
|
79
|
+
)}
|
|
80
|
+
</DialogContent>
|
|
81
|
+
<DialogActions>
|
|
82
|
+
<Button color="error" onClick={() => setOpen(false)}>Cancel</Button>
|
|
83
|
+
<Button color="primary" onClick={handleSubmit}>Load</Button>
|
|
84
|
+
</DialogActions>
|
|
85
|
+
</Dialog>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default LoadFromDatabaseButton;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
3
|
+
import DownloadIcon from '@mui/icons-material/Download';
|
|
4
|
+
import {
|
|
5
|
+
TableCell,
|
|
6
|
+
TableRow,
|
|
7
|
+
IconButton,
|
|
8
|
+
} from '@mui/material';
|
|
9
|
+
import { useSelector } from 'react-redux';
|
|
10
|
+
|
|
11
|
+
function SequencingFileRow({ id, fileName, removeFile, downloadFile }) {
|
|
12
|
+
const fileType = useSelector((state) => state.cloning.files.find((f) => f.file_name === fileName && f.sequence_id === id).file_type);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<TableRow>
|
|
16
|
+
<TableCell sx={{ display: 'flex', alignItems: 'center' }}>
|
|
17
|
+
<IconButton onClick={() => removeFile(fileName)}>
|
|
18
|
+
<DeleteIcon />
|
|
19
|
+
</IconButton>
|
|
20
|
+
<IconButton onClick={() => downloadFile(fileName)} color="primary">
|
|
21
|
+
<DownloadIcon />
|
|
22
|
+
</IconButton>
|
|
23
|
+
</TableCell>
|
|
24
|
+
<TableCell>
|
|
25
|
+
{fileName}
|
|
26
|
+
</TableCell>
|
|
27
|
+
<TableCell>
|
|
28
|
+
{fileType}
|
|
29
|
+
</TableCell>
|
|
30
|
+
</TableRow>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default SequencingFileRow;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import VerificationFileDialog from './VerificationFileDialog';
|
|
3
|
+
import store from '@opencloning/store';
|
|
4
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
5
|
+
import { loadDataAndMount } from '../../../../../cypress/e2e/common_funcions_store';
|
|
6
|
+
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
7
|
+
|
|
8
|
+
const { setFiles, setConfig } = cloningActions;
|
|
9
|
+
|
|
10
|
+
const dummyFiles = [
|
|
11
|
+
{ file_name: 'file1.txt', sequence_id: 1, file_type: 'Sequencing file' },
|
|
12
|
+
{ file_name: 'file2.txt', sequence_id: 1, file_type: 'Sequencing file' },
|
|
13
|
+
{ file_name: 'file1.txt', sequence_id: 2, file_type: 'Sequencing file' },
|
|
14
|
+
];
|
|
15
|
+
// This is the base64 encoded string for the text "hello"
|
|
16
|
+
const base64str = 'aGVsbG8=';
|
|
17
|
+
|
|
18
|
+
describe('<VerificationFileDialog />', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
sessionStorage.clear();
|
|
21
|
+
});
|
|
22
|
+
it('renders and calls setDialogOpen with false when clicking close button', () => {
|
|
23
|
+
// see: https://on.cypress.io/mounting-react
|
|
24
|
+
const setDialogOpenSpy = cy.spy().as('setDialogOpenSpy');
|
|
25
|
+
cy.mount(<VerificationFileDialog id={1} dialogOpen setDialogOpen={setDialogOpenSpy} />);
|
|
26
|
+
|
|
27
|
+
// Click close button
|
|
28
|
+
cy.get('button').contains('Close').click();
|
|
29
|
+
cy.get('@setDialogOpenSpy').should('have.been.calledWith', false);
|
|
30
|
+
|
|
31
|
+
// Reset spy
|
|
32
|
+
setDialogOpenSpy.resetHistory();
|
|
33
|
+
|
|
34
|
+
// Click outside dialog
|
|
35
|
+
cy.get('.MuiDialog-container').click(0, 0);
|
|
36
|
+
cy.get('@setDialogOpenSpy').should('have.been.calledWith', false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('displays existing files and can download and delete them', () => {
|
|
40
|
+
store.dispatch(setFiles(dummyFiles));
|
|
41
|
+
dummyFiles.forEach((file) => {
|
|
42
|
+
sessionStorage.setItem(getVerificationFileName(file), base64str);
|
|
43
|
+
});
|
|
44
|
+
cy.mount(<VerificationFileDialog id={1} dialogOpen setDialogOpen={() => {}} />);
|
|
45
|
+
// Even though there are two files with the same name, only one should be displayed
|
|
46
|
+
cy.get('table td').filter(':contains("file1.txt")').should('have.length', 1);
|
|
47
|
+
cy.get('table').contains('file2.txt');
|
|
48
|
+
cy.get('table td').filter(':contains("Sequencing file")').should('have.length', 2);
|
|
49
|
+
cy.get('[data-testid="DownloadIcon"]').first().click();
|
|
50
|
+
cy.readFile('cypress/downloads/file1.txt').should('eq', 'hello');
|
|
51
|
+
|
|
52
|
+
cy.get('[data-testid="DeleteIcon"]').first().click();
|
|
53
|
+
cy.get('table').contains('file2.txt');
|
|
54
|
+
cy.get('table').contains('file1.txt').should('not.exist');
|
|
55
|
+
|
|
56
|
+
// Check that the file is not in sessionStorage
|
|
57
|
+
cy.window().its('sessionStorage')
|
|
58
|
+
.invoke('getItem', 'verification-1-file1.txt')
|
|
59
|
+
.should('be.null');
|
|
60
|
+
|
|
61
|
+
cy.window().its('sessionStorage')
|
|
62
|
+
.invoke('getItem', 'verification-1-file2.txt')
|
|
63
|
+
.should('not.be.null');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('can submit files and aligns them', () => {
|
|
67
|
+
store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
|
|
68
|
+
|
|
69
|
+
loadDataAndMount(
|
|
70
|
+
'cypress/test_files/sequencing/cloning_strategy_linear.json',
|
|
71
|
+
store,
|
|
72
|
+
() => {
|
|
73
|
+
cy.mount(<VerificationFileDialog id={2} dialogOpen setDialogOpen={() => {}} />);
|
|
74
|
+
},
|
|
75
|
+
).then(() => {
|
|
76
|
+
cy.get('button').contains('Submit files').click();
|
|
77
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/BZO902-13409020-13409020.ab1', { force: true });
|
|
78
|
+
cy.get('table').contains('BZO902-13409020-13409020.ab1', { timeout: 20000 });
|
|
79
|
+
}).then(() => {
|
|
80
|
+
const file = store.getState().cloning.files.find(
|
|
81
|
+
(f) => f.file_name === 'BZO902-13409020-13409020.ab1',
|
|
82
|
+
);
|
|
83
|
+
cy.expect(file.file_type).to.equal('Sequencing file');
|
|
84
|
+
cy.expect(file.sequence_id).to.equal(2);
|
|
85
|
+
cy.expect(file.alignment).to.have.length(2);
|
|
86
|
+
// The file content is stored in sessionStorage
|
|
87
|
+
cy.window().its('sessionStorage')
|
|
88
|
+
.invoke('getItem', getVerificationFileName(file))
|
|
89
|
+
.should('not.be.null', { timeout: 10000 });
|
|
90
|
+
|
|
91
|
+
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
92
|
+
statusCode: 200,
|
|
93
|
+
body: ['A', 'T', 'C'],
|
|
94
|
+
}).as('alignSanger');
|
|
95
|
+
// We upload another file now
|
|
96
|
+
cy.get('button').contains('Submit files').click();
|
|
97
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/BZO903-13409037-13409037.ab1', { force: true });
|
|
98
|
+
|
|
99
|
+
cy.get('table').contains('BZO903-13409037-13409037.ab1', { timeout: 20000 });
|
|
100
|
+
cy.wait('@alignSanger').then((interception) => {
|
|
101
|
+
// Submits both the old and new sequences for alignment
|
|
102
|
+
expect(interception.request.body.traces).to.have.length(2);
|
|
103
|
+
});
|
|
104
|
+
})
|
|
105
|
+
.then(() => {
|
|
106
|
+
// The file has been added to the store
|
|
107
|
+
const file = store.getState().cloning.files.find(
|
|
108
|
+
(f) => f.file_name === 'BZO903-13409037-13409037.ab1',
|
|
109
|
+
);
|
|
110
|
+
cy.expect(file).to.exist;
|
|
111
|
+
cy.expect(file.file_type).to.equal('Sequencing file');
|
|
112
|
+
cy.expect(file.sequence_id).to.equal(2);
|
|
113
|
+
cy.expect(file.alignment).to.deep.equal(['A', 'C']);
|
|
114
|
+
// The file content is stored in sessionStorage
|
|
115
|
+
cy.window().its('sessionStorage')
|
|
116
|
+
.invoke('getItem', getVerificationFileName(file))
|
|
117
|
+
.should('not.be.null', { timeout: 10000 });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it('handles errors', () => {
|
|
121
|
+
store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
|
|
122
|
+
loadDataAndMount(
|
|
123
|
+
'cypress/test_files/sequencing/cloning_strategy_linear.json',
|
|
124
|
+
store,
|
|
125
|
+
() => {
|
|
126
|
+
cy.mount(<VerificationFileDialog id={2} dialogOpen setDialogOpen={() => {}} />);
|
|
127
|
+
},
|
|
128
|
+
).then(() => {
|
|
129
|
+
// Error if submitting non-allowed files
|
|
130
|
+
cy.get('button').contains('Submit files').click();
|
|
131
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/cloning_strategy_linear.json', { force: true });
|
|
132
|
+
cy.contains('Only ab1');
|
|
133
|
+
|
|
134
|
+
// Error if submitting .ab1 files that are not valid
|
|
135
|
+
cy.get('button').contains('Submit files').click();
|
|
136
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/wrong.ab1', { force: true });
|
|
137
|
+
cy.contains('Error parsing wrong.ab1:', { timeout: 20000 });
|
|
138
|
+
|
|
139
|
+
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
140
|
+
statusCode: 200,
|
|
141
|
+
body: ['A', 'T'],
|
|
142
|
+
});
|
|
143
|
+
// Error if name already exists
|
|
144
|
+
// Add it once
|
|
145
|
+
cy.get('button').contains('Submit files').click();
|
|
146
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/BZO903-13409037-13409037.ab1', { force: true });
|
|
147
|
+
cy.get('table').contains('BZO903-13409037-13409037.ab1', { timeout: 20000 });
|
|
148
|
+
|
|
149
|
+
// Add it again
|
|
150
|
+
cy.get('button').contains('Submit files').click();
|
|
151
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/BZO903-13409037-13409037.ab1', { force: true });
|
|
152
|
+
cy.contains('A file named BZO903-13409037-13409037.ab1 is already associated to this sequence');
|
|
153
|
+
|
|
154
|
+
// Error if the server returns an error
|
|
155
|
+
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
156
|
+
statusCode: 500,
|
|
157
|
+
});
|
|
158
|
+
cy.get('button').contains('Submit files').click();
|
|
159
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/BZO902-13409020-13409020.ab1', { force: true });
|
|
160
|
+
cy.contains('Request failed');
|
|
161
|
+
// It has not been added
|
|
162
|
+
cy.get('table').contains('BZO902-13409020-13409020.ab1').should('not.exist');
|
|
163
|
+
cy.window().its('sessionStorage')
|
|
164
|
+
.invoke('getItem', 'verification-2-BZO902-13409020-13409020.ab1')
|
|
165
|
+
.should('be.null');
|
|
166
|
+
|
|
167
|
+
// Connection error
|
|
168
|
+
cy.intercept('POST', 'http://127.0.0.1:8000/align_sanger*', {
|
|
169
|
+
forceNetworkError: true,
|
|
170
|
+
});
|
|
171
|
+
cy.get('button').contains('Submit files').click();
|
|
172
|
+
cy.get('input[type="file"]').selectFile('cypress/test_files/sequencing/BZO902-13409020-13409020.ab1', { force: true });
|
|
173
|
+
cy.contains('Network Error');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogTitle,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogActions,
|
|
7
|
+
Button,
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableContainer,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableRow,
|
|
14
|
+
Paper,
|
|
15
|
+
Box,
|
|
16
|
+
CircularProgress,
|
|
17
|
+
Alert,
|
|
18
|
+
} from '@mui/material';
|
|
19
|
+
import { batch, shallowEqual, useDispatch, useSelector, useStore } from 'react-redux';
|
|
20
|
+
import { isEqual } from 'lodash-es';
|
|
21
|
+
import { getTeselaJsonFromBase64, file2base64 } from '@opencloning/utils/readNwrite';
|
|
22
|
+
import SequencingFileRow from './SequencingFileRow';
|
|
23
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
24
|
+
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
25
|
+
import useStoreEditor from '../../hooks/useStoreEditor';
|
|
26
|
+
import LoadFromDatabaseButton from './LoadFromDatabaseButton';
|
|
27
|
+
import { sequencingFileExtensions } from '@opencloning/utils/sequencingFileExtensions';
|
|
28
|
+
import useHttpClient from '../../hooks/useHttpClient';
|
|
29
|
+
import { getVerificationFileName } from '@opencloning/utils/readNwrite';
|
|
30
|
+
|
|
31
|
+
const { addFile, removeFile: removeFileAction, removeFilesAssociatedToSequence, setMainSequenceId, setCurrentTab } = cloningActions;
|
|
32
|
+
|
|
33
|
+
export default function VerificationFileDialog({ id, dialogOpen, setDialogOpen }) {
|
|
34
|
+
const fileNames = useSelector((state) => state.cloning.files.filter((f) => (f.sequence_id === id)).map((f) => f.file_name), shallowEqual);
|
|
35
|
+
const fileTypes = useSelector((state) => state.cloning.files.filter((f) => (f.sequence_id === id)).map((f) => f.file_type), shallowEqual);
|
|
36
|
+
const hasSequencingFile = fileTypes.includes('Sequencing file');
|
|
37
|
+
const databaseId = useSelector((state) => state.cloning.sources.find((s) => s.id === id)?.database_id, isEqual);
|
|
38
|
+
|
|
39
|
+
const sequence = useSelector((state) => state.cloning.sequences.find((e) => e.id === id), shallowEqual);
|
|
40
|
+
const [loadingMessage, setLoadingMessage] = useState('');
|
|
41
|
+
const [error, setError] = useState('');
|
|
42
|
+
const dispatch = useDispatch();
|
|
43
|
+
const fileInputRef = useRef(null);
|
|
44
|
+
const { updateStoreEditor } = useStoreEditor();
|
|
45
|
+
const backendRoute = useBackendRoute();
|
|
46
|
+
const store = useStore();
|
|
47
|
+
const httpClient = useHttpClient();
|
|
48
|
+
|
|
49
|
+
const toggleMain = React.useCallback(() => {
|
|
50
|
+
dispatch(setMainSequenceId(id));
|
|
51
|
+
dispatch(setCurrentTab(3));
|
|
52
|
+
updateStoreEditor('mainEditor', id);
|
|
53
|
+
// TODO: ideally this should be done with a ref
|
|
54
|
+
document.getElementById('opencloning-app-tabs')?.scrollIntoView();
|
|
55
|
+
}, [id]);
|
|
56
|
+
|
|
57
|
+
const handleFileUpload = async (newFiles) => {
|
|
58
|
+
// Clear the input
|
|
59
|
+
fileInputRef.current.value = '';
|
|
60
|
+
if (newFiles.some((file) => !sequencingFileExtensions.includes(file.name.toLowerCase().split('.').pop()))) {
|
|
61
|
+
throw new Error(`Only ${sequencingFileExtensions.join(', ')} files are accepted`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Read the new sequencing files
|
|
65
|
+
const parsedSequencingFiles = await Promise.all(newFiles.map(async (newFile) => {
|
|
66
|
+
const base64str = await file2base64(newFile);
|
|
67
|
+
return {
|
|
68
|
+
sequence_id: id,
|
|
69
|
+
trace: (await getTeselaJsonFromBase64(base64str, newFile.name)).sequence,
|
|
70
|
+
base64str,
|
|
71
|
+
file_name: newFile.name,
|
|
72
|
+
file_type: 'Sequencing file',
|
|
73
|
+
};
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// Read the existing sequencing files
|
|
77
|
+
const existingSequencingFilesInState = await Promise.all(store.getState().cloning.files
|
|
78
|
+
.filter((f) => f.sequence_id === id && f.file_type === 'Sequencing file')
|
|
79
|
+
.map(async (f) => {
|
|
80
|
+
const base64str = sessionStorage.getItem(getVerificationFileName(f));
|
|
81
|
+
const trace = (await getTeselaJsonFromBase64(base64str, f.file_name)).sequence;
|
|
82
|
+
return { ...f, base64str, trace };
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
// Throw an error if repeated files are found
|
|
86
|
+
const existingSequencingFilesInStateNames = existingSequencingFilesInState.map((f) => f.file_name);
|
|
87
|
+
const repeatedFile = parsedSequencingFiles.find((f) => existingSequencingFilesInStateNames.includes(f.file_name));
|
|
88
|
+
if (repeatedFile) {
|
|
89
|
+
throw new Error(`A file named ${repeatedFile.file_name} is already associated to this sequence`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// We have to align all again to have a common reference
|
|
93
|
+
const allSequencingFiles = [...existingSequencingFilesInState, ...parsedSequencingFiles];
|
|
94
|
+
const alignments = [];
|
|
95
|
+
const traces = allSequencingFiles.map((f) => f.trace);
|
|
96
|
+
const resp = await httpClient.post(backendRoute('align_sanger'), { sequence, traces });
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < allSequencingFiles.length; i++) {
|
|
99
|
+
alignments.push({ ...allSequencingFiles[i], alignment: [resp.data[0], resp.data[i + 1]] });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add the files to the store
|
|
103
|
+
batch(() => {
|
|
104
|
+
dispatch(removeFilesAssociatedToSequence(id));
|
|
105
|
+
alignments.forEach((alignment) => {
|
|
106
|
+
const { trace, base64str, ...rest } = alignment;
|
|
107
|
+
dispatch(addFile(rest));
|
|
108
|
+
});
|
|
109
|
+
alignments.forEach(({ base64str, file_name }) => { sessionStorage.setItem(`verification-${id}-${file_name}`, base64str); });
|
|
110
|
+
});
|
|
111
|
+
setLoadingMessage('');
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const onFileChange = useCallback(async (files) => {
|
|
115
|
+
setLoadingMessage('Aligning...');
|
|
116
|
+
setError('');
|
|
117
|
+
try {
|
|
118
|
+
await handleFileUpload(files);
|
|
119
|
+
setLoadingMessage('');
|
|
120
|
+
} catch (e) {
|
|
121
|
+
setError(e.message);
|
|
122
|
+
setLoadingMessage('');
|
|
123
|
+
}
|
|
124
|
+
}, [handleFileUpload]);
|
|
125
|
+
|
|
126
|
+
const removeFile = useCallback((fileName) => {
|
|
127
|
+
dispatch(removeFileAction({ fileName, sequenceId: id }));
|
|
128
|
+
sessionStorage.removeItem(`verification-${id}-${fileName}`);
|
|
129
|
+
}, [id]);
|
|
130
|
+
|
|
131
|
+
const handleClickUpload = () => {
|
|
132
|
+
fileInputRef.current?.click();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const downloadFile = (fileName) => {
|
|
136
|
+
const base64Content = sessionStorage.getItem(`verification-${id}-${fileName}`);
|
|
137
|
+
if (!base64Content) {
|
|
138
|
+
setError(`File ${fileName} not found in session storage`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const binaryString = atob(base64Content);
|
|
142
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
143
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
144
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
145
|
+
}
|
|
146
|
+
const file = new Blob([bytes], { type: 'application/octet-stream' });
|
|
147
|
+
const url = URL.createObjectURL(file);
|
|
148
|
+
const a = document.createElement('a');
|
|
149
|
+
a.href = url;
|
|
150
|
+
a.download = fileName;
|
|
151
|
+
document.body.appendChild(a);
|
|
152
|
+
a.click();
|
|
153
|
+
document.body.removeChild(a);
|
|
154
|
+
URL.revokeObjectURL(url);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Dialog
|
|
159
|
+
open={dialogOpen}
|
|
160
|
+
onClose={() => setDialogOpen(false)}
|
|
161
|
+
className="verification-file-dialog"
|
|
162
|
+
maxWidth="md"
|
|
163
|
+
fullWidth
|
|
164
|
+
sx={{ textAlign: 'center' }}
|
|
165
|
+
>
|
|
166
|
+
<DialogTitle>Verification files</DialogTitle>
|
|
167
|
+
<DialogContent>
|
|
168
|
+
<input
|
|
169
|
+
type="file"
|
|
170
|
+
accept={sequencingFileExtensions.map((ext) => `.${ext}`).join(', ')}
|
|
171
|
+
multiple
|
|
172
|
+
onChange={(event) => onFileChange(Array.from(event.target.files))}
|
|
173
|
+
style={{ display: 'none' }}
|
|
174
|
+
ref={fileInputRef}
|
|
175
|
+
/>
|
|
176
|
+
|
|
177
|
+
{loadingMessage && (
|
|
178
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', my: 2 }}>
|
|
179
|
+
<CircularProgress />
|
|
180
|
+
<Box sx={{ ml: 2, fontSize: '1.2rem' }}>{loadingMessage}</Box>
|
|
181
|
+
</Box>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<TableContainer component={Paper}>
|
|
185
|
+
<Table>
|
|
186
|
+
<TableHead>
|
|
187
|
+
<TableRow>
|
|
188
|
+
<TableCell />
|
|
189
|
+
<TableCell>File Name</TableCell>
|
|
190
|
+
<TableCell>Type</TableCell>
|
|
191
|
+
</TableRow>
|
|
192
|
+
</TableHead>
|
|
193
|
+
<TableBody>
|
|
194
|
+
{fileNames.map((fileName) => (
|
|
195
|
+
<SequencingFileRow
|
|
196
|
+
key={fileName}
|
|
197
|
+
id={id}
|
|
198
|
+
fileName={fileName}
|
|
199
|
+
removeFile={removeFile}
|
|
200
|
+
downloadFile={downloadFile}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
</TableBody>
|
|
204
|
+
</Table>
|
|
205
|
+
</TableContainer>
|
|
206
|
+
|
|
207
|
+
{error && (
|
|
208
|
+
<Box sx={{ mt: 2 }}>
|
|
209
|
+
<Alert severity="error">
|
|
210
|
+
{error}
|
|
211
|
+
</Alert>
|
|
212
|
+
</Box>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
|
|
216
|
+
<Button
|
|
217
|
+
variant="contained"
|
|
218
|
+
onClick={handleClickUpload}
|
|
219
|
+
>
|
|
220
|
+
Submit files
|
|
221
|
+
</Button>
|
|
222
|
+
|
|
223
|
+
{databaseId && (
|
|
224
|
+
<LoadFromDatabaseButton
|
|
225
|
+
existingFileNames={fileNames}
|
|
226
|
+
databaseId={databaseId}
|
|
227
|
+
onFileChange={onFileChange}
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{hasSequencingFile && (
|
|
232
|
+
<Button
|
|
233
|
+
variant="contained"
|
|
234
|
+
onClick={() => { setDialogOpen(false); toggleMain(); }}
|
|
235
|
+
color="success"
|
|
236
|
+
>
|
|
237
|
+
See alignments in editor
|
|
238
|
+
</Button>
|
|
239
|
+
)}
|
|
240
|
+
</Box>
|
|
241
|
+
|
|
242
|
+
<DialogActions>
|
|
243
|
+
<Button onClick={() => setDialogOpen(false)}>Close</Button>
|
|
244
|
+
</DialogActions>
|
|
245
|
+
</DialogContent>
|
|
246
|
+
</Dialog>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
isFullscreen: false,
|
|
3
|
+
readOnly: false,
|
|
4
|
+
disableBpEditing: true,
|
|
5
|
+
annotationVisibility: { reverseSequence: true, cutsites: false },
|
|
6
|
+
ToolBarProps: {
|
|
7
|
+
toolList: [
|
|
8
|
+
'downloadTool',
|
|
9
|
+
'undoTool',
|
|
10
|
+
'redoTool',
|
|
11
|
+
'cutsiteTool',
|
|
12
|
+
'featureTool',
|
|
13
|
+
'findTool',
|
|
14
|
+
'visibilityTool',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
adjustCircularLabelSpacing: true,
|
|
18
|
+
panelsShown: [[
|
|
19
|
+
{
|
|
20
|
+
id: 'rail',
|
|
21
|
+
name: 'Linear Map',
|
|
22
|
+
active: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'sequence',
|
|
26
|
+
name: 'Sequence Map',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'circular',
|
|
30
|
+
name: 'Circular Map',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'properties',
|
|
34
|
+
name: 'Properties',
|
|
35
|
+
},
|
|
36
|
+
]],
|
|
37
|
+
massageCmds: (cmds) => {
|
|
38
|
+
// just using the print cmd here as an example but other cmds are also valid targets for modifying
|
|
39
|
+
Object.keys(cmds).forEach((key) => {
|
|
40
|
+
delete cmds[key].hotkey;
|
|
41
|
+
});
|
|
42
|
+
return cmds;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useDispatch } from 'react-redux';
|
|
2
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
3
|
+
|
|
4
|
+
export default function useAlerts() {
|
|
5
|
+
const dispatch = useDispatch();
|
|
6
|
+
|
|
7
|
+
const addAlert = (alert) => {
|
|
8
|
+
dispatch(cloningActions.addAlert(alert));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const removeAlert = (message) => {
|
|
12
|
+
dispatch(cloningActions.removeAlert(message));
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return { addAlert, removeAlert };
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import error2String from '@opencloning/utils/error2String';
|
|
3
|
+
import useBackendRoute from './useBackendRoute';
|
|
4
|
+
import useAlerts from './useAlerts';
|
|
5
|
+
import useHttpClient from './useHttpClient';
|
|
6
|
+
|
|
7
|
+
export default function useBackendAPI() {
|
|
8
|
+
const [requestStatus, setRequestStatus] = useState({ status: null, message: '' });
|
|
9
|
+
const [sources, setSources] = useState([]);
|
|
10
|
+
const [sequences, setSequences] = useState([]);
|
|
11
|
+
const backendRoute = useBackendRoute();
|
|
12
|
+
const httpClient = useHttpClient();
|
|
13
|
+
const { addAlert } = useAlerts();
|
|
14
|
+
|
|
15
|
+
const sendPostRequest = useCallback(async ({ endpoint, requestData, config = {}, source: { output }, modifySource = (s) => s }) => {
|
|
16
|
+
setRequestStatus({ status: 'loading', message: 'loading' });
|
|
17
|
+
|
|
18
|
+
// Url built like this in case trailing slash
|
|
19
|
+
const url = backendRoute(endpoint);
|
|
20
|
+
// paramsSerializer: { indexes: null } is to correctly serialize arrays in the URL
|
|
21
|
+
const fullConfig = { ...config, paramsSerializer: { indexes: null } };
|
|
22
|
+
try {
|
|
23
|
+
const resp = await httpClient.post(url, requestData, fullConfig);
|
|
24
|
+
if (resp.headers['x-warning']) {
|
|
25
|
+
addAlert({ message: resp.headers['x-warning'], severity: 'warning' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Most endpoints return an error if no products would be created,
|
|
29
|
+
// but just in case, we check if response is empty and generate an error
|
|
30
|
+
// TODO: Unit test here
|
|
31
|
+
if (resp.data.sources.length === 0) {
|
|
32
|
+
setSources([]);
|
|
33
|
+
setSequences([]);
|
|
34
|
+
setRequestStatus({ status: 'error', message: 'No outputs returned' });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setRequestStatus({ status: null, message: '' });
|
|
39
|
+
|
|
40
|
+
const receivedSources = resp.data.sources.map(modifySource);
|
|
41
|
+
|
|
42
|
+
setSources(receivedSources); setSequences(resp.data.sequences);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
setRequestStatus({ status: 'error', message: error2String(error) });
|
|
45
|
+
setSources([]);
|
|
46
|
+
setSequences([]);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return { requestStatus, sources, sequences, sendPostRequest };
|
|
51
|
+
}
|