Skip to content

Commit

Permalink
Fix crash in "Open session" widget for sessions that have 'track-less…
Browse files Browse the repository at this point in the history
… views' (#4043)
  • Loading branch information
cmdcolin authored Nov 6, 2023
1 parent 1cf7ab8 commit fb97715
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 200 deletions.
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
Loading

0 comments on commit fb97715

Please sign in to comment.