@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,54 @@
1
+ import React from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import { Alert } from '@mui/material';
4
+ import { getSubState } from '@opencloning/utils/network';
5
+ import ELabFTWCategorySelect from './ELabFTWCategorySelect';
6
+
7
+ function PrimersNotInDatabaseComponent({ id, submissionData, setSubmissionData }) {
8
+ const primerCategoryId = submissionData?.primerCategoryId;
9
+ const primers = useSelector((state) => {
10
+ const subState = getSubState(state, id, true);
11
+ return subState.primers.filter((p) => !p.database_id);
12
+ });
13
+
14
+ if (primers.length === 0) return null;
15
+
16
+ return (
17
+ <Alert
18
+ severity={primerCategoryId ? 'success' : 'info'}
19
+ sx={{
20
+ marginTop: 2,
21
+ paddingY: 1,
22
+ width: '100%',
23
+ '& .MuiAlert-message': {
24
+ width: '100%',
25
+ },
26
+ }}
27
+ icon={false}
28
+ >
29
+ {!primerCategoryId && (
30
+ <>
31
+ <div>Do you want used primers to be saved to the database?</div>
32
+ <ul>
33
+ {primers.map((primer) => (
34
+ <li key={primer.id}>
35
+ {primer.name}
36
+ </li>
37
+ ))}
38
+ </ul>
39
+ </>
40
+ )}
41
+
42
+ <ELabFTWCategorySelect
43
+ setCategory={(c) => {
44
+ setSubmissionData((prev) => ({ ...prev, primerCategoryId: c ? c.id : null }));
45
+ }}
46
+ label="Save primers as"
47
+ fullWidth
48
+ />
49
+
50
+ </Alert>
51
+ );
52
+ }
53
+
54
+ export default PrimersNotInDatabaseComponent;
@@ -0,0 +1,185 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { configureStore } from '@reduxjs/toolkit';
4
+ import SubmitToDatabaseComponent from './SubmitToDatabaseComponent';
5
+ import { eLabFTWHttpClient } from './common';
6
+ import { mockSequences, mockPrimers, mockSources, mockTeselaJsonCache } from '../../../../../tests/mockNetworkData';
7
+ import { clearAutocompleteValue } from '../../../../../cypress/e2e/common_functions';
8
+
9
+ const PRIMER_CATEGORY_ID = 3;
10
+
11
+ // Mock initial state with both primers and sequences
12
+ const defaultState = {
13
+ sequences: mockSequences,
14
+ sources: mockSources,
15
+ primers: mockPrimers,
16
+ teselaJsonCache: mockTeselaJsonCache,
17
+ };
18
+
19
+ const createTestStore = (cloningState) => configureStore({
20
+ reducer: {
21
+ cloning: (state = cloningState) => state,
22
+ },
23
+ preloadedState: { cloning: cloningState },
24
+ });
25
+
26
+ describe('<SubmitToDatabaseComponent />', () => {
27
+ it('Primers: initializes with primer name and handles updates', () => {
28
+ cy.stub(eLabFTWHttpClient, 'get')
29
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
30
+ .resolves({
31
+ data: [
32
+ { id: PRIMER_CATEGORY_ID, title: 'Primers' },
33
+ { id: 2, title: 'Sequences' },
34
+ ],
35
+ }).withArgs('/api/v2/info').resolves({
36
+ data: {
37
+ elabftw_version_int: 50200,
38
+ },
39
+ });
40
+ const store = createTestStore(defaultState);
41
+ const setSubmissionDataSpy = cy.spy().as('setSubmissionDataSpy');
42
+
43
+ cy.mount(
44
+ <Provider store={store}>
45
+ <SubmitToDatabaseComponent
46
+ id={mockPrimers[0].id}
47
+ resourceType="primer"
48
+ setSubmissionData={setSubmissionDataSpy}
49
+ />
50
+ </Provider>,
51
+ );
52
+
53
+ // Should initialize with primer name
54
+ cy.get('input#resource_title').should('have.value', 'Primer1');
55
+
56
+ // Change title
57
+ cy.get('input#resource_title').clear();
58
+ cy.get('input#resource_title').type('Modified Primer');
59
+
60
+ // Select category
61
+ cy.get('input').last().click();
62
+ cy.get('li').contains('Primers').click();
63
+
64
+ // Should update submission data with both title and category
65
+ cy.get('@setSubmissionDataSpy').should((spy) => {
66
+ const updateFn = spy.lastCall.args[0];
67
+ const result = updateFn({ existingKey: 'value' });
68
+ expect(result).to.deep.equal({
69
+ existingKey: 'value',
70
+ categoryId: PRIMER_CATEGORY_ID,
71
+ title: 'Modified Primer',
72
+ });
73
+ });
74
+ // Clearing the field should set submission data to null
75
+ clearAutocompleteValue('Save primer as', 'div');
76
+ cy.get('@setSubmissionDataSpy').should((spy) => {
77
+ const result = spy.lastCall.args[0];
78
+ expect(result).equal(null);
79
+ });
80
+ });
81
+
82
+ it('Sequences: initializes with sequence name and handles updates', () => {
83
+ cy.stub(eLabFTWHttpClient, 'get')
84
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
85
+ .resolves({
86
+ data: [
87
+ { id: PRIMER_CATEGORY_ID, title: 'Primers' },
88
+ { id: 2, title: 'Sequences' },
89
+ ],
90
+ }).withArgs('/api/v2/info').resolves({
91
+ data: {
92
+ elabftw_version_int: 50200,
93
+ },
94
+ });
95
+ const store = createTestStore(defaultState);
96
+ const setSubmissionDataSpy = cy.spy().as('setSubmissionDataSpy');
97
+
98
+ cy.mount(
99
+ <Provider store={store}>
100
+ <SubmitToDatabaseComponent
101
+ id={mockSequences[0].id}
102
+ resourceType="sequence"
103
+ setSubmissionData={setSubmissionDataSpy}
104
+ />
105
+ </Provider>,
106
+ );
107
+
108
+ // Should initialize with primer name
109
+ cy.get('input#resource_title').should('have.value', 'Seq1');
110
+
111
+ // Change title
112
+ cy.get('input#resource_title').clear();
113
+ cy.get('input#resource_title').type('Modified Sequence');
114
+
115
+ // Select category
116
+ cy.get('input').last().click();
117
+ cy.get('li').contains('Sequences').click();
118
+
119
+ // Should update submission data with both title and category
120
+ cy.get('@setSubmissionDataSpy').should((spy) => {
121
+ const updateFn = spy.lastCall.args[0];
122
+ const result = updateFn({ existingKey: 'value' });
123
+ expect(result).to.deep.equal({
124
+ existingKey: 'value',
125
+ categoryId: 2,
126
+ title: 'Modified Sequence',
127
+ });
128
+ });
129
+ // Clearing the field should set submission data to null
130
+ clearAutocompleteValue('Save sequence as', 'div');
131
+ cy.get('@setSubmissionDataSpy').should((spy) => {
132
+ const result = spy.lastCall.args[0];
133
+ expect(result).equal(null);
134
+ });
135
+ });
136
+
137
+ it('handles API errors in category loading', () => {
138
+ const store = createTestStore(defaultState);
139
+ let firstCall = true;
140
+
141
+ // Stub API to fail first time
142
+ cy.stub(eLabFTWHttpClient, 'get')
143
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
144
+ .callsFake(() => {
145
+ if (firstCall) {
146
+ firstCall = false;
147
+ const error = new Error('Failed to fetch');
148
+ error.response = { status: 500 };
149
+ return Promise.reject(error);
150
+ }
151
+ return Promise.resolve({
152
+ data: [
153
+ { id: PRIMER_CATEGORY_ID, title: 'Primers' },
154
+ { id: 2, title: 'Sequences' },
155
+ ],
156
+ })
157
+ }).withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' }}).resolves({
158
+ data: {
159
+ elabftw_version_int: 50200,
160
+ },
161
+ });
162
+
163
+ cy.mount(
164
+ <Provider store={store}>
165
+ <SubmitToDatabaseComponent
166
+ id={mockPrimers[0].id}
167
+ resourceType="primer"
168
+ setSubmissionData={cy.spy()}
169
+ />
170
+ </Provider>,
171
+ );
172
+
173
+ // Should show error state
174
+ cy.get('.MuiAlert-colorError').should('exist');
175
+ cy.contains('Could not retrieve categories').should('exist');
176
+
177
+ // Click retry
178
+ cy.contains('Retry').click();
179
+
180
+ // Should show categories after retry
181
+ cy.get('input').last().click();
182
+ cy.get('li').contains('Primers').should('exist');
183
+ cy.get('li').contains('Sequences').should('exist');
184
+ });
185
+ });
@@ -0,0 +1,51 @@
1
+ import { FormControl, TextField } from '@mui/material';
2
+ import React from 'react';
3
+ import { useSelector } from 'react-redux';
4
+ import ELabFTWCategorySelect from './ELabFTWCategorySelect';
5
+
6
+ function SubmitToDatabaseComponent({ id, setSubmissionData, resourceType }) {
7
+ const name = useSelector((state) => {
8
+ if (resourceType === 'primer') {
9
+ return state.cloning.primers.find((p) => p.id === id).name;
10
+ }
11
+ return state.cloning.teselaJsonCache[id].name;
12
+ });
13
+ const [title, setTitle] = React.useState(name);
14
+ const [category, setCategory] = React.useState(null);
15
+
16
+ React.useEffect(() => {
17
+ setTitle(name);
18
+ }, [name]);
19
+
20
+ React.useEffect(() => {
21
+ if (category && title) {
22
+ // We do this not overwrite primerCategoryId, set from a different component
23
+ setSubmissionData((prev) => ({ ...prev, categoryId: category.id, title }));
24
+ } else {
25
+ setSubmissionData(null);
26
+ }
27
+ }, [category, title]);
28
+
29
+ return (
30
+ <>
31
+ <FormControl fullWidth sx={{ mb: 2 }}>
32
+ <TextField
33
+ autoFocus
34
+ required
35
+ id="resource_title"
36
+ label="Resource title"
37
+ variant="standard"
38
+ value={title}
39
+ onChange={(e) => setTitle(e.target.value)}
40
+ />
41
+ </FormControl>
42
+ <ELabFTWCategorySelect
43
+ fullWidth
44
+ label={`Save ${resourceType} as`}
45
+ setCategory={setCategory}
46
+ />
47
+ </>
48
+ );
49
+ }
50
+
51
+ export default SubmitToDatabaseComponent;
@@ -0,0 +1,26 @@
1
+ import axios from 'axios';
2
+ import { readApiKey, writeApiKey, baseUrl as envBaseUrl } from './envValues';
3
+
4
+ export const baseUrl = envBaseUrl;
5
+ export const readHeaders = readApiKey ? { Authorization: readApiKey } : {};
6
+ export const writeHeaders = writeApiKey ? { Authorization: writeApiKey } : {};
7
+
8
+ export const eLabFTWHttpClient = axios.create({
9
+ baseURL: baseUrl,
10
+ });
11
+
12
+ export const makeSequenceMetadata = (sequence) => JSON.stringify({
13
+ extra_fields: {
14
+ sequence: {
15
+ type: 'text',
16
+ value: sequence,
17
+ group_id: null,
18
+ },
19
+ },
20
+ });
21
+
22
+ export const getELabFTWVersion = async () => {
23
+ const url = `/api/v2/info`;
24
+ const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
25
+ return resp.data.elabftw_version_int;
26
+ };
@@ -0,0 +1,294 @@
1
+ import SaveIcon from '@mui/icons-material/Save';
2
+ import LinkIcon from '@mui/icons-material/Link';
3
+ import GetSequenceFileAndDatabaseIdComponent from './GetSequenceFileAndDatabaseIdComponent';
4
+ import SubmitToDatabaseComponent from './SubmitToDatabaseComponent';
5
+ import PrimersNotInDatabaseComponent from './PrimersNotInDatabaseComponent';
6
+ import GetPrimerComponent from './GetPrimerComponent';
7
+ import { eLabFTWHttpClient, writeHeaders, readHeaders, baseUrl, getELabFTWVersion } from './common';
8
+ import { getFileFromELabFTW, error2String } from './utils';
9
+ import LoadHistoryComponent from './LoadHistoryComponent';
10
+
11
+ async function deleteResource(resourceId) {
12
+ const url = `/api/v2/items/${resourceId}`;
13
+ // eLabFTW requires application/json for delete requests, axios seems to require a data field for it to work
14
+ const resp = await eLabFTWHttpClient.delete(url, { data: {}, headers: { ...writeHeaders, 'Content-Type': 'application/json' } });
15
+ return resp.data;
16
+ }
17
+
18
+ const linkToParent = async (childId, parentId) => {
19
+ await eLabFTWHttpClient.post(
20
+ `/api/v2/items/${childId}/items_links/${parentId}`,
21
+ {},
22
+ { headers: writeHeaders },
23
+ );
24
+ };
25
+
26
+ const createResource = async (categoryId) => {
27
+ const eLabFTWVersion = await getELabFTWVersion();
28
+ const categoryKey = eLabFTWVersion && eLabFTWVersion >= 50300 ? 'category' : 'category_id';
29
+ const createdItemResponse = await eLabFTWHttpClient.post(
30
+ '/api/v2/items',
31
+ {
32
+ [categoryKey]: categoryId,
33
+ },
34
+ { headers: writeHeaders },
35
+ );
36
+ return Number(createdItemResponse.headers.location.split('/').pop());
37
+ };
38
+
39
+ const patchResource = async (resourceId, title, metadata = undefined) => eLabFTWHttpClient.patch(
40
+ `/api/v2/items/${resourceId}`,
41
+ { title, ...(metadata !== undefined && { metadata }) },
42
+ { headers: writeHeaders },
43
+ );
44
+
45
+ async function submitPrimerToDatabase({ submissionData: { title, categoryId }, primer, linkedSequenceId = null }) {
46
+ let resourceId;
47
+ try {
48
+ resourceId = await createResource(categoryId);
49
+ } catch (e) {
50
+ console.error(e);
51
+ throw new Error(`Error creating primer: ${error2String(e)}`);
52
+ }
53
+ const metadata = JSON.stringify({ extra_fields: { sequence: { type: 'text', value: primer.sequence, group_id: null } } });
54
+ let stage;
55
+ try {
56
+ stage = 'naming primer';
57
+ await patchResource(resourceId, title, metadata);
58
+ if (linkedSequenceId) {
59
+ stage = 'linking to sequence';
60
+ await linkToParent(linkedSequenceId, resourceId);
61
+ }
62
+ } catch (e) {
63
+ console.error(e);
64
+ try {
65
+ await deleteResource(resourceId);
66
+ } catch (e2) {
67
+ console.error(e2);
68
+ throw new Error(`There was an error (${error2String(e2)}) while trying to delete primer with id ${resourceId} after an error ${stage}.`);
69
+ }
70
+ throw new Error(`Error ${stage}: ${error2String(e)}`);
71
+ }
72
+ return resourceId;
73
+ }
74
+
75
+ async function uploadTextFileToResource(resourceId, fileName, textContent, comment) {
76
+ const blob = new Blob([textContent], { type: 'text/plain' });
77
+ const formData = new FormData();
78
+ formData.append('file', blob, fileName);
79
+ formData.append('comment', comment);
80
+ const response = await eLabFTWHttpClient.post(`/api/v2/items/${resourceId}/uploads`, formData, { headers: writeHeaders });
81
+ return Number(response.headers.location.split('/').pop());
82
+ }
83
+
84
+ async function submitSequenceToDatabase({ submissionData: { title, categoryId, primerCategoryId }, substate, id }) {
85
+ /**
86
+ * Submit a sequence to eLabFTW database
87
+ * @param {Object} params - The parameters object
88
+ * @param {Object} params.submissionData - Data needed for submission
89
+ * @param {string} params.submissionData.title - Title of the sequence
90
+ * @param {number} params.submissionData.categoryId - Category ID in eLabFTW
91
+ * @param {Object} params.substate - The substate containing sequence data
92
+ * @param {Array} params.substate.sources - Array of source objects
93
+ * @param {Array} params.substate.primers - Array of primer objects
94
+ * @param {Array} params.substate.sequences - Array of sequence objects
95
+ * @param {string} params.id - ID of the sequence to submit
96
+ * @returns {Promise<Object>}
97
+ */
98
+
99
+ const { sources, primers, sequences, appInfo } = substate;
100
+ const { backendVersion, schemaVersion, frontendVersion } = appInfo;
101
+
102
+ const sequence2export = sequences.find((e) => e.id === id);
103
+ const parentSource = sources.find((s) => s.id === id);
104
+ if (parentSource.database_id) {
105
+ throw new Error('Sequence already has a database_id');
106
+ }
107
+ // Get ancestor sources that are database sources to link to the sequence
108
+ const parentDatabaseSources = sources.filter((source) => source.database_id);
109
+ const parentResourceIds = parentDatabaseSources.map((source) => source.database_id);
110
+ const primerIds = primers.map((p) => p.id);
111
+
112
+ // Link and/or add used primers
113
+ const newPrimersToSave = [];
114
+ const existingPrimersToLink = primers.filter((p) => primerIds.includes(p.id) && p.database_id).map((p) => p.database_id);
115
+ if (primerCategoryId) {
116
+ newPrimersToSave.push(...primers.filter((p) => primerIds.includes(p.id) && !p.database_id));
117
+ }
118
+
119
+ // Create and name the resource
120
+ let resourceId;
121
+ try {
122
+ resourceId = await createResource(categoryId);
123
+ } catch (e) {
124
+ console.error(e);
125
+ throw new Error(`Error creating resource: ${error2String(e)}`);
126
+ }
127
+ let stage;
128
+ let newPrimerDatabaseIds = [];
129
+ try {
130
+ // Patch the resource with the title
131
+ stage = 'setting resource title';
132
+ await patchResource(resourceId, title);
133
+
134
+ // Add the links to parent Resources
135
+ stage = 'linking to parent resources';
136
+ await Promise.all(parentResourceIds.map((parentId) => linkToParent(resourceId, parentId)));
137
+
138
+ // Add the links to the existing primers
139
+ stage = 'linking to existing primers';
140
+ await Promise.all(existingPrimersToLink.map((primerId) => linkToParent(resourceId, primerId)));
141
+
142
+ // Add the new primers to the database and link them to the resource
143
+ stage = 'submitting new primers';
144
+ newPrimerDatabaseIds = await Promise.all(newPrimersToSave.map((primer) => submitPrimerToDatabase({ submissionData: { title: primer.name, categoryId: primerCategoryId }, primer, linkedSequenceId: resourceId })));
145
+ const primerMappings = newPrimerDatabaseIds.map((databaseId, index) => ({ databaseId, localId: newPrimersToSave[index].id }));
146
+
147
+ // Deep-copy primers and update the primers with the database IDs before storing the history
148
+ const primersCopy = primers.map((p) => ({ ...p }));
149
+ primerMappings.forEach(({ databaseId: dbId, localId }) => {
150
+ primersCopy.find((p) => p.id === localId).database_id = dbId;
151
+ });
152
+
153
+ const cloningStrategy = {
154
+ sources,
155
+ primers: primersCopy,
156
+ sequences,
157
+ backend_version: backendVersion,
158
+ schema_version: schemaVersion,
159
+ frontend_version: frontendVersion,
160
+ };
161
+
162
+ // Add the sequence and history files to the resource
163
+ stage = 'uploading sequence file';
164
+ await uploadTextFileToResource(resourceId, `${title}.gb`, sequence2export.file_content, 'resource sequence - generated by OpenCloning');
165
+ stage = 'uploading history file';
166
+ await uploadTextFileToResource(resourceId, `${title}_history.json`, JSON.stringify(cloningStrategy), 'history file - generated by OpenCloning');
167
+ // Format output values
168
+ return { primerMappings, databaseId: resourceId };
169
+ } catch (e) {
170
+ console.error(e);
171
+ let primersDeleted = false;
172
+ try {
173
+ await Promise.all(newPrimerDatabaseIds.map(deleteResource));
174
+ primersDeleted = true;
175
+ await deleteResource(resourceId);
176
+ } catch (e2) {
177
+ console.error(e2);
178
+ if (primersDeleted) {
179
+ throw new Error(`There was an error (${error2String(e2)}) while trying to delete the sequence with id ${resourceId} after an error ${stage}.`);
180
+ }
181
+ throw new Error(`There was an error (${error2String(e2)}) while trying to delete newly created primers linked to sequence with id ${resourceId} after an error ${stage}.`);
182
+ }
183
+ throw new Error(`Error ${stage}: ${error2String(e)}`);
184
+ }
185
+ }
186
+
187
+ function isSubmissionDataValid(submissionData) {
188
+ // This function is necessary because you might be setting submissionData from multiple components
189
+ return Boolean(submissionData.title && submissionData.categoryId);
190
+ }
191
+
192
+ async function loadSequenceFromUrlParams(urlParams) {
193
+ const { item_id: itemId, file_id: fileId } = urlParams;
194
+
195
+ if (itemId && fileId) {
196
+ const url = `/api/v2/items/${itemId}/uploads/${fileId}`;
197
+ let fileInfo;
198
+ try {
199
+ const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
200
+ fileInfo = resp.data;
201
+ } catch (e) {
202
+ throw new Error(`${error2String(e)}`);
203
+ }
204
+
205
+ // getFileFromELabFTW already handles errors
206
+ const file = await getFileFromELabFTW(itemId, fileInfo);
207
+ return { file, databaseId: itemId };
208
+ }
209
+ return null;
210
+ }
211
+
212
+ async function getPrimer(databaseId) {
213
+ const url = `/api/v2/items/${databaseId}`;
214
+ try {
215
+ const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
216
+ resp.data.metadata = JSON.parse(resp.data.metadata);
217
+ return { name: resp.data.title, database_id: databaseId, sequence: resp.data.metadata.extra_fields.sequence?.value };
218
+ } catch (e) {
219
+ console.error(e);
220
+ if (e.code === 'ERR_NETWORK') {
221
+ throw new Error(`Error getting primer: ${error2String(e)}`);
222
+ }
223
+ throw new Error(`Error getting primer with id ${databaseId}, it might have been deleted or you can no longer access it`);
224
+ }
225
+ }
226
+
227
+ async function getSequenceName(databaseId) {
228
+ const url = `/api/v2/items/${databaseId}`;
229
+ try {
230
+ const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
231
+ return resp.data.title;
232
+ } catch (e) {
233
+ console.error(e);
234
+ if (e.code === 'ERR_NETWORK') {
235
+ throw new Error(`Error getting sequence name: ${error2String(e)}`);
236
+ }
237
+ throw new Error(`Error getting name of sequence with id ${databaseId}, it might have been deleted or you can no longer access it`);
238
+ }
239
+ }
240
+
241
+ async function getSequencingFiles(databaseId) {
242
+ // This function should return an array of objects:
243
+ // name: the name of the file
244
+ // getFile: an async function that returns the file content
245
+ const url = `/api/v2/items/${databaseId}/uploads`;
246
+ try {
247
+ const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
248
+ return resp.data.map((fileInfo) => ({
249
+ name: fileInfo.real_name,
250
+ getFile: async () => getFileFromELabFTW(databaseId, fileInfo),
251
+ }));
252
+ } catch (e) {
253
+ console.error(e);
254
+ throw new Error(`${error2String(e)}`);
255
+ }
256
+ }
257
+
258
+ export default {
259
+ // Name of the database interface
260
+ name: 'eLabFTW',
261
+ // Returns a link to the sequence in the database
262
+ getSequenceLink: (databaseId) => `${baseUrl}/database.php?mode=view&id=${databaseId}`,
263
+ // Returns a link to the primer in the database
264
+ getPrimerLink: (databaseId) => `${baseUrl}/database.php?mode=view&id=${databaseId}`,
265
+ // Component for selecting and loading sequence files from the database
266
+ GetSequenceFileAndDatabaseIdComponent,
267
+ // Component for selecting and loading primers from the database
268
+ GetPrimerComponent,
269
+ // Component for submitting resources to the database
270
+ SubmitToDatabaseComponent,
271
+ // Component for handling primers not yet in database
272
+ PrimersNotInDatabaseComponent,
273
+ // Function to submit a primer to the database
274
+ submitPrimerToDatabase,
275
+ // Function to submit a sequence and its history to the database
276
+ submitSequenceToDatabase,
277
+ // Function to validate submission data
278
+ isSubmissionDataValid,
279
+ // Icon displayed on the node corner to submit
280
+ SubmitIcon: SaveIcon,
281
+ // Icon displayed on the node corner for sequences in the database
282
+ DatabaseIcon: LinkIcon,
283
+ // OPTIONAL =======================================================================
284
+ // Component for loading history from the database (can be hook-like does not have to render anything)
285
+ LoadHistoryComponent,
286
+ // Function to load sequences from url parameters
287
+ loadSequenceFromUrlParams,
288
+ // Function to get the primer ({name, database_id, sequence}) from the database
289
+ getPrimer,
290
+ // Function to get the name of a sequence from the database
291
+ getSequenceName,
292
+ // Function to get the sequencing files from the database, see docs for what the return value should be
293
+ getSequencingFiles,
294
+ };