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

Fix crash in "Open session" widget for sessions that have 'track-less views' #4043

Merged
merged 2 commits into from
Nov 6, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'
import { List, ListSubheader, Paper } from '@mui/material'
import { makeStyles } from 'tss-react/mui'

import { observer } from 'mobx-react'

// icons
import { SessionModel, SessionSnap } from './util'
import SessionListItem from './SessionListItem'

const useStyles = makeStyles()(theme => ({
root: {
margin: theme.spacing(1),
},
}))

const AutosaveSessionsList = observer(function ({
session,
}: {
session: SessionModel
}) {
const { classes } = useStyles()
const autosavedSession = JSON.parse(
localStorage.getItem(session.previousAutosaveId) || '{}',
).session as SessionSnap

return autosavedSession ? (
<Paper className={classes.root}>
<List subheader={<ListSubheader>Previous autosaved entry</ListSubheader>}>
<SessionListItem
session={session}
sessionSnapshot={autosavedSession}
onClick={() => session.loadAutosaveSession()}
/>
</List>
</Paper>
) : null
})

export default AutosaveSessionsList
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@mui/material'
import { Dialog } from '@jbrowse/core/ui'

export default function DeleteDialog({
export default function DeleteSavedSessionDialog({
open,
sessionNameToDelete,
handleClose,
Expand All @@ -17,16 +17,9 @@ export default function DeleteDialog({
handleClose: (arg?: boolean) => void
}) {
return (
<Dialog
open={open}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
title={`Delete session "${sessionNameToDelete}"?`}
>
<Dialog open={open} title={`Delete session "${sessionNameToDelete}"?`}>
<DialogContent>
<DialogContentText id="alert-dialog-description">
This action cannot be undone
</DialogContentText>
<DialogContentText>This action cannot be undone</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => handleClose()} color="primary">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useState } from 'react'
import {
IconButton,
List,
ListSubheader,
Paper,
Typography,
} from '@mui/material'
import { makeStyles } from 'tss-react/mui'

import { observer } from 'mobx-react'

// icons
import DeleteIcon from '@mui/icons-material/Delete'

// locals
import { SessionModel } from './util'
import DeleteSavedSessionDialog from './DeleteSavedSessionDialog'
import SessionListItem from './SessionListItem'

const useStyles = makeStyles()(theme => ({
root: {
margin: theme.spacing(1),
},
message: {
padding: theme.spacing(3),
},
}))

const RegularSavedSessionsList = observer(function ({
session,
}: {
session: SessionModel
}) {
const { classes } = useStyles()
const [sessionIndexToDelete, setSessionIndexToDelete] = useState<number>()

function handleDialogClose(deleteSession = false) {
if (deleteSession && sessionIndexToDelete !== undefined) {
session.removeSavedSession(session.savedSessions[sessionIndexToDelete])
}
setSessionIndexToDelete(undefined)
}

const sessionNameToDelete =
sessionIndexToDelete !== undefined
? session.savedSessions[sessionIndexToDelete].name
: ''
return (
<Paper className={classes.root}>
<List subheader={<ListSubheader>Saved sessions</ListSubheader>}>
{session.savedSessions.length ? (
session.savedSessions.map((sessionSnapshot, idx) => (
<SessionListItem
onClick={() => session.activateSession(sessionSnapshot.name)}
sessionSnapshot={sessionSnapshot}
session={session}
key={sessionSnapshot.name}
secondaryAction={
<IconButton
edge="end"
disabled={session.name === sessionSnapshot.name}
onClick={() => setSessionIndexToDelete(idx)}
>
<DeleteIcon />
</IconButton>
}
/>
))
) : (
<Typography className={classes.message}>
No saved sessions found
</Typography>
)}
</List>
{sessionNameToDelete ? (
<React.Suspense fallback={null}>
<DeleteSavedSessionDialog
open
sessionNameToDelete={sessionNameToDelete}
handleClose={handleDialogClose}
/>
</React.Suspense>
) : null}
</Paper>
)
})

export default RegularSavedSessionsList
56 changes: 56 additions & 0 deletions plugins/menus/src/SessionManager/components/SessionListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react'
import {
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
} from '@mui/material'

import { observer } from 'mobx-react'
import pluralize from 'pluralize'

// icons
import ViewListIcon from '@mui/icons-material/ViewList'
import { AbstractSessionModel, sum } from '@jbrowse/core/util'

// locals
import { SessionSnap } from './util'

const SessionListItem = observer(function ({
session,
sessionSnapshot,
onClick,
secondaryAction,
}: {
sessionSnapshot: SessionSnap
session: AbstractSessionModel
onClick: () => void
secondaryAction?: React.ReactNode
}) {
const { views = [] } = sessionSnapshot || {}
const totalTracks = sum(views.map(view => view.tracks?.length ?? 0))
const n = views.length

return (
<ListItem secondaryAction={secondaryAction}>
<ListItemButton onClick={onClick}>
<ListItemIcon>
<ViewListIcon />
</ListItemIcon>
<ListItemText
primary={sessionSnapshot.name}
secondary={
session.name === sessionSnapshot.name
? 'Currently open'
: `${n} ${pluralize('view', n)}; ${totalTracks} open ${pluralize(
'track',
totalTracks,
)}`
}
/>
</ListItemButton>
</ListItem>
)
})

export default SessionListItem
168 changes: 12 additions & 156 deletions plugins/menus/src/SessionManager/components/SessionManager.tsx
Original file line number Diff line number Diff line change
@@ -1,164 +1,20 @@
import React, { useState } from 'react'
import {
IconButton,
List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
ListSubheader,
Paper,
Typography,
} from '@mui/material'
import { makeStyles } from 'tss-react/mui'

import React from 'react'
import { observer } from 'mobx-react'
import pluralize from 'pluralize'
import { AbstractSessionModel } from '@jbrowse/core/util'

// icons
import DeleteIcon from '@mui/icons-material/Delete'
import ViewListIcon from '@mui/icons-material/ViewList'
import DeleteDialog from './DeleteDialog'

const useStyles = makeStyles()(theme => ({
root: {
margin: theme.spacing(1),
},
message: {
padding: theme.spacing(3),
},
}))

interface SessionSnap {
name: string
views?: { tracks: unknown[] }[]
[key: string]: unknown
}

interface SessionModel extends AbstractSessionModel {
savedSessions: SessionSnap[]
removeSavedSession: (arg: SessionSnap) => void
activateSession: (arg: string) => void
loadAutosaveSession: () => void
previousAutosaveId: string
}

const AutosaveEntry = observer(({ session }: { session: SessionModel }) => {
const { classes } = useStyles()
const autosavedSession = JSON.parse(
localStorage.getItem(session.previousAutosaveId) || '{}',
).session as SessionSnap

const { views = [] } = autosavedSession || {}
const totalTracks = views
.map(view => view.tracks.length)
.reduce((a, b) => a + b, 0)

return autosavedSession ? (
<Paper className={classes.root}>
<List subheader={<ListSubheader>Previous autosaved entry</ListSubheader>}>
<ListItem button onClick={() => session.loadAutosaveSession()}>
<ListItemIcon>
<ViewListIcon />
</ListItemIcon>
<ListItemText
primary={autosavedSession.name}
secondary={
session.name === autosavedSession.name
? 'Currently open'
: `${views.length} ${pluralize(
'view',
views.length,
)}; ${totalTracks}
open ${pluralize('track', totalTracks)}`
}
/>
</ListItem>
</List>
</Paper>
) : null
})

const SessionManager = observer(({ session }: { session: SessionModel }) => {
const { classes } = useStyles()
const [sessionIndexToDelete, setSessionIndexToDelete] = useState<number>()
const [open, setOpen] = useState(false)

function handleDialogClose(deleteSession = false) {
if (deleteSession && sessionIndexToDelete !== undefined) {
session.removeSavedSession(session.savedSessions[sessionIndexToDelete])
}
setSessionIndexToDelete(undefined)
setOpen(false)
}

const sessionNameToDelete =
sessionIndexToDelete !== undefined
? session.savedSessions[sessionIndexToDelete].name
: ''

import { SessionModel } from './util'
import AutosaveSessionsList from './AutosavedSessionsList'
import RegularSavedSessionsList from './RegularSavedSessionsList'

const SessionManager = observer(function ({
session,
}: {
session: SessionModel
}) {
return (
<>
<AutosaveEntry session={session} />
<Paper className={classes.root}>
<List subheader={<ListSubheader>Saved sessions</ListSubheader>}>
{session.savedSessions.length ? (
session.savedSessions.map((sessionSnapshot, idx) => {
const { views = [] } = sessionSnapshot
const totalTracks = views
.map(view => view.tracks.length)
.reduce((a, b) => a + b, 0)
return (
<ListItem
button
disabled={session.name === sessionSnapshot.name}
onClick={() => session.activateSession(sessionSnapshot.name)}
key={sessionSnapshot.name}
>
<ListItemIcon>
<ViewListIcon />
</ListItemIcon>
<ListItemText
primary={sessionSnapshot.name}
secondary={
session.name === sessionSnapshot.name
? 'Currently open'
: `${views.length} ${pluralize(
'view',
views.length,
)}; ${totalTracks}
open ${pluralize('track', totalTracks)}`
}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
disabled={session.name === sessionSnapshot.name}
aria-label="Delete"
onClick={() => {
setSessionIndexToDelete(idx)
setOpen(true)
}}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)
})
) : (
<Typography className={classes.message}>
No saved sessions found
</Typography>
)}
</List>
</Paper>
<DeleteDialog
open={open}
sessionNameToDelete={sessionNameToDelete}
handleClose={handleDialogClose}
/>
<AutosaveSessionsList session={session} />
<RegularSavedSessionsList session={session} />
</>
)
})
Expand Down