From 61c60415bf13cd404e1583e96283deebaebebaa9 Mon Sep 17 00:00:00 2001 From: Colin Diesh Date: Tue, 24 Aug 2021 21:09:35 -0400 Subject: [PATCH] Import bookmarks functionality for grid bookmark widget (#2243) --- .../components/AssemblySelector.tsx | 18 +-- .../components/ClearBookmarks.tsx | 50 +++--- .../components/DeleteBookmark.tsx | 62 ++++--- .../components/DownloadBookmarks.tsx | 72 +++++---- .../components/GridBookmarkWidget.test.js | 4 +- .../components/GridBookmarkWidget.tsx | 90 ++++++----- .../components/ImportBookmarks.tsx | 151 ++++++++++++++++++ .../src/GridBookmarkWidget/model.ts | 72 ++++----- .../src/GridBookmarkWidget/utils.ts | 2 +- 9 files changed, 350 insertions(+), 171 deletions(-) create mode 100644 plugins/grid-bookmark/src/GridBookmarkWidget/components/ImportBookmarks.tsx diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/components/AssemblySelector.tsx b/plugins/grid-bookmark/src/GridBookmarkWidget/components/AssemblySelector.tsx index 87ed451e1b..c7d8a28c35 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/components/AssemblySelector.tsx +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/components/AssemblySelector.tsx @@ -31,10 +31,6 @@ function AssemblySelector({ model }: { model: GridBookmarkModel }) { const { assemblies, selectedAssembly, setSelectedAssembly } = model const noAssemblies = assemblies.length === 0 ? true : false - const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { - setSelectedAssembly(event.target.value as string) - } - const determineCurrentValue = (selectedAssembly: string) => { if (selectedAssembly === 'all') { return 'all' @@ -53,7 +49,7 @@ function AssemblySelector({ model }: { model: GridBookmarkModel }) { diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/components/ClearBookmarks.tsx b/plugins/grid-bookmark/src/GridBookmarkWidget/components/ClearBookmarks.tsx index f659cf9541..a739efee85 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/components/ClearBookmarks.tsx +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/components/ClearBookmarks.tsx @@ -6,6 +6,8 @@ import { IconButton, Dialog, DialogTitle, + DialogContent, + DialogActions, Typography, makeStyles, } from '@material-ui/core' @@ -38,10 +40,11 @@ function ClearBookmarks({ model }: { model: GridBookmarkModel }) { aria-label="clear bookmarks" onClick={() => setDialogOpen(true)} > - Clear bookmarks + Clear setDialogOpen(false)}> + Clear bookmarks -
- <> - Clear all bookmarks? -
-
- -
- -
+ + + Clear all bookmarks? Note this will clear bookmarks for all + assemblies + + + + + +
) diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/components/DeleteBookmark.tsx b/plugins/grid-bookmark/src/GridBookmarkWidget/components/DeleteBookmark.tsx index 125a50d895..8aa223f5e9 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/components/DeleteBookmark.tsx +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/components/DeleteBookmark.tsx @@ -1,11 +1,15 @@ import React from 'react' import { observer } from 'mobx-react' +import { assembleLocString } from '@jbrowse/core/util' + import { IconButton, Button, Dialog, DialogTitle, + DialogContent, + DialogActions, Typography, makeStyles, } from '@material-ui/core' @@ -25,11 +29,11 @@ const useStyles = makeStyles(() => ({ })) function DeleteBookmarkDialog({ - locString, + rowNumber, model, onClose, }: { - locString: string | undefined + rowNumber: number | undefined model: GridBookmarkModel onClose: () => void }) { @@ -38,8 +42,9 @@ function DeleteBookmarkDialog({ const { removeBookmark } = model return ( - + + Delete bookmark -
+ - Remove {locString}? + Remove{' '} + + {rowNumber !== undefined + ? assembleLocString(model.bookmarkedRegions[rowNumber]) + : ''} + {' '} + ? -
-
- -
-
+ + + + + +
) } diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/components/DownloadBookmarks.tsx b/plugins/grid-bookmark/src/GridBookmarkWidget/components/DownloadBookmarks.tsx index 992166bc3a..0d6e5c7cc7 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/components/DownloadBookmarks.tsx +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/components/DownloadBookmarks.tsx @@ -6,8 +6,11 @@ import { Button, Dialog, DialogTitle, - Select, + DialogContent, + DialogActions, MenuItem, + Select, + Typography, makeStyles, } from '@material-ui/core' import CloseIcon from '@material-ui/icons/Close' @@ -39,11 +42,6 @@ function DownloadBookmarks({ model }: { model: GridBookmarkModel }) { const classes = useStyles() const [dialogOpen, setDialogOpen] = useState(false) const [fileType, setFileType] = useState('BED') - - const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { - setFileType(event.target.value as string) - } - const { bookmarkedRegions } = model return ( @@ -61,34 +59,40 @@ function DownloadBookmarks({ model }: { model: GridBookmarkModel }) { -
- <> -
- - -
- -
+ + Format to download + + + + + +
) diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.test.js b/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.test.js index 7d29c036ad..a58a91ef7e 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.test.js +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.test.js @@ -53,7 +53,7 @@ describe('', () => { const { findByText } = render() - expect(findByText('chr1:1-12')).toBeTruthy() + expect(await findByText('chr1:2..12')).toBeTruthy() }) it('deletes individual bookmarks correctly', async () => { @@ -84,7 +84,7 @@ describe('', () => { const { findByText } = render() - fireEvent.click(await findByText('Clear bookmarks')) + fireEvent.click(await findByText('Clear')) fireEvent.click(await findByText('Confirm')) expect(await findByText('No rows')).toBeTruthy() diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.tsx b/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.tsx index cf5e88ed68..8b501b5b0d 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.tsx +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/components/GridBookmarkWidget.tsx @@ -1,65 +1,74 @@ import React, { useState } from 'react' import { observer } from 'mobx-react' - -import { Link, IconButton, Typography, Button } from '@material-ui/core' +import { + Link, + IconButton, + Typography, + Button, + makeStyles, +} from '@material-ui/core' import { DataGrid, GridCellParams } from '@material-ui/data-grid' - import { getSession, assembleLocString, measureText } from '@jbrowse/core/util' - -import { GridBookmarkModel } from '../model' -import { navToBookmark } from '../utils' - import DeleteIcon from '@material-ui/icons/Delete' import ViewCompactIcon from '@material-ui/icons/ViewCompact' + import AssemblySelector from './AssemblySelector' import DeleteBookmarkDialog from './DeleteBookmark' import DownloadBookmarks from './DownloadBookmarks' +import ImportBookmarks from './ImportBookmarks' import ClearBookmarks from './ClearBookmarks' +import { GridBookmarkModel } from '../model' +import { navToBookmark } from '../utils' + +const useStyles = makeStyles(() => ({ + link: { + cursor: 'pointer', + }, +})) -// creates a coarse measurement of column width, similar to code in BaseFeatureDetails +// creates a coarse measurement of column width, similar to code in +// BaseFeatureDetails // eslint-disable-next-line @typescript-eslint/no-explicit-any const measure = (row: any, col: string) => Math.min(Math.max(measureText(String(row[col]), 14) + 20, 80), 1000) const BookmarkGrid = observer( ({ compact, model }: { model: GridBookmarkModel; compact: boolean }) => { - const [dialogOpen, setDialogOpen] = useState() - const { bookmarkedRegions, updateBookmarkLabel, selectedAssembly } = model + const classes = useStyles() + const [dialogRowNumber, setDialogRowNumber] = useState() + const { bookmarkedRegions, selectedAssembly } = model const { views } = getSession(model) const bookmarkRows = bookmarkedRegions .toJS() - .map(region => { - const { assemblyName, ...rest } = region - const displayedId = assembleLocString( - selectedAssembly === 'all' ? region : rest, - ) - const id = assembleLocString(region) - return { - ...region, - displayedId, - id, - delete: id, - } - }) .filter( region => selectedAssembly === 'all' || region.assemblyName === selectedAssembly, ) + .map((region, index) => { + const { assemblyName, ...rest } = region + return { + ...region, + id: index, + delete: index, + locString: assembleLocString( + selectedAssembly === 'all' ? region : rest, + ), + } + }) const columns = [ { - field: 'displayedId', + field: 'locString', headerName: 'bookmark link', - width: Math.max(...bookmarkRows.map(row => measure(row, 'id'))), + width: Math.max(...bookmarkRows.map(row => measure(row, 'locString'))), renderCell: (params: GridCellParams) => { const { value } = params return ( { - navToBookmark(value as string, views, model) - }} + className={classes.link} + onClick={() => navToBookmark(value as string, views, model)} > {value} @@ -83,8 +92,11 @@ const BookmarkGrid = observer( setDialogOpen(value as string)} - style={{ padding: 0 }} + onClick={() => { + if (value !== null && value !== undefined) { + setDialogRowNumber(+value) + } + }} > @@ -102,21 +114,15 @@ const BookmarkGrid = observer( columns={columns} onCellEditCommit={args => { const { value, id } = args - bookmarkRows.forEach(row => { - if (row.id === id) { - updateBookmarkLabel(id, value as string) - } - }) + model.updateBookmarkLabel(id as number, value as string) }} disableSelectionOnClick /> { - setDialogOpen(undefined) - }} + onClose={() => setDialogRowNumber(undefined)} /> ) @@ -124,18 +130,18 @@ const BookmarkGrid = observer( ) function GridBookmarkWidget({ model }: { model: GridBookmarkModel }) { + const { selectedAssembly } = model const [compact, setCompact] = useState(false) return ( <> + diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/components/ImportBookmarks.tsx b/plugins/grid-bookmark/src/GridBookmarkWidget/components/ImportBookmarks.tsx new file mode 100644 index 0000000000..d045e8e9bf --- /dev/null +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/components/ImportBookmarks.tsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react' +import { observer } from 'mobx-react' +import { getSession } from '@jbrowse/core/util' +import AssemblySelector from '@jbrowse/core/ui/AssemblySelector' +import { FileLocation } from '@jbrowse/core/util/types' +import { FileSelector } from '@jbrowse/core/ui' +import { openLocation } from '@jbrowse/core/util/io' +import { + IconButton, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + makeStyles, + Typography, +} from '@material-ui/core' +import CloseIcon from '@material-ui/icons/Close' +import ImportIcon from '@material-ui/icons/Publish' +import { GridBookmarkModel } from '../model' + +const useStyles = makeStyles(() => ({ + closeDialog: { + position: 'absolute', + right: 0, + top: 0, + }, + dialogContainer: { + margin: 15, + }, + flexItem: { + margin: 5, + }, +})) + +function ImportBookmarks({ + model, + assemblyName, +}: { + model: GridBookmarkModel + assemblyName: string +}) { + const classes = useStyles() + const session = getSession(model) + const { assemblyNames } = session + const [dialogOpen, setDialogOpen] = useState(false) + const [location, setLocation] = useState() + const [error, setError] = useState() + const [selectedAsm, setSelectedAsm] = useState( + assemblyName || assemblyNames[0], + ) + + return ( + <> + + setDialogOpen(false)} + maxWidth="xl" + > + + Import bookmarks + setDialogOpen(false)} + > + + + + + + Choose a BED format file to import. The first 4 columns will be used + + + + Select assembly that your data belongs to + setSelectedAsm(val)} + session={session} + selected={selectedAsm} + /> + {error ? ( + {`${error}`} + ) : null} + + + + + + + + ) +} + +export default observer(ImportBookmarks) diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/model.ts b/plugins/grid-bookmark/src/GridBookmarkWidget/model.ts index c5c9cb5e14..380a2b6412 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/model.ts +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/model.ts @@ -1,15 +1,18 @@ -import { types, Instance } from 'mobx-state-tree' - +import { types, cast, Instance } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' - -import { assembleLocString } from '@jbrowse/core/util' import { Region } from '@jbrowse/core/util/types' import { Region as RegionModel, ElementId } from '@jbrowse/core/util/types/mst' -const LabeledRegionModel = types.compose( - RegionModel, - types.model('Label', { label: '' }), -) +const LabeledRegionModel = types + .compose( + RegionModel, + types.model('Label', { label: types.optional(types.string, '') }), + ) + .actions(self => ({ + setLabel(label: string) { + self.label = label + }, + })) export default function f(pluginManager: PluginManager) { return types @@ -20,52 +23,41 @@ export default function f(pluginManager: PluginManager) { pluginManager.pluggableMstType('view', 'stateModel'), ), bookmarkedRegions: types.array(LabeledRegionModel), - selectedAssembly: '', + modelSelectedAssembly: '', }) .actions(self => ({ + importBookmarks(regions: Region[]) { + self.bookmarkedRegions = cast(self.bookmarkedRegions.concat(regions)) + }, addBookmark(region: Region) { - const regionLocString = assembleLocString(region) - const index = self.bookmarkedRegions.findIndex(b => { - const bLocString = assembleLocString(b) - return bLocString === regionLocString - }) - if (index === -1) { - self.bookmarkedRegions.push(region) - this.setSelectedAssembly(region.assemblyName) - } + self.bookmarkedRegions.push(region) }, - removeBookmark(locString: string) { - const index = self.bookmarkedRegions.findIndex(b => { - const bLocString = assembleLocString(b) - return bLocString === locString - }) - if (index !== -1) { - self.bookmarkedRegions.splice(index, 1) - } + removeBookmark(index: number) { + self.bookmarkedRegions.splice(index, 1) }, clearAllBookmarks() { self.bookmarkedRegions.clear() }, - updateBookmarkLabel(locString: string, label: string) { - const index = self.bookmarkedRegions.findIndex(b => { - const bLocString = assembleLocString(b) - return bLocString === locString - }) - if (index !== -1) { - self.bookmarkedRegions[index].label = label - } + updateBookmarkLabel(index: number, label: string) { + self.bookmarkedRegions[index]?.setLabel(label) }, setSelectedAssembly(assembly: string) { - self.selectedAssembly = assembly + self.modelSelectedAssembly = assembly }, })) .views(self => ({ - get assemblies() { - const assemblies = self.bookmarkedRegions.map( - region => region.assemblyName, + get selectedAssembly() { + return ( + self.modelSelectedAssembly || + (self.bookmarkedRegions.length + ? self.bookmarkedRegions[0].assemblyName + : '') ) - const uniqueAssemblies = Array.from(new Set(assemblies)) - return uniqueAssemblies + }, + get assemblies() { + return [ + ...new Set(self.bookmarkedRegions.map(region => region.assemblyName)), + ] }, })) } diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/utils.ts b/plugins/grid-bookmark/src/GridBookmarkWidget/utils.ts index c39e6748a6..90b411686f 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/utils.ts +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/utils.ts @@ -77,7 +77,7 @@ export function downloadBookmarkFile( const fileContents = bookmarkedRegions .map(b => { const { label } = b - const labelVal = label === '' ? 'NA' : label + const labelVal = label === '' ? '.' : label const locString = assembleLocString(b) if (fileFormat === 'BED') {