@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.
- package/dist/BaseLinearDisplay/components/BaseLinearDisplay.d.ts +3 -2
- package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +20 -10
- package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +2 -2
- package/dist/LinearBareDisplay/model.d.ts +10 -9
- package/dist/LinearBasicDisplay/model.d.ts +13 -9
- package/dist/LinearGenomeView/components/Header.d.ts +2 -4
- package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +1 -11
- package/dist/LinearGenomeView/components/ScaleBar.d.ts +46 -14
- package/dist/LinearGenomeView/components/util.d.ts +2 -0
- package/dist/LinearGenomeView/index.d.ts +13 -2
- package/dist/index.d.ts +47 -56
- package/dist/plugin-linear-genome-view.cjs.development.js +642 -423
- package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
- package/dist/plugin-linear-genome-view.esm.js +652 -433
- package/dist/plugin-linear-genome-view.esm.js.map +1 -1
- package/package.json +4 -2
- package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +100 -21
- package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +10 -10
- package/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +15 -13
- package/src/LinearBasicDisplay/model.ts +25 -3
- package/src/LinearGenomeView/components/ExportSvgDialog.tsx +17 -8
- package/src/LinearGenomeView/components/Header.tsx +101 -104
- package/src/LinearGenomeView/components/ImportForm.tsx +146 -113
- package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -6
- package/src/LinearGenomeView/components/OverviewScaleBar.tsx +4 -1
- package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +196 -169
- package/src/LinearGenomeView/components/SearchResultsDialog.tsx +1 -16
- package/src/LinearGenomeView/components/SequenceDialog.tsx +59 -58
- package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +5 -177
- package/src/LinearGenomeView/components/util.ts +8 -0
- package/src/LinearGenomeView/index.tsx +39 -28
- 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 {
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 [
|
|
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
|
|
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
|
|
115
|
+
const regions = assembly?.regions || []
|
|
91
116
|
|
|
92
|
-
const options
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
52
|
-
|
|
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
|
-
|
|
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<
|
|
102
|
-
const [sequence, setSequence] = useState(
|
|
103
|
-
const loading = Boolean(
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
>
|