@opencloning/opencloning-elabftw 1.9.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # @opencloning/opencloning-elabftw
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`c8c46d4`](https://github.com/OpenCloning/OpenCloning_frontend/commit/c8c46d4dc36cb8741d1bd89816c7ca82b89996b3)]:
8
+ - @opencloning/ui@2.1.0
9
+ - @opencloning/utils@2.1.0
10
+
11
+ ## 2.0.0
12
+
13
+ ### Patch Changes
14
+
15
+ - [#778](https://github.com/OpenCloning/OpenCloning_frontend/pull/778) [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394) Thanks [@manulera](https://github.com/manulera)! - Only send the auth token to the backend, and rename the http client APIs.
16
+
17
+ - `@opencloning/utils`: `@opencloning/utils/httpClients` exports `getBackendHttpClient(backendUrl, { authenticated })`, a client that can only reach the backend (`allowAbsoluteUrls: false`) and is the only one carrying the auth token, and `getExternalHttpClient()` for whitelisted external urls. Replaces `@opencloning/utils/getHttpClient` (`getHttpClient`, `getAuthenticatedHttpClient`). `attachAuthInterceptors` refuses clients that could send the token elsewhere.
18
+ - `@opencloning/ui` (breaking): `useHttpClient` is renamed `useBackendHttpClient` and takes urls relative to `backendUrl`; `useBackendRoute` is removed; `useBackendAPI` is renamed `useSourceRequest` and `sendPostRequest` is renamed `sendSourceRequest`.
19
+ - `@opencloning/opencloningdb`, `@opencloning/opencloning-elabftw`: clients can no longer be pointed at other hosts.
20
+
21
+ - [#778](https://github.com/OpenCloning/OpenCloning_frontend/pull/778) [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394) Thanks [@manulera](https://github.com/manulera)! - Encode values interpolated into request URL paths to prevent client-side path traversal. A sample UID such as `../workspaces/1/users/5` could make a user's own request hit a different endpoint, e.g. "delete UID" removing a workspace member.
22
+
23
+ - `@opencloning/utils`: new `encodePathSegment` (`@opencloning/utils/urlUtils`), which applies `encodeURIComponent` and rejects `.` and `..`.
24
+ - `@opencloning/opencloningdb`: all path parameters in `endpoints.js` are encoded.
25
+ - `@opencloning/opencloning-elabftw`: eLabFTW item and upload paths are encoded.
26
+ - `@opencloning/ui`: the `example`, `key` and `template` URL parameters are encoded when loading examples and templates.
27
+
28
+ - Updated dependencies [[`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394), [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394), [`fe1d81c`](https://github.com/OpenCloning/OpenCloning_frontend/commit/fe1d81c6f4b85f6317a2a9e46cc68c571c79c394)]:
29
+ - @opencloning/utils@2.0.0
30
+ - @opencloning/ui@2.0.0
31
+
32
+ ## 1.10.0
33
+
34
+ ### Patch Changes
35
+
36
+ - [#776](https://github.com/OpenCloning/OpenCloning_frontend/pull/776) [`5f84aea`](https://github.com/OpenCloning/OpenCloning_frontend/commit/5f84aea004a24499a5634c17678696925ee780cf) Thanks [@manulera](https://github.com/manulera)! - Hotfix for issue where to load resources the template categories were shown instead
37
+
38
+ - Updated dependencies [[`f7f376e`](https://github.com/OpenCloning/OpenCloning_frontend/commit/f7f376ee351d9b2da52413700836dc55ba54b5ea)]:
39
+ - @opencloning/ui@1.10.0
40
+ - @opencloning/utils@1.10.0
41
+
42
+ ## 1.9.5
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [[`73363e7`](https://github.com/OpenCloning/OpenCloning_frontend/commit/73363e7a7f7e494e20a54bd87e5dba97f8a9197e)]:
47
+ - @opencloning/utils@1.9.5
48
+ - @opencloning/ui@1.9.5
49
+
50
+ ## 1.9.4
51
+
52
+ ### Patch Changes
53
+
54
+ - [#769](https://github.com/OpenCloning/OpenCloning_frontend/pull/769) [`105210b`](https://github.com/OpenCloning/OpenCloning_frontend/commit/105210b8149af71101f348967b3096636aeeea09) Thanks [@manulera](https://github.com/manulera)! - Fix request payloads. Drop support for eLabFTW < 5.3. Related to https://github.com/elabftw/elabftw/issues/7218.
55
+
56
+ - Updated dependencies []:
57
+ - @opencloning/ui@1.9.4
58
+ - @opencloning/utils@1.9.4
59
+
60
+ ## 1.9.3
61
+
62
+ ### Patch Changes
63
+
64
+ - Updated dependencies [[`df69a1b`](https://github.com/OpenCloning/OpenCloning_frontend/commit/df69a1bfb3b3128e93d89eef48a29f81a54ebb82)]:
65
+ - @opencloning/utils@1.9.3
66
+ - @opencloning/ui@1.9.3
67
+
3
68
  ## 1.9.2
4
69
 
5
70
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "license": "MIT",
3
3
  "name": "@opencloning/opencloning-elabftw",
4
- "version": "1.9.2",
4
+ "version": "2.1.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "exports": {
@@ -15,9 +15,9 @@
15
15
  "dependencies": {
16
16
  "@mui/icons-material": "^5.18.0",
17
17
  "@mui/material": "^5.18.0",
18
- "@opencloning/ui": "1.9.2",
19
- "@opencloning/utils": "1.9.2",
20
- "axios": "^1.16.0"
18
+ "@opencloning/ui": "2.1.0",
19
+ "@opencloning/utils": "2.1.0",
20
+ "axios": "^1.20.0"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "react": "^18.3.1",
@@ -5,34 +5,15 @@ import { eLabFTWHttpClient } from './common';
5
5
  describe('<ELabFTWCategorySelect />', () => {
6
6
  it('Allows to retry if the request fails', () => {
7
7
  cy.stub(eLabFTWHttpClient, 'get').withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).as('eLabFTWHttpClientSpy');
8
- cy.mount(<ELabFTWCategorySelect fullWidth />);
8
+ cy.mount(<ELabFTWCategorySelect mode="resource_type" fullWidth />);
9
9
  cy.get('@eLabFTWHttpClientSpy.all').should('have.callCount', 1);
10
10
  cy.get('button').contains('Retry').click();
11
11
  cy.get('@eLabFTWHttpClientSpy.all').should('have.callCount', 2);
12
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
13
  it('shows the right options', () => {
33
14
  const setCategorySpy = cy.spy().as('setCategorySpy');
34
15
  cy.stub(eLabFTWHttpClient, 'get')
35
- .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
16
+ .withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: {} }).resolves({
36
17
  data: [
37
18
  { id: 1, title: 'Category 1' },
38
19
  { id: 2, title: 'Category 2' },
@@ -43,7 +24,7 @@ describe('<ELabFTWCategorySelect />', () => {
43
24
  elabftw_version_int: 50200,
44
25
  },
45
26
  });
46
- cy.mount(<ELabFTWCategorySelect fullWidth setCategory={setCategorySpy} />);
27
+ cy.mount(<ELabFTWCategorySelect mode="resource_type" fullWidth setCategory={setCategorySpy} />);
47
28
  cy.get('.MuiAutocomplete-root').click();
48
29
  cy.get('li').contains('Category 1').should('exist');
49
30
  cy.get('li').contains('Category 2').should('exist');
@@ -51,10 +32,32 @@ describe('<ELabFTWCategorySelect />', () => {
51
32
  cy.get('@setCategorySpy').should('have.been.calledWith', { id: 1, title: 'Category 1' });
52
33
  });
53
34
 
35
+ it('uses the items_types endpoint in template mode', () => {
36
+ const setCategorySpy = cy.spy().as('setCategorySpy');
37
+ cy.stub(eLabFTWHttpClient, 'get')
38
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: {} }).resolves({
39
+ data: [
40
+ { id: 1, title: 'Template 1' },
41
+ { id: 2, title: 'Template 2' },
42
+ ],
43
+ })
44
+ .withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
45
+ data: {
46
+ elabftw_version_int: 50300,
47
+ },
48
+ });
49
+ cy.mount(<ELabFTWCategorySelect mode="template" fullWidth setCategory={setCategorySpy} />);
50
+ cy.get('.MuiAutocomplete-root').click();
51
+ cy.get('li').contains('Template 1').should('exist');
52
+ cy.get('li').contains('Template 2').should('exist');
53
+ cy.get('li').contains('Template 1').click();
54
+ cy.get('@setCategorySpy').should('have.been.calledWith', { id: 1, title: 'Template 1' });
55
+ });
56
+
54
57
  it('shows empty options if no categories are found', () => {
55
- cy.mount(<ELabFTWCategorySelect fullWidth />);
58
+ cy.mount(<ELabFTWCategorySelect mode="resource_type" fullWidth />);
56
59
  cy.stub(eLabFTWHttpClient, 'get')
57
- .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } }).resolves({
60
+ .withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: {} }).resolves({
58
61
  data: [],
59
62
  })
60
63
  .withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } }).resolves({
@@ -67,7 +70,7 @@ describe('<ELabFTWCategorySelect />', () => {
67
70
  });
68
71
 
69
72
  it('shows an error message if the request fails and can retry', () => {
70
- cy.mount(<ELabFTWCategorySelect fullWidth />);
73
+ cy.mount(<ELabFTWCategorySelect mode="resource_type" fullWidth />);
71
74
  cy.stub(eLabFTWHttpClient, 'get')
72
75
  .withArgs('/api/v2/info', { headers: { Authorization: 'test-read-key' } })
73
76
  .resolves({
@@ -75,7 +78,7 @@ describe('<ELabFTWCategorySelect />', () => {
75
78
  elabftw_version_int: 50300,
76
79
  },
77
80
  }).as('eLabFTWHttpClientSpyInfo')
78
- .withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
81
+ .withArgs('/api/v2/teams/current/resources_categories', { headers: { Authorization: 'test-read-key' }, params: {} })
79
82
  .rejects(new Error('Connection error')).as('eLabFTWHttpClientSpy');
80
83
  cy.get('.MuiAlert-message').should('contain', 'Could not retrieve categories');
81
84
  // Clicking the retry button makes the request again
@@ -3,7 +3,7 @@ import GetRequestMultiSelect from '@opencloning/ui/components/form/GetRequestMul
3
3
  import RequestStatusWrapper from '@opencloning/ui/components/form/RequestStatusWrapper';
4
4
  import { eLabFTWHttpClient, getELabFTWVersion, readHeaders } from './common';
5
5
 
6
- function ELabFTWCategorySelect({ setCategory, label = 'Resource category', ...rest }) {
6
+ function ELabFTWCategorySelect({ setCategory, mode, label = 'Resource category', ...rest }) {
7
7
  const [eLabFTWVersion, setELabFTWVersion] = React.useState(null);
8
8
  const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
9
9
  const [retry, setRetry] = React.useState(0);
@@ -16,7 +16,14 @@ function ELabFTWCategorySelect({ setCategory, label = 'Resource category', ...re
16
16
  }
17
17
  ).catch(() => setRequestStatus({ status: 'error', message: 'Could not retrieve eLabFTW version' }));
18
18
  }, [retry]);
19
- const url = eLabFTWVersion && eLabFTWVersion >= 50300 ? '/api/v2/teams/current/resources_categories' : '/api/v2/items_types';
19
+ let url;
20
+ if (mode === 'resource_type') {
21
+ url = '/api/v2/teams/current/resources_categories';
22
+ } else if (mode === 'template') {
23
+ url = '/api/v2/items_types';
24
+ } else {
25
+ throw new Error(`Invalid mode: ${mode}`);
26
+ }
20
27
  const getOptionsFromResponse = (data) => data;
21
28
  const messages = { loadingMessage: 'retrieving categories', errorMessage: 'Could not retrieve categories from eLab' };
22
29
  const onChange = (value) => setCategory(value);
@@ -28,7 +35,6 @@ function ELabFTWCategorySelect({ setCategory, label = 'Resource category', ...re
28
35
  httpClient={eLabFTWHttpClient}
29
36
  requestHeaders={readHeaders}
30
37
  url={url}
31
- requestParams={{ limit: 9999 }}
32
38
  label={label}
33
39
  messages={messages}
34
40
  onChange={onChange}
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
2
  import GetRequestMultiSelect from '@opencloning/ui/components/form/GetRequestMultiSelect';
3
3
  import { eLabFTWHttpClient, readHeaders } from './common';
4
+ import { encodePathSegment } from '@opencloning/utils/urlUtils';
4
5
 
5
6
  function ELabFTWFileSelect({ itemId, setFileInfo, ...rest }) {
6
- const url = `/api/v2/items/${itemId}`;
7
+ const url = `/api/v2/items/${encodePathSegment(itemId)}`;
7
8
  const getOptionsFromResponse = ({ uploads }) => uploads;
8
9
  const label = 'File with sequence';
9
10
  const messages = { loadingMessage: 'retrieving attachments', errorMessage: 'Could not retrieve attachment from eLab' };
@@ -11,7 +11,7 @@ describe('<GetPrimerComponent />', () => {
11
11
 
12
12
  // Stub both endpoints in a single stub
13
13
  cy.stub(eLabFTWHttpClient, 'get').callsFake((url) => {
14
- if (url === '/api/v2/items_types') {
14
+ if (url === '/api/v2/teams/current/resources_categories') {
15
15
  return Promise.resolve({
16
16
  data: [
17
17
  { id: PRIMER_CATEGORY_ID, title: 'Primers' },
@@ -42,7 +42,7 @@ describe('<GetPrimerComponent />', () => {
42
42
 
43
43
  // Stub both endpoints in a single stub
44
44
  cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
45
- if (url === '/api/v2/items_types') {
45
+ if (url === '/api/v2/teams/current/resources_categories') {
46
46
  return Promise.resolve({
47
47
  data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
48
48
  });
@@ -89,7 +89,7 @@ describe('<GetPrimerComponent />', () => {
89
89
 
90
90
  // Stub both endpoints in a single stub
91
91
  cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
92
- if (url === '/api/v2/items_types') {
92
+ if (url === '/api/v2/teams/current/resources_categories') {
93
93
  return Promise.resolve({
94
94
  data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
95
95
  });
@@ -137,7 +137,7 @@ describe('<GetPrimerComponent />', () => {
137
137
  const setErrorSpy = cy.spy().as('setErrorSpy');
138
138
 
139
139
  cy.stub(eLabFTWHttpClient, 'get').callsFake((url) => {
140
- if (url === '/api/v2/items_types') {
140
+ if (url === '/api/v2/teams/current/resources_categories') {
141
141
  return Promise.resolve({
142
142
  data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
143
143
  });
@@ -172,7 +172,7 @@ describe('<GetPrimerComponent />', () => {
172
172
 
173
173
  // Stub both endpoints in a single stub
174
174
  cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
175
- if (url === '/api/v2/items_types') {
175
+ if (url === '/api/v2/teams/current/resources_categories') {
176
176
  return Promise.resolve({
177
177
  data: [{ id: PRIMER_CATEGORY_ID, title: 'Primers' }],
178
178
  });
@@ -218,7 +218,7 @@ describe('<GetPrimerComponent />', () => {
218
218
  let firstCallCategory = true;
219
219
  let firstCallPrimer = true;
220
220
  cy.stub(eLabFTWHttpClient, 'get').callsFake((url, config) => {
221
- if (url === '/api/v2/items_types') {
221
+ if (url === '/api/v2/teams/current/resources_categories') {
222
222
  if (firstCallCategory) {
223
223
  firstCallCategory = false;
224
224
  return Promise.reject(new Error('Network error'));
@@ -37,6 +37,7 @@ function GetPrimerComponent({ primer, setPrimer, setError }) {
37
37
  return (
38
38
  <>
39
39
  <ELabFTWCategorySelect
40
+ mode="resource_type"
40
41
  setCategory={setCategory}
41
42
  fullWidth
42
43
  />
@@ -14,7 +14,7 @@ const OTHER_FILE_ID = newUniqueId();
14
14
  const TEST_FILE_CONTENT = 'test content';
15
15
 
16
16
  const commonStubHandler = (url, config) => {
17
- if (url === '/api/v2/items_types') {
17
+ if (url === '/api/v2/teams/current/resources_categories') {
18
18
  return Promise.resolve({
19
19
  data: [
20
20
  { id: SEQUENCE_CATEGORY_ID, title: 'Sequences' },
@@ -51,7 +51,7 @@ function GetSequenceFileAndDatabaseIdComponent({ setFile, setDatabaseId }) {
51
51
 
52
52
  return (
53
53
  <>
54
- <ELabFTWCategorySelect fullWidth setCategory={setCategory} className="elabftw-category-select" />
54
+ <ELabFTWCategorySelect mode="resource_type" fullWidth setCategory={setCategory} className="elabftw-category-select" />
55
55
  {category && <ELabFTWResourceSelect fullWidth setResource={setResource} categoryId={category.id} className="elabftw-resource-select" />}
56
56
  {resource && <ELabFTWFileSelect fullWidth setFileInfo={setFileInfo} itemId={resource.id} className="elabftw-file-select" />}
57
57
  {fileLoadError && <RetryAlert severity="error" onRetry={() => setRetry((prev) => prev + 1)}>{fileLoadError}</RetryAlert>}
@@ -3,9 +3,10 @@ import React from 'react';
3
3
  import { eLabFTWHttpClient, readHeaders } from './common';
4
4
  import RetryAlert from '@opencloning/ui/components/form/RetryAlert';
5
5
  import { getFileFromELabFTW } from './utils';
6
+ import { encodePathSegment } from '@opencloning/utils/urlUtils';
6
7
 
7
8
  function LoadHistoryComponent({ databaseId, loadDatabaseFile }) {
8
- const url = `/api/v2/items/${databaseId}`;
9
+ const url = `/api/v2/items/${encodePathSegment(databaseId)}`;
9
10
  const [error, setError] = React.useState(null);
10
11
  const [loading, setLoading] = React.useState(false);
11
12
  const [retry, setRetry] = React.useState(0);
@@ -107,7 +107,7 @@ describe('<PrimersNotInDatabaseComponent />', () => {
107
107
  it('shows error when update fails', () => {
108
108
  let firstCall = true;
109
109
  cy.stub(eLabFTWHttpClient, 'get')
110
- .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
110
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: {} })
111
111
  .callsFake((url, config) => {
112
112
  if (url !== '/api/v2/items_types' || config.headers?.Authorization !== 'test-read-key') {
113
113
  throw new Error('Unexpected call to get method with these parameters');
@@ -40,6 +40,7 @@ function PrimersNotInDatabaseComponent({ id, submissionData, setSubmissionData }
40
40
  )}
41
41
 
42
42
  <ELabFTWCategorySelect
43
+ mode="template"
43
44
  setCategory={(c) => {
44
45
  setSubmissionData((prev) => ({ ...prev, primerCategoryId: c ? c.id : null }));
45
46
  }}
@@ -26,7 +26,7 @@ const createTestStore = (cloningState) => configureStore({
26
26
  describe('<SubmitToDatabaseComponent />', () => {
27
27
  it('Primers: initializes with primer name and handles updates', () => {
28
28
  cy.stub(eLabFTWHttpClient, 'get')
29
- .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
29
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: {} })
30
30
  .resolves({
31
31
  data: [
32
32
  { id: PRIMER_CATEGORY_ID, title: 'Primers' },
@@ -81,7 +81,7 @@ describe('<SubmitToDatabaseComponent />', () => {
81
81
 
82
82
  it('Sequences: initializes with sequence name and handles updates', () => {
83
83
  cy.stub(eLabFTWHttpClient, 'get')
84
- .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
84
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: {} })
85
85
  .resolves({
86
86
  data: [
87
87
  { id: PRIMER_CATEGORY_ID, title: 'Primers' },
@@ -140,7 +140,7 @@ describe('<SubmitToDatabaseComponent />', () => {
140
140
 
141
141
  // Stub API to fail first time
142
142
  cy.stub(eLabFTWHttpClient, 'get')
143
- .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: { limit: 9999 } })
143
+ .withArgs('/api/v2/items_types', { headers: { Authorization: 'test-read-key' }, params: {} })
144
144
  .callsFake(() => {
145
145
  if (firstCall) {
146
146
  firstCall = false;
@@ -40,6 +40,7 @@ function SubmitToDatabaseComponent({ id, setSubmissionData, resourceType }) {
40
40
  />
41
41
  </FormControl>
42
42
  <ELabFTWCategorySelect
43
+ mode="template"
43
44
  fullWidth
44
45
  label={`Save ${resourceType} as`}
45
46
  setCategory={setCategory}
package/src/common.js CHANGED
@@ -7,6 +7,8 @@ export const writeHeaders = writeApiKey ? { Authorization: writeApiKey } : {};
7
7
 
8
8
  export const eLabFTWHttpClient = axios.create({
9
9
  baseURL: baseUrl,
10
+ // Every url is appended to baseURL, so requests (and the API key) only reach eLabFTW
11
+ allowAbsoluteUrls: false,
10
12
  });
11
13
 
12
14
  export const makeSequenceMetadata = (sequence) => JSON.stringify({
@@ -6,9 +6,10 @@ import GetPrimerComponent from './GetPrimerComponent';
6
6
  import { eLabFTWHttpClient, writeHeaders, readHeaders, baseUrl, getELabFTWVersion } from './common';
7
7
  import { getFileFromELabFTW, error2String } from './utils';
8
8
  import LoadHistoryComponent from './LoadHistoryComponent';
9
+ import { encodePathSegment } from '@opencloning/utils/urlUtils';
9
10
 
10
11
  async function deleteResource(resourceId) {
11
- const url = `/api/v2/items/${resourceId}`;
12
+ const url = `/api/v2/items/${encodePathSegment(resourceId)}`;
12
13
  // eLabFTW requires application/json for delete requests, axios seems to require a data field for it to work
13
14
  const resp = await eLabFTWHttpClient.delete(url, { data: {}, headers: { ...writeHeaders, 'Content-Type': 'application/json' } });
14
15
  return resp.data;
@@ -16,7 +17,7 @@ async function deleteResource(resourceId) {
16
17
 
17
18
  const linkToParent = async (childId, parentId) => {
18
19
  await eLabFTWHttpClient.post(
19
- `/api/v2/items/${childId}/items_links/${parentId}`,
20
+ `/api/v2/items/${encodePathSegment(childId)}/items_links/${encodePathSegment(parentId)}`,
20
21
  {},
21
22
  { headers: writeHeaders },
22
23
  );
@@ -24,13 +25,7 @@ const linkToParent = async (childId, parentId) => {
24
25
 
25
26
  const createResource = async (categoryId) => {
26
27
  const eLabFTWVersion = await getELabFTWVersion();
27
- let categoryKey
28
- // 0 is for development (nightly image)
29
- if (eLabFTWVersion >= 50300 || eLabFTWVersion === 0) {
30
- categoryKey = 'template';
31
- } else {
32
- categoryKey = 'category_id';
33
- }
28
+ const categoryKey = 'template';
34
29
  const createdItemResponse = await eLabFTWHttpClient.post(
35
30
  '/api/v2/items',
36
31
  {
@@ -42,7 +37,7 @@ const createResource = async (categoryId) => {
42
37
  };
43
38
 
44
39
  const patchResource = async (resourceId, title, metadata = undefined) => eLabFTWHttpClient.patch(
45
- `/api/v2/items/${resourceId}`,
40
+ `/api/v2/items/${encodePathSegment(resourceId)}`,
46
41
  { title, ...(metadata !== undefined && { metadata }) },
47
42
  { headers: writeHeaders },
48
43
  );
@@ -82,7 +77,7 @@ async function uploadTextFileToResource(resourceId, fileName, textContent, comme
82
77
  const formData = new FormData();
83
78
  formData.append('file', blob, fileName);
84
79
  formData.append('comment', comment);
85
- const response = await eLabFTWHttpClient.post(`/api/v2/items/${resourceId}/uploads`, formData, { headers: writeHeaders });
80
+ const response = await eLabFTWHttpClient.post(`/api/v2/items/${encodePathSegment(resourceId)}/uploads`, formData, { headers: writeHeaders });
86
81
  return Number(response.headers.location.split('/').pop());
87
82
  }
88
83
 
@@ -198,7 +193,7 @@ async function loadSequenceFromUrlParams(urlParams) {
198
193
  const { item_id: itemId, file_id: fileId } = urlParams;
199
194
 
200
195
  if (itemId && fileId) {
201
- const url = `/api/v2/items/${itemId}/uploads/${fileId}`;
196
+ const url = `/api/v2/items/${encodePathSegment(itemId)}/uploads/${encodePathSegment(fileId)}`;
202
197
  let fileInfo;
203
198
  try {
204
199
  const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
@@ -215,7 +210,7 @@ async function loadSequenceFromUrlParams(urlParams) {
215
210
  }
216
211
 
217
212
  async function getPrimer(databaseId) {
218
- const url = `/api/v2/items/${databaseId}`;
213
+ const url = `/api/v2/items/${encodePathSegment(databaseId)}`;
219
214
  try {
220
215
  const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
221
216
  resp.data.metadata = JSON.parse(resp.data.metadata);
@@ -230,7 +225,7 @@ async function getPrimer(databaseId) {
230
225
  }
231
226
 
232
227
  async function getSequenceName(databaseId) {
233
- const url = `/api/v2/items/${databaseId}`;
228
+ const url = `/api/v2/items/${encodePathSegment(databaseId)}`;
234
229
  try {
235
230
  const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
236
231
  return resp.data.title;
@@ -247,7 +242,7 @@ async function getSequencingFiles(databaseId) {
247
242
  // This function should return an array of objects:
248
243
  // name: the name of the file
249
244
  // getFile: an async function that returns the file content
250
- const url = `/api/v2/items/${databaseId}/uploads`;
245
+ const url = `/api/v2/items/${encodePathSegment(databaseId)}/uploads`;
251
246
  try {
252
247
  const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders });
253
248
  return resp.data.map((fileInfo) => ({
@@ -135,7 +135,10 @@ async function getScenario(method, url, data, config) {
135
135
  expect(nextScenario).toBeDefined();
136
136
  expect({ method: nextScenario.method, url: nextScenario.url, config: nextScenario.config }).toEqual({ method, url, config });
137
137
 
138
- if (nextScenario.data instanceof FormData && data instanceof FormData) {
138
+ if (nextScenario.data instanceof FormData || data instanceof FormData) {
139
+ expect(data).toBeInstanceOf(FormData);
140
+ expect(nextScenario.data).toBeInstanceOf(FormData);
141
+
139
142
  const scenarioComment = nextScenario.data.get('comment');
140
143
  const requestComment = data.get('comment');
141
144
  expect(scenarioComment).toBe(requestComment);
@@ -152,6 +155,8 @@ async function getScenario(method, url, data, config) {
152
155
  } else if (scenarioFileText !== requestFileText) {
153
156
  expect(scenarioFileText).toBe(requestFileText);
154
157
  }
158
+ } else {
159
+ expect(data).toEqual(nextScenario.data);
155
160
  }
156
161
  return nextScenario;
157
162
  }
@@ -212,8 +217,7 @@ const mockCreateResource = {
212
217
  method: 'post',
213
218
  url: '/api/v2/items',
214
219
  data: {
215
- category_id: MAIN_RESOURCE_CATEGORY_ID,
216
- tags: [],
220
+ template: MAIN_RESOURCE_CATEGORY_ID,
217
221
  },
218
222
  config: { headers: {} },
219
223
  response: { headers: { location: `/api/v2/items/${MAIN_RESOURCE_DATABASE_ID}` } },
@@ -686,8 +690,7 @@ const submissionPayloadWithAncestors = {
686
690
  const mockCreateSecondaryResource = {
687
691
  ...mockCreateResource,
688
692
  data: {
689
- tags: [],
690
- category_id: SECONDARY_RESOURCE_CATEGORY_ID,
693
+ template: SECONDARY_RESOURCE_CATEGORY_ID,
691
694
  },
692
695
  response: { headers: { location: `/api/v2/items/${SECONDARY_RESOURCE_DATABASE_ID1}` } },
693
696
  };
package/src/utils.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // so including them in eLabFTWInterface.js would make circular dependencies.
3
3
  // They are not included in common.js to be able to mock eLabFTWHttpClient in tests.
4
4
  import { eLabFTWHttpClient, readHeaders } from './common';
5
+ import { encodePathSegment } from '@opencloning/utils/urlUtils';
5
6
 
6
7
  export function error2String(error) {
7
8
  if (error.code === 'ERR_NETWORK') { return 'Network error: Cannot connect to eLabFTW'; }
@@ -18,7 +19,7 @@ export function error2String(error) {
18
19
  }
19
20
 
20
21
  export const getFileFromELabFTW = async (itemId, fileInfo) => {
21
- const url = `/api/v2/items/${itemId}/uploads/${fileInfo.id}?format=binary`;
22
+ const url = `/api/v2/items/${encodePathSegment(itemId)}/uploads/${encodePathSegment(fileInfo.id)}?format=binary`;
22
23
  try {
23
24
  const resp = await eLabFTWHttpClient.get(url, { headers: readHeaders, responseType: 'blob' });
24
25
  // Convert blob to file