@jbrowse/plugin-linear-genome-view 1.4.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 (30) hide show
  1. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +16 -9
  2. package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +2 -2
  3. package/dist/LinearBareDisplay/model.d.ts +8 -8
  4. package/dist/LinearBasicDisplay/model.d.ts +11 -8
  5. package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +0 -10
  6. package/dist/LinearGenomeView/components/ScaleBar.d.ts +22 -2
  7. package/dist/LinearGenomeView/components/util.d.ts +2 -0
  8. package/dist/LinearGenomeView/index.d.ts +13 -2
  9. package/dist/index.d.ts +26 -26
  10. package/dist/plugin-linear-genome-view.cjs.development.js +272 -268
  11. package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
  12. package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
  13. package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
  14. package/dist/plugin-linear-genome-view.esm.js +272 -269
  15. package/dist/plugin-linear-genome-view.esm.js.map +1 -1
  16. package/package.json +2 -2
  17. package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +3 -0
  18. package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +3 -7
  19. package/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +15 -13
  20. package/src/LinearBasicDisplay/model.ts +25 -3
  21. package/src/LinearGenomeView/components/ExportSvgDialog.tsx +6 -6
  22. package/src/LinearGenomeView/components/Header.tsx +40 -74
  23. package/src/LinearGenomeView/components/ImportForm.tsx +124 -134
  24. package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -6
  25. package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +20 -25
  26. package/src/LinearGenomeView/components/SequenceDialog.tsx +1 -1
  27. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +0 -176
  28. package/src/LinearGenomeView/components/util.ts +8 -0
  29. package/src/LinearGenomeView/index.tsx +14 -13
  30. package/src/index.ts +3 -1
@@ -1,7 +1,9 @@
1
1
  import React, { useState } 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'
4
+ import BaseResult, {
5
+ RefSequenceResult,
6
+ } from '@jbrowse/core/TextSearch/BaseResults'
5
7
  import AssemblySelector from '@jbrowse/core/ui/AssemblySelector'
6
8
  import {
7
9
  Button,
@@ -15,6 +17,7 @@ import {
15
17
  import RefNameAutocomplete from './RefNameAutocomplete'
16
18
  import SearchResultsDialog from './SearchResultsDialog'
17
19
  import { LinearGenomeViewModel } from '..'
20
+ import { dedupe } from './util'
18
21
 
19
22
  const useStyles = makeStyles(theme => ({
20
23
  importFormContainer: {
@@ -55,10 +58,19 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
55
58
  : 'No configured assemblies'
56
59
  const regions = assembly?.regions || []
57
60
  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
61
+
62
+ const [myOption, setOption] = useState<BaseResult | undefined>()
63
+
64
+ // use this instead of useState initializer because the useState initializer
65
+ // won't update in response to an observable
66
+ const option =
67
+ myOption ||
68
+ new RefSequenceResult({
69
+ refName: regions[0]?.refName,
70
+ label: regions[0]?.refName,
71
+ })
72
+
73
+ const selectedRegion = option?.getLocation()
62
74
 
63
75
  async function fetchResults(queryString: string) {
64
76
  if (!textSearchManager) {
@@ -73,153 +85,131 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
73
85
  rankSearchResults,
74
86
  )
75
87
 
76
- return results?.filter(
77
- (elem, index, self) =>
78
- index === self.findIndex(t => t.getId() === elem.getId()),
79
- )
88
+ return dedupe(results)
80
89
  }
90
+
81
91
  /**
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
92
+ * gets a string as input, or use stored option results from previous query,
93
+ * then re-query and
94
+ * 1) if it has multiple results: pop a dialog
95
+ * 2) if it's a single result navigate to it
96
+ * 3) else assume it's a locstring and navigate to it
94
97
  */
95
98
  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())
99
+ if (!option) {
100
+ return
101
+ }
102
+ let trackId = option.getTrackId()
103
+ let location = input || option.getLocation() || ''
104
+ try {
105
+ if (assembly?.refNames?.includes(location)) {
106
+ model.navToLocString(location, selectedAsm)
108
107
  } else {
109
- if (results?.length === 1) {
108
+ const results = await fetchResults(input)
109
+ if (results && results.length > 1) {
110
+ model.setSearchResults(results, input.toLowerCase())
111
+ return
112
+ } else if (results?.length === 1) {
110
113
  location = results[0].getLocation()
111
114
  trackId = results[0].getTrackId()
112
115
  }
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
- )
116
+
117
+ model.navToLocString(location, selectedAsm)
118
+ if (trackId) {
119
+ model.showTrack(trackId)
133
120
  }
134
121
  }
122
+ } catch (e) {
123
+ console.error(e)
124
+ session.notify(`${e}`, 'warning')
135
125
  }
136
126
  }
137
127
 
128
+ // implementation notes:
129
+ // having this wrapped in a form allows intuitive use of enter key to submit
138
130
  return (
139
131
  <div>
140
132
  {err ? <ErrorDisplay error={err} /> : null}
141
133
  <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>
134
+ <form
135
+ onSubmit={event => {
136
+ event.preventDefault()
137
+ }}
138
+ >
139
+ <Grid
140
+ container
141
+ spacing={1}
142
+ justifyContent="center"
143
+ alignItems="center"
144
+ >
145
+ <Grid item>
146
+ <AssemblySelector
147
+ onChange={val => {
148
+ setError(undefined)
149
+ setSelectedAsm(val)
150
+ }}
151
+ session={session}
152
+ selected={selectedAsm}
153
+ />
154
+ </Grid>
155
+ <Grid item>
156
+ {selectedAsm ? (
157
+ err ? (
158
+ <Typography color="error">X</Typography>
159
+ ) : selectedRegion && model.volatileWidth ? (
160
+ <RefNameAutocomplete
161
+ model={model}
162
+ assemblyName={message ? undefined : selectedAsm}
163
+ value={selectedRegion}
164
+ onSelect={option => {
165
+ setOption(option)
166
+ }}
167
+ TextFieldProps={{
168
+ margin: 'normal',
169
+ variant: 'outlined',
170
+ helperText: 'Enter a sequence or location',
171
+ }}
172
+ />
173
+ ) : (
174
+ <CircularProgress
175
+ role="progressbar"
176
+ size={20}
177
+ disableShrink
178
+ />
179
+ )
180
+ ) : null}
181
+ </Grid>
182
+ <Grid item>
183
+ <Button
184
+ type="submit"
185
+ disabled={!selectedRegion}
186
+ className={classes.button}
187
+ onClick={() => {
188
+ model.setError(undefined)
189
+ if (selectedRegion) {
190
+ handleSelectedRegion(selectedRegion)
191
+ }
192
+ }}
193
+ variant="contained"
194
+ color="primary"
195
+ >
196
+ Open
197
+ </Button>
198
+ <Button
199
+ disabled={!selectedRegion}
200
+ className={classes.button}
201
+ onClick={() => {
202
+ model.setError(undefined)
203
+ model.showAllRegionsInAssembly(selectedAsm)
204
+ }}
205
+ variant="contained"
206
+ color="secondary"
207
+ >
208
+ Show all regions in assembly
209
+ </Button>
210
+ </Grid>
221
211
  </Grid>
222
- </Grid>
212
+ </form>
223
213
  </Container>
224
214
  {isSearchDialogDisplayed ? (
225
215
  <SearchResultsDialog
@@ -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()
@@ -1,9 +1,3 @@
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
3
 
@@ -29,10 +23,6 @@ import Autocomplete from '@material-ui/lab/Autocomplete'
29
23
  // locals
30
24
  import { LinearGenomeViewModel } from '..'
31
25
 
32
- /**
33
- * Option interface used to format results to display in dropdown
34
- * of the materila ui interface
35
- */
36
26
  export interface Option {
37
27
  group?: string
38
28
  result: BaseResult
@@ -64,8 +54,8 @@ async function fetchResults(
64
54
  }
65
55
 
66
56
  // 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
57
+ // because they do not have a matchedObject. the trix search results already
58
+ // filter so don't need re-filtering
69
59
  function filterOptions(options: Option[], searchQuery: string) {
70
60
  return options.filter(option => {
71
61
  const { result } = option
@@ -115,6 +105,7 @@ function RefNameAutocomplete({
115
105
  const [open, setOpen] = useState(false)
116
106
  const [loaded, setLoaded] = useState(true)
117
107
  const [currentSearch, setCurrentSearch] = useState('')
108
+ const [inputValue, setInputValue] = useState('')
118
109
  const [searchOptions, setSearchOptions] = useState([] as Option[])
119
110
  const debouncedSearch = useDebounce(currentSearch, 300)
120
111
  const { coarseVisibleLocStrings, hasDisplayedRegions } = model
@@ -146,10 +137,12 @@ function RefNameAutocomplete({
146
137
 
147
138
  setLoaded(false)
148
139
  const results = await fetchResults(model, debouncedSearch, assemblyName)
149
- if (results && results.length >= 0 && active) {
150
- setSearchOptions(results.map(result => ({ result })))
140
+ if (active) {
141
+ if (results && results.length >= 0) {
142
+ setSearchOptions(results.map(result => ({ result })))
143
+ }
144
+ setLoaded(true)
151
145
  }
152
- setLoaded(true)
153
146
  } catch (e) {
154
147
  console.error(e)
155
148
  if (active) {
@@ -169,9 +162,7 @@ function RefNameAutocomplete({
169
162
  const width = Math.min(Math.max(measureText(inputBoxVal, 16) + 25, 200), 550)
170
163
 
171
164
  // 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
165
+ // The selectOnFocus setting helps highlight the field when clicked
175
166
  return (
176
167
  <Autocomplete
177
168
  id={`refNameAutocomplete-${model.id}`}
@@ -183,10 +174,11 @@ function RefNameAutocomplete({
183
174
  freeSolo
184
175
  includeInputInList
185
176
  selectOnFocus
186
- blurOnSelect
187
177
  style={{ ...style, width }}
188
178
  value={inputBoxVal}
189
179
  loading={!loaded}
180
+ inputValue={inputValue}
181
+ onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
190
182
  loadingText="loading results"
191
183
  open={open}
192
184
  onOpen={() => setOpen(true)}
@@ -202,16 +194,14 @@ function RefNameAutocomplete({
202
194
  if (!selectedOption || !assemblyName) {
203
195
  return
204
196
  }
197
+
205
198
  if (typeof selectedOption === 'string') {
206
199
  // handles string inputs on keyPress enter
207
- const newResult = new BaseResult({
208
- label: selectedOption,
209
- })
210
- onSelect(newResult)
200
+ onSelect(new BaseResult({ label: selectedOption }))
211
201
  } else {
212
- const { result } = selectedOption
213
- onSelect(result)
202
+ onSelect(selectedOption.result)
214
203
  }
204
+ setInputValue(inputBoxVal)
215
205
  }}
216
206
  options={searchOptions.length === 0 ? options : searchOptions}
217
207
  getOptionDisabled={option => option?.group === 'limitOption'}
@@ -238,6 +228,11 @@ function RefNameAutocomplete({
238
228
  const { helperText, InputProps = {} } = TextFieldProps
239
229
  return (
240
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
+ }}
241
236
  {...params}
242
237
  {...TextFieldProps}
243
238
  helperText={helperText}
@@ -101,7 +101,7 @@ function SequenceDialog({
101
101
  }) {
102
102
  const classes = useStyles()
103
103
  const session = getSession(model)
104
- const [error, setError] = useState<Error>()
104
+ const [error, setError] = useState<unknown>()
105
105
  const [sequence, setSequence] = useState<string>()
106
106
  const loading = Boolean(sequence === undefined)
107
107
  const { leftOffset, rightOffset } = model
@@ -563,182 +563,6 @@ exports[`<LinearGenomeView /> renders one track, one region 1`] = `
563
563
  </div>
564
564
  `;
565
565
 
566
- exports[`<LinearGenomeView /> renders setup wizard 1`] = `
567
- <div>
568
- <div
569
- class="MuiContainer-root makeStyles-importFormContainer MuiContainer-maxWidthLg"
570
- >
571
- <div
572
- class="MuiGrid-root MuiGrid-container MuiGrid-spacing-xs-1 MuiGrid-align-items-xs-center MuiGrid-justify-content-xs-center"
573
- >
574
- <div
575
- class="MuiGrid-root MuiGrid-item"
576
- >
577
- <div
578
- class="MuiFormControl-root MuiTextField-root makeStyles-importFormEntry MuiFormControl-marginNormal"
579
- >
580
- <label
581
- class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled"
582
- data-shrink="true"
583
- >
584
- Assembly
585
- </label>
586
- <div
587
- class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl"
588
- >
589
- <div
590
- aria-haspopup="listbox"
591
- class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input"
592
- role="button"
593
- tabindex="0"
594
- >
595
- volMyt1
596
- </div>
597
- <input
598
- aria-hidden="true"
599
- class="MuiSelect-nativeInput"
600
- data-testid="assembly-selector"
601
- tabindex="-1"
602
- value="volMyt1"
603
- />
604
- <svg
605
- aria-hidden="true"
606
- class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
607
- focusable="false"
608
- viewBox="0 0 24 24"
609
- >
610
- <path
611
- d="M7 10l5 5 5-5z"
612
- />
613
- </svg>
614
- <fieldset
615
- aria-hidden="true"
616
- class="PrivateNotchedOutline-root MuiOutlinedInput-notchedOutline"
617
- >
618
- <legend
619
- class="PrivateNotchedOutline-legendLabelled PrivateNotchedOutline-legendNotched"
620
- >
621
- <span>
622
- Assembly
623
- </span>
624
- </legend>
625
- </fieldset>
626
- </div>
627
- <p
628
- class="MuiFormHelperText-root MuiFormHelperText-contained MuiFormHelperText-filled"
629
- >
630
- Select assembly to view
631
- </p>
632
- </div>
633
- </div>
634
- <div
635
- class="MuiGrid-root MuiGrid-item"
636
- >
637
- <div
638
- aria-expanded="false"
639
- class="MuiAutocomplete-root"
640
- data-testid="autocomplete"
641
- role="combobox"
642
- style="width: 200px;"
643
- >
644
- <div
645
- class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
646
- >
647
- <div
648
- class="MuiInputBase-root MuiOutlinedInput-root MuiAutocomplete-inputRoot MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-adornedEnd MuiOutlinedInput-adornedEnd"
649
- >
650
- <input
651
- aria-autocomplete="list"
652
- aria-describedby="refNameAutocomplete-lgv-helper-text"
653
- aria-invalid="false"
654
- autocapitalize="none"
655
- autocomplete="off"
656
- class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
657
- id="refNameAutocomplete-lgv"
658
- placeholder="Search for location"
659
- spellcheck="false"
660
- type="text"
661
- value="ctgA"
662
- />
663
- <div
664
- class="MuiInputAdornment-root MuiInputAdornment-positionEnd"
665
- style="margin-right: 7px;"
666
- >
667
- <svg
668
- aria-hidden="true"
669
- class="MuiSvgIcon-root"
670
- focusable="false"
671
- viewBox="0 0 24 24"
672
- >
673
- <path
674
- d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
675
- />
676
- </svg>
677
- </div>
678
- <div
679
- class="MuiAutocomplete-endAdornment"
680
- />
681
- <fieldset
682
- aria-hidden="true"
683
- class="PrivateNotchedOutline-root MuiOutlinedInput-notchedOutline"
684
- style="padding-left: 8px;"
685
- >
686
- <legend
687
- class="PrivateNotchedOutline-legend"
688
- style="width: 0.01px;"
689
- >
690
- <span>
691
-
692
- </span>
693
- </legend>
694
- </fieldset>
695
- </div>
696
- <p
697
- class="MuiFormHelperText-root MuiFormHelperText-contained MuiFormHelperText-filled"
698
- id="refNameAutocomplete-lgv-helper-text"
699
- >
700
- Enter a sequence or location
701
- </p>
702
- </div>
703
- </div>
704
- </div>
705
- <div
706
- class="MuiGrid-root MuiGrid-item"
707
- >
708
- <button
709
- class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-button MuiButton-containedPrimary"
710
- tabindex="0"
711
- type="button"
712
- >
713
- <span
714
- class="MuiButton-label"
715
- >
716
- Open
717
- </span>
718
- <span
719
- class="MuiTouchRipple-root"
720
- />
721
- </button>
722
- <button
723
- class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-button MuiButton-containedSecondary"
724
- tabindex="0"
725
- type="button"
726
- >
727
- <span
728
- class="MuiButton-label"
729
- >
730
- Show all regions in assembly
731
- </span>
732
- <span
733
- class="MuiTouchRipple-root"
734
- />
735
- </button>
736
- </div>
737
- </div>
738
- </div>
739
- </div>
740
- `;
741
-
742
566
  exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
743
567
  <div
744
568
  style="position: relative;"
@@ -0,0 +1,8 @@
1
+ import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
2
+
3
+ export function dedupe(results?: BaseResult[]) {
4
+ return results?.filter(
5
+ (elem, index, self) =>
6
+ index === self.findIndex(t => t.getId() === elem.getId()),
7
+ )
8
+ }