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