@opencloning/ui 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 +86 -0
- package/package.json +5 -6
- package/src/components/CloningSpeedDial.jsx +3 -5
- package/src/components/EditSequenceNameDialog.jsx +3 -5
- package/src/components/ExternalServicesStatusCheck.jsx +3 -5
- package/src/components/LoadCloningHistoryWrapper.jsx +5 -7
- package/src/components/assembler/Assembler.cy.jsx +184 -0
- package/src/components/assembler/Assembler.jsx +57 -11
- package/src/components/assembler/ExistingSyntaxDialog.jsx +2 -2
- package/src/components/assembler/UploadPlasmidsButton.cy.jsx +88 -6
- package/src/components/assembler/UploadPlasmidsButton.jsx +137 -73
- package/src/components/assembler/assembler_utils.js +25 -12
- package/src/components/assembler/assembler_utils.test.js +40 -1
- package/src/components/assembler/useAssembler.js +24 -8
- package/src/components/assembler/usePlasmidsLogic.js +20 -2
- package/src/components/domesticate/DomesticateDialog.jsx +4 -5
- package/src/components/dummy/DummyInterface.js +6 -0
- package/src/components/form/EnzymeMultiSelect.jsx +3 -5
- package/src/components/navigation/MainAppBar.jsx +4 -2
- package/src/components/navigation/SelectTemplateDialog.jsx +4 -2
- package/src/components/primers/primer_design/SequenceTabComponents/hooks/useDesignPrimers.js +4 -6
- package/src/components/primers/primer_details/usePrimerDetailsEndpoints.js +4 -6
- package/src/components/sources/CollectionSource.jsx +2 -2
- package/src/components/sources/Source.jsx +32 -32
- package/src/components/sources/SourceAnnotation.jsx +2 -2
- package/src/components/sources/SourceAssembly.jsx +7 -7
- package/src/components/sources/SourceDatabase.jsx +2 -2
- package/src/components/sources/SourceFile.jsx +2 -2
- package/src/components/sources/SourceGenomeRegion.cy.jsx +46 -0
- package/src/components/sources/SourceGenomeRegion.jsx +29 -15
- package/src/components/sources/SourceHomologousRecombination.jsx +3 -3
- package/src/components/sources/SourceKnownGenomeRegion.jsx +3 -3
- package/src/components/sources/SourceManuallyTyped.jsx +2 -2
- package/src/components/sources/SourcePCRorHybridization.jsx +3 -3
- package/src/components/sources/SourcePolymeraseExtension.jsx +2 -2
- package/src/components/sources/SourceRepositoryId.jsx +4 -4
- package/src/components/sources/SourceRestriction.jsx +2 -2
- package/src/components/sources/SourceReverseComplement.jsx +2 -2
- package/src/components/sources/SourceServerStaticFile.jsx +3 -3
- package/src/components/verification/VerificationFileDialog.cy.jsx +17 -21
- package/src/components/verification/VerificationFileDialog.jsx +12 -11
- package/src/hooks/index.js +1 -2
- package/src/hooks/useBackendHttpClient.js +17 -0
- package/src/hooks/useCloningHistoryLoader.js +5 -7
- package/src/hooks/useGatewaySites.js +3 -5
- package/src/hooks/useInitializeApp.js +3 -4
- package/src/hooks/useLoadDatabaseFile.js +2 -2
- package/src/hooks/useSequencingAlignment.js +4 -6
- package/src/hooks/useServerStaticFiles.js +2 -2
- package/src/hooks/{useBackendAPI.js → useSourceRequest.js} +6 -8
- package/src/hooks/useStoreEditor.js +2 -1
- package/src/hooks/useUrlParamsLoader.js +8 -5
- package/src/hooks/useValidateState.js +4 -6
- package/src/version.js +1 -1
- package/src/hooks/useBackendRoute.js +0 -23
- package/src/hooks/useHttpClient.js +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# @opencloning/ui
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#786](https://github.com/OpenCloning/OpenCloning_frontend/pull/786) [`c8c46d4`](https://github.com/OpenCloning/OpenCloning_frontend/commit/c8c46d4dc36cb8741d1bd89816c7ca82b89996b3) Thanks [@manulera](https://github.com/manulera)! - Assembler: load plasmids from the database and upload the assemblies to it.
|
|
8
|
+
|
|
9
|
+
- `@opencloning/ui`:
|
|
10
|
+
- The Assembler's plasmid buttons are grouped into a "Load Plasmids" menu (from files, from the local server, from the database).
|
|
11
|
+
- When the database interface provides `GetSequencesInBulkComponent`, plasmids can be loaded by tag or picked, and are used as `DatabaseSource`s. In that case the default plasmids of a syntax are not loaded.
|
|
12
|
+
- When it provides `SubmitCloningStrategiesInBulkComponent`, a new "Upload to DB" button uploads the assemblies with their history, as in the bulk upload of sequences with history.
|
|
13
|
+
- Both are new optional entries of the database interface (see `DummyInterface`).
|
|
14
|
+
- `@opencloning/opencloningdb`:
|
|
15
|
+
- Implements `GetSequencesInBulkComponent` and `SubmitCloningStrategiesInBulkComponent`.
|
|
16
|
+
- Exports `TagMultiSelect` (with `allowCreate` and `onCreateError`), `BulkUploadModal`, `BulkUploadPreview` and `CommonTableRow`, moved from the `opencloningdb` app.
|
|
17
|
+
- `opencloningdb`:
|
|
18
|
+
- The "Upload sequences with history" button uses `SubmitCloningStrategiesInBulkComponent`. Its validation, submission and conflict messages are now shown in the dialog instead of as alerts.
|
|
19
|
+
- The tag selects no longer drop excluded tags from the shared tags cache.
|
|
20
|
+
- `syntax-builder`: the dev server runs on port 3000 and listens on `127.0.0.1`, like the other apps.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies []:
|
|
25
|
+
- @opencloning/store@2.1.0
|
|
26
|
+
- @opencloning/utils@2.1.0
|
|
27
|
+
|
|
28
|
+
## 2.0.0
|
|
29
|
+
|
|
30
|
+
### Major Changes
|
|
31
|
+
|
|
32
|
+
- [#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.
|
|
33
|
+
|
|
34
|
+
- `@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.
|
|
35
|
+
- `@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`.
|
|
36
|
+
- `@opencloning/opencloningdb`, `@opencloning/opencloning-elabftw`: clients can no longer be pointed at other hosts.
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- [#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.
|
|
41
|
+
|
|
42
|
+
- `@opencloning/utils`: new `encodePathSegment` (`@opencloning/utils/urlUtils`), which applies `encodeURIComponent` and rejects `.` and `..`.
|
|
43
|
+
- `@opencloning/opencloningdb`: all path parameters in `endpoints.js` are encoded.
|
|
44
|
+
- `@opencloning/opencloning-elabftw`: eLabFTW item and upload paths are encoded.
|
|
45
|
+
- `@opencloning/ui`: the `example`, `key` and `template` URL parameters are encoded when loading examples and templates.
|
|
46
|
+
|
|
47
|
+
- 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)]:
|
|
48
|
+
- @opencloning/utils@2.0.0
|
|
49
|
+
- @opencloning/store@2.0.0
|
|
50
|
+
|
|
51
|
+
## 1.10.0
|
|
52
|
+
|
|
53
|
+
### Minor Changes
|
|
54
|
+
|
|
55
|
+
- [#773](https://github.com/OpenCloning/OpenCloning_frontend/pull/773) [`f7f376e`](https://github.com/OpenCloning/OpenCloning_frontend/commit/f7f376ee351d9b2da52413700836dc55ba54b5ea) Thanks [@manulera](https://github.com/manulera)! - Handle failed NCBI request to `dataset_report` in `SourceGenomeRegion`.
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies []:
|
|
60
|
+
- @opencloning/store@1.10.0
|
|
61
|
+
- @opencloning/utils@1.10.0
|
|
62
|
+
|
|
63
|
+
## 1.9.5
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- Updated dependencies [[`73363e7`](https://github.com/OpenCloning/OpenCloning_frontend/commit/73363e7a7f7e494e20a54bd87e5dba97f8a9197e)]:
|
|
68
|
+
- @opencloning/utils@1.9.5
|
|
69
|
+
- @opencloning/store@1.9.5
|
|
70
|
+
|
|
71
|
+
## 1.9.4
|
|
72
|
+
|
|
73
|
+
### Patch Changes
|
|
74
|
+
|
|
75
|
+
- Updated dependencies []:
|
|
76
|
+
- @opencloning/store@1.9.4
|
|
77
|
+
- @opencloning/utils@1.9.4
|
|
78
|
+
|
|
79
|
+
## 1.9.3
|
|
80
|
+
|
|
81
|
+
### Patch Changes
|
|
82
|
+
|
|
83
|
+
- [#762](https://github.com/OpenCloning/OpenCloning_frontend/pull/762) [`df69a1b`](https://github.com/OpenCloning/OpenCloning_frontend/commit/df69a1bfb3b3128e93d89eef48a29f81a54ebb82) Thanks [@manulera](https://github.com/manulera)! - Fix QuotaExceededError when adding more than ~10 AB1/sequencing files (OpenCloning/OpenCloning#82). The raw contents of the verification files used to be persisted in Web Storage (localStorage, later sessionStorage), which has a quota of ~5 MB, so submitting around 10+ AB1 files failed with `QuotaExceededError` and the files/alignments disappeared. The contents are now kept in an in-memory store for the duration of the tab session, which has no quota.
|
|
84
|
+
|
|
85
|
+
- Updated dependencies [[`df69a1b`](https://github.com/OpenCloning/OpenCloning_frontend/commit/df69a1bfb3b3128e93d89eef48a29f81a54ebb82)]:
|
|
86
|
+
- @opencloning/utils@1.9.3
|
|
87
|
+
- @opencloning/store@1.9.3
|
|
88
|
+
|
|
3
89
|
## 1.9.2
|
|
4
90
|
|
|
5
91
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"license": "MIT",
|
|
3
3
|
"name": "@opencloning/ui",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"scripts": {
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
"./hooks/useAlerts": "./src/hooks/useAlerts.js",
|
|
26
26
|
"./hooks/useCloningAlerts": "./src/hooks/useCloningAlerts.js",
|
|
27
27
|
"./hooks/useSequencingAlignment": "./src/hooks/useSequencingAlignment.js",
|
|
28
|
-
"./hooks/
|
|
29
|
-
"./hooks/useHttpClient": "./src/hooks/useHttpClient.js",
|
|
28
|
+
"./hooks/useBackendHttpClient": "./src/hooks/useBackendHttpClient.js",
|
|
30
29
|
"./components/SequenceViewer": "./src/components/SequenceViewer.jsx",
|
|
31
30
|
"./standalone": "./src/StandAloneOpenCloning.js"
|
|
32
31
|
},
|
|
@@ -41,14 +40,14 @@
|
|
|
41
40
|
"@mui/icons-material": "^5.18.0",
|
|
42
41
|
"@mui/material": "^5.18.0",
|
|
43
42
|
"@mui/x-data-grid": "^8.28.5",
|
|
44
|
-
"@opencloning/store": "1.
|
|
45
|
-
"@opencloning/utils": "1.
|
|
43
|
+
"@opencloning/store": "2.1.0",
|
|
44
|
+
"@opencloning/utils": "2.1.0",
|
|
46
45
|
"@teselagen/bio-parsers": "^0.4.38",
|
|
47
46
|
"@teselagen/ove": "^0.8.42",
|
|
48
47
|
"@teselagen/range-utils": "^0.3.20",
|
|
49
48
|
"@teselagen/sequence-utils": "^0.3.42",
|
|
50
49
|
"@zip.js/zip.js": "^2.8.26",
|
|
51
|
-
"axios": "^1.
|
|
50
|
+
"axios": "^1.20.0",
|
|
52
51
|
"lodash-es": "^4.18.1",
|
|
53
52
|
"react-draggable": "^4.5.0"
|
|
54
53
|
},
|
|
@@ -7,8 +7,7 @@ import { getSequencesNotInDatabase } from '@opencloning/utils/network';
|
|
|
7
7
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
8
8
|
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
9
9
|
import { cloneDeep } from 'lodash-es';
|
|
10
|
-
import
|
|
11
|
-
import useHttpClient from '../hooks/useHttpClient';
|
|
10
|
+
import useBackendHttpClient from '../hooks/useBackendHttpClient';
|
|
12
11
|
|
|
13
12
|
const { setState: setCloningState } = cloningActions;
|
|
14
13
|
|
|
@@ -17,9 +16,8 @@ function useLocateSequencesInDatabaseAction() {
|
|
|
17
16
|
const database = useDatabase();
|
|
18
17
|
const store = useStore();
|
|
19
18
|
const dispatch = useDispatch();
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const url = backendRoute('normalize_cloning_strategy');
|
|
19
|
+
const httpClient = useBackendHttpClient();
|
|
20
|
+
const url = 'normalize_cloning_strategy';
|
|
23
21
|
|
|
24
22
|
const { addAlert } = useCloningAlerts();
|
|
25
23
|
const main = React.useCallback(async () => {
|
|
@@ -4,16 +4,14 @@ import { useDispatch, useSelector, useStore } from 'react-redux';
|
|
|
4
4
|
import { isEqual } from 'lodash-es';
|
|
5
5
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
6
6
|
import error2String from '@opencloning/utils/error2String';
|
|
7
|
-
import
|
|
8
|
-
import useHttpClient from '../hooks/useHttpClient';
|
|
7
|
+
import useBackendHttpClient from '../hooks/useBackendHttpClient';
|
|
9
8
|
|
|
10
9
|
function EditSequenceNameDialog({ id, dialogOpen, setDialogOpen }) {
|
|
11
10
|
const [name, setName] = React.useState('');
|
|
12
11
|
const [originalName, setOriginalName] = React.useState('');
|
|
13
12
|
const [error, setError] = React.useState('');
|
|
14
13
|
const store = useStore();
|
|
15
|
-
const
|
|
16
|
-
const httpClient = useHttpClient();
|
|
14
|
+
const httpClient = useBackendHttpClient();
|
|
17
15
|
|
|
18
16
|
const { updateSequenceAndItsSource } = cloningActions;
|
|
19
17
|
const dispatch = useDispatch();
|
|
@@ -23,7 +21,7 @@ function EditSequenceNameDialog({ id, dialogOpen, setDialogOpen }) {
|
|
|
23
21
|
const source = sources.find((s) => s.id === id);
|
|
24
22
|
const sequence = sequences.find((s) => s.id === id);
|
|
25
23
|
setError('');
|
|
26
|
-
const url =
|
|
24
|
+
const url = 'rename_sequence';
|
|
27
25
|
try {
|
|
28
26
|
const { data: newSequence } = await httpClient.post(url, sequence, { params: { name } });
|
|
29
27
|
const newSource = { ...source, output_name: newName };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Alert, Button, CircularProgress } from '@mui/material';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { useDispatch } from 'react-redux';
|
|
4
|
-
import
|
|
5
|
-
import useHttpClient from '../hooks/useHttpClient';
|
|
4
|
+
import useBackendHttpClient from '../hooks/useBackendHttpClient';
|
|
6
5
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
7
6
|
import { version } from '../index';
|
|
8
7
|
|
|
@@ -14,8 +13,7 @@ function ExternalServicesStatusCheck() {
|
|
|
14
13
|
const [loading, setLoading] = React.useState(false);
|
|
15
14
|
const [successMessage, setSuccessMessage] = React.useState('');
|
|
16
15
|
const dispatch = useDispatch();
|
|
17
|
-
const
|
|
18
|
-
const httpClient = useHttpClient();
|
|
16
|
+
const httpClient = useBackendHttpClient();
|
|
19
17
|
React.useEffect(() => {
|
|
20
18
|
dispatch(updateAppInfo({ frontendVersion: version }));
|
|
21
19
|
setLoading(true);
|
|
@@ -23,7 +21,7 @@ function ExternalServicesStatusCheck() {
|
|
|
23
21
|
const services = [
|
|
24
22
|
{
|
|
25
23
|
message: 'Backend server is down',
|
|
26
|
-
url:
|
|
24
|
+
url: 'version',
|
|
27
25
|
check: (resp) => {
|
|
28
26
|
if (resp.status === 200) {
|
|
29
27
|
const { backend_version: backendVersion, schema_version: schemaVersion } = resp.data;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { batch, useDispatch, useStore } from 'react-redux';
|
|
3
3
|
import useCloningAlerts from '../hooks/useCloningAlerts';
|
|
4
|
-
import useBackendRoute from '../hooks/useBackendRoute';
|
|
5
4
|
import useCloningHistoryLoader from '../hooks/useCloningHistoryLoader';
|
|
6
5
|
import { getEmptyPlaceholderSource } from '@opencloning/store/cloning_utils';
|
|
7
6
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
8
7
|
import HistoryLoadedDialog from './HistoryLoadedDialog';
|
|
9
|
-
import
|
|
8
|
+
import useBackendHttpClient from '../hooks/useBackendHttpClient';
|
|
10
9
|
|
|
11
10
|
const { deleteSourceAndItsChildren, addSourceAndItsOutputSequence } = cloningActions;
|
|
12
11
|
|
|
13
|
-
async function processSequenceFiles(files,
|
|
12
|
+
async function processSequenceFiles(files, httpClient) {
|
|
14
13
|
const allSources = [];
|
|
15
14
|
const allSequences = [];
|
|
16
15
|
const allWarnings = [];
|
|
@@ -27,7 +26,7 @@ async function processSequenceFiles(files, backendRoute, httpClient) {
|
|
|
27
26
|
'content-type': 'multipart/form-data',
|
|
28
27
|
},
|
|
29
28
|
};
|
|
30
|
-
const url =
|
|
29
|
+
const url = 'read_from_file';
|
|
31
30
|
try {
|
|
32
31
|
const { data: { sources, sequences }, headers } = await httpClient.post(url, formData, config);
|
|
33
32
|
// If there are warnings, add them to the list of warnings
|
|
@@ -57,9 +56,8 @@ async function processSequenceFiles(files, backendRoute, httpClient) {
|
|
|
57
56
|
function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
58
57
|
const { addAlert } = useCloningAlerts();
|
|
59
58
|
const dispatch = useDispatch();
|
|
60
|
-
const backendRoute = useBackendRoute();
|
|
61
59
|
const store = useStore();
|
|
62
|
-
const httpClient =
|
|
60
|
+
const httpClient = useBackendHttpClient();
|
|
63
61
|
const { loadHistoryFromFile, loadSnapgeneHistory } = useCloningHistoryLoader();
|
|
64
62
|
const [fileLoaderFunctions, setFileLoaderFunctions] = React.useState(null);
|
|
65
63
|
|
|
@@ -117,7 +115,7 @@ function LoadCloningHistoryWrapper({ fileList, clearFiles, children }) {
|
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
try {
|
|
120
|
-
const { sources, sequences, warnings } = await processSequenceFiles(files,
|
|
118
|
+
const { sources, sequences, warnings } = await processSequenceFiles(files, httpClient);
|
|
121
119
|
batch(() => {
|
|
122
120
|
// If there is only one source and it is empty, delete it
|
|
123
121
|
const emptyPlaceholder = getEmptyPlaceholderSource(cloningState.sources);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* eslint-disable camelcase */
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
|
|
4
|
+
import { DatabaseProvider } from '@opencloning/ui/providers/DatabaseContext';
|
|
5
|
+
import store from '@opencloning/store';
|
|
4
6
|
import { AssemblerComponent } from './Assembler';
|
|
5
7
|
|
|
6
8
|
|
|
@@ -362,3 +364,185 @@ describe('<AssemblerComponent />', () => {
|
|
|
362
364
|
});
|
|
363
365
|
});
|
|
364
366
|
|
|
367
|
+
describe('<AssemblerComponent /> with plasmids from a database', () => {
|
|
368
|
+
const databasePlasmids = [
|
|
369
|
+
{ id: 1, key: 'CCCT-AACG', database_id: 10 },
|
|
370
|
+
{ id: 2, key: 'AACG-CCCT', database_id: 20 },
|
|
371
|
+
].map(({ id, key, database_id }) => {
|
|
372
|
+
const [left_overhang, right_overhang] = key.split('-');
|
|
373
|
+
return {
|
|
374
|
+
id,
|
|
375
|
+
type: 'databaseSequence',
|
|
376
|
+
plasmid_name: `DB Plasmid ${database_id}`,
|
|
377
|
+
database_id,
|
|
378
|
+
left_overhang,
|
|
379
|
+
right_overhang,
|
|
380
|
+
key,
|
|
381
|
+
userUploaded: true,
|
|
382
|
+
textFileSequence: { ...dummyResponse.sequences[0], id: database_id, file_content: `${dummyResponse.sequences[0].file_content}\n${database_id}` },
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('uses the database sequences and creates DatabaseSources', () => {
|
|
387
|
+
cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', (req) => {
|
|
388
|
+
// Sequence content is the one from the database, no request is made to fetch them
|
|
389
|
+
expect(req.body.sequences.map((s) => s.file_content)).to.deep.equal(databasePlasmids.map((p) => p.textFileSequence.file_content));
|
|
390
|
+
req.reply({ statusCode: 200, body: dummyResponse2 });
|
|
391
|
+
}).as('assemblySuccess');
|
|
392
|
+
|
|
393
|
+
cy.mount(
|
|
394
|
+
<ConfigProvider config={testConfig}>
|
|
395
|
+
<AssemblerComponent
|
|
396
|
+
plasmids={databasePlasmids}
|
|
397
|
+
categories={mockCategories}
|
|
398
|
+
assemblyEnzymes={['assembly_enzyme']}
|
|
399
|
+
addAlert={cy.stub().as('addAlert')}
|
|
400
|
+
appInfo={{}}
|
|
401
|
+
/>
|
|
402
|
+
</ConfigProvider>,
|
|
403
|
+
);
|
|
404
|
+
cy.get('[data-testid="plasmid-select"]').first().within(() => {
|
|
405
|
+
cy.get('input').click();
|
|
406
|
+
});
|
|
407
|
+
// Plasmids from the database are not highlighted (only those uploaded from files)
|
|
408
|
+
cy.get('li').contains('DB Plasmid 10').should('not.have.css', 'background-color', 'rgb(220, 237, 200)');
|
|
409
|
+
cy.get('li').contains('DB Plasmid 10').click();
|
|
410
|
+
cy.get('[data-testid="plasmid-select"]').eq(1).within(() => {
|
|
411
|
+
cy.get('input').click();
|
|
412
|
+
});
|
|
413
|
+
cy.get('li').contains('DB Plasmid 20').click();
|
|
414
|
+
|
|
415
|
+
cy.get('[data-testid="assembler-submit-button"]').click();
|
|
416
|
+
cy.wait('@assemblySuccess');
|
|
417
|
+
cy.get('[data-testid="assembler-product-table"]').contains('DB Plasmid 10').should('exist');
|
|
418
|
+
|
|
419
|
+
cy.get('[data-testid="assembler-product-table-view-button"]').first().click();
|
|
420
|
+
cy.then(() => {
|
|
421
|
+
const { sources, sequences } = store.getState().cloning;
|
|
422
|
+
const databaseSources = sources.filter((s) => s.type === 'DatabaseSource');
|
|
423
|
+
expect(databaseSources.map((s) => s.database_id)).to.deep.equal([10, 20]);
|
|
424
|
+
databaseSources.forEach((source) => {
|
|
425
|
+
expect(source.input).to.deep.equal([]);
|
|
426
|
+
const sequence = sequences.find((seq) => seq.id === source.id);
|
|
427
|
+
expect(sequence.file_content).to.equal(databasePlasmids.find((p) => p.database_id === source.database_id).textFileSequence.file_content);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
cy.get('@addAlert').should('have.not.been.called');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe('Upload to DB', () => {
|
|
434
|
+
function mountAndAssemble(database) {
|
|
435
|
+
cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', { statusCode: 200, body: dummyResponse2 }).as('assemblySuccess');
|
|
436
|
+
const component = (
|
|
437
|
+
<ConfigProvider config={testConfig}>
|
|
438
|
+
<AssemblerComponent
|
|
439
|
+
plasmids={databasePlasmids}
|
|
440
|
+
categories={mockCategories}
|
|
441
|
+
assemblyEnzymes={['assembly_enzyme']}
|
|
442
|
+
addAlert={cy.stub().as('addAlert')}
|
|
443
|
+
appInfo={{ backendVersion: '1', schemaVersion: '2', frontendVersion: '3' }}
|
|
444
|
+
/>
|
|
445
|
+
</ConfigProvider>
|
|
446
|
+
);
|
|
447
|
+
cy.mount(database ? <DatabaseProvider value={database}>{component}</DatabaseProvider> : component);
|
|
448
|
+
cy.get('[data-testid="plasmid-select"]').first().within(() => {
|
|
449
|
+
cy.get('input').click();
|
|
450
|
+
});
|
|
451
|
+
cy.get('li').contains('DB Plasmid 10').click();
|
|
452
|
+
cy.get('[data-testid="plasmid-select"]').eq(1).within(() => {
|
|
453
|
+
cy.get('input').click();
|
|
454
|
+
});
|
|
455
|
+
cy.get('li').contains('DB Plasmid 20').click();
|
|
456
|
+
cy.get('[data-testid="assembler-submit-button"]').click();
|
|
457
|
+
cy.wait('@assemblySuccess');
|
|
458
|
+
cy.get('[data-testid="assembler-download-assemblies-button"]').should('exist');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Mock of the database dialog, stores the props it receives
|
|
462
|
+
// submittedCount: number of strategies the mock reports as submitted on success
|
|
463
|
+
function mockDatabase(submittedCount = 1) {
|
|
464
|
+
const receivedProps = [];
|
|
465
|
+
function SubmitCloningStrategiesInBulkComponent(props) {
|
|
466
|
+
receivedProps.push(props);
|
|
467
|
+
const syncResults = Array.from({ length: submittedCount }, (_, i) => ({ file_name: `${i}.json` }));
|
|
468
|
+
return (
|
|
469
|
+
<div data-testid="mock-submit-dialog">
|
|
470
|
+
<button type="button" onClick={() => { props.onSuccess([{ id: 100 }], syncResults); props.onClose(); }}>mock success</button>
|
|
471
|
+
<button type="button" onClick={props.onClose}>mock close</button>
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return { database: { name: 'MockDB', SubmitCloningStrategiesInBulkComponent }, receivedProps };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
it('is not shown without a database', () => {
|
|
479
|
+
mountAndAssemble(null);
|
|
480
|
+
cy.get('[data-testid="assembler-upload-to-db-button"]').should('not.exist');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('is not shown if the database does not support bulk submission', () => {
|
|
484
|
+
mountAndAssemble({ name: 'MockDB' });
|
|
485
|
+
cy.get('[data-testid="assembler-upload-to-db-button"]').should('not.exist');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('is disabled when an output name is empty', () => {
|
|
489
|
+
const { database } = mockDatabase();
|
|
490
|
+
mountAndAssemble(database);
|
|
491
|
+
cy.get('[data-testid="assembler-upload-to-db-button"]').should('not.be.disabled');
|
|
492
|
+
cy.get('[data-testid="assembler-product-table-edit-button"]').first().click();
|
|
493
|
+
cy.get('[role="dialog"]').find('input').clear();
|
|
494
|
+
cy.get('[role="dialog"]').contains('Save').click();
|
|
495
|
+
cy.get('[data-testid="assembler-upload-to-db-button"]').should('be.disabled');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('passes the cloning strategies of the assemblies to the database dialog', () => {
|
|
499
|
+
const { database, receivedProps } = mockDatabase();
|
|
500
|
+
mountAndAssemble(database);
|
|
501
|
+
cy.get('[data-testid="assembler-product-table-edit-button"]').first().click();
|
|
502
|
+
cy.get('[role="dialog"]').find('input').clear();
|
|
503
|
+
cy.get('[role="dialog"]').find('input').type('my product');
|
|
504
|
+
cy.get('[role="dialog"]').contains('Save').click();
|
|
505
|
+
|
|
506
|
+
cy.get('[data-testid="mock-submit-dialog"]').should('not.exist');
|
|
507
|
+
cy.get('[data-testid="assembler-upload-to-db-button"]').click();
|
|
508
|
+
cy.get('[data-testid="mock-submit-dialog"]').should('exist');
|
|
509
|
+
cy.then(() => {
|
|
510
|
+
const { files } = receivedProps[receivedProps.length - 1];
|
|
511
|
+
expect(files).to.have.length(1);
|
|
512
|
+
expect(files[0]).to.be.instanceOf(File);
|
|
513
|
+
expect(files[0].name).to.equal('my_product.json');
|
|
514
|
+
return files[0].text();
|
|
515
|
+
}).then((content) => {
|
|
516
|
+
const cloningStrategy = JSON.parse(content);
|
|
517
|
+
expect(cloningStrategy.backend_version).to.equal('1');
|
|
518
|
+
const databaseSources = cloningStrategy.sources.filter((s) => s.type === 'DatabaseSource');
|
|
519
|
+
expect(databaseSources.map((s) => s.database_id)).to.deep.equal([10, 20]);
|
|
520
|
+
const finalSource = cloningStrategy.sources.find((s) => s.type === 'RestrictionAndLigationSource');
|
|
521
|
+
expect(finalSource.output_name).to.equal('my_product');
|
|
522
|
+
const finalSequence = cloningStrategy.sequences.find((s) => s.id === finalSource.id);
|
|
523
|
+
expect(finalSequence.file_content.split('\n')[0]).to.contain('my_product');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
cy.contains('button', 'mock success').click();
|
|
527
|
+
cy.get('[data-testid="mock-submit-dialog"]').should('not.exist');
|
|
528
|
+
cy.get('@addAlert').should('have.been.calledOnceWith', { message: 'Uploaded 1 assembly to MockDB', severity: 'success' });
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('reports the number of submitted assemblies, not the number of files', () => {
|
|
532
|
+
const { database } = mockDatabase(2);
|
|
533
|
+
mountAndAssemble(database);
|
|
534
|
+
cy.get('[data-testid="assembler-upload-to-db-button"]').click();
|
|
535
|
+
cy.contains('button', 'mock success').click();
|
|
536
|
+
cy.get('@addAlert').should('have.been.calledOnceWith', { message: 'Uploaded 2 assemblies to MockDB', severity: 'success' });
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('closes the dialog without alerts', () => {
|
|
540
|
+
const { database } = mockDatabase();
|
|
541
|
+
mountAndAssemble(database);
|
|
542
|
+
cy.get('[data-testid="assembler-upload-to-db-button"]').click();
|
|
543
|
+
cy.contains('button', 'mock close').click();
|
|
544
|
+
cy.get('[data-testid="mock-submit-dialog"]').should('not.exist');
|
|
545
|
+
cy.get('@addAlert').should('not.have.been.called');
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
});
|
|
@@ -18,10 +18,10 @@ import EditTextDialog from '../form/EditTextDialog';
|
|
|
18
18
|
import useCombinatorialAssembly from './useCombinatorialAssembly';
|
|
19
19
|
import ExistingSyntaxDialog from './ExistingSyntaxDialog';
|
|
20
20
|
import error2String from '@opencloning/utils/error2String';
|
|
21
|
-
import { categoryFilter, downloadAssemblerFilesAsZip, getFilesToExportFromAssembler, getDefaultAssemblyOutputName, MAX_OUTPUT_NAME_LENGTH, sanitizeOutputName } from './assembler_utils';
|
|
22
|
-
import
|
|
23
|
-
import useHttpClient from '../../hooks/useHttpClient';
|
|
21
|
+
import { categoryFilter, downloadAssemblerFilesAsZip, getFilesToExportFromAssembler, getCloningStrategyFilesFromAssembler, getDefaultAssemblyOutputName, MAX_OUTPUT_NAME_LENGTH, sanitizeOutputName } from './assembler_utils';
|
|
22
|
+
import useBackendHttpClient from '../../hooks/useBackendHttpClient';
|
|
24
23
|
import useCloningAlerts from '../../hooks/useCloningAlerts';
|
|
24
|
+
import useDatabase from '../../hooks/useDatabase';
|
|
25
25
|
import UploadPlasmidsButton from './UploadPlasmidsButton';
|
|
26
26
|
import { useConfig } from '../../providers';
|
|
27
27
|
import { isEqual } from 'lodash-es';
|
|
@@ -31,6 +31,9 @@ import { graphToMSA, partsToGraph } from './graph_utils';
|
|
|
31
31
|
|
|
32
32
|
const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
|
|
33
33
|
|
|
34
|
+
// Shown on the download and upload buttons when they are disabled because of the output names
|
|
35
|
+
const INVALID_OUTPUT_NAMES_MESSAGE = `Output names must be unique, non-empty, and at most ${MAX_OUTPUT_NAME_LENGTH} characters`;
|
|
36
|
+
|
|
34
37
|
function formatItemName(item) {
|
|
35
38
|
// Fallback in case the item is not found (while updating list)
|
|
36
39
|
return item ? `${item.plasmid_name}` : '-'
|
|
@@ -195,7 +198,7 @@ function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, a
|
|
|
195
198
|
const { key, ...restProps } = props
|
|
196
199
|
const plasmid = plasmids.find((d) => d.id === option)
|
|
197
200
|
return (
|
|
198
|
-
<MenuItem key={key} {...restProps} sx={{ backgroundColor: plasmid.
|
|
201
|
+
<MenuItem key={key} {...restProps} sx={{ backgroundColor: plasmid.type === 'loadedFile' ? '#dcedc8' : undefined }}>
|
|
199
202
|
{formatItemName(plasmid)}
|
|
200
203
|
</MenuItem>
|
|
201
204
|
)}}
|
|
@@ -216,11 +219,16 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
216
219
|
const [assemblyOutputNames, setAssemblyOutputNames] = React.useState([])
|
|
217
220
|
const [errorMessage, setErrorMessage] = React.useState('')
|
|
218
221
|
const [loadingMessage, setLoadingMessage] = React.useState('')
|
|
222
|
+
// Cloning strategy files being uploaded to the database (null if the dialog is closed)
|
|
223
|
+
const [filesToUpload, setFilesToUpload] = React.useState(null)
|
|
224
|
+
const database = useDatabase()
|
|
225
|
+
const SubmitCloningStrategiesInBulkComponent = database?.SubmitCloningStrategiesInBulkComponent
|
|
219
226
|
|
|
220
227
|
const clearAssemblySelection = React.useCallback(() => {
|
|
221
228
|
setRequestedAssemblies([])
|
|
222
229
|
setAssemblyOutputNames([])
|
|
223
230
|
setErrorMessage('')
|
|
231
|
+
setFilesToUpload(null)
|
|
224
232
|
}, [])
|
|
225
233
|
|
|
226
234
|
const { assembly, setCategory, setId, expandedAssemblies, assemblyComplete, canBeSubmitted, currentCategories } = useCombinatorialAssembly({ onValueChange: clearAssemblySelection, categories, plasmids })
|
|
@@ -283,6 +291,21 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
283
291
|
}
|
|
284
292
|
}, [requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo, addAlert, assemblyOutputNames])
|
|
285
293
|
|
|
294
|
+
const onUploadToDatabase = React.useCallback(() => {
|
|
295
|
+
try {
|
|
296
|
+
setFilesToUpload(getCloningStrategyFilesFromAssembler({ requestedAssemblies, appInfo, outputNames: assemblyOutputNames })
|
|
297
|
+
.map(({ name, content }) => new File([content], name, { type: 'application/json' })))
|
|
298
|
+
} catch (error) {
|
|
299
|
+
addAlert({ message: `Error preparing assemblies for upload: ${error.message}`, severity: 'error' })
|
|
300
|
+
}
|
|
301
|
+
}, [requestedAssemblies, appInfo, assemblyOutputNames, addAlert])
|
|
302
|
+
|
|
303
|
+
// syncResults are the submitted strategies (some may be skipped, e.g. already in the database)
|
|
304
|
+
const onUploadSuccess = React.useCallback((_sequenceRefs, syncResults) => {
|
|
305
|
+
const count = syncResults.length
|
|
306
|
+
addAlert({ message: `Uploaded ${count} assembl${count === 1 ? 'y' : 'ies'} to ${database.name}`, severity: 'success' })
|
|
307
|
+
}, [addAlert, database])
|
|
308
|
+
|
|
286
309
|
const options = React.useMemo(() => assemblyComplete ? assembly : [...assembly, { category: '', plasmidIds: [] }], [assembly, assemblyComplete])
|
|
287
310
|
|
|
288
311
|
return (
|
|
@@ -312,7 +335,16 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
312
335
|
sx={{ p: 2, fontSize: '1.2rem' }}
|
|
313
336
|
onClick={onDownloadAssemblies}
|
|
314
337
|
disabled={!canDownload}
|
|
315
|
-
title={!canDownload ?
|
|
338
|
+
title={!canDownload ? INVALID_OUTPUT_NAMES_MESSAGE : ''}>Download Assemblies
|
|
339
|
+
</Button>}
|
|
340
|
+
{requestedAssemblies.length > 0 && SubmitCloningStrategiesInBulkComponent && <Button
|
|
341
|
+
color="primary"
|
|
342
|
+
variant="contained"
|
|
343
|
+
data-testid="assembler-upload-to-db-button"
|
|
344
|
+
sx={{ p: 2, fontSize: '1.2rem' }}
|
|
345
|
+
onClick={onUploadToDatabase}
|
|
346
|
+
disabled={!canDownload}
|
|
347
|
+
title={!canDownload ? INVALID_OUTPUT_NAMES_MESSAGE : `Upload the assemblies and their history to ${database.name}`}>Upload to DB
|
|
316
348
|
</Button>}
|
|
317
349
|
</Box>
|
|
318
350
|
{errorMessage && <Alert severity="error" sx={{ my: 2, maxWidth: 300, margin: 'auto', fontSize: '1.2rem' }}>{errorMessage}</Alert>}
|
|
@@ -322,6 +354,13 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzymes, addA
|
|
|
322
354
|
assemblyOutputNames, onOutputNameChange,
|
|
323
355
|
}} />
|
|
324
356
|
}
|
|
357
|
+
{filesToUpload && SubmitCloningStrategiesInBulkComponent && (
|
|
358
|
+
<SubmitCloningStrategiesInBulkComponent
|
|
359
|
+
files={filesToUpload}
|
|
360
|
+
onClose={() => setFilesToUpload(null)}
|
|
361
|
+
onSuccess={onUploadSuccess}
|
|
362
|
+
/>
|
|
363
|
+
)}
|
|
325
364
|
|
|
326
365
|
</Box >
|
|
327
366
|
)
|
|
@@ -376,24 +415,28 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
|
|
|
376
415
|
|
|
377
416
|
function LoadSyntaxButton({ setSyntax, addPlasmids, clearPlasmids }) {
|
|
378
417
|
const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
|
|
379
|
-
const httpClient =
|
|
418
|
+
const httpClient = useBackendHttpClient();
|
|
380
419
|
const { staticContentPath } = useConfig();
|
|
381
|
-
const backendRoute = useBackendRoute();
|
|
382
420
|
const { addAlert } = useCloningAlerts();
|
|
421
|
+
const database = useDatabase();
|
|
422
|
+
// If plasmids can be loaded from the database, the default plasmids of the syntax are not loaded
|
|
423
|
+
const loadDefaultPlasmids = !database?.GetSequencesInBulkComponent;
|
|
383
424
|
const onSyntaxSelect = React.useCallback(async (syntax, plasmids) => {
|
|
384
|
-
const url =
|
|
425
|
+
const url = 'validate_syntax';
|
|
385
426
|
try {
|
|
386
427
|
await httpClient.post(url, syntax);
|
|
387
428
|
setSyntax(syntax)
|
|
388
429
|
clearPlasmids()
|
|
389
|
-
|
|
430
|
+
if (loadDefaultPlasmids) {
|
|
431
|
+
addPlasmids(plasmids)
|
|
432
|
+
}
|
|
390
433
|
} catch (error) {
|
|
391
434
|
addAlert({
|
|
392
435
|
message: error2String(error),
|
|
393
436
|
severity: 'error',
|
|
394
437
|
});
|
|
395
438
|
}
|
|
396
|
-
}, [setSyntax, addPlasmids, clearPlasmids, httpClient,
|
|
439
|
+
}, [setSyntax, addPlasmids, clearPlasmids, httpClient, addAlert, loadDefaultPlasmids])
|
|
397
440
|
return <>
|
|
398
441
|
<Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
|
|
399
442
|
{existingSyntaxDialogOpen && <ExistingSyntaxDialog
|
|
@@ -450,7 +493,10 @@ function Assembler() {
|
|
|
450
493
|
const addPlasmids = React.useCallback((newPlasmids) => {
|
|
451
494
|
setPlasmids((prevPlasmids) => {
|
|
452
495
|
const maxId = Math.max(...prevPlasmids.map((plasmid) => plasmid.id), 0)
|
|
453
|
-
|
|
496
|
+
// Plasmids from the database that are already loaded are skipped
|
|
497
|
+
const loadedDatabaseIds = new Set(prevPlasmids.map((plasmid) => plasmid.database_id).filter((id) => id !== undefined))
|
|
498
|
+
const plasmidsToAdd = newPlasmids.filter((plasmid) => plasmid.database_id === undefined || !loadedDatabaseIds.has(plasmid.database_id))
|
|
499
|
+
return [...prevPlasmids, ...plasmidsToAdd.map((plasmid, index) => ({ ...plasmid, id: maxId + index + 1 }))]
|
|
454
500
|
})
|
|
455
501
|
}, [])
|
|
456
502
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box, ButtonGroup, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'
|
|
3
|
-
import
|
|
3
|
+
import { getExternalHttpClient } from '@opencloning/utils/httpClients';
|
|
4
4
|
import RequestStatusWrapper from '../form/RequestStatusWrapper';
|
|
5
5
|
import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
|
|
6
6
|
import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
|
|
7
7
|
import { normalizeSyntaxEnzymes } from '@opencloning/utils/normalizeSyntax';
|
|
8
8
|
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
|
|
9
9
|
|
|
10
|
-
const httpClient =
|
|
10
|
+
const httpClient = getExternalHttpClient();
|
|
11
11
|
const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
|
|
12
12
|
httpClient.defaults.baseURL = baseURL;
|
|
13
13
|
|