@apollo-annotation/jbrowse-plugin-apollo 0.1.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 (116) hide show
  1. package/README.md +76 -0
  2. package/dist/index.esm.js +10248 -0
  3. package/dist/index.esm.js.map +1 -0
  4. package/dist/index.js +7 -0
  5. package/dist/jbrowse-plugin-apollo.cjs.development.js +10298 -0
  6. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -0
  7. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +2 -0
  8. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -0
  9. package/dist/jbrowse-plugin-apollo.umd.development.js +46957 -0
  10. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -0
  11. package/dist/jbrowse-plugin-apollo.umd.production.min.js +2 -0
  12. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -0
  13. package/package.json +130 -0
  14. package/src/ApolloInternetAccount/addMenuItems.ts +94 -0
  15. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +121 -0
  16. package/src/ApolloInternetAccount/components/LoginButtons.tsx +62 -0
  17. package/src/ApolloInternetAccount/components/LoginIcons.tsx +74 -0
  18. package/src/ApolloInternetAccount/configSchema.ts +26 -0
  19. package/src/ApolloInternetAccount/index.ts +2 -0
  20. package/src/ApolloInternetAccount/model.ts +448 -0
  21. package/src/ApolloJobModel.ts +117 -0
  22. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +186 -0
  23. package/src/ApolloSequenceAdapter/configSchema.ts +12 -0
  24. package/src/ApolloSequenceAdapter/index.ts +21 -0
  25. package/src/ApolloSixFrameRenderer/ApolloSixFrameRenderer.tsx +12 -0
  26. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +692 -0
  27. package/src/ApolloSixFrameRenderer/configSchema.ts +7 -0
  28. package/src/ApolloSixFrameRenderer/index.ts +3 -0
  29. package/src/ApolloTextSearchAdapter/ApolloTextSearchAdapter.ts +64 -0
  30. package/src/ApolloTextSearchAdapter/configSchema.ts +24 -0
  31. package/src/ApolloTextSearchAdapter/index.ts +18 -0
  32. package/src/BackendDrivers/BackendDriver.ts +31 -0
  33. package/src/BackendDrivers/CollaborationServerDriver.ts +318 -0
  34. package/src/BackendDrivers/DesktopFileDriver.ts +170 -0
  35. package/src/BackendDrivers/InMemoryFileDriver.ts +76 -0
  36. package/src/BackendDrivers/index.ts +4 -0
  37. package/src/ChangeManager.ts +148 -0
  38. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +248 -0
  39. package/src/LinearApolloDisplay/components/index.ts +1 -0
  40. package/src/LinearApolloDisplay/configSchema.ts +16 -0
  41. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +422 -0
  42. package/src/LinearApolloDisplay/glyphs/CanonicalGeneGlyph.ts +1191 -0
  43. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +151 -0
  44. package/src/LinearApolloDisplay/glyphs/Glyph.ts +382 -0
  45. package/src/LinearApolloDisplay/glyphs/ImplicitExonGeneGlyph.ts +697 -0
  46. package/src/LinearApolloDisplay/glyphs/index.ts +4 -0
  47. package/src/LinearApolloDisplay/index.ts +2 -0
  48. package/src/LinearApolloDisplay/stateModel/base.ts +146 -0
  49. package/src/LinearApolloDisplay/stateModel/getGlyph.ts +39 -0
  50. package/src/LinearApolloDisplay/stateModel/glyphs.ts +45 -0
  51. package/src/LinearApolloDisplay/stateModel/index.ts +20 -0
  52. package/src/LinearApolloDisplay/stateModel/layouts.ts +230 -0
  53. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +513 -0
  54. package/src/LinearApolloDisplay/stateModel/rendering.ts +441 -0
  55. package/src/LinearApolloDisplay/stateModel/trackHeightMixin.ts +43 -0
  56. package/src/LinearApolloDisplay/types.ts +1 -0
  57. package/src/OntologyManager/OntologyStore/__snapshots__/fulltext.test.ts.snap +208 -0
  58. package/src/OntologyManager/OntologyStore/__snapshots__/index.test.ts.snap +18846 -0
  59. package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +137 -0
  60. package/src/OntologyManager/OntologyStore/fulltext.test.ts +94 -0
  61. package/src/OntologyManager/OntologyStore/fulltext.ts +264 -0
  62. package/src/OntologyManager/OntologyStore/index.test.ts +130 -0
  63. package/src/OntologyManager/OntologyStore/index.ts +526 -0
  64. package/src/OntologyManager/OntologyStore/indexeddb-schema.ts +89 -0
  65. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +180 -0
  66. package/src/OntologyManager/OntologyStore/obo-graph-json-schema.ts +110 -0
  67. package/src/OntologyManager/OntologyStore/prefixes.ts +35 -0
  68. package/src/OntologyManager/index.ts +173 -0
  69. package/src/SixFrameFeatureDisplay/components/TrackLines.tsx +19 -0
  70. package/src/SixFrameFeatureDisplay/components/index.ts +1 -0
  71. package/src/SixFrameFeatureDisplay/configSchema.ts +21 -0
  72. package/src/SixFrameFeatureDisplay/index.ts +2 -0
  73. package/src/SixFrameFeatureDisplay/stateModel.ts +413 -0
  74. package/src/TabularEditor/HybridGrid/ChangeHandling.ts +88 -0
  75. package/src/TabularEditor/HybridGrid/Feature.tsx +346 -0
  76. package/src/TabularEditor/HybridGrid/FeatureAttributes.tsx +34 -0
  77. package/src/TabularEditor/HybridGrid/Highlight.tsx +40 -0
  78. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +138 -0
  79. package/src/TabularEditor/HybridGrid/NumberCell.tsx +77 -0
  80. package/src/TabularEditor/HybridGrid/ToolBar.tsx +59 -0
  81. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +119 -0
  82. package/src/TabularEditor/HybridGrid/index.ts +1 -0
  83. package/src/TabularEditor/TabularEditorPane.tsx +34 -0
  84. package/src/TabularEditor/index.ts +3 -0
  85. package/src/TabularEditor/model.ts +44 -0
  86. package/src/TabularEditor/types.ts +3 -0
  87. package/src/components/AddAssembly.tsx +464 -0
  88. package/src/components/AddChildFeature.tsx +247 -0
  89. package/src/components/AddFeature.tsx +252 -0
  90. package/src/components/CopyFeature.tsx +328 -0
  91. package/src/components/DeleteAssembly.tsx +185 -0
  92. package/src/components/DeleteFeature.tsx +90 -0
  93. package/src/components/Dialog.tsx +47 -0
  94. package/src/components/DownloadGFF3.tsx +213 -0
  95. package/src/components/ImportFeatures.tsx +295 -0
  96. package/src/components/ManageChecks.tsx +280 -0
  97. package/src/components/ManageUsers.tsx +218 -0
  98. package/src/components/ModifyFeatureAttribute.tsx +457 -0
  99. package/src/components/OntologyTermAutocomplete.tsx +240 -0
  100. package/src/components/OntologyTermMultiSelect.tsx +349 -0
  101. package/src/components/OpenLocalFile.tsx +178 -0
  102. package/src/components/ViewChangeLog.tsx +208 -0
  103. package/src/components/ViewCheckResults.tsx +151 -0
  104. package/src/components/index.ts +12 -0
  105. package/src/config.ts +10 -0
  106. package/src/declare.d.ts +3 -0
  107. package/src/extensions/annotationFromPileup.ts +208 -0
  108. package/src/extensions/index.ts +1 -0
  109. package/src/index.ts +394 -0
  110. package/src/makeDisplayComponent.tsx +244 -0
  111. package/src/session/ClientDataStore.ts +282 -0
  112. package/src/session/index.ts +1 -0
  113. package/src/session/session.ts +373 -0
  114. package/src/types.ts +10 -0
  115. package/src/util/index.ts +31 -0
  116. package/src/util/loadAssemblyIntoClient.ts +291 -0
@@ -0,0 +1,349 @@
1
+ import { isAbortException } from '@jbrowse/core/util'
2
+ import {
3
+ Autocomplete,
4
+ AutocompleteRenderGetTagProps,
5
+ Chip,
6
+ Grid,
7
+ TextField,
8
+ Tooltip,
9
+ Typography,
10
+ } from '@mui/material'
11
+ import { debounce } from '@mui/material/utils'
12
+ import highlightMatch from 'autosuggest-highlight/match'
13
+ import highlightParse from 'autosuggest-highlight/parse'
14
+ import { getParent } from 'mobx-state-tree'
15
+ import * as React from 'react'
16
+
17
+ import {
18
+ OntologyManager,
19
+ OntologyRecord,
20
+ OntologyTerm,
21
+ isOntologyClass,
22
+ } from '../OntologyManager'
23
+ import { Match } from '../OntologyManager/OntologyStore/fulltext'
24
+ import { isDeprecated } from '../OntologyManager/OntologyStore/indexeddb-schema'
25
+ import { ApolloSessionModel } from '../session'
26
+
27
+ interface TermValue {
28
+ term: OntologyTerm
29
+ matches?: Match[]
30
+ }
31
+
32
+ // interface TermAutocompleteResult extends TermValue {
33
+ // label: string[]
34
+ // match: string
35
+ // category: string[]
36
+ // taxon: string
37
+ // taxon_label: string
38
+ // highlight: string
39
+ // has_highlight: boolean
40
+ // }
41
+
42
+ // interface TermAutocompleteResponse {
43
+ // docs: TermAutocompleteResult[]
44
+ // }
45
+
46
+ // const hiliteRegex = /(?<=<em class="hilite">)(.*?)(?=<\/em>)/g
47
+
48
+ function TermTagWithTooltip({
49
+ getTagProps,
50
+ index,
51
+ ontology,
52
+ termId,
53
+ }: {
54
+ termId: string
55
+ index: number
56
+ getTagProps: AutocompleteRenderGetTagProps
57
+ ontology: OntologyRecord
58
+ }) {
59
+ const manager = getParent<OntologyManager>(ontology, 2)
60
+
61
+ const [description, setDescription] = React.useState('')
62
+ const [errorMessage, setErrorMessage] = React.useState('')
63
+
64
+ React.useEffect(() => {
65
+ const controller = new AbortController()
66
+ const { signal } = controller
67
+ async function fetchDescription() {
68
+ const termUrl = manager.expandPrefixes(termId)
69
+ const db = await ontology.dataStore?.db
70
+ if (!db || signal.aborted) {
71
+ return
72
+ }
73
+ const term = await db
74
+ .transaction('nodes')
75
+ .objectStore('nodes')
76
+ .get(termUrl)
77
+
78
+ if (term && term.lbl && !signal.aborted) {
79
+ setDescription(term.lbl || 'no label')
80
+ }
81
+ }
82
+ fetchDescription().catch((error) => {
83
+ if (!signal.aborted) {
84
+ setErrorMessage(String(error))
85
+ }
86
+ })
87
+
88
+ return () => {
89
+ controller.abort()
90
+ }
91
+ }, [termId, ontology, manager])
92
+
93
+ return (
94
+ <Tooltip title={description}>
95
+ <div>
96
+ <Chip
97
+ label={errorMessage || manager.applyPrefixes(termId)}
98
+ color={errorMessage ? 'error' : 'default'}
99
+ size="small"
100
+ {...getTagProps({ index })}
101
+ />
102
+ </div>
103
+ </Tooltip>
104
+ )
105
+ }
106
+
107
+ export function OntologyTermMultiSelect({
108
+ includeDeprecated,
109
+ onChange,
110
+ ontologyName,
111
+ ontologyVersion,
112
+ session,
113
+ value: initialValue,
114
+ }: {
115
+ session: ApolloSessionModel
116
+ value: string[]
117
+ ontologyName: string
118
+ ontologyVersion?: string
119
+ /** if true, include deprecated/obsolete terms */
120
+ includeDeprecated?: boolean
121
+ onChange(newValue: string[]): void
122
+ }) {
123
+ const ontologyManager = session.apolloDataStore
124
+ .ontologyManager as OntologyManager
125
+ const ontology = ontologyManager.findOntology(ontologyName, ontologyVersion)
126
+
127
+ const [value, setValue] = React.useState<TermValue[]>(
128
+ initialValue.map((id) => ({ term: { id, type: 'CLASS' } })),
129
+ )
130
+ const [inputValue, setInputValue] = React.useState('')
131
+ const [options, setOptions] = React.useState<readonly TermValue[]>([])
132
+ const [loading, setLoading] = React.useState(false)
133
+ const [errorMessage, setErrorMessage] = React.useState('')
134
+
135
+ const getOntologyTerms = React.useMemo(
136
+ () =>
137
+ debounce(
138
+ async (
139
+ request: { input: string; signal: AbortSignal },
140
+ callback: (results: TermValue[]) => void,
141
+ ) => {
142
+ if (!ontology) {
143
+ return
144
+ }
145
+ const { dataStore } = ontology
146
+ if (!dataStore) {
147
+ return
148
+ }
149
+ const { input, signal } = request
150
+ try {
151
+ const matches = await dataStore.getTermsByFulltext(
152
+ input,
153
+ undefined,
154
+ signal,
155
+ )
156
+ // aggregate the matches by term
157
+ const byTerm = new Map<string, Required<TermValue>>()
158
+ const options: Required<TermValue>[] = []
159
+ for (const match of matches) {
160
+ if (
161
+ !isOntologyClass(match.term) ||
162
+ (!includeDeprecated && isDeprecated(match.term))
163
+ ) {
164
+ continue
165
+ }
166
+ let slot = byTerm.get(match.term.id)
167
+ if (!slot) {
168
+ slot = { term: match.term, matches: [] }
169
+ byTerm.set(match.term.id, slot)
170
+ options.push(slot)
171
+ }
172
+ slot.matches.push(match)
173
+ }
174
+ callback(options)
175
+ } catch (error) {
176
+ if (!isAbortException(error)) {
177
+ setErrorMessage(String(error))
178
+ }
179
+ }
180
+ },
181
+ 400,
182
+ ),
183
+ [includeDeprecated, ontology],
184
+ )
185
+
186
+ React.useEffect(() => {
187
+ const aborter = new AbortController()
188
+ const { signal } = aborter
189
+
190
+ if (inputValue === '') {
191
+ setOptions([])
192
+ return
193
+ }
194
+
195
+ setLoading(true)
196
+
197
+ void getOntologyTerms({ input: inputValue, signal }, (results) => {
198
+ let newOptions: readonly TermValue[] = []
199
+ if (value.length > 0) {
200
+ newOptions = value
201
+ }
202
+ if (results) {
203
+ newOptions = [...newOptions, ...results]
204
+ }
205
+ setOptions(newOptions)
206
+ setLoading(false)
207
+ })
208
+
209
+ return () => {
210
+ aborter.abort()
211
+ }
212
+ }, [getOntologyTerms, ontology, includeDeprecated, inputValue, value])
213
+
214
+ if (!ontology) {
215
+ return null
216
+ }
217
+
218
+ const extraTextFieldParams: { error?: boolean; helperText?: string } = {}
219
+ if (errorMessage) {
220
+ extraTextFieldParams.error = true
221
+ extraTextFieldParams.helperText = errorMessage
222
+ }
223
+
224
+ return (
225
+ <Autocomplete
226
+ getOptionLabel={(option) => ontologyManager.applyPrefixes(option.term.id)}
227
+ filterOptions={(terms) => terms.filter((t) => isOntologyClass(t.term))}
228
+ options={options}
229
+ autoComplete
230
+ includeInputInList
231
+ filterSelectedOptions
232
+ value={value}
233
+ loading={loading}
234
+ isOptionEqualToValue={(option, v) =>
235
+ ontologyManager.applyPrefixes(option.term.id) ===
236
+ ontologyManager.applyPrefixes(v.term.id)
237
+ }
238
+ noOptionsText={inputValue ? 'No matches' : 'Start typing to search'}
239
+ onChange={(_, newValue) => {
240
+ setOptions(newValue ? [...newValue, ...options] : options)
241
+ onChange(newValue.map((v) => ontologyManager.applyPrefixes(v.term.id)))
242
+ setValue(newValue)
243
+ }}
244
+ onInputChange={(event, newInputValue) => {
245
+ if (newInputValue) {
246
+ setLoading(true)
247
+ }
248
+ setOptions([])
249
+ setInputValue(newInputValue)
250
+ }}
251
+ multiple
252
+ renderInput={(params) => (
253
+ <TextField
254
+ {...params}
255
+ {...extraTextFieldParams}
256
+ variant="outlined"
257
+ fullWidth
258
+ />
259
+ )}
260
+ renderOption={(props, option) => (
261
+ <Option
262
+ {...props}
263
+ ontologyManager={ontologyManager}
264
+ option={option}
265
+ inputValue={inputValue}
266
+ />
267
+ )}
268
+ renderTags={(v, getTagProps) =>
269
+ v.map((option, index) => (
270
+ <TermTagWithTooltip
271
+ termId={option.term.id}
272
+ index={index}
273
+ ontology={ontology}
274
+ getTagProps={getTagProps}
275
+ key={option.term.id}
276
+ />
277
+ ))
278
+ }
279
+ />
280
+ )
281
+ }
282
+
283
+ function HighlightedText(props: { str: string; search: string }) {
284
+ const { search, str } = props
285
+
286
+ const highlights = highlightMatch(str, search, {
287
+ insideWords: true,
288
+ findAllOccurrences: true,
289
+ })
290
+ const parts = highlightParse(str, highlights)
291
+ return (
292
+ <>
293
+ {parts.map((part, index) => (
294
+ <Typography
295
+ key={index}
296
+ component="span"
297
+ sx={{ fontWeight: part.highlight ? 'bold' : 'regular' }}
298
+ variant="body2"
299
+ color="text.secondary"
300
+ >
301
+ {part.text}
302
+ </Typography>
303
+ ))}
304
+ </>
305
+ )
306
+ }
307
+ function Option(props: {
308
+ ontologyManager: OntologyManager
309
+ inputValue: string
310
+ option: TermValue
311
+ }) {
312
+ const { inputValue, ontologyManager, option, ...other } = props
313
+ const matches = option.matches ?? []
314
+ const fields = matches
315
+ .filter((match) => match.field.jsonPath !== '$.lbl')
316
+ .map((match) => {
317
+ return (
318
+ <React.Fragment key={`option-${match.term.id}-${match.str}`}>
319
+ <Typography component="dt" variant="body2" color="text.secondary">
320
+ {match.field.displayName}
321
+ </Typography>
322
+ <dd>
323
+ <HighlightedText str={match.str} search={inputValue} />
324
+ </dd>
325
+ </React.Fragment>
326
+ )
327
+ })
328
+ // const lblScore = matches
329
+ // .filter((match) => match.field.jsonPath === '$.lbl')
330
+ // .map((m) => m.score)
331
+ // .join(', ')
332
+ return (
333
+ <li {...other}>
334
+ <Grid container>
335
+ <Grid item>
336
+ <Typography component="span">
337
+ {ontologyManager.applyPrefixes(option.term.id)}
338
+ </Typography>{' '}
339
+ <HighlightedText
340
+ str={option.term.lbl ?? '(no label)'}
341
+ search={inputValue}
342
+ />{' '}
343
+ {/* ({lblScore}) */}
344
+ <dl>{fields}</dl>
345
+ </Grid>
346
+ </Grid>
347
+ </li>
348
+ )
349
+ }
@@ -0,0 +1,178 @@
1
+ import { AbstractSessionModel, isElectron } from '@jbrowse/core/util'
2
+ import {
3
+ Button,
4
+ DialogActions,
5
+ DialogContent,
6
+ DialogContentText,
7
+ FormControl,
8
+ FormHelperText,
9
+ TextField,
10
+ useTheme,
11
+ } from '@mui/material'
12
+ import { nanoid } from 'nanoid'
13
+ import React, { useState } from 'react'
14
+
15
+ import { InMemoryFileDriver } from '../BackendDrivers'
16
+ import { ApolloSessionModel } from '../session'
17
+ import { loadAssemblyIntoClient } from '../util'
18
+ import { Dialog } from './Dialog'
19
+
20
+ interface OpenLocalFileProps {
21
+ session: ApolloSessionModel
22
+ handleClose(): void
23
+ inMemoryFileDriver: InMemoryFileDriver
24
+ }
25
+
26
+ export interface RefSeqInterface {
27
+ refName: string
28
+ uniqueId: string
29
+ aliases?: string[]
30
+ }
31
+
32
+ export function OpenLocalFile({ handleClose, session }: OpenLocalFileProps) {
33
+ const { addApolloTrackConfig, apolloDataStore } = session
34
+ const { addAssembly, addSessionAssembly, assemblyManager, notify } =
35
+ session as unknown as AbstractSessionModel & {
36
+ // eslint-disable-next-line @typescript-eslint/ban-types
37
+ addSessionAssembly: Function
38
+ }
39
+
40
+ const [file, setFile] = useState<File | null>(null)
41
+ const [assemblyName, setAssemblyName] = useState('')
42
+ const [errorMessage, setErrorMessage] = useState('')
43
+ const [submitted, setSubmitted] = useState(false)
44
+ const theme = useTheme()
45
+
46
+ async function handleChangeFile(e: React.ChangeEvent<HTMLInputElement>) {
47
+ const selectedFile = e.target.files?.item(0)
48
+ if (!selectedFile) {
49
+ return
50
+ }
51
+ setErrorMessage('')
52
+ setFile(selectedFile)
53
+ if (!assemblyName) {
54
+ const fileName = selectedFile.name
55
+ const lastDotIndex = fileName.lastIndexOf('.')
56
+ if (lastDotIndex === -1) {
57
+ setAssemblyName(fileName)
58
+ } else {
59
+ setAssemblyName(fileName.slice(0, lastDotIndex))
60
+ }
61
+ }
62
+ }
63
+
64
+ async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
65
+ event.preventDefault()
66
+ setErrorMessage('')
67
+ setSubmitted(true)
68
+
69
+ if (!file) {
70
+ throw new Error('No file selected')
71
+ }
72
+
73
+ // Right now we are not using stream because there was a problem with 'pipe' in ReadStream
74
+ const fileData = await new Response(file).text()
75
+ const assemblyId = `${assemblyName}-${file.name}-${nanoid(8)}`
76
+ try {
77
+ await loadAssemblyIntoClient(assemblyId, fileData, apolloDataStore)
78
+ } catch (error) {
79
+ setErrorMessage(String(error))
80
+ setSubmitted(false)
81
+ }
82
+
83
+ const assemblyConfig = {
84
+ name: assemblyId,
85
+ aliases: [assemblyName],
86
+ displayName: assemblyName,
87
+ sequence: {
88
+ trackId: `sequenceConfigId-${assemblyName}`,
89
+ type: 'ReferenceSequenceTrack',
90
+ adapter: { type: 'ApolloSequenceAdapter', assemblyId },
91
+ metadata: {
92
+ apollo: true,
93
+ ...(isElectron
94
+ ? { file: (file as File & { path: string }).path }
95
+ : {}),
96
+ },
97
+ },
98
+ }
99
+
100
+ // Save assembly into session
101
+ await (addSessionAssembly || addAssembly)(assemblyConfig)
102
+ const a = await assemblyManager.waitForAssembly(assemblyConfig.name)
103
+ if (a) {
104
+ // @ts-expect-error MST type coercion problem?
105
+ addApolloTrackConfig(a)
106
+ notify(`Loaded GFF3 ${file?.name}`, 'success')
107
+ } else {
108
+ notify(`Error loading GFF3 ${file?.name}`, 'error')
109
+ }
110
+ handleClose()
111
+ }
112
+
113
+ function handleAssemblyNameChange(
114
+ event: React.ChangeEvent<HTMLInputElement>,
115
+ ) {
116
+ setAssemblyName(event.target.value)
117
+ }
118
+
119
+ return (
120
+ <Dialog
121
+ open
122
+ title="Open local GFF3 file"
123
+ handleClose={handleClose}
124
+ maxWidth={false}
125
+ data-testid="open-local-file"
126
+ >
127
+ <form onSubmit={onSubmit}>
128
+ <DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
129
+ <FormControl>
130
+ <div style={{ flexDirection: 'row' }}>
131
+ <Button
132
+ variant="contained"
133
+ component="label"
134
+ style={{ marginRight: theme.spacing() }}
135
+ >
136
+ Choose File
137
+ <input
138
+ type="file"
139
+ required
140
+ hidden
141
+ onChange={handleChangeFile}
142
+ />
143
+ </Button>
144
+ {file ? file.name : 'No file chosen'}
145
+ </div>
146
+ <FormHelperText>
147
+ Make sure your GFF3 has an embedded FASTA section
148
+ </FormHelperText>
149
+ </FormControl>
150
+ <TextField
151
+ required
152
+ label="Assembly name"
153
+ value={assemblyName}
154
+ onChange={handleAssemblyNameChange}
155
+ />
156
+ </DialogContent>
157
+ <DialogActions>
158
+ <Button disabled={false} variant="contained" type="submit">
159
+ {submitted ? 'Submitting...' : 'Submit'}
160
+ </Button>
161
+ <Button
162
+ disabled={submitted}
163
+ variant="outlined"
164
+ type="submit"
165
+ onClick={handleClose}
166
+ >
167
+ Cancel
168
+ </Button>
169
+ </DialogActions>
170
+ </form>
171
+ {errorMessage ? (
172
+ <DialogContent>
173
+ <DialogContentText color="error">{errorMessage}</DialogContentText>
174
+ </DialogContent>
175
+ ) : null}
176
+ </Dialog>
177
+ )
178
+ }