@opencloning/ui 1.1.2 → 1.3.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 (41) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +5 -4
  3. package/src/components/MainSequenceEditor.jsx +2 -0
  4. package/src/components/assembler/Assembler.cy.jsx +364 -0
  5. package/src/components/assembler/Assembler.jsx +298 -206
  6. package/src/components/assembler/AssemblerPart.cy.jsx +52 -0
  7. package/src/components/assembler/AssemblerPart.jsx +51 -79
  8. package/src/components/assembler/ExistingSyntaxDialog.cy.jsx +194 -0
  9. package/src/components/assembler/ExistingSyntaxDialog.jsx +65 -0
  10. package/src/components/assembler/PlasmidSyntaxTable.jsx +83 -0
  11. package/src/components/assembler/assembler_utils.js +134 -0
  12. package/src/components/assembler/assembler_utils.test.js +193 -0
  13. package/src/components/assembler/assembly_component.module.css +1 -1
  14. package/src/components/assembler/graph_utils.js +153 -0
  15. package/src/components/assembler/graph_utils.test.js +239 -0
  16. package/src/components/assembler/index.js +9 -0
  17. package/src/components/assembler/useAssembler.js +59 -22
  18. package/src/components/assembler/useCombinatorialAssembly.js +76 -0
  19. package/src/components/assembler/usePlasmidsLogic.js +82 -0
  20. package/src/components/eLabFTW/utils.js +0 -9
  21. package/src/components/index.js +2 -0
  22. package/src/components/navigation/SelectTemplateDialog.jsx +0 -1
  23. package/src/components/primers/DownloadPrimersButton.jsx +0 -1
  24. package/src/components/primers/PrimerList.jsx +4 -3
  25. package/src/components/primers/import_primers/ImportPrimersButton.jsx +0 -1
  26. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx +110 -91
  27. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGatewayBP.jsx +1 -1
  28. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignGibsonAssembly.jsx +2 -2
  29. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignHomologousRecombination.jsx +1 -1
  30. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignRestriction.jsx +1 -1
  31. package/src/components/primers/primer_design/SequenceTabComponents/PrimerDesignSimplePair.jsx +1 -1
  32. package/src/components/primers/primer_design/SequenceTabComponents/PrimerSpacerForm.jsx +5 -22
  33. package/src/components/primers/primer_design/SequenceTabComponents/TabPannelSettings.jsx +6 -4
  34. package/src/hooks/useStoreEditor.js +4 -0
  35. package/src/version.js +1 -1
  36. package/vitest.config.js +2 -4
  37. package/src/components/DraggableDialogPaper.jsx +0 -16
  38. package/src/components/assembler/AssemblePartWidget.jsx +0 -252
  39. package/src/components/assembler/StopIcon.jsx +0 -34
  40. package/src/components/assembler/assembler_data2.json +0 -50
  41. package/src/components/assembler/moclo.json +0 -110
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#597](https://github.com/manulera/OpenCloning_frontend/pull/597) [`d5a456d`](https://github.com/manulera/OpenCloning_frontend/commit/d5a456d70ccfe949b21aae260d2c99507ff6a88e) Thanks [@manulera](https://github.com/manulera)! - Changes associated with new "Syntax Builder" application for creating and managing modular cloning syntaxes, along with significant refactoring of assembler components to support both the new app and the existing OpenCloning application.
8
+
9
+ - Added a new standalone app (`apps/syntax-builder`) for building and editing cloning syntaxes with visual previews
10
+ - Refactored assembler components to be more modular and reusable across applications
11
+ - Enhanced file parsing utilities to support bidirectional conversion between JSON and delimited formats
12
+ - Added graph-based validation and visualization for syntax parts using the graphology library
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [[`d5a456d`](https://github.com/manulera/OpenCloning_frontend/commit/d5a456d70ccfe949b21aae260d2c99507ff6a88e)]:
17
+ - @opencloning/store@1.3.0
18
+ - @opencloning/utils@1.3.0
19
+
20
+ ## 1.2.0
21
+
22
+ ### Minor Changes
23
+
24
+ - [#595](https://github.com/manulera/OpenCloning_frontend/pull/595) [`1b28cc5`](https://github.com/manulera/OpenCloning_frontend/commit/1b28cc5852460a072982dc529b58fc9607fae21f) Thanks [@manulera](https://github.com/manulera)! - Minor improvements and bug fixes:
25
+
26
+ - include name of tracks in alignment + update ove to display correct Track Properties table
27
+ - fix display main sequence when alignments are present
28
+ - change default minimum hib length to 14 for primer design
29
+ - Gibson primer design: default to circular assembly, force circular for single input assemblies
30
+ - Gibson primer design: make product sequence preview circular when assembly is circular
31
+ - Primer design: in circular assemblies of one fragment only, display the spacer before the fragment in the preview.
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies []:
36
+ - @opencloning/store@1.2.0
37
+ - @opencloning/utils@1.2.0
38
+
3
39
  ## 1.1.2
4
40
 
5
41
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "exports": {
11
11
  ".": "./src/index.js",
12
12
  "./components": "./src/components/index.js",
13
+ "./components/assembler": "./src/components/assembler/index.js",
13
14
  "./providers/ConfigProvider": "./src/providers/index.js",
14
15
  "./hooks/useConfig": "./src/hooks/useConfig.js",
15
16
  "./standalone": "./src/StandAloneOpenCloning.js"
@@ -24,10 +25,10 @@
24
25
  "@emotion/styled": "^11.14.0",
25
26
  "@mui/icons-material": "^5.15.17",
26
27
  "@mui/material": "^5.15.17",
27
- "@opencloning/store": "1.1.2",
28
- "@opencloning/utils": "1.1.2",
28
+ "@opencloning/store": "1.3.0",
29
+ "@opencloning/utils": "1.3.0",
29
30
  "@teselagen/bio-parsers": "^0.4.32",
30
- "@teselagen/ove": "^0.8.18",
31
+ "@teselagen/ove": "^0.8.30",
31
32
  "@teselagen/range-utils": "^0.3.13",
32
33
  "@teselagen/sequence-utils": "^0.3.35",
33
34
  "@zip.js/zip.js": "^2.7.62",
@@ -65,6 +65,8 @@ function MainSequenceEditor() {
65
65
  (state) => {
66
66
  const history = state.VectorEditor.mainEditor?.sequenceDataHistory;
67
67
  if (!history) return false;
68
+ const sequenceId = state.VectorEditor.mainEditor?.sequenceData?.id;
69
+ if (sequenceId === 'opencloning_primer_design_product') return false;
68
70
  return state.cloning.mainSequenceId && Object.keys(history).length > 0 && history.future.length === 0;
69
71
  }
70
72
  );
@@ -0,0 +1,364 @@
1
+ /* eslint-disable camelcase */
2
+ import React from 'react';
3
+ import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
4
+ import { AssemblerComponent, UploadPlasmidsButton } from './Assembler';
5
+ import mocloSyntax from '../../../../../cypress/test_files/syntax/moclo_syntax.json';
6
+
7
+ mocloSyntax.overhangNames = {
8
+ ...mocloSyntax.overhangNames,
9
+ CCCT: 'CCCT_overhang',
10
+ AACG: 'AACG_overhang',
11
+ };
12
+
13
+ // Test config
14
+ const testConfig = {
15
+ backendUrl: 'http://localhost:8000',
16
+ showAppBar: false,
17
+ noExternalRequests: false,
18
+ enableAssembler: true,
19
+ enablePlannotate: false,
20
+ };
21
+
22
+ const dummyResponse = {
23
+ sources: [
24
+ {
25
+ id: 1,
26
+ type: "ManuallyTypedSource",
27
+ output_name: null,
28
+ database_id: null,
29
+ input: []
30
+ }
31
+ ],
32
+ sequences: [
33
+ {
34
+ id: 1,
35
+ type: "TextFileSequence",
36
+ sequence_file_format: "genbank",
37
+ overhang_crick_3prime: 0,
38
+ overhang_watson_3prime: 0,
39
+ file_content: "LOCUS name 5 bp DNA linear UNK 01-JAN-1980\nDEFINITION description.\nACCESSION id\nVERSION id\nKEYWORDS .\nSOURCE .\n ORGANISM .\n .\nFEATURES Location/Qualifiers\nORIGIN\n 1 aaaaa\n//"
40
+ }
41
+ ]
42
+ }
43
+
44
+ // Test data
45
+ const mockPlasmids = [
46
+ {
47
+ id: 1,
48
+ plasmid_name: 'Test Plasmid 1',
49
+ left_overhang: 'CCCT',
50
+ right_overhang: 'AACG',
51
+ key: 'CCCT-AACG',
52
+ type: 'AddgeneIdSource',
53
+ source: {
54
+ id: 1,
55
+ type: 'AddgeneIdSource',
56
+ input: [],
57
+ repository_id: '12345',
58
+ },
59
+ },
60
+ {
61
+ id: 2,
62
+ plasmid_name: 'Test Plasmid 2',
63
+ left_overhang: 'AACG',
64
+ right_overhang: 'CCCT',
65
+ key: 'AACG-CCCT',
66
+ type: 'AddgeneIdSource',
67
+ source: {
68
+ id: 2,
69
+ type: 'AddgeneIdSource',
70
+ input: [],
71
+ repository_id: '67890',
72
+ },
73
+ },
74
+ ];
75
+
76
+ const mockCategories = [
77
+ {
78
+ id: 1,
79
+ displayName: 'Category 1',
80
+ left_overhang: 'CCCT',
81
+ right_overhang: 'AACG',
82
+ key: 'CCCT-AACG',
83
+ },
84
+ {
85
+ id: 2,
86
+ displayName: 'Category 2',
87
+ left_overhang: 'AACG',
88
+ right_overhang: 'CCCT',
89
+ key: 'AACG-CCCT',
90
+ },
91
+ ];
92
+
93
+ describe('<AssemblerComponent />', () => {
94
+ beforeEach(() => {
95
+ // Set up a complete assembly for testing
96
+ cy.window().then((win) => {
97
+ // Ensure we have a clean state
98
+ win.localStorage.clear();
99
+ });
100
+
101
+ cy.mount(
102
+ <ConfigProvider config={testConfig}>
103
+ <AssemblerComponent
104
+ plasmids={mockPlasmids}
105
+ categories={mockCategories}
106
+ />
107
+ </ConfigProvider>,
108
+ );
109
+ // Select first plasmid
110
+ cy.get('[data-testid="plasmid-select"]').first().within(() => {
111
+ cy.get('input').click();
112
+ });
113
+ cy.get('li').contains('Test Plasmid 1').click();
114
+
115
+ // Select second plasmid
116
+ cy.get('[data-testid="plasmid-select"]').eq(1).within(() => {
117
+ cy.get('input').click();
118
+ });
119
+ cy.get('li').contains('Test Plasmid 2').click();
120
+ });
121
+
122
+ it('displays error when fetching sequence for a plasmid fails', () => {
123
+ // Intercept the requestSources call (POST to addgene endpoint)
124
+ cy.intercept('POST', 'http://localhost:8000/repository_id*', {
125
+ statusCode: 500,
126
+ body: {
127
+ detail: 'Failed to fetch plasmid sequence',
128
+ },
129
+ }).as('fetchPlasmidError');
130
+
131
+ // Click submit button
132
+ cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
133
+
134
+ // Wait for the error request
135
+ cy.wait('@fetchPlasmidError');
136
+
137
+ // Check that error message is displayed
138
+ cy.get('.MuiAlert-colorError').should('exist');
139
+ cy.contains('Error fetching sequence for').should('exist');
140
+ cy.contains('Test Plasmid 1').should('exist');
141
+ });
142
+
143
+ it('displays error when assembling sequences fails', () => {
144
+ // Mock successful source fetching
145
+ cy.intercept('POST', 'http://localhost:8000/repository_id*', {
146
+ statusCode: 200,
147
+ body: dummyResponse,
148
+ }).as('fetchSourceSuccess');
149
+
150
+ // Mock failed assembly request
151
+ cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', {
152
+ statusCode: 500,
153
+ body: {
154
+ detail: 'Failed to assemble sequences',
155
+ },
156
+ }).as('assemblyError');
157
+
158
+ // Click submit button
159
+ cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
160
+
161
+ // Wait for both requests
162
+ cy.wait('@fetchSourceSuccess');
163
+ cy.wait('@assemblyError');
164
+
165
+ // Check that error message is displayed
166
+ cy.get('.MuiAlert-colorError').should('exist');
167
+ cy.contains('Error assembling').should('exist');
168
+ cy.contains('Test Plasmid 1').should('exist');
169
+ cy.contains('Test Plasmid 2').should('exist');
170
+ });
171
+
172
+ it('displays network error message correctly', () => {
173
+ // Intercept with network error
174
+ cy.intercept('POST', 'http://localhost:8000/repository_id*', {
175
+ forceNetworkError: true,
176
+ }).as('networkError');
177
+
178
+ // Click submit button
179
+ cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
180
+
181
+ // Wait for the error request
182
+ cy.wait('@networkError');
183
+
184
+ // Check that error message is displayed
185
+ cy.get('.MuiAlert-colorError').should('exist');
186
+ cy.contains('Error fetching sequence for').should('exist');
187
+ });
188
+
189
+ it('clears error message when assembly selection is cleared', () => {
190
+ // Intercept with error
191
+ cy.intercept('POST', 'http://localhost:8000/repository_id*', {
192
+ statusCode: 500,
193
+ body: {
194
+ detail: 'Failed to fetch plasmid sequence',
195
+ },
196
+ }).as('fetchPlasmidError');
197
+
198
+
199
+ // Click submit button to trigger error
200
+ cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
201
+ cy.wait('@fetchPlasmidError');
202
+
203
+ // Verify error is displayed
204
+ cy.get('.MuiAlert-colorError').should('exist');
205
+
206
+ // Clear the assembly by clicking the clear button in the first category select
207
+ cy.get('[data-testid="CancelIcon"]').first().click({ force: true });
208
+
209
+ // Error should be cleared
210
+ cy.get('.MuiAlert-colorError').should('not.exist');
211
+ });
212
+ it('works in normal case', () => {
213
+ // Mock successful source fetching
214
+ cy.intercept('POST', 'http://localhost:8000/repository_id*', {
215
+ statusCode: 200,
216
+ body: dummyResponse,
217
+ }).as('fetchSourceSuccess');
218
+ // Mock assembly request
219
+ cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', {
220
+ statusCode: 200,
221
+ body: dummyResponse,
222
+ }).as('assemblySuccess');
223
+
224
+ // Click submit button
225
+ cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
226
+ cy.wait('@fetchSourceSuccess');
227
+
228
+ // Check that the table displays the name
229
+ cy.get('[data-testid="assembler-product-table"]').contains('Category 1').should('exist');
230
+ cy.get('[data-testid="assembler-product-table"]').contains('Category 2').should('exist');
231
+ cy.get('[data-testid="assembler-product-table"]').contains('Test Plasmid 1').should('exist');
232
+ cy.get('[data-testid="assembler-product-table"]').contains('Test Plasmid 2').should('exist');
233
+ });
234
+ });
235
+
236
+ describe('<UploadPlasmidsButton />', () => {
237
+ beforeEach(() => {
238
+ cy.window().then((win) => {
239
+ win.localStorage.clear();
240
+ });
241
+ });
242
+
243
+ it('calls addPlasmids with correctly formatted valid plasmid', () => {
244
+ const addPlasmidsSpy = cy.spy().as('addPlasmidsSpy');
245
+
246
+ cy.mount(
247
+ <ConfigProvider config={testConfig}>
248
+ <UploadPlasmidsButton addPlasmids={addPlasmidsSpy} syntax={mocloSyntax} />
249
+ </ConfigProvider>,
250
+ );
251
+
252
+ cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
253
+ 'cypress/test_files/syntax/pYTK002.gb',
254
+ 'cypress/test_files/syntax/moclo_ytk_multi_part.gb',
255
+ 'cypress/test_files/syntax/pYTK095.gb',
256
+ 'cypress/test_files/sequencing/locus.gb'],
257
+ { force: true });
258
+
259
+ // Wait for the dialog to appear (indicating plasmids were processed)
260
+ cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
261
+
262
+ cy.get('@addPlasmidsSpy').should('not.have.been.called');
263
+
264
+ cy.get('[data-testid="invalid-plasmids-box"]').contains('Invalid Plasmids').should('exist');
265
+ cy.get('[data-testid="valid-plasmids-box"]').contains('Valid Plasmids').should('exist');
266
+
267
+ cy.get('[data-testid="invalid-plasmids-box"]').contains('pYTK057')
268
+ cy.get('[data-testid="invalid-plasmids-box"]').contains('moclo_ytk_multi_part.gb')
269
+ cy.get('[data-testid="invalid-plasmids-box"] .MuiChip-label').contains('ATCC-TGGC')
270
+ cy.get('[data-testid="invalid-plasmids-box"] .MuiChip-label').contains('CCCT-AACG (CCCT_overhang-AACG_overhang)')
271
+ cy.get('[data-testid="invalid-plasmids-box"]').contains('Contains multiple parts')
272
+ cy.get('[data-testid="invalid-plasmids-box"]').contains('locus.gb')
273
+
274
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(0).should('contain', 'pYTK002')
275
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(1).should('contain', 'pYTK002.gb')
276
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(2).should('contain', 'CCCT-AACG (CCCT_overhang-AACG_overhang)')
277
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(3).should('contain', '1')
278
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(4).should('contain', 'ConS')
279
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).then(($el) => {
280
+ const bgColor = window.getComputedStyle($el[0]).backgroundColor;
281
+ cy.wrap(bgColor).should('equal', 'rgb(132, 197, 222)');
282
+ });
283
+
284
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(0).should('contain', 'pYTK095')
285
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(1).should('contain', 'pYTK095.gb')
286
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(2).should('contain', 'TACA-CCCT (TACA-CCCT_overhang)')
287
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(3).should('contain', 'Spans multiple parts')
288
+ cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(4).should('contain', 'AmpR')
289
+
290
+ // Click the import button
291
+ cy.contains('button', 'Import valid plasmids').click();
292
+
293
+ // Verify addPlasmids was called
294
+ cy.get('@addPlasmidsSpy').should('have.been.called');
295
+
296
+ // Verify it was called with an array and check structure
297
+ cy.get('@addPlasmidsSpy').then((spy) => {
298
+ const firstCall = spy.getCall(0);
299
+ console.log('firstCall', firstCall.args);
300
+ cy.wrap(firstCall.args[0]).should('be.an', 'array');
301
+ cy.wrap(firstCall.args[0]).should('have.length', 2);
302
+
303
+ const firstPlasmid = firstCall.args[0][0];
304
+
305
+ cy.wrap(firstPlasmid.file_name).should('equal', 'pYTK002.gb');
306
+ cy.wrap(firstPlasmid.plasmid_name).should('equal', 'pYTK002.gb (ConS)');
307
+ cy.wrap(firstPlasmid.left_overhang).should('equal', 'CCCT');
308
+ cy.wrap(firstPlasmid.right_overhang).should('equal', 'AACG');
309
+ cy.wrap(firstPlasmid.key).should('equal', 'CCCT-AACG');
310
+
311
+ const {appData} = firstPlasmid.sequenceData;
312
+
313
+ cy.wrap(appData.fileName).should('equal', 'pYTK002.gb');
314
+ cy.wrap(appData.correspondingParts).should('deep.equal', ['CCCT-AACG']);
315
+ cy.wrap(appData.correspondingPartsNames).should('deep.equal', ["CCCT_overhang-AACG_overhang"]);
316
+
317
+ });
318
+ });
319
+
320
+
321
+ it('does not allow to submit when all plasmids are invalid', () => {
322
+ cy.mount(
323
+ <ConfigProvider config={testConfig}>
324
+ <UploadPlasmidsButton addPlasmids={() => {}} syntax={mocloSyntax} />
325
+ </ConfigProvider>,
326
+ );
327
+ cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
328
+ 'cypress/test_files/sequencing/locus.gb'],
329
+ { force: true });
330
+
331
+ // Wait for the dialog to appear (indicating plasmids were processed)
332
+ cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
333
+
334
+ cy.get('[data-testid="invalid-plasmids-box"]').contains('Invalid Plasmids').should('exist');
335
+ cy.get('[data-testid="valid-plasmids-box"]').should('not.exist');
336
+
337
+ cy.get('button').contains('Import valid plasmids').should('be.disabled');
338
+
339
+ });
340
+
341
+ it('cancelling does not call addPlasmids', () => {
342
+ const addPlasmidsSpy = cy.spy().as('addPlasmidsSpy');
343
+ cy.mount(
344
+ <ConfigProvider config={testConfig}>
345
+ <UploadPlasmidsButton addPlasmids={addPlasmidsSpy} syntax={mocloSyntax} />
346
+ </ConfigProvider>,
347
+ );
348
+
349
+ cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
350
+ 'cypress/test_files/syntax/pYTK002.gb',
351
+ 'cypress/test_files/syntax/moclo_ytk_multi_part.gb',
352
+ 'cypress/test_files/syntax/pYTK095.gb',
353
+ 'cypress/test_files/sequencing/locus.gb'],
354
+ { force: true });
355
+
356
+ cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
357
+
358
+ cy.get('button').contains('Cancel').click();
359
+
360
+ cy.get('@addPlasmidsSpy').should('not.have.been.called');
361
+
362
+ });
363
+
364
+ });