From dda6d42b02bd7dd854c6755da3f648e122a60346 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 8 Oct 2021 12:54:01 -0400 Subject: [PATCH 01/23] Menu item redesign --- plugins/circular-view/src/index.ts | 2 +- plugins/dotplot-view/src/index.ts | 2 +- plugins/linear-comparative-view/src/index.tsx | 2 +- plugins/linear-genome-view/src/index.ts | 2 +- plugins/spreadsheet-view/src/index.ts | 2 +- plugins/sv-inspector/src/index.ts | 2 +- products/jbrowse-desktop/src/rootModel.ts | 5 ----- 7 files changed, 6 insertions(+), 11 deletions(-) diff --git a/plugins/circular-view/src/index.ts b/plugins/circular-view/src/index.ts index 339b8fbe3b..eca517c92b 100644 --- a/plugins/circular-view/src/index.ts +++ b/plugins/circular-view/src/index.ts @@ -24,7 +24,7 @@ export default class CircularViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { + pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { label: 'Circular view', icon: DataUsageIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/dotplot-view/src/index.ts b/plugins/dotplot-view/src/index.ts index 82fb8d12df..973c0d6c59 100644 --- a/plugins/dotplot-view/src/index.ts +++ b/plugins/dotplot-view/src/index.ts @@ -386,7 +386,7 @@ export default class DotplotPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { + pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { label: 'Dotplot view', icon: TimelineIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/linear-comparative-view/src/index.tsx b/plugins/linear-comparative-view/src/index.tsx index ab69568a2e..467b1562d4 100644 --- a/plugins/linear-comparative-view/src/index.tsx +++ b/plugins/linear-comparative-view/src/index.tsx @@ -707,7 +707,7 @@ export default class extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { + pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { label: 'Linear synteny view', icon: CalendarIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/linear-genome-view/src/index.ts b/plugins/linear-genome-view/src/index.ts index b6859f13aa..3fbfa91dd0 100644 --- a/plugins/linear-genome-view/src/index.ts +++ b/plugins/linear-genome-view/src/index.ts @@ -121,7 +121,7 @@ export default class LinearGenomeViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { + pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { label: 'Linear genome view', icon: LineStyleIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/spreadsheet-view/src/index.ts b/plugins/spreadsheet-view/src/index.ts index b18ea852ec..421c8d2ffc 100644 --- a/plugins/spreadsheet-view/src/index.ts +++ b/plugins/spreadsheet-view/src/index.ts @@ -23,7 +23,7 @@ export default class SpreadsheetViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { + pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { label: 'Spreadsheet view', icon: ViewComfyIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/sv-inspector/src/index.ts b/plugins/sv-inspector/src/index.ts index f7494d490c..4bcc6b689d 100644 --- a/plugins/sv-inspector/src/index.ts +++ b/plugins/sv-inspector/src/index.ts @@ -18,7 +18,7 @@ export default class SvInspectorViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { + pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { label: 'SV inspector', icon: TableChartIcon, onClick: (session: AbstractSessionModel) => { diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 1f0601584f..cf56ea4391 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -218,11 +218,6 @@ export default function rootModelFactory(pluginManager: PluginManager) { self.setSession(undefined) }, }, - ], - }, - { - label: 'Edit', - menuItems: [ { label: 'Open assembly manager', icon: SettingsIcon, From c2318f82107be4bcdf9c7a5e4790c6c13c9dffc9 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 13:49:02 -0400 Subject: [PATCH 02/23] Re-organize icons, pending wiring up --- packages/core/ui/Icons.tsx | 24 ++++ plugins/circular-view/src/index.ts | 2 +- plugins/data-management/src/index.ts | 49 +------- plugins/dotplot-view/src/index.ts | 2 +- plugins/linear-comparative-view/src/index.tsx | 24 ++-- plugins/linear-genome-view/src/index.ts | 2 +- plugins/spreadsheet-view/src/index.ts | 2 +- plugins/sv-inspector/src/index.ts | 2 +- products/jbrowse-desktop/src/rootModel.ts | 114 +++++++++++++++--- 9 files changed, 142 insertions(+), 79 deletions(-) diff --git a/packages/core/ui/Icons.tsx b/packages/core/ui/Icons.tsx index bb9f1a97c1..5c75f78932 100644 --- a/packages/core/ui/Icons.tsx +++ b/packages/core/ui/Icons.tsx @@ -34,3 +34,27 @@ export function TrackSelector(props: SvgIconProps) { ) } + +// content-save-edit from https://materialdesignicons.com/ +export function SaveAs(props: SvgIconProps) { + return ( + + + + ) +} + +// content-save from https://materialdesignicons.com/ +export function Save(props: SvgIconProps) { + return ( + + + + ) +} diff --git a/plugins/circular-view/src/index.ts b/plugins/circular-view/src/index.ts index eca517c92b..48bc235eec 100644 --- a/plugins/circular-view/src/index.ts +++ b/plugins/circular-view/src/index.ts @@ -24,7 +24,7 @@ export default class CircularViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { + pluginManager.rootModel.appendToSubMenu(['Add'], { label: 'Circular view', icon: DataUsageIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/data-management/src/index.ts b/plugins/data-management/src/index.ts index e204b99ecf..71f4f1cc87 100644 --- a/plugins/data-management/src/index.ts +++ b/plugins/data-management/src/index.ts @@ -6,7 +6,6 @@ import PluginManager from '@jbrowse/core/PluginManager' import { SessionWithWidgets, isAbstractMenuManager } from '@jbrowse/core/util' import NoteAddIcon from '@material-ui/icons/NoteAdd' import InputIcon from '@material-ui/icons/Input' -import ExtensionIcon from '@material-ui/icons/Extension' import { configSchema as ucscConfigSchema, modelFactory as ucscModelFactory, @@ -105,53 +104,7 @@ export default class extends Plugin { }) } - configure(pluginManager: PluginManager) { - if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToMenu('File', { - label: 'Open track', - icon: NoteAddIcon, - onClick: (session: SessionWithWidgets) => { - if (session.views.length === 0) { - session.notify('Please open a view to add a track first') - } else if (session.views.length >= 1) { - const widget = session.addWidget( - 'AddTrackWidget', - 'addTrackWidget', - { view: session.views[0].id }, - ) - session.showWidget(widget) - if (session.views.length > 1) { - session.notify( - `This will add a track to the first view. Note: if you want to open a track in a specific view open the track selector for that view and use the add track (plus icon) in the bottom right`, - ) - } - } - }, - }) - pluginManager.rootModel.appendToMenu('File', { - label: 'Open connection', - icon: InputIcon, - onClick: (session: SessionWithWidgets) => { - const widget = session.addWidget( - 'AddConnectionWidget', - 'addConnectionWidget', - ) - session.showWidget(widget) - }, - }) - pluginManager.rootModel.appendToMenu('File', { - label: 'Plugin store', - icon: ExtensionIcon, - onClick: (session: SessionWithWidgets) => { - const widget = session.addWidget( - 'PluginStoreWidget', - 'pluginStoreWidget', - ) - session.showWidget(widget) - }, - }) - } - } + configure(pluginManager: PluginManager) {} } export { AssemblyManager, SetDefaultSession } diff --git a/plugins/dotplot-view/src/index.ts b/plugins/dotplot-view/src/index.ts index 973c0d6c59..4c37026cb8 100644 --- a/plugins/dotplot-view/src/index.ts +++ b/plugins/dotplot-view/src/index.ts @@ -386,7 +386,7 @@ export default class DotplotPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { + pluginManager.rootModel.appendToSubMenu(['Add'], { label: 'Dotplot view', icon: TimelineIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/linear-comparative-view/src/index.tsx b/plugins/linear-comparative-view/src/index.tsx index 467b1562d4..c4b4d18df4 100644 --- a/plugins/linear-comparative-view/src/index.tsx +++ b/plugins/linear-comparative-view/src/index.tsx @@ -13,9 +13,7 @@ import { DialogTitle, IconButton, } from '@material-ui/core' -import CloseIcon from '@material-ui/icons/Close' -import AddIcon from '@material-ui/icons/Add' -import CalendarIcon from '@material-ui/icons/CalendarViewDay' + import { ConfigurationSchema, getConf } from '@jbrowse/core/configuration' import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType' import DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType' @@ -34,12 +32,20 @@ import { getContainingTrack, isAbstractMenuManager, } from '@jbrowse/core/util' - import { MismatchParser, LinearPileupDisplayModel, } from '@jbrowse/plugin-alignments' import { getRpcSessionId } from '@jbrowse/core/util/tracks' +import { PluggableElementType } from '@jbrowse/core/pluggableElementTypes' +import ViewType from '@jbrowse/core/pluggableElementTypes/ViewType' + +// icons +import CloseIcon from '@material-ui/icons/Close' +import AddIcon from '@material-ui/icons/Add' +import CalendarIcon from '@material-ui/icons/CalendarViewDay' +// locals +// import { configSchemaFactory as linearComparativeDisplayConfigSchemaFactory, ReactComponent as LinearComparativeDisplayReactComponent, @@ -59,9 +65,6 @@ import { AdapterClass as MCScanAnchorsAdapter, configSchema as MCScanAnchorsConfigSchema, } from './MCScanAnchorsAdapter' -import { PluggableElementType } from '@jbrowse/core/pluggableElementTypes' -import ViewType from '@jbrowse/core/pluggableElementTypes/ViewType' - const { parseCigar } = MismatchParser function getLengthOnRef(cigar: string) { @@ -189,6 +192,9 @@ interface BasicFeature { refName: string } +// hashmap of refName->array of features +type FeaturesPerRef = { [key: string]: BasicFeature[] } + function gatherOverlaps(regions: BasicFeature[]) { const groups = regions.reduce((memo, x) => { if (!memo[x.refName]) { @@ -196,7 +202,7 @@ function gatherOverlaps(regions: BasicFeature[]) { } memo[x.refName].push(x) return memo - }, {} as { [key: string]: BasicFeature[] }) + }, {} as FeaturesPerRef) return Object.values(groups) .map(group => mergeIntervals(group.sort((a, b) => a.start - b.start))) @@ -707,7 +713,7 @@ export default class extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { + pluginManager.rootModel.appendToSubMenu(['Add'], { label: 'Linear synteny view', icon: CalendarIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/linear-genome-view/src/index.ts b/plugins/linear-genome-view/src/index.ts index 3fbfa91dd0..6d9756f89d 100644 --- a/plugins/linear-genome-view/src/index.ts +++ b/plugins/linear-genome-view/src/index.ts @@ -121,7 +121,7 @@ export default class LinearGenomeViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { + pluginManager.rootModel.appendToSubMenu(['Add'], { label: 'Linear genome view', icon: LineStyleIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/spreadsheet-view/src/index.ts b/plugins/spreadsheet-view/src/index.ts index 421c8d2ffc..e9ba2dfa26 100644 --- a/plugins/spreadsheet-view/src/index.ts +++ b/plugins/spreadsheet-view/src/index.ts @@ -23,7 +23,7 @@ export default class SpreadsheetViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { + pluginManager.rootModel.appendToSubMenu(['Add'], { label: 'Spreadsheet view', icon: ViewComfyIcon, onClick: (session: AbstractSessionModel) => { diff --git a/plugins/sv-inspector/src/index.ts b/plugins/sv-inspector/src/index.ts index 4bcc6b689d..0573d9f849 100644 --- a/plugins/sv-inspector/src/index.ts +++ b/plugins/sv-inspector/src/index.ts @@ -18,7 +18,7 @@ export default class SvInspectorViewPlugin extends Plugin { configure(pluginManager: PluginManager) { if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add view'], { + pluginManager.rootModel.appendToSubMenu(['Add'], { label: 'SV inspector', icon: TableChartIcon, onClick: (session: AbstractSessionModel) => { diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index cf56ea4391..0a8bc470b8 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -1,15 +1,3 @@ -import assemblyManagerFactory, { - assemblyConfigSchemas as AssemblyConfigSchemasFactory, -} from '@jbrowse/core/assemblyManager' -import { autorun } from 'mobx' -import PluginManager from '@jbrowse/core/PluginManager' -import RpcManager from '@jbrowse/core/rpc/RpcManager' -import { MenuItem } from '@jbrowse/core/ui' -import SettingsIcon from '@material-ui/icons/Settings' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' -import AppsIcon from '@material-ui/icons/Apps' -import { UriLocation } from '@jbrowse/core/util/types' -import electron from 'electron' import { addDisposer, cast, @@ -20,12 +8,30 @@ import { SnapshotIn, Instance, } from 'mobx-state-tree' + +import { autorun } from 'mobx' + +import assemblyManagerFactory, { + assemblyConfigSchemas as AssemblyConfigSchemasFactory, +} from '@jbrowse/core/assemblyManager' +import PluginManager from '@jbrowse/core/PluginManager' +import RpcManager from '@jbrowse/core/rpc/RpcManager' +import { MenuItem } from '@jbrowse/core/ui' +import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' +import { UriLocation } from '@jbrowse/core/util/types' +import { ipcRenderer } from 'electron' + +// icons +import ExtensionIcon from '@material-ui/icons/Extension' +import AppsIcon from '@material-ui/icons/Apps' +import { Save, SaveAs } from '@jbrowse/core/ui/Icons' + +// locals +import sessionModelFactory from './sessionModelFactory' import JBrowseDesktop from './jbrowseModel' // @ts-ignore import RenderWorker from './rpc.worker' -import sessionModelFactory from './sessionModelFactory' -const { ipcRenderer } = electron interface Menu { label: string menuItems: MenuItem[] @@ -212,21 +218,95 @@ export default function rootModelFactory(pluginManager: PluginManager) { label: 'File', menuItems: [ { - label: 'Return to start screen', + label: 'Open', icon: AppsIcon, onClick: () => { self.setSession(undefined) }, }, { - label: 'Open assembly manager', - icon: SettingsIcon, + label: 'Save', + icon: Save, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick: (session: any) => { + const rootModel = getParent(session) + rootModel.setAssemblyEditing(true) + }, + }, + { + label: 'Save as...', + icon: SaveAs, // eslint-disable-next-line @typescript-eslint/no-explicit-any onClick: (session: any) => { const rootModel = getParent(session) rootModel.setAssemblyEditing(true) }, }, + { + type: 'divider', + }, + { + label: 'Open assembly...', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick: (session: any) => { + const rootModel = getParent(session) + rootModel.setAssemblyEditing(true) + }, + }, + { + label: 'Open track...', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick: (session: any) => { + const rootModel = getParent(session) + rootModel.setAssemblyEditing(true) + }, + }, + { + label: 'Open connection...', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick: (session: any) => { + const rootModel = getParent(session) + rootModel.setAssemblyEditing(true) + }, + }, + { + type: 'divider', + }, + { + label: 'Return to start screen', + icon: AppsIcon, + onClick: () => { + self.setSession(undefined) + }, + }, + { + label: 'Exit', + onClick: () => { + self.setSession(undefined) + }, + }, + ], + }, + { + label: 'Add', + menuItems: [], + }, + { + label: 'Tools', + menuItems: [ + { + label: 'Plugin store', + icon: ExtensionIcon, + onClick: () => { + if (self.session) { + const widget = self.session.addWidget( + 'PluginStoreWidget', + 'pluginStoreWidget', + ) + self.session.showWidget(widget) + } + }, + }, ], }, ] as Menu[], From 5cfaab79168c66ae03ad597141e23e08d18a6469 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 14:17:26 -0400 Subject: [PATCH 03/23] Copy opensequencedialog --- packages/core/ui/Icons.tsx | 11 +++++ .../src/AssemblyManager/AssemblyAddForm.tsx | 43 +++---------------- .../dialogs => }/OpenSequenceDialog.tsx | 0 .../src/StartScreen/LauncherPanel.tsx | 2 +- products/jbrowse-desktop/src/rootModel.ts | 27 ++++++++++-- 5 files changed, 43 insertions(+), 40 deletions(-) rename products/jbrowse-desktop/src/{StartScreen/dialogs => }/OpenSequenceDialog.tsx (100%) diff --git a/packages/core/ui/Icons.tsx b/packages/core/ui/Icons.tsx index 5c75f78932..577971daf3 100644 --- a/packages/core/ui/Icons.tsx +++ b/packages/core/ui/Icons.tsx @@ -58,3 +58,14 @@ export function Save(props: SvgIconProps) { ) } + +export function DNA(props: SvgIconProps) { + return ( + + + + ) +} diff --git a/plugins/data-management/src/AssemblyManager/AssemblyAddForm.tsx b/plugins/data-management/src/AssemblyManager/AssemblyAddForm.tsx index ffc8d10243..91e69c6b1d 100644 --- a/plugins/data-management/src/AssemblyManager/AssemblyAddForm.tsx +++ b/plugins/data-management/src/AssemblyManager/AssemblyAddForm.tsx @@ -149,6 +149,8 @@ const AdapterInput = observer( }, ) +const blank = { uri: '' } as FileLocation + const AssemblyAddForm = observer( ({ rootModel, @@ -169,48 +171,17 @@ const AssemblyAddForm = observer( const [assemblyName, setAssemblyName] = useState('') const [assemblyDisplayName, setAssemblyDisplayName] = useState('') const [adapterSelection, setAdapterSelection] = useState(adapterTypes[0]) - const [fastaLocation, setFastaLocation] = useState<{ - uri: string - locationType: 'UriLocation' - }>({ - uri: '', - locationType: 'UriLocation', - }) - const [faiLocation, setFaiLocation] = useState<{ - uri: string - locationType: 'UriLocation' - }>({ - uri: '', - locationType: 'UriLocation', - }) - const [gziLocation, setGziLocation] = useState<{ - uri: string - locationType: 'UriLocation' - }>({ - uri: '', - locationType: 'UriLocation', - }) - const [twoBitLocation, setTwoBitLocation] = useState<{ - uri: string - locationType: 'UriLocation' - }>({ - uri: '', - locationType: 'UriLocation', - }) - const [chromSizesLocation, setChromSizesLocation] = useState<{ - uri: string - locationType: 'UriLocation' - }>({ - uri: '', - locationType: 'UriLocation', - }) + const [fastaLocation, setFastaLocation] = useState(blank) + const [faiLocation, setFaiLocation] = useState(blank) + const [gziLocation, setGziLocation] = useState(blank) + const [twoBitLocation, setTwoBitLocation] = useState(blank) + const [chromSizesLocation, setChromSizesLocation] = useState(blank) function createAssembly() { if (assemblyName === '') { rootModel.session.notify("Can't create an assembly without a name") } else { setFormOpen(false) - // setIsAssemblyBeingEdited(true) let newAssembly if (adapterSelection === 'IndexedFastaAdapter') { newAssembly = { diff --git a/products/jbrowse-desktop/src/StartScreen/dialogs/OpenSequenceDialog.tsx b/products/jbrowse-desktop/src/OpenSequenceDialog.tsx similarity index 100% rename from products/jbrowse-desktop/src/StartScreen/dialogs/OpenSequenceDialog.tsx rename to products/jbrowse-desktop/src/OpenSequenceDialog.tsx diff --git a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx index 78d4cc4403..bca5d788e7 100644 --- a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { Button, makeStyles } from '@material-ui/core' import PluginManager from '@jbrowse/core/PluginManager' import PreloadedDatasetSelector from './PreloadedDatasetSelector' -import OpenSequenceDialog from './dialogs/OpenSequenceDialog' +import OpenSequenceDialog from '../OpenSequenceDialog' const useStyles = makeStyles(theme => ({ form: { diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 0a8bc470b8..c704b47fe0 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -24,11 +24,15 @@ import { ipcRenderer } from 'electron' // icons import ExtensionIcon from '@material-ui/icons/Extension' import AppsIcon from '@material-ui/icons/Apps' -import { Save, SaveAs } from '@jbrowse/core/ui/Icons' +import PowerIcon from '@material-ui/icons/Power' +import StorageIcon from '@material-ui/icons/Storage' +import MeetingRoomIcon from '@material-ui/icons/MeetingRoom' +import { Save, SaveAs, DNA } from '@jbrowse/core/ui/Icons' // locals import sessionModelFactory from './sessionModelFactory' import JBrowseDesktop from './jbrowseModel' +import OpenSequenceDialog from './OpenSequenceDialog' // @ts-ignore import RenderWorker from './rpc.worker' @@ -247,6 +251,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { }, { label: 'Open assembly...', + icon: DNA, // eslint-disable-next-line @typescript-eslint/no-explicit-any onClick: (session: any) => { const rootModel = getParent(session) @@ -255,14 +260,29 @@ export default function rootModelFactory(pluginManager: PluginManager) { }, { label: 'Open track...', + icon: StorageIcon, // eslint-disable-next-line @typescript-eslint/no-explicit-any onClick: (session: any) => { - const rootModel = getParent(session) - rootModel.setAssemblyEditing(true) + if (session.views.length === 0) { + session.notify('Please open a view to add a track first') + } else if (session.views.length >= 1) { + const widget = session.addWidget( + 'AddTrackWidget', + 'addTrackWidget', + { view: session.views[0].id }, + ) + session.showWidget(widget) + if (session.views.length > 1) { + session.notify( + `This will add a track to the first view. Note: if you want to open a track in a specific view open the track selector for that view and use the add track (plus icon) in the bottom right`, + ) + } + } }, }, { label: 'Open connection...', + icon: PowerIcon, // eslint-disable-next-line @typescript-eslint/no-explicit-any onClick: (session: any) => { const rootModel = getParent(session) @@ -281,6 +301,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { }, { label: 'Exit', + icon: MeetingRoomIcon, onClick: () => { self.setSession(undefined) }, From cddd1dfb7f111d35c3289da5f0c22e66b3834419 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 15:11:24 -0400 Subject: [PATCH 04/23] Sessions anywhere on filesystem --- products/jbrowse-desktop/public/electron.ts | 46 ++++++------- .../src/OpenSequenceDialog.tsx | 55 ++++----------- .../src/StartScreen/LauncherPanel.tsx | 17 ++++- products/jbrowse-desktop/src/rootModel.ts | 68 ++++++++++++------- 4 files changed, 97 insertions(+), 89 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index eda641bc84..060df58122 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -243,15 +243,11 @@ ipcMain.handle('listSessions', async () => { .map(f => { const base = path.basename(f, '.json') const json = path.join(sessionDir, base + '.json') - const thumb = path.join(sessionDir, base + '.thumbnail') return [ decodeURIComponent(base), { stats: fs.existsSync(json) ? fs.statSync(json) : undefined, - screenshot: fs.existsSync(thumb) - ? fs.readFileSync(thumb, 'utf8') - : undefined, }, ] }), @@ -295,27 +291,34 @@ ipcMain.handle( }) }, ) -ipcMain.handle('saveSession', async (_event: unknown, snap: SessionSnap) => { - const page = await mainWindow?.capturePage() - const name = snap.defaultSession.name - if (page) { - const resizedPage = page.resize({ width: 250 }) - await writeFile(getPath(name, 'thumbnail'), resizedPage.toDataURL()) - } - await writeFile(getPath(name), JSON.stringify(snap, null, 2)) +ipcMain.handle( + 'saveSession', + async (_event: unknown, path: string, snap: SessionSnap) => { + const page = await mainWindow?.capturePage() + await writeFile( + path, + JSON.stringify( + { ...snap, screenshot: page?.resize({ width: 250 }).toDataURL() }, + null, + 2, + ), + ) + }, +) + +ipcMain.handle('promptSessionSaveAs', async (_event: unknown) => { + const toLocalPath = path.join(app.getPath('desktop'), `session.json`) + const choice = await dialog.showSaveDialog({ + defaultPath: toLocalPath, + }) + + return choice.filePath }) ipcMain.handle( 'renameSession', async (_event: unknown, oldName: string, newName: string) => { - try { - await rename(getPath(oldName, 'thumbnail'), getPath(newName, 'thumbnail')) - } catch (e) { - console.error('rename thumbnail failed', e) - } - const snap = JSON.parse(await readFile(getPath(oldName), 'utf8')) - snap.defaultSession.name = newName await unlink(getPath(oldName)) await writeFile(getPath(newName), JSON.stringify(snap, null, 2)) @@ -331,11 +334,6 @@ ipcMain.handle('reset', async () => { ipcMain.handle( 'deleteSession', async (_event: unknown, sessionName: string) => { - try { - await unlink(getPath(sessionName, 'thumbnail')) - } catch (e) { - console.error('delete thumbnail failed', e) - } return unlink(getPath(sessionName)) }, ) diff --git a/products/jbrowse-desktop/src/OpenSequenceDialog.tsx b/products/jbrowse-desktop/src/OpenSequenceDialog.tsx index b1da8d7e7b..e16bc01a1d 100644 --- a/products/jbrowse-desktop/src/OpenSequenceDialog.tsx +++ b/products/jbrowse-desktop/src/OpenSequenceDialog.tsx @@ -14,8 +14,6 @@ import { } from '@material-ui/core' import FileSelector from '@jbrowse/core/ui/FileSelector' import { FileLocation } from '@jbrowse/core/util/types' -import PluginManager from '@jbrowse/core/PluginManager' -import { createPluginManager } from '../util' const useStyles = makeStyles(theme => ({ root: { @@ -150,12 +148,12 @@ function AdapterInput({ return null } +const blank = { uri: '' } as FileLocation + const OpenSequenceDialog = ({ onClose, - setPluginManager, }: { - setPluginManager: (pm: PluginManager) => void - onClose: () => void + onClose: (conf?: unknown) => Promise }) => { const classes = useStyles() @@ -168,26 +166,11 @@ const OpenSequenceDialog = ({ const [assemblyName, setAssemblyName] = useState('') const [assemblyDisplayName, setAssemblyDisplayName] = useState('') const [adapterSelection, setAdapterSelection] = useState(adapterTypes[0]) - const [fastaLocation, setFastaLocation] = useState({ - uri: '', - locationType: 'UriLocation', - }) - const [faiLocation, setFaiLocation] = useState({ - uri: '', - locationType: 'UriLocation', - }) - const [gziLocation, setGziLocation] = useState({ - uri: '', - locationType: 'UriLocation', - }) - const [twoBitLocation, setTwoBitLocation] = useState({ - uri: '', - locationType: 'UriLocation', - }) - const [chromSizesLocation, setChromSizesLocation] = useState({ - uri: '', - locationType: 'UriLocation', - }) + const [fastaLocation, setFastaLocation] = useState(blank) + const [faiLocation, setFaiLocation] = useState(blank) + const [gziLocation, setGziLocation] = useState(blank) + const [twoBitLocation, setTwoBitLocation] = useState(blank) + const [chromSizesLocation, setChromSizesLocation] = useState(blank) function createAssemblyConfig() { if (adapterSelection === 'IndexedFastaAdapter') { @@ -294,23 +277,15 @@ const OpenSequenceDialog = ({ onClick={async () => { try { const assemblyConf = createAssemblyConfig() - const pm = await createPluginManager({ - assemblies: [ - { - ...assemblyConf, - sequence: { - type: 'ReferenceSequenceTrack', - trackId: `${assemblyName}-${Date.now()}`, - ...(assemblyConf.sequence || {}), - }, - }, - ], - defaultSession: { - name: 'New Session ' + new Date().toLocaleString('en-US'), + + await onClose({ + ...assemblyConf, + sequence: { + type: 'ReferenceSequenceTrack', + trackId: `${assemblyName}-${Date.now()}`, + ...(assemblyConf.sequence || {}), }, }) - setPluginManager(pm) - onClose() } catch (e) { setError(e) console.error(e) diff --git a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx index bca5d788e7..14792c102e 100644 --- a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx @@ -3,6 +3,7 @@ import { Button, makeStyles } from '@material-ui/core' import PluginManager from '@jbrowse/core/PluginManager' import PreloadedDatasetSelector from './PreloadedDatasetSelector' import OpenSequenceDialog from '../OpenSequenceDialog' +import { createPluginManager } from './util' const useStyles = makeStyles(theme => ({ form: { @@ -36,8 +37,20 @@ export default function StartScreenOptionsPanel({ {sequenceDialogOpen ? ( setSequenceDialogOpen(false)} - setPluginManager={setPluginManager} + onClose={async (conf: unknown) => { + if (conf) { + // note this can throw before dialog closes, but this is handled + // by the dialog itself + const pm = await createPluginManager({ + assemblies: [conf], + defaultSession: { + name: 'New Session ' + new Date().toLocaleString('en-US'), + }, + }) + setPluginManager(pm) + } + setSequenceDialogOpen(false) + }} /> ) : null} diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index c704b47fe0..5f7e38a2b8 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -22,6 +22,7 @@ import { UriLocation } from '@jbrowse/core/util/types' import { ipcRenderer } from 'electron' // icons +import OpenIcon from '@material-ui/icons/FolderOpen' import ExtensionIcon from '@material-ui/icons/Extension' import AppsIcon from '@material-ui/icons/Apps' import PowerIcon from '@material-ui/icons/Power' @@ -36,6 +37,13 @@ import OpenSequenceDialog from './OpenSequenceDialog' // @ts-ignore import RenderWorker from './rpc.worker' +function getSaveSession(model: RootModel) { + return { + ...getSnapshot(model.jbrowse), + defaultSession: model.session ? getSnapshot(model.session) : {}, + } +} + interface Menu { label: string menuItems: MenuItem[] @@ -68,25 +76,26 @@ export default function rootModelFactory(pluginManager: PluginManager) { pluginManager.pluggableMstType('internet account', 'stateModel'), ), isAssemblyEditing: false, + sessionPath: types.optional(types.string, ''), }) .volatile(() => ({ - error: undefined as Error | undefined, + error: undefined as unknown, textSearchManager: new TextSearchManager(pluginManager), })) .actions(self => ({ async saveSession(val: unknown) { - await ipcRenderer.invoke('saveSession', { - ...getSnapshot(self.jbrowse), - defaultSession: val, - }) + await ipcRenderer.invoke('saveSession', self.sessionPath, val) }, setSavedSessionNames(sessionNames: string[]) { self.savedSessionNames = cast(sessionNames) }, + setSessionPath(path: string) { + self.sessionPath = path + }, setSession(sessionSnapshot?: SnapshotIn) { self.session = cast(sessionSnapshot) }, - setError(error: Error) { + setError(error: unknown) { self.error = error }, setDefaultSession() { @@ -223,27 +232,37 @@ export default function rootModelFactory(pluginManager: PluginManager) { menuItems: [ { label: 'Open', - icon: AppsIcon, - onClick: () => { - self.setSession(undefined) - }, + icon: OpenIcon, + onClick: () => {}, }, { label: 'Save', icon: Save, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onClick: (session: any) => { - const rootModel = getParent(session) - rootModel.setAssemblyEditing(true) + onClick: async () => { + if (self.session) { + try { + await self.saveSession(getSaveSession(self as RootModel)) + } catch (e) { + console.error(e) + self.session?.notify(`${e}`, 'error') + } + } }, }, { label: 'Save as...', icon: SaveAs, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onClick: (session: any) => { - const rootModel = getParent(session) - rootModel.setAssemblyEditing(true) + onClick: async () => { + try { + const saveAsPath = await ipcRenderer.invoke( + 'promptSessionSaveAs', + ) + self.setSessionPath(saveAsPath) + await self.saveSession(getSaveSession(self as RootModel)) + } catch (e) { + console.error(e) + self.session?.notify(`${e}`, 'error') + } }, }, { @@ -252,10 +271,13 @@ export default function rootModelFactory(pluginManager: PluginManager) { { label: 'Open assembly...', icon: DNA, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onClick: (session: any) => { - const rootModel = getParent(session) - rootModel.setAssemblyEditing(true) + onClick: () => { + if (self.session) { + self.session.queueDialog(doneCallback => [ + OpenSequenceDialog, + { model: self, handleClose: doneCallback }, + ]) + } }, }, { @@ -495,7 +517,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { autorun( async () => { if (self.session) { - await self.saveSession(getSnapshot(self.session)) + await self.saveSession(getSaveSession(self as RootModel)) } }, { delay: 1000 }, From d895174310e1909bbce5e72f7d1d391235943771 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 15:33:57 -0400 Subject: [PATCH 05/23] Misc --- products/jbrowse-desktop/public/electron.ts | 65 +++++++-------------- products/jbrowse-desktop/src/rootModel.ts | 4 +- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 060df58122..b323501bf9 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -43,15 +43,10 @@ const devServerUrl = url.parse( process.env.DEV_SERVER_URL || 'http://localhost:3000', ) -const sessionDir = path.join(app.getPath('userData'), 'sessions') - -function getPath(sessionName: string, ext = 'json') { - return path.join(sessionDir, `${encodeURIComponent(sessionName)}.${ext}`) -} - -if (!fs.lstatSync(sessionDir).isDirectory()) { - fs.mkdirSync(sessionDir, { recursive: true }) -} +const recentSessionsPath = path.join( + app.getPath('userData'), + 'recent_sessions.json', +) interface SessionSnap { defaultSession: { @@ -237,29 +232,15 @@ app.on('activate', () => { }) ipcMain.handle('listSessions', async () => { - return new Map( - (await readdir(sessionDir)) - .filter(f => path.extname(f) === '.json') - .map(f => { - const base = path.basename(f, '.json') - const json = path.join(sessionDir, base + '.json') - - return [ - decodeURIComponent(base), - { - stats: fs.existsSync(json) ? fs.statSync(json) : undefined, - }, - ] - }), - ) + return JSON.parse(await readFile(recentSessionsPath, 'utf8')) }) ipcMain.handle('loadExternalConfig', (_event: unknown, sessionPath) => { return readFile(sessionPath, 'utf8') }) -ipcMain.handle('loadSession', async (_event: unknown, sessionName: string) => { - const data = await readFile(getPath(sessionName), 'utf8') +ipcMain.handle('loadSession', async (_event: unknown, sessionPath: string) => { + const data = await readFile(sessionPath, 'utf8') return JSON.parse(data) }) @@ -295,6 +276,16 @@ ipcMain.handle( 'saveSession', async (_event: unknown, path: string, snap: SessionSnap) => { const page = await mainWindow?.capturePage() + const rows = JSON.parse(fs.readFileSync(recentSessionsPath, 'utf8')) as [ + { path: string; updated: number }, + ] + const idx = rows.findIndex(r => r.path === path) + if (idx === -1) { + rows.unshift({ path, updated: +Date.now() }) + } else { + rows[idx].updated = +Date.now() + } + await writeFile( path, JSON.stringify( @@ -307,7 +298,7 @@ ipcMain.handle( ) ipcMain.handle('promptSessionSaveAs', async (_event: unknown) => { - const toLocalPath = path.join(app.getPath('desktop'), `session.json`) + const toLocalPath = path.join(app.getPath('desktop'), `jbrowse_session.json`) const choice = await dialog.showSaveDialog({ defaultPath: toLocalPath, }) @@ -315,26 +306,10 @@ ipcMain.handle('promptSessionSaveAs', async (_event: unknown) => { return choice.filePath }) -ipcMain.handle( - 'renameSession', - async (_event: unknown, oldName: string, newName: string) => { - const snap = JSON.parse(await readFile(getPath(oldName), 'utf8')) - snap.defaultSession.name = newName - await unlink(getPath(oldName)) - await writeFile(getPath(newName), JSON.stringify(snap, null, 2)) - }, -) - -ipcMain.handle('reset', async () => { - await Promise.all( - (await readdir(sessionDir)).map(f => unlink(path.join(sessionDir, f))), - ) -}) - ipcMain.handle( 'deleteSession', - async (_event: unknown, sessionName: string) => { - return unlink(getPath(sessionName)) + async (_event: unknown, sessionPath: string) => { + return unlink(sessionPath) }, ) diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 5f7e38a2b8..f0c767e460 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -84,7 +84,9 @@ export default function rootModelFactory(pluginManager: PluginManager) { })) .actions(self => ({ async saveSession(val: unknown) { - await ipcRenderer.invoke('saveSession', self.sessionPath, val) + if (self.sessionPath) { + await ipcRenderer.invoke('saveSession', self.sessionPath, val) + } }, setSavedSessionNames(sessionNames: string[]) { self.savedSessionNames = cast(sessionNames) From 9b1fad157dd8c485ca83aa08fb6e83e7f8ef7dbd Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 15:51:18 -0400 Subject: [PATCH 06/23] Misc --- products/jbrowse-desktop/public/electron.ts | 26 +++++++----- .../src/StartScreen/RecentSessionsPanel.tsx | 42 +++++++++---------- .../src/StartScreen/SessionCard.tsx | 30 +++++++------ 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index b323501bf9..f9d5c73130 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -7,7 +7,7 @@ import url from 'url' import windowStateKeeper from 'electron-window-state' import { autoUpdater } from 'electron-updater' -const { unlink, rename, readdir, readFile, writeFile } = fs.promises +const { unlink, readFile, writeFile } = fs.promises const { app, ipcMain, shell, BrowserWindow, Menu } = electron @@ -48,6 +48,10 @@ const recentSessionsPath = path.join( 'recent_sessions.json', ) +if (!fs.existsSync(recentSessionsPath)) { + fs.writeFileSync(recentSessionsPath, JSON.stringify([], null, 2), 'utf8') +} + interface SessionSnap { defaultSession: { name: string @@ -280,20 +284,20 @@ ipcMain.handle( { path: string; updated: number }, ] const idx = rows.findIndex(r => r.path === path) + const screenshot = page?.resize({ width: 250 }).toDataURL() + const entry = { + path, + screenshot, + updated: +Date.now(), + name: snap.defaultSession?.name, + } if (idx === -1) { - rows.unshift({ path, updated: +Date.now() }) + rows.unshift(entry) } else { - rows[idx].updated = +Date.now() + rows[idx] = entry } - await writeFile( - path, - JSON.stringify( - { ...snap, screenshot: page?.resize({ width: 250 }).toDataURL() }, - null, - 2, - ), - ) + await writeFile(path, JSON.stringify({ ...snap }, null, 2)) }, ) diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index cf1500b23e..747e958657 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -45,12 +45,14 @@ const useStyles = makeStyles(theme => ({ }, })) -interface SessionStats { - screenshot: string - stats: fs.Stats +interface RecentSessionData { + path: string + name: string + screenshot?: string + updated: number } -type Session = [string, SessionStats] +type Session = [string, RecentSessionData] function RecentSessionsList({ setError, @@ -145,7 +147,7 @@ function RecentSessionsList({ name: sessionName, rename: sessionName, delete: sessionName, - lastModified: session.stats?.mtime, + lastModified: session.updated, }))} rowHeight={25} headerHeight={33} @@ -166,19 +168,20 @@ function RecentSessionsCards({ setSessionsToDelete: (e: string[]) => void setSessionToRename: (e: string) => void setPluginManager: (pm: PluginManager) => void - sortedSessions: Session[] + sortedSessions: RecentSessionData[] }) { return ( - {sortedSessions?.map(([name, sessionData]) => ( - + {sortedSessions?.map(sessionData => ( + { try { - const data = await ipcRenderer.invoke('loadSession', name) + const data = await ipcRenderer.invoke( + 'loadSession', + sessionData.path, + ) const pm = await createPluginManager(data) setPluginManager(pm) } catch (e) { @@ -207,10 +210,6 @@ function ToggleButtonWithTooltip(props: ToggleButtonProps) { ) } -const getTime = (a: Session) => { - return +a[1].stats?.mtime -} - export default function RecentSessionPanel({ setError, setPluginManager, @@ -220,7 +219,7 @@ export default function RecentSessionPanel({ }) { const classes = useStyles() const [displayMode, setDisplayMode] = useLocalStorage('displayMode', 'list') - const [sessions, setSessions] = useState>(new Map()) + const [sessions, setSessions] = useState([]) const [sessionsToDelete, setSessionsToDelete] = useState() const [sessionToRename, setSessionToRename] = useState() const [updateSessionsList, setUpdateSessionsList] = useState(0) @@ -228,11 +227,10 @@ export default function RecentSessionPanel({ const sessionNames = useMemo(() => Object.keys(sessions || {}), [sessions]) - const sortedSessions = useMemo(() => { - return sessions - ? [...sessions.entries()].sort((a, b) => getTime(b) - getTime(a)) - : [] - }, [sessions]) + const sortedSessions = useMemo( + () => sessions?.sort((a, b) => b.updated - a.updated), + [sessions], + ) useEffect(() => { ;(async () => { diff --git a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx index 815deb4813..345ad54d6b 100644 --- a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx +++ b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx @@ -21,26 +21,26 @@ const useStyles = makeStyles({ width: 250, cursor: 'pointer', }, - cardHeader: { - width: 200, - }, media: { height: 0, paddingTop: '56.25%', // 16:9 }, }) +interface RecentSessionData { + path: string + name: string + screenshot?: string + updated: number +} + function RecentSessionCard({ - sessionName, - sessionStats, - sessionScreenshot = defaultSessionScreenshot, + sessionData, onClick, onDelete, onRename, }: { - sessionName: string - sessionStats?: { mtime: Date } - sessionScreenshot: string + sessionData: RecentSessionData onClick: Function onDelete: Function onRename: Function @@ -48,6 +48,7 @@ function RecentSessionCard({ const classes = useStyles() const [hovered, setHovered] = useState(false) const [menuAnchorEl, setMenuAnchorEl] = useState(null) + const sessionName = sessionData.name return ( <> @@ -55,10 +56,13 @@ function RecentSessionCard({ className={classes.card} onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)} - onClick={() => onClick(sessionName)} + onClick={() => onClick(sessionData.name)} raised={Boolean(hovered)} > - + Last modified{' '} - {sessionStats - ? `${sessionStats.mtime.toLocaleString('en-US')}` + {sessionData + ? `${new Date(sessionData.updated).toLocaleString('en-US')}` : null} } From 01d3fb38ccd42eca889f0a2eb733653d8a781132 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 16:08:08 -0400 Subject: [PATCH 07/23] Misc --- packages/core/ui/Snackbar.tsx | 9 ++---- products/jbrowse-desktop/public/electron.ts | 9 +++--- .../src/StartScreen/RecentSessionsPanel.tsx | 28 +++++++++---------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/core/ui/Snackbar.tsx b/packages/core/ui/Snackbar.tsx index dd94d70337..f1e8c7bf53 100644 --- a/packages/core/ui/Snackbar.tsx +++ b/packages/core/ui/Snackbar.tsx @@ -1,10 +1,9 @@ -import IconButton from '@material-ui/core/IconButton' -import Snackbar from '@material-ui/core/Snackbar' +import React, { useEffect, useState } from 'react' +import { IconButton, Snackbar } from '@material-ui/core' import CloseIcon from '@material-ui/icons/Close' import Alert from '@material-ui/lab/Alert' import { observer } from 'mobx-react' import { IAnyStateTreeNode } from 'mobx-state-tree' -import React, { useEffect, useState } from 'react' import { AbstractSessionModel, NotificationLevel } from '../util' type SnackbarMessage = [string, NotificationLevel] @@ -20,9 +19,7 @@ function MessageSnackbar({ session: SnackbarSession & IAnyStateTreeNode }) { const [open, setOpen] = useState(false) - const [snackbarMessage, setSnackbarMessage] = useState< - SnackbarMessage | undefined - >() + const [snackbarMessage, setSnackbarMessage] = useState() const { popSnackbarMessage, snackbarMessages } = session diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index f9d5c73130..ebc9ddda97 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -244,8 +244,7 @@ ipcMain.handle('loadExternalConfig', (_event: unknown, sessionPath) => { }) ipcMain.handle('loadSession', async (_event: unknown, sessionPath: string) => { - const data = await readFile(sessionPath, 'utf8') - return JSON.parse(data) + return JSON.parse(await readFile(sessionPath, 'utf8')) }) ipcMain.handle( @@ -296,8 +295,10 @@ ipcMain.handle( } else { rows[idx] = entry } - - await writeFile(path, JSON.stringify({ ...snap }, null, 2)) + await Promise.all([ + writeFile(recentSessionsPath, JSON.stringify(rows, null, 2)), + writeFile(path, JSON.stringify(snap, null, 2)), + ]) }, ) diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index 747e958657..39a7c1258e 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -52,11 +52,9 @@ interface RecentSessionData { updated: number } -type Session = [string, RecentSessionData] - function RecentSessionsList({ setError, - sortedSessions, + sessions, setSelectedSessions, setSessionToRename, setPluginManager, @@ -65,7 +63,7 @@ function RecentSessionsList({ setSessionToRename: (e: string) => void setPluginManager: (pm: PluginManager) => void setSelectedSessions: (arg: string[]) => void - sortedSessions: Session[] + sessions: RecentSessionData[] }) { const classes = useStyles() const columns = [ @@ -120,7 +118,7 @@ function RecentSessionsList({ if (!value) { return null } - const lastModified = value as Date + const lastModified = new Date(value as string) const now = Date.now() const oneDayLength = 24 * 60 * 60 * 1000 if (now - lastModified.getTime() < oneDayLength) { @@ -142,11 +140,11 @@ function RecentSessionsList({ checkboxSelection disableSelectionOnClick onSelectionModelChange={args => setSelectedSessions(args as string[])} - rows={sortedSessions.map(([sessionName, session]) => ({ - id: sessionName, - name: sessionName, - rename: sessionName, - delete: sessionName, + rows={sessions.map(session => ({ + id: session.name, + name: session.name, + rename: session.name, + delete: session.name, lastModified: session.updated, }))} rowHeight={25} @@ -158,7 +156,7 @@ function RecentSessionsList({ } function RecentSessionsCards({ - sortedSessions, + sessions, setError, setSessionsToDelete, setSessionToRename, @@ -168,11 +166,11 @@ function RecentSessionsCards({ setSessionsToDelete: (e: string[]) => void setSessionToRename: (e: string) => void setPluginManager: (pm: PluginManager) => void - sortedSessions: RecentSessionData[] + sessions: RecentSessionData[] }) { return ( - {sortedSessions?.map(sessionData => ( + {sessions?.map(sessionData => ( Date: Mon, 11 Oct 2021 16:11:16 -0400 Subject: [PATCH 08/23] Recent session table works --- .../src/StartScreen/RecentSessionsPanel.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index 39a7c1258e..ce04c6fb1a 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -96,7 +96,10 @@ function RecentSessionsList({ className={classes.pointer} onClick={async () => { try { - const data = await ipcRenderer.invoke('loadSession', value) + const data = await ipcRenderer.invoke( + 'loadSession', + params.row.path, + ) const pm = await createPluginManager(data) setPluginManager(pm) } catch (e) { @@ -146,6 +149,7 @@ function RecentSessionsList({ rename: session.name, delete: session.name, lastModified: session.updated, + path: session.path, }))} rowHeight={25} headerHeight={33} @@ -170,17 +174,15 @@ function RecentSessionsCards({ }) { return ( - {sessions?.map(sessionData => ( - + {sessions?.map(session => ( + { try { - const data = await ipcRenderer.invoke( - 'loadSession', - sessionData.path, + const pm = await createPluginManager( + await ipcRenderer.invoke('loadSession', session.path), ) - const pm = await createPluginManager(data) setPluginManager(pm) } catch (e) { console.error(e) From d0b9ef66ffe157ebc928e04f93764d7125ea4b92 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 16:12:59 -0400 Subject: [PATCH 09/23] Add path column to table --- products/jbrowse-desktop/public/electron.ts | 59 +++++++++++++------ .../src/StartScreen/RecentSessionsPanel.tsx | 10 +++- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index ebc9ddda97..c6a840c9db 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -11,6 +11,10 @@ const { unlink, readFile, writeFile } = fs.promises const { app, ipcMain, shell, BrowserWindow, Menu } = electron +function stringify(obj: unknown) { + return JSON.stringify(obj, null, 2) +} + // manual auto-updates https://github.com/electron-userland/electron-builder/blob/docs/encapsulated%20manual%20update%20via%20menu.js autoUpdater.autoDownload = false @@ -21,20 +25,18 @@ autoUpdater.on('error', error => { ) }) -autoUpdater.on('update-available', () => { - dialog - .showMessageBox({ - type: 'info', - title: 'Found updates', - message: - 'Found updates, do you want update now? No status will appear while the update downloads, but a dialog will appear once complete', - buttons: ['Yes', 'No'], - }) - .then(buttonIndex => { - if (buttonIndex.response === 0) { - autoUpdater.downloadUpdate() - } - }) +autoUpdater.on('update-available', async () => { + const result = await dialog.showMessageBox({ + type: 'info', + title: 'Found updates', + message: + 'Found updates, do you want update now? Note: the update will download in the background, and a dialog will appear once complete', + buttons: ['Yes', 'No'], + }) + + if (result.response === 0) { + autoUpdater.downloadUpdate() + } }) debug({ showDevTools: false }) @@ -49,7 +51,7 @@ const recentSessionsPath = path.join( ) if (!fs.existsSync(recentSessionsPath)) { - fs.writeFileSync(recentSessionsPath, JSON.stringify([], null, 2), 'utf8') + fs.writeFileSync(recentSessionsPath, stringify([]), 'utf8') } interface SessionSnap { @@ -296,8 +298,8 @@ ipcMain.handle( rows[idx] = entry } await Promise.all([ - writeFile(recentSessionsPath, JSON.stringify(rows, null, 2)), - writeFile(path, JSON.stringify(snap, null, 2)), + writeFile(recentSessionsPath, stringify(rows)), + writeFile(path, stringify(snap)), ]) }, ) @@ -337,3 +339,26 @@ autoUpdater.on('update-downloaded', () => { buttons: ['OK'], }) }) + +ipcMain.handle( + 'renameSession', + async (_event: unknown, path: string, newName: string) => { + const sessions = JSON.parse(await readFile(recentSessionsPath, 'utf8')) as { + path: string + name: string + }[] + const session = JSON.parse(await readFile(path, 'utf8')) + const idx = sessions.findIndex(row => row.path === path) + if (idx !== -1) { + sessions[idx].name = newName + session.defaultSession.name = newName + } else { + throw new Error(`Session at ${path} not found`) + } + + await Promise.all([ + writeFile(recentSessionsPath, stringify(sessions)), + writeFile(path, stringify(session)), + ]) + }, +) diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index ce04c6fb1a..25a58380eb 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -113,7 +113,15 @@ function RecentSessionsList({ ) }, }, - + { + field: 'path', + headerName: 'Session path', + flex: 0.7, + renderCell: (params: GridCellParams) => { + const { value } = params + return value + }, + }, { field: 'lastModified', headerName: 'Last modified', From c02ae72430c4944fda1c333bcec445aafcc2292c Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 16:48:58 -0400 Subject: [PATCH 10/23] Updates --- .../src/StartScreen/RecentSessionsPanel.tsx | 37 +++++++++++-------- .../src/StartScreen/SessionCard.tsx | 13 +++---- .../dialogs/DeleteSessionDialog.tsx | 6 +-- .../dialogs/RenameSessionDialog.tsx | 18 +++------ products/jbrowse-desktop/src/rootModel.ts | 9 ++--- 5 files changed, 38 insertions(+), 45 deletions(-) diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index 25a58380eb..0b85ff7310 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -52,6 +52,8 @@ interface RecentSessionData { updated: number } +type RecentSessions = RecentSessionData[] + function RecentSessionsList({ setError, sessions, @@ -60,9 +62,9 @@ function RecentSessionsList({ setPluginManager, }: { setError: (e: unknown) => void - setSessionToRename: (e: string) => void + setSessionToRename: (arg: RecentSessionData) => void setPluginManager: (pm: PluginManager) => void - setSelectedSessions: (arg: string[]) => void + setSelectedSessions: (arg: RecentSessionData[]) => void sessions: RecentSessionData[] }) { const classes = useStyles() @@ -75,9 +77,10 @@ function RecentSessionsList({ filterable: false, headerName: ' ', renderCell: (params: GridCellParams) => { - const { value } = params return ( - setSessionToRename(value as string)}> + setSessionToRename(params.row as RecentSessionData)} + > @@ -150,9 +153,14 @@ function RecentSessionsList({ setSelectedSessions(args as string[])} + onSelectionModelChange={args => { + console.log(sessions.filter(session => args.includes(session.path))) + setSelectedSessions( + sessions.filter(session => args.includes(session.path)), + ) + }} rows={sessions.map(session => ({ - id: session.name, + id: session.path, name: session.name, rename: session.name, delete: session.name, @@ -175,8 +183,8 @@ function RecentSessionsCards({ setPluginManager, }: { setError: (e: unknown) => void - setSessionsToDelete: (e: string[]) => void - setSessionToRename: (e: string) => void + setSessionsToDelete: (e: RecentSessionData[]) => void + setSessionToRename: (arg: RecentSessionData) => void setPluginManager: (pm: PluginManager) => void sessions: RecentSessionData[] }) { @@ -197,7 +205,7 @@ function RecentSessionsCards({ setError(e) } }} - onDelete={(del: string) => setSessionsToDelete([del])} + onDelete={del => setSessionsToDelete([del])} onRename={setSessionToRename} /> @@ -227,13 +235,11 @@ export default function RecentSessionPanel({ }) { const classes = useStyles() const [displayMode, setDisplayMode] = useLocalStorage('displayMode', 'list') - const [sessions, setSessions] = useState([]) - const [sessionsToDelete, setSessionsToDelete] = useState() - const [sessionToRename, setSessionToRename] = useState() + const [sessions, setSessions] = useState([]) + const [sessionToRename, setSessionToRename] = useState() const [updateSessionsList, setUpdateSessionsList] = useState(0) - const [selectedSessions, setSelectedSessions] = useState() - - const sessionNames = useMemo(() => Object.keys(sessions || {}), [sessions]) + const [selectedSessions, setSelectedSessions] = useState() + const [sessionsToDelete, setSessionsToDelete] = useState() const sortedSessions = useMemo( () => sessions?.sort((a, b) => b.updated - a.updated), @@ -271,7 +277,6 @@ export default function RecentSessionPanel({
{ setSessionToRename(undefined) setUpdateSessionsList(s => s + 1) diff --git a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx index 345ad54d6b..d57df11d37 100644 --- a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx +++ b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx @@ -41,9 +41,9 @@ function RecentSessionCard({ onRename, }: { sessionData: RecentSessionData - onClick: Function - onDelete: Function - onRename: Function + onClick: (arg: RecentSessionData) => void + onDelete: (arg: RecentSessionData) => void + onRename: (arg: RecentSessionData) => void }) { const classes = useStyles() const [hovered, setHovered] = useState(false) @@ -56,7 +56,7 @@ function RecentSessionCard({ className={classes.card} onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)} - onClick={() => onClick(sessionData.name)} + onClick={() => onClick(sessionData)} raised={Boolean(hovered)} > { setMenuAnchorEl(null) - onRename(sessionName) + onRename(sessionData) }} > @@ -117,7 +116,7 @@ function RecentSessionCard({ { - onDelete(sessionName) + onDelete(sessionData) setMenuAnchorEl(null) }} > diff --git a/products/jbrowse-desktop/src/StartScreen/dialogs/DeleteSessionDialog.tsx b/products/jbrowse-desktop/src/StartScreen/dialogs/DeleteSessionDialog.tsx index be7b7e9c02..542b2f4fea 100644 --- a/products/jbrowse-desktop/src/StartScreen/dialogs/DeleteSessionDialog.tsx +++ b/products/jbrowse-desktop/src/StartScreen/dialogs/DeleteSessionDialog.tsx @@ -14,7 +14,7 @@ const DeleteSessionDialog = ({ onClose, setError, }: { - sessionsToDelete: string[] + sessionsToDelete: { path: string }[] onClose: (arg0: boolean) => void setError: (e: unknown) => void }) => { @@ -32,8 +32,8 @@ const DeleteSessionDialog = ({ onClick={async () => { try { await Promise.all( - sessionsToDelete.map(sessionName => - ipcRenderer.invoke('deleteSession', sessionName), + sessionsToDelete.map(session => + ipcRenderer.invoke('deleteSession', session.path), ), ) onClose(true) diff --git a/products/jbrowse-desktop/src/StartScreen/dialogs/RenameSessionDialog.tsx b/products/jbrowse-desktop/src/StartScreen/dialogs/RenameSessionDialog.tsx index 6ed9d26005..ba9cf18658 100644 --- a/products/jbrowse-desktop/src/StartScreen/dialogs/RenameSessionDialog.tsx +++ b/products/jbrowse-desktop/src/StartScreen/dialogs/RenameSessionDialog.tsx @@ -14,12 +14,10 @@ import electron from 'electron' const { ipcRenderer } = electron const RenameSessionDialog = ({ - sessionNames, sessionToRename, onClose, }: { - sessionNames: string[] - sessionToRename?: string + sessionToRename?: { path: string; name: string } onClose: (arg0: boolean) => void }) => { const [newSessionName, setNewSessionName] = useState('') @@ -27,19 +25,14 @@ const RenameSessionDialog = ({ return ( onClose(false)}> - Rename + Rename session - + Please enter a new name for the session: - {sessionNames.includes(newSessionName) ? ( - - There is already a session named "{newSessionName}" - - ) : null} setNewSessionName(event.target.value)} /> {error ? ( @@ -55,7 +48,7 @@ const RenameSessionDialog = ({ try { await ipcRenderer.invoke( 'renameSession', - sessionToRename, + sessionToRename?.path, newSessionName, ) onClose(true) @@ -66,7 +59,6 @@ const RenameSessionDialog = ({ }} color="primary" variant="contained" - disabled={!newSessionName || sessionNames.includes(newSessionName)} > OK diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index f0c767e460..b57cafaf03 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -107,13 +107,10 @@ export default function rootModelFactory(pluginManager: PluginManager) { self.isAssemblyEditing = flag }, - renameCurrentSession(sessionName: string) { + async renameCurrentSession(newName: string) { if (self.session) { - const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) - const oldName = snapshot.name - snapshot.name = sessionName - this.setSession(snapshot) - ipcRenderer.invoke('renameSession', oldName, sessionName) + this.setSession({ ...getSnapshot(self.session), name: newName }) + await this.saveSession(getSaveSession(self as RootModel)) } }, duplicateCurrentSession() { From 81cb71480d58981c58a39de5ab142d0e5be2cbfa Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 16:54:00 -0400 Subject: [PATCH 11/23] Fix delete --- products/jbrowse-desktop/public/electron.ts | 22 ++++++++++++++++++--- products/jbrowse-desktop/src/rootModel.ts | 14 ++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index c6a840c9db..85c6bbc7e5 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -45,6 +45,8 @@ const devServerUrl = url.parse( process.env.DEV_SERVER_URL || 'http://localhost:3000', ) +const defaultSessionName = `jbrowse_session.json` + const recentSessionsPath = path.join( app.getPath('userData'), 'recent_sessions.json', @@ -305,7 +307,7 @@ ipcMain.handle( ) ipcMain.handle('promptSessionSaveAs', async (_event: unknown) => { - const toLocalPath = path.join(app.getPath('desktop'), `jbrowse_session.json`) + const toLocalPath = path.join(app.getPath('desktop'), defaultSessionName) const choice = await dialog.showSaveDialog({ defaultPath: toLocalPath, }) @@ -316,12 +318,26 @@ ipcMain.handle('promptSessionSaveAs', async (_event: unknown) => { ipcMain.handle( 'deleteSession', async (_event: unknown, sessionPath: string) => { - return unlink(sessionPath) + const sessions = JSON.parse(await readFile(recentSessionsPath, 'utf8')) as { + path: string + name: string + }[] + + const idx = sessions.findIndex(row => row.path === sessionPath) + if (idx !== -1) { + sessions.splice(idx, 1) + } else { + throw new Error(`Session at ${path} not found`) + } + + await Promise.all([ + writeFile(recentSessionsPath, stringify(sessions)), + unlink(sessionPath).catch(e => console.error(e)), + ]) }, ) /// from https://github.com/iffy/electron-updater-example/blob/master/main.js -// autoUpdater.on('checking-for-update', () => { sendStatusToWindow('Checking for update...') }) diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index b57cafaf03..cee76c6e37 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -274,7 +274,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { if (self.session) { self.session.queueDialog(doneCallback => [ OpenSequenceDialog, - { model: self, handleClose: doneCallback }, + { model: self, onClose: doneCallback }, ]) } }, @@ -304,10 +304,14 @@ export default function rootModelFactory(pluginManager: PluginManager) { { label: 'Open connection...', icon: PowerIcon, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onClick: (session: any) => { - const rootModel = getParent(session) - rootModel.setAssemblyEditing(true) + onClick: () => { + if (self.session) { + const widget = self.session.addWidget( + 'AddConnectionWidget', + 'addConnectionWidget', + ) + self.session.showWidget(widget) + } }, }, { From 85f4d6493354eb2f1f7bdb2732865a00420214b1 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 17:03:49 -0400 Subject: [PATCH 12/23] Add quit --- packages/core/ui/Icons.tsx | 1 + products/jbrowse-desktop/public/electron.ts | 4 ++++ products/jbrowse-desktop/src/rootModel.ts | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/ui/Icons.tsx b/packages/core/ui/Icons.tsx index 577971daf3..e1e65b5cf8 100644 --- a/packages/core/ui/Icons.tsx +++ b/packages/core/ui/Icons.tsx @@ -59,6 +59,7 @@ export function Save(props: SvgIconProps) { ) } +// dna from https://materialdesignicons.com/ export function DNA(props: SvgIconProps) { return ( diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 85c6bbc7e5..3586a490ee 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -239,6 +239,10 @@ app.on('activate', () => { } }) +ipcMain.handle('quit', () => { + app.quit() +}) + ipcMain.handle('listSessions', async () => { return JSON.parse(await readFile(recentSessionsPath, 'utf8')) }) diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index cee76c6e37..bb66e3c0e9 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -328,7 +328,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { label: 'Exit', icon: MeetingRoomIcon, onClick: () => { - self.setSession(undefined) + ipcRenderer.invoke('quit') }, }, ], From 994660384a418d6c5679a0b78215ed12596e80f3 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 17:06:04 -0400 Subject: [PATCH 13/23] Lint fixes --- plugins/data-management/src/index.ts | 3 --- .../jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx | 2 -- products/jbrowse-desktop/src/rootModel.ts | 1 - 3 files changed, 6 deletions(-) diff --git a/plugins/data-management/src/index.ts b/plugins/data-management/src/index.ts index 71f4f1cc87..c89249bf74 100644 --- a/plugins/data-management/src/index.ts +++ b/plugins/data-management/src/index.ts @@ -3,9 +3,6 @@ import ConnectionType from '@jbrowse/core/pluggableElementTypes/ConnectionType' import WidgetType from '@jbrowse/core/pluggableElementTypes/WidgetType' import Plugin from '@jbrowse/core/Plugin' import PluginManager from '@jbrowse/core/PluginManager' -import { SessionWithWidgets, isAbstractMenuManager } from '@jbrowse/core/util' -import NoteAddIcon from '@material-ui/icons/NoteAdd' -import InputIcon from '@material-ui/icons/Input' import { configSchema as ucscConfigSchema, modelFactory as ucscModelFactory, diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index 0b85ff7310..17f2444aaf 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useMemo } from 'react' -import fs from 'fs' import { CircularProgress, FormControl, @@ -154,7 +153,6 @@ function RecentSessionsList({ checkboxSelection disableSelectionOnClick onSelectionModelChange={args => { - console.log(sessions.filter(session => args.includes(session.path))) setSelectedSessions( sessions.filter(session => args.includes(session.path)), ) diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index bb66e3c0e9..8109876dcf 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -2,7 +2,6 @@ import { addDisposer, cast, resolveIdentifier, - getParent, getSnapshot, types, SnapshotIn, From 2956d2494bc54b508bbde6a9edcf7f0f52f20057 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 18:00:41 -0400 Subject: [PATCH 14/23] Add open track back --- products/jbrowse-web/src/rootModel.ts | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/products/jbrowse-web/src/rootModel.ts b/products/jbrowse-web/src/rootModel.ts index 2895959a51..29d84fd006 100644 --- a/products/jbrowse-web/src/rootModel.ts +++ b/products/jbrowse-web/src/rootModel.ts @@ -40,6 +40,8 @@ import FileCopyIcon from '@material-ui/icons/FileCopy' import FolderOpenIcon from '@material-ui/icons/FolderOpen' import GetAppIcon from '@material-ui/icons/GetApp' import PublishIcon from '@material-ui/icons/Publish' +import StorageIcon from '@material-ui/icons/Storage' +import PowerIcon from '@material-ui/icons/Power' import SaveIcon from '@material-ui/icons/Save' // other @@ -498,6 +500,43 @@ export default function RootModel( } }, }, + { type: 'divider' }, + { + label: 'Open track...', + icon: StorageIcon, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick: (session: any) => { + if (session.views.length === 0) { + session.notify('Please open a view to add a track first') + } else if (session.views.length >= 1) { + const widget = session.addWidget( + 'AddTrackWidget', + 'addTrackWidget', + { view: session.views[0].id }, + ) + session.showWidget(widget) + if (session.views.length > 1) { + session.notify( + `This will add a track to the first view. Note: if you want to open a track in a specific view open the track selector for that view and use the add track (plus icon) in the bottom right`, + ) + } + } + }, + }, + { + label: 'Open connection...', + icon: PowerIcon, + onClick: () => { + if (self.session) { + const widget = self.session.addWidget( + 'AddConnectionWidget', + 'addConnectionWidget', + ) + self.session.showWidget(widget) + } + }, + }, + { type: 'divider' }, { label: 'Return to splash screen', icon: AppsIcon, From 7017bd17afd4f770cfcc6a9e19aaa391992d0670 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 18:19:35 -0400 Subject: [PATCH 15/23] Replace with cable icon --- packages/core/ui/Icons.tsx | 8 ++++++++ products/jbrowse-desktop/src/rootModel.ts | 5 ++--- products/jbrowse-web/src/rootModel.ts | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/core/ui/Icons.tsx b/packages/core/ui/Icons.tsx index e1e65b5cf8..ca237b3293 100644 --- a/packages/core/ui/Icons.tsx +++ b/packages/core/ui/Icons.tsx @@ -70,3 +70,11 @@ export function DNA(props: SvgIconProps) { ) } + +export function Cable(props: SvgIconProps) { + return ( + + + + ) +} diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 8109876dcf..701b377bd0 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -24,10 +24,9 @@ import { ipcRenderer } from 'electron' import OpenIcon from '@material-ui/icons/FolderOpen' import ExtensionIcon from '@material-ui/icons/Extension' import AppsIcon from '@material-ui/icons/Apps' -import PowerIcon from '@material-ui/icons/Power' import StorageIcon from '@material-ui/icons/Storage' import MeetingRoomIcon from '@material-ui/icons/MeetingRoom' -import { Save, SaveAs, DNA } from '@jbrowse/core/ui/Icons' +import { Save, SaveAs, DNA, Cable } from '@jbrowse/core/ui/Icons' // locals import sessionModelFactory from './sessionModelFactory' @@ -302,7 +301,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { }, { label: 'Open connection...', - icon: PowerIcon, + icon: Cable, onClick: () => { if (self.session) { const widget = self.session.addWidget( diff --git a/products/jbrowse-web/src/rootModel.ts b/products/jbrowse-web/src/rootModel.ts index 29d84fd006..5391283e3e 100644 --- a/products/jbrowse-web/src/rootModel.ts +++ b/products/jbrowse-web/src/rootModel.ts @@ -41,8 +41,8 @@ import FolderOpenIcon from '@material-ui/icons/FolderOpen' import GetAppIcon from '@material-ui/icons/GetApp' import PublishIcon from '@material-ui/icons/Publish' import StorageIcon from '@material-ui/icons/Storage' -import PowerIcon from '@material-ui/icons/Power' import SaveIcon from '@material-ui/icons/Save' +import { Cable } from '@jbrowse/core/ui/Icons' // other import corePlugins from './corePlugins' @@ -525,7 +525,7 @@ export default function RootModel( }, { label: 'Open connection...', - icon: PowerIcon, + icon: Cable, onClick: () => { if (self.session) { const widget = self.session.addWidget( From 90279086d6190cc520398b843beb326847742793 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 11 Oct 2021 18:44:24 -0400 Subject: [PATCH 16/23] Updates --- .../src/__snapshots__/rootModel.test.js.snap | 46 +++++++++++++++++++ .../src/tests/CircularView.test.js | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/products/jbrowse-web/src/__snapshots__/rootModel.test.js.snap b/products/jbrowse-web/src/__snapshots__/rootModel.test.js.snap index 615db23190..05a3ef174a 100644 --- a/products/jbrowse-web/src/__snapshots__/rootModel.test.js.snap +++ b/products/jbrowse-web/src/__snapshots__/rootModel.test.js.snap @@ -77,6 +77,29 @@ Array [ "label": "Duplicate session", "onClick": [Function], }, + Object { + "type": "divider", + }, + Object { + "icon": Object { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Open track...", + "onClick": [Function], + }, + Object { + "icon": [Function], + "label": "Open connection...", + "onClick": [Function], + }, + Object { + "type": "divider", + }, Object { "icon": Object { "$$typeof": Symbol(react.memo), @@ -171,6 +194,29 @@ Array [ "label": "Duplicate session", "onClick": [Function], }, + Object { + "type": "divider", + }, + Object { + "icon": Object { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": Object { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Open track...", + "onClick": [Function], + }, + Object { + "icon": [Function], + "label": "Open connection...", + "onClick": [Function], + }, + Object { + "type": "divider", + }, Object { "icon": Object { "$$typeof": Symbol(react.memo), diff --git a/products/jbrowse-web/src/tests/CircularView.test.js b/products/jbrowse-web/src/tests/CircularView.test.js index 6007424e68..56c092cd09 100644 --- a/products/jbrowse-web/src/tests/CircularView.test.js +++ b/products/jbrowse-web/src/tests/CircularView.test.js @@ -48,7 +48,7 @@ describe('circular views', () => { await findByText('Help') // try opening a track before opening the actual view fireEvent.click(await findByText('File')) - fireEvent.click(await findByText('Open track')) + fireEvent.click(await findByText(/Open track/)) fireEvent.click(await findByText('Open')) From 179bcc3700383e9d417c1a5654a6020070982de2 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 11:40:10 -0400 Subject: [PATCH 17/23] Within query in test --- products/jbrowse-web/src/tests/Authentication.test.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/products/jbrowse-web/src/tests/Authentication.test.js b/products/jbrowse-web/src/tests/Authentication.test.js index 450b5c91ad..62ac8336d0 100644 --- a/products/jbrowse-web/src/tests/Authentication.test.js +++ b/products/jbrowse-web/src/tests/Authentication.test.js @@ -1,5 +1,5 @@ // library -import { cleanup, fireEvent, render } from '@testing-library/react' +import { cleanup, fireEvent, render, within } from '@testing-library/react' import React from 'react' import { LocalFile, RemoteFile } from 'generic-filehandle' @@ -119,11 +119,13 @@ describe('authentication', () => { fireEvent.click( await findByTestId('htsTrackEntry-volvox_microarray_externaltoken'), ) - await findByTestId('externalToken-form') + const { findByText: findByTextWithin } = within( + await findByTestId('externalToken-form'), + ) fireEvent.change(await findByTestId('entry-externalToken'), { target: { value: 'testentry' }, }) - fireEvent.click(await findByText('Add')) + fireEvent.click(await findByTextWithin('Add')) expect(Object.keys(sessionStorage)).toContain('ExternalTokenTest-token') expect(Object.values(sessionStorage)).toContain('testentry') From bd277f7f517fa1701df13e1fe4ef7f3d4bc99b1e Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 12:27:00 -0400 Subject: [PATCH 18/23] Fix tsc --- products/jbrowse-desktop/public/electron.ts | 10 +++------- .../jbrowse-desktop/src/StartScreen/LauncherPanel.tsx | 4 ++-- .../src/StartScreen/RecentSessionsPanel.tsx | 6 ++++-- .../jbrowse-desktop/src/StartScreen/SessionCard.tsx | 1 + 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 64d9bc30dd..34e86983e6 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -48,10 +48,9 @@ const devServerUrl = url.parse( const defaultSessionName = `jbrowse_session.json` -const recentSessionsPath = path.join( - app.getPath('userData'), - 'recent_sessions.json', -) +const userData = app.getPath('userData') +const recentSessionsPath = path.join(userData, 'recent_sessions.json') +const quickstartDir = path.join(userData, 'quickstart') function getQuickstartPath(sessionName: string, ext = 'json') { return path.join(quickstartDir, `${encodeURIComponent(sessionName)}.${ext}`) @@ -367,7 +366,6 @@ ipcMain.handle( }, ) - ipcMain.handle( 'renameSession', async (_event: unknown, path: string, newName: string) => { @@ -391,7 +389,6 @@ ipcMain.handle( }, ) - /// from https://github.com/iffy/electron-updater-example/blob/master/main.js autoUpdater.on('checking-for-update', () => { sendStatusToWindow('Checking for update...') @@ -410,4 +407,3 @@ autoUpdater.on('update-downloaded', () => { buttons: ['OK'], }) }) - diff --git a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx index e4e11dc766..af7b37feca 100644 --- a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' -import { Button, makeStyles } from '@material-ui/core' +import { Button, Typography, makeStyles } from '@material-ui/core' import PluginManager from '@jbrowse/core/PluginManager' -import PreloadedDatasetSelector from './PreloadedDatasetSelector' +import QuickstartPanel from './QuickstartPanel' import OpenSequenceDialog from '../OpenSequenceDialog' import { createPluginManager } from './util' diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index ae2eace548..9e18bc9e1d 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -275,9 +275,11 @@ export default function RecentSessionPanel({ ) } - async function addToQuickstartList(arg: string[]) { + async function addToQuickstartList(arg: RecentSessionData[]) { await Promise.all( - arg.map(session => ipcRenderer.invoke('addToQuickstartList', session)), + arg.map(session => + ipcRenderer.invoke('addToQuickstartList', session.path), + ), ) } diff --git a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx index b8b2d62766..5bcead5fb0 100644 --- a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx +++ b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx @@ -40,6 +40,7 @@ function RecentSessionCard({ onClick, onDelete, onRename, + onAddToQuickstartList, }: { sessionData: RecentSessionData onClick: (arg: RecentSessionData) => void From dfbfd7cf1a309f8e537a4a3547e19897966611d6 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 13:26:21 -0400 Subject: [PATCH 19/23] Fix lint --- products/jbrowse-desktop/public/electron.ts | 35 ++++++++++++++++++- .../src/StartScreen/RecentSessionsPanel.tsx | 2 +- .../src/tests/Authentication.test.js | 2 +- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 34e86983e6..124495f36f 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -8,7 +8,7 @@ import windowStateKeeper from 'electron-window-state' import { autoUpdater } from 'electron-updater' import fetch from 'node-fetch' -const { unlink, readFile, copyFile, writeFile } = fs.promises +const { unlink, readFile, copyFile, readdir, writeFile } = fs.promises const { app, ipcMain, shell, BrowserWindow, Menu } = electron @@ -280,6 +280,39 @@ ipcMain.handle('loadSession', async (_event: unknown, sessionPath: string) => { return JSON.parse(await readFile(sessionPath, 'utf8')) }) +ipcMain.handle( + 'addToQuickstartList', + async (_event: unknown, sessionPath: string, sessionName: string) => { + await copyFile(sessionPath, getQuickstartPath(sessionName)) + }, +) + +ipcMain.handle('listQuickstarts', async (_event: unknown) => { + return (await readdir(quickstartDir)) + .filter(f => path.extname(f) === '.json') + .map(f => decodeURIComponent(path.basename(f, '.json'))) +}) + +ipcMain.handle('deleteQuickstart', async (_event: unknown, name: string) => { + fs.unlinkSync(getQuickstartPath(name)) + + // add a gravestone '.deleted' file when we delete a session, so that if it + // comes from the https://jbrowse.org/genomes/sessions.json, we don't + // recreate it + fs.writeFileSync(getQuickstartPath(name) + '.deleted', '', 'utf8') +}) + +ipcMain.handle( + 'renameQuickstart', + async (_event: unknown, oldName: string, newName: string) => { + return fs.renameSync(getQuickstartPath(oldName), getQuickstartPath(newName)) + }, +) + +ipcMain.handle('getQuickstart', async (_event: unknown, name: string) => { + return JSON.parse(await readFile(getQuickstartPath(name), 'utf8')) +}) + ipcMain.handle( 'openAuthWindow', (_event: unknown, { internetAccountId, data, url }) => { diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index 9e18bc9e1d..21c880c0f3 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -278,7 +278,7 @@ export default function RecentSessionPanel({ async function addToQuickstartList(arg: RecentSessionData[]) { await Promise.all( arg.map(session => - ipcRenderer.invoke('addToQuickstartList', session.path), + ipcRenderer.invoke('addToQuickstartList', session.path, session.name), ), ) } diff --git a/products/jbrowse-web/src/tests/Authentication.test.js b/products/jbrowse-web/src/tests/Authentication.test.js index 62ac8336d0..494d059be4 100644 --- a/products/jbrowse-web/src/tests/Authentication.test.js +++ b/products/jbrowse-web/src/tests/Authentication.test.js @@ -112,7 +112,7 @@ describe('authentication', () => { ], }) const state = pluginManager.rootModel - const { findByTestId, findAllByTestId, findByText } = render( + const { findByTestId, findAllByTestId } = render( , ) state.session.views[0].setNewView(5, 0) From a933d9fc91b56c81806e2f9608eaebb3c7b242a8 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 14:28:54 -0400 Subject: [PATCH 20/23] Implement open --- products/jbrowse-desktop/public/electron.ts | 10 ++++++++++ products/jbrowse-desktop/src/JBrowse.tsx | 2 +- products/jbrowse-desktop/src/Loader.tsx | 11 +++++++++-- products/jbrowse-desktop/src/rootModel.ts | 13 ++++++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 124495f36f..2e7d130aa4 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -368,6 +368,16 @@ ipcMain.handle( }, ) + +ipcMain.handle('promptOpenFile', async (_event: unknown) => { + const toLocalPath = path.join(app.getPath('desktop'), defaultSessionName) + const choice = await dialog.showOpenDialog({ + defaultPath: toLocalPath, + }) + + return choice.filePaths[0] +}) + ipcMain.handle('promptSessionSaveAs', async (_event: unknown) => { const toLocalPath = path.join(app.getPath('desktop'), defaultSessionName) const choice = await dialog.showSaveDialog({ diff --git a/products/jbrowse-desktop/src/JBrowse.tsx b/products/jbrowse-desktop/src/JBrowse.tsx index 921b4d296b..8756a87bcb 100644 --- a/products/jbrowse-desktop/src/JBrowse.tsx +++ b/products/jbrowse-desktop/src/JBrowse.tsx @@ -10,7 +10,7 @@ import { AssemblyManager } from '@jbrowse/plugin-data-management' import { RootModel } from './rootModel' const JBrowse = observer( - ({ pluginManager }: { pluginManager: PluginManager }) => { + ({ pluginManager }: { pluginManager: PluginManager; }) => { const { rootModel } = pluginManager return rootModel ? ( diff --git a/products/jbrowse-desktop/src/Loader.tsx b/products/jbrowse-desktop/src/Loader.tsx index 48c02b401c..b84d472bc4 100644 --- a/products/jbrowse-desktop/src/Loader.tsx +++ b/products/jbrowse-desktop/src/Loader.tsx @@ -30,7 +30,7 @@ const ErrorMessage = ({ error, snapshotError, }: { - error: Error + error: unknown snapshotError?: string }) => { const classes = useStyles() @@ -52,7 +52,7 @@ const ErrorMessage = ({ const Loader = observer(() => { const [pluginManager, setPluginManager] = useState() const [config, setConfig] = useQueryParam('config', StringParam) - const [error, setError] = useState() + const [error, setError] = useState() const [snapshotError, setSnapshotError] = useState('') function handleError(e: unknown) { @@ -74,6 +74,13 @@ const Loader = observer(() => { const handleSetPluginManager = useCallback( (pm: PluginManager) => { + // @ts-ignore + pm.rootModel?.setOpenNewSessionCallback(async () => { + const path = await ipcRenderer.invoke('promptOpenFile') + const data = await ipcRenderer.invoke('loadSession', path) + const pm = await createPluginManager(data) + handleSetPluginManager(pm) + }) setPluginManager(pm) setError(undefined) setSnapshotError('') diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 701b377bd0..3ad067cf27 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -79,6 +79,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { .volatile(() => ({ error: undefined as unknown, textSearchManager: new TextSearchManager(pluginManager), + openNewSessionCallback: () => { console.error('openNewSessionCallback unimplemented') } })) .actions(self => ({ async saveSession(val: unknown) { @@ -86,6 +87,9 @@ export default function rootModelFactory(pluginManager: PluginManager) { await ipcRenderer.invoke('saveSession', self.sessionPath, val) } }, + setOpenNewSessionCallback(cb: () => Promise) { + self.openNewSessionCallback = cb + }, setSavedSessionNames(sessionNames: string[]) { self.savedSessionNames = cast(sessionNames) }, @@ -230,7 +234,14 @@ export default function rootModelFactory(pluginManager: PluginManager) { { label: 'Open', icon: OpenIcon, - onClick: () => {}, + onClick: async () => { + try { + await self.openNewSessionCallback() + } catch(e) { + console.error(e) + self.session?.notify(`${e}`, 'error') + } + }, }, { label: 'Save', From 204743acbc1707c335469d0c135005d7ca8baff3 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 15:33:47 -0400 Subject: [PATCH 21/23] Create autosave files for launcher panel entries --- products/jbrowse-desktop/public/electron.ts | 57 +++++++++++++++++-- products/jbrowse-desktop/src/JBrowse.tsx | 2 +- products/jbrowse-desktop/src/Loader.tsx | 12 ++-- .../src/StartScreen/LauncherPanel.tsx | 18 +++--- .../src/StartScreen/QuickstartPanel.tsx | 13 +++-- .../src/StartScreen/RecentSessionsPanel.tsx | 38 +++++++++---- .../jbrowse-desktop/src/StartScreen/util.tsx | 13 ++++- products/jbrowse-desktop/src/rootModel.ts | 15 +++-- 8 files changed, 126 insertions(+), 42 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 2e7d130aa4..5ab4e87a2c 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -51,11 +51,16 @@ const defaultSessionName = `jbrowse_session.json` const userData = app.getPath('userData') const recentSessionsPath = path.join(userData, 'recent_sessions.json') const quickstartDir = path.join(userData, 'quickstart') +const autosaveDir = path.join(userData, 'autosaved') function getQuickstartPath(sessionName: string, ext = 'json') { return path.join(quickstartDir, `${encodeURIComponent(sessionName)}.${ext}`) } +function getAutosavePath(sessionName: string, ext = 'json') { + return path.join(autosaveDir, `${encodeURIComponent(sessionName)}.${ext}`) +} + if (!fs.existsSync(recentSessionsPath)) { fs.writeFileSync(recentSessionsPath, stringify([]), 'utf8') } @@ -64,6 +69,10 @@ if (!fs.existsSync(quickstartDir)) { fs.mkdirSync(quickstartDir, { recursive: true }) } +if (!fs.existsSync(autosaveDir)) { + fs.mkdirSync(autosaveDir, { recursive: true }) +} + interface SessionSnap { defaultSession: { name: string @@ -268,9 +277,20 @@ ipcMain.handle('quit', () => { app.quit() }) -ipcMain.handle('listSessions', async () => { - return JSON.parse(await readFile(recentSessionsPath, 'utf8')) -}) +ipcMain.handle( + 'listSessions', + async (_event: unknown, showAutosaves: boolean) => { + const sessions = JSON.parse(await readFile(recentSessionsPath, 'utf8')) as { + path: string + }[] + + if (!showAutosaves) { + return sessions.filter(f => !f.path.startsWith(autosaveDir)) + } else { + return sessions + } + }, +) ipcMain.handle('loadExternalConfig', (_event: unknown, sessionPath) => { return readFile(sessionPath, 'utf8') @@ -341,6 +361,36 @@ ipcMain.handle( }) }, ) + +// creates an initial entry in autosave folder +ipcMain.handle( + 'createInitialAutosaveFile', + async (_event: unknown, snap: SessionSnap) => { + const rows = JSON.parse(fs.readFileSync(recentSessionsPath, 'utf8')) as [ + { path: string; updated: number }, + ] + const idx = rows.findIndex(r => r.path === path) + const path = getAutosavePath(`${+Date.now()}`) + const entry = { + path, + updated: +Date.now(), + name: snap.defaultSession?.name, + } + if (idx === -1) { + rows.unshift(entry) + } else { + rows[idx] = entry + } + await Promise.all([ + writeFile(recentSessionsPath, stringify(rows)), + writeFile(path, stringify(snap)), + ]) + + return path + }, +) + +// snapshots page and saves to path ipcMain.handle( 'saveSession', async (_event: unknown, path: string, snap: SessionSnap) => { @@ -368,7 +418,6 @@ ipcMain.handle( }, ) - ipcMain.handle('promptOpenFile', async (_event: unknown) => { const toLocalPath = path.join(app.getPath('desktop'), defaultSessionName) const choice = await dialog.showOpenDialog({ diff --git a/products/jbrowse-desktop/src/JBrowse.tsx b/products/jbrowse-desktop/src/JBrowse.tsx index 8756a87bcb..921b4d296b 100644 --- a/products/jbrowse-desktop/src/JBrowse.tsx +++ b/products/jbrowse-desktop/src/JBrowse.tsx @@ -10,7 +10,7 @@ import { AssemblyManager } from '@jbrowse/plugin-data-management' import { RootModel } from './rootModel' const JBrowse = observer( - ({ pluginManager }: { pluginManager: PluginManager; }) => { + ({ pluginManager }: { pluginManager: PluginManager }) => { const { rootModel } = pluginManager return rootModel ? ( diff --git a/products/jbrowse-desktop/src/Loader.tsx b/products/jbrowse-desktop/src/Loader.tsx index b84d472bc4..ad6d6ae607 100644 --- a/products/jbrowse-desktop/src/Loader.tsx +++ b/products/jbrowse-desktop/src/Loader.tsx @@ -5,7 +5,7 @@ import { CssBaseline, ThemeProvider, makeStyles } from '@material-ui/core' import { createJBrowseTheme } from '@jbrowse/core/ui' import { StringParam, useQueryParam } from 'use-query-params' import { ipcRenderer } from 'electron' -import { createPluginManager } from './StartScreen/util' +import { loadPluginManager } from './StartScreen/util' import JBrowse from './JBrowse' import StartScreen from './StartScreen' @@ -77,10 +77,10 @@ const Loader = observer(() => { // @ts-ignore pm.rootModel?.setOpenNewSessionCallback(async () => { const path = await ipcRenderer.invoke('promptOpenFile') - const data = await ipcRenderer.invoke('loadSession', path) - const pm = await createPluginManager(data) - handleSetPluginManager(pm) + handleSetPluginManager(await loadPluginManager(path)) }) + + // @ts-ignore setPluginManager(pm) setError(undefined) setSnapshotError('') @@ -93,9 +93,7 @@ const Loader = observer(() => { ;(async () => { if (config) { try { - const data = await ipcRenderer.invoke('loadSession', config) - const pm = await createPluginManager(data) - handleSetPluginManager(pm) + handleSetPluginManager(await loadPluginManager(config)) } catch (e) { handleError(e) } diff --git a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx index af7b37feca..2a64634396 100644 --- a/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/LauncherPanel.tsx @@ -1,9 +1,10 @@ import React, { useState } from 'react' import { Button, Typography, makeStyles } from '@material-ui/core' import PluginManager from '@jbrowse/core/PluginManager' +import { ipcRenderer } from 'electron' import QuickstartPanel from './QuickstartPanel' import OpenSequenceDialog from '../OpenSequenceDialog' -import { createPluginManager } from './util' +import { loadPluginManager } from './util' const useStyles = makeStyles(theme => ({ form: { @@ -50,13 +51,16 @@ export default function StartScreenOptionsPanel({ if (conf) { // note this can throw before dialog closes, but this is handled // by the dialog itself - const pm = await createPluginManager({ - assemblies: [conf], - defaultSession: { - name: 'New Session ' + new Date().toLocaleString('en-US'), + const path = await ipcRenderer.invoke( + 'createInitialAutosaveFile', + { + assemblies: [conf], + defaultSession: { + name: 'New Session ' + new Date().toLocaleString('en-US'), + }, }, - }) - setPluginManager(pm) + ) + setPluginManager(await loadPluginManager(path)) } setSequenceDialogOpen(false) }} diff --git a/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx b/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx index 6f3468836b..eb5a5fa2aa 100644 --- a/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/QuickstartPanel.tsx @@ -22,7 +22,7 @@ import { ipcRenderer } from 'electron' import deepmerge from 'deepmerge' // locals -import { createPluginManager } from './util' +import { loadPluginManager } from './util' const useStyles = makeStyles(theme => ({ button: { @@ -43,7 +43,7 @@ const useStyles = makeStyles(theme => ({ }, })) -function PreloadedDatasetSelector({ +function QuickstartPanel({ setPluginManager, }: { setPluginManager: (arg0: PluginManager) => void @@ -111,8 +111,11 @@ function PreloadedDatasetSelector({ config.defaultSession.name = `New session ${new Date().toLocaleString( 'en-US', )}` - const pm = await createPluginManager(config) - setPluginManager(pm) + const path = await ipcRenderer.invoke( + 'createInitialAutosaveFile', + config, + ) + setPluginManager(await loadPluginManager(path)) }} variant="contained" color="primary" @@ -211,4 +214,4 @@ function PreloadedDatasetSelector({ ) } -export default PreloadedDatasetSelector +export default QuickstartPanel diff --git a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx index 21c880c0f3..d3f78fa2c7 100644 --- a/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx +++ b/products/jbrowse-desktop/src/StartScreen/RecentSessionsPanel.tsx @@ -1,7 +1,9 @@ import React, { useState, useEffect, useMemo } from 'react' import { + Checkbox, CircularProgress, FormControl, + FormControlLabel, Grid, IconButton, Link, @@ -29,7 +31,7 @@ import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd' // locals import RenameSessionDialog from './dialogs/RenameSessionDialog' import DeleteSessionDialog from './dialogs/DeleteSessionDialog' -import { useLocalStorage, createPluginManager } from './util' +import { useLocalStorage, loadPluginManager } from './util' import SessionCard from './SessionCard' const useStyles = makeStyles(theme => ({ @@ -99,12 +101,7 @@ function RecentSessionsList({ className={classes.pointer} onClick={async () => { try { - const data = await ipcRenderer.invoke( - 'loadSession', - params.row.path, - ) - const pm = await createPluginManager(data) - setPluginManager(pm) + setPluginManager(await loadPluginManager(params.row.path)) } catch (e) { console.error(e) setError(e) @@ -197,9 +194,7 @@ function RecentSessionsCards({ sessionData={session} onClick={async () => { try { - const pm = await createPluginManager( - await ipcRenderer.invoke('loadSession', session.path), - ) + const pm = await loadPluginManager(session.path) setPluginManager(pm) } catch (e) { console.error(e) @@ -242,6 +237,10 @@ export default function RecentSessionPanel({ const [updateSessionsList, setUpdateSessionsList] = useState(0) const [selectedSessions, setSelectedSessions] = useState() const [sessionsToDelete, setSessionsToDelete] = useState() + const [showAutosaves, setShowAutosaves] = useLocalStorage( + 'showAutosaves', + 'false', + ) const sortedSessions = useMemo( () => sessions?.sort((a, b) => b.updated - a.updated), @@ -251,14 +250,17 @@ export default function RecentSessionPanel({ useEffect(() => { ;(async () => { try { - const sessions = await ipcRenderer.invoke('listSessions') + const sessions = await ipcRenderer.invoke( + 'listSessions', + showAutosaves === 'true', + ) setSessions(sessions) } catch (e) { console.error(e) setError(e) } })() - }, [setError, updateSessionsList]) + }, [setError, updateSessionsList, showAutosaves]) if (!sessions) { return ( @@ -338,6 +340,18 @@ export default function RecentSessionPanel({ + + setShowAutosaves(showAutosaves === 'true' ? 'false' : 'true') + } + /> + } + label="Show autosaves" + /> + {sortedSessions.length ? ( displayMode === 'grid' ? ( o.internetAccountId) + // remove duplicates while mixing in default internet accounts jbrowse.internetAccounts = jbrowse.internetAccounts.filter( - ({ internetAccountId }, index) => - !ids.includes(internetAccountId, index + 1), + (arg, index) => !ids.includes(arg.internetAccountId, index + 1), ) const rootModel = JBrowseRootModel.create( diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 3ad067cf27..7d6da941ec 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -79,7 +79,9 @@ export default function rootModelFactory(pluginManager: PluginManager) { .volatile(() => ({ error: undefined as unknown, textSearchManager: new TextSearchManager(pluginManager), - openNewSessionCallback: () => { console.error('openNewSessionCallback unimplemented') } + openNewSessionCallback: () => { + console.error('openNewSessionCallback unimplemented') + }, })) .actions(self => ({ async saveSession(val: unknown) { @@ -237,7 +239,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { onClick: async () => { try { await self.openNewSessionCallback() - } catch(e) { + } catch (e) { console.error(e) self.session?.notify(`${e}`, 'error') } @@ -392,8 +394,13 @@ export default function rootModelFactory(pluginManager: PluginManager) { } const url = window.location.href.split('?')[0] - const name = self.session?.name || '' - window.location.href = `${url}?config=${encodeURIComponent(name)}` + if (!self.sessionPath) { + self.session?.notify('You must save your session first') + } else { + window.location.href = `${url}?config=${encodeURIComponent( + self.sessionPath, + )}` + } }, /** * Add a top-level menu From 69203ccbecf83af2d833c7a5628874448f49450e Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 20:14:40 -0400 Subject: [PATCH 22/23] Increase size of thumbnail --- products/jbrowse-desktop/public/electron.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 5ab4e87a2c..2853105a07 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -399,7 +399,7 @@ ipcMain.handle( { path: string; updated: number }, ] const idx = rows.findIndex(r => r.path === path) - const screenshot = page?.resize({ width: 250 }).toDataURL() + const screenshot = page?.resize({ width: 500 }).toDataURL() const entry = { path, screenshot, From f504fff6f22e14e04250fc8ae8c7bd7c843ce2f8 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 12 Oct 2021 20:45:22 -0400 Subject: [PATCH 23/23] Write thumbnail file externally --- products/jbrowse-desktop/public/electron.ts | 21 +++++++++- .../src/StartScreen/SessionCard.tsx | 38 ++++++++++++++----- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/products/jbrowse-desktop/public/electron.ts b/products/jbrowse-desktop/public/electron.ts index 2853105a07..c4babbef9d 100644 --- a/products/jbrowse-desktop/public/electron.ts +++ b/products/jbrowse-desktop/public/electron.ts @@ -51,12 +51,17 @@ const defaultSessionName = `jbrowse_session.json` const userData = app.getPath('userData') const recentSessionsPath = path.join(userData, 'recent_sessions.json') const quickstartDir = path.join(userData, 'quickstart') +const thumbnailDir = path.join(userData, 'thumbnails') const autosaveDir = path.join(userData, 'autosaved') function getQuickstartPath(sessionName: string, ext = 'json') { return path.join(quickstartDir, `${encodeURIComponent(sessionName)}.${ext}`) } +function getThumbnailPath(name: string) { + return path.join(thumbnailDir, `${encodeURIComponent(name)}.data`) +} + function getAutosavePath(sessionName: string, ext = 'json') { return path.join(autosaveDir, `${encodeURIComponent(sessionName)}.${ext}`) } @@ -69,6 +74,10 @@ if (!fs.existsSync(quickstartDir)) { fs.mkdirSync(quickstartDir, { recursive: true }) } +if (!fs.existsSync(thumbnailDir)) { + fs.mkdirSync(thumbnailDir, { recursive: true }) +} + if (!fs.existsSync(autosaveDir)) { fs.mkdirSync(autosaveDir, { recursive: true }) } @@ -399,10 +408,9 @@ ipcMain.handle( { path: string; updated: number }, ] const idx = rows.findIndex(r => r.path === path) - const screenshot = page?.resize({ width: 500 }).toDataURL() + const png = page?.resize({ width: 500 }).toDataURL() const entry = { path, - screenshot, updated: +Date.now(), name: snap.defaultSession?.name, } @@ -412,12 +420,21 @@ ipcMain.handle( rows[idx] = entry } await Promise.all([ + ...(png ? [writeFile(getThumbnailPath(path), png)] : []), writeFile(recentSessionsPath, stringify(rows)), writeFile(path, stringify(snap)), ]) }, ) +ipcMain.handle('loadThumbnail', (_event: unknown, name: string) => { + const path = getThumbnailPath(name) + if (fs.existsSync(path)) { + return readFile(path, 'utf8') + } + return undefined +}) + ipcMain.handle('promptOpenFile', async (_event: unknown) => { const toLocalPath = path.join(app.getPath('desktop'), defaultSessionName) const choice = await dialog.showOpenDialog({ diff --git a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx index 5bcead5fb0..fbd381c8a5 100644 --- a/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx +++ b/products/jbrowse-desktop/src/StartScreen/SessionCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Card, CardHeader, @@ -11,6 +11,7 @@ import { Typography, makeStyles, } from '@material-ui/core' +import { ipcRenderer } from 'electron' import PlaylistAddIcon from '@material-ui/icons/PlaylistAdd' import DeleteIcon from '@material-ui/icons/Delete' @@ -31,8 +32,8 @@ const useStyles = makeStyles({ interface RecentSessionData { path: string name: string - screenshot?: string updated: number + screenshotPath?: string } function RecentSessionCard({ @@ -51,7 +52,24 @@ function RecentSessionCard({ const classes = useStyles() const [hovered, setHovered] = useState(false) const [menuAnchorEl, setMenuAnchorEl] = useState(null) - const sessionName = sessionData.name + const [screenshot, setScreenshot] = useState() + const { name, path } = sessionData + + useEffect(() => { + ;(async () => { + try { + const data = await ipcRenderer.invoke('loadThumbnail', path) + if (data) { + setScreenshot(data) + } else { + setScreenshot(defaultSessionScreenshot) + } + } catch (e) { + console.error(e) + setScreenshot(defaultSessionScreenshot) + } + })() + }, [path]) return ( <> @@ -62,10 +80,12 @@ function RecentSessionCard({ onClick={() => onClick(sessionData)} raised={Boolean(hovered)} > - + {screenshot ? ( + + ) : null} + - {sessionName} + {name} }