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