@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,7 @@
|
|
|
1
|
+
/* v8 ignore start */
|
|
2
|
+
|
|
3
|
+
export const writeApiKey = import.meta.env.VITE_ELABFTW_API_WRITE_KEY || '';
|
|
4
|
+
export const readApiKey = import.meta.env.VITE_ELABFTW_API_READ_KEY || '';
|
|
5
|
+
export const baseUrl = import.meta.env.VITE_ELABFTW_BASE_URL || '';
|
|
6
|
+
|
|
7
|
+
/* v8 ignore end */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// These functions are here because they are used across files,
|
|
2
|
+
// so including them in eLabFTWInterface.js would make circular dependencies.
|
|
3
|
+
// They are not included in common.js to be able to mock eLabFTWHttpClient in tests.
|
|
4
|
+
import { eLabFTWHttpClient, readHeaders } from './common';
|
|
5
|
+
|
|
6
|
+
export function error2String(error) {
|
|
7
|
+
if (error.code === 'ERR_NETWORK') { return 'Network error: Cannot connect to eLabFTW'; }
|
|
8
|
+
if (!error.code) {
|
|
9
|
+
return error.message || 'Internal error, please contact the developers.';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (error.response.status === 500) return 'Internal server error';
|
|
13
|
+
const { description } = error.response.data;
|
|
14
|
+
if (typeof description === 'string') {
|
|
15
|
+
return description;
|
|
16
|
+
}
|
|
17
|
+
return 'Request error, please contact the developers.';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getFileFromELabFTW = async (itemId, fileInfo) => {
|
|
21
|
+
const url = `/api/v2/items/${itemId}/uploads/${fileInfo.id}?format=binary`;
|
|
22
|
+
try {
|
|
23
|
+
const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders, responseType: 'blob' });
|
|
24
|
+
// Convert blob to file
|
|
25
|
+
return new File([resp.data], fileInfo.real_name);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error(e);
|
|
28
|
+
throw new Error(`${error2String(e)}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function arrayCombinations(sets) {
|
|
33
|
+
if (sets.length === 1) {
|
|
34
|
+
return sets[0].map((el) => [el]);
|
|
35
|
+
} else
|
|
36
|
+
return sets[0].flatMap((val) =>
|
|
37
|
+
arrayCombinations(sets.slice(1)).map((c) => [val].concat(c))
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FormHelperText } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
function CustomFormHelperText({ children }) {
|
|
5
|
+
return (
|
|
6
|
+
<FormHelperText component="div" style={{ fontSize: 'x-small', marginLeft: 0, marginRight: 0, position: 'absolute', botom: '0px' }}>{children}</FormHelperText>
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default CustomFormHelperText;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import EnzymeMultiSelect from './EnzymeMultiSelect';
|
|
3
|
+
import store from '@opencloning/store';
|
|
4
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
5
|
+
|
|
6
|
+
const { setConfig } = cloningActions;
|
|
7
|
+
describe('<EnzymeMultiSelect />', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
store.dispatch(setConfig({ backendUrl: 'http://127.0.0.1:8000' }));
|
|
10
|
+
});
|
|
11
|
+
it('can add and remove enzymes, sets enzymes', () => {
|
|
12
|
+
// see: https://on.cypress.io/mounting-react
|
|
13
|
+
const setEnzymesSpy = cy.spy().as('setEnzymesSpy');
|
|
14
|
+
cy.mount(<EnzymeMultiSelect setEnzymes={setEnzymesSpy} />);
|
|
15
|
+
cy.get('.MuiInputBase-root').click();
|
|
16
|
+
// All enzymes shown
|
|
17
|
+
cy.get('div[role="presentation"]', { timeout: 20000 }).contains('AanI');
|
|
18
|
+
// Type EcoRI
|
|
19
|
+
cy.get('label').contains('Enzymes used').siblings('div').children('input')
|
|
20
|
+
.type('EcoRI');
|
|
21
|
+
cy.get('div[role="presentation"]').contains('AanI').should('not.exist');
|
|
22
|
+
cy.get('div[role="presentation"]').contains('EcoRI');
|
|
23
|
+
// Select the option
|
|
24
|
+
cy.get('div[role="presentation"]').contains('EcoRI').click();
|
|
25
|
+
cy.get('@setEnzymesSpy').should('have.been.calledWith', ['EcoRI']);
|
|
26
|
+
// Select SalI
|
|
27
|
+
cy.get('label').contains('Enzymes used').siblings('div').children('input')
|
|
28
|
+
.clear('');
|
|
29
|
+
cy.get('label').contains('Enzymes used').siblings('div').children('input')
|
|
30
|
+
.type('SalI');
|
|
31
|
+
cy.get('div[role="presentation"]').contains('SalI').click();
|
|
32
|
+
cy.get('@setEnzymesSpy').should('have.been.calledWith', ['EcoRI', 'SalI']);
|
|
33
|
+
// There should be two chips
|
|
34
|
+
cy.get('.MuiChip-root').contains('EcoRI');
|
|
35
|
+
cy.get('.MuiChip-root').contains('SalI');
|
|
36
|
+
// We can remove SalI
|
|
37
|
+
cy.get('.MuiChip-root').contains('SalI').siblings('svg').click();
|
|
38
|
+
cy.get('.MuiChip-root').contains('SalI').should('not.exist');
|
|
39
|
+
cy.get('.MuiChip-root').contains('EcoRI');
|
|
40
|
+
// We can remove EcoRI
|
|
41
|
+
cy.get('.MuiChip-root').contains('EcoRI').siblings('svg').click();
|
|
42
|
+
cy.get('.MuiChip-root').should('not.exist');
|
|
43
|
+
});
|
|
44
|
+
it('shows error message when server is down', () => {
|
|
45
|
+
cy.intercept('GET', '/restriction_enzyme_list', {
|
|
46
|
+
statusCode: 500,
|
|
47
|
+
body: 'Server down',
|
|
48
|
+
});
|
|
49
|
+
cy.mount(<EnzymeMultiSelect setEnzymes={() => {}} />);
|
|
50
|
+
cy.get('.MuiAlert-message').contains('Could not retrieve enzymes from server');
|
|
51
|
+
});
|
|
52
|
+
it('shows loading message', () => {
|
|
53
|
+
cy.intercept('GET', '/restriction_enzyme_list', {
|
|
54
|
+
delayMs: 1000,
|
|
55
|
+
body: { enzyme_names: ['EcoRI', 'SalI'] },
|
|
56
|
+
});
|
|
57
|
+
cy.mount(<EnzymeMultiSelect setEnzymes={() => {}} />);
|
|
58
|
+
cy.get('.MuiCircularProgress-svg');
|
|
59
|
+
cy.contains('retrieving enzymes...').should('exist');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import GetRequestMultiSelect from './GetRequestMultiSelect';
|
|
3
|
+
import useBackendRoute from '../../hooks/useBackendRoute';
|
|
4
|
+
import useHttpClient from '../../hooks/useHttpClient';
|
|
5
|
+
|
|
6
|
+
function EnzymeMultiSelect({ setEnzymes, label = 'Enzymes used', multiple = true }) {
|
|
7
|
+
const backendRoute = useBackendRoute();
|
|
8
|
+
const url = backendRoute('restriction_enzyme_list');
|
|
9
|
+
const getOptionsFromResponse = React.useCallback((data) => data.enzyme_names, []);
|
|
10
|
+
const messages = React.useMemo(() => ({
|
|
11
|
+
loadingMessage: 'retrieving enzymes...',
|
|
12
|
+
errorMessage: 'Could not retrieve enzymes from server',
|
|
13
|
+
}), []);
|
|
14
|
+
const onChange = React.useCallback((value) => setEnzymes(value), [setEnzymes]);
|
|
15
|
+
const httpClient = useHttpClient();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
|
|
19
|
+
<GetRequestMultiSelect
|
|
20
|
+
fullWidth
|
|
21
|
+
className="enzyme-multi-select"
|
|
22
|
+
getOptionsFromResponse={getOptionsFromResponse}
|
|
23
|
+
url={url}
|
|
24
|
+
label={label}
|
|
25
|
+
httpClient={httpClient}
|
|
26
|
+
messages={messages}
|
|
27
|
+
onChange={onChange}
|
|
28
|
+
multiple={multiple}
|
|
29
|
+
getOptionLabel={(option) => option}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default React.memo(EnzymeMultiSelect);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import Autocomplete from '@mui/material/Autocomplete';
|
|
3
|
+
import TextField from '@mui/material/TextField';
|
|
4
|
+
import { Alert, Button, CircularProgress, FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
export default function GetRequestMultiSelect({
|
|
7
|
+
getOptionsFromResponse,
|
|
8
|
+
url,
|
|
9
|
+
label,
|
|
10
|
+
messages,
|
|
11
|
+
onChange,
|
|
12
|
+
httpClient,
|
|
13
|
+
multiple = true,
|
|
14
|
+
autoComplete = true,
|
|
15
|
+
getOptionLabel,
|
|
16
|
+
requestHeaders = {},
|
|
17
|
+
noOptionsMessage = 'No options found',
|
|
18
|
+
requestParams = {},
|
|
19
|
+
...rest
|
|
20
|
+
}) {
|
|
21
|
+
const { loadingMessage, errorMessage } = messages;
|
|
22
|
+
const [options, setOptions] = React.useState([]);
|
|
23
|
+
const [connectAttempt, setConnectAttemp] = React.useState(0);
|
|
24
|
+
const [error, setError] = React.useState(false);
|
|
25
|
+
const [waitingMessage, setWaitingMessage] = React.useState(loadingMessage);
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
httpClient.get(url, { headers: requestHeaders, params: requestParams }).then(({ data }) => {
|
|
29
|
+
setWaitingMessage(null);
|
|
30
|
+
const respOptions = getOptionsFromResponse(data);
|
|
31
|
+
if (!Array.isArray(respOptions)) {
|
|
32
|
+
throw new Error('Expected array of options from getOptionsFromResponse');
|
|
33
|
+
}
|
|
34
|
+
setOptions(respOptions);
|
|
35
|
+
setError(false);
|
|
36
|
+
}).catch((e) => { setWaitingMessage(errorMessage); setError(true); setOptions([]); });
|
|
37
|
+
}, [connectAttempt]);
|
|
38
|
+
|
|
39
|
+
if (error) {
|
|
40
|
+
return (
|
|
41
|
+
<Alert
|
|
42
|
+
sx={{ alignItems: 'center' }}
|
|
43
|
+
severity="error"
|
|
44
|
+
action={(
|
|
45
|
+
<Button color="inherit" size="small" onClick={() => { setWaitingMessage('Retrying...'); setConnectAttemp(connectAttempt + 1); }}>
|
|
46
|
+
Retry
|
|
47
|
+
</Button>
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{waitingMessage}
|
|
51
|
+
</Alert>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (waitingMessage) {
|
|
56
|
+
return (
|
|
57
|
+
|
|
58
|
+
<Alert severity="info" icon={<CircularProgress color="inherit" size="1em" />}>
|
|
59
|
+
{waitingMessage}
|
|
60
|
+
</Alert>
|
|
61
|
+
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<FormControl {...rest}>
|
|
67
|
+
{autoComplete ? (
|
|
68
|
+
<Autocomplete
|
|
69
|
+
multiple={multiple}
|
|
70
|
+
onChange={(event, value) => { onChange(value); }}
|
|
71
|
+
id="tags-standard"
|
|
72
|
+
options={options}
|
|
73
|
+
getOptionLabel={getOptionLabel}
|
|
74
|
+
defaultValue={multiple ? [] : ''}
|
|
75
|
+
freeSolo
|
|
76
|
+
forcePopupIcon
|
|
77
|
+
renderInput={(params) => (
|
|
78
|
+
<TextField
|
|
79
|
+
{...params}
|
|
80
|
+
label={label}
|
|
81
|
+
error={error}
|
|
82
|
+
helperText={options.length === 0 ? noOptionsMessage : ''}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
) : (
|
|
87
|
+
<>
|
|
88
|
+
<InputLabel id={`select-${label.replaceAll(' ', '-')}`}>{label}</InputLabel>
|
|
89
|
+
<Select
|
|
90
|
+
multiple={multiple}
|
|
91
|
+
onChange={(event) => { onChange(event.target.value, options); }}
|
|
92
|
+
label={label}
|
|
93
|
+
defaultValue={multiple ? [] : ''}
|
|
94
|
+
error={options.length === 0}
|
|
95
|
+
>
|
|
96
|
+
{options.map((option) => (
|
|
97
|
+
<MenuItem key={getOptionLabel(option)} value={getOptionLabel(option)}>
|
|
98
|
+
{getOptionLabel(option)}
|
|
99
|
+
</MenuItem>
|
|
100
|
+
))}
|
|
101
|
+
</Select>
|
|
102
|
+
<FormHelperText>{options.length === 0 ? noOptionsMessage : ''}</FormHelperText>
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
</FormControl>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Tooltip } from '@mui/material';
|
|
3
|
+
import InfoIcon from '@mui/icons-material/Info';
|
|
4
|
+
|
|
5
|
+
function LabelWithTooltip({ label, tooltip }) {
|
|
6
|
+
return (
|
|
7
|
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
8
|
+
<span>{label}</span>
|
|
9
|
+
<Tooltip title={<span style={{ fontSize: '1.4em' }}>{tooltip}</span>} arrow placement="right">
|
|
10
|
+
<InfoIcon fontSize="small" color="primary" sx={{ marginLeft: '0.25em' }} />
|
|
11
|
+
</Tooltip>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default React.memo(LabelWithTooltip);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PostRequestSelect from './PostRequestSelect';
|
|
3
|
+
|
|
4
|
+
describe('<PostRequestSelect />', () => {
|
|
5
|
+
it('works normally', () => {
|
|
6
|
+
// see: https://on.cypress.io/mounting-react
|
|
7
|
+
const setValueSpy = cy.spy().as('setValueSpy');
|
|
8
|
+
// Return a normal promise with a list of dummy objects with a delay of 2 seconds
|
|
9
|
+
const getOptions = (query) => new Promise((resolve) => setTimeout(() => resolve([{ name: `${query}-1` }, { name: `${query}-2` }]), 2000));
|
|
10
|
+
const getOptionLabel = (option) => option.name;
|
|
11
|
+
const isOptionEqualToValue = (option, value) => option.name === value.name;
|
|
12
|
+
|
|
13
|
+
cy.mount(<PostRequestSelect setValue={setValueSpy} getOptions={getOptions} getOptionLabel={getOptionLabel} isOptionEqualToValue={isOptionEqualToValue} textLabel="Test label" />);
|
|
14
|
+
cy.get('input').type('dummy');
|
|
15
|
+
// waiting message is shown
|
|
16
|
+
cy.contains('Loading options...').should('exist');
|
|
17
|
+
// wait for the options to appear
|
|
18
|
+
cy.contains('dummy-1').should('exist');
|
|
19
|
+
cy.contains('dummy-2').should('exist');
|
|
20
|
+
// Click on the first option
|
|
21
|
+
cy.contains('dummy-1').click();
|
|
22
|
+
// The spy should have been called with the first option
|
|
23
|
+
cy.get('@setValueSpy').should('have.been.calledWith', { name: 'dummy-1' });
|
|
24
|
+
});
|
|
25
|
+
it('shows an error message when the request fails', () => {
|
|
26
|
+
const setValueSpy = cy.spy().as('setValueSpy');
|
|
27
|
+
// Same, but 1 second delay
|
|
28
|
+
const getOptions = () => new Promise((_, reject) => setTimeout(() => reject(new Error('Network error')), 1000));
|
|
29
|
+
|
|
30
|
+
const getOptionLabel = (option) => option.name;
|
|
31
|
+
const isOptionEqualToValue = (option, value) => option.name === value.name;
|
|
32
|
+
|
|
33
|
+
cy.mount(<PostRequestSelect setValue={setValueSpy} getOptions={getOptions} getOptionLabel={getOptionLabel} isOptionEqualToValue={isOptionEqualToValue} textLabel="Test label" />);
|
|
34
|
+
cy.get('input').type('dummy');
|
|
35
|
+
// waiting message is shown
|
|
36
|
+
cy.contains('Loading options...').should('exist');
|
|
37
|
+
// wait for the error message to appear
|
|
38
|
+
cy.contains('Could not retrieve data').should('exist');
|
|
39
|
+
// Click on the retry button
|
|
40
|
+
cy.contains('Retry').click();
|
|
41
|
+
// waiting message is shown
|
|
42
|
+
cy.contains('Retrying...').should('exist');
|
|
43
|
+
// wait for the error message to appear
|
|
44
|
+
cy.contains('Could not retrieve data').should('exist');
|
|
45
|
+
});
|
|
46
|
+
it('does not filter options when disableFiltering is true', () => {
|
|
47
|
+
const setValueSpy = cy.spy().as('setValueSpy');
|
|
48
|
+
const getOptions = (query) => new Promise((resolve) => resolve([{ name: 'AAA-1' }, { name: 'AAA-2' }]));
|
|
49
|
+
const getOptionLabel = (option) => option.name;
|
|
50
|
+
const isOptionEqualToValue = (option, value) => option.name === value.name;
|
|
51
|
+
cy.mount(<PostRequestSelect setValue={setValueSpy} getOptions={getOptions} getOptionLabel={getOptionLabel} isOptionEqualToValue={isOptionEqualToValue} textLabel="Test label" disableFiltering />);
|
|
52
|
+
cy.get('input').type('BBB');
|
|
53
|
+
cy.contains('AAA-1').should('exist');
|
|
54
|
+
cy.contains('AAA-2').should('exist');
|
|
55
|
+
cy.get('input').clear();
|
|
56
|
+
cy.get('input').type('AAA');
|
|
57
|
+
cy.contains('AAA-1').should('exist');
|
|
58
|
+
cy.contains('AAA-2').should('exist');
|
|
59
|
+
|
|
60
|
+
cy.mount(<PostRequestSelect setValue={setValueSpy} getOptions={getOptions} getOptionLabel={getOptionLabel} isOptionEqualToValue={isOptionEqualToValue} textLabel="Test label" disableFiltering={false} />);
|
|
61
|
+
cy.get('input').clear();
|
|
62
|
+
cy.get('input').type('BBB');
|
|
63
|
+
cy.contains('AAA-1').should('not.exist');
|
|
64
|
+
cy.contains('AAA-2').should('not.exist');
|
|
65
|
+
cy.get('input').clear();
|
|
66
|
+
cy.get('input').type('AAA');
|
|
67
|
+
cy.contains('AAA-1').should('exist');
|
|
68
|
+
cy.contains('AAA-2').should('exist');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import Autocomplete from '@mui/material/Autocomplete';
|
|
3
|
+
import TextField from '@mui/material/TextField';
|
|
4
|
+
import { Alert, Button, FormControl } from '@mui/material';
|
|
5
|
+
|
|
6
|
+
export default function PostRequestSelect({ setValue, getOptions, getOptionLabel, isOptionEqualToValue, textLabel, disableFiltering = true, ...rest }) {
|
|
7
|
+
// The reason for disableFiltering is that we allow the server to filter the options,
|
|
8
|
+
// and whatever matches may not end up in the option label. For instance, when querying
|
|
9
|
+
// for species, we show the species name + id, but it may be that something else matches
|
|
10
|
+
// .e.g. "yeast" for "Saccharomyces cerevisiae - 4932"
|
|
11
|
+
const [options, setOptions] = React.useState([]);
|
|
12
|
+
const [connectAttempt, setConnectAttemp] = React.useState(0);
|
|
13
|
+
const [error, setError] = React.useState(false);
|
|
14
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
15
|
+
// user input state
|
|
16
|
+
const [userInput, setUserInput] = React.useState('');
|
|
17
|
+
const [noOptionsText, setNoOptionsText] = React.useState('');
|
|
18
|
+
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
async function fetchData() {
|
|
21
|
+
if (userInput.length >= 3) {
|
|
22
|
+
try {
|
|
23
|
+
setNoOptionsText('Loading options...');
|
|
24
|
+
const receivedOptions = await getOptions(userInput);
|
|
25
|
+
setOptions(receivedOptions);
|
|
26
|
+
setErrorMessage(null);
|
|
27
|
+
setError(false);
|
|
28
|
+
if (receivedOptions.length === 0) { setNoOptionsText('No results found'); }
|
|
29
|
+
} catch (e) {
|
|
30
|
+
setErrorMessage('Could not retrieve data');
|
|
31
|
+
setError(true);
|
|
32
|
+
}
|
|
33
|
+
} else { setNoOptionsText(''); setOptions([]); }
|
|
34
|
+
}
|
|
35
|
+
// Delay the fetch to avoid too many requests
|
|
36
|
+
const timeOutId = setTimeout(() => fetchData(), 500);
|
|
37
|
+
return () => clearTimeout(timeOutId);
|
|
38
|
+
}, [connectAttempt]);
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
// Reset the value when the component is re-rendered at the same position
|
|
42
|
+
// with different functions
|
|
43
|
+
setUserInput('');
|
|
44
|
+
setOptions([]);
|
|
45
|
+
setNoOptionsText('');
|
|
46
|
+
}, [setValue, getOptions, getOptionLabel, isOptionEqualToValue, textLabel]);
|
|
47
|
+
|
|
48
|
+
if (error) {
|
|
49
|
+
return (
|
|
50
|
+
<Alert
|
|
51
|
+
sx={{ alignItems: 'center' }}
|
|
52
|
+
severity="error"
|
|
53
|
+
action={(
|
|
54
|
+
<Button color="inherit" size="small" onClick={() => { setErrorMessage('Retrying...'); setConnectAttemp(connectAttempt + 1); }}>
|
|
55
|
+
Retry
|
|
56
|
+
</Button>
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{errorMessage}
|
|
60
|
+
</Alert>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<FormControl {...rest}>
|
|
66
|
+
<Autocomplete
|
|
67
|
+
onChange={(event, value) => { setValue(value); setUserInput(getOptionLabel(value)); }}
|
|
68
|
+
// Change options only when input changes (not when an option is picked)
|
|
69
|
+
onInputChange={(event, newInputValue, reason) => { if (reason === 'input') { setUserInput(newInputValue); setConnectAttemp(connectAttempt + 1); } }}
|
|
70
|
+
id="tags-standard"
|
|
71
|
+
options={options}
|
|
72
|
+
noOptionsText={noOptionsText || 'Type at least 3 characters to search'}
|
|
73
|
+
getOptionLabel={getOptionLabel}
|
|
74
|
+
isOptionEqualToValue={isOptionEqualToValue}
|
|
75
|
+
inputValue={userInput}
|
|
76
|
+
filterOptions={disableFiltering ? (x) => x : undefined}
|
|
77
|
+
renderInput={(params) => (
|
|
78
|
+
<TextField
|
|
79
|
+
{...params}
|
|
80
|
+
label={textLabel}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
</FormControl>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import RetryAlert from './RetryAlert';
|
|
3
|
+
|
|
4
|
+
function RequestStatusWrapper({ children, requestStatus, retry }) {
|
|
5
|
+
if (requestStatus.status === 'success') {
|
|
6
|
+
return children;
|
|
7
|
+
}
|
|
8
|
+
if (requestStatus.status === 'loading') {
|
|
9
|
+
return <div>Loading...</div>;
|
|
10
|
+
}
|
|
11
|
+
if (requestStatus.status === 'error') {
|
|
12
|
+
return <RetryAlert onRetry={retry} sx={{ margin: 'auto', width: '80%', my: 2 }}>{requestStatus.message}</RetryAlert>;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default RequestStatusWrapper;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Alert, Button } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
function RetryAlert({ onRetry, children, ...props }) {
|
|
5
|
+
return (
|
|
6
|
+
<Alert
|
|
7
|
+
severity="error"
|
|
8
|
+
action={(
|
|
9
|
+
<Button color="inherit" size="small" onClick={onRetry}>
|
|
10
|
+
Retry
|
|
11
|
+
</Button>
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
</Alert>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default RetryAlert;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Alert, Button, CircularProgress } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
function SubmitButtonBackendAPI({ requestStatus, children, ...other }) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Button fullWidth className="submit-backend-api" type="submit" variant="contained" style={{ height: '2.5em' }} {...other}>
|
|
8
|
+
{requestStatus.status !== 'loading' ? children : (<CircularProgress className="loading-progress" color="inherit" size="2em" />)}
|
|
9
|
+
</Button>
|
|
10
|
+
{requestStatus.status === 'error' && (<Alert sx={{ marginTop: '10px' }} severity="error">{requestStatus.message}</Alert>)}
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default SubmitButtonBackendAPI;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { batch, useDispatch, useSelector, useStore } from 'react-redux';
|
|
4
|
+
import useDatabase from '../../hooks/useDatabase';
|
|
5
|
+
import { cloningActions } from '@opencloning/store/cloning';
|
|
6
|
+
import { getSubState } from '@opencloning/utils/network';
|
|
7
|
+
import IntermediatesDisclaimer from './intermediates_disclaimer.svg';
|
|
8
|
+
|
|
9
|
+
function SubmitToDatabaseDialog({ id, dialogOpen, setDialogOpen, resourceType }) {
|
|
10
|
+
const dispatch = useDispatch();
|
|
11
|
+
const store = useStore();
|
|
12
|
+
const [submissionData, setSubmissionData] = React.useState(null);
|
|
13
|
+
const [errorMessage, setErrorMessage] = React.useState('');
|
|
14
|
+
const [disclaimerAccepted, setDisclaimerAccepted] = React.useState(false);
|
|
15
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
16
|
+
const database = useDatabase();
|
|
17
|
+
|
|
18
|
+
// Checks if there are parent sources that are not in the database
|
|
19
|
+
const hasUnsavedIntermediates = useSelector((state) => {
|
|
20
|
+
if (resourceType === 'primer') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const substate = getSubState(state, id, true);
|
|
24
|
+
const immediateParent = substate.sources.find((source) => source.id === id);
|
|
25
|
+
return substate.sources.some((source) => (source.id !== immediateParent?.id) && !source.database_id);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const handleClose = () => {
|
|
29
|
+
setDialogOpen(false);
|
|
30
|
+
setErrorMessage('');
|
|
31
|
+
setDisclaimerAccepted(false);
|
|
32
|
+
setIsSubmitting(false);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (hasUnsavedIntermediates && !disclaimerAccepted) {
|
|
36
|
+
return (
|
|
37
|
+
<Dialog open={dialogOpen} onClose={handleClose} sx={{ textAlign: 'center' }}>
|
|
38
|
+
<DialogTitle>Unsaved Intermediates</DialogTitle>
|
|
39
|
+
<DialogContent sx={{ fontSize: '1.2em' }}>
|
|
40
|
+
<p>There are intermediate sequences between the sequence being saved and its first ancestor in the database.</p>
|
|
41
|
+
<p>
|
|
42
|
+
Intermediate sequences will be stored in the history of the sequence you are saving, but not as separate entities
|
|
43
|
+
in the database.
|
|
44
|
+
</p>
|
|
45
|
+
<img style={{ marginTop: '10px' }} src={IntermediatesDisclaimer} alt="Intermediates Disclaimer" />
|
|
46
|
+
</DialogContent>
|
|
47
|
+
<DialogActions>
|
|
48
|
+
<Button color="success" onClick={() => setDisclaimerAccepted(true)}>I understand</Button>
|
|
49
|
+
<Button color="error" onClick={handleClose}>Cancel</Button>
|
|
50
|
+
</DialogActions>
|
|
51
|
+
</Dialog>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Dialog
|
|
57
|
+
open={dialogOpen}
|
|
58
|
+
onClose={handleClose}
|
|
59
|
+
PaperProps={{
|
|
60
|
+
component: 'form',
|
|
61
|
+
sx: { width: '40%' },
|
|
62
|
+
onSubmit: async (event) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
if (isSubmitting) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// This should never happen
|
|
68
|
+
if (!database.isSubmissionDataValid(submissionData)) {
|
|
69
|
+
setErrorMessage('Submission data is invalid');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
setIsSubmitting(true);
|
|
74
|
+
if (resourceType === 'primer') {
|
|
75
|
+
const oldPrimer = store.getState().cloning.primers.find((p) => p.id === id);
|
|
76
|
+
const primerDatabaseId = await database.submitPrimerToDatabase({ submissionData, primer: oldPrimer });
|
|
77
|
+
const newPrimer = { ...oldPrimer, database_id: primerDatabaseId };
|
|
78
|
+
batch(() => {
|
|
79
|
+
dispatch(cloningActions.editPrimer(newPrimer));
|
|
80
|
+
dispatch(cloningActions.addAlert({
|
|
81
|
+
message: 'Primer created successfully',
|
|
82
|
+
severity: 'success',
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
} else if (resourceType === 'sequence') {
|
|
86
|
+
const substate = getSubState(store.getState(), id, true);
|
|
87
|
+
let databaseId;
|
|
88
|
+
let primerMappings;
|
|
89
|
+
try {
|
|
90
|
+
({ databaseId, primerMappings } = await database.submitSequenceToDatabase({ submissionData, substate, id }));
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(error);
|
|
93
|
+
setErrorMessage(error.message);
|
|
94
|
+
setIsSubmitting(false);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
batch(() => {
|
|
98
|
+
primerMappings.forEach((mapping) => dispatch(cloningActions.addDatabaseIdToPrimer(mapping)));
|
|
99
|
+
dispatch(cloningActions.addDatabaseIdToSequence({ databaseId, id }));
|
|
100
|
+
dispatch(cloningActions.addAlert({
|
|
101
|
+
message: 'Sequence created successfully',
|
|
102
|
+
severity: 'success',
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(error);
|
|
108
|
+
setErrorMessage(error.message);
|
|
109
|
+
setIsSubmitting(false);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setIsSubmitting(false);
|
|
113
|
+
setDialogOpen(false);
|
|
114
|
+
setErrorMessage('');
|
|
115
|
+
},
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<DialogTitle>{`Save ${resourceType} to ${database.name}`}</DialogTitle>
|
|
119
|
+
<DialogContent>
|
|
120
|
+
<database.SubmitToDatabaseComponent id={id} submissionData={submissionData} setSubmissionData={setSubmissionData} resourceType={resourceType} />
|
|
121
|
+
{resourceType === 'sequence' && <database.PrimersNotInDatabaseComponent id={id} submissionData={submissionData} setSubmissionData={setSubmissionData} />}
|
|
122
|
+
{errorMessage && <Alert sx={{ marginTop: 2 }} severity="error">{errorMessage}</Alert>}
|
|
123
|
+
</DialogContent>
|
|
124
|
+
<DialogActions>
|
|
125
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
126
|
+
<Button type="submit" disabled={isSubmitting || submissionData === null || !database.isSubmissionDataValid(submissionData)}>Submit</Button>
|
|
127
|
+
</DialogActions>
|
|
128
|
+
</Dialog>
|
|
129
|
+
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default SubmitToDatabaseDialog;
|