Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow searching by gene name using linear synteny view #4096

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions plugins/linear-comparative-view/src/LaunchLinearSyntenyView.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import PluginManager from '@jbrowse/core/PluginManager'
import { AbstractSessionModel } from '@jbrowse/core/util'
import { LinearSyntenyViewModel } from './LinearSyntenyView/model'
import { when } from 'mobx'

// locals
import { LinearSyntenyViewModel } from './LinearSyntenyView/model'
type LSV = LinearSyntenyViewModel

export default function LaunchLinearSyntenyView(pluginManager: PluginManager) {
Expand Down Expand Up @@ -48,8 +49,12 @@ export default function LaunchLinearSyntenyView(pluginManager: PluginManager) {
await Promise.all(
views.map(async (data, idx) => {
const view = model.views[idx]
const { loc, tracks = [] } = data
await view.navToLocString(loc)
const { assembly, loc, tracks = [] } = data
const asm = await assemblyManager.waitForAssembly(assembly)
if (!asm) {
throw new Error(`Assembly ${data.assembly} failed to load`)
}
await view.navToSearchString({ input: loc, assembly: asm })
tracks.forEach(track => tryTrack(view, track, idsNotFound))
}),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
import CloseIcon from '@mui/icons-material/Close'

// locals
import RefNameAutocomplete from './RefNameAutocomplete'
import { fetchResults, splitLast } from './util'
import { LinearGenomeViewModel } from '..'
import { handleSelectedRegion, navToOption } from '../../searchUtils'
import ImportFormRefNameAutocomplete from './ImportFormRefNameAutocomplete'

const useStyles = makeStyles()(theme => ({
importFormContainer: {
Expand All @@ -41,11 +41,10 @@ const LinearGenomeViewImportForm = observer(function ({
}) {
const { classes } = useStyles()
const session = getSession(model)
const { assemblyNames, assemblyManager, textSearchManager } = session
const { rankSearchResults, error } = model
const { assemblyNames, assemblyManager } = session
const { error } = model
const [selectedAsm, setSelectedAsm] = useState(assemblyNames[0])
const [option, setOption] = useState<BaseResult>()
const searchScope = model.searchScope(selectedAsm)
const assembly = assemblyManager.get(selectedAsm)
const assemblyError = assemblyNames.length
? assembly?.error
Expand All @@ -65,61 +64,6 @@ const LinearGenomeViewImportForm = observer(function ({
setValue(r0)
}, [r0, selectedAsm])

async function navToOption(option: BaseResult) {
const location = option.getLocation()
const trackId = option.getTrackId()
if (location) {
await model.navToLocString(location, selectedAsm)
if (trackId) {
model.showTrack(trackId)
}
}
}

// gets a string as input, or use stored option results from previous query,
// then re-query and
// 1) if it has multiple results: pop a dialog
// 2) if it's a single result navigate to it
// 3) else assume it's a locstring and navigate to it
async function handleSelectedRegion(input: string) {
try {
if (option?.getDisplayString() === input && option.hasLocation()) {
await navToOption(option)
} else if (option?.results?.length) {
model.setSearchResults(option.results, option.getLabel(), selectedAsm)
} else {
const [ref, rest] = splitLast(input, ':')
const allRefs = assembly?.allRefNamesWithLowerCase || []
if (
allRefs.includes(input) ||
(allRefs.includes(ref) && !Number.isNaN(Number.parseInt(rest, 10)))
) {
await model.navToLocString(input, selectedAsm)
} else {
const results = await fetchResults({
queryString: input,
searchType: 'exact',
searchScope,
rankSearchResults,
textSearchManager,
assembly,
})

if (results.length > 1) {
model.setSearchResults(results, input.toLowerCase(), selectedAsm)
} else if (results.length === 1) {
await navToOption(results[0])
} else {
await model.navToLocString(input, selectedAsm)
}
}
}
} catch (e) {
console.error(e)
session.notify(`${e}`, 'warning')
}
}

// implementation notes:
// having this wrapped in a form allows intuitive use of enter key to submit
return (
Expand All @@ -132,7 +76,29 @@ const LinearGenomeViewImportForm = observer(function ({
model.setError(undefined)
if (value) {
// has it's own error handling
await handleSelectedRegion(value)
try {
if (
option?.getDisplayString() === value &&
option.hasLocation()
) {
await navToOption({
option,
model,
assemblyName: selectedAsm,
})
} else if (option?.results?.length) {
model.setSearchResults(
option.results,
option.getLabel(),
selectedAsm,
)
} else if (assembly) {
await handleSelectedRegion({ input: value, assembly, model })
}
} catch (e) {
console.error(e)
session.notify(`${e}`, 'warning')
}
}
}}
>
Expand All @@ -158,27 +124,12 @@ const LinearGenomeViewImportForm = observer(function ({
<CloseIcon style={{ color: 'red' }} />
) : assemblyLoaded ? (
<FormControl>
<RefNameAutocomplete
fetchResults={queryString =>
fetchResults({
queryString,
assembly,
textSearchManager,
rankSearchResults,
searchScope,
})
}
model={model}
assemblyName={selectedAsm}
<ImportFormRefNameAutocomplete
value={value}
minWidth={270}
onChange={str => setValue(str)}
onSelect={val => setOption(val)}
TextFieldProps={{
variant: 'outlined',
helperText:
'Enter sequence name, feature name, or location',
}}
setValue={setValue}
selectedAsm={selectedAsm}
setOption={setOption}
model={model}
/>
</FormControl>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react'
import { observer } from 'mobx-react'
import { getSession } from '@jbrowse/core/util'
import BaseResult from '@jbrowse/core/TextSearch/BaseResults'

// locals
import RefNameAutocomplete from './RefNameAutocomplete'
import { fetchResults } from './util'
import { LinearGenomeViewModel } from '..'

type LGV = LinearGenomeViewModel

const ImportFormRefNameAutocomplete = observer(function ({
model,
selectedAsm,
value,
setValue,
setOption,
}: {
value: string
setValue: (arg: string) => void
model: LGV
selectedAsm: string
setOption: (arg: BaseResult) => void
}) {
const session = getSession(model)
const { assemblyManager, textSearchManager } = session
const { rankSearchResults } = model
const searchScope = model.searchScope(selectedAsm)
const assembly = assemblyManager.get(selectedAsm)
return (
<RefNameAutocomplete
fetchResults={queryString =>
fetchResults({
queryString,
assembly,
textSearchManager,
rankSearchResults,
searchScope,
})
}
model={model}
assemblyName={selectedAsm}
value={value}
minWidth={270}
onChange={str => setValue(str)}
onSelect={val => setOption(val)}
TextFieldProps={{
variant: 'outlined',
helperText: 'Enter sequence name, feature name, or location',
}}
/>
)
})

export default ImportFormRefNameAutocomplete
22 changes: 22 additions & 0 deletions plugins/linear-genome-view/src/LinearGenomeView/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import MenuOpenIcon from '@mui/icons-material/MenuOpen'
import MiniControls from './components/MiniControls'
import Header from './components/Header'
import { generateLocations, parseLocStrings } from './util'
import { Assembly } from '@jbrowse/core/assemblyManager/assembly'
import { handleSelectedRegion } from '../searchUtils'
// lazies
const ReturnToImportFormDialog = lazy(
() => import('@jbrowse/core/ui/ReturnToImportFormDialog'),
Expand Down Expand Up @@ -1315,6 +1317,26 @@ export function stateModelFactory(pluginManager: PluginManager) {
)
},

/**
* #action
* Performs a text index search, and navigates to it immediately if a
* single result is returned. Will pop up a search dialog if multiple
* results are returned
*/
async navToSearchString({
input,
assembly,
}: {
input: string
assembly: Assembly
}) {
await handleSelectedRegion({
input,
assembly,
model: self as LinearGenomeViewModel,
})
},

/**
* #action
* Similar to `navToLocString`, but accepts parsed location objects
Expand Down
55 changes: 25 additions & 30 deletions plugins/linear-genome-view/src/searchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,38 +41,33 @@ export async function handleSelectedRegion({
model: LinearGenomeViewModel
assembly: Assembly
}) {
try {
const allRefs = assembly?.allRefNamesWithLowerCase || []
const assemblyName = assembly.name
if (input.split(' ').every(entry => checkRef(entry, allRefs))) {
await model.navToLocString(input, assembly.name)
} else {
const searchScope = model.searchScope(assemblyName)
const { textSearchManager } = getSession(model)
const results = await fetchResults({
queryString: input,
searchType: 'exact',
searchScope,
rankSearchResults: model.rankSearchResults,
textSearchManager,
assembly,
})
const allRefs = assembly?.allRefNamesWithLowerCase || []
const assemblyName = assembly.name
if (input.split(' ').every(entry => checkRef(entry, allRefs))) {
await model.navToLocString(input, assembly.name)
} else {
const searchScope = model.searchScope(assemblyName)
const { textSearchManager } = getSession(model)
const results = await fetchResults({
queryString: input,
searchType: 'exact',
searchScope,
rankSearchResults: model.rankSearchResults,
textSearchManager,
assembly,
})

if (results.length > 1) {
model.setSearchResults(results, input.toLowerCase(), assemblyName)
} else if (results.length === 1) {
await navToOption({
option: results[0],
model,
assemblyName,
})
} else {
await model.navToLocString(input, assemblyName)
}
if (results.length > 1) {
model.setSearchResults(results, input.toLowerCase(), assemblyName)
} else if (results.length === 1) {
await navToOption({
option: results[0],
model,
assemblyName,
})
} else {
await model.navToLocString(input, assemblyName)
}
} catch (e) {
console.error(e)
getSession(model).notify(`${e}`, 'warning')
}
}

Expand Down
8 changes: 4 additions & 4 deletions products/jbrowse-web/src/tests/JBrowse.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jest.mock('../makeWorkerInstance', () => () => {})

setup()

const delay = { timeout: 15000 }
const delay = { timeout: 30000 }

beforeEach(() => {
doBeforeEach()
Expand Down Expand Up @@ -70,7 +70,7 @@ test('assembly aliases', async () => {
await findByTestId(hts('volvox_filtered_vcf_assembly_alias'), {}, delay),
)
await findByTestId('box-test-vcf-604453', {}, delay)
}, 15000)
}, 30000)

test('nclist track test with long name', async () => {
const { view, findByTestId, findByText } = await createView()
Expand Down Expand Up @@ -98,7 +98,7 @@ test('test sharing', async () => {
expect(
((await findByLabelText('URL', {}, delay)) as HTMLInputElement).value,
).toBe('http://localhost/?session=share-abc&password=123')
}, 15000)
}, 30000)

test('looks at about this track dialog', async () => {
const { findByTestId, findAllByText, findByText } = await createView()
Expand All @@ -108,4 +108,4 @@ test('looks at about this track dialog', async () => {
fireEvent.click(await findByTestId('track_menu_icon', {}, delay))
fireEvent.click(await findByText('About track'))
await findAllByText(/SQ/, {}, delay)
}, 15000)
}, 30000)
Loading