@opencloning/ui 1.3.3 → 1.4.1

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,37 @@
1
1
  # @opencloning/ui
2
2
 
3
+ ## 1.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#616](https://github.com/manulera/OpenCloning_frontend/pull/616) [`1419d5c`](https://github.com/manulera/OpenCloning_frontend/commit/1419d5cb80f738b737076e4d3b958c31eeb0f7b4) Thanks [@manulera](https://github.com/manulera)! - Allow to "select all" when importing static sequences from server
8
+
9
+ - Updated dependencies []:
10
+ - @opencloning/store@1.4.1
11
+ - @opencloning/utils@1.4.1
12
+
13
+ ## 1.4.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#611](https://github.com/manulera/OpenCloning_frontend/pull/611) [`74a58c4`](https://github.com/manulera/OpenCloning_frontend/commit/74a58c463a32bf6443eaf062094dc4aac5c76b5a) Thanks [@manulera](https://github.com/manulera)! - Adds functionality to load files (sequences and syntaxes) from a local public folder, providing an alternative to uploading files manually. The implementation enables users to pre-configure collections of sequences and syntaxes that can be easily selected through a UI.
18
+
19
+ **Changes:**
20
+
21
+ - Added `useRequestForEffect` hook for managing async requests with retry capability
22
+ - Added `useServerStaticFiles` hook for fetching and managing local file collections
23
+ - Added `ServerStaticFileSelect` component for selecting files from the local collection with category filtering
24
+ - Added `SourceServerStaticFile` component and integrated it into the source selection flow
25
+ - Added local file loading capability to the assembler's plasmid uploader and syntax loader
26
+ - Configured build system to copy example collection folder to public directory during development
27
+ - Added comprehensive test coverage for all new components and functionality
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies []:
32
+ - @opencloning/store@1.4.0
33
+ - @opencloning/utils@1.4.0
34
+
3
35
  ## 1.3.3
4
36
 
5
37
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencloning/ui",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
@@ -25,15 +25,15 @@
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.3.3",
29
- "@opencloning/utils": "1.3.3",
30
- "@teselagen/bio-parsers": "^0.4.32",
31
- "@teselagen/ove": "^0.8.30",
32
- "@teselagen/range-utils": "^0.3.13",
33
- "@teselagen/sequence-utils": "^0.3.35",
28
+ "@opencloning/store": "1.4.1",
29
+ "@opencloning/utils": "1.4.1",
30
+ "@teselagen/bio-parsers": "^0.4.34",
31
+ "@teselagen/ove": "^0.8.34",
32
+ "@teselagen/range-utils": "^0.3.20",
33
+ "@teselagen/sequence-utils": "^0.3.42",
34
34
  "@zip.js/zip.js": "^2.7.62",
35
35
  "axios": "^1.12.2",
36
- "lodash-es": "^4.17.21",
36
+ "lodash-es": "^4.17.23",
37
37
  "react-draggable": "^4.4.6"
38
38
  },
39
39
  "peerDependencies": {
@@ -1,14 +1,7 @@
1
1
  /* eslint-disable camelcase */
2
2
  import React from 'react';
3
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
- };
4
+ import { AssemblerComponent } from './Assembler';
12
5
 
13
6
  // Test config
14
7
  const testConfig = {
@@ -233,138 +226,3 @@ describe('<AssemblerComponent />', () => {
233
226
  });
234
227
  });
235
228
 
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
- // This one just to verify that it works with no features
258
- 'cypress/test_files/syntax/pYTK002_no_features.gb'
259
- ],
260
- { force: true });
261
-
262
- // Wait for the dialog to appear (indicating plasmids were processed)
263
- cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
264
-
265
- cy.get('@addPlasmidsSpy').should('not.have.been.called');
266
-
267
- cy.get('[data-testid="invalid-plasmids-box"]').contains('Invalid Plasmids').should('exist');
268
- cy.get('[data-testid="valid-plasmids-box"]').contains('Valid Plasmids').should('exist');
269
-
270
- cy.get('[data-testid="invalid-plasmids-box"]').contains('pYTK057')
271
- cy.get('[data-testid="invalid-plasmids-box"]').contains('moclo_ytk_multi_part.gb')
272
- cy.get('[data-testid="invalid-plasmids-box"] .MuiChip-label').contains('ATCC-TGGC')
273
- cy.get('[data-testid="invalid-plasmids-box"] .MuiChip-label').contains('CCCT-AACG (CCCT_overhang-AACG_overhang)')
274
- cy.get('[data-testid="invalid-plasmids-box"]').contains('Contains multiple parts')
275
- cy.get('[data-testid="invalid-plasmids-box"]').contains('locus.gb')
276
-
277
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(0).should('contain', 'pYTK002')
278
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(1).should('contain', 'pYTK002.gb')
279
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(2).should('contain', 'CCCT-AACG (CCCT_overhang-AACG_overhang)')
280
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(3).should('contain', '1')
281
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).find('td').eq(4).should('contain', 'ConS')
282
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(1).then(($el) => {
283
- const bgColor = window.getComputedStyle($el[0]).backgroundColor;
284
- cy.wrap(bgColor).should('equal', 'rgb(132, 197, 222)');
285
- });
286
-
287
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(0).should('contain', 'pYTK095')
288
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(1).should('contain', 'pYTK095.gb')
289
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(2).should('contain', 'TACA-CCCT (TACA-CCCT_overhang)')
290
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(3).should('contain', 'Spans multiple parts')
291
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(2).find('td').eq(4).should('contain', 'AmpR')
292
-
293
- // No features
294
- cy.get('[data-testid="valid-plasmids-box"] tr').eq(3).find('td').eq(4).should('contain', '-');
295
-
296
- // Click the import button
297
- cy.contains('button', 'Import valid plasmids').click();
298
-
299
- // Verify addPlasmids was called
300
- cy.get('@addPlasmidsSpy').should('have.been.called');
301
-
302
- // Verify it was called with an array and check structure
303
- cy.get('@addPlasmidsSpy').then((spy) => {
304
- const firstCall = spy.getCall(0);
305
- console.log('firstCall', firstCall.args);
306
- cy.wrap(firstCall.args[0]).should('be.an', 'array');
307
- cy.wrap(firstCall.args[0]).should('have.length', 3);
308
-
309
- const firstPlasmid = firstCall.args[0][0];
310
-
311
- cy.wrap(firstPlasmid.file_name).should('equal', 'pYTK002.gb');
312
- cy.wrap(firstPlasmid.plasmid_name).should('equal', 'pYTK002.gb (ConS)');
313
- cy.wrap(firstPlasmid.left_overhang).should('equal', 'CCCT');
314
- cy.wrap(firstPlasmid.right_overhang).should('equal', 'AACG');
315
- cy.wrap(firstPlasmid.key).should('equal', 'CCCT-AACG');
316
-
317
- const {appData} = firstPlasmid.sequenceData;
318
-
319
- cy.wrap(appData.fileName).should('equal', 'pYTK002.gb');
320
- cy.wrap(appData.correspondingParts).should('deep.equal', ['CCCT-AACG']);
321
- cy.wrap(appData.correspondingPartsNames).should('deep.equal', ["CCCT_overhang-AACG_overhang"]);
322
-
323
- });
324
- });
325
-
326
-
327
- it('does not allow to submit when all plasmids are invalid', () => {
328
- cy.mount(
329
- <ConfigProvider config={testConfig}>
330
- <UploadPlasmidsButton addPlasmids={() => {}} syntax={mocloSyntax} />
331
- </ConfigProvider>,
332
- );
333
- cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
334
- 'cypress/test_files/sequencing/locus.gb'],
335
- { force: true });
336
-
337
- // Wait for the dialog to appear (indicating plasmids were processed)
338
- cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
339
-
340
- cy.get('[data-testid="invalid-plasmids-box"]').contains('Invalid Plasmids').should('exist');
341
- cy.get('[data-testid="valid-plasmids-box"]').should('not.exist');
342
-
343
- cy.get('button').contains('Import valid plasmids').should('be.disabled');
344
-
345
- });
346
-
347
- it('cancelling does not call addPlasmids', () => {
348
- const addPlasmidsSpy = cy.spy().as('addPlasmidsSpy');
349
- cy.mount(
350
- <ConfigProvider config={testConfig}>
351
- <UploadPlasmidsButton addPlasmids={addPlasmidsSpy} syntax={mocloSyntax} />
352
- </ConfigProvider>,
353
- );
354
-
355
- cy.get('button').contains('Add Plasmids').siblings('input').selectFile([
356
- 'cypress/test_files/syntax/pYTK002.gb',
357
- 'cypress/test_files/syntax/moclo_ytk_multi_part.gb',
358
- 'cypress/test_files/syntax/pYTK095.gb',
359
- 'cypress/test_files/sequencing/locus.gb'],
360
- { force: true });
361
-
362
- cy.get('.MuiDialog-root', { timeout: 10000 }).should('be.visible');
363
-
364
- cy.get('button').contains('Cancel').click();
365
-
366
- cy.get('@addPlasmidsSpy').should('not.have.been.called');
367
-
368
- });
369
-
370
- });
@@ -1,7 +1,6 @@
1
1
  import React from 'react'
2
2
  import {
3
- Alert, Autocomplete, Box, Button, CircularProgress, Dialog, DialogTitle, DialogContent,
4
- DialogActions, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, Stack, Table,
3
+ Alert, Autocomplete, Box, Button, CircularProgress, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, Stack, Table,
5
4
  TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, ButtonGroup
6
5
  } from '@mui/material'
7
6
  import { Clear as ClearIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
@@ -9,42 +8,18 @@ import { useAssembler } from './useAssembler';
9
8
  import { useDispatch } from 'react-redux';
10
9
  import { cloningActions } from '@opencloning/store/cloning';
11
10
  import AssemblerPart from './AssemblerPart';
12
- import { jsonToGenbank } from '@teselagen/bio-parsers';
11
+
13
12
  import useCombinatorialAssembly from './useCombinatorialAssembly';
14
- import { usePlasmidsLogic } from './usePlasmidsLogic';
15
- import PlasmidSyntaxTable from './PlasmidSyntaxTable';
16
13
  import ExistingSyntaxDialog from './ExistingSyntaxDialog';
17
14
  import error2String from '@opencloning/utils/error2String';
18
15
  import { categoryFilter } from './assembler_utils';
19
16
  import useBackendRoute from '../../hooks/useBackendRoute';
20
17
  import useHttpClient from '../../hooks/useHttpClient';
21
18
  import useAlerts from '../../hooks/useAlerts';
19
+ import UploadPlasmidsButton from './UploadPlasmidsButton';
22
20
 
23
- const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
24
-
25
- function formatPlasmid(sequenceData) {
26
-
27
- const { appData } = sequenceData;
28
- const { fileName, correspondingParts, longestFeature } = appData;
29
- const [left_overhang, right_overhang] = correspondingParts[0].split('-');
30
21
 
31
- let plasmidName = fileName;
32
- if (longestFeature[0]?.name) {
33
- plasmidName += ` (${longestFeature[0].name})`;
34
- }
35
-
36
- return {
37
- type: 'loadedFile',
38
- plasmid_name: plasmidName,
39
- file_name: fileName,
40
- left_overhang,
41
- right_overhang,
42
- key: `${left_overhang}-${right_overhang}`,
43
- sequenceData,
44
- genbankString: jsonToGenbank(sequenceData),
45
- };
46
-
47
- }
22
+ const { setState: setCloningState, setCurrentTab: setCurrentTabAction } = cloningActions;
48
23
 
49
24
 
50
25
  function formatItemName(item) {
@@ -286,61 +261,7 @@ function LoadSyntaxButton({ setSyntax, addPlasmids }) {
286
261
  </>
287
262
  }
288
263
 
289
- export function UploadPlasmidsButton({ addPlasmids, syntax }) {
290
- const { uploadPlasmids, linkedPlasmids, setLinkedPlasmids } = usePlasmidsLogic(syntax)
291
- const validPlasmids = React.useMemo(() => linkedPlasmids.filter((plasmid) => plasmid.appData.correspondingParts.length === 1), [linkedPlasmids])
292
- const invalidPlasmids = React.useMemo(() => linkedPlasmids.filter((plasmid) => plasmid.appData.correspondingParts.length !== 1), [linkedPlasmids])
293
- const fileInputRef = React.useRef(null)
294
-
295
- const handleFileChange = (event) => {
296
- uploadPlasmids(Array.from(event.target.files))
297
- fileInputRef.current.value = ''
298
- }
299
-
300
- const handleImportValidPlasmids = React.useCallback(() => {
301
- addPlasmids(validPlasmids.map(formatPlasmid))
302
- setLinkedPlasmids([])
303
- }, [addPlasmids, validPlasmids, setLinkedPlasmids])
304
264
 
305
- return (<>
306
- <Button color="primary" onClick={() => fileInputRef.current.click()}>
307
- Add Plasmids
308
- </Button>
309
- <input multiple type="file" ref={fileInputRef} style={{ display: 'none' }} onChange={handleFileChange} accept=".gbk,.gb,.fasta,.fa,.dna" />
310
- <Dialog
311
- maxWidth="lg"
312
- fullWidth
313
- open={invalidPlasmids.length > 0 || validPlasmids.length > 0}
314
- onClose={() => setLinkedPlasmids([])}
315
- PaperProps={{
316
- style: {
317
- maxHeight: '80vh',
318
- },
319
- }}
320
- >
321
- <DialogActions sx={{ justifyContent: 'center', position: 'sticky', top: 0, zIndex: 99, background: '#fff' }}>
322
- <Button disabled={validPlasmids.length === 0} variant="contained" color="success" onClick={handleImportValidPlasmids}>Import valid plasmids</Button>
323
- <Button variant="contained" color="error" onClick={() => setLinkedPlasmids([])}>Cancel</Button>
324
- </DialogActions>
325
- {invalidPlasmids.length > 0 && (
326
- <Box data-testid="invalid-plasmids-box">
327
- <DialogTitle>Invalid Plasmids</DialogTitle>
328
- <DialogContent>
329
- <PlasmidSyntaxTable plasmids={invalidPlasmids} />
330
- </DialogContent>
331
- </Box>
332
- )}
333
- {validPlasmids.length > 0 && (
334
- <Box data-testid="valid-plasmids-box">
335
- <DialogTitle>Valid Plasmids</DialogTitle>
336
- <DialogContent>
337
- <PlasmidSyntaxTable plasmids={validPlasmids} />
338
- </DialogContent>
339
- </Box>
340
- )}
341
- </Dialog>
342
- </>)
343
- }
344
265
 
345
266
  function Assembler() {
346
267
  const [syntax, setSyntax] = React.useState(null);
@@ -1,6 +1,16 @@
1
1
  import React from 'react';
2
+ import { ConfigProvider } from '@opencloning/ui/providers/ConfigProvider';
2
3
  import ExistingSyntaxDialog from './ExistingSyntaxDialog';
3
4
 
5
+ // Test config
6
+ const testConfig = {
7
+ backendUrl: 'http://localhost:8000',
8
+ showAppBar: false,
9
+ noExternalRequests: false,
10
+ enableAssembler: true,
11
+ enablePlannotate: false,
12
+ };
13
+
4
14
  const mockSyntaxes = [
5
15
  {
6
16
  path: 'test-syntax-1',
@@ -40,10 +50,12 @@ describe('<ExistingSyntaxDialog />', () => {
40
50
  const onSyntaxSelectSpy = cy.spy().as('onSyntaxSelectSpy');
41
51
 
42
52
  cy.mount(
43
- <ExistingSyntaxDialog
44
- onClose={onCloseSpy}
45
- onSyntaxSelect={onSyntaxSelectSpy}
46
- />,
53
+ <ConfigProvider config={testConfig}>
54
+ <ExistingSyntaxDialog
55
+ onClose={onCloseSpy}
56
+ onSyntaxSelect={onSyntaxSelectSpy}
57
+ />
58
+ </ConfigProvider>,
47
59
  );
48
60
 
49
61
  cy.wait('@getSyntaxes');
@@ -70,10 +82,12 @@ describe('<ExistingSyntaxDialog />', () => {
70
82
  }).as('getPlasmidsData');
71
83
 
72
84
  cy.mount(
73
- <ExistingSyntaxDialog
74
- onClose={onCloseSpy}
75
- onSyntaxSelect={onSyntaxSelectSpy}
76
- />,
85
+ <ConfigProvider config={testConfig}>
86
+ <ExistingSyntaxDialog
87
+ onClose={onCloseSpy}
88
+ onSyntaxSelect={onSyntaxSelectSpy}
89
+ />
90
+ </ConfigProvider>,
77
91
  );
78
92
 
79
93
  cy.wait('@getSyntaxes');
@@ -97,10 +111,12 @@ describe('<ExistingSyntaxDialog />', () => {
97
111
  }).as('getSyntaxDataError');
98
112
 
99
113
  cy.mount(
100
- <ExistingSyntaxDialog
101
- onClose={onCloseSpy}
102
- onSyntaxSelect={onSyntaxSelectSpy}
103
- />,
114
+ <ConfigProvider config={testConfig}>
115
+ <ExistingSyntaxDialog
116
+ onClose={onCloseSpy}
117
+ onSyntaxSelect={onSyntaxSelectSpy}
118
+ />
119
+ </ConfigProvider>,
104
120
  );
105
121
 
106
122
  cy.wait('@getSyntaxes');
@@ -130,10 +146,12 @@ describe('<ExistingSyntaxDialog />', () => {
130
146
  }).as('getPlasmidsDataError');
131
147
 
132
148
  cy.mount(
133
- <ExistingSyntaxDialog
134
- onClose={onCloseSpy}
135
- onSyntaxSelect={onSyntaxSelectSpy}
136
- />,
149
+ <ConfigProvider config={testConfig}>
150
+ <ExistingSyntaxDialog
151
+ onClose={onCloseSpy}
152
+ onSyntaxSelect={onSyntaxSelectSpy}
153
+ />
154
+ </ConfigProvider>,
137
155
  );
138
156
 
139
157
  cy.wait('@getSyntaxes');
@@ -169,10 +187,12 @@ describe('<ExistingSyntaxDialog />', () => {
169
187
  }).as('getPlasmidsData2');
170
188
 
171
189
  cy.mount(
172
- <ExistingSyntaxDialog
173
- onClose={onCloseSpy}
174
- onSyntaxSelect={onSyntaxSelectSpy}
175
- />,
190
+ <ConfigProvider config={testConfig}>
191
+ <ExistingSyntaxDialog
192
+ onClose={onCloseSpy}
193
+ onSyntaxSelect={onSyntaxSelectSpy}
194
+ />
195
+ </ConfigProvider>,
176
196
  );
177
197
 
178
198
  cy.wait('@getSyntaxes');
@@ -212,10 +232,12 @@ describe('<ExistingSyntaxDialog />', () => {
212
232
  }
213
233
 
214
234
  cy.mount(
215
- <ExistingSyntaxDialog
216
- onClose={onCloseSpy}
217
- onSyntaxSelect={onSyntaxSelectSpy}
218
- />,
235
+ <ConfigProvider config={testConfig}>
236
+ <ExistingSyntaxDialog
237
+ onClose={onCloseSpy}
238
+ onSyntaxSelect={onSyntaxSelectSpy}
239
+ />
240
+ </ConfigProvider>,
219
241
  );
220
242
 
221
243
  cy.wait('@getSyntaxes');
@@ -240,10 +262,12 @@ describe('<ExistingSyntaxDialog />', () => {
240
262
  }
241
263
 
242
264
  cy.mount(
243
- <ExistingSyntaxDialog
244
- onClose={onCloseSpy}
245
- onSyntaxSelect={onSyntaxSelectSpy}
246
- />,
265
+ <ConfigProvider config={testConfig}>
266
+ <ExistingSyntaxDialog
267
+ onClose={onCloseSpy}
268
+ onSyntaxSelect={onSyntaxSelectSpy}
269
+ />
270
+ </ConfigProvider>,
247
271
  );
248
272
 
249
273
  cy.wait('@getSyntaxes');
@@ -1,18 +1,41 @@
1
1
  import React from 'react'
2
- import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box } from '@mui/material'
2
+ import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemText, ListItemButton, Alert, Button, Box, ButtonGroup } from '@mui/material'
3
3
  import getHttpClient from '@opencloning/utils/getHttpClient';
4
4
  import RequestStatusWrapper from '../form/RequestStatusWrapper';
5
+ import { useConfig } from '../../providers';
6
+ import ServerStaticFileSelect from '../form/ServerStaticFileSelect';
7
+ import { readSubmittedTextFile } from '@opencloning/utils/readNwrite';
5
8
 
6
9
  const httpClient = getHttpClient();
7
10
  const baseURL = 'https://assets.opencloning.org/syntaxes/syntaxes/';
8
11
  httpClient.defaults.baseURL = baseURL;
9
12
 
13
+ function LocalSyntaxDialog({ onClose, onSyntaxSelect }) {
14
+
15
+ const onFileSelected = React.useCallback(async (file) => {
16
+ const text = await readSubmittedTextFile(file);
17
+ const syntaxData = JSON.parse(text);
18
+ onSyntaxSelect(syntaxData, []);
19
+ onClose();
20
+ }, [onSyntaxSelect, onClose]);
21
+ return (
22
+ <Dialog data-testid="local-syntax-dialog" open onClose={onClose}>
23
+ <DialogTitle>Load syntax from local server</DialogTitle>
24
+ <DialogContent sx={{ minWidth: '400px' }}>
25
+ <ServerStaticFileSelect onFileSelected={onFileSelected} type="syntax" />
26
+ </DialogContent>
27
+ </Dialog>
28
+ )
29
+ }
30
+
10
31
  function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
11
32
  const [syntaxes, setSyntaxes] = React.useState([]);
12
33
  const [connectAttempt, setConnectAttempt] = React.useState(0);
13
34
  const [requestStatus, setRequestStatus] = React.useState({ status: 'loading' });
14
35
  const [loadError, setLoadError] = React.useState(null);
15
36
  const fileInputRef = React.useRef(null);
37
+ const { staticContentPath } = useConfig();
38
+ const [localDialogOpen, setLocalDialogOpen] = React.useState(false);
16
39
 
17
40
  React.useEffect(() => {
18
41
  setRequestStatus({ status: 'loading' });
@@ -88,13 +111,21 @@ function ExistingSyntaxDialog({ onClose, onSyntaxSelect }) {
88
111
  onChange={handleFileUpload}
89
112
  style={{ display: 'none' }}
90
113
  />
91
- <Button
92
- variant="outlined"
93
- sx={{ display: 'block', mx: 'auto' }}
94
- onClick={() => fileInputRef.current?.click()}
95
- >
96
- Upload syntax from JSON file
97
- </Button>
114
+ <ButtonGroup sx={{ display: 'flex', justifyContent: 'center' }}>
115
+ <Button
116
+ onClick={() => fileInputRef.current?.click()}
117
+ >
118
+ Upload syntax from JSON file
119
+ </Button>
120
+ {staticContentPath && (
121
+ <Button
122
+ onClick={() => setLocalDialogOpen(true)}
123
+ >
124
+ Load syntax from local server
125
+ </Button>
126
+ )}
127
+ </ButtonGroup>
128
+ {localDialogOpen && <LocalSyntaxDialog onClose={() => {setLocalDialogOpen(false); onClose()}} onSyntaxSelect={onSyntaxSelect} />}
98
129
  </Box>
99
130
  </DialogContent>
100
131
  </Dialog>