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