@jbrowse/plugin-linear-genome-view 1.4.1 → 1.5.2

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,20 +1,23 @@
1
- import React, { useState } from 'react'
1
+ import React, { useState, lazy } from 'react'
2
2
  import { observer } from 'mobx-react'
3
3
  import { getSession } from '@jbrowse/core/util'
4
- // import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
5
- import AssemblySelector from '@jbrowse/core/ui/AssemblySelector'
6
4
  import {
7
5
  Button,
8
6
  CircularProgress,
9
7
  Container,
10
8
  Grid,
11
- Typography,
12
9
  makeStyles,
13
10
  } from '@material-ui/core'
14
- // other
11
+ import { SearchType } from '@jbrowse/core/data_adapters/BaseAdapter'
12
+ import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
13
+ import AssemblySelector from '@jbrowse/core/ui/AssemblySelector'
14
+ import ErrorMessage from '@jbrowse/core/ui/ErrorMessage'
15
+ import CloseIcon from '@material-ui/icons/Close'
16
+
17
+ // locals
15
18
  import RefNameAutocomplete from './RefNameAutocomplete'
16
- import SearchResultsDialog from './SearchResultsDialog'
17
19
  import { LinearGenomeViewModel } from '..'
20
+ const SearchResultsDialog = lazy(() => import('./SearchResultsDialog'))
18
21
 
19
22
  const useStyles = makeStyles(theme => ({
20
23
  importFormContainer: {
@@ -27,14 +30,6 @@ const useStyles = makeStyles(theme => ({
27
30
 
28
31
  type LGV = LinearGenomeViewModel
29
32
 
30
- const ErrorDisplay = observer(({ error }: { error?: Error | string }) => {
31
- return (
32
- <Typography variant="h6" color="error">
33
- {`${error}`}
34
- </Typography>
35
- )
36
- })
37
-
38
33
  const ImportForm = observer(({ model }: { model: LGV }) => {
39
34
  const classes = useStyles()
40
35
  const session = getSession(model)
@@ -46,7 +41,6 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
46
41
  } = model
47
42
  const [selectedAsm, setSelectedAsm] = useState(assemblyNames[0])
48
43
  const [error, setError] = useState<typeof modelError | undefined>(modelError)
49
- const message = !assemblyNames.length ? 'No configured assemblies' : ''
50
44
  const searchScope = model.searchScope(selectedAsm)
51
45
 
52
46
  const assembly = assemblyManager.get(selectedAsm)
@@ -55,179 +49,166 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
55
49
  : 'No configured assemblies'
56
50
  const regions = assembly?.regions || []
57
51
  const err = assemblyError || error
58
- const [mySelectedRegion, setSelectedRegion] = useState<string>()
59
- const [optionTrackId, setOptionTrackId] = useState<string>()
60
- const [optionLocation, setOptionLocation] = useState<string>()
61
- const selectedRegion = mySelectedRegion || regions[0]?.refName
62
52
 
63
- async function fetchResults(queryString: string) {
53
+ const [myOption, setOption] = useState<BaseResult>()
54
+
55
+ // use this instead of useState initializer because the useState initializer
56
+ // won't update in response to an observable
57
+ const option =
58
+ myOption ||
59
+ new BaseResult({
60
+ label: regions[0]?.refName,
61
+ })
62
+
63
+ const selectedRegion = option?.getLocation()
64
+
65
+ async function fetchResults(query: string, searchType?: SearchType) {
64
66
  if (!textSearchManager) {
65
67
  console.warn('No text search manager')
66
68
  }
67
- const results = await textSearchManager?.search(
69
+
70
+ const textSearchResults = await textSearchManager?.search(
68
71
  {
69
- queryString: queryString.toLowerCase(),
70
- searchType: 'exact',
72
+ queryString: query,
73
+ searchType,
71
74
  },
72
75
  searchScope,
73
76
  rankSearchResults,
74
77
  )
75
78
 
76
- return results?.filter(
77
- (elem, index, self) =>
78
- index === self.findIndex(t => t.getId() === elem.getId()),
79
- )
79
+ const refNameResults = assembly?.allRefNames
80
+ ?.filter(refName => refName.startsWith(query))
81
+ .map(r => new BaseResult({ label: r }))
82
+ .slice(0, 10)
83
+
84
+ return [...(refNameResults || []), ...(textSearchResults || [])]
80
85
  }
81
- /**
82
- * We first check to see if the identifier/label is an appropriate region,
83
- * if it is then we set that as our displayed region
84
- * if the label was not a valid region, then
85
- * 1) we get the trackId and the location/locStr of the option we chose
86
- * 2) we then use the label to try and fetch for exact matches through our
87
- * textSearchManager
88
- * 3) if we get any hits by requerying the textSearchManager, then we either
89
- * navigate to single hit's location or pop open the the dialog with all the results
90
- * 4) if there were no hits from requerying, then we use (1) the chosen options'
91
- * trackId and locStr to navigate and show that track
92
- * 5) error handling
93
- * @param input - selectedRegion/result label
94
- */
86
+
87
+ // gets a string as input, or use stored option results from previous query,
88
+ // then re-query and
89
+ // 1) if it has multiple results: pop a dialog
90
+ // 2) if it's a single result navigate to it
91
+ // 3) else assume it's a locstring and navigate to it
95
92
  async function handleSelectedRegion(input: string) {
96
- let trackId = optionTrackId
97
- let location = optionLocation
98
- const newRegion = regions.find(r => selectedRegion === r.refName)
99
- if (newRegion) {
100
- model.setDisplayedRegions([newRegion])
101
- // we use showAllRegions after setDisplayedRegions to make the entire
102
- // region visible, xref #1703
103
- model.showAllRegions()
104
- } else {
105
- const results = await fetchResults(input)
106
- if (results && results.length > 1) {
107
- model.setSearchResults(results, input.toLowerCase())
93
+ if (!option) {
94
+ return
95
+ }
96
+ let trackId = option.getTrackId()
97
+ let location = input || option.getLocation() || ''
98
+ try {
99
+ if (assembly?.allRefNames?.includes(location)) {
100
+ model.navToLocString(location, selectedAsm)
108
101
  } else {
109
- if (results?.length === 1) {
102
+ const results = await fetchResults(input, 'exact')
103
+ if (results && results.length > 1) {
104
+ model.setSearchResults(results, input.toLowerCase())
105
+ return
106
+ } else if (results?.length === 1) {
110
107
  location = results[0].getLocation()
111
108
  trackId = results[0].getTrackId()
112
109
  }
113
- try {
114
- if (location) {
115
- model.navToLocString(location, selectedAsm)
116
- }
117
- } catch (e) {
118
- if (`${e}` === `Error: Unknown reference sequence "${input}"`) {
119
- model.setSearchResults(results, input.toLocaleLowerCase())
120
- } else {
121
- console.warn(e)
122
- session.notify(`${e}`, 'warning')
123
- }
124
- }
125
- try {
126
- if (trackId) {
127
- model.showTrack(trackId)
128
- }
129
- } catch (e) {
130
- console.warn(
131
- `'${e}' occurred while attempting to show track: ${trackId}`,
132
- )
110
+
111
+ model.navToLocString(location, selectedAsm)
112
+ if (trackId) {
113
+ model.showTrack(trackId)
133
114
  }
134
115
  }
116
+ } catch (e) {
117
+ console.error(e)
118
+ session.notify(`${e}`, 'warning')
135
119
  }
136
120
  }
137
121
 
122
+ // implementation notes:
123
+ // having this wrapped in a form allows intuitive use of enter key to submit
138
124
  return (
139
125
  <div>
140
- {err ? <ErrorDisplay error={err} /> : null}
126
+ {err ? <ErrorMessage error={err} /> : null}
141
127
  <Container className={classes.importFormContainer}>
142
- <Grid container spacing={1} justifyContent="center" alignItems="center">
143
- <Grid item>
144
- <AssemblySelector
145
- onChange={val => {
146
- setError(undefined)
147
- setSelectedAsm(val)
148
- }}
149
- session={session}
150
- selected={selectedAsm}
151
- />
152
- </Grid>
153
- <Grid item>
154
- {selectedAsm ? (
155
- err ? (
156
- <Typography color="error">X</Typography>
157
- ) : selectedRegion && model.volatileWidth ? (
158
- <RefNameAutocomplete
159
- model={model}
160
- assemblyName={message ? undefined : selectedAsm}
161
- value={selectedRegion}
162
- onSelect={option => {
163
- setSelectedRegion(option.getLabel())
164
- setOptionTrackId(option.getTrackId() || '')
165
- setOptionLocation(option.getLocation())
166
- }}
167
- TextFieldProps={{
168
- margin: 'normal',
169
- variant: 'outlined',
170
- helperText: 'Enter a sequence or location',
171
- onBlur: event => {
172
- if (event.target.value !== '') {
173
- setSelectedRegion(event.target.value)
174
- } else {
175
- setSelectedRegion(regions[0].refName)
176
- }
177
- },
178
- onKeyPress: event => {
179
- const elt = event.target as HTMLInputElement
180
- // maybe check regular expression here to see if it's a
181
- // locstring try defaulting exact matches to first exact
182
- // match
183
- if (event.key === 'Enter') {
184
- handleSelectedRegion(elt.value)
185
- }
186
- },
187
- }}
188
- />
189
- ) : (
190
- <CircularProgress role="progressbar" size={20} disableShrink />
191
- )
192
- ) : null}
193
- </Grid>
194
- <Grid item>
195
- <Button
196
- disabled={!selectedRegion}
197
- className={classes.button}
198
- onClick={() => {
199
- model.setError(undefined)
200
- if (selectedRegion) {
201
- handleSelectedRegion(selectedRegion)
202
- }
203
- }}
204
- variant="contained"
205
- color="primary"
206
- >
207
- Open
208
- </Button>
209
- <Button
210
- disabled={!selectedRegion}
211
- className={classes.button}
212
- onClick={() => {
213
- model.setError(undefined)
214
- model.showAllRegionsInAssembly(selectedAsm)
215
- }}
216
- variant="contained"
217
- color="secondary"
218
- >
219
- Show all regions in assembly
220
- </Button>
128
+ <form onSubmit={event => event.preventDefault()}>
129
+ <Grid
130
+ container
131
+ spacing={1}
132
+ justifyContent="center"
133
+ alignItems="center"
134
+ >
135
+ <Grid item>
136
+ <AssemblySelector
137
+ onChange={val => {
138
+ setError(undefined)
139
+ setSelectedAsm(val)
140
+ }}
141
+ session={session}
142
+ selected={selectedAsm}
143
+ />
144
+ </Grid>
145
+ <Grid item>
146
+ {selectedAsm ? (
147
+ err ? (
148
+ <CloseIcon style={{ color: 'red' }} />
149
+ ) : selectedRegion ? (
150
+ <RefNameAutocomplete
151
+ fetchResults={fetchResults}
152
+ model={model}
153
+ assemblyName={assemblyError ? undefined : selectedAsm}
154
+ value={selectedRegion}
155
+ // note: minWidth 270 accomodates full width of helperText
156
+ minWidth={270}
157
+ onSelect={option => setOption(option)}
158
+ TextFieldProps={{
159
+ margin: 'normal',
160
+ variant: 'outlined',
161
+ helperText:
162
+ 'Enter sequence name, feature name, or location',
163
+ }}
164
+ />
165
+ ) : (
166
+ <CircularProgress
167
+ role="progressbar"
168
+ size={20}
169
+ disableShrink
170
+ />
171
+ )
172
+ ) : null}
173
+ </Grid>
174
+ <Grid item></Grid>
175
+ <Grid item>
176
+ <Button
177
+ type="submit"
178
+ disabled={!selectedRegion}
179
+ className={classes.button}
180
+ onClick={() => {
181
+ model.setError(undefined)
182
+ if (selectedRegion) {
183
+ handleSelectedRegion(selectedRegion)
184
+ }
185
+ }}
186
+ variant="contained"
187
+ color="primary"
188
+ >
189
+ Open
190
+ </Button>
191
+ <Button
192
+ disabled={!selectedRegion}
193
+ className={classes.button}
194
+ onClick={() => {
195
+ model.setError(undefined)
196
+ model.showAllRegionsInAssembly(selectedAsm)
197
+ }}
198
+ variant="contained"
199
+ color="secondary"
200
+ >
201
+ Show all regions in assembly
202
+ </Button>
203
+ </Grid>
221
204
  </Grid>
222
- </Grid>
205
+ </form>
223
206
  </Container>
224
207
  {isSearchDialogDisplayed ? (
225
208
  <SearchResultsDialog
226
209
  model={model}
227
210
  optAssemblyName={selectedAsm}
228
- handleClose={() => {
229
- model.setSearchResults(undefined, undefined)
230
- }}
211
+ handleClose={() => model.setSearchResults(undefined, undefined)}
231
212
  />
232
213
  ) : null}
233
214
  </div>
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { render } from '@testing-library/react'
2
+ import { fireEvent, render, waitFor } from '@testing-library/react'
3
3
  import { createTestSession } from '@jbrowse/web/src/rootModel'
4
4
  import sizeMe from 'react-sizeme'
5
5
  import 'requestidlecallback-polyfill'
@@ -34,12 +34,12 @@ describe('<LinearGenomeView />', () => {
34
34
  session.addView('LinearGenomeView', { id: 'lgv' })
35
35
  const model = session.views[0]
36
36
  model.setWidth(800)
37
- const { container, findByText } = render(<LinearGenomeView model={model} />)
38
- const openButton = await findByText('Open')
39
- expect(container.firstChild).toMatchSnapshot()
37
+ const { findByText } = render(<LinearGenomeView model={model} />)
40
38
  expect(model.displayedRegions.length).toEqual(0)
41
- openButton.click()
42
- expect(model.displayedRegions.length).toEqual(1)
39
+ fireEvent.click(await findByText('Open'))
40
+ await waitFor(() => {
41
+ expect(model.displayedRegions.length).toEqual(1)
42
+ })
43
43
  })
44
44
  it('renders one track, one region', async () => {
45
45
  const session = createTestSession()