@jbrowse/plugin-linear-genome-view 1.4.4 → 1.5.3

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 (43) hide show
  1. package/dist/BaseLinearDisplay/components/Block.d.ts +7 -10
  2. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +16 -9
  3. package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +2 -2
  4. package/dist/LinearBareDisplay/model.d.ts +8 -8
  5. package/dist/LinearBasicDisplay/model.d.ts +11 -8
  6. package/dist/LinearGenomeView/components/HelpDialog.d.ts +5 -0
  7. package/dist/LinearGenomeView/components/LinearGenomeView.d.ts +3 -5
  8. package/dist/LinearGenomeView/components/LinearGenomeViewSvg.d.ts +4 -0
  9. package/dist/LinearGenomeView/components/OverviewRubberBand.d.ts +2 -3
  10. package/dist/LinearGenomeView/components/OverviewScaleBar.d.ts +116 -2
  11. package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +3 -11
  12. package/dist/LinearGenomeView/components/ScaleBar.d.ts +36 -2
  13. package/dist/LinearGenomeView/components/util.d.ts +2 -0
  14. package/dist/LinearGenomeView/index.d.ts +22 -4
  15. package/dist/index.d.ts +26 -26
  16. package/dist/plugin-linear-genome-view.cjs.development.js +3178 -2884
  17. package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
  18. package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
  19. package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
  20. package/dist/plugin-linear-genome-view.esm.js +3191 -2898
  21. package/dist/plugin-linear-genome-view.esm.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +3 -0
  24. package/src/BaseLinearDisplay/components/Block.tsx +20 -33
  25. package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +3 -7
  26. package/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +15 -13
  27. package/src/LinearBasicDisplay/model.ts +25 -3
  28. package/src/LinearGenomeView/components/ExportSvgDialog.tsx +6 -6
  29. package/src/LinearGenomeView/components/Header.tsx +56 -78
  30. package/src/LinearGenomeView/components/HelpDialog.tsx +81 -0
  31. package/src/LinearGenomeView/components/ImportForm.tsx +139 -158
  32. package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -6
  33. package/src/LinearGenomeView/components/LinearGenomeView.tsx +30 -245
  34. package/src/LinearGenomeView/components/LinearGenomeViewSvg.tsx +317 -0
  35. package/src/LinearGenomeView/components/OverviewRubberBand.tsx +74 -34
  36. package/src/LinearGenomeView/components/OverviewScaleBar.tsx +326 -177
  37. package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +152 -157
  38. package/src/LinearGenomeView/components/SearchResultsDialog.tsx +12 -34
  39. package/src/LinearGenomeView/components/SequenceDialog.tsx +10 -9
  40. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +127 -254
  41. package/src/LinearGenomeView/components/util.ts +10 -0
  42. package/src/LinearGenomeView/index.tsx +69 -27
  43. package/src/index.ts +3 -1
@@ -1,71 +1,40 @@
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
- import React, { useMemo, useEffect, useState } from 'react'
1
+ import React, { lazy, useMemo, useEffect, useState } from 'react'
8
2
  import { observer } from 'mobx-react'
9
-
10
- // jbrowse core
11
3
  import { getSession, useDebounce, measureText } from '@jbrowse/core/util'
12
4
  import BaseResult, {
13
5
  RefSequenceResult,
14
6
  } from '@jbrowse/core/TextSearch/BaseResults'
15
-
16
- // material ui
17
7
  import {
18
8
  CircularProgress,
9
+ IconButton,
19
10
  InputAdornment,
20
11
  Popper,
12
+ PopperProps,
21
13
  TextField,
22
14
  TextFieldProps as TFP,
23
- PopperProps,
24
15
  Typography,
25
16
  } from '@material-ui/core'
17
+
18
+ // icons
26
19
  import SearchIcon from '@material-ui/icons/Search'
27
20
  import Autocomplete from '@material-ui/lab/Autocomplete'
21
+ import HelpIcon from '@material-ui/icons/Help'
28
22
 
29
23
  // locals
30
24
  import { LinearGenomeViewModel } from '..'
25
+ import { dedupe } from './util'
26
+
27
+ // lazy
28
+ const HelpDialog = lazy(() => import('./HelpDialog'))
31
29
 
32
- /**
33
- * Option interface used to format results to display in dropdown
34
- * of the materila ui interface
35
- */
36
30
  export interface Option {
37
31
  group?: string
38
32
  result: BaseResult
39
33
  }
40
34
 
41
- async function fetchResults(
42
- self: LinearGenomeViewModel,
43
- query: string,
44
- assemblyName: string,
45
- ) {
46
- const { textSearchManager } = getSession(self)
47
- const { rankSearchResults } = self
48
- const searchScope = self.searchScope(assemblyName)
49
- return textSearchManager
50
- ?.search(
51
- {
52
- queryString: query,
53
- searchType: 'prefix',
54
- },
55
- searchScope,
56
- rankSearchResults,
57
- )
58
- .then(results =>
59
- results.filter(
60
- (elem, index, self) =>
61
- index === self.findIndex(t => t.label === elem.label),
62
- ),
63
- )
64
- }
65
-
66
35
  // the logic of this method is to only apply a filter to RefSequenceResults
67
- // because they do not have a matchedObject. the trix search results
68
- // already filter so don't need re-filtering
36
+ // because they do not have a matchedObject. the trix search results already
37
+ // filter so don't need re-filtering
69
38
  function filterOptions(options: Option[], searchQuery: string) {
70
39
  return options.filter(option => {
71
40
  const { result } = option
@@ -100,22 +69,28 @@ function RefNameAutocomplete({
100
69
  onSelect,
101
70
  assemblyName,
102
71
  style,
72
+ fetchResults,
103
73
  value,
74
+ minWidth = 200,
104
75
  TextFieldProps = {},
105
76
  }: {
106
77
  model: LinearGenomeViewModel
107
78
  onSelect: (region: BaseResult) => void
108
79
  assemblyName?: string
109
80
  value?: string
81
+ fetchResults: (query: string) => Promise<BaseResult[]>
110
82
  style?: React.CSSProperties
83
+ minWidth?: number
111
84
  TextFieldProps?: TFP
112
85
  }) {
113
86
  const session = getSession(model)
114
87
  const { assemblyManager } = session
115
88
  const [open, setOpen] = useState(false)
116
89
  const [loaded, setLoaded] = useState(true)
90
+ const [isHelpDialogDisplayed, setHelpDialogDisplayed] = useState(false)
117
91
  const [currentSearch, setCurrentSearch] = useState('')
118
- const [searchOptions, setSearchOptions] = useState([] as Option[])
92
+ const [inputValue, setInputValue] = useState('')
93
+ const [searchOptions, setSearchOptions] = useState<Option[]>()
119
94
  const debouncedSearch = useDebounce(currentSearch, 300)
120
95
  const { coarseVisibleLocStrings, hasDisplayedRegions } = model
121
96
  const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined
@@ -145,11 +120,15 @@ function RefNameAutocomplete({
145
120
  }
146
121
 
147
122
  setLoaded(false)
148
- const results = await fetchResults(model, debouncedSearch, assemblyName)
149
- if (results && results.length >= 0 && active) {
150
- setSearchOptions(results.map(result => ({ result })))
123
+ const results = await fetchResults(debouncedSearch)
124
+ if (active) {
125
+ setSearchOptions(
126
+ dedupe(results, r => r.getDisplayString()).map(result => ({
127
+ result,
128
+ })),
129
+ )
130
+ setLoaded(true)
151
131
  }
152
- setLoaded(true)
153
132
  } catch (e) {
154
133
  console.error(e)
155
134
  if (active) {
@@ -161,123 +140,139 @@ function RefNameAutocomplete({
161
140
  return () => {
162
141
  active = false
163
142
  }
164
- }, [assemblyName, debouncedSearch, session, model])
143
+ }, [assemblyName, fetchResults, debouncedSearch, session, model])
165
144
 
166
145
  const inputBoxVal = coarseVisibleLocStrings || value || ''
167
146
 
168
- // heuristic, text width + icon width, minimum 200
169
- const width = Math.min(Math.max(measureText(inputBoxVal, 16) + 25, 200), 550)
147
+ // heuristic, text width + icon width
148
+ // + 45 accomodates help icon and search icon
149
+ const width = Math.min(
150
+ Math.max(measureText(inputBoxVal, 16) + 45, minWidth),
151
+ 550,
152
+ )
170
153
 
171
154
  // notes on implementation:
172
- // selectOnFocus helps highlight the field when clicked
173
- // blurOnSelect helps it so that when the user-re-clicks on the textfield,
174
- // that selectOnFocus re-activates
155
+ // The selectOnFocus setting helps highlight the field when clicked
175
156
  return (
176
- <Autocomplete
177
- id={`refNameAutocomplete-${model.id}`}
178
- data-testid="autocomplete"
179
- disableListWrap
180
- disableClearable
181
- PopperComponent={MyPopper}
182
- disabled={!assemblyName}
183
- freeSolo
184
- includeInputInList
185
- selectOnFocus
186
- blurOnSelect
187
- style={{ ...style, width }}
188
- value={inputBoxVal}
189
- loading={!loaded}
190
- loadingText="loading results"
191
- open={open}
192
- onOpen={() => setOpen(true)}
193
- onClose={() => {
194
- setOpen(false)
195
- setLoaded(true)
196
- if (hasDisplayedRegions) {
197
- setCurrentSearch('')
198
- setSearchOptions([])
199
- }
200
- }}
201
- onChange={(_event, selectedOption) => {
202
- if (!selectedOption || !assemblyName) {
203
- return
204
- }
205
- if (typeof selectedOption === 'string') {
206
- // handles string inputs on keyPress enter
207
- const newResult = new BaseResult({
208
- label: selectedOption,
209
- })
210
- onSelect(newResult)
211
- } else {
212
- const { result } = selectedOption
213
- onSelect(result)
214
- }
215
- }}
216
- options={searchOptions.length === 0 ? options : searchOptions}
217
- getOptionDisabled={option => option?.group === 'limitOption'}
218
- filterOptions={(options, params) => {
219
- const filtered = filterOptions(
220
- options,
221
- params.inputValue.toLocaleLowerCase(),
222
- )
223
- return [
224
- ...filtered.slice(0, 100),
225
- ...(filtered.length > 100
226
- ? [
227
- {
228
- group: 'limitOption',
229
- result: new BaseResult({
230
- label: 'keep typing for more results',
231
- }),
232
- },
233
- ]
234
- : []),
235
- ]
236
- }}
237
- renderInput={params => {
238
- const { helperText, InputProps = {} } = TextFieldProps
239
- return (
240
- <TextField
241
- {...params}
242
- {...TextFieldProps}
243
- helperText={helperText}
244
- InputProps={{
245
- ...params.InputProps,
246
- ...InputProps,
157
+ <>
158
+ <Autocomplete
159
+ id={`refNameAutocomplete-${model.id}`}
160
+ data-testid="autocomplete"
161
+ disableListWrap
162
+ disableClearable
163
+ PopperComponent={MyPopper}
164
+ disabled={!assemblyName}
165
+ freeSolo
166
+ includeInputInList
167
+ selectOnFocus
168
+ style={{ ...style, width }}
169
+ value={inputBoxVal}
170
+ loading={!loaded}
171
+ inputValue={inputValue}
172
+ onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
173
+ loadingText="loading results"
174
+ open={open}
175
+ onOpen={() => setOpen(true)}
176
+ onClose={() => {
177
+ setOpen(false)
178
+ setLoaded(true)
179
+ if (hasDisplayedRegions) {
180
+ setCurrentSearch('')
181
+ setSearchOptions(undefined)
182
+ }
183
+ }}
184
+ onChange={(_event, selectedOption) => {
185
+ if (!selectedOption || !assemblyName) {
186
+ return
187
+ }
247
188
 
248
- endAdornment: (
249
- <>
250
- {regions.length === 0 ? (
251
- <CircularProgress color="inherit" size={20} />
252
- ) : (
253
- <InputAdornment position="end" style={{ marginRight: 7 }}>
254
- <SearchIcon />
255
- </InputAdornment>
256
- )}
257
- {params.InputProps.endAdornment}
258
- </>
259
- ),
260
- }}
261
- placeholder="Search for location"
262
- onChange={e => {
263
- setCurrentSearch(e.target.value)
264
- }}
265
- />
266
- )
267
- }}
268
- renderOption={option => {
269
- const { result } = option
270
- const component = result.getRenderingComponent()
271
- if (component && React.isValidElement(component)) {
272
- return component
273
- }
189
+ if (typeof selectedOption === 'string') {
190
+ // handles string inputs on keyPress enter
191
+ onSelect(new BaseResult({ label: selectedOption }))
192
+ } else {
193
+ onSelect(selectedOption.result)
194
+ }
195
+ setInputValue(inputBoxVal)
196
+ }}
197
+ options={!searchOptions?.length ? options : searchOptions}
198
+ getOptionDisabled={option => option?.group === 'limitOption'}
199
+ filterOptions={(options, params) => {
200
+ const filtered = filterOptions(
201
+ options,
202
+ params.inputValue.toLocaleLowerCase(),
203
+ )
204
+ return [
205
+ ...filtered.slice(0, 100),
206
+ ...(filtered.length > 100
207
+ ? [
208
+ {
209
+ group: 'limitOption',
210
+ result: new BaseResult({
211
+ label: 'keep typing for more results',
212
+ }),
213
+ },
214
+ ]
215
+ : []),
216
+ ]
217
+ }}
218
+ renderInput={params => {
219
+ const { helperText, InputProps = {} } = TextFieldProps
220
+ return (
221
+ <TextField
222
+ onBlur={() => {
223
+ // this is used to restore a refName or the non-user-typed input
224
+ // to the box on blurring
225
+ setInputValue(inputBoxVal)
226
+ }}
227
+ {...params}
228
+ {...TextFieldProps}
229
+ helperText={helperText}
230
+ InputProps={{
231
+ ...params.InputProps,
232
+ ...InputProps,
274
233
 
275
- return <Typography noWrap>{result.getDisplayString()}</Typography>
276
- }}
277
- getOptionLabel={option =>
278
- (typeof option === 'string' ? option : option.result.getLabel()) || ''
279
- }
280
- />
234
+ endAdornment: (
235
+ <>
236
+ {regions.length === 0 ? (
237
+ <CircularProgress color="inherit" size={20} />
238
+ ) : (
239
+ <InputAdornment position="end" style={{ marginRight: 7 }}>
240
+ <SearchIcon />
241
+ <IconButton
242
+ onClick={() => setHelpDialogDisplayed(true)}
243
+ >
244
+ <HelpIcon />
245
+ </IconButton>
246
+ </InputAdornment>
247
+ )}
248
+ {params.InputProps.endAdornment}
249
+ </>
250
+ ),
251
+ }}
252
+ placeholder="Search for location"
253
+ onChange={e => {
254
+ setCurrentSearch(e.target.value)
255
+ }}
256
+ />
257
+ )
258
+ }}
259
+ renderOption={option => {
260
+ const { result } = option
261
+ const component = result.getRenderingComponent()
262
+ if (component && React.isValidElement(component)) {
263
+ return component
264
+ }
265
+
266
+ return <Typography noWrap>{result.getDisplayString()}</Typography>
267
+ }}
268
+ getOptionLabel={option =>
269
+ (typeof option === 'string' ? option : option.result.getLabel()) || ''
270
+ }
271
+ />
272
+ {isHelpDialogDisplayed ? (
273
+ <HelpDialog handleClose={() => setHelpDialogDisplayed(false)} />
274
+ ) : null}
275
+ </>
281
276
  )
282
277
  }
283
278
 
@@ -6,7 +6,6 @@ import {
6
6
  Dialog,
7
7
  DialogActions,
8
8
  DialogContent,
9
- DialogContentText,
10
9
  DialogTitle,
11
10
  Divider,
12
11
  IconButton,
@@ -18,10 +17,9 @@ import {
18
17
  TableRow,
19
18
  Typography,
20
19
  Paper,
20
+ makeStyles,
21
21
  } from '@material-ui/core'
22
22
  import CloseIcon from '@material-ui/icons/Close'
23
- import { makeStyles } from '@material-ui/core/styles'
24
- import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
25
23
  import { LinearGenomeViewModel } from '../..'
26
24
 
27
25
  export const useStyles = makeStyles(theme => ({
@@ -101,8 +99,8 @@ export default function SearchResultsDialog({
101
99
 
102
100
  return (
103
101
  <Dialog open maxWidth="xl" onClose={handleClose}>
104
- <DialogTitle id="search-results-dialog">
105
- Search Results
102
+ <DialogTitle>
103
+ Search results
106
104
  {handleClose ? (
107
105
  <IconButton
108
106
  data-testid="close-resultsDialog"
@@ -117,18 +115,15 @@ export default function SearchResultsDialog({
117
115
  </DialogTitle>
118
116
  <Divider />
119
117
  <DialogContent>
120
- {model.searchResults?.length === 0 ||
121
- model.searchResults === undefined ? (
118
+ {!model.searchResults?.length ? (
122
119
  <Typography>
123
- {`No results found for `}
124
- <b>{model.searchQuery}</b>
120
+ No results found for <b>{model.searchQuery}</b>
125
121
  </Typography>
126
122
  ) : (
127
123
  <>
128
- <DialogContentText id="alert-dialog-slide-description">
129
- {`Showing results for `}
130
- <b>{model.searchQuery}</b>
131
- </DialogContentText>
124
+ <Typography>
125
+ Showing results for <b>{model.searchQuery}</b>
126
+ </Typography>
132
127
  <TableContainer component={Paper}>
133
128
  <Table>
134
129
  <TableHead>
@@ -140,8 +135,8 @@ export default function SearchResultsDialog({
140
135
  </TableRow>
141
136
  </TableHead>
142
137
  <TableBody>
143
- {model.searchResults.map((result: BaseResult, index) => (
144
- <TableRow key={`${result.getLabel()}-${index}`}>
138
+ {model.searchResults.map(result => (
139
+ <TableRow key={`${result.getId()}`}>
145
140
  <TableCell component="th" scope="row">
146
141
  {result.getLabel()}
147
142
  </TableCell>
@@ -151,18 +146,6 @@ export default function SearchResultsDialog({
151
146
  <TableCell align="right">
152
147
  {getTrackName(result.getTrackId()) || 'N/A'}
153
148
  </TableCell>
154
- <TableCell align="right">
155
- <Button
156
- onClick={() => {
157
- handleClick(result.getLocation())
158
- handleClose()
159
- }}
160
- color="primary"
161
- variant="contained"
162
- >
163
- Go to location
164
- </Button>
165
- </TableCell>
166
149
  <TableCell align="right">
167
150
  <Button
168
151
  onClick={() => {
@@ -177,7 +160,7 @@ export default function SearchResultsDialog({
177
160
  color="primary"
178
161
  variant="contained"
179
162
  >
180
- Show Track
163
+ Go
181
164
  </Button>
182
165
  </TableCell>
183
166
  </TableRow>
@@ -190,12 +173,7 @@ export default function SearchResultsDialog({
190
173
  </DialogContent>
191
174
  <Divider />
192
175
  <DialogActions>
193
- <Button
194
- onClick={() => {
195
- handleClose()
196
- }}
197
- color="primary"
198
- >
176
+ <Button onClick={() => handleClose()} color="primary">
199
177
  Cancel
200
178
  </Button>
201
179
  </DialogActions>
@@ -17,14 +17,18 @@ import {
17
17
  import { observer } from 'mobx-react'
18
18
  import { saveAs } from 'file-saver'
19
19
  import { Region } from '@jbrowse/core/util/types'
20
- import { readConfObject } from '@jbrowse/core/configuration'
20
+ import { getConf } from '@jbrowse/core/configuration'
21
21
  import copy from 'copy-to-clipboard'
22
- import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
23
- import CloseIcon from '@material-ui/icons/Close'
24
- import GetAppIcon from '@material-ui/icons/GetApp'
25
22
  import { getSession } from '@jbrowse/core/util'
26
23
  import { Feature } from '@jbrowse/core/util/simpleFeature'
27
24
  import { formatSeqFasta } from '@jbrowse/core/util/formatFastaStrings'
25
+
26
+ // icons
27
+ import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
28
+ import CloseIcon from '@material-ui/icons/Close'
29
+ import GetAppIcon from '@material-ui/icons/GetApp'
30
+
31
+ // locals
28
32
  import { LinearGenomeViewModel } from '..'
29
33
 
30
34
  const useStyles = makeStyles(theme => ({
@@ -71,10 +75,7 @@ async function fetchSequence(
71
75
  if (!assembly) {
72
76
  throw new Error(`assembly ${assemblyName} not found`)
73
77
  }
74
- const adapterConfig = readConfObject(assembly.configuration, [
75
- 'sequence',
76
- 'adapter',
77
- ])
78
+ const adapterConfig = getConf(assembly, ['sequence', 'adapter'])
78
79
 
79
80
  const sessionId = 'getSequence'
80
81
  const chunks = (await Promise.all(
@@ -101,7 +102,7 @@ function SequenceDialog({
101
102
  }) {
102
103
  const classes = useStyles()
103
104
  const session = getSession(model)
104
- const [error, setError] = useState<Error>()
105
+ const [error, setError] = useState<unknown>()
105
106
  const [sequence, setSequence] = useState<string>()
106
107
  const loading = Boolean(sequence === undefined)
107
108
  const { leftOffset, rightOffset } = model