@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,86 @@
1
+ import React from 'react';
2
+ import ELabFTWCategorySelect from './ELabFTWCategorySelect';
3
+ import { eLabFTWHttpClient } from './common';
4
+
5
+ describe('<ELabFTWCategorySelect />', () => {
6
+ it('Allows to retry if the request fails', () => {
7
+ cy.stub(eLabFTWHttpClient, 'get').withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).as('eLabFTWHttpClientSpy');
8
+ cy.mount(<ELabFTWCategorySelect fullWidth />);
9
+ cy.get('@eLabFTWHttpClientSpy.all').should('have.callCount', 1);
10
+ cy.get('button').contains('Retry').click();
11
+ cy.get('@eLabFTWHttpClientSpy.all').should('have.callCount', 2);
12
+ });
13
+ it('shows the right options for eLabFTW version 50300', () => {
14
+ const setCategorySpy = cy.spy().as('setCategorySpy');
15
+ cy.stub(eLabFTWHttpClient, 'get').withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
16
+ data: {
17
+ elabftw_version_int: 50300,
18
+ },
19
+ }).withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
20
+ data: [
21
+ { id: 1, title: 'Category 1' },
22
+ { id: 2, title: 'Category 2' },
23
+ ],
24
+ });
25
+ cy.mount(<ELabFTWCategorySelect fullWidth setCategory={setCategorySpy} />);
26
+ cy.get('.MuiAutocomplete-root').click();
27
+ cy.get('li').contains('Category 1').should('exist');
28
+ cy.get('li').contains('Category 2').should('exist');
29
+ cy.get('li').contains('Category 1').click();
30
+ cy.get('@setCategorySpy').should('have.been.calledWith', { id: 1, title: 'Category 1' });
31
+ });
32
+ it('shows the right options', () => {
33
+ const setCategorySpy = cy.spy().as('setCategorySpy');
34
+ cy.stub(eLabFTWHttpClient, 'get')
35
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
36
+ data: [
37
+ { id: 1, title: 'Category 1' },
38
+ { id: 2, title: 'Category 2' },
39
+ ],
40
+ })
41
+ .withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
42
+ data: {
43
+ elabftw_version_int: 50200,
44
+ },
45
+ });
46
+ cy.mount(<ELabFTWCategorySelect fullWidth setCategory={setCategorySpy} />);
47
+ cy.get('.MuiAutocomplete-root').click();
48
+ cy.get('li').contains('Category 1').should('exist');
49
+ cy.get('li').contains('Category 2').should('exist');
50
+ cy.get('li').contains('Category 1').click();
51
+ cy.get('@setCategorySpy').should('have.been.calledWith', { id: 1, title: 'Category 1' });
52
+ });
53
+
54
+ it('shows empty options if no categories are found', () => {
55
+ cy.mount(<ELabFTWCategorySelect fullWidth />);
56
+ cy.stub(eLabFTWHttpClient, 'get')
57
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
58
+ data: [],
59
+ })
60
+ .withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
61
+ data: {
62
+ elabftw_version_int: 50200,
63
+ },
64
+ });
65
+ cy.get('.MuiAutocomplete-root').click();
66
+ cy.get('li').should('not.exist');
67
+ });
68
+
69
+ it('shows an error message if the request fails and can retry', () => {
70
+ cy.mount(<ELabFTWCategorySelect fullWidth />);
71
+ cy.stub(eLabFTWHttpClient, 'get')
72
+ .withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } })
73
+ .resolves({
74
+ data: {
75
+ elabftw_version_int: 50300,
76
+ },
77
+ }).as('eLabFTWHttpClientSpyInfo')
78
+ .withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
79
+ .rejects(new Error('Connection error')).as('eLabFTWHttpClientSpy');
80
+ cy.get('.MuiAlert-message').should('contain', 'Could not retrieve categories');
81
+ // Clicking the retry button makes the request again
82
+ cy.get('button').contains('Retry').click();
83
+ cy.get('@eLabFTWHttpClientSpy').should('have.callCount', 2);
84
+ cy.get('@eLabFTWHttpClientSpyInfo').should('have.callCount', 1);
85
+ });
86
+ });
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import GetRequestMultiSelect from '../form/GetRequestMultiSelect';
3
+ import { eLabFTWHttpClient, getELabFTWVersion, readHeaders } from './common';
4
+ import RequestStatusWrapper from '../form/RequestStatusWrapper';
5
+
6
+ function ELabFTWCategorySelect({ setCategory, label = 'Resource category', ...rest }) {
7
+ const [eLabFTWVersion, setELabFTWVersion] = React.useState(null);
8
+ const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
9
+ const [retry, setRetry] = React.useState(0);
10
+ React.useEffect(() => {
11
+ setRequestStatus({ status: 'loading' });
12
+ getELabFTWVersion().then(
13
+ (version) => {
14
+ setELabFTWVersion(version);
15
+ setRequestStatus({ status: 'success' });
16
+ }
17
+ ).catch(() => setRequestStatus({ status: 'error', message: 'Could not retrieve eLabFTW version' }));
18
+ }, [retry]);
19
+ const url = eLabFTWVersion && eLabFTWVersion >= 50300 ? '/api/v2/teams/current/resources_categories' : '/api/v2/items_types';
20
+ const getOptionsFromResponse = (data) => data;
21
+ const messages = { loadingMessage: 'retrieving categories', errorMessage: 'Could not retrieve categories from eLab' };
22
+ const onChange = (value) => setCategory(value);
23
+
24
+ return (
25
+ <RequestStatusWrapper requestStatus={requestStatus} retry={() => { setRetry(retry + 1); }}>
26
+ <GetRequestMultiSelect
27
+ getOptionsFromResponse={getOptionsFromResponse}
28
+ httpClient={eLabFTWHttpClient}
29
+ requestHeaders={readHeaders}
30
+ url={url}
31
+ requestParams={{ limit: 9999 }}
32
+ label={label}
33
+ messages={messages}
34
+ onChange={onChange}
35
+ getOptionLabel={(option) => (option === '' ? '' : option.title)}
36
+ multiple={false}
37
+ {...rest}
38
+ />
39
+ </RequestStatusWrapper>
40
+ );
41
+ }
42
+
43
+ export default ELabFTWCategorySelect;
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import ELabFTWFileSelect from './ELabFTWFileSelect';
3
+ import { eLabFTWHttpClient } from './common';
4
+
5
+ describe('<ELabFTWFileSelect />', () => {
6
+ it('shows the right options', () => {
7
+ const setFileInfoSpy = cy.spy().as('setFileInfoSpy');
8
+ // Stub the eLabFTWHttpClient similar to the pattern in eLabFTWInterface.test.js
9
+ cy.stub(eLabFTWHttpClient, 'get').withArgs('/api/v2/items/1', { headers: { Authorization: 'test-read-key' }, params: {} }).resolves({
10
+ data: {
11
+ uploads: [
12
+ { id: 1, real_name: 'file1.txt' },
13
+ { id: 2, real_name: 'file2.txt' },
14
+ ],
15
+ },
16
+ });
17
+ cy.mount(<ELabFTWFileSelect fullWidth itemId={1} setFileInfo={setFileInfoSpy} />);
18
+ cy.get('div.MuiSelect-select').click();
19
+ cy.get('li').contains('file1.txt').should('exist');
20
+ cy.get('li').contains('file2.txt').should('exist');
21
+ cy.get('li').contains('file1.txt').click();
22
+ cy.get('@setFileInfoSpy').should('have.been.calledWith', { id: 1, real_name: 'file1.txt' });
23
+ });
24
+ it('shows empty options if no files are found', () => {
25
+ cy.mount(<ELabFTWFileSelect fullWidth itemId={1} />);
26
+ cy.stub(eLabFTWHttpClient, 'get').withArgs('/api/v2/items/1', { headers: { Authorization: 'test-read-key' }, params: {} }).resolves({
27
+ data: {
28
+ uploads: [],
29
+ },
30
+ });
31
+ cy.get('div.MuiSelect-select').click();
32
+ cy.get('li').should('not.exist');
33
+ });
34
+
35
+ it('shows an error message if the request fails and can retry', () => {
36
+ cy.mount(<ELabFTWFileSelect fullWidth itemId={1} />);
37
+ cy.get('.MuiAlert-message').should('contain', 'Could not retrieve attachment');
38
+ // Clicking the retry button makes the request again
39
+ cy.spy(eLabFTWHttpClient, 'get').as('eLabFTWHttpClientSpy');
40
+ cy.get('button').contains('Retry').click();
41
+ cy.get('@eLabFTWHttpClientSpy').should('have.been.calledWith', '/api/v2/items/1', { headers: { Authorization: 'test-read-key' }, params: {} });
42
+ });
43
+ });
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import GetRequestMultiSelect from '../form/GetRequestMultiSelect';
3
+ import { eLabFTWHttpClient, readHeaders } from './common';
4
+
5
+ function ELabFTWFileSelect({ itemId, setFileInfo, ...rest }) {
6
+ const url = `/api/v2/items/${itemId}`;
7
+ const getOptionsFromResponse = ({ uploads }) => uploads;
8
+ const label = 'File with sequence';
9
+ const messages = { loadingMessage: 'retrieving attachments', errorMessage: 'Could not retrieve attachment from eLab' };
10
+ const onChange = (realName, options) => setFileInfo(options.find((option) => option.real_name === realName));
11
+
12
+ return (
13
+ <GetRequestMultiSelect
14
+ getOptionsFromResponse={getOptionsFromResponse}
15
+ httpClient={eLabFTWHttpClient}
16
+ requestHeaders={readHeaders}
17
+ url={url}
18
+ label={label}
19
+ messages={messages}
20
+ onChange={onChange}
21
+ getOptionLabel={(option) => (option === '' ? '' : option.real_name)}
22
+ multiple={false}
23
+ autoComplete={false}
24
+ {...rest}
25
+ />
26
+ );
27
+ }
28
+
29
+ export default ELabFTWFileSelect;
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import ELabFTWResourceSelect from './ELabFTWResourceSelect';
3
+ import { eLabFTWHttpClient } from './common';
4
+
5
+ describe('<ELabFTWResourceSelect />', () => {
6
+ it('shows the right options when searching', () => {
7
+ const setResourceSpy = cy.spy().as('setResourceSpy');
8
+ cy.stub(eLabFTWHttpClient, 'get')
9
+ .withArgs('/api/v2/items', {
10
+ headers: { Authorization: 'test-read-key' },
11
+ params: { cat: 1, extended: 'title:test', limit: 9999 },
12
+ })
13
+ .resolves({
14
+ data: [
15
+ { id: 1, title: 'Test Resource 1' },
16
+ { id: 2, title: 'Test Resource 2' },
17
+ ],
18
+ });
19
+
20
+ cy.mount(<ELabFTWResourceSelect fullWidth categoryId={1} setResource={setResourceSpy} />);
21
+
22
+ // Type in the search field
23
+ cy.get('.MuiAutocomplete-input').type('test');
24
+
25
+ // Check if options are displayed
26
+ cy.get('li').contains('Test Resource 1').should('exist');
27
+ cy.get('li').contains('Test Resource 2').should('exist');
28
+
29
+ // Select an option
30
+ cy.get('li').contains('Test Resource 1').click();
31
+ cy.get('@setResourceSpy').should('have.been.calledWith', { id: 1, title: 'Test Resource 1' });
32
+ });
33
+
34
+ it('shows empty options if no resources are found', () => {
35
+ cy.stub(eLabFTWHttpClient, 'get')
36
+ .withArgs('/api/v2/items', {
37
+ headers: { Authorization: 'test-read-key' },
38
+ params: { cat: 1, extended: 'title:nonexistent', limit: 9999 },
39
+ })
40
+ .resolves({
41
+ data: [],
42
+ });
43
+
44
+ cy.mount(<ELabFTWResourceSelect fullWidth categoryId={1} />);
45
+ cy.get('.MuiAutocomplete-input').type('nonexistent');
46
+ cy.get('li').should('not.exist');
47
+ });
48
+
49
+ it('handles API errors gracefully', () => {
50
+ let firstCall = true;
51
+ cy.stub(eLabFTWHttpClient, 'get')
52
+ .withArgs('/api/v2/items', {
53
+ headers: { Authorization: 'test-read-key' },
54
+ params: { cat: 1, extended: 'title:test', limit: 9999 },
55
+ })
56
+ .callsFake(() => {
57
+ if (firstCall) {
58
+ firstCall = false;
59
+ return Promise.reject(new Error('API Error'));
60
+ }
61
+ return Promise.resolve({ data: [
62
+ { id: 1, title: 'Test Resource 1' },
63
+ { id: 2, title: 'Test Resource 2' },
64
+ ] });
65
+ });
66
+
67
+ cy.mount(<ELabFTWResourceSelect fullWidth categoryId={1} />);
68
+ cy.get('.MuiAutocomplete-input').type('test');
69
+ // The component should handle the error gracefully
70
+ cy.get('.MuiAlert-message').should('contain', 'Could not retrieve data');
71
+ // Clicking the retry button makes the request again
72
+ cy.get('button').contains('Retry').click();
73
+ // Verify the second call was made
74
+ cy.get('div.MuiInputBase-root').click();
75
+ cy.get('li').contains('Test Resource 1').should('exist');
76
+ cy.get('li').contains('Test Resource 2').should('exist');
77
+ });
78
+
79
+ it('updates search results when categoryId changes', () => {
80
+ const getStub = cy.stub(eLabFTWHttpClient, 'get');
81
+
82
+ // First category results
83
+ getStub.withArgs('/api/v2/items', {
84
+ headers: { Authorization: 'test-read-key' },
85
+ params: { cat: 1, extended: 'title:test', limit: 9999 },
86
+ }).resolves({
87
+ data: [{ id: 1, title: 'Category 1 Resource' }],
88
+ });
89
+
90
+ // Second category results
91
+ getStub.withArgs('/api/v2/items', {
92
+ headers: { Authorization: 'test-read-key' },
93
+ params: { cat: 2, extended: 'title:test', limit: 9999 },
94
+ }).resolves({
95
+ data: [{ id: 2, title: 'Category 2 Resource' }],
96
+ });
97
+
98
+ cy.mount(<ELabFTWResourceSelect fullWidth categoryId={1} />);
99
+ cy.get('.MuiAutocomplete-input').type('test');
100
+ cy.get('li').contains('Category 1 Resource').should('exist');
101
+
102
+ // Change category and verify new results
103
+ cy.mount(<ELabFTWResourceSelect fullWidth categoryId={2} />);
104
+ cy.get('.MuiAutocomplete-input').type('test');
105
+ cy.get('li').contains('Category 2 Resource').should('exist');
106
+ });
107
+ });
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import PostRequestSelect from '../form/PostRequestSelect';
3
+ import { eLabFTWHttpClient, readHeaders } from './common';
4
+
5
+ function ELabFTWResourceSelect({ setResource, categoryId, ...rest }) {
6
+ const url = '/api/v2/items';
7
+
8
+ const resourcePostRequestSettings = React.useMemo(() => ({
9
+ setValue: setResource,
10
+ getOptions: async (userInput) => {
11
+ const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders, params: { cat: categoryId, extended: `title:${userInput}`, limit: 9999 } });
12
+ return resp.data;
13
+ },
14
+ getOptionLabel: (option) => (option ? option.title : ''),
15
+ isOptionEqualToValue: (option, value) => option?.id === value?.id,
16
+ textLabel: 'Resource',
17
+ }), [setResource, categoryId]);
18
+ return (
19
+ <PostRequestSelect {...resourcePostRequestSettings} {...rest} />
20
+ );
21
+ }
22
+
23
+ export default ELabFTWResourceSelect;
@@ -0,0 +1,261 @@
1
+ import React from 'react';
2
+ import GetPrimerComponent from './GetPrimerComponent';
3
+ import { eLabFTWHttpClient } from './common';
4
+
5
+ const PRIMER_CATEGORY_ID = 3;
6
+
7
+ describe('<GetPrimerComponent />', () => {
8
+ it('shows category select and then resource select after category is chosen', () => {
9
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
10
+ const setErrorSpy = cy.spy().as('setErrorSpy');
11
+
12
+ // Stub both endpoints in a single stub
13
+ cy.stub(eLabFTWHttpClient, 'get').callsFake((url) => {
14
+ if (url === '/api/v2/items_types') {
15
+ return Promise.resolve({
16
+ data: [
17
+ { id: PRIMER_CATEGORY_ID, title: 'Primers' },
18
+ { id: 2, title: 'Other Category' },
19
+ ],
20
+ });
21
+ }
22
+ return Promise.resolve({ data: [] });
23
+ });
24
+
25
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
26
+
27
+ // Initially, only category select should be visible
28
+ cy.get('.MuiAutocomplete-root').should('have.length', 1);
29
+
30
+ // Select a category
31
+ cy.get('.MuiAutocomplete-input').first().click();
32
+ cy.get('.MuiAutocomplete-input').first().type('Primers');
33
+ cy.get('li').contains('Primers').click();
34
+
35
+ // Now resource select should appear
36
+ cy.get('.MuiAutocomplete-root').should('have.length', 2);
37
+ });
38
+
39
+ it('successfully selects a primer with valid metadata', () => {
40
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
41
+ const setErrorSpy = cy.spy().as('setErrorSpy');
42
+
43
+ // Stub both endpoints in a single stub
44
+ cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
45
+ if (url === '/api/v2/items_types') {
46
+ return Promise.resolve({
47
+ data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
48
+ });
49
+ }
50
+ if (url === '/api/v2/items' && config?.params?.cat === PRIMER_CATEGORY_ID) {
51
+ return Promise.resolve({
52
+ data: [{
53
+ id: 1,
54
+ title: 'Test Primer',
55
+ metadata: JSON.stringify({
56
+ extra_fields: {
57
+ sequence: { value: 'ATCG' },
58
+ },
59
+ }),
60
+ }],
61
+ });
62
+ }
63
+ return Promise.resolve({ data: [] });
64
+ });
65
+
66
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
67
+
68
+ // Select category
69
+ cy.get('.MuiAutocomplete-input').first().click();
70
+ cy.get('.MuiAutocomplete-input').first().type('Primers');
71
+ cy.get('li').contains('Primers').click();
72
+
73
+ // Select primer
74
+ cy.get('.MuiAutocomplete-input').last().type('test');
75
+ cy.get('li').contains('Test Primer').click();
76
+
77
+ // Check if setPrimer was called with correct data
78
+ cy.get('@setPrimerSpy').should('have.been.calledWith', {
79
+ name: 'Test Primer',
80
+ sequence: 'ATCG',
81
+ database_id: 1,
82
+ });
83
+ cy.get('@setErrorSpy').should('not.have.been.called');
84
+ });
85
+
86
+ it('handles primer with invalid metadata', () => {
87
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
88
+ const setErrorSpy = cy.spy().as('setErrorSpy');
89
+
90
+ // Stub both endpoints in a single stub
91
+ cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
92
+ if (url === '/api/v2/items_types') {
93
+ return Promise.resolve({
94
+ data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
95
+ });
96
+ }
97
+ if (url === '/api/v2/items' && config?.params?.cat === PRIMER_CATEGORY_ID) {
98
+ return Promise.resolve({
99
+ data: [{
100
+ id: 1,
101
+ title: 'Invalid Primer',
102
+ metadata: JSON.stringify({
103
+ extra_fields: {
104
+ // Missing sequence field
105
+ },
106
+ }),
107
+ }],
108
+ });
109
+ }
110
+ if (url === '/api/v2/info') {
111
+ return Promise.resolve({
112
+ data: {
113
+ elabftw_version_int: 50200,
114
+ },
115
+ });
116
+ }
117
+ });
118
+
119
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
120
+
121
+ // Select category
122
+ cy.get('.MuiAutocomplete-input').first().click();
123
+ cy.get('.MuiAutocomplete-input').first().type('Primers');
124
+ cy.get('li').contains('Primers').click();
125
+
126
+ // Select primer
127
+ cy.get('.MuiAutocomplete-input').last().type('test');
128
+ cy.get('li').contains('Invalid Primer').click();
129
+
130
+ // Check if error was set and primer was cleared
131
+ cy.get('@setErrorSpy').should('have.been.calledWith', 'No sequence found in metadata');
132
+ cy.get('@setPrimerSpy').should('have.been.calledWith', null);
133
+ });
134
+
135
+ it('clears primer when category is cleared', () => {
136
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
137
+ const setErrorSpy = cy.spy().as('setErrorSpy');
138
+
139
+ cy.stub(eLabFTWHttpClient, 'get').callsFake((url) => {
140
+ if (url === '/api/v2/items_types') {
141
+ return Promise.resolve({
142
+ data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
143
+ });
144
+ }
145
+ if (url === '/api/v2/info') {
146
+ return Promise.resolve({
147
+ data: {
148
+ elabftw_version_int: 50200,
149
+ },
150
+ });
151
+ }
152
+ });
153
+
154
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
155
+
156
+ // Select category
157
+ cy.get('.MuiAutocomplete-input').first().click();
158
+ cy.get('.MuiAutocomplete-input').first().type('Primers');
159
+ cy.get('li').contains('Primers').click();
160
+
161
+ // Clear category
162
+ cy.get('.MuiAutocomplete-clearIndicator').first().click();
163
+
164
+ // Check if primer was cleared
165
+ cy.get('@setPrimerSpy').should('have.been.calledWith', null);
166
+ cy.get('.MuiAutocomplete-root').should('have.length', 1);
167
+ });
168
+
169
+ it('clears error when resource is cleared', () => {
170
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
171
+ const setErrorSpy = cy.spy().as('setErrorSpy');
172
+
173
+ // Stub both endpoints in a single stub
174
+ cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
175
+ if (url === '/api/v2/items_types') {
176
+ return Promise.resolve({
177
+ data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
178
+ });
179
+ }
180
+ if (url === '/api/v2/items' && config?.params?.cat === PRIMER_CATEGORY_ID) {
181
+ return Promise.resolve({
182
+ data: [{
183
+ id: 1,
184
+ title: 'Invalid Primer',
185
+ metadata: '{invalid json}',
186
+ }],
187
+ });
188
+ }
189
+ if (url === '/api/v2/info') {
190
+ return Promise.resolve({
191
+ data: {
192
+ elabftw_version_int: 50200,
193
+ },
194
+ });
195
+ }
196
+ });
197
+
198
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
199
+
200
+ // Select category
201
+ cy.get('.MuiAutocomplete-input').first().click();
202
+ cy.get('.MuiAutocomplete-input').first().type('Primers');
203
+ cy.get('li').contains('Primers').click();
204
+
205
+ // Select and then clear resource
206
+ cy.get('.MuiAutocomplete-input').last().type('test');
207
+ cy.get('li').contains('Invalid Primer').click();
208
+ cy.get('.MuiAutocomplete-clearIndicator').last().click();
209
+
210
+ // Check if error was cleared
211
+ cy.get('@setErrorSpy').should('have.been.calledWith', '');
212
+ cy.get('@setPrimerSpy').should('have.been.calledWith', null);
213
+ });
214
+ it('handles network errors', () => {
215
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
216
+ const setErrorSpy = cy.spy().as('setErrorSpy');
217
+
218
+ let firstCallCategory = true;
219
+ let firstCallPrimer = true;
220
+ cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
221
+ if (url === '/api/v2/items_types') {
222
+ if (firstCallCategory) {
223
+ firstCallCategory = false;
224
+ return Promise.reject(new Error('Network error'));
225
+ }
226
+ return Promise.resolve({
227
+ data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
228
+ });
229
+ }
230
+ if (url === '/api/v2/items' && config?.params?.cat === PRIMER_CATEGORY_ID) {
231
+ if (firstCallPrimer) {
232
+ firstCallPrimer = false;
233
+ return Promise.reject(new Error('Network error'));
234
+ }
235
+ return Promise.resolve({
236
+ data: [{ id: 1, title: 'Test Primer' }],
237
+ });
238
+ }
239
+ if (url === '/api/v2/info') {
240
+ return Promise.resolve({
241
+ data: {
242
+ elabftw_version_int: 50200,
243
+ },
244
+ });
245
+ }
246
+ });
247
+
248
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
249
+
250
+ cy.get('.MuiAlert-message').should('contain', 'Could not retrieve categories');
251
+ cy.get('button').contains('Retry').click();
252
+ cy.get('.MuiAutocomplete-input').first().click();
253
+ cy.get('.MuiAutocomplete-input').first().type('Primers');
254
+ cy.get('li').contains('Primers').click();
255
+ cy.get('.MuiAutocomplete-input').last().type('test');
256
+ cy.get('.MuiAlert-message').should('contain', 'Could not retrieve');
257
+ cy.get('button').contains('Retry').click();
258
+ cy.get('.MuiAutocomplete-root').eq(1).click();
259
+ cy.get('li').contains('Test Primer').click();
260
+ });
261
+ });
@@ -0,0 +1,55 @@
1
+ import React, { useCallback } from 'react';
2
+ import ELabFTWCategorySelect from './ELabFTWCategorySelect';
3
+ import ELabFTWResourceSelect from './ELabFTWResourceSelect';
4
+
5
+ function GetPrimerComponent({ primer, setPrimer, setError }) {
6
+ const [category, setCategory] = React.useState(null);
7
+
8
+ React.useEffect(() => {
9
+ if (category === null) {
10
+ setPrimer(null);
11
+ }
12
+ }, [category]);
13
+
14
+ const handleResourceSelect = useCallback(async (resource) => {
15
+ if (resource === null) {
16
+ setPrimer(null);
17
+ setError('');
18
+ return;
19
+ }
20
+ let sequence;
21
+ try {
22
+ sequence = JSON.parse(resource.metadata).extra_fields?.sequence?.value;
23
+ if (!sequence) {
24
+ setError('No sequence found in metadata');
25
+ setPrimer(null);
26
+ return;
27
+ }
28
+ } catch (e) {
29
+ setError('No sequence found in metadata');
30
+ setPrimer(null);
31
+ return;
32
+ }
33
+
34
+ setPrimer({ name: resource.title, sequence, database_id: resource.id });
35
+ }, [setPrimer, setError]);
36
+
37
+ return (
38
+ <>
39
+ <ELabFTWCategorySelect
40
+ setCategory={setCategory}
41
+ fullWidth
42
+ />
43
+
44
+ {category && (
45
+ <ELabFTWResourceSelect
46
+ setResource={handleResourceSelect}
47
+ categoryId={category.id}
48
+ fullWidth
49
+ />
50
+ )}
51
+ </>
52
+ );
53
+ }
54
+
55
+ export default GetPrimerComponent;