@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.
Files changed (207) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +37 -0
  3. package/src/components/AppAlerts.jsx +19 -0
  4. package/src/components/CloningHistory.jsx +40 -0
  5. package/src/components/DataModelDisplayer.jsx +30 -0
  6. package/src/components/DescriptionEditor.jsx +68 -0
  7. package/src/components/DownloadCloningStrategyDialog.jsx +84 -0
  8. package/src/components/DownloadSequenceFileDialog.jsx +90 -0
  9. package/src/components/DragAndDropCloningHistoryWrapper.jsx +39 -0
  10. package/src/components/DraggableDialogPaper.jsx +16 -0
  11. package/src/components/EditSequenceNameDialog.jsx +90 -0
  12. package/src/components/ExternalServicesStatusCheck.jsx +92 -0
  13. package/src/components/HistoryLoadedDialog.jsx +49 -0
  14. package/src/components/LoadCloningHistoryWrapper.jsx +166 -0
  15. package/src/components/MainSequenceCheckBox.jsx +83 -0
  16. package/src/components/MainSequenceEditor.jsx +165 -0
  17. package/src/components/NetworkNode.jsx +159 -0
  18. package/src/components/NetworkTree.css +127 -0
  19. package/src/components/ObjectTable.jsx +24 -0
  20. package/src/components/OpenCloning.jsx +102 -0
  21. package/src/components/OverhangsDisplay.jsx +25 -0
  22. package/src/components/SequenceEditor.jsx +120 -0
  23. package/src/components/SequenceTab.jsx +14 -0
  24. package/src/components/TemplateSequence.jsx +38 -0
  25. package/src/components/annotation/PlannotateAnnotationReport.jsx +33 -0
  26. package/src/components/annotation/useUpdateAnnotationInMainSequence.js +39 -0
  27. package/src/components/assembler/AssemblePartWidget.jsx +252 -0
  28. package/src/components/assembler/Assembler.jsx +273 -0
  29. package/src/components/assembler/AssemblerPart.jsx +99 -0
  30. package/src/components/assembler/StopIcon.jsx +34 -0
  31. package/src/components/assembler/assembler_data2.json +50 -0
  32. package/src/components/assembler/assembly_component.module.css +81 -0
  33. package/src/components/assembler/moclo.json +110 -0
  34. package/src/components/assembler/sbol_visual_glyphs/LICENSE.html +21 -0
  35. package/src/components/assembler/sbol_visual_glyphs/assembly-scar.svg +63 -0
  36. package/src/components/assembler/sbol_visual_glyphs/cds-stop.svg +85 -0
  37. package/src/components/assembler/sbol_visual_glyphs/cds.svg +60 -0
  38. package/src/components/assembler/sbol_visual_glyphs/chromosomal-locus.svg +78 -0
  39. package/src/components/assembler/sbol_visual_glyphs/engineered-region.svg +56 -0
  40. package/src/components/assembler/sbol_visual_glyphs/five-prime-sticky-restriction-site.svg +56 -0
  41. package/src/components/assembler/sbol_visual_glyphs/origin-of-replication.svg +57 -0
  42. package/src/components/assembler/sbol_visual_glyphs/primer-binding-site.svg +59 -0
  43. package/src/components/assembler/sbol_visual_glyphs/promoter.svg +60 -0
  44. package/src/components/assembler/sbol_visual_glyphs/ribosome-entry-site.svg +56 -0
  45. package/src/components/assembler/sbol_visual_glyphs/specific-recombination-site.svg +59 -0
  46. package/src/components/assembler/sbol_visual_glyphs/terminator.svg +60 -0
  47. package/src/components/assembler/sbol_visual_glyphs/three-prime-sticky-restriction-site.svg +56 -0
  48. package/src/components/assembler/sbol_visual_glyphs.js +36 -0
  49. package/src/components/assembler/useAssembler.js +71 -0
  50. package/src/components/dummy/DummyInterface.js +41 -0
  51. package/src/components/dummy/GetSequenceFileAndDatabaseIdComponent.jsx +59 -0
  52. package/src/components/eLabFTW/ELabFTWCategorySelect.cy.jsx +86 -0
  53. package/src/components/eLabFTW/ELabFTWCategorySelect.jsx +43 -0
  54. package/src/components/eLabFTW/ELabFTWFileSelect.cy.jsx +43 -0
  55. package/src/components/eLabFTW/ELabFTWFileSelect.jsx +29 -0
  56. package/src/components/eLabFTW/ELabFTWResourceSelect.cy.jsx +107 -0
  57. package/src/components/eLabFTW/ELabFTWResourceSelect.jsx +23 -0
  58. package/src/components/eLabFTW/GetPrimerComponent.cy.jsx +261 -0
  59. package/src/components/eLabFTW/GetPrimerComponent.jsx +55 -0
  60. package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.cy.jsx +184 -0
  61. package/src/components/eLabFTW/GetSequenceFileAndDatabaseIdComponent.jsx +62 -0
  62. package/src/components/eLabFTW/LoadHistoryComponent.cy.jsx +235 -0
  63. package/src/components/eLabFTW/LoadHistoryComponent.jsx +51 -0
  64. package/src/components/eLabFTW/PrimersNotInDatabaseComponent.cy.jsx +159 -0
  65. package/src/components/eLabFTW/PrimersNotInDatabaseComponent.jsx +54 -0
  66. package/src/components/eLabFTW/SubmitToDatabaseComponent.cy.jsx +185 -0
  67. package/src/components/eLabFTW/SubmitToDatabaseComponent.jsx +51 -0
  68. package/src/components/eLabFTW/common.js +26 -0
  69. package/src/components/eLabFTW/eLabFTWInterface.js +294 -0
  70. package/src/components/eLabFTW/eLabFTWInterface.test.js +839 -0
  71. package/src/components/eLabFTW/envValues.js +7 -0
  72. package/src/components/eLabFTW/utils.js +39 -0
  73. package/src/components/form/CustomFormHelperText.jsx +10 -0
  74. package/src/components/form/EnzymeMultiSelect.cy.jsx +61 -0
  75. package/src/components/form/EnzymeMultiSelect.jsx +34 -0
  76. package/src/components/form/GetRequestMultiSelect.jsx +107 -0
  77. package/src/components/form/LabelWithTooltip.jsx +16 -0
  78. package/src/components/form/PostRequestSelect.cy.jsx +70 -0
  79. package/src/components/form/PostRequestSelect.jsx +86 -0
  80. package/src/components/form/RequestStatusWrapper.jsx +17 -0
  81. package/src/components/form/RetryAlert.jsx +20 -0
  82. package/src/components/form/ServerErrorMessage.jsx +10 -0
  83. package/src/components/form/SubmitButtonBackendAPI.jsx +15 -0
  84. package/src/components/form/SubmitToDatabaseDialog.jsx +133 -0
  85. package/src/components/form/TextFieldValidate.jsx +67 -0
  86. package/src/components/form/ValidatedTextField.jsx +33 -0
  87. package/src/components/form/intermediates_disclaimer.svg +181 -0
  88. package/src/components/navigation/ButtonWithMenu.jsx +43 -0
  89. package/src/components/navigation/CustomTab.jsx +14 -0
  90. package/src/components/navigation/FeedbackDialog.jsx +34 -0
  91. package/src/components/navigation/GithubCornerRight.jsx +29 -0
  92. package/src/components/navigation/MainAppBar.css +26 -0
  93. package/src/components/navigation/MainAppBar.jsx +205 -0
  94. package/src/components/navigation/SelectExampleDialog.jsx +69 -0
  95. package/src/components/navigation/SelectTemplateDialog.jsx +107 -0
  96. package/src/components/navigation/TabPanel.jsx +28 -0
  97. package/src/components/navigation/VersionDialog.jsx +33 -0
  98. package/src/components/primers/CreatePrimerFromSequenceForm.jsx +42 -0
  99. package/src/components/primers/DownloadPrimersButton.jsx +104 -0
  100. package/src/components/primers/PrimerForm.css +14 -0
  101. package/src/components/primers/PrimerForm.jsx +107 -0
  102. package/src/components/primers/PrimerList.css +46 -0
  103. package/src/components/primers/PrimerList.cy.jsx +95 -0
  104. package/src/components/primers/PrimerList.jsx +126 -0
  105. package/src/components/primers/PrimerTableRow.cy.jsx +57 -0
  106. package/src/components/primers/PrimerTableRow.jsx +84 -0
  107. package/src/components/primers/SelectPrimerForm.jsx +66 -0
  108. package/src/components/primers/import_primers/ImportPrimersButton.jsx +101 -0
  109. package/src/components/primers/import_primers/ImportPrimersTable.jsx +51 -0
  110. package/src/components/primers/import_primers/PrimerDatabaseImportForm.jsx +107 -0
  111. package/src/components/primers/import_primers/styles.css +60 -0
  112. package/src/components/primers/primer_design/SequenceTabComponents/CollapsableLabel.jsx +31 -0
  113. package/src/components/primers/primer_design/SequenceTabComponents/GatewayRoiSelect.jsx +164 -0
  114. package/src/components/primers/primer_design/SequenceTabComponents/OrientationPicker.jsx +37 -0
  115. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +369 -0
  116. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignEBIC.jsx +24 -0
  117. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignForm.jsx +29 -0
  118. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGatewayBP.jsx +36 -0
  119. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +26 -0
  120. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignHomologousRecombination.jsx +32 -0
  121. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignRestriction.jsx +25 -0
  122. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +25 -0
  123. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignStepper.jsx +53 -0
  124. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesigner.jsx +80 -0
  125. package/src/components/primers/primer_design/SequenceTabComponents/PrimerResultForm.jsx +49 -0
  126. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSettingsForm.jsx +68 -0
  127. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +84 -0
  128. package/src/components/primers/primer_design/SequenceTabComponents/RestrictionSpacerForm.jsx +85 -0
  129. package/src/components/primers/primer_design/SequenceTabComponents/StepNavigation.jsx +48 -0
  130. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx +216 -0
  131. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelResults.jsx +42 -0
  132. package/src/components/primers/primer_design/SequenceTabComponents/TabPanelSelectRoi.jsx +61 -0
  133. package/src/components/primers/primer_design/SequenceTabComponents/TabPannelSettings.jsx +59 -0
  134. package/src/components/primers/primer_design/SequenceTabComponents/primerDesignMinimalValues.json +5 -0
  135. package/src/components/primers/primer_design/SequenceTabComponents/useEBICPrimerDesignSettings.js +31 -0
  136. package/src/components/primers/primer_design/SequenceTabComponents/useEnzymePrimerDesignSettings.js +57 -0
  137. package/src/components/primers/primer_design/SequenceTabComponents/useGatewayPrimerDesignSettings.js +18 -0
  138. package/src/components/primers/primer_design/SequenceTabComponents/usePrimerDesignSettings.js +31 -0
  139. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGatewayBP.jsx +88 -0
  140. package/src/components/primers/primer_design/SourceComponents/PrimerDesignGibsonAssembly.jsx +65 -0
  141. package/src/components/primers/primer_design/SourceComponents/PrimerDesignHomologousRecombination.jsx +84 -0
  142. package/src/components/primers/primer_design/SourceComponents/PrimerDesignSourceForm.jsx +74 -0
  143. package/src/components/primers/primer_design/common/NoAttPSitesError.jsx +31 -0
  144. package/src/components/primers/primer_details/PCRTable.cy.jsx +51 -0
  145. package/src/components/primers/primer_details/PCRTable.jsx +35 -0
  146. package/src/components/primers/primer_details/Primer3Figure.jsx +25 -0
  147. package/src/components/primers/primer_details/PrimerDetailsTds.jsx +39 -0
  148. package/src/components/primers/primer_details/PrimerInfoIcon.cy.jsx +137 -0
  149. package/src/components/primers/primer_details/PrimerInfoIcon.jsx +132 -0
  150. package/src/components/primers/primer_details/TableSection.jsx +17 -0
  151. package/src/components/primers/primer_details/primerDetailsFormatting.js +3 -0
  152. package/src/components/primers/primer_details/useMultiplePrimerDetails.js +29 -0
  153. package/src/components/primers/primer_details/usePCRDetails.js +47 -0
  154. package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +49 -0
  155. package/src/components/primers/primer_details/useSinglePrimerSequenceDetails.js +25 -0
  156. package/src/components/primers/primersToTabularFile.js +49 -0
  157. package/src/components/primers/primersToTabularFile.test.js +108 -0
  158. package/src/components/settings/SettingsTab.cy.jsx +267 -0
  159. package/src/components/settings/SettingsTab.jsx +170 -0
  160. package/src/components/sources/AssemblyPlanDisplayer.cy.jsx +22 -0
  161. package/src/components/sources/AssemblyPlanDisplayer.jsx +27 -0
  162. package/src/components/sources/CollectionSource.jsx +97 -0
  163. package/src/components/sources/FinishedSource.jsx +397 -0
  164. package/src/components/sources/KnownSourceErrors.jsx +50 -0
  165. package/src/components/sources/MultipleInputsSelector.jsx +63 -0
  166. package/src/components/sources/MultipleOutputsSelector.jsx +63 -0
  167. package/src/components/sources/NewSourceBox.jsx +37 -0
  168. package/src/components/sources/PCRUnitForm.jsx +102 -0
  169. package/src/components/sources/SingleInputSelector.jsx +36 -0
  170. package/src/components/sources/Source.jsx +125 -0
  171. package/src/components/sources/SourceAnnotation.jsx +44 -0
  172. package/src/components/sources/SourceAssembly.jsx +201 -0
  173. package/src/components/sources/SourceBox.css +18 -0
  174. package/src/components/sources/SourceBox.jsx +60 -0
  175. package/src/components/sources/SourceCopySequence.jsx +38 -0
  176. package/src/components/sources/SourceDatabase.jsx +28 -0
  177. package/src/components/sources/SourceFile.jsx +188 -0
  178. package/src/components/sources/SourceGenomeRegion.cy.jsx +131 -0
  179. package/src/components/sources/SourceGenomeRegion.jsx +486 -0
  180. package/src/components/sources/SourceHomologousRecombination.jsx +125 -0
  181. package/src/components/sources/SourceKnownGenomeRegion.jsx +60 -0
  182. package/src/components/sources/SourceManuallyTyped.jsx +116 -0
  183. package/src/components/sources/SourcePCRorHybridization.jsx +165 -0
  184. package/src/components/sources/SourcePolymeraseExtension.jsx +44 -0
  185. package/src/components/sources/SourceRepositoryId.jsx +409 -0
  186. package/src/components/sources/SourceRestriction.jsx +41 -0
  187. package/src/components/sources/SourceReverseComplement.jsx +33 -0
  188. package/src/components/sources/SourceTypeSelector.jsx +94 -0
  189. package/src/components/sources/SubSequenceDisplayer.jsx +70 -0
  190. package/src/components/sources/VerifyDeleteDialog.jsx +23 -0
  191. package/src/components/sources/repositoryMetadata.js +14 -0
  192. package/src/components/verification/LoadFromDatabaseButton.jsx +90 -0
  193. package/src/components/verification/SequencingFileRow.jsx +34 -0
  194. package/src/components/verification/VerificationFileDialog.cy.jsx +176 -0
  195. package/src/components/verification/VerificationFileDialog.jsx +248 -0
  196. package/src/config/defaultMainEditorProps.js +44 -0
  197. package/src/hooks/useAlerts.js +16 -0
  198. package/src/hooks/useBackendAPI.js +51 -0
  199. package/src/hooks/useBackendRoute.js +22 -0
  200. package/src/hooks/useDatabase.js +18 -0
  201. package/src/hooks/useDragAndDropFile.js +31 -0
  202. package/src/hooks/useGatewaySites.js +40 -0
  203. package/src/hooks/useHttpClient.js +12 -0
  204. package/src/hooks/useLoadDatabaseFile.js +108 -0
  205. package/src/hooks/useStoreEditor.js +101 -0
  206. package/src/hooks/useValidateState.js +43 -0
  207. 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
+ }