@opencloning/utils 1.7.3 → 1.9.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 +75 -0
- package/package.json +9 -7
- package/src/utils/alignmentUtils.js +120 -0
- package/src/utils/dnaUtils.js +22 -0
- package/src/utils/getHttpClient.js +7 -0
- package/src/utils/getHttpClient.test.js +19 -0
- package/src/utils/httpClientAuth.js +36 -0
- package/src/utils/httpClientAuth.test.js +52 -0
- package/src/utils/index.js +2 -0
- package/src/utils/network.js +27 -4
- package/src/utils/readNwrite.js +8 -0
- package/src/utils/thunks.js +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# @opencloning/utils
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#711](https://github.com/OpenCloning/OpenCloning_frontend/pull/711) [`6813620`](https://github.com/OpenCloning/OpenCloning_frontend/commit/6813620f4a47c44857045866d667474efbf9a64c) Thanks [@manulera](https://github.com/manulera)! - Use authentication for cloning endpoints in opencloning-db
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies []:
|
|
12
|
+
- @opencloning/store@1.9.0
|
|
13
|
+
|
|
14
|
+
## 1.8.2
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies []:
|
|
19
|
+
- @opencloning/store@1.8.2
|
|
20
|
+
|
|
21
|
+
## 1.8.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Updated dependencies []:
|
|
26
|
+
- @opencloning/store@1.8.1
|
|
27
|
+
|
|
28
|
+
## 1.8.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- [#694](https://github.com/OpenCloning/OpenCloning_frontend/pull/694) [`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57) Thanks [@manulera](https://github.com/manulera)! - \* Add alignment functionality functions.
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- Updated dependencies [[`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57)]:
|
|
37
|
+
- @opencloning/store@1.8.0
|
|
38
|
+
|
|
39
|
+
## 1.7.8
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Updated dependencies []:
|
|
44
|
+
- @opencloning/store@1.7.8
|
|
45
|
+
|
|
46
|
+
## 1.7.7
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- [#689](https://github.com/manulera/OpenCloning_frontend/pull/689) [`c5d9309`](https://github.com/manulera/OpenCloning_frontend/commit/c5d93099583fdfafafd40e9924d2216b596e0cec) Thanks [@manulera](https://github.com/manulera)! - Added experimental feature for domestication that relies on API calls to goldenbraidpro
|
|
51
|
+
|
|
52
|
+
- Updated dependencies []:
|
|
53
|
+
- @opencloning/store@1.7.7
|
|
54
|
+
|
|
55
|
+
## 1.7.6
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies []:
|
|
60
|
+
- @opencloning/store@1.7.6
|
|
61
|
+
|
|
62
|
+
## 1.7.5
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [[`034c738`](https://github.com/manulera/OpenCloning_frontend/commit/034c73840dc183b6b3584365c1db899d2f0c548b)]:
|
|
67
|
+
- @opencloning/store@1.7.5
|
|
68
|
+
|
|
69
|
+
## 1.7.4
|
|
70
|
+
|
|
71
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- [#682](https://github.com/manulera/OpenCloning_frontend/pull/682) [`04703a9`](https://github.com/manulera/OpenCloning_frontend/commit/04703a9269a5d4cc4bdde478c1ec61e5c73b0d2a) Thanks [@manulera](https://github.com/manulera)! - Fix bug when copying pcr products
|
|
74
|
+
|
|
75
|
+
- Updated dependencies []:
|
|
76
|
+
- @opencloning/store@1.7.4
|
|
77
|
+
|
|
3
78
|
## 1.7.3
|
|
4
79
|
|
|
5
80
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"license": "MIT",
|
|
3
3
|
"name": "@opencloning/utils",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.9.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/utils/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./readNwrite": "./src/utils/readNwrite.js",
|
|
10
10
|
"./network": "./src/utils/network.js",
|
|
11
11
|
"./getHttpClient": "./src/utils/getHttpClient.js",
|
|
12
|
+
"./httpClientAuth": "./src/utils/httpClientAuth.js",
|
|
12
13
|
"./sourceFunctions": "./src/utils/sourceFunctions.js",
|
|
13
14
|
"./error2String": "./src/utils/error2String.js",
|
|
14
15
|
"./enzyme_utils": "./src/utils/enzyme_utils.js",
|
|
@@ -21,20 +22,21 @@
|
|
|
21
22
|
"./other": "./src/utils/other.js",
|
|
22
23
|
"./transformCoords": "./src/utils/transformCoords.js",
|
|
23
24
|
"./selectedRegionUtils": "./src/utils/selectedRegionUtils.js",
|
|
24
|
-
"./normalizeSyntax": "./src/utils/normalizeSyntax.js"
|
|
25
|
+
"./normalizeSyntax": "./src/utils/normalizeSyntax.js",
|
|
26
|
+
"./alignmentUtils": "./src/utils/alignmentUtils.js"
|
|
25
27
|
},
|
|
26
28
|
"repository": {
|
|
27
29
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/
|
|
30
|
+
"url": "https://github.com/OpenCloning/OpenCloning_frontend.git",
|
|
29
31
|
"directory": "packages/utils"
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
32
|
-
"@opencloning/store": "1.
|
|
33
|
-
"@teselagen/bio-parsers": "^0.4.
|
|
34
|
+
"@opencloning/store": "1.9.0",
|
|
35
|
+
"@teselagen/bio-parsers": "^0.4.38",
|
|
34
36
|
"@teselagen/sequence-utils": "^0.3.42",
|
|
35
|
-
"@zip.js/zip.js": "^2.
|
|
37
|
+
"@zip.js/zip.js": "^2.8.26",
|
|
36
38
|
"dom-to-svg": "^0.12.2",
|
|
37
|
-
"lodash-es": "^4.
|
|
39
|
+
"lodash-es": "^4.18.1"
|
|
38
40
|
},
|
|
39
41
|
"peerDependencies": {
|
|
40
42
|
"react": "^18.3.1",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getReverseComplementSequenceAndAnnotations, getReverseComplementSequenceString, rotateSequenceDataToPosition } from '@teselagen/sequence-utils';
|
|
2
|
+
import { findRotation, syncChromatogramDataWithAlignment } from './sequenceManipulation.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Process alignment sequence data by handling rotation and reverse complement.
|
|
6
|
+
* If fileContent has features, it aligns the sequence data to match the alignment
|
|
7
|
+
* by computing rotation and optionally reverse-complementing.
|
|
8
|
+
*/
|
|
9
|
+
export function processAlignmentSequenceData(fileContent, alignmentSequence) {
|
|
10
|
+
if (!(fileContent.features && fileContent.features.length > 0)) {
|
|
11
|
+
return {
|
|
12
|
+
name: fileContent.name || '',
|
|
13
|
+
sequence: alignmentSequence,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let alignmentSequenceData = fileContent;
|
|
18
|
+
let rotation = findRotation(fileContent.sequence, alignmentSequence);
|
|
19
|
+
const reverseComplemented = rotation === -1;
|
|
20
|
+
if (reverseComplemented) {
|
|
21
|
+
rotation = findRotation(fileContent.sequence, getReverseComplementSequenceString(alignmentSequence));
|
|
22
|
+
alignmentSequenceData = getReverseComplementSequenceAndAnnotations(fileContent);
|
|
23
|
+
}
|
|
24
|
+
if (rotation !== -1 && rotation !== 0) {
|
|
25
|
+
rotation = reverseComplemented ? fileContent.sequence.length - rotation : rotation;
|
|
26
|
+
alignmentSequenceData = rotateSequenceDataToPosition(alignmentSequenceData, rotation);
|
|
27
|
+
}
|
|
28
|
+
return alignmentSequenceData;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a single alignment track from parsed file content and an alignment file object.
|
|
33
|
+
* @param {Object} fileContent - Parsed sequence data (output of getTeselaJsonFromBase64)
|
|
34
|
+
* @param {Object} alignmentFile - Alignment file object with { file_name, alignment }
|
|
35
|
+
* @returns {{ sequenceData: Object, alignmentData: Object, chromatogramData: Object|undefined }}
|
|
36
|
+
*/
|
|
37
|
+
export function buildAlignmentTrack(fileContent, alignmentFile) {
|
|
38
|
+
const alignmentSequence = alignmentFile.alignment[1].replaceAll('-', '');
|
|
39
|
+
let chromatogramData = fileContent.chromatogramData;
|
|
40
|
+
|
|
41
|
+
if (chromatogramData) {
|
|
42
|
+
chromatogramData = syncChromatogramDataWithAlignment(chromatogramData, alignmentFile.alignment[1]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const alignmentSequenceData = processAlignmentSequenceData(fileContent, alignmentSequence);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
sequenceData: { ...alignmentSequenceData, name: alignmentFile.file_name },
|
|
49
|
+
alignmentData: {
|
|
50
|
+
sequence: alignmentFile.alignment[1],
|
|
51
|
+
name: alignmentFile.file_name,
|
|
52
|
+
},
|
|
53
|
+
chromatogramData,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the reference (first) alignment track.
|
|
59
|
+
* @param {Object} sequenceData - The reference sequence data
|
|
60
|
+
* @param {string} firstAlignmentSequence - The alignment string for the reference
|
|
61
|
+
* @returns {{ sequenceData: Object, alignmentData: Object }}
|
|
62
|
+
*/
|
|
63
|
+
export function buildReferenceTrack(sequenceData, firstAlignmentSequence) {
|
|
64
|
+
return {
|
|
65
|
+
sequenceData,
|
|
66
|
+
alignmentData: {
|
|
67
|
+
sequence: firstAlignmentSequence,
|
|
68
|
+
name: sequenceData.name,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the full alignment configuration object for addAlignment.
|
|
75
|
+
* @param {number} id - Sequence id
|
|
76
|
+
* @param {Object} sequenceData - Reference sequence data
|
|
77
|
+
* @param {Array} alignmentTracks - Array of alignment track objects (reference + others)
|
|
78
|
+
* @returns {Object} Alignment config for addAlignment
|
|
79
|
+
*/
|
|
80
|
+
export function buildAlignmentConfig(id, sequenceData, alignmentTracks) {
|
|
81
|
+
return {
|
|
82
|
+
id: 'simpleAlignment',
|
|
83
|
+
alignmentType: 'Sequencing alignment',
|
|
84
|
+
name: `Seq. ${id}`,
|
|
85
|
+
alignmentAnnotationVisibility: {
|
|
86
|
+
features: true,
|
|
87
|
+
parts: true,
|
|
88
|
+
translations: true,
|
|
89
|
+
},
|
|
90
|
+
alignmentTracks,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove a panel from panelsShown by id.
|
|
96
|
+
* @param {Array} panelsShown - Current panelsShown array (array of arrays)
|
|
97
|
+
* @param {string} panelId - The panel id to remove
|
|
98
|
+
* @returns {Array} New panelsShown with the panel filtered out
|
|
99
|
+
*/
|
|
100
|
+
export function removePanelFromShown(panelsShown, panelId) {
|
|
101
|
+
return [[...panelsShown[0].filter((p) => p.id !== panelId)]];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Return new panelsShown with the simpleAlignment panel appended.
|
|
106
|
+
* @param {Array} panelsShown - Current panelsShown (already with simpleAlignment removed)
|
|
107
|
+
* @returns {Array} New panelsShown with simpleAlignment panel added
|
|
108
|
+
*/
|
|
109
|
+
export function updatePanelsShownWithAlignment(panelsShown) {
|
|
110
|
+
return [[
|
|
111
|
+
...panelsShown[0].filter((p) => p.id !== 'simpleAlignment'),
|
|
112
|
+
{
|
|
113
|
+
id: 'simpleAlignment',
|
|
114
|
+
type: 'alignment',
|
|
115
|
+
name: 'Alignments',
|
|
116
|
+
active: true,
|
|
117
|
+
isFullscreen: false,
|
|
118
|
+
},
|
|
119
|
+
]];
|
|
120
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
export function featureToGenbankLocationString(feat) {
|
|
3
|
+
const [ inclusive1BasedStart, inclusive1BasedEnd ] = [false, false];
|
|
4
|
+
let locStr = "";
|
|
5
|
+
|
|
6
|
+
if (feat.locations && feat.locations.length > 1) {
|
|
7
|
+
feat.locations.forEach((loc, i) => {
|
|
8
|
+
locStr +=`${parseInt(loc.start, 10) + (inclusive1BasedStart ? 0 : 1)}..${parseInt(loc.end, 10) + (inclusive1BasedEnd ? 0 : 1)}`;
|
|
9
|
+
if (i !== feat.locations.length - 1) {
|
|
10
|
+
locStr += ",";
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
locStr = "join(" + locStr + ")";
|
|
14
|
+
} else {
|
|
15
|
+
locStr +=`${parseInt(feat.start, 10) + (inclusive1BasedStart ? 0 : 1)}..${parseInt(feat.end, 10) + (inclusive1BasedEnd ? 0 : 1)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (feat.strand === -1) {
|
|
19
|
+
locStr = "complement(" + locStr + ")";
|
|
20
|
+
}
|
|
21
|
+
return locStr;
|
|
22
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { attachAuthInterceptors } from './httpClientAuth';
|
|
2
3
|
import urlWhitelist from '../config/urlWhitelist';
|
|
3
4
|
|
|
4
5
|
export default function getHttpClient(extraUrls = []) {
|
|
@@ -22,3 +23,9 @@ export default function getHttpClient(extraUrls = []) {
|
|
|
22
23
|
|
|
23
24
|
return client;
|
|
24
25
|
}
|
|
26
|
+
|
|
27
|
+
export function getAuthenticatedHttpClient(extraUrls = []) {
|
|
28
|
+
const client = getHttpClient(extraUrls);
|
|
29
|
+
attachAuthInterceptors(client);
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import getHttpClient, { getAuthenticatedHttpClient } from './getHttpClient';
|
|
4
|
+
|
|
5
|
+
describe('getHttpClient', () => {
|
|
6
|
+
it('creates a generic client without auth interceptors', () => {
|
|
7
|
+
const client = getHttpClient();
|
|
8
|
+
|
|
9
|
+
expect(client.interceptors.request.handlers.filter(Boolean)).toHaveLength(1);
|
|
10
|
+
expect(client.interceptors.response.handlers.filter(Boolean)).toHaveLength(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('creates an authenticated client with auth interceptors', () => {
|
|
14
|
+
const client = getAuthenticatedHttpClient();
|
|
15
|
+
|
|
16
|
+
expect(client.interceptors.request.handlers.filter(Boolean)).toHaveLength(2);
|
|
17
|
+
expect(client.interceptors.response.handlers.filter(Boolean)).toHaveLength(1);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const AUTH_INTERCEPTORS_ATTACHED = '__opencloningHttpClientAuthAttached';
|
|
2
|
+
|
|
3
|
+
let unauthorizedHandler = null;
|
|
4
|
+
|
|
5
|
+
export function setHttpClientUnauthorizedHandler(fn) {
|
|
6
|
+
unauthorizedHandler = fn;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function attachAuthInterceptors(client, { tokenStorageKey = 'token' } = {}) {
|
|
10
|
+
if (client[AUTH_INTERCEPTORS_ATTACHED]) {
|
|
11
|
+
return client;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
client[AUTH_INTERCEPTORS_ATTACHED] = true;
|
|
15
|
+
|
|
16
|
+
client.interceptors.request.use((config) => {
|
|
17
|
+
const token = localStorage.getItem(tokenStorageKey);
|
|
18
|
+
if (token) {
|
|
19
|
+
config.headers = config.headers ?? {};
|
|
20
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
21
|
+
}
|
|
22
|
+
return config;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
client.interceptors.response.use(
|
|
26
|
+
(response) => response,
|
|
27
|
+
(error) => {
|
|
28
|
+
if (error.response?.status === 401 && unauthorizedHandler) {
|
|
29
|
+
unauthorizedHandler();
|
|
30
|
+
}
|
|
31
|
+
return Promise.reject(error);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return client;
|
|
36
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { attachAuthInterceptors, setHttpClientUnauthorizedHandler } from './httpClientAuth';
|
|
5
|
+
|
|
6
|
+
describe('httpClientAuth', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
localStorage.clear();
|
|
9
|
+
setHttpClientUnauthorizedHandler(null);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
localStorage.clear();
|
|
14
|
+
setHttpClientUnauthorizedHandler(null);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('adds the bearer token to authenticated requests', () => {
|
|
18
|
+
const client = attachAuthInterceptors(axios.create());
|
|
19
|
+
const requestFulfilled = client.interceptors.request.handlers[0].fulfilled;
|
|
20
|
+
const config = { headers: {} };
|
|
21
|
+
|
|
22
|
+
localStorage.setItem('token', '__TEST_TOKEN__');
|
|
23
|
+
|
|
24
|
+
const result = requestFulfilled(config);
|
|
25
|
+
|
|
26
|
+
expect(result).toBe(config);
|
|
27
|
+
expect(config.headers.Authorization).toBe('Bearer __TEST_TOKEN__');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('calls the shared unauthorized handler for 401 responses', async () => {
|
|
31
|
+
const client = attachAuthInterceptors(axios.create());
|
|
32
|
+
const responseRejected = client.interceptors.response.handlers[0].rejected;
|
|
33
|
+
const handler = vi.fn();
|
|
34
|
+
const error = { response: { status: 401 } };
|
|
35
|
+
|
|
36
|
+
setHttpClientUnauthorizedHandler(handler);
|
|
37
|
+
|
|
38
|
+
await expect(responseRejected(error)).rejects.toBe(error);
|
|
39
|
+
|
|
40
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not attach duplicate interceptors to the same client', () => {
|
|
44
|
+
const client = axios.create();
|
|
45
|
+
|
|
46
|
+
attachAuthInterceptors(client);
|
|
47
|
+
attachAuthInterceptors(client);
|
|
48
|
+
|
|
49
|
+
expect(client.interceptors.request.handlers.filter(Boolean)).toHaveLength(1);
|
|
50
|
+
expect(client.interceptors.response.handlers.filter(Boolean)).toHaveLength(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/utils/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export * from './readNwrite.js';
|
|
5
5
|
export * from './network.js';
|
|
6
6
|
export * from './getHttpClient.js';
|
|
7
|
+
export * from './httpClientAuth.js';
|
|
7
8
|
export * from './sourceFunctions.js';
|
|
8
9
|
export { default as error2String } from './error2String.js';
|
|
9
10
|
export * from './enzyme_utils.js';
|
|
@@ -17,3 +18,4 @@ export * from './other.js';
|
|
|
17
18
|
export { default as getTransformCoords } from './transformCoords.js';
|
|
18
19
|
export * from './selectedRegionUtils.js';
|
|
19
20
|
export * from './normalizeSyntax.js';
|
|
21
|
+
export * from './dnaUtils.js';
|
package/src/utils/network.js
CHANGED
|
@@ -100,11 +100,19 @@ export const shiftState = (newState, oldState, skipPrimers = false) => {
|
|
|
100
100
|
return shiftStateIds(newState, oldState, skipPrimers);
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
export function getSequenceIdsThatAreInput(sources) {
|
|
104
|
+
return sources.reduce((result, source) => result.concat(source.input.map(({sequence}) => sequence)), []);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getSequenceIdsThatAreNotInput(sequences, sources) {
|
|
108
|
+
const sequenceIdsThatAreInput = getSequenceIdsThatAreInput(sources);
|
|
109
|
+
return sequences.filter((sequence) => !sequenceIdsThatAreInput.includes(sequence.id)).map((sequence) => sequence.id);
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
export function getGraftSequenceId({ sources, sequences }) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const sourcesWithoutOutput = sources.filter((source) => !allSequenceIds.includes(source.id));
|
|
113
|
+
const allSequenceIds = new Set(sequences.map((seq) => seq.id));
|
|
114
|
+
const sequenceIdsThatAreNotInput = getSequenceIdsThatAreNotInput(sequences, sources);
|
|
115
|
+
const sourcesWithoutOutput = sources.filter((source) => !allSequenceIds.has(source.id));
|
|
108
116
|
if (sourcesWithoutOutput.length === 0 && sequenceIdsThatAreNotInput.length === 1) {
|
|
109
117
|
return sequenceIdsThatAreNotInput[0];
|
|
110
118
|
}
|
|
@@ -182,3 +190,18 @@ export const mergeStates = (newState, oldState, skipPrimers = false) => {
|
|
|
182
190
|
}
|
|
183
191
|
return { mergedState, idShift };
|
|
184
192
|
};
|
|
193
|
+
|
|
194
|
+
export function getParentSequencesFromSource(source, sequences) {
|
|
195
|
+
const parentSequenceIds = source.input.map(({sequence}) => sequence);
|
|
196
|
+
return sequences.filter((sequence) => parentSequenceIds.includes(sequence.id));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getParentSourcesFromSource(source, sources) {
|
|
200
|
+
const parentSourceIds = source.input.map(({sequence}) => sequence);
|
|
201
|
+
return sources.filter((source) => parentSourceIds.includes(source.id));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getSequencesNotInDatabase(sources, sequences) {
|
|
205
|
+
const sequenceIds = sources.filter((s) => !s.database_id).map((s) => s.id);
|
|
206
|
+
return sequences.filter((sequence) => sequenceIds.includes(sequence.id));
|
|
207
|
+
}
|
package/src/utils/readNwrite.js
CHANGED
|
@@ -43,6 +43,14 @@ export const file2base64 = (file) => new Promise((resolve) => {
|
|
|
43
43
|
reader.readAsDataURL(file);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
export const file2string = (file) => new Promise((resolve) => {
|
|
47
|
+
const reader = new FileReader();
|
|
48
|
+
reader.onload = () => {
|
|
49
|
+
resolve(reader.result);
|
|
50
|
+
};
|
|
51
|
+
reader.readAsText(file);
|
|
52
|
+
});
|
|
53
|
+
|
|
46
54
|
export const downloadTextFile = (text, fileName, type = 'text/plain') => {
|
|
47
55
|
const blob = new Blob([text], { type });
|
|
48
56
|
downloadBlob(blob, fileName);
|
package/src/utils/thunks.js
CHANGED
|
@@ -3,6 +3,7 @@ import { base64ToBlob, downloadStateAsJson, downloadStateAsZip, loadFilesToSessi
|
|
|
3
3
|
import { cloningActions } from '@opencloning/store/cloning';
|
|
4
4
|
import { collectParentSequencesAndSources, getSubState, mergeStates } from './network';
|
|
5
5
|
import { getVerificationFileName } from './readNwrite';
|
|
6
|
+
import { getUsedPrimerIds } from '@opencloning/store/cloning_utils';
|
|
6
7
|
|
|
7
8
|
const { setState: setCloningState } = cloningActions;
|
|
8
9
|
|
|
@@ -27,12 +28,14 @@ export const CopySequenceThunk = (sequenceId) => async (dispatch, getState) => {
|
|
|
27
28
|
const { parentSequences, parentSources } = collectParentSequencesAndSources(sourcesToCopy[0], sources, sequences);
|
|
28
29
|
sequencesToCopy.push(...parentSequences);
|
|
29
30
|
sourcesToCopy.push(...parentSources);
|
|
31
|
+
const primerIdsToCopy = getUsedPrimerIds(sourcesToCopy);
|
|
32
|
+
const primersToCopy = state.cloning.primers.filter((p) => primerIdsToCopy.includes(p.id));
|
|
30
33
|
const sequenceIds = sequencesToCopy.map((e) => e.id);
|
|
31
34
|
const filesToCopy = state.cloning.files.filter((f) => sequenceIds.includes(f.sequence_id));
|
|
32
35
|
const newState = cloneDeep({
|
|
33
36
|
sequences: sequencesToCopy,
|
|
34
37
|
sources: sourcesToCopy,
|
|
35
|
-
primers:
|
|
38
|
+
primers: primersToCopy,
|
|
36
39
|
files: filesToCopy,
|
|
37
40
|
});
|
|
38
41
|
const files = filesToCopy.map((f) => new File(
|
|
@@ -40,7 +43,7 @@ export const CopySequenceThunk = (sequenceId) => async (dispatch, getState) => {
|
|
|
40
43
|
getVerificationFileName(f),
|
|
41
44
|
{ type: 'application/octet-stream' },
|
|
42
45
|
));
|
|
43
|
-
const { mergedState, idShift } = mergeStates(newState, state.cloning,
|
|
46
|
+
const { mergedState, idShift } = mergeStates(newState, state.cloning, false);
|
|
44
47
|
dispatch(setCloningState(mergedState));
|
|
45
48
|
await loadFilesToSessionStorage(files, idShift);
|
|
46
49
|
};
|