@jbrowse/plugin-linear-genome-view 1.3.4 → 1.5.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 (34) hide show
  1. package/dist/BaseLinearDisplay/components/BaseLinearDisplay.d.ts +3 -2
  2. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +20 -10
  3. package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +2 -2
  4. package/dist/LinearBareDisplay/model.d.ts +10 -9
  5. package/dist/LinearBasicDisplay/model.d.ts +13 -9
  6. package/dist/LinearGenomeView/components/Header.d.ts +2 -4
  7. package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +1 -11
  8. package/dist/LinearGenomeView/components/ScaleBar.d.ts +46 -14
  9. package/dist/LinearGenomeView/components/util.d.ts +2 -0
  10. package/dist/LinearGenomeView/index.d.ts +13 -2
  11. package/dist/index.d.ts +47 -56
  12. package/dist/plugin-linear-genome-view.cjs.development.js +642 -423
  13. package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
  14. package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
  15. package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
  16. package/dist/plugin-linear-genome-view.esm.js +652 -433
  17. package/dist/plugin-linear-genome-view.esm.js.map +1 -1
  18. package/package.json +4 -2
  19. package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +100 -21
  20. package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +10 -10
  21. package/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +15 -13
  22. package/src/LinearBasicDisplay/model.ts +25 -3
  23. package/src/LinearGenomeView/components/ExportSvgDialog.tsx +17 -8
  24. package/src/LinearGenomeView/components/Header.tsx +101 -104
  25. package/src/LinearGenomeView/components/ImportForm.tsx +146 -113
  26. package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -6
  27. package/src/LinearGenomeView/components/OverviewScaleBar.tsx +4 -1
  28. package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +196 -169
  29. package/src/LinearGenomeView/components/SearchResultsDialog.tsx +1 -16
  30. package/src/LinearGenomeView/components/SequenceDialog.tsx +59 -58
  31. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +5 -177
  32. package/src/LinearGenomeView/components/util.ts +8 -0
  33. package/src/LinearGenomeView/index.tsx +39 -28
  34. package/src/index.ts +3 -1
@@ -1,66 +1,90 @@
1
- /**
2
- * Based on:
3
- * https://material-ui.com/components/autocomplete/#Virtualize.tsx
4
- * Asynchronous Requests for autocomplete:
5
- * https://material-ui.com/components/autocomplete/
6
- */
7
1
  import React, { useMemo, useEffect, useState } from 'react'
8
2
  import { observer } from 'mobx-react'
9
- import { getEnv } from 'mobx-state-tree'
10
3
 
11
4
  // jbrowse core
12
- import { Region } from '@jbrowse/core/util/types'
13
- import { getSession, useDebounce } from '@jbrowse/core/util' // useDebounce
5
+ import { getSession, useDebounce, measureText } from '@jbrowse/core/util'
14
6
  import BaseResult, {
15
7
  RefSequenceResult,
16
8
  } from '@jbrowse/core/TextSearch/BaseResults'
9
+
17
10
  // material ui
18
- import CircularProgress from '@material-ui/core/CircularProgress'
19
- import TextField, { TextFieldProps as TFP } from '@material-ui/core/TextField'
20
- import Typography from '@material-ui/core/Typography'
11
+ import {
12
+ CircularProgress,
13
+ InputAdornment,
14
+ Popper,
15
+ TextField,
16
+ TextFieldProps as TFP,
17
+ PopperProps,
18
+ Typography,
19
+ } from '@material-ui/core'
21
20
  import SearchIcon from '@material-ui/icons/Search'
22
- import { InputAdornment } from '@material-ui/core'
23
- import Autocomplete, {
24
- createFilterOptions,
25
- } from '@material-ui/lab/Autocomplete'
26
- // other
21
+ import Autocomplete from '@material-ui/lab/Autocomplete'
22
+
23
+ // locals
27
24
  import { LinearGenomeViewModel } from '..'
28
25
 
29
- /**
30
- * Option interface used to format results to display in dropdown
31
- * of the materila ui interface
32
- */
33
26
  export interface Option {
34
27
  group?: string
35
28
  result: BaseResult
36
29
  }
37
30
 
38
- // filters for options to display in dropdown
39
- const filter = createFilterOptions<Option>({
40
- trim: true,
41
- ignoreCase: true,
42
- limit: 100,
43
- })
44
-
45
31
  async function fetchResults(
46
32
  self: LinearGenomeViewModel,
47
33
  query: string,
48
34
  assemblyName: string,
49
35
  ) {
50
- const session = getSession(self)
51
- const { pluginManager } = getEnv(session)
36
+ const { textSearchManager } = getSession(self)
52
37
  const { rankSearchResults } = self
53
- const { textSearchManager } = pluginManager.rootModel
54
38
  const searchScope = self.searchScope(assemblyName)
55
- const args = {
56
- queryString: query,
57
- searchType: 'prefix',
58
- }
59
- const searchResults =
60
- (await textSearchManager?.search(args, searchScope, rankSearchResults)) ||
61
- []
62
- return searchResults
39
+ return textSearchManager
40
+ ?.search(
41
+ {
42
+ queryString: query,
43
+ searchType: 'prefix',
44
+ },
45
+ searchScope,
46
+ rankSearchResults,
47
+ )
48
+ .then(results =>
49
+ results.filter(
50
+ (elem, index, self) =>
51
+ index === self.findIndex(t => t.label === elem.label),
52
+ ),
53
+ )
54
+ }
55
+
56
+ // the logic of this method is to only apply a filter to RefSequenceResults
57
+ // because they do not have a matchedObject. the trix search results already
58
+ // filter so don't need re-filtering
59
+ function filterOptions(options: Option[], searchQuery: string) {
60
+ return options.filter(option => {
61
+ const { result } = option
62
+ return (
63
+ result.getLabel().toLowerCase().includes(searchQuery) ||
64
+ result.matchedObject
65
+ )
66
+ })
63
67
  }
68
+
69
+ // MyPopper used to expand search results box wider if needed
70
+ // xref https://stackoverflow.com/a/63583835/2129219
71
+ const MyPopper = function (
72
+ props: PopperProps & { style?: { width?: unknown } },
73
+ ) {
74
+ const { style } = props
75
+ return (
76
+ <Popper
77
+ {...props}
78
+ style={{
79
+ width: 'fit-content',
80
+ minWidth: Math.min(+(style?.width || 0), 200),
81
+ background: 'white',
82
+ }}
83
+ placement="bottom-start"
84
+ />
85
+ )
86
+ }
87
+
64
88
  function RefNameAutocomplete({
65
89
  model,
66
90
  onSelect,
@@ -77,55 +101,52 @@ function RefNameAutocomplete({
77
101
  TextFieldProps?: TFP
78
102
  }) {
79
103
  const session = getSession(model)
104
+ const { assemblyManager } = session
80
105
  const [open, setOpen] = useState(false)
81
- const [error, setError] = useState<Error>()
106
+ const [loaded, setLoaded] = useState(true)
82
107
  const [currentSearch, setCurrentSearch] = useState('')
108
+ const [inputValue, setInputValue] = useState('')
109
+ const [searchOptions, setSearchOptions] = useState([] as Option[])
83
110
  const debouncedSearch = useDebounce(currentSearch, 300)
84
- const [searchOptions, setSearchOptions] = useState<Option[]>([])
85
- const { assemblyManager } = session
86
- const { coarseVisibleLocStrings } = model
111
+ const { coarseVisibleLocStrings, hasDisplayedRegions } = model
87
112
  const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined
88
113
 
89
114
  // eslint-disable-next-line react-hooks/exhaustive-deps
90
- const regions: Region[] = assembly?.regions || []
115
+ const regions = assembly?.regions || []
91
116
 
92
- const options: Option[] = useMemo(() => {
93
- const defaultOptions = regions.map(option => {
94
- return {
117
+ const options = useMemo(
118
+ () =>
119
+ regions.map(option => ({
95
120
  result: new RefSequenceResult({
96
121
  refName: option.refName,
97
122
  label: option.refName,
98
123
  matchedAttribute: 'refName',
99
124
  }),
100
- }
101
- })
102
- return defaultOptions
103
- }, [regions])
125
+ })),
126
+ [regions],
127
+ )
104
128
 
105
129
  useEffect(() => {
106
130
  let active = true
107
131
 
108
132
  ;(async () => {
109
133
  try {
110
- let results: BaseResult[] = []
111
- if (debouncedSearch && debouncedSearch !== '' && assemblyName) {
112
- const searchResults = await fetchResults(
113
- model,
114
- debouncedSearch,
115
- assemblyName,
116
- )
117
- results = results.concat(searchResults)
134
+ if (debouncedSearch === '' || !assemblyName) {
135
+ return
118
136
  }
119
- if (results.length > 0 && active) {
120
- const adapterResults: Option[] = results.map(result => {
121
- return { result }
122
- })
123
- setSearchOptions(adapterResults)
137
+
138
+ setLoaded(false)
139
+ const results = await fetchResults(model, debouncedSearch, assemblyName)
140
+ if (active) {
141
+ if (results && results.length >= 0) {
142
+ setSearchOptions(results.map(result => ({ result })))
143
+ }
144
+ setLoaded(true)
124
145
  }
125
146
  } catch (e) {
126
147
  console.error(e)
127
148
  if (active) {
128
- setError(e)
149
+ session.notify(`${e}`, 'error')
129
150
  }
130
151
  }
131
152
  })()
@@ -133,119 +154,125 @@ function RefNameAutocomplete({
133
154
  return () => {
134
155
  active = false
135
156
  }
136
- }, [assemblyName, debouncedSearch, model])
137
-
138
- function onChange(selectedOption: Option | string) {
139
- if (selectedOption && assemblyName) {
140
- if (typeof selectedOption === 'string') {
141
- // handles string inputs on keyPress enter
142
- const newResult = new BaseResult({
143
- label: selectedOption,
144
- })
145
- onSelect(newResult)
146
- } else {
147
- const { result } = selectedOption
148
- onSelect(result)
149
- }
150
- }
151
- }
157
+ }, [assemblyName, debouncedSearch, session, model])
158
+
159
+ const inputBoxVal = coarseVisibleLocStrings || value || ''
160
+
161
+ // heuristic, text width + icon width, minimum 200
162
+ const width = Math.min(Math.max(measureText(inputBoxVal, 16) + 25, 200), 550)
163
+
164
+ // notes on implementation:
165
+ // The selectOnFocus setting helps highlight the field when clicked
152
166
  return (
153
- <>
154
- <Autocomplete
155
- id={`refNameAutocomplete-${model.id}`}
156
- data-testid="autocomplete"
157
- freeSolo
158
- disableListWrap
159
- disableClearable
160
- includeInputInList
161
- clearOnBlur
162
- selectOnFocus
163
- disabled={!!error || !assemblyName}
164
- style={style}
165
- value={coarseVisibleLocStrings || value || ''}
166
- open={open}
167
- onOpen={() => setOpen(true)}
168
- onClose={() => {
169
- setOpen(false)
167
+ <Autocomplete
168
+ id={`refNameAutocomplete-${model.id}`}
169
+ data-testid="autocomplete"
170
+ disableListWrap
171
+ disableClearable
172
+ PopperComponent={MyPopper}
173
+ disabled={!assemblyName}
174
+ freeSolo
175
+ includeInputInList
176
+ selectOnFocus
177
+ style={{ ...style, width }}
178
+ value={inputBoxVal}
179
+ loading={!loaded}
180
+ inputValue={inputValue}
181
+ onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
182
+ loadingText="loading results"
183
+ open={open}
184
+ onOpen={() => setOpen(true)}
185
+ onClose={() => {
186
+ setOpen(false)
187
+ setLoaded(true)
188
+ if (hasDisplayedRegions) {
170
189
  setCurrentSearch('')
171
190
  setSearchOptions([])
172
- }}
173
- options={searchOptions.length === 0 ? options : searchOptions}
174
- getOptionDisabled={option => option?.group === 'limitOption'}
175
- filterOptions={(possibleOptions, params) => {
176
- const filtered = filter(possibleOptions, params)
177
- return filtered.length >= 100
178
- ? filtered.concat([
191
+ }
192
+ }}
193
+ onChange={(_event, selectedOption) => {
194
+ if (!selectedOption || !assemblyName) {
195
+ return
196
+ }
197
+
198
+ if (typeof selectedOption === 'string') {
199
+ // handles string inputs on keyPress enter
200
+ onSelect(new BaseResult({ label: selectedOption }))
201
+ } else {
202
+ onSelect(selectedOption.result)
203
+ }
204
+ setInputValue(inputBoxVal)
205
+ }}
206
+ options={searchOptions.length === 0 ? options : searchOptions}
207
+ getOptionDisabled={option => option?.group === 'limitOption'}
208
+ filterOptions={(options, params) => {
209
+ const filtered = filterOptions(
210
+ options,
211
+ params.inputValue.toLocaleLowerCase(),
212
+ )
213
+ return [
214
+ ...filtered.slice(0, 100),
215
+ ...(filtered.length > 100
216
+ ? [
179
217
  {
180
218
  group: 'limitOption',
181
219
  result: new BaseResult({
182
220
  label: 'keep typing for more results',
183
- renderingComponent: (
184
- <Typography>{'keep typing for more results'}</Typography>
185
- ),
186
221
  }),
187
222
  },
188
- ])
189
- : filtered
190
- }}
191
- ListboxProps={{ style: { maxHeight: 250 } }}
192
- onChange={(_, selectedOption) => onChange(selectedOption)}
193
- renderInput={params => {
194
- const { helperText, InputProps = {} } = TextFieldProps
195
- const TextFieldInputProps = {
196
- ...params.InputProps,
197
- ...InputProps,
198
- endAdornment: (
199
- <>
200
- {regions.length === 0 && searchOptions.length === 0 ? (
201
- <CircularProgress color="inherit" size={20} />
202
- ) : (
203
- <InputAdornment position="end" style={{ marginRight: 7 }}>
204
- <SearchIcon />
205
- </InputAdornment>
206
- )}
207
- {params.InputProps.endAdornment}
208
- </>
209
- ),
210
- }
211
- return (
212
- <TextField
213
- {...params}
214
- {...TextFieldProps}
215
- helperText={helperText}
216
- value={coarseVisibleLocStrings || value || ''}
217
- InputProps={TextFieldInputProps}
218
- placeholder="Search for location"
219
- onChange={e => {
220
- setCurrentSearch(e.target.value)
221
- }}
222
- />
223
- )
224
- }}
225
- renderOption={option => {
226
- const { result } = option
227
- const rendering = result.getLabel()
228
- // if renderingComponent is provided render that
229
- const component = result.getRenderingComponent()
230
- if (component) {
231
- if (React.isValidElement(component)) {
232
- return component
233
- }
234
- }
235
- return <Typography noWrap>{rendering}</Typography>
236
- }}
237
- getOptionLabel={option => {
238
- // needed for filtering options and value
239
- return (
240
- (typeof option === 'string' ? option : option.result.getLabel()) ||
241
- ''
242
- )
243
- }}
244
- />
245
- {error ? (
246
- <Typography variant="h6" color="error">{`${error}`}</Typography>
247
- ) : null}
248
- </>
223
+ ]
224
+ : []),
225
+ ]
226
+ }}
227
+ renderInput={params => {
228
+ const { helperText, InputProps = {} } = TextFieldProps
229
+ return (
230
+ <TextField
231
+ onBlur={() => {
232
+ // this is used to restore a refName or the non-user-typed input
233
+ // to the box on blurring
234
+ setInputValue(inputBoxVal)
235
+ }}
236
+ {...params}
237
+ {...TextFieldProps}
238
+ helperText={helperText}
239
+ InputProps={{
240
+ ...params.InputProps,
241
+ ...InputProps,
242
+
243
+ endAdornment: (
244
+ <>
245
+ {regions.length === 0 ? (
246
+ <CircularProgress color="inherit" size={20} />
247
+ ) : (
248
+ <InputAdornment position="end" style={{ marginRight: 7 }}>
249
+ <SearchIcon />
250
+ </InputAdornment>
251
+ )}
252
+ {params.InputProps.endAdornment}
253
+ </>
254
+ ),
255
+ }}
256
+ placeholder="Search for location"
257
+ onChange={e => {
258
+ setCurrentSearch(e.target.value)
259
+ }}
260
+ />
261
+ )
262
+ }}
263
+ renderOption={option => {
264
+ const { result } = option
265
+ const component = result.getRenderingComponent()
266
+ if (component && React.isValidElement(component)) {
267
+ return component
268
+ }
269
+
270
+ return <Typography noWrap>{result.getDisplayString()}</Typography>
271
+ }}
272
+ getOptionLabel={option =>
273
+ (typeof option === 'string' ? option : option.result.getLabel()) || ''
274
+ }
275
+ />
249
276
  )
250
277
  }
251
278
 
@@ -83,21 +83,6 @@ export default function SearchResultsDialog({
83
83
  session.notify(`${e}`, 'warning')
84
84
  }
85
85
  }
86
- function handleShowTrack(trackId: string) {
87
- const trackConfigSchema = pluginManager.pluggableConfigSchemaType('track')
88
- const configuration = resolveIdentifier(
89
- trackConfigSchema,
90
- getRoot(model),
91
- trackId,
92
- )
93
- // check if we have any tracks with that configuration
94
- const shownTracks = model.tracks.filter(
95
- t => t.configuration === configuration,
96
- )
97
- if (shownTracks.length === 0) {
98
- model.showTrack(trackId)
99
- }
100
- }
101
86
 
102
87
  function getTrackName(trackId: string | undefined) {
103
88
  if (trackId) {
@@ -184,7 +169,7 @@ export default function SearchResultsDialog({
184
169
  handleClick(result.getLocation())
185
170
  const resultTrackId = result.getTrackId()
186
171
  if (resultTrackId) {
187
- handleShowTrack(resultTrackId)
172
+ model.showTrack(resultTrackId)
188
173
  }
189
174
  handleClose()
190
175
  }}
@@ -1,10 +1,5 @@
1
1
  import React, { useEffect, useMemo, useState } from 'react'
2
- import { observer } from 'mobx-react'
3
- import { saveAs } from 'file-saver'
4
- import { Region } from '@jbrowse/core/util/types'
5
- import { readConfObject } from '@jbrowse/core/configuration'
6
- import copy from 'copy-to-clipboard'
7
- import { makeStyles } from '@material-ui/core/styles'
2
+
8
3
  import {
9
4
  Button,
10
5
  CircularProgress,
@@ -17,7 +12,13 @@ import {
17
12
  Divider,
18
13
  IconButton,
19
14
  TextField,
15
+ makeStyles,
20
16
  } from '@material-ui/core'
17
+ import { observer } from 'mobx-react'
18
+ import { saveAs } from 'file-saver'
19
+ import { Region } from '@jbrowse/core/util/types'
20
+ import { readConfObject } from '@jbrowse/core/configuration'
21
+ import copy from 'copy-to-clipboard'
21
22
  import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
22
23
  import CloseIcon from '@material-ui/icons/Close'
23
24
  import GetAppIcon from '@material-ui/icons/GetApp'
@@ -44,12 +45,14 @@ const useStyles = makeStyles(theme => ({
44
45
  },
45
46
  }))
46
47
 
48
+ type LGV = LinearGenomeViewModel
49
+
47
50
  /**
48
51
  * Fetches and returns a list features for a given list of regions
49
52
  */
50
53
  async function fetchSequence(
51
- model: LinearGenomeViewModel,
52
- selectedRegions: Region[],
54
+ model: LGV,
55
+ regions: Region[],
53
56
  signal?: AbortSignal,
54
57
  ) {
55
58
  const session = getSession(model)
@@ -75,7 +78,7 @@ async function fetchSequence(
75
78
 
76
79
  const sessionId = 'getSequence'
77
80
  const chunks = (await Promise.all(
78
- selectedRegions.map(region =>
81
+ regions.map(region =>
79
82
  rpcManager.call(sessionId, 'CoreGetFeatures', {
80
83
  adapterConfig,
81
84
  region,
@@ -98,9 +101,9 @@ function SequenceDialog({
98
101
  }) {
99
102
  const classes = useStyles()
100
103
  const session = getSession(model)
101
- const [error, setError] = useState<Error>()
102
- const [sequence, setSequence] = useState('')
103
- const loading = Boolean(!sequence) || Boolean(error)
104
+ const [error, setError] = useState<unknown>()
105
+ const [sequence, setSequence] = useState<string>()
106
+ const loading = Boolean(sequence === undefined)
104
107
  const { leftOffset, rightOffset } = model
105
108
 
106
109
  // avoid infinite looping of useEffect
@@ -115,26 +118,6 @@ function SequenceDialog({
115
118
  let active = true
116
119
  const controller = new AbortController()
117
120
 
118
- function formatSequence(seqChunks: Feature[]) {
119
- return formatSeqFasta(
120
- seqChunks.map(chunk => {
121
- const chunkSeq = chunk.get('seq')
122
- const chunkRefName = chunk.get('refName')
123
- const chunkStart = chunk.get('start') + 1
124
- const chunkEnd = chunk.get('end')
125
- const chunkLocstring = `${chunkRefName}:${chunkStart}-${chunkEnd}`
126
- if (chunkSeq?.length !== chunkEnd - chunkStart + 1) {
127
- throw new Error(
128
- `${chunkLocstring} returned ${chunkSeq.length.toLocaleString()} bases, but should have returned ${(
129
- chunkEnd - chunkStart
130
- ).toLocaleString()}`,
131
- )
132
- }
133
- return { header: chunkLocstring, seq: chunkSeq }
134
- }),
135
- )
136
- }
137
-
138
121
  ;(async () => {
139
122
  try {
140
123
  if (regionsSelected.length > 0) {
@@ -144,7 +127,27 @@ function SequenceDialog({
144
127
  controller.signal,
145
128
  )
146
129
  if (active) {
147
- setSequence(formatSequence(chunks))
130
+ setSequence(
131
+ formatSeqFasta(
132
+ chunks
133
+ .filter(f => !!f)
134
+ .map(chunk => {
135
+ const chunkSeq = chunk.get('seq')
136
+ const chunkRefName = chunk.get('refName')
137
+ const chunkStart = chunk.get('start') + 1
138
+ const chunkEnd = chunk.get('end')
139
+ const chunkLocstring = `${chunkRefName}:${chunkStart}-${chunkEnd}`
140
+ if (chunkSeq?.length !== chunkEnd - chunkStart + 1) {
141
+ throw new Error(
142
+ `${chunkLocstring} returned ${chunkSeq.length.toLocaleString()} bases, but should have returned ${(
143
+ chunkEnd - chunkStart
144
+ ).toLocaleString()}`,
145
+ )
146
+ }
147
+ return { header: chunkLocstring, seq: chunkSeq }
148
+ }),
149
+ ),
150
+ )
148
151
  }
149
152
  } else {
150
153
  throw new Error('Selected region is out of bounds')
@@ -163,7 +166,7 @@ function SequenceDialog({
163
166
  }
164
167
  }, [model, session, regionsSelected, setSequence])
165
168
 
166
- const sequenceTooLarge = sequence.length > 1_000_000
169
+ const sequenceTooLarge = sequence ? sequence.length > 1_000_000 : false
167
170
 
168
171
  return (
169
172
  <Dialog
@@ -205,28 +208,26 @@ function SequenceDialog({
205
208
  />
206
209
  </Container>
207
210
  ) : null}
208
- {sequence !== undefined ? (
209
- <TextField
210
- data-testid="rubberband-sequence"
211
- variant="outlined"
212
- multiline
213
- rows={5}
214
- disabled={sequenceTooLarge}
215
- className={classes.dialogContent}
216
- fullWidth
217
- value={
218
- sequenceTooLarge
219
- ? 'Reference sequence too large to display, use the download FASTA button'
220
- : sequence
221
- }
222
- InputProps={{
223
- readOnly: true,
224
- classes: {
225
- input: classes.textAreaFont,
226
- },
227
- }}
228
- />
229
- ) : null}
211
+ <TextField
212
+ data-testid="rubberband-sequence"
213
+ variant="outlined"
214
+ multiline
215
+ rows={5}
216
+ disabled={sequenceTooLarge}
217
+ className={classes.dialogContent}
218
+ fullWidth
219
+ value={
220
+ sequenceTooLarge
221
+ ? 'Reference sequence too large to display, use the download FASTA button'
222
+ : sequence
223
+ }
224
+ InputProps={{
225
+ readOnly: true,
226
+ classes: {
227
+ input: classes.textAreaFont,
228
+ },
229
+ }}
230
+ />
230
231
  </DialogContent>
231
232
  <DialogActions>
232
233
  <Button
@@ -234,7 +235,7 @@ function SequenceDialog({
234
235
  copy(sequence || '')
235
236
  session.notify('Copied to clipboard', 'success')
236
237
  }}
237
- disabled={loading || sequenceTooLarge}
238
+ disabled={loading || !!error || sequenceTooLarge}
238
239
  color="primary"
239
240
  startIcon={<ContentCopyIcon />}
240
241
  >
@@ -247,7 +248,7 @@ function SequenceDialog({
247
248
  })
248
249
  saveAs(seqFastaFile, 'jbrowse_ref_seq.fa')
249
250
  }}
250
- disabled={loading}
251
+ disabled={loading || !!error}
251
252
  color="primary"
252
253
  startIcon={<GetAppIcon />}
253
254
  >