@apollo-annotation/jbrowse-plugin-apollo 0.3.4 → 0.3.5

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 (44) hide show
  1. package/dist/index.esm.js +1513 -1074
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +1510 -1065
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +4681 -2097
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +4 -4
  12. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +13 -0
  13. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +88 -17
  14. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +144 -22
  15. package/src/FeatureDetailsWidget/Attributes.tsx +93 -43
  16. package/src/FeatureDetailsWidget/BasicInformation.tsx +2 -4
  17. package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +6 -4
  18. package/src/FeatureDetailsWidget/Sequence.tsx +16 -33
  19. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +137 -92
  20. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +600 -0
  21. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +54 -0
  22. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +13 -6
  23. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +6 -27
  24. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +35 -13
  25. package/src/LinearApolloDisplay/stateModel/layouts.ts +7 -2
  26. package/src/LinearApolloDisplay/stateModel/rendering.ts +4 -0
  27. package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +10 -1
  28. package/src/OntologyManager/OntologyStore/index.test.ts +1 -0
  29. package/src/OntologyManager/index.ts +2 -0
  30. package/src/SixFrameFeatureDisplay/stateModel.ts +5 -1
  31. package/src/TabularEditor/HybridGrid/Feature.tsx +0 -1
  32. package/src/TabularEditor/HybridGrid/NumberCell.tsx +8 -1
  33. package/src/TabularEditor/HybridGrid/ToolBar.tsx +14 -12
  34. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +3 -27
  35. package/src/components/AddAssembly.tsx +608 -291
  36. package/src/components/CreateApolloAnnotation.tsx +144 -37
  37. package/src/components/OntologyTermMultiSelect.tsx +3 -0
  38. package/src/components/index.ts +0 -1
  39. package/src/extensions/annotationFromJBrowseFeature.ts +3 -2
  40. package/src/makeDisplayComponent.tsx +3 -4
  41. package/src/util/annotationFeatureUtils.ts +53 -0
  42. package/src/util/index.ts +1 -0
  43. package/src/FeatureDetailsWidget/TranscriptBasic.tsx +0 -200
  44. package/src/components/ModifyFeatureAttribute.tsx +0 -460
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
2
1
  /* eslint-disable @typescript-eslint/unbound-method */
3
2
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
4
3
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
@@ -11,31 +10,35 @@ import {
11
10
  } from '@apollo-annotation/shared'
12
11
  import { readConfObject } from '@jbrowse/core/configuration'
13
12
  import { AbstractSessionModel } from '@jbrowse/core/util'
14
- import LinkIcon from '@mui/icons-material/Link'
15
13
  import {
16
- Box,
17
14
  Button,
18
- Checkbox,
19
15
  DialogActions,
20
16
  DialogContent,
21
17
  DialogContentText,
22
- FormControl,
23
- FormControlLabel,
18
+ Accordion,
19
+ AccordionSummary,
20
+ AccordionDetails,
21
+ Typography,
22
+ Box,
24
23
  FormGroup,
25
- FormLabel,
26
- MenuItem,
27
- Radio,
28
- RadioGroup,
29
- Select,
30
- SelectChangeEvent,
24
+ FormControlLabel,
25
+ Checkbox,
26
+ LinearProgress,
31
27
  TextField,
32
- Typography,
28
+ Tooltip,
29
+ IconButton,
30
+ Table,
31
+ TableCell,
32
+ TableBody,
33
+ InputAdornment,
34
+ TableRow,
33
35
  } from '@mui/material'
34
- import InputAdornment from '@mui/material/InputAdornment'
35
- import LinearProgress from '@mui/material/LinearProgress'
36
+
37
+ import { makeStyles } from 'tss-react/mui'
38
+
36
39
  import ObjectID from 'bson-objectid'
37
40
  import { getRoot } from 'mobx-state-tree'
38
- import React, { useState } from 'react'
41
+ import React, { useEffect, useState } from 'react'
39
42
 
40
43
  import { ApolloInternetAccountModel } from '../ApolloInternetAccount/model'
41
44
  import { ChangeManager } from '../ChangeManager'
@@ -44,6 +47,12 @@ import { ApolloRootModel } from '../types'
44
47
  import { createFetchErrorMessage } from '../util'
45
48
  import { Dialog } from './Dialog'
46
49
 
50
+ import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked'
51
+ import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked'
52
+
53
+ import InfoIcon from '@mui/icons-material/Info'
54
+ import LinkIcon from '@mui/icons-material/Link'
55
+
47
56
  interface AddAssemblyProps {
48
57
  session: ApolloSessionModel
49
58
  handleClose(): void
@@ -53,14 +62,71 @@ interface AddAssemblyProps {
53
62
  enum FileType {
54
63
  GFF3 = 'text/x-gff3',
55
64
  FASTA = 'text/x-fasta',
65
+ BGZIP_FASTA = 'application/x-bgzip-fasta',
66
+ FAI = 'text/x-fai',
67
+ GZI = 'application/x-gzi',
56
68
  EXTERNAL = 'text/x-external',
57
69
  }
58
70
 
71
+ const useStyles = makeStyles()((theme) => ({
72
+ accordion: {
73
+ border: `1px solid ${theme.palette.divider}`,
74
+ '&:not(:last-child)': {
75
+ borderBottom: 0,
76
+ },
77
+ },
78
+ accordionSummary: {
79
+ flexDirection: 'row-reverse',
80
+ },
81
+ accordionDetails: {
82
+ padding: theme.spacing(2),
83
+ borderTop: '1px solid rgba(0, 0, 0, .125)',
84
+ },
85
+ radioIcon: {
86
+ color: theme?.palette?.tertiary?.contrastText,
87
+ },
88
+ dialog: {
89
+ // minHeight: 500,
90
+ minWidth: 550,
91
+ maxWidth: 800,
92
+ },
93
+ }))
94
+
95
+ function checkSumbission(
96
+ validAsm: boolean,
97
+ sequenceIsEditable: boolean,
98
+ fileType: FileType,
99
+ fastaFile: File | null,
100
+ fastaIndexFile: File | null,
101
+ fastaGziIndexFile: File | null,
102
+ validFastaUrl: boolean,
103
+ validFastaIndexUrl: boolean,
104
+ validFastaGziIndexUrl: boolean,
105
+ ) {
106
+ if (!validAsm) {
107
+ return false
108
+ }
109
+ if (sequenceIsEditable && fastaFile) {
110
+ return true
111
+ }
112
+ if (fileType === FileType.GFF3 && fastaFile) {
113
+ return true
114
+ }
115
+ if (fastaFile && fastaIndexFile && fastaGziIndexFile) {
116
+ return true
117
+ }
118
+ if (validFastaUrl && validFastaIndexUrl && validFastaGziIndexUrl) {
119
+ return true
120
+ }
121
+ return false
122
+ }
123
+
59
124
  export function AddAssembly({
60
125
  changeManager,
61
126
  handleClose,
62
127
  session,
63
128
  }: AddAssemblyProps) {
129
+ const { classes } = useStyles()
64
130
  const { internetAccounts } = getRoot<ApolloRootModel>(session)
65
131
  const { notify } = session as unknown as AbstractSessionModel
66
132
  const apolloInternetAccounts = internetAccounts.filter(
@@ -72,62 +138,39 @@ export function AddAssembly({
72
138
  const [assemblyName, setAssemblyName] = useState('')
73
139
  const [errorMessage, setErrorMessage] = useState('')
74
140
  const [validAsm, setValidAsm] = useState(false)
75
- const [file, setFile] = useState<File | null>(null)
76
- const [fileType, setFileType] = useState(FileType.GFF3)
141
+ const [fileType, setFileType] = useState(FileType.BGZIP_FASTA)
77
142
  const [importFeatures, setImportFeatures] = useState(true)
143
+ const [sequenceIsEditable, setSequenceIsEditable] = useState(false)
78
144
  const [submitted, setSubmitted] = useState(false)
79
- const [selectedInternetAccount, setSelectedInternetAccount] = useState(
80
- apolloInternetAccounts[0],
81
- )
82
- const [fastaFile, setFastaFile] = useState('')
83
- const [fastaIndexFile, setFastaIndexFile] = useState('')
84
- const [fastaGziIndexFile, setFastaGziIndexFile] = useState('')
145
+
146
+ const [fastaFile, setFastaFile] = useState<File | null>(null)
147
+ const [fastaIndexFile, setFastaIndexFile] = useState<File | null>(null)
148
+ const [fastaGziIndexFile, setFastaGziIndexFile] = useState<File | null>(null)
149
+
150
+ const [fastaUrl, setFastaUrl] = useState<string>('')
151
+ const [fastaIndexUrl, setFastaIndexUrl] = useState<string>('')
152
+ const [fastaGziIndexUrl, setFastaGziIndexUrl] = useState<string>('')
153
+
85
154
  const [loading, setLoading] = useState(false)
155
+ const [isGzip, setIsGzip] = useState<boolean>(false)
86
156
 
87
- function handleChangeInternetAccount(e: SelectChangeEvent) {
88
- setSubmitted(false)
89
- const newlySelectedInternetAccount = apolloInternetAccounts.find(
90
- (ia) => ia.internetAccountId === e.target.value,
91
- )
92
- if (!newlySelectedInternetAccount) {
93
- throw new Error(
94
- `Could not find internetAccount with ID "${e.target.value}"`,
95
- )
96
- }
97
- setSelectedInternetAccount(newlySelectedInternetAccount)
98
- }
157
+ useEffect(() => {
158
+ setFastaIndexUrl(fastaUrl ? `${fastaUrl}.fai` : '')
159
+ }, [fastaUrl])
99
160
 
100
- function handleChangeFile(e: React.ChangeEvent<HTMLInputElement>) {
101
- if (!e.target.files) {
102
- return
103
- }
104
- const selectedFile = e.target.files.item(0)
105
- setFile(selectedFile)
106
- if (!selectedFile) {
107
- return
108
- }
109
- const fileNameLower = selectedFile.name.toLowerCase()
110
- if (
111
- fileNameLower.endsWith('.fasta') ||
112
- fileNameLower.endsWith('.fna') ||
113
- fileNameLower.endsWith('.fa')
114
- ) {
115
- setFileType(FileType.FASTA)
116
- } else if (
117
- fileNameLower.endsWith('.gff3') ||
118
- fileNameLower.endsWith('.gff')
119
- ) {
120
- setFileType(FileType.GFF3)
121
- }
122
- }
161
+ useEffect(() => {
162
+ setFastaGziIndexUrl(fastaUrl ? `${fastaUrl}.gzi` : '')
163
+ }, [fastaUrl])
123
164
 
124
- function handleChangeFileType(e: React.ChangeEvent<HTMLInputElement>) {
125
- setFileType(e.target.value as FileType)
126
- setImportFeatures(e.target.value === FileType.GFF3)
127
- setFastaFile('')
128
- setFastaIndexFile('')
129
- setFile(null)
130
- }
165
+ useEffect(() => {
166
+ if (sequenceIsEditable || fileType === FileType.GFF3) {
167
+ setIsGzip(
168
+ fastaFile?.name.toLocaleLowerCase().endsWith('.gz') ? true : false,
169
+ )
170
+ } else {
171
+ setIsGzip(true)
172
+ }
173
+ }, [fastaFile, sequenceIsEditable, fileType])
131
174
 
132
175
  function checkAssemblyName(assembly: string) {
133
176
  const { assemblies } = session as unknown as AbstractSessionModel
@@ -143,6 +186,70 @@ export function AddAssembly({
143
186
  }
144
187
  }
145
188
 
189
+ async function uploadFile(file: File, fileType: FileType): Promise<string> {
190
+ const { jobsManager } = session
191
+ const controller = new AbortController()
192
+
193
+ const [{ baseURL, getFetcher }] = apolloInternetAccounts
194
+ const url = new URL('files', baseURL)
195
+
196
+ url.searchParams.set('type', fileType)
197
+ const uri = url.href
198
+ const formData = new FormData()
199
+ let filename = file.name
200
+
201
+ if (fileType === FileType.FAI || fileType === FileType.GZI) {
202
+ filename = `${filename}.txt`
203
+ } else if (isGzip && !file.name.toLocaleLowerCase().endsWith('.gz')) {
204
+ filename = `${filename}.gz`
205
+ } else if (!isGzip && file.name.toLocaleLowerCase().endsWith('.gz')) {
206
+ filename = `${filename}.txt`
207
+ }
208
+ formData.append('file', file, filename)
209
+ formData.append('type', fileType)
210
+ const apolloFetchFile = getFetcher({
211
+ locationType: 'UriLocation',
212
+ uri,
213
+ })
214
+ if (apolloFetchFile) {
215
+ const job = {
216
+ name: `UploadAssemblyFile for ${assemblyName}`,
217
+ statusMessage: 'Pre-validating',
218
+ progressPct: 0,
219
+ cancelCallback: () => {
220
+ controller.abort()
221
+ jobsManager.abortJob(job.name)
222
+ },
223
+ }
224
+ jobsManager.runJob(job)
225
+ jobsManager.update(
226
+ job.name,
227
+ `Uploading ${file.name}, this may take awhile`,
228
+ )
229
+ const { signal } = controller
230
+
231
+ const response = await apolloFetchFile(uri, {
232
+ method: 'POST',
233
+ body: formData,
234
+ signal,
235
+ })
236
+ if (!response.ok) {
237
+ const newErrorMessage = await createFetchErrorMessage(
238
+ response,
239
+ 'Error when inserting new assembly (while uploading file)',
240
+ )
241
+ jobsManager.abortJob(job.name, newErrorMessage)
242
+ setErrorMessage(newErrorMessage)
243
+ return ''
244
+ }
245
+ const result = await response.json()
246
+ const fileId = result._id as string
247
+ jobsManager.done(job)
248
+ return fileId
249
+ }
250
+ throw new Error('Failed to fetch')
251
+ }
252
+
146
253
  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
147
254
  event.preventDefault()
148
255
  setErrorMessage('')
@@ -153,158 +260,137 @@ export function AddAssembly({
153
260
  handleClose()
154
261
  event.preventDefault()
155
262
 
156
- const { jobsManager } = session
157
- const controller = new AbortController()
158
-
159
- const job = {
160
- name: `UploadAssemblyFile for ${assemblyName}`,
161
- statusMessage: 'Pre-validating',
162
- progressPct: 0,
163
- cancelCallback: () => {
164
- controller.abort()
165
- jobsManager.abortJob(job.name)
166
- },
167
- }
168
-
169
- jobsManager.runJob(job)
170
-
171
- let fileId = ''
172
- const { baseURL, getFetcher, internetAccountId } = selectedInternetAccount
173
- if (fileType !== FileType.EXTERNAL && file) {
174
- // First upload file
175
- const url = new URL('files', baseURL)
176
- url.searchParams.set('type', fileType)
177
- const uri = url.href
178
- const formData = new FormData()
179
- formData.append('file', file)
180
- formData.append('fileName', file.name)
181
- formData.append('type', fileType)
182
- const apolloFetchFile = getFetcher({
183
- locationType: 'UriLocation',
184
- uri,
185
- })
186
- if (apolloFetchFile) {
187
- jobsManager.update(job.name, 'Uploading file, this may take awhile')
188
- const { signal } = controller
189
- const response = await apolloFetchFile(uri, {
190
- method: 'POST',
191
- body: formData,
192
- signal,
193
- })
194
- if (!response.ok) {
195
- const newErrorMessage = await createFetchErrorMessage(
196
- response,
197
- 'Error when inserting new assembly (while uploading file)',
198
- )
199
- jobsManager.abortJob(job.name, newErrorMessage)
200
- setErrorMessage(newErrorMessage)
201
- return
202
- }
203
- const result = await response.json()
204
- fileId = result._id
205
- }
206
- }
207
-
208
263
  let change:
209
264
  | AddAssemblyFromExternalChange
210
265
  | AddAssemblyAndFeaturesFromFileChange
211
266
  | AddAssemblyFromFileChange
267
+
212
268
  if (fileType === FileType.EXTERNAL) {
213
269
  change = new AddAssemblyFromExternalChange({
214
270
  typeName: 'AddAssemblyFromExternalChange',
215
-
216
271
  assembly: new ObjectID().toHexString(),
217
272
  assemblyName,
218
273
  externalLocation: {
219
- fa: fastaFile,
220
- fai: fastaIndexFile,
221
- ...(fastaGziIndexFile ? { gzi: fastaGziIndexFile } : {}),
274
+ fa: fastaUrl,
275
+ fai: fastaIndexUrl,
276
+ gzi: fastaGziIndexUrl,
222
277
  },
223
278
  })
224
279
  } else {
225
- const fileUploadChangeBase = {
226
- assembly: new ObjectID().toHexString(),
227
- assemblyName,
228
- fileIds: { fa: fileId },
280
+ if (!fastaFile) {
281
+ throw new Error('Missing fasta file')
229
282
  }
230
- change =
231
- fileType === FileType.GFF3 && importFeatures
232
- ? new AddAssemblyAndFeaturesFromFileChange({
233
- typeName: 'AddAssemblyAndFeaturesFromFileChange',
234
- ...fileUploadChangeBase,
235
- })
236
- : new AddAssemblyFromFileChange({
237
- typeName: 'AddAssemblyFromFileChange',
238
- ...fileUploadChangeBase,
239
- })
240
- }
283
+ if (fileType === FileType.GFF3 && importFeatures) {
284
+ const faId = await uploadFile(fastaFile, FileType.GFF3)
285
+ change = new AddAssemblyAndFeaturesFromFileChange({
286
+ typeName: 'AddAssemblyAndFeaturesFromFileChange',
287
+ assembly: new ObjectID().toHexString(),
288
+ assemblyName,
289
+ fileIds: { fa: faId },
290
+ })
291
+ } else if (fileType === FileType.GFF3) {
292
+ const faId = await uploadFile(fastaFile, FileType.GFF3)
293
+ change = new AddAssemblyFromFileChange({
294
+ typeName: 'AddAssemblyFromFileChange',
295
+ assembly: new ObjectID().toHexString(),
296
+ assemblyName,
297
+ fileIds: {
298
+ fa: faId,
299
+ },
300
+ })
301
+ } else if (sequenceIsEditable) {
302
+ const faId = await uploadFile(fastaFile, FileType.FASTA)
303
+ change = new AddAssemblyFromFileChange({
304
+ typeName: 'AddAssemblyFromFileChange',
305
+ assembly: new ObjectID().toHexString(),
306
+ assemblyName,
307
+ fileIds: {
308
+ fa: faId,
309
+ },
310
+ })
311
+ } else {
312
+ if (!fastaIndexFile || !fastaGziIndexFile) {
313
+ throw new Error('Missing fasta index files')
314
+ }
315
+ const faId = await uploadFile(fastaFile, FileType.BGZIP_FASTA)
316
+ const faiId = await uploadFile(fastaIndexFile, FileType.FAI)
317
+ const gziId = await uploadFile(fastaGziIndexFile, FileType.GZI)
241
318
 
242
- jobsManager.done(job)
319
+ change = new AddAssemblyFromFileChange({
320
+ typeName: 'AddAssemblyFromFileChange',
321
+ assembly: new ObjectID().toHexString(),
322
+ assemblyName,
323
+ fileIds: {
324
+ fa: faId,
325
+ fai: faiId,
326
+ gzi: gziId,
327
+ },
328
+ })
329
+ }
330
+ }
243
331
 
332
+ const [{ internetAccountId }] = apolloInternetAccounts
244
333
  await changeManager.submit(change, {
245
334
  internetAccountId,
246
335
  updateJobsManager: true,
247
336
  })
248
-
249
337
  setSubmitted(false)
250
338
  setLoading(false)
251
339
  }
252
340
 
253
- let validFastaFile = false
341
+ let validFastaUrl = false
254
342
  try {
255
- const url = new URL(fastaFile)
343
+ const url = new URL(fastaUrl)
256
344
  if (url.protocol === 'http:' || url.protocol === 'https:') {
257
- validFastaFile = true
345
+ validFastaUrl = true
258
346
  }
259
347
  } catch {
260
348
  // pass
261
349
  }
262
- let validFastaIndexFile = false
350
+ let validFastaIndexUrl = false
263
351
  try {
264
- const url = new URL(fastaIndexFile)
352
+ const url = new URL(fastaIndexUrl)
265
353
  if (url.protocol === 'http:' || url.protocol === 'https:') {
266
- validFastaIndexFile = true
354
+ validFastaIndexUrl = true
267
355
  }
268
356
  } catch {
269
357
  // pass
270
358
  }
271
- let validFastaGziIndexFile = false
359
+ let validFastaGziIndexUrl = false
272
360
  try {
273
- const url = new URL(fastaGziIndexFile)
361
+ const url = new URL(fastaGziIndexUrl)
274
362
  if (url.protocol === 'http:' || url.protocol === 'https:') {
275
- validFastaGziIndexFile = true
363
+ validFastaGziIndexUrl = true
276
364
  }
277
365
  } catch {
278
366
  // pass
279
367
  }
280
368
 
369
+ const [expanded, setExpanded] = React.useState<string>('panelFastaInput')
370
+
371
+ const handleAccordionChange =
372
+ (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => {
373
+ if (newExpanded) {
374
+ setExpanded(panel)
375
+ }
376
+ if (panel === 'panelGffInput') {
377
+ setIsGzip(false)
378
+ } else {
379
+ setIsGzip(true)
380
+ }
381
+ }
382
+
281
383
  return (
282
384
  <Dialog
283
- open
284
- maxWidth={false}
385
+ open={true}
386
+ handleClose={handleClose}
285
387
  data-testid="add-assembly-dialog"
286
388
  title="Add new assembly"
287
- handleClose={handleClose}
389
+ maxWidth={false}
288
390
  >
289
- {loading ? <LinearProgress /> : null}
290
- <form onSubmit={onSubmit}>
291
- <DialogContent style={{ display: 'flex', flexDirection: 'column' }}>
292
- {apolloInternetAccounts.length > 1 ? (
293
- <>
294
- <DialogContentText>Select account</DialogContentText>
295
- <Select
296
- value={selectedInternetAccount.internetAccountId}
297
- onChange={handleChangeInternetAccount}
298
- disabled={submitted && !errorMessage}
299
- >
300
- {internetAccounts.map((option) => (
301
- <MenuItem key={option.id} value={option.internetAccountId}>
302
- {option.name}
303
- </MenuItem>
304
- ))}
305
- </Select>
306
- </>
307
- ) : null}
391
+ <form onSubmit={onSubmit} data-testid="submit-form">
392
+ <DialogContent className={classes.dialog}>
393
+ {loading ? <LinearProgress /> : null}
308
394
  <TextField
309
395
  margin="dense"
310
396
  id="name"
@@ -319,147 +405,378 @@ export function AddAssembly({
319
405
  }}
320
406
  disabled={submitted && !errorMessage}
321
407
  />
322
- <FormControl style={{ marginTop: 20 }}>
323
- <FormLabel>Select GFF3, FASTA or EXTERNAL option</FormLabel>
324
- <RadioGroup
325
- aria-labelledby="demo-radio-buttons-group-label"
326
- defaultValue={FileType.GFF3}
327
- name="radio-buttons-group"
328
- onChange={handleChangeFileType}
329
- value={fileType}
408
+
409
+ <Accordion
410
+ disableGutters
411
+ elevation={0}
412
+ square
413
+ className={classes.accordion}
414
+ expanded={expanded === 'panelFastaInput'}
415
+ onChange={handleAccordionChange('panelFastaInput')}
416
+ >
417
+ <AccordionSummary
418
+ className={classes.accordionSummary}
419
+ expandIcon={
420
+ expanded === 'panelFastaInput' ? (
421
+ <RadioButtonCheckedIcon
422
+ className={classes.radioIcon}
423
+ sx={{ fontSize: '1.2rem', ml: 5 }}
424
+ />
425
+ ) : (
426
+ <RadioButtonUncheckedIcon
427
+ className={classes.radioIcon}
428
+ sx={{ fontSize: '1.2rem', mr: 5 }}
429
+ />
430
+ )
431
+ }
432
+ aria-controls="panelFastaInputd-content"
433
+ id="panelFastaInputd-header"
330
434
  >
331
- <FormControlLabel
332
- value={FileType.GFF3}
333
- control={<Radio />}
334
- label="GFF3"
335
- disabled={submitted && !errorMessage}
336
- />
337
- <FormControlLabel
338
- value={FileType.FASTA}
339
- control={<Radio />}
340
- label="FASTA"
341
- disabled={submitted && !errorMessage}
342
- />
343
- <FormControlLabel
344
- value={FileType.EXTERNAL}
345
- control={<Radio />}
346
- label="External"
347
- disabled={submitted && !errorMessage}
348
- />
349
- </RadioGroup>
350
- </FormControl>
351
- {fileType === FileType.EXTERNAL ? (
352
- <Box style={{ marginTop: 20 }}>
353
- <Typography variant="caption">
354
- Enter FASTA and FASTA index(es) URL
355
- </Typography>
356
- <TextField
357
- margin="dense"
358
- helperText="Can be bgz-compressed"
359
- id="fasta"
360
- label="FASTA"
361
- type="TextField"
362
- fullWidth
363
- variant="outlined"
364
- error={!validFastaFile}
365
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
366
- setFastaFile(e.target.value)
367
- }}
368
- disabled={submitted && !errorMessage}
369
- InputProps={{
370
- startAdornment: (
371
- <InputAdornment position="start">
372
- <LinkIcon />
373
- </InputAdornment>
374
- ),
375
- }}
376
- />
377
- <TextField
378
- margin="dense"
379
- id="fasta-index"
380
- label="FASTA Index"
381
- helperText=".fai or .gz.fai"
382
- type="TextField"
383
- fullWidth
384
- variant="outlined"
385
- error={!validFastaIndexFile}
386
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
387
- setFastaIndexFile(e.target.value)
388
- }}
389
- disabled={submitted && !errorMessage}
390
- InputProps={{
391
- startAdornment: (
392
- <InputAdornment position="start">
393
- <LinkIcon />
394
- </InputAdornment>
395
- ),
396
- }}
397
- />
398
- <TextField
399
- margin="dense"
400
- id="fasta-gzi-index"
401
- label="FASTA GZI Index"
402
- helperText="Only for bgz-compressed FASTA, .gz.gzi"
403
- type="TextField"
404
- fullWidth
405
- variant="outlined"
406
- error={Boolean(fastaGziIndexFile) && !validFastaGziIndexFile}
407
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
408
- setFastaGziIndexFile(e.target.value)
409
- }}
410
- disabled={submitted && !errorMessage}
411
- InputProps={{
412
- startAdornment: (
413
- <InputAdornment position="start">
414
- <LinkIcon />
415
- </InputAdornment>
416
- ),
417
- }}
418
- />
419
- </Box>
420
- ) : (
421
- <Box style={{ marginTop: 20 }}>
422
- <input
423
- type="file"
424
- onChange={handleChangeFile}
425
- disabled={submitted && !errorMessage}
426
- />
435
+ <Typography component="span">FASTA input</Typography>
436
+ </AccordionSummary>
437
+ <AccordionDetails className={classes.accordionDetails}>
427
438
  <FormGroup>
428
439
  <FormControlLabel
440
+ data-testid="files-on-url-checkbox"
429
441
  control={
430
442
  <Checkbox
431
- checked={fileType === FileType.GFF3 && importFeatures}
432
443
  onChange={() => {
433
- setImportFeatures(!importFeatures)
444
+ setFileType(
445
+ fileType === FileType.EXTERNAL
446
+ ? FileType.BGZIP_FASTA
447
+ : FileType.EXTERNAL,
448
+ )
449
+ if (fileType === FileType.EXTERNAL) {
450
+ setSequenceIsEditable(false)
451
+ }
434
452
  }}
453
+ checked={fileType === FileType.EXTERNAL}
435
454
  disabled={
436
- fileType !== FileType.GFF3 ||
437
- (submitted && !errorMessage)
455
+ sequenceIsEditable && fileType !== FileType.GFF3
438
456
  }
439
457
  />
440
458
  }
441
- label="Also load features from GFF3 file"
459
+ label={
460
+ <Box display="flex" alignItems="center">
461
+ Use external URLs
462
+ <Tooltip
463
+ title="Use external URLs to provide FASTA and index files. Does not copy the files to the Apollo collaboration server, so ensure the URLs are stable."
464
+ placement="top-start"
465
+ >
466
+ <IconButton size="small">
467
+ <InfoIcon sx={{ fontSize: 18 }} />
468
+ </IconButton>
469
+ </Tooltip>
470
+ </Box>
471
+ }
442
472
  />
473
+
474
+ <FormControlLabel
475
+ data-testid="sequence-is-editable-checkbox"
476
+ control={
477
+ <Checkbox
478
+ onChange={() => {
479
+ setSequenceIsEditable(!sequenceIsEditable)
480
+ }}
481
+ />
482
+ }
483
+ checked={sequenceIsEditable}
484
+ disabled={fileType === FileType.EXTERNAL}
485
+ label={
486
+ <Box display="flex" alignItems="center">
487
+ Store sequence in database
488
+ <Tooltip
489
+ title="Enables users to edit the genomic sequence, but comes with performance impacts. Use with care."
490
+ placement="top-start"
491
+ >
492
+ <IconButton size="small">
493
+ <InfoIcon sx={{ fontSize: 18 }} />
494
+ </IconButton>
495
+ </Tooltip>
496
+ </Box>
497
+ }
498
+ />
499
+ <FormControlLabel
500
+ data-testid="fasta-is-gzip-checkbox"
501
+ control={
502
+ <Checkbox
503
+ checked={isGzip}
504
+ onChange={() => {
505
+ if (sequenceIsEditable) {
506
+ setIsGzip(!isGzip)
507
+ } else {
508
+ setIsGzip(true)
509
+ }
510
+ }}
511
+ disabled={!sequenceIsEditable}
512
+ />
513
+ }
514
+ label="FASTA is gzip compressed"
515
+ />
516
+
517
+ {fileType === FileType.BGZIP_FASTA ||
518
+ fileType === FileType.GFF3 ? (
519
+ <Table size="small" sx={{ mt: 2 }}>
520
+ <TableBody>
521
+ <TableRow />
522
+ <TableCell style={{ borderBottomWidth: 0 }}>
523
+ <Box display="flex" alignItems="center">
524
+ <span>FASTA</span>
525
+ <Tooltip title='Unless "Store sequence in database" enabled, FASTA input must be compressed with bgzip and indexed with samtools faidx (or equivalent). Compression is optional for sequences stored in the database.'>
526
+ <IconButton size="small">
527
+ <InfoIcon sx={{ fontSize: 18 }} />
528
+ </IconButton>
529
+ </Tooltip>
530
+ </Box>
531
+ </TableCell>
532
+ <TableCell style={{ borderBottomWidth: 0 }}>
533
+ <input
534
+ data-testid="fasta-input-file"
535
+ type="file"
536
+ onChange={(
537
+ e: React.ChangeEvent<HTMLInputElement>,
538
+ ) => {
539
+ setFastaFile(e.target.files?.item(0) ?? null)
540
+ }}
541
+ disabled={submitted && !errorMessage}
542
+ />
543
+ </TableCell>
544
+
545
+ <TableRow />
546
+ <TableCell style={{ borderBottomWidth: 0 }}>
547
+ FASTA index (.fai)
548
+ </TableCell>
549
+ <TableCell style={{ borderBottomWidth: 0 }}>
550
+ <input
551
+ data-testid="fai-input-file"
552
+ type="file"
553
+ onChange={(
554
+ e: React.ChangeEvent<HTMLInputElement>,
555
+ ) => {
556
+ setFastaIndexFile(e.target.files?.item(0) ?? null)
557
+ }}
558
+ disabled={
559
+ (submitted && !errorMessage) || sequenceIsEditable
560
+ }
561
+ />
562
+ </TableCell>
563
+
564
+ <TableRow />
565
+ <TableCell style={{ borderBottomWidth: 0 }}>
566
+ FASTA binary index (.gzi)
567
+ </TableCell>
568
+ <TableCell style={{ borderBottomWidth: 0 }}>
569
+ <input
570
+ data-testid="gzi-input-file"
571
+ type="file"
572
+ onChange={(
573
+ e: React.ChangeEvent<HTMLInputElement>,
574
+ ) => {
575
+ setFastaGziIndexFile(
576
+ e.target.files?.item(0) ?? null,
577
+ )
578
+ }}
579
+ disabled={
580
+ (submitted && !errorMessage) || sequenceIsEditable
581
+ }
582
+ />
583
+ </TableCell>
584
+ </TableBody>
585
+ </Table>
586
+ ) : (
587
+ <Table size="small" sx={{ mt: 2 }}>
588
+ <TableBody>
589
+ <TableRow />
590
+ <TableCell style={{ borderBottomWidth: 0 }}>
591
+ <Box display="flex" alignItems="center">
592
+ <span>FASTA</span>
593
+ <Tooltip title="Remote FASTA input must be compressed with bgzip and indexed with samtools faidx (or equivalent)">
594
+ <IconButton size="small">
595
+ <InfoIcon sx={{ fontSize: 18 }} />
596
+ </IconButton>
597
+ </Tooltip>
598
+ </Box>
599
+ </TableCell>
600
+ <TableCell style={{ borderBottomWidth: 0 }}>
601
+ <TextField
602
+ data-testid="fasta-input-url"
603
+ variant="outlined"
604
+ value={fastaUrl}
605
+ error={!validFastaUrl}
606
+ onChange={(
607
+ e: React.ChangeEvent<HTMLInputElement>,
608
+ ) => {
609
+ setFastaUrl(e.target.value)
610
+ }}
611
+ disabled={submitted && !errorMessage}
612
+ slotProps={{
613
+ input: {
614
+ startAdornment: (
615
+ <InputAdornment position="start">
616
+ <LinkIcon />
617
+ </InputAdornment>
618
+ ),
619
+ },
620
+ }}
621
+ />
622
+ </TableCell>
623
+
624
+ <TableRow />
625
+ <TableCell style={{ borderBottomWidth: 0 }}>
626
+ FASTA index (.fai)
627
+ </TableCell>
628
+ <TableCell style={{ borderBottomWidth: 0 }}>
629
+ <TextField
630
+ data-testid="fai-input-url"
631
+ variant="outlined"
632
+ value={fastaIndexUrl}
633
+ error={!validFastaIndexUrl}
634
+ onChange={(
635
+ e: React.ChangeEvent<HTMLInputElement>,
636
+ ) => {
637
+ setFastaIndexUrl(e.target.value)
638
+ }}
639
+ disabled={submitted && !errorMessage}
640
+ slotProps={{
641
+ input: {
642
+ startAdornment: (
643
+ <InputAdornment position="start">
644
+ <LinkIcon />
645
+ </InputAdornment>
646
+ ),
647
+ },
648
+ }}
649
+ />
650
+ </TableCell>
651
+
652
+ <TableRow />
653
+ <TableCell style={{ borderBottomWidth: 0 }}>
654
+ FASTA binary index (.gzi)
655
+ </TableCell>
656
+ <TableCell style={{ borderBottomWidth: 0 }}>
657
+ <TextField
658
+ data-testid="gzi-input-url"
659
+ variant="outlined"
660
+ value={fastaGziIndexUrl}
661
+ error={!validFastaGziIndexUrl}
662
+ onChange={(
663
+ e: React.ChangeEvent<HTMLInputElement>,
664
+ ) => {
665
+ setFastaGziIndexUrl(e.target.value)
666
+ }}
667
+ disabled={submitted && !errorMessage}
668
+ slotProps={{
669
+ input: {
670
+ startAdornment: (
671
+ <InputAdornment position="start">
672
+ <LinkIcon />
673
+ </InputAdornment>
674
+ ),
675
+ },
676
+ }}
677
+ />
678
+ </TableCell>
679
+ </TableBody>
680
+ </Table>
681
+ )}
443
682
  </FormGroup>
444
- </Box>
445
- )}
683
+ </AccordionDetails>
684
+ </Accordion>
685
+ <Accordion
686
+ disableGutters
687
+ elevation={0}
688
+ square
689
+ className={classes.accordion}
690
+ expanded={expanded === 'panelGffInput'}
691
+ onChange={handleAccordionChange('panelGffInput')}
692
+ >
693
+ <AccordionSummary
694
+ className={classes.accordionSummary}
695
+ expandIcon={
696
+ expanded === 'panelGffInput' ? (
697
+ <RadioButtonCheckedIcon
698
+ className={classes.radioIcon}
699
+ sx={{ fontSize: '1.2rem', ml: 5 }}
700
+ />
701
+ ) : (
702
+ <RadioButtonUncheckedIcon
703
+ className={classes.radioIcon}
704
+ sx={{ fontSize: '1.2rem', mr: 5 }}
705
+ />
706
+ )
707
+ }
708
+ aria-controls="panelGffInputd-content"
709
+ >
710
+ <Typography component="span">
711
+ GFF3 input
712
+ <Tooltip title="GFF3 must includes FASTA sequences. File can be gzip compressed.">
713
+ <InfoIcon
714
+ className={classes.radioIcon}
715
+ sx={{ fontSize: 18 }}
716
+ />
717
+ </Tooltip>
718
+ </Typography>
719
+ </AccordionSummary>
720
+ <AccordionDetails className={classes.accordionDetails}>
721
+ <Box style={{ marginTop: 20 }}>
722
+ <input
723
+ data-testid="gff3-input-file"
724
+ type="file"
725
+ disabled={submitted && !errorMessage}
726
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
727
+ setFastaFile(e.target.files?.item(0) ?? null)
728
+ setFileType(FileType.GFF3)
729
+ }}
730
+ />
731
+ <FormGroup style={{ display: 'grid' }}>
732
+ <FormControlLabel
733
+ control={
734
+ <Checkbox
735
+ checked={importFeatures}
736
+ onChange={() => {
737
+ setImportFeatures(!importFeatures)
738
+ }}
739
+ disabled={submitted && !errorMessage}
740
+ />
741
+ }
742
+ label="Load features from GFF3 file"
743
+ />
744
+ <FormControlLabel
745
+ data-testid="gff3-is-gzip-checkbox"
746
+ control={
747
+ <Checkbox
748
+ checked={isGzip}
749
+ onChange={() => {
750
+ setIsGzip(!isGzip)
751
+ }}
752
+ disabled={submitted && !errorMessage}
753
+ />
754
+ }
755
+ label="GFF3 is gzip compressed"
756
+ />
757
+ </FormGroup>
758
+ </Box>
759
+ </AccordionDetails>
760
+ </Accordion>
446
761
  </DialogContent>
447
762
  <DialogActions>
448
763
  <Button
449
764
  disabled={
450
- !validAsm ||
451
- !(
452
- (assemblyName && file) ??
453
- (assemblyName &&
454
- fastaFile &&
455
- fastaIndexFile &&
456
- validFastaFile &&
457
- validFastaIndexFile)
458
- ) ||
459
- submitted
765
+ !checkSumbission(
766
+ validAsm,
767
+ sequenceIsEditable,
768
+ fileType,
769
+ fastaFile,
770
+ fastaIndexFile,
771
+ fastaGziIndexFile,
772
+ validFastaUrl,
773
+ validFastaIndexUrl,
774
+ validFastaGziIndexUrl,
775
+ ) || submitted
460
776
  }
461
777
  variant="contained"
462
778
  type="submit"
779
+ data-testid="submit-button"
463
780
  >
464
781
  {submitted ? 'Submitting...' : 'Submit'}
465
782
  </Button>