@opencloning/ui 1.4.5 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 1.4.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [#629](https://github.com/manulera/OpenCloning_frontend/pull/629) [`8820965`](https://github.com/manulera/OpenCloning_frontend/commit/8820965e5a91ec29bb0c9788191123f996a8ecd8) Thanks [@manulera](https://github.com/manulera)! - \* improve category names + clear plasmids when loading new syntax
8
+ - improve handling of files uploaded by user or associated with kit
9
+ - Allow downloading the results from the Assembler.
10
+ - Updated dependencies []:
11
+ - @opencloning/store@1.4.6
12
+ - @opencloning/utils@1.4.6
13
+
3
14
  ## 1.4.5
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -25,8 +25,8 @@
25
25
  "@emotion/styled": "^11.14.0",
26
26
  "@mui/icons-material": "^5.15.17",
27
27
  "@mui/material": "^5.15.17",
28
- "@opencloning/store": "1.4.5",
29
- "@opencloning/utils": "1.4.5",
28
+ "@opencloning/store": "1.4.6",
29
+ "@opencloning/utils": "1.4.6",
30
30
  "@teselagen/bio-parsers": "^0.4.34",
31
31
  "@teselagen/ove": "^0.8.34",
32
32
  "@teselagen/range-utils": "^0.3.20",
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
4
4
  import { AssemblerComponent } from './Assembler';
5
5
 
6
+
6
7
  // Test config
7
8
  const testConfig = {
8
9
  backendUrl: 'http://localhost:8000',
@@ -33,6 +34,25 @@ const dummyResponse = {
33
34
  }
34
35
  ]
35
36
  }
37
+ const dummyResponse2 = {
38
+ sources: [{
39
+ ...dummyResponse.sources[0],
40
+ id: 2,
41
+ type: 'RestrictionAndLigationSource',
42
+ input: [
43
+ {
44
+ sequence: 1,
45
+ },
46
+ {
47
+ sequence: 2,
48
+ }],
49
+ }
50
+ ],
51
+ sequences: [{
52
+ ...dummyResponse.sequences[0],
53
+ id: 2,
54
+ }],
55
+ }
36
56
 
37
57
  // Test data
38
58
  const mockPlasmids = [
@@ -91,12 +111,15 @@ describe('<AssemblerComponent />', () => {
91
111
  win.localStorage.clear();
92
112
  });
93
113
 
114
+ const addAlertStub = cy.stub().as('addAlert')
94
115
  cy.mount(
95
116
  <ConfigProvider config={testConfig}>
96
117
  <AssemblerComponent
97
118
  plasmids={mockPlasmids}
98
119
  categories={mockCategories}
99
120
  assemblyEnzyme="assembly_enzyme"
121
+ addAlert={addAlertStub}
122
+ appInfo={{}}
100
123
  />
101
124
  </ConfigProvider>,
102
125
  );
@@ -203,6 +226,7 @@ describe('<AssemblerComponent />', () => {
203
226
  // Error should be cleared
204
227
  cy.get('.MuiAlert-colorError').should('not.exist');
205
228
  });
229
+
206
230
  it('works in normal case', () => {
207
231
  // Mock successful source fetching
208
232
  cy.intercept('POST', 'http://localhost:8000/repository_id*', {
@@ -217,19 +241,57 @@ describe('<AssemblerComponent />', () => {
217
241
  expect(req.body.source.restriction_enzymes).to.include('assembly_enzyme');
218
242
  req.reply({
219
243
  statusCode: 200,
220
- body: dummyResponse,
244
+ body: dummyResponse2,
221
245
  });
222
246
  }).as('assemblySuccess');
223
247
 
224
248
  // Click submit button
225
249
  cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
226
250
  cy.wait('@fetchSourceSuccess');
251
+ cy.wait('@assemblySuccess');
227
252
 
228
253
  // Check that the table displays the name
229
254
  cy.get('[data-testid="assembler-product-table"]').contains('Category 1').should('exist');
230
255
  cy.get('[data-testid="assembler-product-table"]').contains('Category 2').should('exist');
231
256
  cy.get('[data-testid="assembler-product-table"]').contains('Test Plasmid 1').should('exist');
232
257
  cy.get('[data-testid="assembler-product-table"]').contains('Test Plasmid 2').should('exist');
258
+
259
+ // Stub URL.createObjectURL before clicking (it's called with a Blob, not the filename)
260
+ cy.window().then((win) => {
261
+ cy.stub(win.URL, 'createObjectURL').callsFake((blob) => `blob:mock-${blob?.size ?? 0}`).as('createObjectURL');
262
+ });
263
+
264
+ // Click download assemblies button
265
+ cy.get('[data-testid="assembler-download-assemblies-button"]').click();
266
+
267
+ // Check that the download was triggered (createObjectURL receives the zip Blob)
268
+
269
+ cy.get('@createObjectURL').should((stub) => {
270
+ expect(stub).to.have.been.calledOnce;
271
+ expect(stub.firstCall.args[0]).to.be.instanceOf(Blob);
272
+ expect(stub.firstCall.args[0].type).to.equal('application/zip');
273
+ });
274
+
275
+ cy.get('@addAlert').should('have.not.been.called');
276
+
277
+ });
278
+ it('displays error message when downloading assemblies fails', () => {
279
+
280
+ cy.intercept('POST', 'http://localhost:8000/repository_id*', { statusCode: 200, body: dummyResponse }).as('fetchSourceSuccess');
281
+ cy.intercept('POST', 'http://localhost:8000/restriction_and_ligation*', { statusCode: 200, body: dummyResponse }).as('assemblySuccess');
282
+
283
+ cy.get('[data-testid="assembler-submit-button"]').should('be.visible').click();
284
+ cy.wait('@fetchSourceSuccess');
285
+ cy.wait('@assemblySuccess');
286
+
287
+
288
+ cy.get('[data-testid="assembler-download-assemblies-button"]').click();
289
+
290
+ cy.get('@addAlert').should('have.been.calledOnce');
291
+ cy.get('@addAlert').should((stub) => {
292
+ expect(stub.firstCall.args[0].message).to.include('Error downloading assemblies:');
293
+ expect(stub.firstCall.args[0].severity).to.equal('error');
294
+ });
233
295
  });
234
296
  });
235
297
 
@@ -5,19 +5,20 @@ import {
5
5
  } from '@mui/material'
6
6
  import { Clear as ClearIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
7
7
  import { useAssembler } from './useAssembler';
8
- import { useDispatch } from 'react-redux';
8
+ import { useDispatch, useSelector } from 'react-redux';
9
9
  import { cloningActions } from '@opencloning/store/cloning';
10
10
  import AssemblerPart from './AssemblerPart';
11
11
 
12
12
  import useCombinatorialAssembly from './useCombinatorialAssembly';
13
13
  import ExistingSyntaxDialog from './ExistingSyntaxDialog';
14
14
  import error2String from '@opencloning/utils/error2String';
15
- import { categoryFilter } from './assembler_utils';
15
+ import { categoryFilter, downloadAssemblerFilesAsZip, getFilesToExportFromAssembler } from './assembler_utils';
16
16
  import useBackendRoute from '../../hooks/useBackendRoute';
17
17
  import useHttpClient from '../../hooks/useHttpClient';
18
18
  import useAlerts from '../../hooks/useAlerts';
19
19
  import UploadPlasmidsButton from './UploadPlasmidsButton';
20
20
  import { useConfig } from '../../providers';
21
+ import { isEqual } from 'lodash-es';
21
22
 
22
23
 
23
24
  const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
@@ -108,7 +109,7 @@ function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, a
108
109
  const { key, ...restProps } = props
109
110
  const plasmid = plasmids.find((d) => d.id === option)
110
111
  return (
111
- <MenuItem key={key} {...restProps} sx={{ backgroundColor: plasmid.type === 'loadedFile' ? 'success.light' : undefined }}>
112
+ <MenuItem key={key} {...restProps} sx={{ backgroundColor: plasmid.userUploaded === true ? '#dcedc8' : undefined }}>
112
113
  {formatItemName(plasmid)}
113
114
  </MenuItem>
114
115
  )}}
@@ -123,7 +124,7 @@ function AssemblerBox({ item, index, setCategory, setId, categories, plasmids, a
123
124
  )
124
125
  }
125
126
 
126
- export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
127
+ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme, addAlert, appInfo }) {
127
128
 
128
129
  const [requestedAssemblies, setRequestedAssemblies] = React.useState([])
129
130
  const [errorMessage, setErrorMessage] = React.useState('')
@@ -137,7 +138,7 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
137
138
  const { assembly, setCategory, setId, expandedAssemblies, assemblyComplete, canBeSubmitted, currentCategories } = useCombinatorialAssembly({ onValueChange: clearAssemblySelection, categories, plasmids })
138
139
  const { requestSources, requestAssemblies } = useAssembler()
139
140
 
140
- const onSubmitAssembly = async () => {
141
+ const onSubmitAssembly = React.useCallback(async () => {
141
142
  clearAssemblySelection()
142
143
  const selectedPlasmids = assembly.map(({ plasmidIds }) => plasmidIds.map((id) => (plasmids.find((item) => item.id === id))))
143
144
 
@@ -159,7 +160,20 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
159
160
  } finally {
160
161
  setLoadingMessage(false)
161
162
  }
162
- }
163
+ }, [assemblyEnzyme, assembly, plasmids, requestSources, requestAssemblies, clearAssemblySelection])
164
+
165
+ const onDownloadAssemblies = React.useCallback(async () => {
166
+ try {
167
+ const files = getFilesToExportFromAssembler({requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo})
168
+ await downloadAssemblerFilesAsZip(files);
169
+ } catch (error) {
170
+ console.error('Error downloading assemblies:', error);
171
+ addAlert({
172
+ message: `Error downloading assemblies: ${error.message}`,
173
+ severity: 'error',
174
+ });
175
+ }
176
+ }, [requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo, addAlert])
163
177
 
164
178
  const options = React.useMemo(() => assemblyComplete ? assembly : [...assembly, { category: '', plasmidIds: [] }], [assembly, assemblyComplete])
165
179
 
@@ -171,17 +185,26 @@ export function AssemblerComponent({ plasmids, categories, assemblyEnzyme }) {
171
185
  <AssemblerBox key={index} {...{item, index, setCategory, setId, categories, plasmids, assembly}} />
172
186
  )}
173
187
  </Stack>
174
- {canBeSubmitted && <>
175
- <Button
176
- sx={{ p: 2, px: 4, my: 2, fontSize: '1.2rem' }}
188
+ <Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, my: 2 }}>
189
+ {canBeSubmitted && <>
190
+ <Button
191
+ sx={{ p: 2, fontSize: '1.2rem' }}
192
+ variant="contained"
193
+ color="primary"
194
+ data-testid="assembler-submit-button"
195
+ onClick={onSubmitAssembly}
196
+ disabled={Boolean(loadingMessage)}>
197
+ {loadingMessage ? <><CircularProgress /> {loadingMessage}</> : 'Submit'}
198
+ </Button>
199
+ </>}
200
+ {requestedAssemblies.length > 0 && <Button
201
+ color="success"
177
202
  variant="contained"
178
- color="primary"
179
- data-testid="assembler-submit-button"
180
- onClick={onSubmitAssembly}
181
- disabled={Boolean(loadingMessage)}>
182
- {loadingMessage ? <><CircularProgress /> {loadingMessage}</> : 'Submit'}
183
- </Button>
184
- </>}
203
+ data-testid="assembler-download-assemblies-button"
204
+ sx={{ p: 2, fontSize: '1.2rem' }}
205
+ onClick={onDownloadAssemblies}>Download Assemblies
206
+ </Button>}
207
+ </Box>
185
208
  {errorMessage && <Alert severity="error" sx={{ my: 2, maxWidth: 300, margin: 'auto', fontSize: '1.2rem' }}>{errorMessage}</Alert>}
186
209
  {requestedAssemblies.length > 0 &&
187
210
  <AssemblerProductTable {...{requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories}} />
@@ -196,10 +219,10 @@ function displayNameFromCategory(category) {
196
219
  if (category.name) {
197
220
  name = category.name
198
221
  if (category.info)
199
- name += ` (${category.info}) `
222
+ name += ` (${category.info})`
200
223
  }
201
224
  if (category.left_name && category.right_name) {
202
- name += `${category.left_name}_${category.right_name}`
225
+ name += ` (${category.left_name}_${category.right_name})`
203
226
  }
204
227
  if (name === '') {
205
228
  name = category.key
@@ -238,7 +261,7 @@ function categoriesFromSyntaxAndPlasmids(syntax, plasmids) {
238
261
  return newCategories
239
262
  }
240
263
 
241
- function LoadSyntaxButton({ setSyntax, addPlasmids }) {
264
+ function LoadSyntaxButton({ setSyntax, addPlasmids, clearPlasmids }) {
242
265
  const [existingSyntaxDialogOpen, setExistingSyntaxDialogOpen] = React.useState(false)
243
266
  const httpClient = useHttpClient();
244
267
  const { staticContentPath } = useConfig();
@@ -249,6 +272,7 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
249
272
  try {
250
273
  await httpClient.post(url, syntax);
251
274
  setSyntax(syntax)
275
+ clearPlasmids()
252
276
  addPlasmids(plasmids)
253
277
  } catch (error) {
254
278
  addAlert({
@@ -256,7 +280,7 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
256
280
  severity: 'error',
257
281
  });
258
282
  }
259
- }, [setSyntax, addPlasmids, httpClient, backendRoute, addAlert])
283
+ }, [setSyntax, addPlasmids, clearPlasmids, httpClient, backendRoute, addAlert])
260
284
  return <>
261
285
  <Button color="success" onClick={() => setExistingSyntaxDialogOpen(true)}>Load Syntax</Button>
262
286
  {existingSyntaxDialogOpen && <ExistingSyntaxDialog staticContentPath={staticContentPath} onClose={() => setExistingSyntaxDialogOpen(false)} onSyntaxSelect={onSyntaxSelect}/>}
@@ -268,6 +292,8 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
268
292
  function Assembler() {
269
293
  const [syntax, setSyntax] = React.useState(null);
270
294
  const [plasmids, setPlasmids] = React.useState([])
295
+ const { addAlert } = useAlerts();
296
+ const appInfo = useSelector(({ cloning }) => cloning.appInfo, isEqual);
271
297
 
272
298
  const categories = React.useMemo(() => {
273
299
  return categoriesFromSyntaxAndPlasmids(syntax, plasmids)
@@ -280,8 +306,12 @@ function Assembler() {
280
306
  })
281
307
  }, [])
282
308
 
309
+ const clearLoadedPlasmids = React.useCallback(() => {
310
+ setPlasmids(prev => prev.filter((plasmid) => plasmid.userUploaded !== true))
311
+ }, [])
312
+
283
313
  const clearPlasmids = React.useCallback(() => {
284
- setPlasmids(prev => prev.filter((plasmid) => plasmid.type !== 'loadedFile'))
314
+ setPlasmids([])
285
315
  }, [])
286
316
 
287
317
  return (
@@ -290,11 +320,11 @@ function Assembler() {
290
320
  The Assembler is experimental. Use with caution.
291
321
  </Alert>
292
322
  <ButtonGroup>
293
- <LoadSyntaxButton setSyntax={setSyntax} addPlasmids={addPlasmids} />
323
+ <LoadSyntaxButton setSyntax={setSyntax} addPlasmids={addPlasmids} clearPlasmids={clearPlasmids} />
294
324
  {syntax && <UploadPlasmidsButton addPlasmids={addPlasmids} syntax={syntax} />}
295
- {syntax && <Button color="error" onClick={clearPlasmids}>Remove uploaded plasmids</Button>}
325
+ {syntax && <Button color="error" onClick={clearLoadedPlasmids}>Remove uploaded plasmids</Button>}
296
326
  </ButtonGroup>
297
- {syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories} assemblyEnzyme={syntax.assemblyEnzyme} />}
327
+ {syntax && <AssemblerComponent plasmids={plasmids} syntax={syntax} categories={categories} assemblyEnzyme={syntax.assemblyEnzyme} addAlert={addAlert} appInfo={appInfo} />}
298
328
  </>
299
329
  )
300
330
  }
@@ -2,7 +2,6 @@ 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
3
  import getHttpClient from '@opencloning/utils/getHttpClient';
4
4
  import RequestStatusWrapper from '../form/RequestStatusWrapper';
5
- import { useConfig } from '../../providers';
6
5
  import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
7
6
  import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
8
7
  import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
@@ -26,6 +26,7 @@ function formatPlasmid(sequenceData) {
26
26
  key: `${left_overhang}-${right_overhang}`,
27
27
  sequenceData,
28
28
  genbankString: jsonToGenbank(sequenceData),
29
+ userUploaded: true,
29
30
  };
30
31
 
31
32
  }
@@ -2,6 +2,9 @@ import { isRangeWithinRange } from '@teselagen/range-utils';
2
2
  import { getComplementSequenceString, getAminoAcidFromSequenceTriplet, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString } from '@teselagen/sequence-utils';
3
3
  import { allSimplePaths } from 'graphology-simple-path';
4
4
  import { openCycleAtNode } from './graph_utils';
5
+ import { downloadBlob, formatStateForJsonExport, getZipFileBlob } from '@opencloning/utils/readNwrite';
6
+ import { getGraftSequenceId } from '@opencloning/utils/network';
7
+ import { TextReader} from '@zip.js/zip.js';
5
8
 
6
9
  export function tripletsToTranslation(triplets) {
7
10
  if (!triplets) return ''
@@ -146,3 +149,45 @@ export function categoryFilter(category, categories, previousCategoryId) {
146
149
  const previousCategory = categories.find((category) => category.id === previousCategoryId)
147
150
  return previousCategory?.right_overhang === category.left_overhang
148
151
  }
152
+
153
+ export function getFilesToExportFromAssembler({requestedAssemblies, expandedAssemblies, plasmids, currentCategories, categories, appInfo}) {
154
+ const files2Export = [];
155
+ const categoryNames = ['Assembly', ...currentCategories.map(categoryId => categories.find(c => c.id === categoryId).displayName)];
156
+ const assemblyNames = expandedAssemblies.map((assembly, index) => {
157
+ return [index + 1, ...assembly.map(part => plasmids.find(p => p.id === part).plasmid_name)];
158
+ });
159
+ for (const delimiter of ['\t', ',']) {
160
+ const tableHeader = categoryNames.join(delimiter);
161
+ const tableRows = assemblyNames.map(assemblyName => assemblyName.join(delimiter));
162
+ const table = [tableHeader, ...tableRows].join('\n');
163
+ const extension = delimiter === '\t' ? 'tsv' : 'csv';
164
+ files2Export.push({
165
+ name: `assemblies.${extension}`,
166
+ content: table,
167
+ });
168
+ }
169
+
170
+ for (let i = 0; i < requestedAssemblies.length; i++) {
171
+ const name = `${String(i + 1).padStart(3, '0')}_${assemblyNames[i].slice(1).join('+')}`;
172
+ const requestedAssembly = requestedAssemblies[i];
173
+ const jsonContent = formatStateForJsonExport({...requestedAssembly, appInfo});
174
+ files2Export.push({
175
+ name: `${name}.json`,
176
+ content: JSON.stringify(jsonContent, null, 2),
177
+ });
178
+ const finalSequenceId = getGraftSequenceId(requestedAssembly)
179
+ const finalSequence = requestedAssembly.sequences.find(s => s.id === finalSequenceId)
180
+ files2Export.push({
181
+ name: `${name}.gbk`,
182
+ content: finalSequence.file_content,
183
+ });
184
+ }
185
+ return files2Export;
186
+ }
187
+
188
+ export async function downloadAssemblerFilesAsZip(files) {
189
+ const files2write = files.map(({name, content}) => ({name, reader: new TextReader(content)}));
190
+
191
+ const blob = await getZipFileBlob(files2write);
192
+ downloadBlob(blob, 'assemblies.zip');
193
+ }
@@ -1,8 +1,8 @@
1
1
  import { aliasedEnzymesByName, getDigestFragmentsForRestrictionEnzymes, getReverseComplementSequenceString, getComplementSequenceString, getReverseComplementSequenceAndAnnotations } from "@teselagen/sequence-utils";
2
2
  import fs from 'fs';
3
- import { assignSequenceToSyntaxPart, simplifyDigestFragment, reverseComplementSimplifiedDigestFragment, tripletsToTranslation, partDataToDisplayData, arrayCombinations, getSimplifiedDigestFragments } from "./assembler_utils";
3
+ import { assignSequenceToSyntaxPart, simplifyDigestFragment, reverseComplementSimplifiedDigestFragment, tripletsToTranslation, partDataToDisplayData, arrayCombinations, getFilesToExportFromAssembler } from "./assembler_utils";
4
4
  import { partsToEdgesGraph } from "./graph_utils";
5
- import { genbankToJson } from '@teselagen/bio-parsers';
5
+
6
6
 
7
7
  const sequenceBsaI = 'tgggtctcaTACTagagtcacacaggactactaAATGagagacctac';
8
8
  const sequenceBsaI2 = 'tgggtctcaAATGagagtcacacaggactactaAGGTagagacctac'
@@ -257,3 +257,57 @@ describe('arrayCombinations', () => {
257
257
  expect(arrayCombinations([[1, 2], [3, 4], [5, 6]])).toEqual([[1, 3, 5], [1, 3, 6], [1, 4, 5], [1, 4, 6], [2, 3, 5], [2, 3, 6], [2, 4, 5], [2, 4, 6]]);
258
258
  });
259
259
  });
260
+
261
+
262
+ const goldenGateCloningStrategy = JSON.parse(fs.readFileSync('apps/opencloning/public/examples/golden_gate.json', 'utf8'));
263
+ const gatewayCloningStrategy = JSON.parse(fs.readFileSync('apps/opencloning/public/examples/gateway.json', 'utf8'));
264
+
265
+ const dummyData = {
266
+ requestedAssemblies: [ goldenGateCloningStrategy, gatewayCloningStrategy],
267
+ expandedAssemblies: [[1, 5, 7], [1, 2, 3]],
268
+ plasmids: [
269
+ {id: 1, plasmid_name: 'p1'},
270
+ {id: 2, plasmid_name: 'p2'},
271
+ {id: 3, plasmid_name: 'p3'},
272
+ {id: 4, plasmid_name: 'p4'},
273
+ {id: 5, plasmid_name: 'p5'},
274
+ {id: 6, plasmid_name: 'p6'},
275
+ {id: 7, plasmid_name: 'p7'},
276
+ ],
277
+ currentCategories: [1, 2, 3],
278
+ categories: [
279
+ {id: 1, displayName: 'Category 1'},
280
+ {id: 2, displayName: 'Category 2'},
281
+ {id: 3, displayName: 'Category 3'},
282
+ ],
283
+ appInfo: {backendVersion: '0.5.1', schemaVersion: '0.4.9', frontendVersion: '__VERSION__'},
284
+ };
285
+
286
+
287
+
288
+ describe('getZipFileFromAssemblies', () => {
289
+ it('returns a zip file', () => {
290
+ const files = getFilesToExportFromAssembler(dummyData);
291
+ expect(files[0].name).toBe('assemblies.tsv');
292
+ expect(files[0].content).toBe('Assembly\tCategory 1\tCategory 2\tCategory 3\n1\tp1\tp5\tp7\n2\tp1\tp2\tp3');
293
+ expect(files[1].name).toBe('assemblies.csv');
294
+ expect(files[1].content).toBe('Assembly,Category 1,Category 2,Category 3\n1,p1,p5,p7\n2,p1,p2,p3');
295
+
296
+ const fileNames = ['001_p1+p5+p7', '002_p1+p2+p3'];
297
+ for (let i = 0; i < 2; i++) {
298
+ const fileIndex1 = i * 2 + 2;
299
+ const fileIndex2 = fileIndex1 + 1;
300
+ expect(files[fileIndex1].name).toBe(`${fileNames[i]}.json`);
301
+ const cloningStrategy = JSON.parse(files[fileIndex1].content);
302
+ expect(cloningStrategy.sequences).toEqual(dummyData.requestedAssemblies[i].sequences);
303
+ expect(cloningStrategy.sources).toEqual(dummyData.requestedAssemblies[i].sources);
304
+ expect(cloningStrategy.primers).toEqual(dummyData.requestedAssemblies[i].primers);
305
+
306
+ expect(files[fileIndex2].name).toBe(`${fileNames[i]}.gbk`);
307
+ const genbankContent = files[fileIndex2].content;
308
+ expect(genbankContent).toBe(dummyData.requestedAssemblies[i].sequences[dummyData.requestedAssemblies[i].sequences.length - 1].file_content);
309
+ }
310
+
311
+
312
+ })
313
+ })
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Version placeholder - replaced at publish time via prepack script
2
- export const version = "1.4.5";
2
+ export const version = "1.4.6";