From 7de3662ece3a67d1d0744c7eb8f71c54b4e2b6f7 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 24 Apr 2023 12:02:43 -0700 Subject: [PATCH 01/44] wip --- .../models/BaseConnectionModelFactory.ts | 11 +- packages/core/util/types/index.ts | 1 - packages/product-core/package.json | 56 +++ packages/product-core/src/RootModel/index.ts | 10 + .../product-core/src/Session/Assemblies.ts | 0 .../product-core/src/Session/Connections.ts | 148 +++++++ .../src/Session/ReferenceManagement.ts | 105 +++++ packages/product-core/src/Session/index.ts | 2 + packages/product-core/src/index.ts | 1 + packages/product-core/tsconfig.build.es5.json | 14 + packages/product-core/tsconfig.build.esm.json | 14 + packages/product-core/tsconfig.json | 7 + products/jbrowse-web/package.json | 1 + .../jbrowse-web/src/sessionModelFactory.ts | 366 ++++-------------- 14 files changed, 445 insertions(+), 291 deletions(-) create mode 100644 packages/product-core/package.json create mode 100644 packages/product-core/src/RootModel/index.ts create mode 100644 packages/product-core/src/Session/Assemblies.ts create mode 100644 packages/product-core/src/Session/Connections.ts create mode 100644 packages/product-core/src/Session/ReferenceManagement.ts create mode 100644 packages/product-core/src/Session/index.ts create mode 100644 packages/product-core/src/index.ts create mode 100644 packages/product-core/tsconfig.build.es5.json create mode 100644 packages/product-core/tsconfig.build.esm.json create mode 100644 packages/product-core/tsconfig.json diff --git a/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts b/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts index d40c3c1702..a4909a5a4e 100644 --- a/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts +++ b/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts @@ -1,7 +1,11 @@ import { cast, types } from 'mobx-state-tree' -import { AnyConfigurationModel } from '../../configuration' +import { + AnyConfigurationModel, + ConfigurationReference, +} from '../../configuration' import PluginManager from '../../PluginManager' +import configSchema from './baseConnectionConfig' /** * #stateModel BaseConnectionModel */ @@ -16,6 +20,11 @@ function stateModelFactory(pluginManager: PluginManager) { * #property */ tracks: types.array(pluginManager.pluggableConfigSchemaType('track')), + + /** + * #property + */ + configuration: ConfigurationReference(configSchema), }) .actions(self => ({ afterAttach() { diff --git a/packages/core/util/types/index.ts b/packages/core/util/types/index.ts index c492021ca3..c270490301 100644 --- a/packages/core/util/types/index.ts +++ b/packages/core/util/types/index.ts @@ -103,7 +103,6 @@ export interface AbstractSessionModel extends AbstractViewContainer { sessionConnections?: AnyConfigurationModel[] connectionInstances?: { name: string - connectionId: string tracks: AnyConfigurationModel[] configuration: AnyConfigurationModel }[] diff --git a/packages/product-core/package.json b/packages/product-core/package.json new file mode 100644 index 0000000000..1d6ee396be --- /dev/null +++ b/packages/product-core/package.json @@ -0,0 +1,56 @@ +{ + "name": "@jbrowse/product-core", + "version": "2.4.2", + "description": "JBrowse 2 code shared between products but not used by plugins", + "keywords": [ + "jbrowse", + "jbrowse2", + "bionode", + "biojs", + "genomics" + ], + "license": "Apache-2.0", + "homepage": "https://jbrowse.org", + "bugs": "https://github.com/GMOD/jbrowse-components/issues", + "repository": { + "type": "git", + "url": "https://github.com/GMOD/jbrowse-components.git", + "directory": "packages/text-indexing" + }, + "author": "JBrowse Team", + "distMain": "dist/index.js", + "distModule": "esm/index.js", + "srcMain": "src/index.ts", + "srcModule": "src/index.ts", + "main": "src/index.ts", + "module": "", + "files": [ + "dist", + "esm", + "src" + ], + "scripts": { + "build:esm": "tsc --build tsconfig.build.esm.json", + "build:es5": "tsc --build tsconfig.build.es5.json", + "build": "npm run build:esm && npm run build:es5", + "test": "cd ../..; jest packages/text-indexing", + "clean": "rimraf dist esm *.tsbuildinfo", + "prebuild": "yarn clean", + "prepack": "yarn build && yarn useDist", + "postpack": "yarn useSrc", + "useDist": "node ../../scripts/useDist.js", + "useSrc": "node ../../scripts/useSrc.js" + }, + "dependencies": { + "@babel/runtime": "^7.16.3" + }, + "peerDependencies": { + "mobx": "^6.0.0", + "mobx-react": "^7.0.0", + "mobx-state-tree": "^5.0.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "rxjs": "^7.0.0" + }, + "private": true +} diff --git a/packages/product-core/src/RootModel/index.ts b/packages/product-core/src/RootModel/index.ts new file mode 100644 index 0000000000..72dde1023b --- /dev/null +++ b/packages/product-core/src/RootModel/index.ts @@ -0,0 +1,10 @@ +import assemblyManagerFactory from '@jbrowse/core/assemblyManager' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { AbstractSessionModel } from '@jbrowse/core/util' + +export interface RootModel { + jbrowse: AnyConfigurationModel + session: AbstractSessionModel + assemblyManager: ReturnType + version: string +} diff --git a/packages/product-core/src/Session/Assemblies.ts b/packages/product-core/src/Session/Assemblies.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts new file mode 100644 index 0000000000..18c20595e5 --- /dev/null +++ b/packages/product-core/src/Session/Connections.ts @@ -0,0 +1,148 @@ +/** MST props, views, actions, etc related to managing connections */ + +import PluginManager from '@jbrowse/core/PluginManager' +import { + AnyConfigurationModel, + readConfObject, +} from '@jbrowse/core/configuration' +import { getParent, types } from 'mobx-state-tree' +import ReferenceManagement from './ReferenceManagement' +import { RootModel } from '../RootModel' +import type baseConnectionConfig from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' +import { BaseConnectionModelFactory } from '@jbrowse/core/pluggableElementTypes' + +export default function Connections(pluginManager: PluginManager) { + // connections: AnyConfigurationModel[] + // deleteConnection?: Function + // sessionConnections?: AnyConfigurationModel[] + // connectionInstances?: { + // name: string + // connectionId: string + // tracks: AnyConfigurationModel[] + // configuration: AnyConfigurationModel + // }[] + // makeConnection?: Function + + return types + .compose( + 'ConnectionsManagementSessionMixin', + ReferenceManagement(pluginManager), + types.model({ + /** + * #property + */ + connectionInstances: types.array( + pluginManager.pluggableMstType( + 'connection', + 'stateModel', + ) as ReturnType, + ), + + /** + * #property + */ + sessionConnections: types.array( + pluginManager.pluggableConfigSchemaType('connection'), + ), + }), + ) + .views(self => ({ + /** + * #getter + */ + get connections(): AnyConfigurationModel[] { + return [...self.sessionConnections, ...self.jbrowse.connections] + }, + })) + .actions(self => ({ + /** + * #action + */ + makeConnection( + configuration: AnyConfigurationModel, + initialSnapshot = {}, + ) { + const { type } = configuration + if (!type) { + throw new Error('track configuration has no `type` listed') + } + const name = readConfObject(configuration, 'name') + const connectionType = pluginManager.getConnectionType(type) + if (!connectionType) { + throw new Error(`unknown connection type ${type}`) + } + const connectionData = { + ...initialSnapshot, + name, + type, + configuration, + } + const length = self.connectionInstances.push(connectionData) + return self.connectionInstances[length - 1] + }, + + /** + * #action + */ + prepareToBreakConnection(configuration: AnyConfigurationModel) { + const callbacksToDereferenceTrack: Function[] = [] + const dereferenceTypeCount: Record = {} + const name = readConfObject(configuration, 'name') + const connection = self.connectionInstances.find(c => c.name === name) + if (connection) { + connection.tracks.forEach(track => { + const referring = self.getReferring(track) + self.removeReferring( + referring, + track, + callbacksToDereferenceTrack, + dereferenceTypeCount, + ) + }) + const safelyBreakConnection = () => { + callbacksToDereferenceTrack.forEach(cb => cb()) + this.breakConnection(configuration) + } + return [safelyBreakConnection, dereferenceTypeCount] + } + return undefined + }, + + /** + * #action + */ + breakConnection(configuration: AnyConfigurationModel) { + const name = readConfObject(configuration, 'name') + const connection = self.connectionInstances.find(c => c.name === name) + if (!connection) { + throw new Error(`no connection found with name ${name}`) + } + self.connectionInstances.remove(connection) + }, + + /** + * #action + */ + deleteConnection(configuration: AnyConfigurationModel) { + return getParent(self).jbrowse.deleteConnectionConf( + configuration, + ) + }, + + /** + * #action + */ + addConnectionConf(connectionConf: typeof baseConnectionConfig) { + return getParent(self).jbrowse.addConnectionConf( + connectionConf, + ) + }, + + /** + * #action + */ + clearConnections() { + self.connectionInstances.length = 0 + }, + })) +} diff --git a/packages/product-core/src/Session/ReferenceManagement.ts b/packages/product-core/src/Session/ReferenceManagement.ts new file mode 100644 index 0000000000..4c723c30a7 --- /dev/null +++ b/packages/product-core/src/Session/ReferenceManagement.ts @@ -0,0 +1,105 @@ +/** MST props, views, actions, etc related to managing connections */ + +import PluginManager from '@jbrowse/core/PluginManager' +import { + SessionWithWidgets, + TrackViewModel, + getContainingView, +} from '@jbrowse/core/util' +import { + IAnyStateTreeNode, + getMembers, + getParent, + getSnapshot, + getType, + isModelType, + isReferenceType, + types, + walk, +} from 'mobx-state-tree' + +export interface ReferringNode { + node: IAnyStateTreeNode + key: string +} + +export default function ReferenceManagement(pluginManager: PluginManager) { + return types + .model('ReferenceManagementSessionMixin', {}) + .views(self => ({ + /** + * #method + * See if any MST nodes currently have a types.reference to this object. + * + * @param object - object + * @returns An array where the first element is the node referring + * to the object and the second element is they property name the node is + * using to refer to the object + */ + getReferring(object: IAnyStateTreeNode) { + const refs: ReferringNode[] = [] + walk(getParent(self), node => { + if (isModelType(getType(node))) { + const members = getMembers(node) + Object.entries(members.properties).forEach(([key, value]) => { + if (isReferenceType(value) && node[key] === object) { + refs.push({ node, key }) + } + }) + } + }) + return refs + }, + })) + .actions(self => ({ + /** + * #action + */ + removeReferring( + referring: { node: ReferringNode }[], + track: unknown, + callbacks: Function[], + dereferenceTypeCount: Record, + ) { + referring.forEach(({ node }) => { + let dereferenced = false + try { + // If a view is referring to the track config, remove the track + // from the view + const type = 'open track(s)' + const view = getContainingView(node) as TrackViewModel + callbacks.push(() => view.hideTrack(track.trackId)) + dereferenced = true + if (!dereferenceTypeCount[type]) { + dereferenceTypeCount[type] = 0 + } + dereferenceTypeCount[type] += 1 + } catch (err1) { + // ignore + } + + // @ts-ignore + if (self.widgets.has(node.id)) { + // If a configuration editor widget has the track config + // open, close the widget + const type = 'configuration editor widget(s)' + callbacks.push(() => + (self as unknown as SessionWithWidgets).hideWidget(node), + ) + dereferenced = true + if (!dereferenceTypeCount[type]) { + dereferenceTypeCount[type] = 0 + } + dereferenceTypeCount[type] += 1 + } + if (!dereferenced) { + throw new Error( + `Error when closing this connection, the following node is still referring to a track configuration: ${JSON.stringify( + getSnapshot(node), + )}`, + ) + } + }) + }, + })) +} diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts new file mode 100644 index 0000000000..00a098bfee --- /dev/null +++ b/packages/product-core/src/Session/index.ts @@ -0,0 +1,2 @@ +export { default as ReferenceManagement } from './ReferenceManagement' +export { default as Connections } from './Connections' diff --git a/packages/product-core/src/index.ts b/packages/product-core/src/index.ts new file mode 100644 index 0000000000..247733160f --- /dev/null +++ b/packages/product-core/src/index.ts @@ -0,0 +1 @@ +export * as Session from './Session' diff --git a/packages/product-core/tsconfig.build.es5.json b/packages/product-core/tsconfig.build.es5.json new file mode 100644 index 0000000000..2160c329a4 --- /dev/null +++ b/packages/product-core/tsconfig.build.es5.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "declaration": true, + "noEmit": false, + "outDir": "dist", + "rootDir": "./src", + "target": "es5", + "composite": true + }, + "include": ["./src/**/*.ts*", "./src/**/*.js*"], + "exclude": ["src/**/*.test.ts*", "src/**/*.test.js*"], + "references": [{ "path": "../core/tsconfig.build.json" }] +} diff --git a/packages/product-core/tsconfig.build.esm.json b/packages/product-core/tsconfig.build.esm.json new file mode 100644 index 0000000000..e84e2fab73 --- /dev/null +++ b/packages/product-core/tsconfig.build.esm.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "declaration": true, + "noEmit": false, + "outDir": "esm", + "rootDir": "./src", + "target": "es2018", + "composite": true + }, + "include": ["./src/**/*.ts*", "./src/**/*.js*"], + "exclude": ["src/**/*.test.ts*", "src/**/*.test.js*"], + "references": [{ "path": "../core/tsconfig.build.json" }] +} diff --git a/packages/product-core/tsconfig.json b/packages/product-core/tsconfig.json new file mode 100644 index 0000000000..2e51998c84 --- /dev/null +++ b/packages/product-core/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true + } +} diff --git a/products/jbrowse-web/package.json b/products/jbrowse-web/package.json index 7347746520..3e8fbc5d25 100644 --- a/products/jbrowse-web/package.json +++ b/products/jbrowse-web/package.json @@ -47,6 +47,7 @@ "@jbrowse/plugin-trix": "^2.5.0", "@jbrowse/plugin-variants": "^2.5.0", "@jbrowse/plugin-wiggle": "^2.5.0", + "@jbrowse/product-core": "^2.5.0", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.10.17", "@mui/x-data-grid": "^6.0.1", diff --git a/products/jbrowse-web/src/sessionModelFactory.ts b/products/jbrowse-web/src/sessionModelFactory.ts index 6fe9b80733..67b12b1b80 100644 --- a/products/jbrowse-web/src/sessionModelFactory.ts +++ b/products/jbrowse-web/src/sessionModelFactory.ts @@ -13,32 +13,21 @@ import { import { Region, AbstractSessionModel, - TrackViewModel, JBrowsePlugin, DialogComponentType, } from '@jbrowse/core/util/types' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' import { ThemeOptions } from '@mui/material' -import { - getContainingView, - localStorageGetItem, - localStorageSetItem, -} from '@jbrowse/core/util' +import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' import { autorun, observable } from 'mobx' import { addDisposer, cast, - getMembers, getParent, getRoot, getSnapshot, - getType, isAlive, - isModelType, - isReferenceType, types, - walk, - IAnyStateTreeNode, Instance, SnapshotIn, SnapshotOut, @@ -47,6 +36,8 @@ import PluginManager from '@jbrowse/core/PluginManager' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' +import { Session } from '@jbrowse/product-core' + // icons import SettingsIcon from '@mui/icons-material/Settings' import CopyIcon from '@mui/icons-material/FileCopy' @@ -55,11 +46,6 @@ import InfoIcon from '@mui/icons-material/Info' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) -export declare interface ReferringNode { - node: IAnyStateTreeNode - key: string -} - export declare interface ReactProps { [key: string]: any } @@ -80,87 +66,81 @@ export default function sessionModelFactory( ) { const minDrawerWidth = 128 const sessionModel = types - .model('JBrowseWebSessionModel', { - /** - * #property - */ - id: types.optional(types.identifier, shortid()), - /** - * #property - */ - name: types.string, - /** - * #property - */ - margin: 0, - /** - * #property - */ - drawerWidth: types.optional( - types.refinement(types.integer, width => width >= minDrawerWidth), - 384, - ), - /** - * #property - */ - views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), - /** - * #property - */ - widgets: types.map( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( + .compose( + types.model({ + /** + * #property + */ + id: types.optional(types.identifier, shortid()), + /** + * #property + */ + name: types.string, + /** + * #property + */ + margin: 0, + /** + * #property + */ + drawerWidth: types.optional( + types.refinement(types.integer, width => width >= minDrawerWidth), + 384, + ), + /** + * #property + */ + views: types.array( + pluginManager.pluggableMstType('view', 'stateModel'), + ), + /** + * #property + */ + widgets: types.map( pluginManager.pluggableMstType('widget', 'stateModel'), ), - ), - /** - * #property - */ - connectionInstances: types.array( - pluginManager.pluggableMstType('connection', 'stateModel'), - ), - /** - * #property - */ - sessionTracks: types.array( - pluginManager.pluggableConfigSchemaType('track'), - ), - /** - * #property - */ - sessionConnections: types.array( - pluginManager.pluggableConfigSchemaType('connection'), - ), - /** - * #property - */ - sessionAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - temporaryAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - sessionPlugins: types.array(types.frozen()), - /** - * #property - */ - minimized: types.optional(types.boolean, false), - - /** - * #property - */ - drawerPosition: types.optional( - types.string, - () => localStorageGetItem('drawerPosition') || 'right', - ), - }) + /** + * #property + */ + activeWidgets: types.map( + types.safeReference( + pluginManager.pluggableMstType('widget', 'stateModel'), + ), + ), + /** + * #property + */ + sessionTracks: types.array( + pluginManager.pluggableConfigSchemaType('track'), + ), + /** + * #property + */ + sessionAssemblies: types.array(assemblyConfigSchemasType), + /** + * #property + */ + temporaryAssemblies: types.array(assemblyConfigSchemasType), + /** + * #property + */ + sessionPlugins: types.array(types.frozen()), + /** + * #property + */ + minimized: types.optional(types.boolean, false), + + /** + * #property + */ + drawerPosition: types.optional( + types.string, + () => localStorageGetItem('drawerPosition') || 'right', + ), + }), + Session.Connections(pluginManager), + ) + .named('JBrowseWebSessionModel') .volatile((/* self */) => ({ /** * #volatile @@ -291,12 +271,6 @@ export default function sessionModelFactory( get textSearchManager(): TextSearchManager { return getParent(self).textSearchManager }, - /** - * #getter - */ - get connections(): AnyConfigurationModel[] { - return [...self.sessionConnections, ...self.jbrowse.connections] - }, /** * #getter */ @@ -365,30 +339,6 @@ export default function sessionModelFactory( } return undefined }, - /** - * #method - * See if any MST nodes currently have a types.reference to this object. - * - * @param object - object - * @returns An array where the first element is the node referring - * to the object and the second element is they property name the node is - * using to refer to the object - */ - getReferring(object: IAnyStateTreeNode) { - const refs: ReferringNode[] = [] - walk(getParent(self), node => { - if (isModelType(getType(node))) { - const members = getMembers(node) - Object.entries(members.properties).forEach(([key, value]) => { - // @ts-ignore - if (isReferenceType(value) && node[key] === object) { - refs.push({ node, key }) - } - }) - } - }) - return refs - }, })) .actions(self => ({ /** @@ -522,137 +472,6 @@ export default function sessionModelFactory( const rootModel = getParent(self) rootModel.setPluginsUpdated(true) }, - /** - * #action - */ - makeConnection( - configuration: AnyConfigurationModel, - initialSnapshot = {}, - ) { - const { type } = configuration - if (!type) { - throw new Error('track configuration has no `type` listed') - } - const name = readConfObject(configuration, 'name') - const connectionType = pluginManager.getConnectionType(type) - if (!connectionType) { - throw new Error(`unknown connection type ${type}`) - } - const connectionData = { - ...initialSnapshot, - name, - type, - configuration, - } - const length = self.connectionInstances.push(connectionData) - return self.connectionInstances[length - 1] - }, - - /** - * #action - */ - removeReferring( - referring: any, - track: any, - callbacks: Function[], - dereferenceTypeCount: Record, - ) { - referring.forEach(({ node }: ReferringNode) => { - let dereferenced = false - try { - // If a view is referring to the track config, remove the track - // from the view - const type = 'open track(s)' - const view = getContainingView(node) as TrackViewModel - callbacks.push(() => view.hideTrack(track.trackId)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } catch (err1) { - // ignore - } - - // @ts-ignore - if (self.widgets.has(node.id)) { - // If a configuration editor widget has the track config - // open, close the widget - const type = 'configuration editor widget(s)' - callbacks.push(() => this.hideWidget(node)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } - if (!dereferenced) { - throw new Error( - `Error when closing this connection, the following node is still referring to a track configuration: ${JSON.stringify( - getSnapshot(node), - )}`, - ) - } - }) - }, - - /** - * #action - */ - prepareToBreakConnection(configuration: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - if (connection) { - connection.tracks.forEach((track: any) => { - const referring = self.getReferring(track) - this.removeReferring( - referring, - track, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - }) - const safelyBreakConnection = () => { - callbacksToDereferenceTrack.forEach(cb => cb()) - this.breakConnection(configuration) - } - return [safelyBreakConnection, dereferenceTypeCount] - } - return undefined - }, - - /** - * #action - */ - breakConnection(configuration: AnyConfigurationModel) { - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - self.connectionInstances.remove(connection) - }, - - /** - * #action - */ - deleteConnection(configuration: AnyConfigurationModel) { - let deletedConn - if (self.adminMode) { - deletedConn = - getParent(self).jbrowse.deleteConnectionConf(configuration) - } - if (!deletedConn) { - const { connectionId } = configuration - const idx = self.sessionConnections.findIndex( - c => c.connectionId === connectionId, - ) - if (idx === -1) { - return undefined - } - return self.sessionConnections.splice(idx, 1) - } - return deletedConn - }, /** * #action @@ -743,7 +562,7 @@ export default function sessionModelFactory( const callbacksToDereferenceTrack: Function[] = [] const dereferenceTypeCount: Record = {} const referring = self.getReferring(trackConf) - this.removeReferring( + self.removeReferring( referring, trackConf, callbacksToDereferenceTrack, @@ -761,30 +580,6 @@ export default function sessionModelFactory( return self.sessionTracks.splice(idx, 1) }, - /** - * #action - */ - addConnectionConf(connectionConf: any) { - if (self.adminMode) { - return getParent(self).jbrowse.addConnectionConf(connectionConf) - } - const { connectionId, type } = connectionConf as { - type: string - connectionId: string - } - if (!type) { - throw new Error(`unknown connection type ${type}`) - } - const connection = self.sessionTracks.find( - (c: any) => c.connectionId === connectionId, - ) - if (connection) { - return connection - } - const length = self.sessionConnections.push(connectionConf) - return self.sessionConnections[length - 1] - }, - /** * #action */ @@ -916,13 +711,6 @@ export default function sessionModelFactory( self.selection = undefined }, - /** - * #action - */ - clearConnections() { - self.connectionInstances.length = 0 - }, - /** * #action */ From a0c2eb3cc50a1ecee59d20f87ccbe0e14ca4c0e7 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 24 Apr 2023 12:50:14 -0700 Subject: [PATCH 02/44] wip --- .../product-core/src/Session/Connections.ts | 13 ++++++++----- .../src/Session/ReferenceManagement.ts | 17 +++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts index 18c20595e5..0d6dda6f8c 100644 --- a/packages/product-core/src/Session/Connections.ts +++ b/packages/product-core/src/Session/Connections.ts @@ -5,11 +5,13 @@ import { AnyConfigurationModel, readConfObject, } from '@jbrowse/core/configuration' -import { getParent, types } from 'mobx-state-tree' +import { Instance, getParent, types } from 'mobx-state-tree' import ReferenceManagement from './ReferenceManagement' import { RootModel } from '../RootModel' -import type baseConnectionConfig from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' -import { BaseConnectionModelFactory } from '@jbrowse/core/pluggableElementTypes' +import { + BaseConnectionModelFactory, + baseConnectionConfig, +} from '@jbrowse/core/pluggableElementTypes' export default function Connections(pluginManager: PluginManager) { // connections: AnyConfigurationModel[] @@ -50,8 +52,9 @@ export default function Connections(pluginManager: PluginManager) { /** * #getter */ - get connections(): AnyConfigurationModel[] { - return [...self.sessionConnections, ...self.jbrowse.connections] + get connections(): Instance[] { + const jbConf = getParent(self).jbrowse + return [...self.sessionConnections, ...jbConf.connections] }, })) .actions(self => ({ diff --git a/packages/product-core/src/Session/ReferenceManagement.ts b/packages/product-core/src/Session/ReferenceManagement.ts index 4c723c30a7..50abef2459 100644 --- a/packages/product-core/src/Session/ReferenceManagement.ts +++ b/packages/product-core/src/Session/ReferenceManagement.ts @@ -2,9 +2,9 @@ import PluginManager from '@jbrowse/core/PluginManager' import { - SessionWithWidgets, TrackViewModel, getContainingView, + isSessionModelWithWidgets, } from '@jbrowse/core/util' import { IAnyStateTreeNode, @@ -18,6 +18,8 @@ import { walk, } from 'mobx-state-tree' +import type { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' + export interface ReferringNode { node: IAnyStateTreeNode key: string @@ -56,8 +58,8 @@ export default function ReferenceManagement(pluginManager: PluginManager) { * #action */ removeReferring( - referring: { node: ReferringNode }[], - track: unknown, + referring: ReferringNode[], + track: BaseTrackConfig, callbacks: Function[], dereferenceTypeCount: Record, ) { @@ -78,14 +80,13 @@ export default function ReferenceManagement(pluginManager: PluginManager) { // ignore } - // @ts-ignore - if (self.widgets.has(node.id)) { + if (isSessionModelWithWidgets(self) && self.widgets.has(node.id)) { // If a configuration editor widget has the track config // open, close the widget const type = 'configuration editor widget(s)' - callbacks.push(() => - (self as unknown as SessionWithWidgets).hideWidget(node), - ) + if (isSessionModelWithWidgets(self)) { + callbacks.push(() => self.hideWidget(node)) + } dereferenced = true if (!dereferenceTypeCount[type]) { dereferenceTypeCount[type] = 0 From 30349d9f52dddffad85801d72348c680822218ce Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 24 Apr 2023 12:53:04 -0700 Subject: [PATCH 03/44] wip --- .../jbrowse-web/src/sessionModelFactory.ts | 143 +++++++++--------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/products/jbrowse-web/src/sessionModelFactory.ts b/products/jbrowse-web/src/sessionModelFactory.ts index 67b12b1b80..c3d8570a8a 100644 --- a/products/jbrowse-web/src/sessionModelFactory.ts +++ b/products/jbrowse-web/src/sessionModelFactory.ts @@ -65,82 +65,81 @@ export default function sessionModelFactory( assemblyConfigSchemasType = types.frozen(), ) { const minDrawerWidth = 128 + + const BaseSession = types.model({ + /** + * #property + */ + id: types.optional(types.identifier, shortid()), + /** + * #property + */ + name: types.string, + /** + * #property + */ + margin: 0, + /** + * #property + */ + drawerWidth: types.optional( + types.refinement(types.integer, width => width >= minDrawerWidth), + 384, + ), + /** + * #property + */ + views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), + /** + * #property + */ + widgets: types.map(pluginManager.pluggableMstType('widget', 'stateModel')), + /** + * #property + */ + activeWidgets: types.map( + types.safeReference( + pluginManager.pluggableMstType('widget', 'stateModel'), + ), + ), + /** + * #property + */ + sessionTracks: types.array( + pluginManager.pluggableConfigSchemaType('track'), + ), + /** + * #property + */ + sessionAssemblies: types.array(assemblyConfigSchemasType), + /** + * #property + */ + temporaryAssemblies: types.array(assemblyConfigSchemasType), + /** + * #property + */ + sessionPlugins: types.array(types.frozen()), + /** + * #property + */ + minimized: types.optional(types.boolean, false), + + /** + * #property + */ + drawerPosition: types.optional( + types.string, + () => localStorageGetItem('drawerPosition') || 'right', + ), + }) + const sessionModel = types .compose( - types.model({ - /** - * #property - */ - id: types.optional(types.identifier, shortid()), - /** - * #property - */ - name: types.string, - /** - * #property - */ - margin: 0, - /** - * #property - */ - drawerWidth: types.optional( - types.refinement(types.integer, width => width >= minDrawerWidth), - 384, - ), - /** - * #property - */ - views: types.array( - pluginManager.pluggableMstType('view', 'stateModel'), - ), - /** - * #property - */ - widgets: types.map( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - ), - /** - * #property - */ - sessionTracks: types.array( - pluginManager.pluggableConfigSchemaType('track'), - ), - /** - * #property - */ - sessionAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - temporaryAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - sessionPlugins: types.array(types.frozen()), - /** - * #property - */ - minimized: types.optional(types.boolean, false), - - /** - * #property - */ - drawerPosition: types.optional( - types.string, - () => localStorageGetItem('drawerPosition') || 'right', - ), - }), + 'JBrowseWebSessionModel', + BaseSession, Session.Connections(pluginManager), ) - .named('JBrowseWebSessionModel') .volatile((/* self */) => ({ /** * #volatile From a63fe4f31306af7ce707a4d8f46ae1641b7f7b6c Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 24 Apr 2023 14:14:11 -0700 Subject: [PATCH 04/44] wip --- .../product-core/src/Session/Assemblies.ts | 0 .../product-core/src/Session/Connections.ts | 10 +- products/jbrowse-web/src/JBrowse.tsx | 2 +- products/jbrowse-web/src/rootModel.ts | 2 +- .../src/sessionModel/Assemblies.ts | 19 + products/jbrowse-web/src/sessionModel/Base.ts | 55 ++ .../jbrowse-web/src/sessionModel/Drawer.ts | 62 ++ .../src/sessionModel/SessionConnections.ts | 65 ++ .../index.test.js} | 0 .../jbrowse-web/src/sessionModel/index.ts | 814 ++++++++++++++++ .../jbrowse-web/src/sessionModelFactory.ts | 915 +----------------- 11 files changed, 1022 insertions(+), 922 deletions(-) delete mode 100644 packages/product-core/src/Session/Assemblies.ts create mode 100644 products/jbrowse-web/src/sessionModel/Assemblies.ts create mode 100644 products/jbrowse-web/src/sessionModel/Base.ts create mode 100644 products/jbrowse-web/src/sessionModel/Drawer.ts create mode 100644 products/jbrowse-web/src/sessionModel/SessionConnections.ts rename products/jbrowse-web/src/{sessionModelFactory.test.js => sessionModel/index.test.js} (100%) create mode 100644 products/jbrowse-web/src/sessionModel/index.ts diff --git a/packages/product-core/src/Session/Assemblies.ts b/packages/product-core/src/Session/Assemblies.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts index 0d6dda6f8c..ebfae49d8a 100644 --- a/packages/product-core/src/Session/Connections.ts +++ b/packages/product-core/src/Session/Connections.ts @@ -39,13 +39,6 @@ export default function Connections(pluginManager: PluginManager) { 'stateModel', ) as ReturnType, ), - - /** - * #property - */ - sessionConnections: types.array( - pluginManager.pluggableConfigSchemaType('connection'), - ), }), ) .views(self => ({ @@ -53,8 +46,7 @@ export default function Connections(pluginManager: PluginManager) { * #getter */ get connections(): Instance[] { - const jbConf = getParent(self).jbrowse - return [...self.sessionConnections, ...jbConf.connections] + return getParent(self).jbrowse.connections }, })) .actions(self => ({ diff --git a/products/jbrowse-web/src/JBrowse.tsx b/products/jbrowse-web/src/JBrowse.tsx index e0c29a84fa..f00cee7c22 100644 --- a/products/jbrowse-web/src/JBrowse.tsx +++ b/products/jbrowse-web/src/JBrowse.tsx @@ -11,7 +11,7 @@ import PluginManager from '@jbrowse/core/PluginManager' // locals import ShareButton from './ShareButton' import AdminComponent from './AdminComponent' -import { SessionModel } from './sessionModelFactory' +import { SessionModel } from './sessionModel' export default observer(function ({ pluginManager, diff --git a/products/jbrowse-web/src/rootModel.ts b/products/jbrowse-web/src/rootModel.ts index cdaac1fadd..5f03da1711 100644 --- a/products/jbrowse-web/src/rootModel.ts +++ b/products/jbrowse-web/src/rootModel.ts @@ -40,7 +40,7 @@ import { Cable } from '@jbrowse/core/ui/Icons' import makeWorkerInstance from './makeWorkerInstance' import corePlugins from './corePlugins' import jbrowseWebFactory from './jbrowseModel' -import sessionModelFactory from './sessionModelFactory' +import sessionModelFactory from './sessionModel' import { filterSessionInPlace } from './util' import { AnyConfigurationModel } from '@jbrowse/core/configuration' diff --git a/products/jbrowse-web/src/sessionModel/Assemblies.ts b/products/jbrowse-web/src/sessionModel/Assemblies.ts new file mode 100644 index 0000000000..f969fc8ea3 --- /dev/null +++ b/products/jbrowse-web/src/sessionModel/Assemblies.ts @@ -0,0 +1,19 @@ +import { types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' + +export default function Assemblies( + pluginManager: PluginManager, + assemblyConfigSchemasType = types.frozen(), +) { + return types.model({ + /** + * #property + */ + sessionAssemblies: types.array(assemblyConfigSchemasType), + /** + * #property + */ + temporaryAssemblies: types.array(assemblyConfigSchemasType), + }) +} diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts new file mode 100644 index 0000000000..f0ec816279 --- /dev/null +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -0,0 +1,55 @@ +import { Instance, types } from 'mobx-state-tree' +import shortid from 'shortid' + +import PluginManager from '@jbrowse/core/PluginManager' + +export function BaseSession(pluginManager: PluginManager) { + const BaseSession = types.model({ + /** + * #property + */ + id: types.optional(types.identifier, shortid()), + /** + * #property + */ + name: types.string, + /** + * #property + */ + margin: 0, + /** + * #property + */ + views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), + /** + * #property + */ + widgets: types.map(pluginManager.pluggableMstType('widget', 'stateModel')), + /** + * #property + */ + activeWidgets: types.map( + types.safeReference( + pluginManager.pluggableMstType('widget', 'stateModel'), + ), + ), + /** + * #property + */ + sessionTracks: types.array( + pluginManager.pluggableConfigSchemaType('track'), + ), + + /** + * #property + */ + sessionPlugins: types.array(types.frozen()), + /** + * #property + */ + minimized: types.optional(types.boolean, false), + }) + return BaseSession +} + +export type BaseSessionInstance = Instance> diff --git a/products/jbrowse-web/src/sessionModel/Drawer.ts b/products/jbrowse-web/src/sessionModel/Drawer.ts new file mode 100644 index 0000000000..c37c61de3c --- /dev/null +++ b/products/jbrowse-web/src/sessionModel/Drawer.ts @@ -0,0 +1,62 @@ +import { types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { localStorageGetItem } from '@jbrowse/core/util' + +const minDrawerWidth = 128 + +export default function Drawer(pluginManager: PluginManager) { + return types + .model({ + /** + * #property + */ + drawerPosition: types.optional( + types.string, + () => localStorageGetItem('drawerPosition') || 'right', + ), + /** + * #property + */ + drawerWidth: types.optional( + types.refinement(types.integer, width => width >= minDrawerWidth), + 384, + ), + }) + .actions(self => ({ + /** + * #action + */ + setDrawerPosition(arg: string) { + self.drawerPosition = arg + }, + + /** + * #action + */ + updateDrawerWidth(drawerWidth: number) { + if (drawerWidth === self.drawerWidth) { + return self.drawerWidth + } + let newDrawerWidth = drawerWidth + if (newDrawerWidth < minDrawerWidth) { + newDrawerWidth = minDrawerWidth + } + self.drawerWidth = newDrawerWidth + return newDrawerWidth + }, + + /** + * #action + */ + resizeDrawer(distance: number) { + if (self.drawerPosition === 'left') { + distance *= -1 + } + const oldDrawerWidth = self.drawerWidth + const newDrawerWidth = this.updateDrawerWidth(oldDrawerWidth - distance) + const actualDistance = oldDrawerWidth - newDrawerWidth + return actualDistance + }, + })) +} diff --git a/products/jbrowse-web/src/sessionModel/SessionConnections.ts b/products/jbrowse-web/src/sessionModel/SessionConnections.ts new file mode 100644 index 0000000000..5ce20257e8 --- /dev/null +++ b/products/jbrowse-web/src/sessionModel/SessionConnections.ts @@ -0,0 +1,65 @@ +import { types } from 'mobx-state-tree' + +import { Session } from '@jbrowse/product-core' +import PluginManager from '@jbrowse/core/PluginManager' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' + +export default function Connections(pluginManager: PluginManager) { + return types + .compose( + 'SessionConnectionsManagement', + Session.Connections(pluginManager), + types.model({ + /** + * #property + */ + sessionConnections: types.array( + pluginManager.pluggableConfigSchemaType('connection'), + ), + }), + ) + .actions(self => { + const superDeleteConnection = self.deleteConnection + const superAddConnectionConf = self.addConnectionConf + return { + addConnectionConf(connectionConf: any) { + if (self.adminMode) { + return superAddConnectionConf(connectionConf) + } + const { connectionId, type } = connectionConf as { + type: string + connectionId: string + } + if (!type) { + throw new Error(`unknown connection type ${type}`) + } + const connection = self.sessionTracks.find( + (c: any) => c.connectionId === connectionId, + ) + if (connection) { + return connection + } + const length = self.sessionConnections.push(connectionConf) + return self.sessionConnections[length - 1] + }, + + deleteConnection(configuration: AnyConfigurationModel) { + let deletedConn + if (self.adminMode) { + deletedConn = superDeleteConnection(configuration) + } + if (!deletedConn) { + const { connectionId } = configuration + const idx = self.sessionConnections.findIndex( + c => c.connectionId === connectionId, + ) + if (idx === -1) { + return undefined + } + return self.sessionConnections.splice(idx, 1) + } + return deletedConn + }, + } + }) +} diff --git a/products/jbrowse-web/src/sessionModelFactory.test.js b/products/jbrowse-web/src/sessionModel/index.test.js similarity index 100% rename from products/jbrowse-web/src/sessionModelFactory.test.js rename to products/jbrowse-web/src/sessionModel/index.test.js diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts new file mode 100644 index 0000000000..cd802027a8 --- /dev/null +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -0,0 +1,814 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { lazy } from 'react' +import clone from 'clone' +import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui/theme' +import { PluginDefinition } from '@jbrowse/core/PluginLoader' +import { + readConfObject, + getConf, + isConfigurationModel, + AnyConfigurationModel, +} from '@jbrowse/core/configuration' +import { + Region, + AbstractSessionModel, + JBrowsePlugin, + DialogComponentType, +} from '@jbrowse/core/util/types' +import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' +import { ThemeOptions } from '@mui/material' +import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' +import { autorun, observable } from 'mobx' +import { + addDisposer, + cast, + getParent, + getRoot, + getSnapshot, + isAlive, + types, + Instance, + SnapshotIn, + SnapshotOut, +} from 'mobx-state-tree' +import PluginManager from '@jbrowse/core/PluginManager' +import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' +import RpcManager from '@jbrowse/core/rpc/RpcManager' + +import { Session } from '@jbrowse/product-core' + +// icons +import SettingsIcon from '@mui/icons-material/Settings' +import CopyIcon from '@mui/icons-material/FileCopy' +import DeleteIcon from '@mui/icons-material/Delete' +import InfoIcon from '@mui/icons-material/Info' + +import { BaseSession } from './Base' +import Assemblies from './Assemblies' +import Drawer from './Drawer' + +const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) + +export declare interface ReactProps { + [key: string]: any +} + +type AnyConfiguration = + | AnyConfigurationModel + | SnapshotOut + +type ThemeMap = { [key: string]: ThemeOptions } + +/** + * #stateModel JBrowseWebSessionModel + * inherits SnackbarModel + */ +export default function sessionModelFactory( + pluginManager: PluginManager, + assemblyConfigSchemasType = types.frozen(), +) { + const sessionModel = types + .compose( + BaseSession(pluginManager), + Assemblies(pluginManager, assemblyConfigSchemasType), + Session.ReferenceManagement(pluginManager), + Session.Connections(pluginManager), + Drawer(pluginManager), + ) + .named('JBrowseWebSessionModel') + .volatile((/* self */) => ({ + /** + * #volatile + */ + sessionThemeName: localStorageGetItem('themeName') || 'default', + /** + * #volatile + * this is the globally "selected" object. can be anything. + * code that wants to deal with this should examine it to see what + * kind of thing it is. + */ + selection: undefined, + /** + * #volatile + * this is the current "task" that is being performed in the UI. + * this is usually an object of the form + * `{ taskName: "configure", target: thing_being_configured }` + */ + task: undefined, + /** + * #volatile + */ + queueOfDialogs: observable.array( + [] as [DialogComponentType, ReactProps][], + ), + })) + .views(self => ({ + /** + * #getter + */ + get jbrowse() { + return getParent(self).jbrowse + }, + })) + .views(self => ({ + /** + * #method + */ + allThemes(): ThemeMap { + const extraThemes = getConf(self.jbrowse, 'extraThemes') + return { ...defaultThemes, ...extraThemes } + }, + })) + .views(self => ({ + /** + * #getter + */ + get themeName() { + const { sessionThemeName } = self + const all = self.allThemes() + return all[sessionThemeName] ? sessionThemeName : 'default' + }, + })) + .views(self => ({ + /** + * #getter + */ + get theme() { + const configTheme = getConf(self.jbrowse, 'theme') + const all = self.allThemes() + return createJBrowseTheme(configTheme, all, self.themeName) + }, + + /** + * #getter + */ + get DialogComponent() { + if (self.queueOfDialogs.length) { + const firstInQueue = self.queueOfDialogs[0] + return firstInQueue && firstInQueue[0] + } + return undefined + }, + /** + * #getter + */ + get DialogProps() { + if (self.queueOfDialogs.length) { + const firstInQueue = self.queueOfDialogs[0] + return firstInQueue && firstInQueue[1] + } + return undefined + }, + /** + * #getter + */ + get shareURL() { + return getConf(self.jbrowse, 'shareURL') + }, + /** + * #getter + */ + get rpcManager() { + return self.jbrowse.rpcManager as RpcManager + }, + + /** + * #getter + */ + get configuration(): AnyConfigurationModel { + return self.jbrowse.configuration + }, + /** + * #getter + */ + get assemblies(): AnyConfigurationModel[] { + return self.jbrowse.assemblies + }, + /** + * #getter + */ + get assemblyNames(): string[] { + const { assemblyNames } = getParent(self).jbrowse + const sessionAssemblyNames = self.sessionAssemblies.map(assembly => + readConfObject(assembly, 'name'), + ) + return [...assemblyNames, ...sessionAssemblyNames] + }, + /** + * #getter + */ + get tracks(): AnyConfigurationModel[] { + return [...self.sessionTracks, ...getParent(self).jbrowse.tracks] + }, + /** + * #getter + */ + get textSearchManager(): TextSearchManager { + return getParent(self).textSearchManager + }, + /** + * #getter + */ + get adminMode(): boolean { + return getParent(self).adminMode + }, + /** + * #getter + */ + get savedSessions() { + return getParent(self).savedSessions + }, + /** + * #getter + */ + get previousAutosaveId() { + return getParent(self).previousAutosaveId + }, + /** + * #getter + */ + get savedSessionNames() { + return getParent(self).savedSessionNames + }, + /** + * #getter + */ + get history() { + return getParent(self).history + }, + /** + * #getter + */ + get menus() { + return getParent(self).menus + }, + /** + * #getter + */ + get assemblyManager() { + return getParent(self).assemblyManager + }, + /** + * #getter + */ + get version() { + return getParent(self).version + }, + + /** + * #method + */ + renderProps() { + return { + theme: this.theme, + } + }, + + /** + * #getter + */ + get visibleWidget() { + if (isAlive(self)) { + // returns most recently added item in active widgets + return [...self.activeWidgets.values()][self.activeWidgets.size - 1] + } + return undefined + }, + })) + .actions(self => ({ + /** + * #action + */ + setThemeName(name: string) { + self.sessionThemeName = name + }, + /** + * #action + */ + moveViewUp(id: string) { + const idx = self.views.findIndex(v => v.id === id) + + if (idx === -1) { + return + } + if (idx > 0) { + self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]) + } + }, + /** + * #action + */ + moveViewDown(id: string) { + const idx = self.views.findIndex(v => v.id === id) + + if (idx === -1) { + return + } + + if (idx < self.views.length - 1) { + self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]) + } + }, + /** + * #action + */ + queueDialog( + callback: ( + doneCallback: () => void, + ) => [DialogComponentType, ReactProps], + ) { + const [component, props] = callback(() => self.queueOfDialogs.shift()) + self.queueOfDialogs.push([component, props]) + }, + /** + * #action + */ + setName(str: string) { + self.name = str + }, + + /** + * #action + */ + addAssembly(conf: AnyConfiguration) { + const asm = self.sessionAssemblies.find(f => f.name === conf.name) + if (asm) { + console.warn(`Assembly ${conf.name} was already existing`) + return asm + } + const length = self.sessionAssemblies.push(conf) + return self.sessionAssemblies[length - 1] + }, + + /** + * #action + * used for read vs ref type assemblies. + */ + addTemporaryAssembly(conf: AnyConfiguration) { + const asm = self.sessionAssemblies.find(f => f.name === conf.name) + if (asm) { + console.warn(`Assembly ${conf.name} was already existing`) + return asm + } + const length = self.temporaryAssemblies.push(conf) + return self.temporaryAssemblies[length - 1] + }, + /** + * #action + */ + addSessionPlugin(plugin: JBrowsePlugin) { + if (self.sessionPlugins.some(p => p.name === plugin.name)) { + throw new Error('session plugin cannot be installed twice') + } + self.sessionPlugins.push(plugin) + getRoot(self).setPluginsUpdated(true) + }, + /** + * #action + */ + removeAssembly(assemblyName: string) { + const index = self.sessionAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.sessionAssemblies.splice(index, 1) + } + }, + /** + * #action + */ + removeTemporaryAssembly(assemblyName: string) { + const index = self.temporaryAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.temporaryAssemblies.splice(index, 1) + } + }, + /** + * #action + */ + removeSessionPlugin(pluginDefinition: PluginDefinition) { + self.sessionPlugins = cast( + self.sessionPlugins.filter( + plugin => + plugin.url !== pluginDefinition.url || + plugin.umdUrl !== pluginDefinition.umdUrl || + plugin.cjsUrl !== pluginDefinition.cjsUrl || + plugin.esmUrl !== pluginDefinition.esmUrl, + ), + ) + const rootModel = getParent(self) + rootModel.setPluginsUpdated(true) + }, + + /** + * #action + */ + addView(typeName: string, initialState = {}) { + const typeDefinition = pluginManager.getElementType('view', typeName) + if (!typeDefinition) { + throw new Error(`unknown view type ${typeName}`) + } + + const length = self.views.push({ + ...initialState, + type: typeName, + }) + return self.views[length - 1] + }, + + /** + * #action + */ + removeView(view: any) { + for (const [, widget] of self.activeWidgets) { + if (widget.view && widget.view.id === view.id) { + this.hideWidget(widget) + } + } + self.views.remove(view) + }, + + /** + * #action + */ + addAssemblyConf(assemblyConf: AnyConfiguration) { + return getParent(self).jbrowse.addAssemblyConf(assemblyConf) + }, + + /** + * #action + */ + addTrackConf(trackConf: AnyConfiguration) { + if (self.adminMode) { + return getParent(self).jbrowse.addTrackConf(trackConf) + } + const { trackId, type } = trackConf as { type: string; trackId: string } + if (!type) { + throw new Error(`unknown track type ${type}`) + } + const track = self.sessionTracks.find((t: any) => t.trackId === trackId) + if (track) { + return track + } + const length = self.sessionTracks.push(trackConf) + return self.sessionTracks[length - 1] + }, + + /** + * #action + */ + deleteTrackConf(trackConf: AnyConfigurationModel) { + const callbacksToDereferenceTrack: Function[] = [] + const dereferenceTypeCount: Record = {} + const referring = self.getReferring(trackConf) + self.removeReferring( + referring, + trackConf, + callbacksToDereferenceTrack, + dereferenceTypeCount, + ) + callbacksToDereferenceTrack.forEach(cb => cb()) + if (self.adminMode) { + return getParent(self).jbrowse.deleteTrackConf(trackConf) + } + const { trackId } = trackConf + const idx = self.sessionTracks.findIndex(t => t.trackId === trackId) + if (idx === -1) { + return undefined + } + return self.sessionTracks.splice(idx, 1) + }, + + /** + * #action + */ + addLinearGenomeViewOfAssembly(assemblyName: string, initialState = {}) { + return this.addViewOfAssembly( + 'LinearGenomeView', + assemblyName, + initialState, + ) + }, + + /** + * #action + */ + addViewOfAssembly( + viewType: any, + assemblyName: string, + initialState: any = {}, + ) { + const assembly = self.assemblies.find( + s => readConfObject(s, 'name') === assemblyName, + ) + if (!assembly) { + throw new Error( + `Could not add view of assembly "${assemblyName}", assembly name not found`, + ) + } + initialState.displayRegionsFromAssemblyName = readConfObject( + assembly, + 'name', + ) + return this.addView(viewType, initialState) + }, + + /** + * #action + */ + addViewFromAnotherView( + viewType: string, + otherView: any, + initialState: { displayedRegions?: Region[] } = {}, + ) { + const state = { ...initialState } + state.displayedRegions = getSnapshot(otherView.displayedRegions) + return this.addView(viewType, state) + }, + + /** + * #action + */ + addWidget( + typeName: string, + id: string, + initialState = {}, + conf?: unknown, + ) { + const typeDefinition = pluginManager.getElementType('widget', typeName) + if (!typeDefinition) { + throw new Error(`unknown widget type ${typeName}`) + } + const data = { + ...initialState, + id, + type: typeName, + configuration: conf || { type: typeName }, + } + self.widgets.set(id, data) + return self.widgets.get(id) + }, + + /** + * #action + */ + showWidget(widget: any) { + if (self.activeWidgets.has(widget.id)) { + self.activeWidgets.delete(widget.id) + } + self.activeWidgets.set(widget.id, widget) + self.minimized = false + }, + + /** + * #action + */ + hasWidget(widget: any) { + return self.activeWidgets.has(widget.id) + }, + + /** + * #action + */ + hideWidget(widget: any) { + self.activeWidgets.delete(widget.id) + }, + /** + * #action + */ + minimizeWidgetDrawer() { + self.minimized = true + }, + /** + * #action + */ + showWidgetDrawer() { + self.minimized = false + }, + /** + * #action + */ + hideAllWidgets() { + self.activeWidgets.clear() + }, + + /** + * #action + * set the global selection, i.e. the globally-selected object. + * can be a feature, a view, just about anything + * @param thing - + */ + setSelection(thing: any) { + self.selection = thing + }, + + /** + * #action + * clears the global selection + */ + clearSelection() { + self.selection = undefined + }, + + /** + * #action + */ + addSavedSession(sessionSnapshot: SnapshotIn) { + return getParent(self).addSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + removeSavedSession(sessionSnapshot: any) { + return getParent(self).removeSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + renameCurrentSession(sessionName: string) { + return getParent(self).renameCurrentSession(sessionName) + }, + + /** + * #action + */ + duplicateCurrentSession() { + return getParent(self).duplicateCurrentSession() + }, + /** + * #action + */ + activateSession(sessionName: any) { + return getParent(self).activateSession(sessionName) + }, + + /** + * #action + */ + setDefaultSession() { + return getParent(self).setDefaultSession() + }, + + /** + * #action + */ + saveSessionToLocalStorage() { + return getParent(self).saveSessionToLocalStorage() + }, + + /** + * #action + */ + loadAutosaveSession() { + return getParent(self).loadAutosaveSession() + }, + + /** + * #action + */ + setSession(sessionSnapshot: SnapshotIn) { + return getParent(self).setSession(sessionSnapshot) + }, + })) + + .actions(self => ({ + /** + * #action + * opens a configuration editor to configure the given thing, + * and sets the current task to be configuring it + * @param configuration - + */ + editConfiguration(configuration: AnyConfigurationModel) { + if (!isConfigurationModel(configuration)) { + throw new Error( + 'must pass a configuration model to editConfiguration', + ) + } + const editableConfigSession = self + const editor = editableConfigSession.addWidget( + 'ConfigurationEditorWidget', + 'configEditor', + { target: configuration }, + ) + editableConfigSession.showWidget(editor) + }, + + /** + * #action + */ + editTrackConfiguration(configuration: AnyConfigurationModel) { + const { adminMode, sessionTracks } = self + if (!adminMode && !sessionTracks.includes(configuration)) { + throw new Error("Can't edit the configuration of a non-session track") + } + this.editConfiguration(configuration) + }, + })) + .views(self => ({ + /** + * #method + */ + getTrackActionMenuItems(config: AnyConfigurationModel) { + const { adminMode, sessionTracks } = self + const canEdit = + adminMode || sessionTracks.find(t => t.trackId === config.trackId) + + // disable if it is a reference sequence track + const isRefSeq = + readConfObject(config, 'type') === 'ReferenceSequenceTrack' + return [ + { + label: 'About track', + onClick: () => { + self.queueDialog(handleClose => [ + AboutDialog, + { config, handleClose }, + ]) + }, + icon: InfoIcon, + }, + { + label: 'Settings', + disabled: !canEdit, + onClick: () => self.editTrackConfiguration(config), + icon: SettingsIcon, + }, + { + label: 'Delete track', + disabled: !canEdit || isRefSeq, + onClick: () => self.deleteTrackConf(config), + icon: DeleteIcon, + }, + { + label: 'Copy track', + disabled: isRefSeq, + onClick: () => { + const snap = clone(getSnapshot(config)) as any + const now = Date.now() + snap.trackId += `-${now}` + snap.displays.forEach((display: { displayId: string }) => { + display.displayId += `-${now}` + }) + // the -sessionTrack suffix to trackId is used as metadata for + // the track selector to store the track in a special category, + // and default category is also cleared + if (!self.adminMode) { + snap.trackId += '-sessionTrack' + snap.category = undefined + } + snap.name += ' (copy)' + self.addTrackConf(snap) + }, + icon: CopyIcon, + }, + ] + }, + })) + .actions(self => ({ + afterAttach() { + addDisposer( + self, + autorun(() => { + localStorageSetItem('drawerPosition', self.drawerPosition) + localStorageSetItem('themeName', self.themeName) + }), + ) + }, + })) + + const extendedSessionModel = pluginManager.evaluateExtensionPoint( + 'Core-extendSession', + sessionModel, + ) as typeof sessionModel + + return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { + // @ts-expect-error + preProcessor(snapshot) { + if (snapshot) { + // @ts-expect-error + const { connectionInstances, ...rest } = snapshot || {} + // connectionInstances schema changed from object to an array, so any + // old connectionInstances as object is in snapshot, filter it out + // https://github.com/GMOD/jbrowse-components/issues/1903 + if (!Array.isArray(connectionInstances)) { + return rest + } + } + return snapshot + }, + }) +} + +export type SessionStateModel = ReturnType +export type SessionModel = Instance + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function z(x: Instance): AbstractSessionModel { + // this function's sole purpose is to get typescript to check + // that the session model implements all of AbstractSessionModel + return x +} diff --git a/products/jbrowse-web/src/sessionModelFactory.ts b/products/jbrowse-web/src/sessionModelFactory.ts index c3d8570a8a..7e8652d6b0 100644 --- a/products/jbrowse-web/src/sessionModelFactory.ts +++ b/products/jbrowse-web/src/sessionModelFactory.ts @@ -1,912 +1,5 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { lazy } from 'react' -import clone from 'clone' -import shortid from 'shortid' -import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui/theme' -import { PluginDefinition } from '@jbrowse/core/PluginLoader' -import { - readConfObject, - getConf, - isConfigurationModel, - AnyConfigurationModel, -} from '@jbrowse/core/configuration' -import { - Region, - AbstractSessionModel, - JBrowsePlugin, - DialogComponentType, -} from '@jbrowse/core/util/types' -import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { ThemeOptions } from '@mui/material' -import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' -import { autorun, observable } from 'mobx' -import { - addDisposer, - cast, - getParent, - getRoot, - getSnapshot, - isAlive, - types, - Instance, - SnapshotIn, - SnapshotOut, -} from 'mobx-state-tree' -import PluginManager from '@jbrowse/core/PluginManager' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' -import RpcManager from '@jbrowse/core/rpc/RpcManager' +import oldSessionModelFactory from './sessionModel' -import { Session } from '@jbrowse/product-core' - -// icons -import SettingsIcon from '@mui/icons-material/Settings' -import CopyIcon from '@mui/icons-material/FileCopy' -import DeleteIcon from '@mui/icons-material/Delete' -import InfoIcon from '@mui/icons-material/Info' - -const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) - -export declare interface ReactProps { - [key: string]: any -} - -type AnyConfiguration = - | AnyConfigurationModel - | SnapshotOut - -type ThemeMap = { [key: string]: ThemeOptions } - -/** - * #stateModel JBrowseWebSessionModel - * inherits SnackbarModel - */ -export default function sessionModelFactory( - pluginManager: PluginManager, - assemblyConfigSchemasType = types.frozen(), -) { - const minDrawerWidth = 128 - - const BaseSession = types.model({ - /** - * #property - */ - id: types.optional(types.identifier, shortid()), - /** - * #property - */ - name: types.string, - /** - * #property - */ - margin: 0, - /** - * #property - */ - drawerWidth: types.optional( - types.refinement(types.integer, width => width >= minDrawerWidth), - 384, - ), - /** - * #property - */ - views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), - /** - * #property - */ - widgets: types.map(pluginManager.pluggableMstType('widget', 'stateModel')), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - ), - /** - * #property - */ - sessionTracks: types.array( - pluginManager.pluggableConfigSchemaType('track'), - ), - /** - * #property - */ - sessionAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - temporaryAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - sessionPlugins: types.array(types.frozen()), - /** - * #property - */ - minimized: types.optional(types.boolean, false), - - /** - * #property - */ - drawerPosition: types.optional( - types.string, - () => localStorageGetItem('drawerPosition') || 'right', - ), - }) - - const sessionModel = types - .compose( - 'JBrowseWebSessionModel', - BaseSession, - Session.Connections(pluginManager), - ) - .volatile((/* self */) => ({ - /** - * #volatile - */ - sessionThemeName: localStorageGetItem('themeName') || 'default', - /** - * #volatile - * this is the globally "selected" object. can be anything. - * code that wants to deal with this should examine it to see what - * kind of thing it is. - */ - selection: undefined, - /** - * #volatile - * this is the current "task" that is being performed in the UI. - * this is usually an object of the form - * `{ taskName: "configure", target: thing_being_configured }` - */ - task: undefined, - /** - * #volatile - */ - queueOfDialogs: observable.array( - [] as [DialogComponentType, ReactProps][], - ), - })) - .views(self => ({ - /** - * #getter - */ - get jbrowse() { - return getParent(self).jbrowse - }, - })) - .views(self => ({ - /** - * #method - */ - allThemes(): ThemeMap { - const extraThemes = getConf(self.jbrowse, 'extraThemes') - return { ...defaultThemes, ...extraThemes } - }, - })) - .views(self => ({ - /** - * #getter - */ - get themeName() { - const { sessionThemeName } = self - const all = self.allThemes() - return all[sessionThemeName] ? sessionThemeName : 'default' - }, - })) - .views(self => ({ - /** - * #getter - */ - get theme() { - const configTheme = getConf(self.jbrowse, 'theme') - const all = self.allThemes() - return createJBrowseTheme(configTheme, all, self.themeName) - }, - - /** - * #getter - */ - get DialogComponent() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[0] - } - return undefined - }, - /** - * #getter - */ - get DialogProps() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[1] - } - return undefined - }, - /** - * #getter - */ - get shareURL() { - return getConf(self.jbrowse, 'shareURL') - }, - /** - * #getter - */ - get rpcManager() { - return self.jbrowse.rpcManager as RpcManager - }, - - /** - * #getter - */ - get configuration(): AnyConfigurationModel { - return self.jbrowse.configuration - }, - /** - * #getter - */ - get assemblies(): AnyConfigurationModel[] { - return self.jbrowse.assemblies - }, - /** - * #getter - */ - get assemblyNames(): string[] { - const { assemblyNames } = getParent(self).jbrowse - const sessionAssemblyNames = self.sessionAssemblies.map(assembly => - readConfObject(assembly, 'name'), - ) - return [...assemblyNames, ...sessionAssemblyNames] - }, - /** - * #getter - */ - get tracks(): AnyConfigurationModel[] { - return [...self.sessionTracks, ...getParent(self).jbrowse.tracks] - }, - /** - * #getter - */ - get textSearchManager(): TextSearchManager { - return getParent(self).textSearchManager - }, - /** - * #getter - */ - get adminMode(): boolean { - return getParent(self).adminMode - }, - /** - * #getter - */ - get savedSessions() { - return getParent(self).savedSessions - }, - /** - * #getter - */ - get previousAutosaveId() { - return getParent(self).previousAutosaveId - }, - /** - * #getter - */ - get savedSessionNames() { - return getParent(self).savedSessionNames - }, - /** - * #getter - */ - get history() { - return getParent(self).history - }, - /** - * #getter - */ - get menus() { - return getParent(self).menus - }, - /** - * #getter - */ - get assemblyManager() { - return getParent(self).assemblyManager - }, - /** - * #getter - */ - get version() { - return getParent(self).version - }, - - /** - * #method - */ - renderProps() { - return { - theme: this.theme, - } - }, - - /** - * #getter - */ - get visibleWidget() { - if (isAlive(self)) { - // returns most recently added item in active widgets - return [...self.activeWidgets.values()][self.activeWidgets.size - 1] - } - return undefined - }, - })) - .actions(self => ({ - /** - * #action - */ - setThemeName(name: string) { - self.sessionThemeName = name - }, - /** - * #action - */ - moveViewUp(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - if (idx > 0) { - self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]) - } - }, - /** - * #action - */ - moveViewDown(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - - if (idx < self.views.length - 1) { - self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]) - } - }, - /** - * #action - */ - setDrawerPosition(arg: string) { - self.drawerPosition = arg - }, - /** - * #action - */ - queueDialog( - callback: ( - doneCallback: () => void, - ) => [DialogComponentType, ReactProps], - ) { - const [component, props] = callback(() => self.queueOfDialogs.shift()) - self.queueOfDialogs.push([component, props]) - }, - /** - * #action - */ - setName(str: string) { - self.name = str - }, - - /** - * #action - */ - addAssembly(conf: AnyConfiguration) { - const asm = self.sessionAssemblies.find(f => f.name === conf.name) - if (asm) { - console.warn(`Assembly ${conf.name} was already existing`) - return asm - } - const length = self.sessionAssemblies.push(conf) - return self.sessionAssemblies[length - 1] - }, - - /** - * #action - * used for read vs ref type assemblies. - */ - addTemporaryAssembly(conf: AnyConfiguration) { - const asm = self.sessionAssemblies.find(f => f.name === conf.name) - if (asm) { - console.warn(`Assembly ${conf.name} was already existing`) - return asm - } - const length = self.temporaryAssemblies.push(conf) - return self.temporaryAssemblies[length - 1] - }, - /** - * #action - */ - addSessionPlugin(plugin: JBrowsePlugin) { - if (self.sessionPlugins.some(p => p.name === plugin.name)) { - throw new Error('session plugin cannot be installed twice') - } - self.sessionPlugins.push(plugin) - getRoot(self).setPluginsUpdated(true) - }, - /** - * #action - */ - removeAssembly(assemblyName: string) { - const index = self.sessionAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.sessionAssemblies.splice(index, 1) - } - }, - /** - * #action - */ - removeTemporaryAssembly(assemblyName: string) { - const index = self.temporaryAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.temporaryAssemblies.splice(index, 1) - } - }, - /** - * #action - */ - removeSessionPlugin(pluginDefinition: PluginDefinition) { - self.sessionPlugins = cast( - self.sessionPlugins.filter( - plugin => - plugin.url !== pluginDefinition.url || - plugin.umdUrl !== pluginDefinition.umdUrl || - plugin.cjsUrl !== pluginDefinition.cjsUrl || - plugin.esmUrl !== pluginDefinition.esmUrl, - ), - ) - const rootModel = getParent(self) - rootModel.setPluginsUpdated(true) - }, - - /** - * #action - */ - updateDrawerWidth(drawerWidth: number) { - if (drawerWidth === self.drawerWidth) { - return self.drawerWidth - } - let newDrawerWidth = drawerWidth - if (newDrawerWidth < minDrawerWidth) { - newDrawerWidth = minDrawerWidth - } - self.drawerWidth = newDrawerWidth - return newDrawerWidth - }, - - /** - * #action - */ - resizeDrawer(distance: number) { - if (self.drawerPosition === 'left') { - distance *= -1 - } - const oldDrawerWidth = self.drawerWidth - const newDrawerWidth = this.updateDrawerWidth(oldDrawerWidth - distance) - const actualDistance = oldDrawerWidth - newDrawerWidth - return actualDistance - }, - - /** - * #action - */ - addView(typeName: string, initialState = {}) { - const typeDefinition = pluginManager.getElementType('view', typeName) - if (!typeDefinition) { - throw new Error(`unknown view type ${typeName}`) - } - - const length = self.views.push({ - ...initialState, - type: typeName, - }) - return self.views[length - 1] - }, - - /** - * #action - */ - removeView(view: any) { - for (const [, widget] of self.activeWidgets) { - if (widget.view && widget.view.id === view.id) { - this.hideWidget(widget) - } - } - self.views.remove(view) - }, - - /** - * #action - */ - addAssemblyConf(assemblyConf: AnyConfiguration) { - return getParent(self).jbrowse.addAssemblyConf(assemblyConf) - }, - - /** - * #action - */ - addTrackConf(trackConf: AnyConfiguration) { - if (self.adminMode) { - return getParent(self).jbrowse.addTrackConf(trackConf) - } - const { trackId, type } = trackConf as { type: string; trackId: string } - if (!type) { - throw new Error(`unknown track type ${type}`) - } - const track = self.sessionTracks.find((t: any) => t.trackId === trackId) - if (track) { - return track - } - const length = self.sessionTracks.push(trackConf) - return self.sessionTracks[length - 1] - }, - - /** - * #action - */ - deleteTrackConf(trackConf: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const referring = self.getReferring(trackConf) - self.removeReferring( - referring, - trackConf, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - callbacksToDereferenceTrack.forEach(cb => cb()) - if (self.adminMode) { - return getParent(self).jbrowse.deleteTrackConf(trackConf) - } - const { trackId } = trackConf - const idx = self.sessionTracks.findIndex(t => t.trackId === trackId) - if (idx === -1) { - return undefined - } - return self.sessionTracks.splice(idx, 1) - }, - - /** - * #action - */ - addLinearGenomeViewOfAssembly(assemblyName: string, initialState = {}) { - return this.addViewOfAssembly( - 'LinearGenomeView', - assemblyName, - initialState, - ) - }, - - /** - * #action - */ - addViewOfAssembly( - viewType: any, - assemblyName: string, - initialState: any = {}, - ) { - const assembly = self.assemblies.find( - s => readConfObject(s, 'name') === assemblyName, - ) - if (!assembly) { - throw new Error( - `Could not add view of assembly "${assemblyName}", assembly name not found`, - ) - } - initialState.displayRegionsFromAssemblyName = readConfObject( - assembly, - 'name', - ) - return this.addView(viewType, initialState) - }, - - /** - * #action - */ - addViewFromAnotherView( - viewType: string, - otherView: any, - initialState: { displayedRegions?: Region[] } = {}, - ) { - const state = { ...initialState } - state.displayedRegions = getSnapshot(otherView.displayedRegions) - return this.addView(viewType, state) - }, - - /** - * #action - */ - addWidget( - typeName: string, - id: string, - initialState = {}, - conf?: unknown, - ) { - const typeDefinition = pluginManager.getElementType('widget', typeName) - if (!typeDefinition) { - throw new Error(`unknown widget type ${typeName}`) - } - const data = { - ...initialState, - id, - type: typeName, - configuration: conf || { type: typeName }, - } - self.widgets.set(id, data) - return self.widgets.get(id) - }, - - /** - * #action - */ - showWidget(widget: any) { - if (self.activeWidgets.has(widget.id)) { - self.activeWidgets.delete(widget.id) - } - self.activeWidgets.set(widget.id, widget) - self.minimized = false - }, - - /** - * #action - */ - hasWidget(widget: any) { - return self.activeWidgets.has(widget.id) - }, - - /** - * #action - */ - hideWidget(widget: any) { - self.activeWidgets.delete(widget.id) - }, - /** - * #action - */ - minimizeWidgetDrawer() { - self.minimized = true - }, - /** - * #action - */ - showWidgetDrawer() { - self.minimized = false - }, - /** - * #action - */ - hideAllWidgets() { - self.activeWidgets.clear() - }, - - /** - * #action - * set the global selection, i.e. the globally-selected object. - * can be a feature, a view, just about anything - * @param thing - - */ - setSelection(thing: any) { - self.selection = thing - }, - - /** - * #action - * clears the global selection - */ - clearSelection() { - self.selection = undefined - }, - - /** - * #action - */ - addSavedSession(sessionSnapshot: SnapshotIn) { - return getParent(self).addSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - removeSavedSession(sessionSnapshot: any) { - return getParent(self).removeSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return getParent(self).renameCurrentSession(sessionName) - }, - - /** - * #action - */ - duplicateCurrentSession() { - return getParent(self).duplicateCurrentSession() - }, - /** - * #action - */ - activateSession(sessionName: any) { - return getParent(self).activateSession(sessionName) - }, - - /** - * #action - */ - setDefaultSession() { - return getParent(self).setDefaultSession() - }, - - /** - * #action - */ - saveSessionToLocalStorage() { - return getParent(self).saveSessionToLocalStorage() - }, - - /** - * #action - */ - loadAutosaveSession() { - return getParent(self).loadAutosaveSession() - }, - - /** - * #action - */ - setSession(sessionSnapshot: SnapshotIn) { - return getParent(self).setSession(sessionSnapshot) - }, - })) - - .actions(self => ({ - /** - * #action - * opens a configuration editor to configure the given thing, - * and sets the current task to be configuring it - * @param configuration - - */ - editConfiguration(configuration: AnyConfigurationModel) { - if (!isConfigurationModel(configuration)) { - throw new Error( - 'must pass a configuration model to editConfiguration', - ) - } - const editableConfigSession = self - const editor = editableConfigSession.addWidget( - 'ConfigurationEditorWidget', - 'configEditor', - { target: configuration }, - ) - editableConfigSession.showWidget(editor) - }, - - /** - * #action - */ - editTrackConfiguration(configuration: AnyConfigurationModel) { - const { adminMode, sessionTracks } = self - if (!adminMode && !sessionTracks.includes(configuration)) { - throw new Error("Can't edit the configuration of a non-session track") - } - this.editConfiguration(configuration) - }, - })) - .views(self => ({ - /** - * #method - */ - getTrackActionMenuItems(config: AnyConfigurationModel) { - const { adminMode, sessionTracks } = self - const canEdit = - adminMode || sessionTracks.find(t => t.trackId === config.trackId) - - // disable if it is a reference sequence track - const isRefSeq = - readConfObject(config, 'type') === 'ReferenceSequenceTrack' - return [ - { - label: 'About track', - onClick: () => { - self.queueDialog(handleClose => [ - AboutDialog, - { config, handleClose }, - ]) - }, - icon: InfoIcon, - }, - { - label: 'Settings', - disabled: !canEdit, - onClick: () => self.editTrackConfiguration(config), - icon: SettingsIcon, - }, - { - label: 'Delete track', - disabled: !canEdit || isRefSeq, - onClick: () => self.deleteTrackConf(config), - icon: DeleteIcon, - }, - { - label: 'Copy track', - disabled: isRefSeq, - onClick: () => { - const snap = clone(getSnapshot(config)) as any - const now = Date.now() - snap.trackId += `-${now}` - snap.displays.forEach((display: { displayId: string }) => { - display.displayId += `-${now}` - }) - // the -sessionTrack suffix to trackId is used as metadata for - // the track selector to store the track in a special category, - // and default category is also cleared - if (!self.adminMode) { - snap.trackId += '-sessionTrack' - snap.category = undefined - } - snap.name += ' (copy)' - self.addTrackConf(snap) - }, - icon: CopyIcon, - }, - ] - }, - })) - .actions(self => ({ - afterAttach() { - addDisposer( - self, - autorun(() => { - localStorageSetItem('drawerPosition', self.drawerPosition) - localStorageSetItem('themeName', self.themeName) - }), - ) - }, - })) - - const extendedSessionModel = pluginManager.evaluateExtensionPoint( - 'Core-extendSession', - sessionModel, - ) as typeof sessionModel - - return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { - // @ts-expect-error - preProcessor(snapshot) { - if (snapshot) { - // @ts-expect-error - const { connectionInstances, ...rest } = snapshot || {} - // connectionInstances schema changed from object to an array, so any - // old connectionInstances as object is in snapshot, filter it out - // https://github.com/GMOD/jbrowse-components/issues/1903 - if (!Array.isArray(connectionInstances)) { - return rest - } - } - return snapshot - }, - }) -} - -export type SessionStateModel = ReturnType -export type SessionModel = Instance - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function z(x: Instance): AbstractSessionModel { - // this function's sole purpose is to get typescript to check - // that the session model implements all of AbstractSessionModel - return x -} +/** @deprecated moved to ./sessionModel */ +const sessionModelFactory = oldSessionModelFactory +export default sessionModelFactory From 075f78804bec9528978016efdbca65e45ed8fbff Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 24 Apr 2023 14:14:16 -0700 Subject: [PATCH 05/44] wip --- products/jbrowse-web/src/sessionModel/index.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/products/jbrowse-web/src/sessionModel/index.test.js b/products/jbrowse-web/src/sessionModel/index.test.js index 46e3c88195..2338276edc 100644 --- a/products/jbrowse-web/src/sessionModel/index.test.js +++ b/products/jbrowse-web/src/sessionModel/index.test.js @@ -2,8 +2,8 @@ import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' import { configure } from 'mobx' -import { createTestSession } from './rootModel' -import sessionModelFactory from './sessionModelFactory' +import { createTestSession } from '../rootModel' +import sessionModelFactory from '.' jest.mock('./makeWorkerInstance', () => () => {}) // mock warnings to avoid unnecessary outputs From f3f977bf6846152f165361e61726d65038de93a7 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Tue, 25 Apr 2023 09:25:43 -0700 Subject: [PATCH 06/44] wip --- .../sessionModel/sessionModelFactory.test.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js diff --git a/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js new file mode 100644 index 0000000000..2338276edc --- /dev/null +++ b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js @@ -0,0 +1,65 @@ +// we use mainthread rpc so we mock the makeWorkerInstance to an empty file +import PluginManager from '@jbrowse/core/PluginManager' +import { getSnapshot } from 'mobx-state-tree' +import { configure } from 'mobx' +import { createTestSession } from '../rootModel' +import sessionModelFactory from '.' +jest.mock('./makeWorkerInstance', () => () => {}) + +// mock warnings to avoid unnecessary outputs +beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}) + configure({ computedConfigurable: true }) // so jest.spyOn works for MST get +}) + +afterEach(() => { + console.warn.mockRestore() +}) + +describe('JBrowseWebSessionModel', () => { + it('creates with no parent and just a name', () => { + const pluginManager = new PluginManager() + pluginManager.configure() + const sessionModel = sessionModelFactory(pluginManager) + const session = sessionModel.create( + { name: 'testSession' }, + { pluginManager }, + ) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, ...rest } = getSnapshot(session) + expect(rest).toMatchSnapshot() + }) + + it('accepts a custom drawer width', () => { + const session = createTestSession({ drawerWidth: 256 }) + expect(session.drawerWidth).toBe(256) + }) + + xit('adds connection to session connections', () => { + const pluginManager = new PluginManager() + pluginManager.configure() + const sessionModel = sessionModelFactory(pluginManager) + const session = sessionModel.create( + { name: 'testSession' }, + { pluginManager }, + ) + + jest + .spyOn(session, 'adminMode', 'get') + .mockImplementationOnce(() => {}) + .mockReturnValueOnce(false) + + session.addConnectionConf({ + assemblyName: 'test1', + connectionId: 'TestConnection-test1-1', + hubTxtLocation: { + uri: 'https://example.com', + locationType: 'UriLocation', + }, + type: 'JBrowse1Connection', + }) + + expect(session.sessionConnections.length).toBe(1) + }) +}) From 7a23b3d896fa36d15b3c5286a98bff9784058f76 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Tue, 25 Apr 2023 10:22:41 -0700 Subject: [PATCH 07/44] wip --- .../models/BaseConnectionModelFactory.ts | 1 + .../models/baseConnectionConfig.ts | 7 +- .../product-core/src/Session/Connections.ts | 14 ++- products/jbrowse-web/src/sessionModel/Base.ts | 100 ++++++++++-------- .../src/sessionModel/SessionConnections.ts | 23 ++-- .../jbrowse-web/src/sessionModel/index.ts | 6 -- 6 files changed, 81 insertions(+), 70 deletions(-) diff --git a/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts b/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts index a4909a5a4e..250c10493f 100644 --- a/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts +++ b/packages/core/pluggableElementTypes/models/BaseConnectionModelFactory.ts @@ -61,4 +61,5 @@ function stateModelFactory(pluginManager: PluginManager) { })) } +export type BaseConnectionModel = ReturnType export default stateModelFactory diff --git a/packages/core/pluggableElementTypes/models/baseConnectionConfig.ts b/packages/core/pluggableElementTypes/models/baseConnectionConfig.ts index 952d046d03..064e614eab 100644 --- a/packages/core/pluggableElementTypes/models/baseConnectionConfig.ts +++ b/packages/core/pluggableElementTypes/models/baseConnectionConfig.ts @@ -1,3 +1,4 @@ +import type { Instance } from 'mobx-state-tree' import { ConfigurationSchema } from '../../configuration' /** @@ -5,7 +6,7 @@ import { ConfigurationSchema } from '../../configuration' */ function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars -export default ConfigurationSchema( +const BaseConnectionConfig = ConfigurationSchema( 'BaseConnection', { /** @@ -33,3 +34,7 @@ export default ConfigurationSchema( explicitIdentifier: 'connectionId', }, ) + +export default BaseConnectionConfig +export type BaseConnectionConfigSchema = typeof BaseConnectionConfig +export type BaseConnectionConfigModel = Instance diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts index ebfae49d8a..b21114edfd 100644 --- a/packages/product-core/src/Session/Connections.ts +++ b/packages/product-core/src/Session/Connections.ts @@ -5,13 +5,11 @@ import { AnyConfigurationModel, readConfObject, } from '@jbrowse/core/configuration' -import { Instance, getParent, types } from 'mobx-state-tree' +import { getParent, types } from 'mobx-state-tree' import ReferenceManagement from './ReferenceManagement' import { RootModel } from '../RootModel' -import { - BaseConnectionModelFactory, - baseConnectionConfig, -} from '@jbrowse/core/pluggableElementTypes' +import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' +import { BaseConnectionModel } from '@jbrowse/core/pluggableElementTypes/models/BaseConnectionModelFactory' export default function Connections(pluginManager: PluginManager) { // connections: AnyConfigurationModel[] @@ -37,7 +35,7 @@ export default function Connections(pluginManager: PluginManager) { pluginManager.pluggableMstType( 'connection', 'stateModel', - ) as ReturnType, + ) as BaseConnectionModel, ), }), ) @@ -45,7 +43,7 @@ export default function Connections(pluginManager: PluginManager) { /** * #getter */ - get connections(): Instance[] { + get connections(): BaseConnectionConfigModel[] { return getParent(self).jbrowse.connections }, })) @@ -127,7 +125,7 @@ export default function Connections(pluginManager: PluginManager) { /** * #action */ - addConnectionConf(connectionConf: typeof baseConnectionConfig) { + addConnectionConf(connectionConf: BaseConnectionConfigModel) { return getParent(self).jbrowse.addConnectionConf( connectionConf, ) diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts index f0ec816279..261390ea14 100644 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -1,55 +1,67 @@ -import { Instance, types } from 'mobx-state-tree' +import { Instance, getParent, types } from 'mobx-state-tree' import shortid from 'shortid' import PluginManager from '@jbrowse/core/PluginManager' export function BaseSession(pluginManager: PluginManager) { - const BaseSession = types.model({ - /** - * #property - */ - id: types.optional(types.identifier, shortid()), - /** - * #property - */ - name: types.string, - /** - * #property - */ - margin: 0, - /** - * #property - */ - views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), - /** - * #property - */ - widgets: types.map(pluginManager.pluggableMstType('widget', 'stateModel')), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( + const BaseSession = types + .model({ + /** + * #property + */ + id: types.optional(types.identifier, shortid()), + /** + * #property + */ + name: types.string, + /** + * #property + */ + margin: 0, + /** + * #property + */ + views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), + /** + * #property + */ + widgets: types.map( pluginManager.pluggableMstType('widget', 'stateModel'), ), - ), - /** - * #property - */ - sessionTracks: types.array( - pluginManager.pluggableConfigSchemaType('track'), - ), + /** + * #property + */ + activeWidgets: types.map( + types.safeReference( + pluginManager.pluggableMstType('widget', 'stateModel'), + ), + ), + /** + * #property + */ + sessionTracks: types.array( + pluginManager.pluggableConfigSchemaType('track'), + ), - /** - * #property - */ - sessionPlugins: types.array(types.frozen()), - /** - * #property - */ - minimized: types.optional(types.boolean, false), - }) + /** + * #property + */ + sessionPlugins: types.array(types.frozen()), + /** + * #property + */ + minimized: types.optional(types.boolean, false), + }) + .views(self => ({ + /** + * #getter + */ + get adminMode(): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return getParent(self).adminMode + }, + })) return BaseSession } -export type BaseSessionInstance = Instance> +export type BaseSessionModel = Instance> diff --git a/products/jbrowse-web/src/sessionModel/SessionConnections.ts b/products/jbrowse-web/src/sessionModel/SessionConnections.ts index 5ce20257e8..d770f37282 100644 --- a/products/jbrowse-web/src/sessionModel/SessionConnections.ts +++ b/products/jbrowse-web/src/sessionModel/SessionConnections.ts @@ -3,6 +3,8 @@ import { types } from 'mobx-state-tree' import { Session } from '@jbrowse/product-core' import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import type { BaseSessionModel } from './Base' +import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' export default function Connections(pluginManager: PluginManager) { return types @@ -18,23 +20,22 @@ export default function Connections(pluginManager: PluginManager) { ), }), ) - .actions(self => { - const superDeleteConnection = self.deleteConnection - const superAddConnectionConf = self.addConnectionConf + .actions(s => { + const self = s as typeof s & BaseSessionModel + + const super_deleteConnection = self.deleteConnection + const super_addConnectionConf = self.addConnectionConf return { - addConnectionConf(connectionConf: any) { + addConnectionConf(connectionConf: BaseConnectionConfigModel) { if (self.adminMode) { - return superAddConnectionConf(connectionConf) - } - const { connectionId, type } = connectionConf as { - type: string - connectionId: string + return super_addConnectionConf(connectionConf) } + const { connectionId, type } = connectionConf if (!type) { throw new Error(`unknown connection type ${type}`) } const connection = self.sessionTracks.find( - (c: any) => c.connectionId === connectionId, + c => c.connectionId === connectionId, ) if (connection) { return connection @@ -46,7 +47,7 @@ export default function Connections(pluginManager: PluginManager) { deleteConnection(configuration: AnyConfigurationModel) { let deletedConn if (self.adminMode) { - deletedConn = superDeleteConnection(configuration) + deletedConn = super_deleteConnection(configuration) } if (!deletedConn) { const { connectionId } = configuration diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index cd802027a8..fda335cfe4 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -206,12 +206,6 @@ export default function sessionModelFactory( get textSearchManager(): TextSearchManager { return getParent(self).textSearchManager }, - /** - * #getter - */ - get adminMode(): boolean { - return getParent(self).adminMode - }, /** * #getter */ From e77ba9bb74b11e58c1bb8af3adb6610a15927c7b Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Tue, 25 Apr 2023 10:29:38 -0700 Subject: [PATCH 08/44] wip --- products/jbrowse-web/src/jbrowseModel.ts | 3 +-- .../jbrowse-web/src/sessionModel/SessionConnections.ts | 6 +++--- products/jbrowse-web/src/sessionModel/index.ts | 7 ++++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/products/jbrowse-web/src/jbrowseModel.ts b/products/jbrowse-web/src/jbrowseModel.ts index 3187ccd788..99690b5bc3 100644 --- a/products/jbrowse-web/src/jbrowseModel.ts +++ b/products/jbrowse-web/src/jbrowseModel.ts @@ -17,9 +17,9 @@ import { toJS } from 'mobx' import clone from 'clone' // locals -import { SessionStateModel } from './sessionModelFactory' import JBrowseConfigF from './jbrowseConfig' import RpcManager from '@jbrowse/core/rpc/RpcManager' +import { SessionStateModel } from './sessionModel' // poke some things for testing (this stuff will eventually be removed) // @ts-expect-error @@ -196,7 +196,6 @@ export default function JBrowseWeb( throw new Error(`unable to set default session to ${newDefault.name}`) } - // @ts-expect-error complains about name missing, but above line checks this self.defaultSession = cast(newDefault) }, /** diff --git a/products/jbrowse-web/src/sessionModel/SessionConnections.ts b/products/jbrowse-web/src/sessionModel/SessionConnections.ts index d770f37282..59e33c5c25 100644 --- a/products/jbrowse-web/src/sessionModel/SessionConnections.ts +++ b/products/jbrowse-web/src/sessionModel/SessionConnections.ts @@ -1,16 +1,16 @@ import { types } from 'mobx-state-tree' -import { Session } from '@jbrowse/product-core' +import { Session as CoreSession } from '@jbrowse/product-core' import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfigurationModel } from '@jbrowse/core/configuration' import type { BaseSessionModel } from './Base' import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' -export default function Connections(pluginManager: PluginManager) { +export default function SessionConnections(pluginManager: PluginManager) { return types .compose( 'SessionConnectionsManagement', - Session.Connections(pluginManager), + CoreSession.Connections(pluginManager), types.model({ /** * #property diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index fda335cfe4..b085c02f84 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -35,7 +35,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' -import { Session } from '@jbrowse/product-core' +import { Session as CoreSession } from '@jbrowse/product-core' // icons import SettingsIcon from '@mui/icons-material/Settings' @@ -46,6 +46,7 @@ import InfoIcon from '@mui/icons-material/Info' import { BaseSession } from './Base' import Assemblies from './Assemblies' import Drawer from './Drawer' +import SessionConnections from './SessionConnections' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) @@ -71,8 +72,8 @@ export default function sessionModelFactory( .compose( BaseSession(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), - Session.ReferenceManagement(pluginManager), - Session.Connections(pluginManager), + CoreSession.ReferenceManagement(pluginManager), + SessionConnections(pluginManager), Drawer(pluginManager), ) .named('JBrowseWebSessionModel') From efb8ac7eca81b896e4f2738f196550856908d678 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Tue, 25 Apr 2023 10:54:07 -0700 Subject: [PATCH 09/44] fix tests --- .../sessionModelFactory.test.js.snap | 0 .../src/sessionModel/index.test.js | 65 ------------------- .../sessionModel/sessionModelFactory.test.js | 2 +- 3 files changed, 1 insertion(+), 66 deletions(-) rename products/jbrowse-web/src/{ => sessionModel}/__snapshots__/sessionModelFactory.test.js.snap (100%) delete mode 100644 products/jbrowse-web/src/sessionModel/index.test.js diff --git a/products/jbrowse-web/src/__snapshots__/sessionModelFactory.test.js.snap b/products/jbrowse-web/src/sessionModel/__snapshots__/sessionModelFactory.test.js.snap similarity index 100% rename from products/jbrowse-web/src/__snapshots__/sessionModelFactory.test.js.snap rename to products/jbrowse-web/src/sessionModel/__snapshots__/sessionModelFactory.test.js.snap diff --git a/products/jbrowse-web/src/sessionModel/index.test.js b/products/jbrowse-web/src/sessionModel/index.test.js deleted file mode 100644 index 2338276edc..0000000000 --- a/products/jbrowse-web/src/sessionModel/index.test.js +++ /dev/null @@ -1,65 +0,0 @@ -// we use mainthread rpc so we mock the makeWorkerInstance to an empty file -import PluginManager from '@jbrowse/core/PluginManager' -import { getSnapshot } from 'mobx-state-tree' -import { configure } from 'mobx' -import { createTestSession } from '../rootModel' -import sessionModelFactory from '.' -jest.mock('./makeWorkerInstance', () => () => {}) - -// mock warnings to avoid unnecessary outputs -beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(() => {}) - configure({ computedConfigurable: true }) // so jest.spyOn works for MST get -}) - -afterEach(() => { - console.warn.mockRestore() -}) - -describe('JBrowseWebSessionModel', () => { - it('creates with no parent and just a name', () => { - const pluginManager = new PluginManager() - pluginManager.configure() - const sessionModel = sessionModelFactory(pluginManager) - const session = sessionModel.create( - { name: 'testSession' }, - { pluginManager }, - ) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, ...rest } = getSnapshot(session) - expect(rest).toMatchSnapshot() - }) - - it('accepts a custom drawer width', () => { - const session = createTestSession({ drawerWidth: 256 }) - expect(session.drawerWidth).toBe(256) - }) - - xit('adds connection to session connections', () => { - const pluginManager = new PluginManager() - pluginManager.configure() - const sessionModel = sessionModelFactory(pluginManager) - const session = sessionModel.create( - { name: 'testSession' }, - { pluginManager }, - ) - - jest - .spyOn(session, 'adminMode', 'get') - .mockImplementationOnce(() => {}) - .mockReturnValueOnce(false) - - session.addConnectionConf({ - assemblyName: 'test1', - connectionId: 'TestConnection-test1-1', - hubTxtLocation: { - uri: 'https://example.com', - locationType: 'UriLocation', - }, - type: 'JBrowse1Connection', - }) - - expect(session.sessionConnections.length).toBe(1) - }) -}) diff --git a/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js index 2338276edc..b73246f2a3 100644 --- a/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js +++ b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js @@ -4,7 +4,7 @@ import { getSnapshot } from 'mobx-state-tree' import { configure } from 'mobx' import { createTestSession } from '../rootModel' import sessionModelFactory from '.' -jest.mock('./makeWorkerInstance', () => () => {}) +jest.mock('../makeWorkerInstance', () => () => {}) // mock warnings to avoid unnecessary outputs beforeEach(() => { From 7b59ce948eed89c93a1ccc3b45109e8e90bd9e41 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Tue, 25 Apr 2023 11:30:55 -0700 Subject: [PATCH 10/44] factor out drawer widgets --- .../product-core/src/Session/DrawerWidgets.ts | 160 ++++++++++++++++++ packages/product-core/src/Session/index.ts | 1 + products/jbrowse-web/src/sessionModel/Base.ts | 19 --- .../jbrowse-web/src/sessionModel/Drawer.ts | 62 ------- .../jbrowse-web/src/sessionModel/index.ts | 85 +--------- 5 files changed, 164 insertions(+), 163 deletions(-) create mode 100644 packages/product-core/src/Session/DrawerWidgets.ts delete mode 100644 products/jbrowse-web/src/sessionModel/Drawer.ts diff --git a/packages/product-core/src/Session/DrawerWidgets.ts b/packages/product-core/src/Session/DrawerWidgets.ts new file mode 100644 index 0000000000..240fb34792 --- /dev/null +++ b/packages/product-core/src/Session/DrawerWidgets.ts @@ -0,0 +1,160 @@ +import { isAlive, types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { localStorageGetItem } from '@jbrowse/core/util' + +const minDrawerWidth = 128 + +export default function DrawerWidgets(pluginManager: PluginManager) { + return types + .model({ + /** + * #property + */ + drawerPosition: types.optional( + types.string, + () => localStorageGetItem('drawerPosition') || 'right', + ), + /** + * #property + */ + drawerWidth: types.optional( + types.refinement(types.integer, width => width >= minDrawerWidth), + 384, + ), + /** + * #property + */ + widgets: types.map( + pluginManager.pluggableMstType('widget', 'stateModel'), + ), + /** + * #property + */ + activeWidgets: types.map( + types.safeReference( + pluginManager.pluggableMstType('widget', 'stateModel'), + ), + ), + + /** + * #property + */ + minimized: types.optional(types.boolean, false), + }) + .views(self => ({ + /** + * #getter + */ + get visibleWidget() { + if (isAlive(self)) { + // returns most recently added item in active widgets + return [...self.activeWidgets.values()][self.activeWidgets.size - 1] + } + return undefined + }, + })) + .actions(self => ({ + /** + * #action + */ + setDrawerPosition(arg: string) { + self.drawerPosition = arg + }, + + /** + * #action + */ + updateDrawerWidth(drawerWidth: number) { + if (drawerWidth === self.drawerWidth) { + return self.drawerWidth + } + let newDrawerWidth = drawerWidth + if (newDrawerWidth < minDrawerWidth) { + newDrawerWidth = minDrawerWidth + } + self.drawerWidth = newDrawerWidth + return newDrawerWidth + }, + + /** + * #action + */ + resizeDrawer(distance: number) { + if (self.drawerPosition === 'left') { + distance *= -1 + } + const oldDrawerWidth = self.drawerWidth + const newDrawerWidth = this.updateDrawerWidth(oldDrawerWidth - distance) + const actualDistance = oldDrawerWidth - newDrawerWidth + return actualDistance + }, + + /** + * #action + */ + addWidget( + typeName: string, + id: string, + initialState = {}, + conf?: unknown, + ) { + const typeDefinition = pluginManager.getElementType('widget', typeName) + if (!typeDefinition) { + throw new Error(`unknown widget type ${typeName}`) + } + const data = { + ...initialState, + id, + type: typeName, + configuration: conf || { type: typeName }, + } + self.widgets.set(id, data) + return self.widgets.get(id) + }, + + /** + * #action + */ + showWidget(widget: any) { + if (self.activeWidgets.has(widget.id)) { + self.activeWidgets.delete(widget.id) + } + self.activeWidgets.set(widget.id, widget) + self.minimized = false + }, + + /** + * #action + */ + hasWidget(widget: any) { + return self.activeWidgets.has(widget.id) + }, + + /** + * #action + */ + hideWidget(widget: any) { + self.activeWidgets.delete(widget.id) + }, + + /** + * #action + */ + minimizeWidgetDrawer() { + self.minimized = true + }, + /** + * #action + */ + showWidgetDrawer() { + self.minimized = false + }, + /** + * #action + */ + hideAllWidgets() { + self.activeWidgets.clear() + }, + })) +} diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts index 00a098bfee..9c50ee8f61 100644 --- a/packages/product-core/src/Session/index.ts +++ b/packages/product-core/src/Session/index.ts @@ -1,2 +1,3 @@ export { default as ReferenceManagement } from './ReferenceManagement' export { default as Connections } from './Connections' +export { default as DrawerWidgets } from './DrawerWidgets' diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts index 261390ea14..73b6c3dbe9 100644 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -22,35 +22,16 @@ export function BaseSession(pluginManager: PluginManager) { * #property */ views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), - /** - * #property - */ - widgets: types.map( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - ), /** * #property */ sessionTracks: types.array( pluginManager.pluggableConfigSchemaType('track'), ), - /** * #property */ sessionPlugins: types.array(types.frozen()), - /** - * #property - */ - minimized: types.optional(types.boolean, false), }) .views(self => ({ /** diff --git a/products/jbrowse-web/src/sessionModel/Drawer.ts b/products/jbrowse-web/src/sessionModel/Drawer.ts deleted file mode 100644 index c37c61de3c..0000000000 --- a/products/jbrowse-web/src/sessionModel/Drawer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { types } from 'mobx-state-tree' - -import PluginManager from '@jbrowse/core/PluginManager' -import { localStorageGetItem } from '@jbrowse/core/util' - -const minDrawerWidth = 128 - -export default function Drawer(pluginManager: PluginManager) { - return types - .model({ - /** - * #property - */ - drawerPosition: types.optional( - types.string, - () => localStorageGetItem('drawerPosition') || 'right', - ), - /** - * #property - */ - drawerWidth: types.optional( - types.refinement(types.integer, width => width >= minDrawerWidth), - 384, - ), - }) - .actions(self => ({ - /** - * #action - */ - setDrawerPosition(arg: string) { - self.drawerPosition = arg - }, - - /** - * #action - */ - updateDrawerWidth(drawerWidth: number) { - if (drawerWidth === self.drawerWidth) { - return self.drawerWidth - } - let newDrawerWidth = drawerWidth - if (newDrawerWidth < minDrawerWidth) { - newDrawerWidth = minDrawerWidth - } - self.drawerWidth = newDrawerWidth - return newDrawerWidth - }, - - /** - * #action - */ - resizeDrawer(distance: number) { - if (self.drawerPosition === 'left') { - distance *= -1 - } - const oldDrawerWidth = self.drawerWidth - const newDrawerWidth = this.updateDrawerWidth(oldDrawerWidth - distance) - const actualDistance = oldDrawerWidth - newDrawerWidth - return actualDistance - }, - })) -} diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index b085c02f84..cafda54902 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -25,7 +25,6 @@ import { getParent, getRoot, getSnapshot, - isAlive, types, Instance, SnapshotIn, @@ -45,7 +44,6 @@ import InfoIcon from '@mui/icons-material/Info' import { BaseSession } from './Base' import Assemblies from './Assemblies' -import Drawer from './Drawer' import SessionConnections from './SessionConnections' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) @@ -71,10 +69,10 @@ export default function sessionModelFactory( const sessionModel = types .compose( BaseSession(pluginManager), - Assemblies(pluginManager, assemblyConfigSchemasType), CoreSession.ReferenceManagement(pluginManager), + CoreSession.DrawerWidgets(pluginManager), + Assemblies(pluginManager, assemblyConfigSchemasType), SessionConnections(pluginManager), - Drawer(pluginManager), ) .named('JBrowseWebSessionModel') .volatile((/* self */) => ({ @@ -258,17 +256,6 @@ export default function sessionModelFactory( theme: this.theme, } }, - - /** - * #getter - */ - get visibleWidget() { - if (isAlive(self)) { - // returns most recently added item in active widgets - return [...self.activeWidgets.values()][self.activeWidgets.size - 1] - } - return undefined - }, })) .actions(self => ({ /** @@ -419,7 +406,7 @@ export default function sessionModelFactory( removeView(view: any) { for (const [, widget] of self.activeWidgets) { if (widget.view && widget.view.id === view.id) { - this.hideWidget(widget) + self.hideWidget(widget) } } self.views.remove(view) @@ -523,72 +510,6 @@ export default function sessionModelFactory( return this.addView(viewType, state) }, - /** - * #action - */ - addWidget( - typeName: string, - id: string, - initialState = {}, - conf?: unknown, - ) { - const typeDefinition = pluginManager.getElementType('widget', typeName) - if (!typeDefinition) { - throw new Error(`unknown widget type ${typeName}`) - } - const data = { - ...initialState, - id, - type: typeName, - configuration: conf || { type: typeName }, - } - self.widgets.set(id, data) - return self.widgets.get(id) - }, - - /** - * #action - */ - showWidget(widget: any) { - if (self.activeWidgets.has(widget.id)) { - self.activeWidgets.delete(widget.id) - } - self.activeWidgets.set(widget.id, widget) - self.minimized = false - }, - - /** - * #action - */ - hasWidget(widget: any) { - return self.activeWidgets.has(widget.id) - }, - - /** - * #action - */ - hideWidget(widget: any) { - self.activeWidgets.delete(widget.id) - }, - /** - * #action - */ - minimizeWidgetDrawer() { - self.minimized = true - }, - /** - * #action - */ - showWidgetDrawer() { - self.minimized = false - }, - /** - * #action - */ - hideAllWidgets() { - self.activeWidgets.clear() - }, - /** * #action * set the global selection, i.e. the globally-selected object. From e9d1e42ad6848d02108424288d703f839d170d62 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Wed, 26 Apr 2023 12:37:05 -0700 Subject: [PATCH 11/44] wip --- .../product-core/src/Session/DialogQueue.ts | 70 ++ .../product-core/src/Session/DrawerWidgets.ts | 14 +- packages/product-core/src/Session/index.ts | 1 + products/jbrowse-desktop/package.json | 1 + products/jbrowse-desktop/src/jbrowseModel.ts | 2 +- products/jbrowse-desktop/src/rootModel.ts | 2 +- .../src/sessionModel/Assemblies.ts | 91 ++ .../jbrowse-desktop/src/sessionModel/Base.ts | 70 ++ .../src/sessionModel/Themes.ts | 65 ++ .../jbrowse-desktop/src/sessionModel/index.ts | 463 +++++++++ .../src/sessionModelFactory.ts | 971 ------------------ 11 files changed, 775 insertions(+), 975 deletions(-) create mode 100644 packages/product-core/src/Session/DialogQueue.ts create mode 100644 products/jbrowse-desktop/src/sessionModel/Assemblies.ts create mode 100644 products/jbrowse-desktop/src/sessionModel/Base.ts create mode 100644 products/jbrowse-desktop/src/sessionModel/Themes.ts create mode 100644 products/jbrowse-desktop/src/sessionModel/index.ts delete mode 100644 products/jbrowse-desktop/src/sessionModelFactory.ts diff --git a/packages/product-core/src/Session/DialogQueue.ts b/packages/product-core/src/Session/DialogQueue.ts new file mode 100644 index 0000000000..4b960b32c1 --- /dev/null +++ b/packages/product-core/src/Session/DialogQueue.ts @@ -0,0 +1,70 @@ +/** MST mixin for managing a queue of dialogs at the level of the session */ + +import PluginManager from '@jbrowse/core/PluginManager' +import { + DialogComponentType, + TrackViewModel, + getContainingView, + isSessionModelWithWidgets, +} from '@jbrowse/core/util' +import { + IAnyStateTreeNode, + getMembers, + getParent, + getSnapshot, + getType, + isModelType, + isReferenceType, + types, + walk, +} from 'mobx-state-tree' + +import type { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' + +export interface ReferringNode { + node: IAnyStateTreeNode + key: string +} + +export default function DialogQueue(pluginManager: PluginManager) { + return types + .model('DialogQueueSessionMixin', {}) + .volatile(() => ({ + queueOfDialogs: [] as [DialogComponentType, any][], + })) + .views(self => ({ + /** + * #getter + */ + get DialogComponent() { + if (self.queueOfDialogs.length) { + const firstInQueue = self.queueOfDialogs[0] + return firstInQueue && firstInQueue[0] + } + return undefined + }, + /** + * #getter + */ + get DialogProps() { + if (self.queueOfDialogs.length) { + const firstInQueue = self.queueOfDialogs[0] + return firstInQueue && firstInQueue[1] + } + return undefined + }, + })) + .actions(self => ({ + /** + * #action + */ + queueDialog( + callback: (doneCallback: () => void) => [DialogComponentType, any], + ): void { + const [component, props] = callback(() => { + self.queueOfDialogs.shift() + }) + self.queueOfDialogs.push([component, props]) + }, + })) +} diff --git a/packages/product-core/src/Session/DrawerWidgets.ts b/packages/product-core/src/Session/DrawerWidgets.ts index 240fb34792..e5cbea7a75 100644 --- a/packages/product-core/src/Session/DrawerWidgets.ts +++ b/packages/product-core/src/Session/DrawerWidgets.ts @@ -1,7 +1,8 @@ -import { isAlive, types } from 'mobx-state-tree' +import { addDisposer, isAlive, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import { localStorageGetItem } from '@jbrowse/core/util' +import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' +import { autorun } from 'mobx' const minDrawerWidth = 128 @@ -156,5 +157,14 @@ export default function DrawerWidgets(pluginManager: PluginManager) { hideAllWidgets() { self.activeWidgets.clear() }, + + afterAttach() { + addDisposer( + self, + autorun(() => { + localStorageSetItem('drawerPosition', self.drawerPosition) + }), + ) + }, })) } diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts index 9c50ee8f61..fd9726931e 100644 --- a/packages/product-core/src/Session/index.ts +++ b/packages/product-core/src/Session/index.ts @@ -1,3 +1,4 @@ export { default as ReferenceManagement } from './ReferenceManagement' export { default as Connections } from './Connections' export { default as DrawerWidgets } from './DrawerWidgets' +export { default as DialogQueue } from './DialogQueue' \ No newline at end of file diff --git a/products/jbrowse-desktop/package.json b/products/jbrowse-desktop/package.json index a28e3be163..6877832469 100644 --- a/products/jbrowse-desktop/package.json +++ b/products/jbrowse-desktop/package.json @@ -75,6 +75,7 @@ "@jbrowse/plugin-variants": "^2.5.0", "@jbrowse/plugin-wiggle": "^2.5.0", "@jbrowse/text-indexing": "^2.5.0", + "@jbrowse/product-core": "^2.5.0", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.10.17", "@mui/x-data-grid": "^6.0.1", diff --git a/products/jbrowse-desktop/src/jbrowseModel.ts b/products/jbrowse-desktop/src/jbrowseModel.ts index da159b4c0e..b266880be4 100644 --- a/products/jbrowse-desktop/src/jbrowseModel.ts +++ b/products/jbrowse-desktop/src/jbrowseModel.ts @@ -12,8 +12,8 @@ import { getSnapshot, resolveIdentifier, } from 'mobx-state-tree' -import { SessionStateModel } from './sessionModelFactory' import JBrowseConfigF from './jbrowseConfig' +import { SessionStateModel } from './sessionModel' // poke some things for testing (this stuff will eventually be removed) // @ts-expect-error diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 7bbebe130d..c439c5e967 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -31,7 +31,7 @@ import RedoIcon from '@mui/icons-material/Redo' import { Save, SaveAs, DNA, Cable } from '@jbrowse/core/ui/Icons' // locals -import sessionModelFactory from './sessionModelFactory' +import sessionModelFactory from './sessionModel' import jobsModelFactory from './indexJobsModel' import JBrowseDesktop from './jbrowseModel' import OpenSequenceDialog from './OpenSequenceDialog' diff --git a/products/jbrowse-desktop/src/sessionModel/Assemblies.ts b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts new file mode 100644 index 0000000000..834775a56b --- /dev/null +++ b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts @@ -0,0 +1,91 @@ +import { Instance, getParent, types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { RootModel } from '../rootModel' + +export default function Assemblies( + pluginManager: PluginManager, + assemblyConfigSchemasType = types.frozen(), +) { + return types + .model({ + /** + * #property + */ + sessionAssemblies: types.array(assemblyConfigSchemasType), + /** + * #property + */ + temporaryAssemblies: types.array(assemblyConfigSchemasType), + }) + .views(self => ({ + /** + * #getter + */ + get assemblies(): AnyConfigurationModel[] { + return getParent(self).jbrowse.assemblies + }, + /** + * #getter + */ + get assemblyNames(): string[] { + return getParent(self).jbrowse.assemblyNames + }, + })) + .actions(self => ({ + /** + * #action + */ + addAssembly(assemblyConfig: Instance) { + self.sessionAssemblies.push(assemblyConfig) + }, + + /** + * #action + */ + removeAssembly(assemblyName: string) { + const index = self.sessionAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.sessionAssemblies.splice(index, 1) + } + }, + + /** + * #action + */ + removeTemporaryAssembly(assemblyName: string) { + const index = self.temporaryAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.temporaryAssemblies.splice(index, 1) + } + }, + + /** + * #action + * used for read vs ref type assemblies + */ + addTemporaryAssembly(assemblyConfig: AnyConfigurationModel) { + const asm = self.sessionAssemblies.find( + f => f.name === assemblyConfig.name, + ) + if (asm) { + console.warn(`Assembly ${assemblyConfig.name} was already existing`) + return asm + } + const length = self.temporaryAssemblies.push(assemblyConfig) + return self.temporaryAssemblies[length - 1] + }, + + /** + * #action + */ + addAssemblyConf(assemblyConf: any) { + return getParent(self).jbrowse.addAssemblyConf(assemblyConf) + }, + })) +} diff --git a/products/jbrowse-desktop/src/sessionModel/Base.ts b/products/jbrowse-desktop/src/sessionModel/Base.ts new file mode 100644 index 0000000000..b1325bc8bd --- /dev/null +++ b/products/jbrowse-desktop/src/sessionModel/Base.ts @@ -0,0 +1,70 @@ +import PluginManager from '@jbrowse/core/PluginManager' +import { Instance, getParent, types } from 'mobx-state-tree' + +export default function BaseSession(pluginManager: PluginManager) { + return types + .model({ + /** + * #property + */ + name: types.identifier, + /** + * #property + */ + margin: 0, + + /** + * #property + */ + views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), + }) + .views(self => ({ + /** + * #getter + */ + get jbrowse() { + return getParent(self).jbrowse + }, + /** + * #getter + */ + get adminMode() { + return true + }, + })) + .volatile((/* self */) => ({ + /** + * this is the globally "selected" object. can be anything. + * code that wants to deal with this should examine it to see what + * kind of thing it is. + */ + selection: undefined as unknown, + /** + * this is the current "task" that is being performed in the UI. + * this is usually an object of the form + * `{ taskName: "configure", target: thing_being_configured }` + */ + task: undefined, + })) + .actions(self => ({ + /** + * #action + * set the global selection, i.e. the globally-selected object. + * can be a feature, a view, just about anything + * @param thing - + */ + setSelection(thing: unknown) { + self.selection = thing + }, + + /** + * #action + * clears the global selection + */ + clearSelection() { + self.selection = undefined + }, + })) +} + +export type BaseSessionModel = Instance> diff --git a/products/jbrowse-desktop/src/sessionModel/Themes.ts b/products/jbrowse-desktop/src/sessionModel/Themes.ts new file mode 100644 index 0000000000..4a297d5a12 --- /dev/null +++ b/products/jbrowse-desktop/src/sessionModel/Themes.ts @@ -0,0 +1,65 @@ +import { addDisposer, types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { getConf } from '@jbrowse/core/configuration' +import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui' +import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' +import type { BaseSessionModel } from './Base' +import { ThemeOptions } from '@mui/material' +import { autorun } from 'mobx' + +type ThemeMap = { [key: string]: ThemeOptions } + +export default function Themes( + pluginManager: PluginManager, + assemblyConfigSchemasType = types.frozen(), +) { + return types + .model({}) + .volatile(() => ({ + sessionThemeName: localStorageGetItem('themeName') || 'default', + })) + .views(s => ({ + /** + * #method + */ + allThemes(): ThemeMap { + const self = s as typeof s & BaseSessionModel + const extraThemes = getConf(self.jbrowse, 'extraThemes') + return { ...defaultThemes, ...extraThemes } + }, + /** + * #getter + */ + get themeName() { + const { sessionThemeName } = s + const all = this.allThemes() + return all[sessionThemeName] ? sessionThemeName : 'default' + }, + /** + * #getter + */ + get theme() { + const self = s as typeof s & BaseSessionModel + const configTheme = getConf(self.jbrowse, 'theme') + const all = this.allThemes() + return createJBrowseTheme(configTheme, all, this.themeName) + }, + })) + .actions(self => ({ + /** + * #action + */ + setThemeName(name: string) { + self.sessionThemeName = name + }, + afterAttach() { + addDisposer( + self, + autorun(() => { + localStorageSetItem('themeName', self.themeName) + }), + ) + }, + })) +} diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts new file mode 100644 index 0000000000..441a03c2ce --- /dev/null +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -0,0 +1,463 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { lazy } from 'react' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { + readConfObject, + isConfigurationModel, +} from '@jbrowse/core/configuration' +import { Region } from '@jbrowse/core/util/types' +import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' +import { localStorageSetItem } from '@jbrowse/core/util' +import { supportedIndexingAdapters } from '@jbrowse/text-indexing' +import { autorun } from 'mobx' +import { + addDisposer, + getParent, + getSnapshot, + isAlive, + types, + IAnyStateTreeNode, + SnapshotIn, +} from 'mobx-state-tree' +import PluginManager from '@jbrowse/core/PluginManager' +import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' + +import { Session as CoreSession } from '@jbrowse/product-core' + +// icons +import SettingsIcon from '@mui/icons-material/Settings' +import CopyIcon from '@mui/icons-material/FileCopy' +import DeleteIcon from '@mui/icons-material/Delete' +import InfoIcon from '@mui/icons-material/Info' +import { Indexing } from '@jbrowse/core/ui/Icons' +import Base from './Base' +import Assemblies from './Assemblies' + +const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) + +export declare interface ReferringNode { + node: IAnyStateTreeNode + key: string +} + +/** + * #stateModel JBrowseDesktopSessionModel + * inherits SnackbarModel + */ +export default function sessionModelFactory( + pluginManager: PluginManager, + assemblyConfigSchemasType = types.frozen(), +) { + const sessionModel = types + .compose( + 'JBrowseDesktopSessionModel', + Base(pluginManager), + CoreSession.ReferenceManagement(pluginManager), + CoreSession.Connections(pluginManager), + CoreSession.DrawerWidgets(pluginManager), + CoreSession.DialogQueue(pluginManager), + Assemblies(pluginManager, assemblyConfigSchemasType), + ) + .views(self => ({ + /** + * #getter + */ + get rpcManager() { + return getParent(self).jbrowse.rpcManager + }, + /** + * #getter + */ + get configuration(): AnyConfigurationModel { + return getParent(self).jbrowse.configuration + }, + /** + * #getter + */ + get tracks(): AnyConfigurationModel[] { + return getParent(self).jbrowse.tracks + }, + /** + * #getter + */ + get textSearchManager(): TextSearchManager { + return getParent(self).textSearchManager + }, + /** + * #getter + */ + get connections() { + return getParent(self).jbrowse.connections + }, + /** + * #getter + */ + get savedSessions() { + return getParent(self).jbrowse.savedSessions + }, + /** + * #getter + */ + get savedSessionNames() { + return getParent(self).jbrowse.savedSessionNames + }, + /** + * #getter + */ + get history() { + return getParent(self).history + }, + /** + * #getter + */ + get menus() { + return getParent(self).menus + }, + + /** + * #getter + */ + get assemblyManager() { + return getParent(self).assemblyManager + }, + /** + * #getter + */ + get version() { + return getParent(self).version + }, + /** + * #method + */ + renderProps() { + return { theme: readConfObject(this.configuration, 'theme') } + }, + /** + * #getter + */ + get visibleWidget() { + if (isAlive(self)) { + // returns most recently added item in active widgets + return [...self.activeWidgets.values()][self.activeWidgets.size - 1] + } + return undefined + }, + })) + .actions(self => ({ + /** + * #action + */ + moveViewUp(id: string) { + const idx = self.views.findIndex(v => v.id === id) + + if (idx === -1) { + return + } + if (idx > 0) { + self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]) + } + }, + /** + * #action + */ + moveViewDown(id: string) { + const idx = self.views.findIndex(v => v.id === id) + + if (idx === -1) { + return + } + + if (idx < self.views.length - 1) { + self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]) + } + }, + + /** + * #action + */ + setDrawerPosition(arg: string) { + self.drawerPosition = arg + localStorage.setItem('drawerPosition', arg) + }, + + /** + * #action + */ + addView(typeName: string, initialState = {}) { + const typeDefinition = pluginManager.getElementType('view', typeName) + if (!typeDefinition) { + throw new Error(`unknown view type ${typeName}`) + } + + const length = self.views.push({ + ...initialState, + type: typeName, + }) + return self.views[length - 1] + }, + + /** + * #action + */ + removeView(view: any) { + for (const [, widget] of self.activeWidgets) { + if (widget.view && widget.view.id === view.id) { + self.hideWidget(widget) + } + } + self.views.remove(view) + }, + + /** + * #action + */ + addTrackConf(trackConf: any) { + return getParent(self).jbrowse.addTrackConf(trackConf) + }, + + /** + * #action + */ + deleteTrackConf(trackConf: AnyConfigurationModel) { + const callbacksToDereferenceTrack: Function[] = [] + const dereferenceTypeCount: Record = {} + const referring = self.getReferring(trackConf) + self.removeReferring( + referring, + trackConf, + callbacksToDereferenceTrack, + dereferenceTypeCount, + ) + callbacksToDereferenceTrack.forEach(cb => cb()) + return getParent(self).jbrowse.deleteTrackConf(trackConf) + }, + + /** + * #action + */ + addLinearGenomeViewOfAssembly(assemblyName: string, initialState = {}) { + return this.addViewOfAssembly( + 'LinearGenomeView', + assemblyName, + initialState, + ) + }, + + /** + * #action + */ + addViewOfAssembly( + viewType: any, + assemblyName: string, + initialState: any = {}, + ) { + const asm = self.assemblies.find( + s => readConfObject(s, 'name') === assemblyName, + ) + if (!asm) { + throw new Error( + `Could not add view of assembly "${assemblyName}", assembly name not found`, + ) + } + return this.addView(viewType, { + ...initialState, + displayRegionsFromAssemblyName: readConfObject(asm, 'name'), + }) + }, + + /** + * #action + */ + addViewFromAnotherView( + viewType: string, + otherView: any, + initialState: { displayedRegions?: Region[] } = {}, + ) { + const state = { ...initialState } + state.displayedRegions = getSnapshot(otherView.displayedRegions) + return this.addView(viewType, state) + }, + + /** + * #action + * opens a configuration editor to configure the given thing, + * and sets the current task to be configuring it + * @param configuration - + */ + editConfiguration(configuration: AnyConfigurationModel) { + if (!isConfigurationModel(configuration)) { + throw new Error( + 'must pass a configuration model to editConfiguration', + ) + } + const editor = self.addWidget( + 'ConfigurationEditorWidget', + 'configEditor', + { target: configuration }, + ) + self.showWidget(editor) + }, + + /** + * #action + */ + editTrackConfiguration(configuration: AnyConfigurationModel) { + this.editConfiguration(configuration) + }, + + /** + * #action + */ + addSavedSession(sessionSnapshot: SnapshotIn) { + return getParent(self).jbrowse.addSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + removeSavedSession(sessionSnapshot: any) { + return getParent(self).jbrowse.removeSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + renameCurrentSession(sessionName: string) { + return getParent(self).renameCurrentSession(sessionName) + }, + + /** + * #action + */ + duplicateCurrentSession() { + return getParent(self).duplicateCurrentSession() + }, + + /** + * #action + */ + activateSession(sessionName: any) { + return getParent(self).activateSession(sessionName) + }, + + /** + * #action + */ + setDefaultSession() { + return getParent(self).setDefaultSession() + }, + + /** + * #action + */ + setSession(sessionSnapshot: SnapshotIn) { + return getParent(self).setSession(sessionSnapshot) + }, + })) + + .views(self => ({ + /** + * #method + */ + getTrackActionMenuItems(config: any) { + const session = self + const trackSnapshot = JSON.parse(JSON.stringify(getSnapshot(config))) + return [ + { + label: 'About track', + onClick: () => { + session.queueDialog(doneCallback => [ + AboutDialog, + { config, handleClose: doneCallback }, + ]) + }, + icon: InfoIcon, + }, + { + label: 'Settings', + onClick: () => session.editConfiguration(config), + icon: SettingsIcon, + }, + { + label: 'Delete track', + onClick: () => { + session.deleteTrackConf(config) + }, + icon: DeleteIcon, + }, + { + label: 'Copy track', + onClick: () => { + const now = Date.now() + trackSnapshot.trackId += `-${now}` + trackSnapshot.displays.forEach((d: { displayId: string }) => { + d.displayId += `-${now}` + }) + trackSnapshot.name += ' (copy)' + trackSnapshot.category = undefined + session.addTrackConf(trackSnapshot) + }, + icon: CopyIcon, + }, + { + label: trackSnapshot.textSearching + ? 'Re-index track' + : 'Index track', + disabled: !supportedIndexingAdapters(trackSnapshot.adapter.type), + onClick: () => { + const rootModel = getParent(self) + const { jobsManager } = rootModel + const { trackId, assemblyNames, textSearching, name } = + trackSnapshot + const indexName = `${name}-index` + // TODO: open jobs list widget + jobsManager.queueJob({ + indexingParams: { + attributes: textSearching?.indexingAttributes || [ + 'Name', + 'ID', + ], + exclude: textSearching?.indexingFeatureTypesToExclude || [ + 'CDS', + 'exon', + ], + assemblies: assemblyNames, + tracks: [trackId], + indexType: 'perTrack', + timestamp: new Date().toISOString(), + name: indexName, + }, + name: indexName, + cancelCallback: () => jobsManager.abortJob(), + }) + }, + icon: Indexing, + }, + ] + }, + })) + + const extendedSessionModel = pluginManager.evaluateExtensionPoint( + 'Core-extendSession', + sessionModel, + ) as typeof sessionModel + + return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { + // @ts-expect-error + preProcessor(snapshot) { + if (snapshot) { + // @ts-expect-error + const { connectionInstances, ...rest } = snapshot || {} + // connectionInstances schema changed from object to an array, so any + // old connectionInstances as object is in snapshot, filter it out + // https://github.com/GMOD/jbrowse-components/issues/1903 + if (!Array.isArray(connectionInstances)) { + return rest + } + } + return snapshot + }, + }) +} + +export type SessionStateModel = ReturnType diff --git a/products/jbrowse-desktop/src/sessionModelFactory.ts b/products/jbrowse-desktop/src/sessionModelFactory.ts deleted file mode 100644 index a2d67cff65..0000000000 --- a/products/jbrowse-desktop/src/sessionModelFactory.ts +++ /dev/null @@ -1,971 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { lazy } from 'react' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import { - readConfObject, - isConfigurationModel, - getConf, -} from '@jbrowse/core/configuration' -import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui/theme' -import { - Region, - TrackViewModel, - DialogComponentType, -} from '@jbrowse/core/util/types' -import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { - getContainingView, - localStorageGetItem, - localStorageSetItem, -} from '@jbrowse/core/util' -import { supportedIndexingAdapters } from '@jbrowse/text-indexing' -import { autorun, observable } from 'mobx' -import { - addDisposer, - getMembers, - getParent, - getSnapshot, - getType, - isAlive, - isModelType, - isReferenceType, - types, - walk, - IAnyStateTreeNode, - SnapshotIn, -} from 'mobx-state-tree' -import PluginManager from '@jbrowse/core/PluginManager' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' - -// icons -import SettingsIcon from '@mui/icons-material/Settings' -import CopyIcon from '@mui/icons-material/FileCopy' -import DeleteIcon from '@mui/icons-material/Delete' -import InfoIcon from '@mui/icons-material/Info' -import { Indexing } from '@jbrowse/core/ui/Icons' -import { ThemeOptions } from '@mui/material' - -const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) - -export declare interface ReferringNode { - node: IAnyStateTreeNode - key: string -} - -type ThemeMap = { [key: string]: ThemeOptions } - -/** - * #stateModel JBrowseDesktopSessionModel - * inherits SnackbarModel - */ -export default function sessionModelFactory( - pluginManager: PluginManager, - assemblyConfigSchemasType = types.frozen(), -) { - const minDrawerWidth = 128 - const sessionModel = types - .model('JBrowseDesktopSessionModel', { - /** - * #property - */ - name: types.identifier, - /** - * #property - */ - margin: 0, - /** - * #property - */ - drawerWidth: types.optional( - types.refinement(types.integer, width => width >= minDrawerWidth), - 384, - ), - /** - * #property - */ - views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), - /** - * #property - */ - widgets: types.map( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - ), - /** - * #property - */ - connectionInstances: types.array( - pluginManager.pluggableMstType('connection', 'stateModel'), - ), - /** - * #property - */ - sessionAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - temporaryAssemblies: types.array(assemblyConfigSchemasType), - - /** - * #property - */ - minimized: types.optional(types.boolean, false), - - /** - * #property - */ - drawerPosition: types.optional( - types.string, - () => localStorageGetItem('drawerPosition') || 'right', - ), - }) - .volatile((/* self */) => ({ - sessionThemeName: localStorageGetItem('themeName') || 'default', - /** - * this is the globally "selected" object. can be anything. - * code that wants to deal with this should examine it to see what - * kind of thing it is. - */ - selection: undefined, - /** - * this is the current "task" that is being performed in the UI. - * this is usually an object of the form - * `{ taskName: "configure", target: thing_being_configured }` - */ - task: undefined, - queueOfDialogs: observable.array([] as [DialogComponentType, any][]), - })) - .views(self => ({ - /** - * #getter - */ - get jbrowse() { - return getParent(self).jbrowse - }, - })) - .views(self => ({ - /** - * #method - */ - allThemes(): ThemeMap { - const extraThemes = getConf(self.jbrowse, 'extraThemes') - return { ...defaultThemes, ...extraThemes } - }, - })) - .views(self => ({ - /** - * #getter - */ - get themeName() { - const { sessionThemeName } = self - const all = self.allThemes() - return all[sessionThemeName] ? sessionThemeName : 'default' - }, - })) - .views(self => ({ - /** - * #getter - */ - get theme() { - const configTheme = getConf(self.jbrowse, 'theme') - const all = self.allThemes() - return createJBrowseTheme(configTheme, all, self.themeName) - }, - - /** - * #getter - */ - get DialogComponent() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[0] - } - return undefined - }, - /** - * #getter - */ - get DialogProps() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[1] - } - return undefined - }, - /** - * #getter - */ - get rpcManager() { - return getParent(self).jbrowse.rpcManager - }, - /** - * #getter - */ - get configuration(): AnyConfigurationModel { - return getParent(self).jbrowse.configuration - }, - /** - * #getter - */ - get assemblies(): AnyConfigurationModel[] { - return getParent(self).jbrowse.assemblies - }, - /** - * #getter - */ - get assemblyNames(): string[] { - return getParent(self).jbrowse.assemblyNames - }, - /** - * #getter - */ - get tracks(): AnyConfigurationModel[] { - return getParent(self).jbrowse.tracks - }, - /** - * #getter - */ - get textSearchManager(): TextSearchManager { - return getParent(self).textSearchManager - }, - /** - * #getter - */ - get connections() { - return getParent(self).jbrowse.connections - }, - /** - * #getter - */ - get savedSessions() { - return getParent(self).jbrowse.savedSessions - }, - /** - * #getter - */ - get savedSessionNames() { - return getParent(self).jbrowse.savedSessionNames - }, - /** - * #getter - */ - get history() { - return getParent(self).history - }, - /** - * #getter - */ - get menus() { - return getParent(self).menus - }, - - /** - * #getter - */ - get assemblyManager() { - return getParent(self).assemblyManager - }, - /** - * #getter - */ - get version() { - return getParent(self).version - }, - /** - * #method - */ - renderProps() { - return { theme: readConfObject(this.configuration, 'theme') } - }, - /** - * #getter - */ - get visibleWidget() { - if (isAlive(self)) { - // returns most recently added item in active widgets - return [...self.activeWidgets.values()][self.activeWidgets.size - 1] - } - return undefined - }, - - /** - * #getter - */ - get adminMode() { - return true - }, - /** - * #method - * See if any MST nodes currently have a types.reference to this object. - * - * @param object - object - * - * @returns An array where the first element is the node referring to the - * object and the second element is they property name the node is using to - * refer to the object - */ - getReferring(object: IAnyStateTreeNode) { - const refs: ReferringNode[] = [] - walk(getParent(self), node => { - if (isModelType(getType(node))) { - const members = getMembers(node) - Object.entries(members.properties).forEach(([key, value]) => { - // @ts-ignore - if (isReferenceType(value) && node[key] === object) { - refs.push({ node, key }) - } - }) - } - }) - return refs - }, - })) - .actions(self => ({ - /** - * #action - */ - setThemeName(name: string) { - self.sessionThemeName = name - }, - /** - * #action - */ - moveViewUp(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - if (idx > 0) { - self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]) - } - }, - /** - * #action - */ - moveViewDown(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - - if (idx < self.views.length - 1) { - self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]) - } - }, - - /** - * #action - */ - setDrawerPosition(arg: string) { - self.drawerPosition = arg - localStorage.setItem('drawerPosition', arg) - }, - - /** - * #action - */ - queueDialog( - callback: (doneCallback: () => void) => [DialogComponentType, any], - ): void { - const [component, props] = callback(() => { - self.queueOfDialogs.shift() - }) - self.queueOfDialogs.push([component, props]) - }, - - /** - * #action - */ - makeConnection( - configuration: AnyConfigurationModel, - initialSnapshot = {}, - ) { - const { type } = configuration - if (!type) { - throw new Error('track configuration has no `type` listed') - } - const name = readConfObject(configuration, 'name') - const connectionType = pluginManager.getConnectionType(type) - if (!connectionType) { - throw new Error(`unknown connection type ${type}`) - } - const connectionData = { - ...initialSnapshot, - name, - type, - configuration, - } - const length = self.connectionInstances.push(connectionData) - return self.connectionInstances[length - 1] - }, - - /** - * #action - */ - prepareToBreakConnection(configuration: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - if (connection) { - connection.tracks.forEach((track: any) => { - const referring = self.getReferring(track) - this.removeReferring( - referring, - track, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - }) - const safelyBreakConnection = () => { - callbacksToDereferenceTrack.forEach(cb => cb()) - this.breakConnection(configuration) - } - return [safelyBreakConnection, dereferenceTypeCount] - } - return undefined - }, - - /** - * #action - */ - breakConnection(configuration: AnyConfigurationModel) { - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - self.connectionInstances.remove(connection) - }, - - /** - * #action - */ - deleteConnection(configuration: AnyConfigurationModel) { - return getParent(self).jbrowse.deleteConnectionConf(configuration) - }, - - /** - * #action - */ - updateDrawerWidth(drawerWidth: number) { - if (drawerWidth === self.drawerWidth) { - return self.drawerWidth - } - let newDrawerWidth = drawerWidth - if (newDrawerWidth < minDrawerWidth) { - newDrawerWidth = minDrawerWidth - } - self.drawerWidth = newDrawerWidth - return newDrawerWidth - }, - - /** - * #action - */ - resizeDrawer(distance: number) { - const oldDrawerWidth = self.drawerWidth - const newDrawerWidth = this.updateDrawerWidth(oldDrawerWidth - distance) - const actualDistance = oldDrawerWidth - newDrawerWidth - return actualDistance - }, - - /** - * #action - */ - addView(typeName: string, initialState = {}) { - const typeDefinition = pluginManager.getElementType('view', typeName) - if (!typeDefinition) { - throw new Error(`unknown view type ${typeName}`) - } - - const length = self.views.push({ - ...initialState, - type: typeName, - }) - return self.views[length - 1] - }, - - /** - * #action - */ - removeView(view: any) { - for (const [, widget] of self.activeWidgets) { - if (widget.view && widget.view.id === view.id) { - this.hideWidget(widget) - } - } - self.views.remove(view) - }, - - /** - * #action - */ - addAssembly(assemblyConfig: any) { - self.sessionAssemblies.push(assemblyConfig) - }, - - /** - * #action - */ - removeAssembly(assemblyName: string) { - const index = self.sessionAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.sessionAssemblies.splice(index, 1) - } - }, - - /** - * #action - */ - removeTemporaryAssembly(assemblyName: string) { - const index = self.temporaryAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.temporaryAssemblies.splice(index, 1) - } - }, - - /** - * #action - * used for read vs ref type assemblies - */ - addTemporaryAssembly(assemblyConfig: AnyConfigurationModel) { - const asm = self.sessionAssemblies.find( - f => f.name === assemblyConfig.name, - ) - if (asm) { - console.warn(`Assembly ${assemblyConfig.name} was already existing`) - return asm - } - const length = self.temporaryAssemblies.push(assemblyConfig) - return self.temporaryAssemblies[length - 1] - }, - - /** - * #action - */ - addAssemblyConf(assemblyConf: any) { - return getParent(self).jbrowse.addAssemblyConf(assemblyConf) - }, - - /** - * #action - */ - addTrackConf(trackConf: any) { - return getParent(self).jbrowse.addTrackConf(trackConf) - }, - - /** - * #action - */ - hasWidget(widget: any) { - return self.activeWidgets.has(widget.id) - }, - - /** - * #action - */ - removeReferring( - referring: any, - track: any, - callbacks: Function[], - dereferenceTypeCount: Record, - ) { - referring.forEach(({ node }: ReferringNode) => { - let dereferenced = false - try { - // If a view is referring to the track config, remove the track - // from the view - const type = 'open track(s)' - const view = getContainingView(node) as TrackViewModel - callbacks.push(() => view.hideTrack(track.trackId)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } catch (err1) { - // ignore - } - - if (self.widgets.has(node.id)) { - // If a configuration editor widget has the track config - // open, close the widget - const type = 'configuration editor widget(s)' - callbacks.push(() => this.hideWidget(node)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } - if (!dereferenced) { - throw new Error( - `Error when closing this connection, the following node is still referring to a track configuration: ${JSON.stringify( - getSnapshot(node), - )}`, - ) - } - }) - }, - - /** - * #action - */ - deleteTrackConf(trackConf: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const referring = self.getReferring(trackConf) - this.removeReferring( - referring, - trackConf, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - callbacksToDereferenceTrack.forEach(cb => cb()) - return getParent(self).jbrowse.deleteTrackConf(trackConf) - }, - - /** - * #action - */ - addConnectionConf(connectionConf: any) { - return getParent(self).jbrowse.addConnectionConf(connectionConf) - }, - - /** - * #action - */ - addLinearGenomeViewOfAssembly(assemblyName: string, initialState = {}) { - return this.addViewOfAssembly( - 'LinearGenomeView', - assemblyName, - initialState, - ) - }, - - /** - * #action - */ - addViewOfAssembly( - viewType: any, - assemblyName: string, - initialState: any = {}, - ) { - const asm = self.assemblies.find( - s => readConfObject(s, 'name') === assemblyName, - ) - if (!asm) { - throw new Error( - `Could not add view of assembly "${assemblyName}", assembly name not found`, - ) - } - return this.addView(viewType, { - ...initialState, - displayRegionsFromAssemblyName: readConfObject(asm, 'name'), - }) - }, - - /** - * #action - */ - addViewFromAnotherView( - viewType: string, - otherView: any, - initialState: { displayedRegions?: Region[] } = {}, - ) { - const state = { ...initialState } - state.displayedRegions = getSnapshot(otherView.displayedRegions) - return this.addView(viewType, state) - }, - - /** - * #action - */ - addWidget( - typeName: string, - id: string, - initialState = {}, - conf?: unknown, - ) { - const typeDefinition = pluginManager.getElementType('widget', typeName) - if (!typeDefinition) { - throw new Error(`unknown widget type ${typeName}`) - } - const data = { - ...initialState, - id, - type: typeName, - configuration: conf || { type: typeName }, - } - self.widgets.set(id, data) - return self.widgets.get(id) - }, - - /** - * #action - */ - showWidget(widget: any) { - if (self.activeWidgets.has(widget.id)) { - self.activeWidgets.delete(widget.id) - } - self.activeWidgets.set(widget.id, widget) - }, - - /** - * #action - */ - hideWidget(widget: any) { - self.activeWidgets.delete(widget.id) - }, - - /** - * #action - */ - minimizeWidgetDrawer() { - self.minimized = true - }, - - /** - * #action - */ - showWidgetDrawer() { - self.minimized = false - }, - - /** - * #action - */ - hideAllWidgets() { - self.activeWidgets.clear() - }, - - /** - * #action - * set the global selection, i.e. the globally-selected object. - * can be a feature, a view, just about anything - * @param thing - - */ - setSelection(thing: any) { - self.selection = thing - }, - - /** - * #action - * clears the global selection - */ - clearSelection() { - self.selection = undefined - }, - - /** - * #action - * opens a configuration editor to configure the given thing, - * and sets the current task to be configuring it - * @param configuration - - */ - editConfiguration(configuration: AnyConfigurationModel) { - if (!isConfigurationModel(configuration)) { - throw new Error( - 'must pass a configuration model to editConfiguration', - ) - } - const editor = this.addWidget( - 'ConfigurationEditorWidget', - 'configEditor', - { target: configuration }, - ) - this.showWidget(editor) - }, - - /** - * #action - */ - editTrackConfiguration(configuration: AnyConfigurationModel) { - this.editConfiguration(configuration) - }, - - /** - * #action - */ - clearConnections() { - self.connectionInstances.length = 0 - }, - - /** - * #action - */ - addSavedSession(sessionSnapshot: SnapshotIn) { - return getParent(self).jbrowse.addSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - removeSavedSession(sessionSnapshot: any) { - return getParent(self).jbrowse.removeSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return getParent(self).renameCurrentSession(sessionName) - }, - - /** - * #action - */ - duplicateCurrentSession() { - return getParent(self).duplicateCurrentSession() - }, - - /** - * #action - */ - activateSession(sessionName: any) { - return getParent(self).activateSession(sessionName) - }, - - /** - * #action - */ - setDefaultSession() { - return getParent(self).setDefaultSession() - }, - - /** - * #action - */ - setSession(sessionSnapshot: SnapshotIn) { - return getParent(self).setSession(sessionSnapshot) - }, - })) - - .views(self => ({ - /** - * #method - */ - getTrackActionMenuItems(config: any) { - const session = self - const trackSnapshot = JSON.parse(JSON.stringify(getSnapshot(config))) - return [ - { - label: 'About track', - onClick: () => { - session.queueDialog(doneCallback => [ - AboutDialog, - { config, handleClose: doneCallback }, - ]) - }, - icon: InfoIcon, - }, - { - label: 'Settings', - onClick: () => session.editConfiguration(config), - icon: SettingsIcon, - }, - { - label: 'Delete track', - onClick: () => { - session.deleteTrackConf(config) - }, - icon: DeleteIcon, - }, - { - label: 'Copy track', - onClick: () => { - const now = Date.now() - trackSnapshot.trackId += `-${now}` - trackSnapshot.displays.forEach((d: { displayId: string }) => { - d.displayId += `-${now}` - }) - trackSnapshot.name += ' (copy)' - trackSnapshot.category = undefined - session.addTrackConf(trackSnapshot) - }, - icon: CopyIcon, - }, - { - label: trackSnapshot.textSearching - ? 'Re-index track' - : 'Index track', - disabled: !supportedIndexingAdapters(trackSnapshot.adapter.type), - onClick: () => { - const rootModel = getParent(self) - const { jobsManager } = rootModel - const { trackId, assemblyNames, textSearching, name } = - trackSnapshot - const indexName = `${name}-index` - // TODO: open jobs list widget - jobsManager.queueJob({ - indexingParams: { - attributes: textSearching?.indexingAttributes || [ - 'Name', - 'ID', - ], - exclude: textSearching?.indexingFeatureTypesToExclude || [ - 'CDS', - 'exon', - ], - assemblies: assemblyNames, - tracks: [trackId], - indexType: 'perTrack', - timestamp: new Date().toISOString(), - name: indexName, - }, - name: indexName, - cancelCallback: () => jobsManager.abortJob(), - }) - }, - icon: Indexing, - }, - ] - }, - })) - .actions(self => ({ - afterAttach() { - addDisposer( - self, - autorun(() => { - localStorageSetItem('drawerPosition', self.drawerPosition) - localStorageSetItem('themeName', self.themeName) - }), - ) - }, - })) - - const extendedSessionModel = pluginManager.evaluateExtensionPoint( - 'Core-extendSession', - sessionModel, - ) as typeof sessionModel - - return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { - // @ts-expect-error - preProcessor(snapshot) { - if (snapshot) { - // @ts-expect-error - const { connectionInstances, ...rest } = snapshot || {} - // connectionInstances schema changed from object to an array, so any - // old connectionInstances as object is in snapshot, filter it out - // https://github.com/GMOD/jbrowse-components/issues/1903 - if (!Array.isArray(connectionInstances)) { - return rest - } - } - return snapshot - }, - }) -} - -export type SessionStateModel = ReturnType From 63e6b52203f535ce776d25aaef82089e12655bd0 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Thu, 27 Apr 2023 11:44:58 -0700 Subject: [PATCH 12/44] wip --- packages/core/configuration/index.ts | 1 + packages/core/configuration/types.ts | 7 +- .../models/baseTrackConfig.ts | 4 +- .../pluggableElementTypes/models/index.ts | 2 +- packages/product-core/package.json | 4 +- packages/product-core/src/Session/Base.ts | 61 ++ .../product-core/src/Session/DialogQueue.ts | 25 +- .../product-core/src/Session/DrawerWidgets.ts | 26 +- .../product-core/src/Session}/Themes.ts | 7 +- packages/product-core/src/Session/Tracks.ts | 54 ++ packages/product-core/src/Session/Views.ts | 120 ++++ packages/product-core/src/Session/index.ts | 6 +- packages/product-core/src/index.ts | 1 + .../jbrowse-desktop/src/indexJobsModel.ts | 1 + products/jbrowse-desktop/src/rootModel.ts | 12 +- .../src/sessionModel/Assemblies.ts | 6 + .../jbrowse-desktop/src/sessionModel/Base.ts | 26 +- .../src/sessionModel/TrackMenu.ts | 100 +++ .../jbrowse-desktop/src/sessionModel/index.ts | 360 +--------- products/jbrowse-desktop/tsconfig.json | 2 +- .../src/createModel/createSessionModel.ts | 7 - .../src/sessionModel/Assemblies.ts | 111 ++- products/jbrowse-web/src/sessionModel/Base.ts | 55 +- .../jbrowse-web/src/sessionModel/index.ts | 634 +++++------------- 24 files changed, 744 insertions(+), 888 deletions(-) create mode 100644 packages/product-core/src/Session/Base.ts rename {products/jbrowse-desktop/src/sessionModel => packages/product-core/src/Session}/Themes.ts (91%) create mode 100644 packages/product-core/src/Session/Tracks.ts create mode 100644 packages/product-core/src/Session/Views.ts create mode 100644 products/jbrowse-desktop/src/sessionModel/TrackMenu.ts diff --git a/packages/core/configuration/index.ts b/packages/core/configuration/index.ts index 3c16a5f099..9e3e0619f7 100644 --- a/packages/core/configuration/index.ts +++ b/packages/core/configuration/index.ts @@ -8,6 +8,7 @@ export type { AnyConfigurationModel, AnyConfigurationSlot, AnyConfigurationSlotType, + AnyConfiguration, } from './types' export * from './util' diff --git a/packages/core/configuration/types.ts b/packages/core/configuration/types.ts index 66ca700c67..03d2c37ba7 100644 --- a/packages/core/configuration/types.ts +++ b/packages/core/configuration/types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { IStateTreeNode, Instance } from 'mobx-state-tree' +import type { IStateTreeNode, Instance, SnapshotOut } from 'mobx-state-tree' import type { ConfigurationSchemaType, ConfigurationSchemaOptions, @@ -61,5 +61,10 @@ export type AnyConfigurationModel = Instance export type AnyConfigurationSlotType = ReturnType export type AnyConfigurationSlot = Instance +/** any configuration model, or snapshot thereof */ +export type AnyConfiguration = + | AnyConfigurationModel + | SnapshotOut + export type ConfigurationModel = Instance diff --git a/packages/core/pluggableElementTypes/models/baseTrackConfig.ts b/packages/core/pluggableElementTypes/models/baseTrackConfig.ts index 640f3b7e6c..6859223090 100644 --- a/packages/core/pluggableElementTypes/models/baseTrackConfig.ts +++ b/packages/core/pluggableElementTypes/models/baseTrackConfig.ts @@ -185,5 +185,5 @@ export function createBaseTrackConfig(pluginManager: PluginManager) { ) } -export type BaseTrackConfigModel = ReturnType -export type BaseTrackConfig = Instance +export type BaseTrackConfigSchema = ReturnType +export type BaseTrackConfig = Instance diff --git a/packages/core/pluggableElementTypes/models/index.ts b/packages/core/pluggableElementTypes/models/index.ts index 39e01c4f8e..2930bac85d 100644 --- a/packages/core/pluggableElementTypes/models/index.ts +++ b/packages/core/pluggableElementTypes/models/index.ts @@ -16,4 +16,4 @@ export { BaseInternetAccountConfig } from './baseInternetAccountConfig' export { createBaseTrackModel } from './BaseTrackModel' export type { BaseTrackModel, BaseTrackStateModel } from './BaseTrackModel' export { createBaseTrackConfig } from './baseTrackConfig' -export type { BaseTrackConfig, BaseTrackConfigModel } from './baseTrackConfig' +export type { BaseTrackConfig, BaseTrackConfigSchema as BaseTrackConfigModel } from './baseTrackConfig' diff --git a/packages/product-core/package.json b/packages/product-core/package.json index 1d6ee396be..6023851cf7 100644 --- a/packages/product-core/package.json +++ b/packages/product-core/package.json @@ -42,7 +42,9 @@ "useSrc": "node ../../scripts/useSrc.js" }, "dependencies": { - "@babel/runtime": "^7.16.3" + "@babel/runtime": "^7.16.3", + "@mui/material": "^5.10.17", + "shortid": "^2.2.15" }, "peerDependencies": { "mobx": "^6.0.0", diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts new file mode 100644 index 0000000000..e2f8862484 --- /dev/null +++ b/packages/product-core/src/Session/Base.ts @@ -0,0 +1,61 @@ +import shortid from 'shortid' + +import type PluginManager from '@jbrowse/core/PluginManager' +import { getParent, types } from 'mobx-state-tree' +import { RootModel } from '../RootModel' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' + +/** base session shared by **all** JBrowse products. Be careful what you include here, everything will use it. */ +export default function BaseSession( + pluginManager: PluginManager, + defaultAdminMode = true, +) { + return types + .model({ + /** + * #property + */ + id: types.optional(types.identifier, shortid()), + /** + * #property + */ + name: types.identifier, + }) + .volatile(() => ({ + /** + * #volatile + * Boolean indicating whether the session is in admin mode or not + */ + adminMode: defaultAdminMode, + /** + * #volatile + * this is the globally "selected" object. can be anything. + * code that wants to deal with this should examine it to see what + * kind of thing it is. + */ + selection: undefined as unknown, + })) + .views(self => ({ + get root() { + return getParent(self) + }, + /** + * #getter + */ + get jbrowse() { + return this.root.jbrowse + }, + /** + * #getter + */ + get rpcManager() { + return this.jbrowse.rpcManager + }, + /** + * #getter + */ + get configuration(): AnyConfigurationModel { + return this.jbrowse.configuration + }, + })) +} diff --git a/packages/product-core/src/Session/DialogQueue.ts b/packages/product-core/src/Session/DialogQueue.ts index 4b960b32c1..8f41b61d79 100644 --- a/packages/product-core/src/Session/DialogQueue.ts +++ b/packages/product-core/src/Session/DialogQueue.ts @@ -1,25 +1,8 @@ /** MST mixin for managing a queue of dialogs at the level of the session */ import PluginManager from '@jbrowse/core/PluginManager' -import { - DialogComponentType, - TrackViewModel, - getContainingView, - isSessionModelWithWidgets, -} from '@jbrowse/core/util' -import { - IAnyStateTreeNode, - getMembers, - getParent, - getSnapshot, - getType, - isModelType, - isReferenceType, - types, - walk, -} from 'mobx-state-tree' - -import type { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' +import { DialogComponentType } from '@jbrowse/core/util' +import { IAnyStateTreeNode, Instance, types } from 'mobx-state-tree' export interface ReferringNode { node: IAnyStateTreeNode @@ -61,6 +44,8 @@ export default function DialogQueue(pluginManager: PluginManager) { queueDialog( callback: (doneCallback: () => void) => [DialogComponentType, any], ): void { + // NOTE: this base implementation doesn't include the changes from #2469, + // hoping it's not needed anymore const [component, props] = callback(() => { self.queueOfDialogs.shift() }) @@ -68,3 +53,5 @@ export default function DialogQueue(pluginManager: PluginManager) { }, })) } + +export type DialogQueueManager = Instance> diff --git a/packages/product-core/src/Session/DrawerWidgets.ts b/packages/product-core/src/Session/DrawerWidgets.ts index e5cbea7a75..1ac5504da8 100644 --- a/packages/product-core/src/Session/DrawerWidgets.ts +++ b/packages/product-core/src/Session/DrawerWidgets.ts @@ -1,8 +1,9 @@ -import { addDisposer, isAlive, types } from 'mobx-state-tree' +import { Instance, addDisposer, isAlive, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' import { autorun } from 'mobx' +import { AnyConfigurationModel, isConfigurationModel } from '@jbrowse/core/configuration' const minDrawerWidth = 128 @@ -61,6 +62,7 @@ export default function DrawerWidgets(pluginManager: PluginManager) { */ setDrawerPosition(arg: string) { self.drawerPosition = arg + localStorage.setItem('drawerPosition', arg) }, /** @@ -158,6 +160,26 @@ export default function DrawerWidgets(pluginManager: PluginManager) { self.activeWidgets.clear() }, + /** + * #action + * opens a configuration editor to configure the given thing, + * and sets the current task to be configuring it + * @param configuration - + */ + editConfiguration(configuration: AnyConfigurationModel) { + if (!isConfigurationModel(configuration)) { + throw new Error( + 'must pass a configuration model to editConfiguration', + ) + } + const editor = this.addWidget( + 'ConfigurationEditorWidget', + 'configEditor', + { target: configuration }, + ) + this.showWidget(editor) + }, + afterAttach() { addDisposer( self, @@ -168,3 +190,5 @@ export default function DrawerWidgets(pluginManager: PluginManager) { }, })) } + +export type DrawerWidgetManager = Instance> \ No newline at end of file diff --git a/products/jbrowse-desktop/src/sessionModel/Themes.ts b/packages/product-core/src/Session/Themes.ts similarity index 91% rename from products/jbrowse-desktop/src/sessionModel/Themes.ts rename to packages/product-core/src/Session/Themes.ts index 4a297d5a12..b5c1685f32 100644 --- a/products/jbrowse-desktop/src/sessionModel/Themes.ts +++ b/packages/product-core/src/Session/Themes.ts @@ -4,16 +4,13 @@ import PluginManager from '@jbrowse/core/PluginManager' import { getConf } from '@jbrowse/core/configuration' import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' -import type { BaseSessionModel } from './Base' +import type { BaseSessionModel } from '../../../../products/jbrowse-desktop/src/sessionModel/Base' import { ThemeOptions } from '@mui/material' import { autorun } from 'mobx' type ThemeMap = { [key: string]: ThemeOptions } -export default function Themes( - pluginManager: PluginManager, - assemblyConfigSchemasType = types.frozen(), -) { +export default function Themes(pluginManager: PluginManager) { return types .model({}) .volatile(() => ({ diff --git a/packages/product-core/src/Session/Tracks.ts b/packages/product-core/src/Session/Tracks.ts new file mode 100644 index 0000000000..7621ed68ec --- /dev/null +++ b/packages/product-core/src/Session/Tracks.ts @@ -0,0 +1,54 @@ +import { Instance, addDisposer, getParent, types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { AnyConfigurationModel, getConf } from '@jbrowse/core/configuration' +import type { BaseSessionModel } from '../../../../products/jbrowse-desktop/src/sessionModel/Base' +import { ThemeOptions } from '@mui/material' +import { autorun } from 'mobx' +import DrawerWidgets from './DrawerWidgets' +import BaseSession from './Base' + +export default function Tracks(pluginManager: PluginManager) { + return types + .compose('TracksManagerSessionMixin', + BaseSession(pluginManager), + ReferenceManagement(pluginManager) + ) + .views(self => ({ + /** + * #getter + */ + get tracks(): AnyConfigurationModel[] { + return getParent(self).jbrowse.tracks + }, + })) + .actions(self => ({ + /** + * #action + */ + addTrackConf(trackConf: any) { + return getParent(self).jbrowse.addTrackConf(trackConf) + }, + + /** + * #action + */ + deleteTrackConf(trackConf: AnyConfigurationModel) { + const callbacksToDereferenceTrack: Function[] = [] + const dereferenceTypeCount: Record = {} + const referring = self.getReferring(trackConf) + self.removeReferring( + referring, + trackConf, + callbacksToDereferenceTrack, + dereferenceTypeCount, + ) + callbacksToDereferenceTrack.forEach(cb => cb()) + if (self.adminMode) { + return getParent(self).jbrowse.deleteTrackConf(trackConf) + } + }, + })) +} + +export type TracksManager = Instance> \ No newline at end of file diff --git a/packages/product-core/src/Session/Views.ts b/packages/product-core/src/Session/Views.ts new file mode 100644 index 0000000000..a64373f3e2 --- /dev/null +++ b/packages/product-core/src/Session/Views.ts @@ -0,0 +1,120 @@ +import { addDisposer, getParent, getSnapshot, types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { AnyConfigurationModel, getConf, readConfObject } from '@jbrowse/core/configuration' +import type { BaseSessionModel } from '../../../../products/jbrowse-desktop/src/sessionModel/Base' +import { autorun } from 'mobx' +import { Region } from '@jbrowse/core/util' + +export default function Views(pluginManager: PluginManager) { + return types + .model({ + /** + * #property + */ + views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), + }) + .actions(self => ({ + /** + * #action + */ + moveViewUp(id: string) { + const idx = self.views.findIndex(v => v.id === id) + + if (idx === -1) { + return + } + if (idx > 0) { + self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]) + } + }, + /** + * #action + */ + moveViewDown(id: string) { + const idx = self.views.findIndex(v => v.id === id) + + if (idx === -1) { + return + } + + if (idx < self.views.length - 1) { + self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]) + } + }, + + /** + * #action + */ + addView(typeName: string, initialState = {}) { + const typeDefinition = pluginManager.getElementType('view', typeName) + if (!typeDefinition) { + throw new Error(`unknown view type ${typeName}`) + } + + const length = self.views.push({ + ...initialState, + type: typeName, + }) + return self.views[length - 1] + }, + + /** + * #action + */ + removeView(view: any) { + for (const [, widget] of self.activeWidgets) { + if (widget.view && widget.view.id === view.id) { + self.hideWidget(widget) + } + } + self.views.remove(view) + }, + + /** + * #action + */ + addLinearGenomeViewOfAssembly(assemblyName: string, initialState = {}) { + return this.addViewOfAssembly( + 'LinearGenomeView', + assemblyName, + initialState, + ) + }, + + /** + * #action + */ + addViewOfAssembly( + viewType: any, + assemblyName: string, + initialState: any = {}, + ) { + const asm = self.assemblies.find( + s => readConfObject(s, 'name') === assemblyName, + ) + if (!asm) { + throw new Error( + `Could not add view of assembly "${assemblyName}", assembly name not found`, + ) + } + return this.addView(viewType, { + ...initialState, + displayRegionsFromAssemblyName: readConfObject(asm, 'name'), + }) + }, + + /** + * #action + */ + addViewFromAnotherView( + viewType: string, + otherView: any, + initialState: { displayedRegions?: Region[] } = {}, + ) { + const state = { ...initialState } + state.displayedRegions = getSnapshot(otherView.displayedRegions) + return this.addView(viewType, state) + }, + })) +} diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts index fd9726931e..1e4b81ae93 100644 --- a/packages/product-core/src/Session/index.ts +++ b/packages/product-core/src/Session/index.ts @@ -1,4 +1,8 @@ export { default as ReferenceManagement } from './ReferenceManagement' export { default as Connections } from './Connections' export { default as DrawerWidgets } from './DrawerWidgets' -export { default as DialogQueue } from './DialogQueue' \ No newline at end of file +export { default as DialogQueue } from './DialogQueue' +export { default as Themes } from './Themes' +export { default as Tracks } from './Tracks' +export { default as Views } from './Views' +export { default as Base } from './Base' diff --git a/packages/product-core/src/index.ts b/packages/product-core/src/index.ts index 247733160f..a99dd78d20 100644 --- a/packages/product-core/src/index.ts +++ b/packages/product-core/src/index.ts @@ -1 +1,2 @@ +export * from './RootModel' export * as Session from './Session' diff --git a/products/jbrowse-desktop/src/indexJobsModel.ts b/products/jbrowse-desktop/src/indexJobsModel.ts index 850425d475..eead608d47 100644 --- a/products/jbrowse-desktop/src/indexJobsModel.ts +++ b/products/jbrowse-desktop/src/indexJobsModel.ts @@ -23,6 +23,7 @@ interface TrackTextIndexing { assemblies: string[] tracks: string[] // trackIds indexType: string + timestamp?: string } interface JobsEntry { diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index c439c5e967..07c8968cd0 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -19,6 +19,8 @@ import { MenuItem } from '@jbrowse/core/ui' import { AnyConfigurationModel } from '@jbrowse/core/configuration' import { UriLocation } from '@jbrowse/core/util/types' +import type { RootModel as BaseRootModel } from '@jbrowse/product-core' + // icons import OpenIcon from '@mui/icons-material/FolderOpen' import ExtensionIcon from '@mui/icons-material/Extension' @@ -40,7 +42,7 @@ const { ipcRenderer } = window.require('electron') const PreferencesDialog = lazy(() => import('./PreferencesDialog')) -function getSaveSession(model: RootModel) { +function getSaveSession(model: BaseRootModel) { return { ...getSnapshot(model.jbrowse), defaultSession: model.session ? getSnapshot(model.session) : {}, @@ -308,7 +310,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { onClick: async () => { if (self.session) { try { - await self.saveSession(getSaveSession(self as RootModel)) + await self.saveSession(getSaveSession(self)) } catch (e) { console.error(e) self.session?.notify(`${e}`, 'error') @@ -325,7 +327,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { 'promptSessionSaveAs', ) self.setSessionPath(saveAsPath) - await self.saveSession(getSaveSession(self as RootModel)) + await self.saveSession(getSaveSession(self)) } catch (e) { console.error(e) self.session?.notify(`${e}`, 'error') @@ -505,7 +507,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { }, async setPluginsUpdated() { if (self.session) { - await self.saveSession(getSaveSession(self as RootModel)) + await self.saveSession(getSaveSession(self)) } await self.openNewSessionCallback(self.sessionPath) }, @@ -690,7 +692,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { async () => { if (self.session) { try { - await self.saveSession(getSaveSession(self as RootModel)) + await self.saveSession(getSaveSession(self)) } catch (e) { console.error(e) } diff --git a/products/jbrowse-desktop/src/sessionModel/Assemblies.ts b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts index 834775a56b..3d960f1e3d 100644 --- a/products/jbrowse-desktop/src/sessionModel/Assemblies.ts +++ b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts @@ -32,6 +32,12 @@ export default function Assemblies( get assemblyNames(): string[] { return getParent(self).jbrowse.assemblyNames }, + /** + * #getter + */ + get assemblyManager() { + return getParent(self).assemblyManager + }, })) .actions(self => ({ /** diff --git a/products/jbrowse-desktop/src/sessionModel/Base.ts b/products/jbrowse-desktop/src/sessionModel/Base.ts index b1325bc8bd..da0868652f 100644 --- a/products/jbrowse-desktop/src/sessionModel/Base.ts +++ b/products/jbrowse-desktop/src/sessionModel/Base.ts @@ -1,9 +1,10 @@ import PluginManager from '@jbrowse/core/PluginManager' -import { Instance, getParent, types } from 'mobx-state-tree' +import { Instance, types } from 'mobx-state-tree' +import { Session as CoreSession } from '@jbrowse/product-core' export default function BaseSession(pluginManager: PluginManager) { - return types - .model({ + return CoreSession.Base(pluginManager) + .props({ /** * #property */ @@ -12,33 +13,22 @@ export default function BaseSession(pluginManager: PluginManager) { * #property */ margin: 0, - - /** - * #property - */ - views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), }) .views(self => ({ /** * #getter */ - get jbrowse() { - return getParent(self).jbrowse + get adminMode() { + return true }, /** * #getter */ - get adminMode() { - return true + get version() { + return self.root.version }, })) .volatile((/* self */) => ({ - /** - * this is the globally "selected" object. can be anything. - * code that wants to deal with this should examine it to see what - * kind of thing it is. - */ - selection: undefined as unknown, /** * this is the current "task" that is being performed in the UI. * this is usually an object of the form diff --git a/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts new file mode 100644 index 0000000000..5aefc0bd86 --- /dev/null +++ b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts @@ -0,0 +1,100 @@ +import { getParent, getSnapshot, types } from 'mobx-state-tree' + +import SettingsIcon from '@mui/icons-material/Settings' +import CopyIcon from '@mui/icons-material/FileCopy' +import DeleteIcon from '@mui/icons-material/Delete' +import InfoIcon from '@mui/icons-material/Info' +import { Indexing } from '@jbrowse/core/ui/Icons' + +import PluginManager from '@jbrowse/core/PluginManager' +import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' +import { supportedIndexingAdapters } from '@jbrowse/text-indexing' +import { lazy } from 'react' + +import type { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' +import type { TracksManager } from '@jbrowse/product-core/src/Session/Tracks' +import type { DrawerWidgetManager } from '@jbrowse/product-core/src/Session/DrawerWidgets' +import { RootModel } from '../rootModel' + +const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) + +export default function TrackMenu(pluginManager: PluginManager) { + return types.model({}).views(self => ({ + /** + * #method + */ + getTrackActionMenuItems(trackConfig: BaseTrackConfig) { + const session = self as DialogQueueManager & + TracksManager & + DrawerWidgetManager + const trackSnapshot = JSON.parse(JSON.stringify(getSnapshot(trackConfig))) + return [ + { + label: 'About track', + onClick: () => { + session.queueDialog(doneCallback => [ + AboutDialog, + { config: trackConfig, handleClose: doneCallback }, + ]) + }, + icon: InfoIcon, + }, + { + label: 'Settings', + onClick: () => session.editConfiguration(trackConfig), + icon: SettingsIcon, + }, + { + label: 'Delete track', + onClick: () => { + session.deleteTrackConf(trackConfig) + }, + icon: DeleteIcon, + }, + { + label: 'Copy track', + onClick: () => { + const now = Date.now() + trackSnapshot.trackId += `-${now}` + trackSnapshot.displays.forEach((d: { displayId: string }) => { + d.displayId += `-${now}` + }) + trackSnapshot.name += ' (copy)' + trackSnapshot.category = undefined + session.addTrackConf(trackSnapshot) + }, + icon: CopyIcon, + }, + { + label: trackSnapshot.textSearching ? 'Re-index track' : 'Index track', + disabled: !supportedIndexingAdapters(trackSnapshot.adapter.type), + onClick: () => { + const rootModel = getParent(self) + const { jobsManager } = rootModel + const { trackId, assemblyNames, textSearching, name } = + trackSnapshot + const indexName = `${name}-index` + // TODO: open jobs list widget + jobsManager?.queueJob({ + indexingParams: { + attributes: textSearching?.indexingAttributes || ['Name', 'ID'], + exclude: textSearching?.indexingFeatureTypesToExclude || [ + 'CDS', + 'exon', + ], + assemblies: assemblyNames, + tracks: [trackId], + indexType: 'perTrack', + timestamp: new Date().toISOString(), + name: indexName, + }, + name: indexName, + cancelCallback: () => jobsManager.abortJob(), + }) + }, + icon: Indexing, + }, + ] + }, + })) +} diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 441a03c2ce..9f7009f9d1 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -1,39 +1,16 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { lazy } from 'react' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import { - readConfObject, - isConfigurationModel, -} from '@jbrowse/core/configuration' -import { Region } from '@jbrowse/core/util/types' +import { readConfObject } from '@jbrowse/core/configuration' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { localStorageSetItem } from '@jbrowse/core/util' -import { supportedIndexingAdapters } from '@jbrowse/text-indexing' -import { autorun } from 'mobx' -import { - addDisposer, - getParent, - getSnapshot, - isAlive, - types, - IAnyStateTreeNode, - SnapshotIn, -} from 'mobx-state-tree' +import { types, IAnyStateTreeNode, SnapshotIn } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import { Session as CoreSession } from '@jbrowse/product-core' // icons -import SettingsIcon from '@mui/icons-material/Settings' -import CopyIcon from '@mui/icons-material/FileCopy' -import DeleteIcon from '@mui/icons-material/Delete' -import InfoIcon from '@mui/icons-material/Info' -import { Indexing } from '@jbrowse/core/ui/Icons' import Base from './Base' import Assemblies from './Assemblies' - -const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) +import TrackMenu from './TrackMenu' +import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' export declare interface ReferringNode { node: IAnyStateTreeNode @@ -52,388 +29,112 @@ export default function sessionModelFactory( .compose( 'JBrowseDesktopSessionModel', Base(pluginManager), - CoreSession.ReferenceManagement(pluginManager), - CoreSession.Connections(pluginManager), - CoreSession.DrawerWidgets(pluginManager), - CoreSession.DialogQueue(pluginManager), + types.compose( + CoreSession.ReferenceManagement(pluginManager), + CoreSession.Connections(pluginManager), + CoreSession.DrawerWidgets(pluginManager), + CoreSession.DialogQueue(pluginManager), + CoreSession.Themes(pluginManager), + CoreSession.Tracks(pluginManager), + CoreSession.Views(pluginManager), + ), Assemblies(pluginManager, assemblyConfigSchemasType), + TrackMenu(pluginManager), ) .views(self => ({ - /** - * #getter - */ - get rpcManager() { - return getParent(self).jbrowse.rpcManager - }, - /** - * #getter - */ - get configuration(): AnyConfigurationModel { - return getParent(self).jbrowse.configuration - }, - /** - * #getter - */ - get tracks(): AnyConfigurationModel[] { - return getParent(self).jbrowse.tracks - }, /** * #getter */ get textSearchManager(): TextSearchManager { - return getParent(self).textSearchManager - }, - /** - * #getter - */ - get connections() { - return getParent(self).jbrowse.connections + self.connections + return self.root.textSearchManager }, /** * #getter */ get savedSessions() { - return getParent(self).jbrowse.savedSessions + return self.jbrowse.savedSessions }, /** * #getter */ get savedSessionNames() { - return getParent(self).jbrowse.savedSessionNames + return self.jbrowse.savedSessionNames }, /** * #getter */ get history() { - return getParent(self).history + return self.root.history }, /** * #getter */ get menus() { - return getParent(self).menus - }, - - /** - * #getter - */ - get assemblyManager() { - return getParent(self).assemblyManager - }, - /** - * #getter - */ - get version() { - return getParent(self).version + return self.root.menus }, /** * #method */ renderProps() { - return { theme: readConfObject(this.configuration, 'theme') } - }, - /** - * #getter - */ - get visibleWidget() { - if (isAlive(self)) { - // returns most recently added item in active widgets - return [...self.activeWidgets.values()][self.activeWidgets.size - 1] - } - return undefined + return { theme: readConfObject(self.configuration, 'theme') } }, })) .actions(self => ({ /** * #action */ - moveViewUp(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - if (idx > 0) { - self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]) - } - }, - /** - * #action - */ - moveViewDown(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - - if (idx < self.views.length - 1) { - self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]) - } - }, - - /** - * #action - */ - setDrawerPosition(arg: string) { - self.drawerPosition = arg - localStorage.setItem('drawerPosition', arg) - }, - - /** - * #action - */ - addView(typeName: string, initialState = {}) { - const typeDefinition = pluginManager.getElementType('view', typeName) - if (!typeDefinition) { - throw new Error(`unknown view type ${typeName}`) - } - - const length = self.views.push({ - ...initialState, - type: typeName, - }) - return self.views[length - 1] - }, - - /** - * #action - */ - removeView(view: any) { - for (const [, widget] of self.activeWidgets) { - if (widget.view && widget.view.id === view.id) { - self.hideWidget(widget) - } - } - self.views.remove(view) - }, - - /** - * #action - */ - addTrackConf(trackConf: any) { - return getParent(self).jbrowse.addTrackConf(trackConf) - }, - - /** - * #action - */ - deleteTrackConf(trackConf: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const referring = self.getReferring(trackConf) - self.removeReferring( - referring, - trackConf, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - callbacksToDereferenceTrack.forEach(cb => cb()) - return getParent(self).jbrowse.deleteTrackConf(trackConf) - }, - - /** - * #action - */ - addLinearGenomeViewOfAssembly(assemblyName: string, initialState = {}) { - return this.addViewOfAssembly( - 'LinearGenomeView', - assemblyName, - initialState, - ) - }, - - /** - * #action - */ - addViewOfAssembly( - viewType: any, - assemblyName: string, - initialState: any = {}, - ) { - const asm = self.assemblies.find( - s => readConfObject(s, 'name') === assemblyName, - ) - if (!asm) { - throw new Error( - `Could not add view of assembly "${assemblyName}", assembly name not found`, - ) - } - return this.addView(viewType, { - ...initialState, - displayRegionsFromAssemblyName: readConfObject(asm, 'name'), - }) - }, - - /** - * #action - */ - addViewFromAnotherView( - viewType: string, - otherView: any, - initialState: { displayedRegions?: Region[] } = {}, - ) { - const state = { ...initialState } - state.displayedRegions = getSnapshot(otherView.displayedRegions) - return this.addView(viewType, state) - }, - - /** - * #action - * opens a configuration editor to configure the given thing, - * and sets the current task to be configuring it - * @param configuration - - */ - editConfiguration(configuration: AnyConfigurationModel) { - if (!isConfigurationModel(configuration)) { - throw new Error( - 'must pass a configuration model to editConfiguration', - ) - } - const editor = self.addWidget( - 'ConfigurationEditorWidget', - 'configEditor', - { target: configuration }, - ) - self.showWidget(editor) - }, - - /** - * #action - */ - editTrackConfiguration(configuration: AnyConfigurationModel) { - this.editConfiguration(configuration) + editTrackConfiguration(configuration: BaseTrackConfig) { + self.editConfiguration(configuration) }, /** * #action */ addSavedSession(sessionSnapshot: SnapshotIn) { - return getParent(self).jbrowse.addSavedSession(sessionSnapshot) + return self.root.addSavedSession(sessionSnapshot) }, /** * #action */ removeSavedSession(sessionSnapshot: any) { - return getParent(self).jbrowse.removeSavedSession(sessionSnapshot) + return self.root.removeSavedSession(sessionSnapshot) }, /** * #action */ renameCurrentSession(sessionName: string) { - return getParent(self).renameCurrentSession(sessionName) + return self.root.renameCurrentSession(sessionName) }, /** * #action */ duplicateCurrentSession() { - return getParent(self).duplicateCurrentSession() + return self.root.duplicateCurrentSession() }, /** * #action */ activateSession(sessionName: any) { - return getParent(self).activateSession(sessionName) + return self.root.activateSession(sessionName) }, /** * #action */ setDefaultSession() { - return getParent(self).setDefaultSession() + return self.root.setDefaultSession() }, /** * #action */ setSession(sessionSnapshot: SnapshotIn) { - return getParent(self).setSession(sessionSnapshot) - }, - })) - - .views(self => ({ - /** - * #method - */ - getTrackActionMenuItems(config: any) { - const session = self - const trackSnapshot = JSON.parse(JSON.stringify(getSnapshot(config))) - return [ - { - label: 'About track', - onClick: () => { - session.queueDialog(doneCallback => [ - AboutDialog, - { config, handleClose: doneCallback }, - ]) - }, - icon: InfoIcon, - }, - { - label: 'Settings', - onClick: () => session.editConfiguration(config), - icon: SettingsIcon, - }, - { - label: 'Delete track', - onClick: () => { - session.deleteTrackConf(config) - }, - icon: DeleteIcon, - }, - { - label: 'Copy track', - onClick: () => { - const now = Date.now() - trackSnapshot.trackId += `-${now}` - trackSnapshot.displays.forEach((d: { displayId: string }) => { - d.displayId += `-${now}` - }) - trackSnapshot.name += ' (copy)' - trackSnapshot.category = undefined - session.addTrackConf(trackSnapshot) - }, - icon: CopyIcon, - }, - { - label: trackSnapshot.textSearching - ? 'Re-index track' - : 'Index track', - disabled: !supportedIndexingAdapters(trackSnapshot.adapter.type), - onClick: () => { - const rootModel = getParent(self) - const { jobsManager } = rootModel - const { trackId, assemblyNames, textSearching, name } = - trackSnapshot - const indexName = `${name}-index` - // TODO: open jobs list widget - jobsManager.queueJob({ - indexingParams: { - attributes: textSearching?.indexingAttributes || [ - 'Name', - 'ID', - ], - exclude: textSearching?.indexingFeatureTypesToExclude || [ - 'CDS', - 'exon', - ], - assemblies: assemblyNames, - tracks: [trackId], - indexType: 'perTrack', - timestamp: new Date().toISOString(), - name: indexName, - }, - name: indexName, - cancelCallback: () => jobsManager.abortJob(), - }) - }, - icon: Indexing, - }, - ] + return self.root.setSession(sessionSnapshot) }, })) @@ -443,7 +144,6 @@ export default function sessionModelFactory( ) as typeof sessionModel return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { - // @ts-expect-error preProcessor(snapshot) { if (snapshot) { // @ts-expect-error diff --git a/products/jbrowse-desktop/tsconfig.json b/products/jbrowse-desktop/tsconfig.json index abdd6b8790..113a46c4d8 100644 --- a/products/jbrowse-desktop/tsconfig.json +++ b/products/jbrowse-desktop/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.json", - "include": ["src"], + "include": ["src", "../../packages/product-core/src/Session/Themes.ts"], "compilerOptions": { "jsx": "react-jsx", "noEmit": true diff --git a/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts b/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts index ee2204a718..87dd8e7fe2 100644 --- a/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts +++ b/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts @@ -90,13 +90,6 @@ export default function sessionModelFactory(pluginManager: PluginManager) { * kind of thing it is. */ selection: undefined, - /** - * this is the current "task" that is being performed in the UI. - * this is usually an object of the form - * `{ taskName: "configure", target: thing_being_configured }` - */ - task: undefined, - queueOfDialogs: [] as [DialogComponentType, any][], })) .views(self => ({ diff --git a/products/jbrowse-web/src/sessionModel/Assemblies.ts b/products/jbrowse-web/src/sessionModel/Assemblies.ts index f969fc8ea3..9142cae78f 100644 --- a/products/jbrowse-web/src/sessionModel/Assemblies.ts +++ b/products/jbrowse-web/src/sessionModel/Assemblies.ts @@ -1,19 +1,108 @@ -import { types } from 'mobx-state-tree' +import { getParent, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' +import { + AnyConfiguration, + AnyConfigurationModel, + readConfObject, +} from '@jbrowse/core/configuration' +import { BaseSessionModel } from './Base' export default function Assemblies( pluginManager: PluginManager, assemblyConfigSchemasType = types.frozen(), ) { - return types.model({ - /** - * #property - */ - sessionAssemblies: types.array(assemblyConfigSchemasType), - /** - * #property - */ - temporaryAssemblies: types.array(assemblyConfigSchemasType), - }) + return types + .model({ + /** + * #property + */ + sessionAssemblies: types.array(assemblyConfigSchemasType), + /** + * #property + */ + temporaryAssemblies: types.array(assemblyConfigSchemasType), + }) + .views(self => ({ + /** + * #getter + */ + get assemblies(): AnyConfigurationModel[] { + return (self as typeof self & BaseSessionModel).jbrowse.assemblies + }, + /** + * #getter + */ + get assemblyNames(): string[] { + const { assemblyNames } = getParent(self).jbrowse + const sessionAssemblyNames = self.sessionAssemblies.map(assembly => + readConfObject(assembly, 'name'), + ) + return [...assemblyNames, ...sessionAssemblyNames] + }, + /** + * #getter + */ + get assemblyManager() { + return getParent(self).assemblyManager + }, + })) + .actions(self => ({ + /** + * #action + */ + addAssembly(conf: AnyConfiguration) { + const asm = self.sessionAssemblies.find(f => f.name === conf.name) + if (asm) { + console.warn(`Assembly ${conf.name} was already existing`) + return asm + } + const length = self.sessionAssemblies.push(conf) + return self.sessionAssemblies[length - 1] + }, + + /** + * #action + * used for read vs ref type assemblies. + */ + addTemporaryAssembly(conf: AnyConfiguration) { + const asm = self.sessionAssemblies.find(f => f.name === conf.name) + if (asm) { + console.warn(`Assembly ${conf.name} was already existing`) + return asm + } + const length = self.temporaryAssemblies.push(conf) + return self.temporaryAssemblies[length - 1] + }, + + /** + * #action + */ + addAssemblyConf(assemblyConf: AnyConfiguration) { + return getParent(self).jbrowse.addAssemblyConf(assemblyConf) + }, + + /** + * #action + */ + removeAssembly(assemblyName: string) { + const index = self.sessionAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.sessionAssemblies.splice(index, 1) + } + }, + /** + * #action + */ + removeTemporaryAssembly(assemblyName: string) { + const index = self.temporaryAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.temporaryAssemblies.splice(index, 1) + } + }, + })) } diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts index 73b6c3dbe9..7c51dad090 100644 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -1,27 +1,18 @@ import { Instance, getParent, types } from 'mobx-state-tree' -import shortid from 'shortid' import PluginManager from '@jbrowse/core/PluginManager' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import RpcManager from '@jbrowse/core/rpc/RpcManager' + +import { Session as CoreSession } from '@jbrowse/product-core' export function BaseSession(pluginManager: PluginManager) { - const BaseSession = types - .model({ - /** - * #property - */ - id: types.optional(types.identifier, shortid()), - /** - * #property - */ - name: types.string, + const BaseSession = CoreSession.Base(pluginManager) + .props({ /** * #property */ margin: 0, - /** - * #property - */ - views: types.array(pluginManager.pluggableMstType('view', 'stateModel')), /** * #property */ @@ -34,6 +25,26 @@ export function BaseSession(pluginManager: PluginManager) { sessionPlugins: types.array(types.frozen()), }) .views(self => ({ + /** + * #getter + */ + get jbrowse() { + return getParent(self).jbrowse + }, + })) + .views(self => ({ + /** + * #getter + */ + get rpcManager() { + return self.jbrowse.rpcManager as RpcManager + }, + /** + * #getter + */ + get configuration(): AnyConfigurationModel { + return self.jbrowse.configuration + }, /** * #getter */ @@ -41,6 +52,20 @@ export function BaseSession(pluginManager: PluginManager) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return getParent(self).adminMode }, + /** + * #getter + */ + get tracks(): AnyConfigurationModel[] { + return [...self.sessionTracks, ...getParent(self).jbrowse.tracks] + }, + })) + .actions(self => ({ + /** + * #action + */ + setName(str: string) { + self.name = str + }, })) return BaseSession } diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index cafda54902..9671f283e4 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { lazy } from 'react' import clone from 'clone' -import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui/theme' import { PluginDefinition } from '@jbrowse/core/PluginLoader' import { readConfObject, getConf, isConfigurationModel, AnyConfigurationModel, + AnyConfiguration, } from '@jbrowse/core/configuration' import { Region, @@ -16,9 +16,8 @@ import { DialogComponentType, } from '@jbrowse/core/util/types' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { ThemeOptions } from '@mui/material' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' -import { autorun, observable } from 'mobx' +import { autorun } from 'mobx' import { addDisposer, cast, @@ -45,6 +44,7 @@ import InfoIcon from '@mui/icons-material/Info' import { BaseSession } from './Base' import Assemblies from './Assemblies' import SessionConnections from './SessionConnections' +import { BaseTrackConfig, BaseTrackConfigModel } from '@jbrowse/core/pluggableElementTypes' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) @@ -52,12 +52,6 @@ export declare interface ReactProps { [key: string]: any } -type AnyConfiguration = - | AnyConfigurationModel - | SnapshotOut - -type ThemeMap = { [key: string]: ThemeOptions } - /** * #stateModel JBrowseWebSessionModel * inherits SnackbarModel @@ -68,13 +62,17 @@ export default function sessionModelFactory( ) { const sessionModel = types .compose( + 'JBrowseWebSessionModel', BaseSession(pluginManager), CoreSession.ReferenceManagement(pluginManager), CoreSession.DrawerWidgets(pluginManager), + CoreSession.DialogQueue(pluginManager), + CoreSession.Themes(pluginManager), + CoreSession.Views(pluginManager), + CoreSession.Tracks(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), SessionConnections(pluginManager), ) - .named('JBrowseWebSessionModel') .volatile((/* self */) => ({ /** * #volatile @@ -94,111 +92,14 @@ export default function sessionModelFactory( * `{ taskName: "configure", target: thing_being_configured }` */ task: undefined, - /** - * #volatile - */ - queueOfDialogs: observable.array( - [] as [DialogComponentType, ReactProps][], - ), - })) - .views(self => ({ - /** - * #getter - */ - get jbrowse() { - return getParent(self).jbrowse - }, - })) - .views(self => ({ - /** - * #method - */ - allThemes(): ThemeMap { - const extraThemes = getConf(self.jbrowse, 'extraThemes') - return { ...defaultThemes, ...extraThemes } - }, - })) - .views(self => ({ - /** - * #getter - */ - get themeName() { - const { sessionThemeName } = self - const all = self.allThemes() - return all[sessionThemeName] ? sessionThemeName : 'default' - }, })) .views(self => ({ - /** - * #getter - */ - get theme() { - const configTheme = getConf(self.jbrowse, 'theme') - const all = self.allThemes() - return createJBrowseTheme(configTheme, all, self.themeName) - }, - - /** - * #getter - */ - get DialogComponent() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[0] - } - return undefined - }, - /** - * #getter - */ - get DialogProps() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[1] - } - return undefined - }, /** * #getter */ get shareURL() { return getConf(self.jbrowse, 'shareURL') }, - /** - * #getter - */ - get rpcManager() { - return self.jbrowse.rpcManager as RpcManager - }, - - /** - * #getter - */ - get configuration(): AnyConfigurationModel { - return self.jbrowse.configuration - }, - /** - * #getter - */ - get assemblies(): AnyConfigurationModel[] { - return self.jbrowse.assemblies - }, - /** - * #getter - */ - get assemblyNames(): string[] { - const { assemblyNames } = getParent(self).jbrowse - const sessionAssemblyNames = self.sessionAssemblies.map(assembly => - readConfObject(assembly, 'name'), - ) - return [...assemblyNames, ...sessionAssemblyNames] - }, - /** - * #getter - */ - get tracks(): AnyConfigurationModel[] { - return [...self.sessionTracks, ...getParent(self).jbrowse.tracks] - }, /** * #getter */ @@ -235,12 +136,6 @@ export default function sessionModelFactory( get menus() { return getParent(self).menus }, - /** - * #getter - */ - get assemblyManager() { - return getParent(self).assemblyManager - }, /** * #getter */ @@ -253,366 +148,166 @@ export default function sessionModelFactory( */ renderProps() { return { - theme: this.theme, + theme: self.theme, } }, })) - .actions(self => ({ - /** - * #action - */ - setThemeName(name: string) { - self.sessionThemeName = name - }, - /** - * #action - */ - moveViewUp(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - if (idx > 0) { - self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]) - } - }, - /** - * #action - */ - moveViewDown(id: string) { - const idx = self.views.findIndex(v => v.id === id) - - if (idx === -1) { - return - } - - if (idx < self.views.length - 1) { - self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]) - } - }, - /** - * #action - */ - queueDialog( - callback: ( - doneCallback: () => void, - ) => [DialogComponentType, ReactProps], - ) { - const [component, props] = callback(() => self.queueOfDialogs.shift()) - self.queueOfDialogs.push([component, props]) - }, - /** - * #action - */ - setName(str: string) { - self.name = str - }, - - /** - * #action - */ - addAssembly(conf: AnyConfiguration) { - const asm = self.sessionAssemblies.find(f => f.name === conf.name) - if (asm) { - console.warn(`Assembly ${conf.name} was already existing`) - return asm - } - const length = self.sessionAssemblies.push(conf) - return self.sessionAssemblies[length - 1] - }, - - /** - * #action - * used for read vs ref type assemblies. - */ - addTemporaryAssembly(conf: AnyConfiguration) { - const asm = self.sessionAssemblies.find(f => f.name === conf.name) - if (asm) { - console.warn(`Assembly ${conf.name} was already existing`) - return asm - } - const length = self.temporaryAssemblies.push(conf) - return self.temporaryAssemblies[length - 1] - }, - /** - * #action - */ - addSessionPlugin(plugin: JBrowsePlugin) { - if (self.sessionPlugins.some(p => p.name === plugin.name)) { - throw new Error('session plugin cannot be installed twice') - } - self.sessionPlugins.push(plugin) - getRoot(self).setPluginsUpdated(true) - }, - /** - * #action - */ - removeAssembly(assemblyName: string) { - const index = self.sessionAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.sessionAssemblies.splice(index, 1) - } - }, - /** - * #action - */ - removeTemporaryAssembly(assemblyName: string) { - const index = self.temporaryAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.temporaryAssemblies.splice(index, 1) - } - }, - /** - * #action - */ - removeSessionPlugin(pluginDefinition: PluginDefinition) { - self.sessionPlugins = cast( - self.sessionPlugins.filter( - plugin => - plugin.url !== pluginDefinition.url || - plugin.umdUrl !== pluginDefinition.umdUrl || - plugin.cjsUrl !== pluginDefinition.cjsUrl || - plugin.esmUrl !== pluginDefinition.esmUrl, - ), - ) - const rootModel = getParent(self) - rootModel.setPluginsUpdated(true) - }, - - /** - * #action - */ - addView(typeName: string, initialState = {}) { - const typeDefinition = pluginManager.getElementType('view', typeName) - if (!typeDefinition) { - throw new Error(`unknown view type ${typeName}`) - } - - const length = self.views.push({ - ...initialState, - type: typeName, - }) - return self.views[length - 1] - }, - - /** - * #action - */ - removeView(view: any) { - for (const [, widget] of self.activeWidgets) { - if (widget.view && widget.view.id === view.id) { - self.hideWidget(widget) + .actions(self => { + const super_addTrackConf = self.addTrackConf + const super_deletetrackConf = self.deleteTrackConf + return { + /** + * #action + */ + addSessionPlugin(plugin: JBrowsePlugin) { + if (self.sessionPlugins.some(p => p.name === plugin.name)) { + throw new Error('session plugin cannot be installed twice') } - } - self.views.remove(view) - }, - - /** - * #action - */ - addAssemblyConf(assemblyConf: AnyConfiguration) { - return getParent(self).jbrowse.addAssemblyConf(assemblyConf) - }, - - /** - * #action - */ - addTrackConf(trackConf: AnyConfiguration) { - if (self.adminMode) { - return getParent(self).jbrowse.addTrackConf(trackConf) - } - const { trackId, type } = trackConf as { type: string; trackId: string } - if (!type) { - throw new Error(`unknown track type ${type}`) - } - const track = self.sessionTracks.find((t: any) => t.trackId === trackId) - if (track) { - return track - } - const length = self.sessionTracks.push(trackConf) - return self.sessionTracks[length - 1] - }, - - /** - * #action - */ - deleteTrackConf(trackConf: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const referring = self.getReferring(trackConf) - self.removeReferring( - referring, - trackConf, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - callbacksToDereferenceTrack.forEach(cb => cb()) - if (self.adminMode) { - return getParent(self).jbrowse.deleteTrackConf(trackConf) - } - const { trackId } = trackConf - const idx = self.sessionTracks.findIndex(t => t.trackId === trackId) - if (idx === -1) { - return undefined - } - return self.sessionTracks.splice(idx, 1) - }, - - /** - * #action - */ - addLinearGenomeViewOfAssembly(assemblyName: string, initialState = {}) { - return this.addViewOfAssembly( - 'LinearGenomeView', - assemblyName, - initialState, - ) - }, - - /** - * #action - */ - addViewOfAssembly( - viewType: any, - assemblyName: string, - initialState: any = {}, - ) { - const assembly = self.assemblies.find( - s => readConfObject(s, 'name') === assemblyName, - ) - if (!assembly) { - throw new Error( - `Could not add view of assembly "${assemblyName}", assembly name not found`, + self.sessionPlugins.push(plugin) + getRoot(self).setPluginsUpdated(true) + }, + + /** + * #action + */ + removeSessionPlugin(pluginDefinition: PluginDefinition) { + self.sessionPlugins = cast( + self.sessionPlugins.filter( + plugin => + plugin.url !== pluginDefinition.url || + plugin.umdUrl !== pluginDefinition.umdUrl || + plugin.cjsUrl !== pluginDefinition.cjsUrl || + plugin.esmUrl !== pluginDefinition.esmUrl, + ), ) - } - initialState.displayRegionsFromAssemblyName = readConfObject( - assembly, - 'name', - ) - return this.addView(viewType, initialState) - }, - - /** - * #action - */ - addViewFromAnotherView( - viewType: string, - otherView: any, - initialState: { displayedRegions?: Region[] } = {}, - ) { - const state = { ...initialState } - state.displayedRegions = getSnapshot(otherView.displayedRegions) - return this.addView(viewType, state) - }, - - /** - * #action - * set the global selection, i.e. the globally-selected object. - * can be a feature, a view, just about anything - * @param thing - - */ - setSelection(thing: any) { - self.selection = thing - }, - - /** - * #action - * clears the global selection - */ - clearSelection() { - self.selection = undefined - }, - - /** - * #action - */ - addSavedSession(sessionSnapshot: SnapshotIn) { - return getParent(self).addSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - removeSavedSession(sessionSnapshot: any) { - return getParent(self).removeSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return getParent(self).renameCurrentSession(sessionName) - }, - - /** - * #action - */ - duplicateCurrentSession() { - return getParent(self).duplicateCurrentSession() - }, - /** - * #action - */ - activateSession(sessionName: any) { - return getParent(self).activateSession(sessionName) - }, - - /** - * #action - */ - setDefaultSession() { - return getParent(self).setDefaultSession() - }, - - /** - * #action - */ - saveSessionToLocalStorage() { - return getParent(self).saveSessionToLocalStorage() - }, - - /** - * #action - */ - loadAutosaveSession() { - return getParent(self).loadAutosaveSession() - }, - - /** - * #action - */ - setSession(sessionSnapshot: SnapshotIn) { - return getParent(self).setSession(sessionSnapshot) - }, - })) - - .actions(self => ({ - /** - * #action - * opens a configuration editor to configure the given thing, - * and sets the current task to be configuring it - * @param configuration - - */ - editConfiguration(configuration: AnyConfigurationModel) { - if (!isConfigurationModel(configuration)) { - throw new Error( - 'must pass a configuration model to editConfiguration', + const rootModel = getParent(self) + rootModel.setPluginsUpdated(true) + }, + + /** + * #action + */ + addTrackConf(trackConf: AnyConfiguration) { + if (self.adminMode) { + return super_addTrackConf(trackConf) + } + const { trackId, type } = trackConf as { + type: string + trackId: string + } + if (!type) { + throw new Error(`unknown track type ${type}`) + } + const track = self.sessionTracks.find( + (t: any) => t.trackId === trackId, ) - } - const editableConfigSession = self - const editor = editableConfigSession.addWidget( - 'ConfigurationEditorWidget', - 'configEditor', - { target: configuration }, - ) - editableConfigSession.showWidget(editor) - }, - + if (track) { + return track + } + const length = self.sessionTracks.push(trackConf) + return self.sessionTracks[length - 1] + }, + + /** + * #action + */ + deleteTrackConf(trackConf: AnyConfigurationModel) { + // try to delete it in the main config if in admin mode + const found = super_deletetrackConf(trackConf) + if (found) { + return found + } + // if not found or not in admin mode, try to delete it in the sessionTracks + const { trackId } = trackConf + const idx = self.sessionTracks.findIndex(t => t.trackId === trackId) + if (idx === -1) { + return undefined + } + return self.sessionTracks.splice(idx, 1) + }, + + /** + * #action + * set the global selection, i.e. the globally-selected object. + * can be a feature, a view, just about anything + * @param thing - + */ + setSelection(thing: any) { + self.selection = thing + }, + + /** + * #action + * clears the global selection + */ + clearSelection() { + self.selection = undefined + }, + + /** + * #action + */ + addSavedSession(sessionSnapshot: SnapshotIn) { + return getParent(self).addSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + removeSavedSession(sessionSnapshot: any) { + return getParent(self).removeSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + renameCurrentSession(sessionName: string) { + return getParent(self).renameCurrentSession(sessionName) + }, + + /** + * #action + */ + duplicateCurrentSession() { + return getParent(self).duplicateCurrentSession() + }, + /** + * #action + */ + activateSession(sessionName: any) { + return getParent(self).activateSession(sessionName) + }, + + /** + * #action + */ + setDefaultSession() { + return getParent(self).setDefaultSession() + }, + + /** + * #action + */ + saveSessionToLocalStorage() { + return getParent(self).saveSessionToLocalStorage() + }, + + /** + * #action + */ + loadAutosaveSession() { + return getParent(self).loadAutosaveSession() + }, + + /** + * #action + */ + setSession(sessionSnapshot: SnapshotIn) { + return getParent(self).setSession(sessionSnapshot) + }, + } + }) + .actions(self => ({ /** * #action */ @@ -628,14 +323,13 @@ export default function sessionModelFactory( /** * #method */ - getTrackActionMenuItems(config: AnyConfigurationModel) { + getTrackActionMenuItems(config: BaseTrackConfig) { const { adminMode, sessionTracks } = self const canEdit = adminMode || sessionTracks.find(t => t.trackId === config.trackId) // disable if it is a reference sequence track - const isRefSeq = - readConfObject(config, 'type') === 'ReferenceSequenceTrack' + const isRefSeq = config.type === 'ReferenceSequenceTrack' return [ { label: 'About track', From cb9a20675ba8ea5504b02d387d2cdf27ca4c071a Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Thu, 27 Apr 2023 13:26:58 -0700 Subject: [PATCH 13/44] wip --- .../models/BaseViewModel.ts | 12 ++++++++-- .../product-core/src/Session/DrawerWidgets.ts | 7 ++++-- packages/product-core/src/Session/Tracks.ts | 24 +++++++++---------- packages/product-core/src/Session/Views.ts | 21 ++++++++-------- .../jbrowse-desktop/src/indexJobsModel.ts | 1 + .../jbrowse-desktop/src/sessionModel/index.ts | 1 + .../jbrowse-web/src/sessionModel/index.ts | 15 +++--------- 7 files changed, 42 insertions(+), 39 deletions(-) diff --git a/packages/core/pluggableElementTypes/models/BaseViewModel.ts b/packages/core/pluggableElementTypes/models/BaseViewModel.ts index 98476adc91..61844e67fa 100644 --- a/packages/core/pluggableElementTypes/models/BaseViewModel.ts +++ b/packages/core/pluggableElementTypes/models/BaseViewModel.ts @@ -1,6 +1,7 @@ import { types, Instance } from 'mobx-state-tree' import { ElementId } from '../../util/types/mst' import { MenuItem } from '../../ui' +import { Region } from '../../util/types/mst' /** * #stateModel BaseViewModel @@ -69,5 +70,12 @@ const BaseViewModel = types })) export default BaseViewModel -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IBaseViewModel extends Instance {} + +export type IBaseViewModel = Instance + +export const BaseViewModelWithDisplayedRegions = BaseViewModel.props({ + displayedRegions: types.array(Region), +}) +export type IBaseViewModelWithDisplayedRegions = Instance< + typeof BaseViewModelWithDisplayedRegions +> diff --git a/packages/product-core/src/Session/DrawerWidgets.ts b/packages/product-core/src/Session/DrawerWidgets.ts index 1ac5504da8..c342b541b3 100644 --- a/packages/product-core/src/Session/DrawerWidgets.ts +++ b/packages/product-core/src/Session/DrawerWidgets.ts @@ -3,7 +3,10 @@ import { Instance, addDisposer, isAlive, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' import { autorun } from 'mobx' -import { AnyConfigurationModel, isConfigurationModel } from '@jbrowse/core/configuration' +import { + AnyConfigurationModel, + isConfigurationModel, +} from '@jbrowse/core/configuration' const minDrawerWidth = 128 @@ -191,4 +194,4 @@ export default function DrawerWidgets(pluginManager: PluginManager) { })) } -export type DrawerWidgetManager = Instance> \ No newline at end of file +export type DrawerWidgetManager = Instance> diff --git a/packages/product-core/src/Session/Tracks.ts b/packages/product-core/src/Session/Tracks.ts index 7621ed68ec..e7cf0a789c 100644 --- a/packages/product-core/src/Session/Tracks.ts +++ b/packages/product-core/src/Session/Tracks.ts @@ -1,25 +1,23 @@ -import { Instance, addDisposer, getParent, types } from 'mobx-state-tree' +import { Instance, getParent, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import { AnyConfigurationModel, getConf } from '@jbrowse/core/configuration' -import type { BaseSessionModel } from '../../../../products/jbrowse-desktop/src/sessionModel/Base' -import { ThemeOptions } from '@mui/material' -import { autorun } from 'mobx' -import DrawerWidgets from './DrawerWidgets' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' import BaseSession from './Base' +import ReferenceManagement from './ReferenceManagement' export default function Tracks(pluginManager: PluginManager) { return types - .compose('TracksManagerSessionMixin', - BaseSession(pluginManager), - ReferenceManagement(pluginManager) + .compose( + 'TracksManagerSessionMixin', + BaseSession(pluginManager), + ReferenceManagement(pluginManager), ) .views(self => ({ /** * #getter */ get tracks(): AnyConfigurationModel[] { - return getParent(self).jbrowse.tracks + return self.jbrowse.tracks }, })) .actions(self => ({ @@ -27,7 +25,7 @@ export default function Tracks(pluginManager: PluginManager) { * #action */ addTrackConf(trackConf: any) { - return getParent(self).jbrowse.addTrackConf(trackConf) + return self.jbrowse.addTrackConf(trackConf) }, /** @@ -45,10 +43,10 @@ export default function Tracks(pluginManager: PluginManager) { ) callbacksToDereferenceTrack.forEach(cb => cb()) if (self.adminMode) { - return getParent(self).jbrowse.deleteTrackConf(trackConf) + return self.jbrowse.deleteTrackConf(trackConf) } }, })) } -export type TracksManager = Instance> \ No newline at end of file +export type TracksManager = Instance> diff --git a/packages/product-core/src/Session/Views.ts b/packages/product-core/src/Session/Views.ts index a64373f3e2..39ac7cfcff 100644 --- a/packages/product-core/src/Session/Views.ts +++ b/packages/product-core/src/Session/Views.ts @@ -1,14 +1,15 @@ -import { addDisposer, getParent, getSnapshot, types } from 'mobx-state-tree' +import { getSnapshot, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import { AnyConfigurationModel, getConf, readConfObject } from '@jbrowse/core/configuration' -import type { BaseSessionModel } from '../../../../products/jbrowse-desktop/src/sessionModel/Base' -import { autorun } from 'mobx' +import { readConfObject } from '@jbrowse/core/configuration' import { Region } from '@jbrowse/core/util' +import DrawerWidgets from './DrawerWidgets' +import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes' +import { IBaseViewModelWithDisplayedRegions } from '@jbrowse/core/pluggableElementTypes/models/BaseViewModel' export default function Views(pluginManager: PluginManager) { - return types - .model({ + return DrawerWidgets(pluginManager) + .props({ /** * #property */ @@ -62,7 +63,7 @@ export default function Views(pluginManager: PluginManager) { /** * #action */ - removeView(view: any) { + removeView(view: IBaseViewModel) { for (const [, widget] of self.activeWidgets) { if (widget.view && widget.view.id === view.id) { self.hideWidget(widget) @@ -86,9 +87,9 @@ export default function Views(pluginManager: PluginManager) { * #action */ addViewOfAssembly( - viewType: any, + viewType: string, assemblyName: string, - initialState: any = {}, + initialState: Record = {}, ) { const asm = self.assemblies.find( s => readConfObject(s, 'name') === assemblyName, @@ -109,7 +110,7 @@ export default function Views(pluginManager: PluginManager) { */ addViewFromAnotherView( viewType: string, - otherView: any, + otherView: IBaseViewModelWithDisplayedRegions, initialState: { displayedRegions?: Region[] } = {}, ) { const state = { ...initialState } diff --git a/products/jbrowse-desktop/src/indexJobsModel.ts b/products/jbrowse-desktop/src/indexJobsModel.ts index eead608d47..3b476e12cd 100644 --- a/products/jbrowse-desktop/src/indexJobsModel.ts +++ b/products/jbrowse-desktop/src/indexJobsModel.ts @@ -24,6 +24,7 @@ interface TrackTextIndexing { tracks: string[] // trackIds indexType: string timestamp?: string + name?: string } interface JobsEntry { diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 9f7009f9d1..7598ad09e8 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -144,6 +144,7 @@ export default function sessionModelFactory( ) as typeof sessionModel return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { + // @ts-expect-error preProcessor(snapshot) { if (snapshot) { // @ts-expect-error diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 9671f283e4..60db2eef64 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -3,18 +3,11 @@ import { lazy } from 'react' import clone from 'clone' import { PluginDefinition } from '@jbrowse/core/PluginLoader' import { - readConfObject, getConf, - isConfigurationModel, AnyConfigurationModel, AnyConfiguration, } from '@jbrowse/core/configuration' -import { - Region, - AbstractSessionModel, - JBrowsePlugin, - DialogComponentType, -} from '@jbrowse/core/util/types' +import { AbstractSessionModel, JBrowsePlugin } from '@jbrowse/core/util/types' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' import { autorun } from 'mobx' @@ -27,11 +20,9 @@ import { types, Instance, SnapshotIn, - SnapshotOut, } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' -import RpcManager from '@jbrowse/core/rpc/RpcManager' import { Session as CoreSession } from '@jbrowse/product-core' @@ -44,7 +35,7 @@ import InfoIcon from '@mui/icons-material/Info' import { BaseSession } from './Base' import Assemblies from './Assemblies' import SessionConnections from './SessionConnections' -import { BaseTrackConfig, BaseTrackConfigModel } from '@jbrowse/core/pluggableElementTypes' +import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) @@ -316,7 +307,7 @@ export default function sessionModelFactory( if (!adminMode && !sessionTracks.includes(configuration)) { throw new Error("Can't edit the configuration of a non-session track") } - this.editConfiguration(configuration) + self.editConfiguration(configuration) }, })) .views(self => ({ From 3eeb219b8fe6a10c1aca7e524e052d575e5fc896 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Thu, 27 Apr 2023 17:49:08 -0700 Subject: [PATCH 14/44] wip --- .../assemblyManager/assemblyConfigSchema.ts | 1 + packages/core/assemblyManager/index.ts | 1 + packages/product-core/src/RootModel/index.ts | 77 ++++++++++++++-- packages/product-core/src/Session/Base.ts | 43 ++++++--- .../src/Session/SessionManagement.ts | 71 ++++++++++++++ packages/product-core/src/Session/Views.ts | 4 +- packages/product-core/src/Session/index.ts | 1 + products/jbrowse-desktop/src/JBrowse.test.tsx | 1 + products/jbrowse-desktop/src/jbrowseConfig.ts | 4 +- products/jbrowse-desktop/src/jbrowseModel.ts | 7 +- products/jbrowse-desktop/src/rootModel.ts | 54 +++-------- .../src/sessionModel/Assemblies.ts | 18 ++-- .../jbrowse-desktop/src/sessionModel/index.ts | 92 +------------------ products/jbrowse-web/src/jbrowseConfig.ts | 2 +- products/jbrowse-web/src/jbrowseModel.ts | 1 + 15 files changed, 209 insertions(+), 168 deletions(-) create mode 100644 packages/product-core/src/Session/SessionManagement.ts diff --git a/packages/core/assemblyManager/assemblyConfigSchema.ts b/packages/core/assemblyManager/assemblyConfigSchema.ts index 2e93958d0a..231367d604 100644 --- a/packages/core/assemblyManager/assemblyConfigSchema.ts +++ b/packages/core/assemblyManager/assemblyConfigSchema.ts @@ -104,3 +104,4 @@ function assemblyConfigSchema(pluginManager: PluginManager) { } export default assemblyConfigSchema +export type BaseAssemblyConfigSchema = ReturnType diff --git a/packages/core/assemblyManager/index.ts b/packages/core/assemblyManager/index.ts index 4576983426..61a2a9d0d1 100644 --- a/packages/core/assemblyManager/index.ts +++ b/packages/core/assemblyManager/index.ts @@ -1,2 +1,3 @@ export { default } from './assemblyManager' export { default as assemblyConfigSchemaFactory } from './assemblyConfigSchema' +export type { BaseAssemblyConfigSchema } from './assemblyConfigSchema' diff --git a/packages/product-core/src/RootModel/index.ts b/packages/product-core/src/RootModel/index.ts index 72dde1023b..3b3b2d49f5 100644 --- a/packages/product-core/src/RootModel/index.ts +++ b/packages/product-core/src/RootModel/index.ts @@ -1,10 +1,69 @@ -import assemblyManagerFactory from '@jbrowse/core/assemblyManager' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import { AbstractSessionModel } from '@jbrowse/core/util' - -export interface RootModel { - jbrowse: AnyConfigurationModel - session: AbstractSessionModel - assemblyManager: ReturnType - version: string +import PluginManager from '@jbrowse/core/PluginManager' +import assemblyManagerFactory, { + BaseAssemblyConfigSchema, +} from '@jbrowse/core/assemblyManager' +import RpcManager from '@jbrowse/core/rpc/RpcManager' +import { IAnyType, Instance, types } from 'mobx-state-tree' +import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' + +export function BaseRootModel< + JBROWSE_MODEL_TYPE extends IAnyType, + SESSION_MODEL_TYPE extends IAnyType, +>( + pluginManager: PluginManager, + jbrowseModelType: JBROWSE_MODEL_TYPE, + sessionModelType: SESSION_MODEL_TYPE, + assemblyConfigSchema: BaseAssemblyConfigSchema, +) { + return types + .model('BaseRootModel', { + /** + * #property + * `jbrowse` is a mapping of the config.json into the in-memory state tree + */ + jbrowse: jbrowseModelType, + /** + * #property + */ + version: types.string, + + /** + * #property + * `session` encompasses the currently active state of the app, including + * views open, tracks open in those views, etc. + */ + session: types.maybe(sessionModelType), + /** + * #property + */ + assemblyManager: assemblyManagerFactory( + assemblyConfigSchema, + pluginManager, + ), + }) + .volatile(self => ({ + rpcManager: new RpcManager( + pluginManager, + self.jbrowse.configuration.rpc, + { + MainThreadRpcDriver: {}, + }, + ), + + /** + * #volatile + * Boolean indicating whether the session is in admin mode or not + */ + adminMode: true, + + isAssemblyEditing: false, + error: undefined as unknown, + textSearchManager: new TextSearchManager(pluginManager), + openNewSessionCallback: async (_path: string) => { + console.error('openNewSessionCallback unimplemented') + }, + pluginManager, + })) } + +export type RootModel = Instance> diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index e2f8862484..f4840c4a01 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -1,15 +1,13 @@ import shortid from 'shortid' import type PluginManager from '@jbrowse/core/PluginManager' -import { getParent, types } from 'mobx-state-tree' +import { Instance, getParent, types } from 'mobx-state-tree' import { RootModel } from '../RootModel' import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager' /** base session shared by **all** JBrowse products. Be careful what you include here, everything will use it. */ -export default function BaseSession( - pluginManager: PluginManager, - defaultAdminMode = true, -) { +export default function BaseSession(pluginManager: PluginManager) { return types .model({ /** @@ -22,11 +20,6 @@ export default function BaseSession( name: types.identifier, }) .volatile(() => ({ - /** - * #volatile - * Boolean indicating whether the session is in admin mode or not - */ - adminMode: defaultAdminMode, /** * #volatile * this is the globally "selected" object. can be anything. @@ -49,7 +42,7 @@ export default function BaseSession( * #getter */ get rpcManager() { - return this.jbrowse.rpcManager + return this.root.rpcManager }, /** * #getter @@ -57,5 +50,33 @@ export default function BaseSession( get configuration(): AnyConfigurationModel { return this.jbrowse.configuration }, + + get adminMode() { + return this.root.adminMode + }, + /** + * #getter + */ + get assemblies(): Instance[] { + return this.jbrowse.assemblies + }, + /** + * #getter + */ + get textSearchManager() { + return this.root.textSearchManager + }, + /** + * #getter + */ + get history() { + return self.root.history + }, + /** + * #getter + */ + get menus() { + return self.root.menus + }, })) } diff --git a/packages/product-core/src/Session/SessionManagement.ts b/packages/product-core/src/Session/SessionManagement.ts new file mode 100644 index 0000000000..eba94cf614 --- /dev/null +++ b/packages/product-core/src/Session/SessionManagement.ts @@ -0,0 +1,71 @@ +import { SnapshotIn } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { Base } from '@jbrowse/product-core/src/Session' + +export default function SessionManagement(pluginManager: PluginManager) { + return Base(pluginManager) + .props({}) + .views(self => ({ + /** + * #getter + */ + get savedSessions() { + return self.jbrowse.savedSessions + }, + /** + * #getter + */ + get savedSessionNames() { + return self.jbrowse.savedSessionNames + }, + /** + * #action + */ + addSavedSession(sessionSnapshot: SnapshotIn) { + return self.root.addSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + removeSavedSession(sessionSnapshot: any) { + return self.root.removeSavedSession(sessionSnapshot) + }, + + /** + * #action + */ + renameCurrentSession(sessionName: string) { + return self.root.renameCurrentSession(sessionName) + }, + + /** + * #action + */ + duplicateCurrentSession() { + return self.root.duplicateCurrentSession() + }, + + /** + * #action + */ + activateSession(sessionName: any) { + return self.root.activateSession(sessionName) + }, + + /** + * #action + */ + setDefaultSession() { + return self.root.setDefaultSession() + }, + + /** + * #action + */ + setSession(sessionSnapshot: SnapshotIn) { + return self.root.setSession(sessionSnapshot) + }, + })) +} diff --git a/packages/product-core/src/Session/Views.ts b/packages/product-core/src/Session/Views.ts index 39ac7cfcff..4c6b5e0487 100644 --- a/packages/product-core/src/Session/Views.ts +++ b/packages/product-core/src/Session/Views.ts @@ -6,9 +6,11 @@ import { Region } from '@jbrowse/core/util' import DrawerWidgets from './DrawerWidgets' import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes' import { IBaseViewModelWithDisplayedRegions } from '@jbrowse/core/pluggableElementTypes/models/BaseViewModel' +import Base from './Base' export default function Views(pluginManager: PluginManager) { - return DrawerWidgets(pluginManager) + return types + .compose(Base(pluginManager), DrawerWidgets(pluginManager)) .props({ /** * #property diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts index 1e4b81ae93..5872256929 100644 --- a/packages/product-core/src/Session/index.ts +++ b/packages/product-core/src/Session/index.ts @@ -6,3 +6,4 @@ export { default as Themes } from './Themes' export { default as Tracks } from './Tracks' export { default as Views } from './Views' export { default as Base } from './Base' +export { default as SessionManagement } from './SessionManagement' diff --git a/products/jbrowse-desktop/src/JBrowse.test.tsx b/products/jbrowse-desktop/src/JBrowse.test.tsx index df579bd3c8..68c8c228f3 100644 --- a/products/jbrowse-desktop/src/JBrowse.test.tsx +++ b/products/jbrowse-desktop/src/JBrowse.test.tsx @@ -32,6 +32,7 @@ function getPluginManager(initialState?: SnapshotIn) { { jbrowse: initialState || configSnapshot, assemblyManager: {}, + version: 'testing', }, { pluginManager }, ) diff --git a/products/jbrowse-desktop/src/jbrowseConfig.ts b/products/jbrowse-desktop/src/jbrowseConfig.ts index 3de131f148..a2b64fbbdc 100644 --- a/products/jbrowse-desktop/src/jbrowseConfig.ts +++ b/products/jbrowse-desktop/src/jbrowseConfig.ts @@ -7,7 +7,7 @@ import { PluginDefinition } from '@jbrowse/core/PluginLoader' import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' import { types } from 'mobx-state-tree' -import { SessionStateModel } from './sessionModelFactory' +import { SessionStateModelType } from './sessionModel' /** * #config JBrowseDesktopConfiguration @@ -17,7 +17,7 @@ function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars export default function JBrowseConfigF( pluginManager: PluginManager, - Session: SessionStateModel, + Session: SessionStateModelType, assemblyConfigSchemasType: AnyConfigurationSchemaType, ) { return types.model('JBrowseDesktop', { diff --git a/products/jbrowse-desktop/src/jbrowseModel.ts b/products/jbrowse-desktop/src/jbrowseModel.ts index b266880be4..bc266d36ea 100644 --- a/products/jbrowse-desktop/src/jbrowseModel.ts +++ b/products/jbrowse-desktop/src/jbrowseModel.ts @@ -13,7 +13,8 @@ import { resolveIdentifier, } from 'mobx-state-tree' import JBrowseConfigF from './jbrowseConfig' -import { SessionStateModel } from './sessionModel' +import { SessionStateModelType } from './sessionModel' +import { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager' // poke some things for testing (this stuff will eventually be removed) // @ts-expect-error @@ -30,8 +31,8 @@ function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars export default function JBrowseDesktop( pluginManager: PluginManager, - Session: SessionStateModel, - assemblyConfigSchemasType: AnyConfigurationSchemaType, + Session: SessionStateModelType, + assemblyConfigSchemasType: BaseAssemblyConfigSchema, ) { return JBrowseConfigF(pluginManager, Session, assemblyConfigSchemasType) .views(self => ({ diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 07c8968cd0..87fe1be58f 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -9,17 +9,15 @@ import { } from 'mobx-state-tree' import { autorun } from 'mobx' import makeWorkerInstance from './makeWorkerInstance' -import assemblyManagerFactory from '@jbrowse/core/assemblyManager' import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import TimeTraveller from '@jbrowse/core/util/TimeTraveller' import { MenuItem } from '@jbrowse/core/ui' import { AnyConfigurationModel } from '@jbrowse/core/configuration' import { UriLocation } from '@jbrowse/core/util/types' -import type { RootModel as BaseRootModel } from '@jbrowse/product-core' +import { BaseRootModel } from '@jbrowse/product-core' // icons import OpenIcon from '@mui/icons-material/FolderOpen' @@ -42,7 +40,7 @@ const { ipcRenderer } = window.require('electron') const PreferencesDialog = lazy(() => import('./PreferencesDialog')) -function getSaveSession(model: BaseRootModel) { +function getSaveSession(model: Instance>) { return { ...getSnapshot(model.jbrowse), defaultSession: model.session ? getSnapshot(model.session) : {}, @@ -63,38 +61,21 @@ export default function rootModelFactory(pluginManager: PluginManager) { const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) const JobsManager = jobsModelFactory(pluginManager) - return types - .model('Root', { - /** - * #property - * `jbrowse` is a mapping of the config.json into the in-memory state tree - */ - jbrowse: JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), - /** - * #property - * `session` encompasses the currently active state of the app, including - * views open, tracks open in those views, etc. - */ - session: types.maybe(Session), + return BaseRootModel( + pluginManager, + JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), + Session, + assemblyConfigSchema, + ) + .props({ /** * #property */ jobsManager: types.maybe(JobsManager), - /** - * #property - */ - assemblyManager: assemblyManagerFactory( - assemblyConfigSchema, - pluginManager, - ), /** * #property */ savedSessionNames: types.maybe(types.array(types.string)), - /** - * #property - */ - version: types.maybe(types.string), /** * #property */ @@ -111,15 +92,6 @@ export default function rootModelFactory(pluginManager: PluginManager) { */ history: types.optional(TimeTraveller, { targetPath: '../session' }), }) - .volatile(() => ({ - isAssemblyEditing: false, - error: undefined as unknown, - textSearchManager: new TextSearchManager(pluginManager), - openNewSessionCallback: async (_path: string) => { - console.error('openNewSessionCallback unimplemented') - }, - pluginManager, - })) .actions(self => ({ /** * #action @@ -310,7 +282,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { onClick: async () => { if (self.session) { try { - await self.saveSession(getSaveSession(self)) + await self.saveSession(getSaveSession(self as RootModel)) } catch (e) { console.error(e) self.session?.notify(`${e}`, 'error') @@ -327,7 +299,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { 'promptSessionSaveAs', ) self.setSessionPath(saveAsPath) - await self.saveSession(getSaveSession(self)) + await self.saveSession(getSaveSession(self as RootModel)) } catch (e) { console.error(e) self.session?.notify(`${e}`, 'error') @@ -507,7 +479,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { }, async setPluginsUpdated() { if (self.session) { - await self.saveSession(getSaveSession(self)) + await self.saveSession(getSaveSession(self as RootModel)) } await self.openNewSessionCallback(self.sessionPath) }, @@ -692,7 +664,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { async () => { if (self.session) { try { - await self.saveSession(getSaveSession(self)) + await self.saveSession(getSaveSession(self as RootModel)) } catch (e) { console.error(e) } diff --git a/products/jbrowse-desktop/src/sessionModel/Assemblies.ts b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts index 3d960f1e3d..ff41749f41 100644 --- a/products/jbrowse-desktop/src/sessionModel/Assemblies.ts +++ b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts @@ -2,14 +2,14 @@ import { Instance, getParent, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import { RootModel } from '../rootModel' +import { Base } from '@jbrowse/product-core/src/Session' export default function Assemblies( pluginManager: PluginManager, assemblyConfigSchemasType = types.frozen(), ) { - return types - .model({ + return Base(pluginManager) + .props({ /** * #property */ @@ -20,23 +20,17 @@ export default function Assemblies( temporaryAssemblies: types.array(assemblyConfigSchemasType), }) .views(self => ({ - /** - * #getter - */ - get assemblies(): AnyConfigurationModel[] { - return getParent(self).jbrowse.assemblies - }, /** * #getter */ get assemblyNames(): string[] { - return getParent(self).jbrowse.assemblyNames + return self.jbrowse.assemblyNames }, /** * #getter */ get assemblyManager() { - return getParent(self).assemblyManager + return self.root.assemblyManager }, })) .actions(self => ({ @@ -91,7 +85,7 @@ export default function Assemblies( * #action */ addAssemblyConf(assemblyConf: any) { - return getParent(self).jbrowse.addAssemblyConf(assemblyConf) + return self.jbrowse.addAssemblyConf(assemblyConf) }, })) } diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 7598ad09e8..f9947611d9 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -1,8 +1,7 @@ import { readConfObject } from '@jbrowse/core/configuration' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { types, IAnyStateTreeNode, SnapshotIn } from 'mobx-state-tree' +import { types, Instance } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import { Session as CoreSession } from '@jbrowse/product-core' @@ -12,11 +11,6 @@ import Assemblies from './Assemblies' import TrackMenu from './TrackMenu' import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' -export declare interface ReferringNode { - node: IAnyStateTreeNode - key: string -} - /** * #stateModel JBrowseDesktopSessionModel * inherits SnackbarModel @@ -37,42 +31,12 @@ export default function sessionModelFactory( CoreSession.Themes(pluginManager), CoreSession.Tracks(pluginManager), CoreSession.Views(pluginManager), + CoreSession.SessionManagement(pluginManager), ), Assemblies(pluginManager, assemblyConfigSchemasType), TrackMenu(pluginManager), ) .views(self => ({ - /** - * #getter - */ - get textSearchManager(): TextSearchManager { - self.connections - return self.root.textSearchManager - }, - /** - * #getter - */ - get savedSessions() { - return self.jbrowse.savedSessions - }, - /** - * #getter - */ - get savedSessionNames() { - return self.jbrowse.savedSessionNames - }, - /** - * #getter - */ - get history() { - return self.root.history - }, - /** - * #getter - */ - get menus() { - return self.root.menus - }, /** * #method */ @@ -87,55 +51,6 @@ export default function sessionModelFactory( editTrackConfiguration(configuration: BaseTrackConfig) { self.editConfiguration(configuration) }, - - /** - * #action - */ - addSavedSession(sessionSnapshot: SnapshotIn) { - return self.root.addSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - removeSavedSession(sessionSnapshot: any) { - return self.root.removeSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return self.root.renameCurrentSession(sessionName) - }, - - /** - * #action - */ - duplicateCurrentSession() { - return self.root.duplicateCurrentSession() - }, - - /** - * #action - */ - activateSession(sessionName: any) { - return self.root.activateSession(sessionName) - }, - - /** - * #action - */ - setDefaultSession() { - return self.root.setDefaultSession() - }, - - /** - * #action - */ - setSession(sessionSnapshot: SnapshotIn) { - return self.root.setSession(sessionSnapshot) - }, })) const extendedSessionModel = pluginManager.evaluateExtensionPoint( @@ -161,4 +76,5 @@ export default function sessionModelFactory( }) } -export type SessionStateModel = ReturnType +export type SessionStateModelType = ReturnType +export type SessionStateModel = Instance diff --git a/products/jbrowse-web/src/jbrowseConfig.ts b/products/jbrowse-web/src/jbrowseConfig.ts index e5e37bf2c2..be049d3078 100644 --- a/products/jbrowse-web/src/jbrowseConfig.ts +++ b/products/jbrowse-web/src/jbrowseConfig.ts @@ -6,7 +6,7 @@ import RpcManager from '@jbrowse/core/rpc/RpcManager' import PluginManager from '@jbrowse/core/PluginManager' import { PluginDefinition } from '@jbrowse/core/PluginLoader' import { types } from 'mobx-state-tree' -import { SessionStateModel } from './sessionModelFactory' +import { SessionStateModel } from './sessionModel' /** * #config JBrowseWebConfiguration diff --git a/products/jbrowse-web/src/jbrowseModel.ts b/products/jbrowse-web/src/jbrowseModel.ts index 99690b5bc3..c4e1eac560 100644 --- a/products/jbrowse-web/src/jbrowseModel.ts +++ b/products/jbrowse-web/src/jbrowseModel.ts @@ -196,6 +196,7 @@ export default function JBrowseWeb( throw new Error(`unable to set default session to ${newDefault.name}`) } + // @ts-expect-error complains about name missing, but above line checks this self.defaultSession = cast(newDefault) }, /** From 3361fe8e7455303406b408229a7cf17a4bf92ba7 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Fri, 28 Apr 2023 11:33:00 -0700 Subject: [PATCH 15/44] wip --- packages/product-core/src/RootModel/Base.ts | 121 ++++++++++++++++++ .../src/RootModel/InternetAccounts.ts | 5 + packages/product-core/src/RootModel/index.ts | 72 +---------- packages/product-core/src/Session/Base.ts | 4 +- products/jbrowse-desktop/src/rootModel.ts | 54 +------- 5 files changed, 134 insertions(+), 122 deletions(-) create mode 100644 packages/product-core/src/RootModel/Base.ts create mode 100644 packages/product-core/src/RootModel/InternetAccounts.ts diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts new file mode 100644 index 0000000000..877c948e3a --- /dev/null +++ b/packages/product-core/src/RootModel/Base.ts @@ -0,0 +1,121 @@ +import PluginManager from '@jbrowse/core/PluginManager' +import assemblyManagerFactory, { + BaseAssemblyConfigSchema, +} from '@jbrowse/core/assemblyManager' +import RpcManager from '@jbrowse/core/rpc/RpcManager' +import { IAnyType, SnapshotIn, cast, getSnapshot, types } from 'mobx-state-tree' +import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' + +/** + * factory function for the Base-level root model shared by all products + */ +export function BaseRootModel< + JBROWSE_MODEL_TYPE extends IAnyType, + SESSION_MODEL_TYPE extends BaseSession, +>( + pluginManager: PluginManager, + jbrowseModelType: JBROWSE_MODEL_TYPE, + sessionModelType: SESSION_MODEL_TYPE, + assemblyConfigSchema: BaseAssemblyConfigSchema, +) { + return types + .model('BaseRootModel', { + /** + * #property + * `jbrowse` is a mapping of the config.json into the in-memory state tree + */ + jbrowse: jbrowseModelType, + /** + * #property + */ + version: types.string, + + /** + * #property + * `session` encompasses the currently active state of the app, including + * views open, tracks open in those views, etc. + */ + session: types.maybe(sessionModelType), + /** + * #property + */ + sessionPath: types.optional(types.string, ''), + + /** + * #property + */ + assemblyManager: assemblyManagerFactory( + assemblyConfigSchema, + pluginManager, + ), + }) + .volatile(self => ({ + rpcManager: new RpcManager( + pluginManager, + self.jbrowse.configuration.rpc, + { + MainThreadRpcDriver: {}, + }, + ), + + /** + * #volatile + * Boolean indicating whether the session is in admin mode or not + */ + adminMode: true, + + isAssemblyEditing: false, + error: undefined as unknown, + textSearchManager: new TextSearchManager(pluginManager), + openNewSessionCallback: async (_path: string) => { + console.error('openNewSessionCallback unimplemented') + }, + pluginManager, + })) + .actions(self => ({ + /** + * #action + */ + setError(error: unknown) { + self.error = error + }, + /** + * #action + */ + setSession(sessionSnapshot?: SnapshotIn) { + self.session = cast(sessionSnapshot) + }, + /** + * #action + */ + setDefaultSession() { + this.setSession(self.jbrowse.defaultSession) + }, + /** + * #action + */ + setSessionPath(path: string) { + self.sessionPath = path + }, + /** + * #action + */ + async renameCurrentSession(newName: string) { + if (self.session) { + this.setSession({ ...getSnapshot(self.session), name: newName }) + } + }, + /** + * #action + */ + setOpenNewSessionCallback(cb: (arg: string) => Promise) { + self.openNewSessionCallback = cb + }, + /** + * #action + */ + setAssemblyEditing(flag: boolean) { + self.isAssemblyEditing = flag + }, + })) +} diff --git a/packages/product-core/src/RootModel/InternetAccounts.ts b/packages/product-core/src/RootModel/InternetAccounts.ts new file mode 100644 index 0000000000..814e956f85 --- /dev/null +++ b/packages/product-core/src/RootModel/InternetAccounts.ts @@ -0,0 +1,5 @@ +import PluginManager from '@jbrowse/core/PluginManager' + +export default function InternetAccounts(pluginManager: PluginManager) { + +} \ No newline at end of file diff --git a/packages/product-core/src/RootModel/index.ts b/packages/product-core/src/RootModel/index.ts index 3b3b2d49f5..1a3a70b0c2 100644 --- a/packages/product-core/src/RootModel/index.ts +++ b/packages/product-core/src/RootModel/index.ts @@ -1,69 +1,5 @@ -import PluginManager from '@jbrowse/core/PluginManager' -import assemblyManagerFactory, { - BaseAssemblyConfigSchema, -} from '@jbrowse/core/assemblyManager' -import RpcManager from '@jbrowse/core/rpc/RpcManager' -import { IAnyType, Instance, types } from 'mobx-state-tree' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' +import { Instance } from 'mobx-state-tree' +import { BaseRootModel as BaseRootModelF } from './Base' -export function BaseRootModel< - JBROWSE_MODEL_TYPE extends IAnyType, - SESSION_MODEL_TYPE extends IAnyType, ->( - pluginManager: PluginManager, - jbrowseModelType: JBROWSE_MODEL_TYPE, - sessionModelType: SESSION_MODEL_TYPE, - assemblyConfigSchema: BaseAssemblyConfigSchema, -) { - return types - .model('BaseRootModel', { - /** - * #property - * `jbrowse` is a mapping of the config.json into the in-memory state tree - */ - jbrowse: jbrowseModelType, - /** - * #property - */ - version: types.string, - - /** - * #property - * `session` encompasses the currently active state of the app, including - * views open, tracks open in those views, etc. - */ - session: types.maybe(sessionModelType), - /** - * #property - */ - assemblyManager: assemblyManagerFactory( - assemblyConfigSchema, - pluginManager, - ), - }) - .volatile(self => ({ - rpcManager: new RpcManager( - pluginManager, - self.jbrowse.configuration.rpc, - { - MainThreadRpcDriver: {}, - }, - ), - - /** - * #volatile - * Boolean indicating whether the session is in admin mode or not - */ - adminMode: true, - - isAssemblyEditing: false, - error: undefined as unknown, - textSearchManager: new TextSearchManager(pluginManager), - openNewSessionCallback: async (_path: string) => { - console.error('openNewSessionCallback unimplemented') - }, - pluginManager, - })) -} - -export type RootModel = Instance> +export { BaseRootModel } from './Base' +export type RootModel = Instance> diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index f4840c4a01..25e1064c56 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -70,13 +70,13 @@ export default function BaseSession(pluginManager: PluginManager) { * #getter */ get history() { - return self.root.history + return this.root.history }, /** * #getter */ get menus() { - return self.root.menus + return this.root.menus }, })) } diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 87fe1be58f..72e25500fc 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -101,60 +101,10 @@ export default function rootModelFactory(pluginManager: PluginManager) { await ipcRenderer.invoke('saveSession', self.sessionPath, val) } }, - /** - * #action - */ - setOpenNewSessionCallback(cb: (arg: string) => Promise) { - self.openNewSessionCallback = cb - }, - /** - * #action - */ - setSavedSessionNames(sessionNames: string[]) { - self.savedSessionNames = cast(sessionNames) - }, - /** - * #action - */ - setSessionPath(path: string) { - self.sessionPath = path - }, - /** - * #action - */ - setSession(sessionSnapshot?: SnapshotIn) { - self.session = cast(sessionSnapshot) - }, - /** - * #action - */ - setError(error: unknown) { - self.error = error - }, - /** - * #action - */ - setDefaultSession() { - this.setSession(self.jbrowse.defaultSession) - }, - /** - * #action - */ - setAssemblyEditing(flag: boolean) { - self.isAssemblyEditing = flag - }, - /** - * #action - */ - async renameCurrentSession(newName: string) { - if (self.session) { - this.setSession({ ...getSnapshot(self.session), name: newName }) - } - }, - /** + /** * #action */ - duplicateCurrentSession() { + duplicateCurrentSession() { if (self.session) { const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) let newSnapshotName = `${self.session.name} (copy)` From 8a528153c75984775d687c06e6fb36a25a279e58 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Fri, 28 Apr 2023 13:15:09 -0700 Subject: [PATCH 16/44] wip --- packages/product-core/src/RootModel/Base.ts | 2 +- packages/product-core/src/Session/Base.ts | 21 ++++++++++++++++ packages/product-core/src/Session/index.ts | 1 - products/jbrowse-desktop/src/rootModel.ts | 6 ++--- .../jbrowse-desktop/src/sessionModel/Base.ts | 19 -------------- .../src/sessionModel}/SessionManagement.ts | 0 .../jbrowse-desktop/src/sessionModel/index.ts | 3 ++- .../jbrowse-web/src/sessionModel/index.ts | 25 ------------------- 8 files changed, 27 insertions(+), 50 deletions(-) rename {packages/product-core/src/Session => products/jbrowse-desktop/src/sessionModel}/SessionManagement.ts (100%) diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts index 877c948e3a..8680e769b3 100644 --- a/packages/product-core/src/RootModel/Base.ts +++ b/packages/product-core/src/RootModel/Base.ts @@ -11,7 +11,7 @@ import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' */ export function BaseRootModel< JBROWSE_MODEL_TYPE extends IAnyType, - SESSION_MODEL_TYPE extends BaseSession, + SESSION_MODEL_TYPE extends IAnyType, >( pluginManager: PluginManager, jbrowseModelType: JBROWSE_MODEL_TYPE, diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index 25e1064c56..96f239b433 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -79,4 +79,25 @@ export default function BaseSession(pluginManager: PluginManager) { return this.root.menus }, })) + .actions(self => ({ + /** + * #action + * set the global selection, i.e. the globally-selected object. + * can be a feature, a view, just about anything + * @param thing - + */ + setSelection(thing: unknown) { + self.selection = thing + }, + + /** + * #action + * clears the global selection + */ + clearSelection() { + self.selection = undefined + }, + })) } + +export type BaseSessionType = ReturnType diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts index 5872256929..1e4b81ae93 100644 --- a/packages/product-core/src/Session/index.ts +++ b/packages/product-core/src/Session/index.ts @@ -6,4 +6,3 @@ export { default as Themes } from './Themes' export { default as Tracks } from './Tracks' export { default as Views } from './Views' export { default as Base } from './Base' -export { default as SessionManagement } from './SessionManagement' diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 72e25500fc..701a2bd9fc 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -101,10 +101,10 @@ export default function rootModelFactory(pluginManager: PluginManager) { await ipcRenderer.invoke('saveSession', self.sessionPath, val) } }, - /** + /** * #action */ - duplicateCurrentSession() { + duplicateCurrentSession() { if (self.session) { const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) let newSnapshotName = `${self.session.name} (copy)` @@ -116,7 +116,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { } while (self.jbrowse.savedSessionNames.includes(newSnapshotName)) } snapshot.name = newSnapshotName - this.setSession(snapshot) + self.setSession(snapshot) } }, /** diff --git a/products/jbrowse-desktop/src/sessionModel/Base.ts b/products/jbrowse-desktop/src/sessionModel/Base.ts index da0868652f..f2ca28a63a 100644 --- a/products/jbrowse-desktop/src/sessionModel/Base.ts +++ b/products/jbrowse-desktop/src/sessionModel/Base.ts @@ -36,25 +36,6 @@ export default function BaseSession(pluginManager: PluginManager) { */ task: undefined, })) - .actions(self => ({ - /** - * #action - * set the global selection, i.e. the globally-selected object. - * can be a feature, a view, just about anything - * @param thing - - */ - setSelection(thing: unknown) { - self.selection = thing - }, - - /** - * #action - * clears the global selection - */ - clearSelection() { - self.selection = undefined - }, - })) } export type BaseSessionModel = Instance> diff --git a/packages/product-core/src/Session/SessionManagement.ts b/products/jbrowse-desktop/src/sessionModel/SessionManagement.ts similarity index 100% rename from packages/product-core/src/Session/SessionManagement.ts rename to products/jbrowse-desktop/src/sessionModel/SessionManagement.ts diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index f9947611d9..d843ee4df3 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -10,6 +10,7 @@ import Base from './Base' import Assemblies from './Assemblies' import TrackMenu from './TrackMenu' import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' +import SessionManagement from './SessionManagement' /** * #stateModel JBrowseDesktopSessionModel @@ -31,10 +32,10 @@ export default function sessionModelFactory( CoreSession.Themes(pluginManager), CoreSession.Tracks(pluginManager), CoreSession.Views(pluginManager), - CoreSession.SessionManagement(pluginManager), ), Assemblies(pluginManager, assemblyConfigSchemasType), TrackMenu(pluginManager), + SessionManagement(pluginManager), ) .views(self => ({ /** diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 60db2eef64..15064de92d 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -69,13 +69,6 @@ export default function sessionModelFactory( * #volatile */ sessionThemeName: localStorageGetItem('themeName') || 'default', - /** - * #volatile - * this is the globally "selected" object. can be anything. - * code that wants to deal with this should examine it to see what - * kind of thing it is. - */ - selection: undefined, /** * #volatile * this is the current "task" that is being performed in the UI. @@ -217,24 +210,6 @@ export default function sessionModelFactory( return self.sessionTracks.splice(idx, 1) }, - /** - * #action - * set the global selection, i.e. the globally-selected object. - * can be a feature, a view, just about anything - * @param thing - - */ - setSelection(thing: any) { - self.selection = thing - }, - - /** - * #action - * clears the global selection - */ - clearSelection() { - self.selection = undefined - }, - /** * #action */ From 063fd5d26e832c0d2c341909778d2ea2dcb88ffd Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Fri, 28 Apr 2023 15:53:31 -0700 Subject: [PATCH 17/44] wip --- packages/product-core/src/Session/Base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index 96f239b433..c6bf108702 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -17,7 +17,7 @@ export default function BaseSession(pluginManager: PluginManager) { /** * #property */ - name: types.identifier, + name: types.string, }) .volatile(() => ({ /** From dcd135cc34a8c14cb6d28b185ca1fdd4b3b498a4 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 29 Apr 2023 08:12:36 -0700 Subject: [PATCH 18/44] queueOfDialogs actually does need to be observable --- packages/product-core/src/Session/DialogQueue.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/product-core/src/Session/DialogQueue.ts b/packages/product-core/src/Session/DialogQueue.ts index 8f41b61d79..5daab9634d 100644 --- a/packages/product-core/src/Session/DialogQueue.ts +++ b/packages/product-core/src/Session/DialogQueue.ts @@ -2,6 +2,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import { DialogComponentType } from '@jbrowse/core/util' +import { observable } from 'mobx' import { IAnyStateTreeNode, Instance, types } from 'mobx-state-tree' export interface ReferringNode { @@ -13,7 +14,9 @@ export default function DialogQueue(pluginManager: PluginManager) { return types .model('DialogQueueSessionMixin', {}) .volatile(() => ({ - queueOfDialogs: [] as [DialogComponentType, any][], + queueOfDialogs: observable.array( + [] as [DialogComponentType, Record][], + ), })) .views(self => ({ /** From c2952c9d169815af84ad2e1c29197afb1409bf8b Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 29 Apr 2023 08:13:01 -0700 Subject: [PATCH 19/44] wip --- products/jbrowse-web/src/sessionModel/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 15064de92d..c5d31032a7 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -39,10 +39,6 @@ import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) -export declare interface ReactProps { - [key: string]: any -} - /** * #stateModel JBrowseWebSessionModel * inherits SnackbarModel From 4b9e5ba070be0cd3bfff752585739461458af385 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 29 Apr 2023 09:43:30 -0700 Subject: [PATCH 20/44] pass more tests --- packages/product-core/src/RootModel/Base.ts | 2 +- products/jbrowse-desktop/src/sessionModel/Base.ts | 4 ---- products/jbrowse-desktop/src/sessionModel/index.ts | 2 +- products/jbrowse-web/src/sessionModel/index.ts | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts index 8680e769b3..4b057ac871 100644 --- a/packages/product-core/src/RootModel/Base.ts +++ b/packages/product-core/src/RootModel/Base.ts @@ -28,7 +28,7 @@ export function BaseRootModel< /** * #property */ - version: types.string, + version: 'development', /** * #property diff --git a/products/jbrowse-desktop/src/sessionModel/Base.ts b/products/jbrowse-desktop/src/sessionModel/Base.ts index f2ca28a63a..728ebd1a00 100644 --- a/products/jbrowse-desktop/src/sessionModel/Base.ts +++ b/products/jbrowse-desktop/src/sessionModel/Base.ts @@ -5,10 +5,6 @@ import { Session as CoreSession } from '@jbrowse/product-core' export default function BaseSession(pluginManager: PluginManager) { return CoreSession.Base(pluginManager) .props({ - /** - * #property - */ - name: types.identifier, /** * #property */ diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index d843ee4df3..07c2ca757f 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -23,7 +23,6 @@ export default function sessionModelFactory( const sessionModel = types .compose( 'JBrowseDesktopSessionModel', - Base(pluginManager), types.compose( CoreSession.ReferenceManagement(pluginManager), CoreSession.Connections(pluginManager), @@ -33,6 +32,7 @@ export default function sessionModelFactory( CoreSession.Tracks(pluginManager), CoreSession.Views(pluginManager), ), + Base(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), TrackMenu(pluginManager), SessionManagement(pluginManager), diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index c5d31032a7..5a17db47fb 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -50,13 +50,13 @@ export default function sessionModelFactory( const sessionModel = types .compose( 'JBrowseWebSessionModel', - BaseSession(pluginManager), CoreSession.ReferenceManagement(pluginManager), CoreSession.DrawerWidgets(pluginManager), CoreSession.DialogQueue(pluginManager), CoreSession.Themes(pluginManager), CoreSession.Views(pluginManager), CoreSession.Tracks(pluginManager), + BaseSession(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), SessionConnections(pluginManager), ) From 969575546a91a73873e74db15a4ca2b206b0b05d Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 29 Apr 2023 10:47:40 -0700 Subject: [PATCH 21/44] update snaps, tests now passing --- .../components/__snapshots__/LinearGenomeView.test.tsx.snap | 4 ++-- .../__snapshots__/JBrowseLinearGenomeView.test.tsx.snap | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap index 79c012e959..06f37dea25 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap @@ -157,7 +157,7 @@ exports[`renders one track, one region 1`] = ` />
@@ -731,7 +731,7 @@ exports[`renders two tracks, two regions 1`] = ` />
diff --git a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap index fe55ab9a88..0fd291e2fc 100644 --- a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap +++ b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap @@ -246,7 +246,7 @@ exports[` renders successfully 1`] = ` />
From 373bbfa7d9238ae36060d47e04e441f212b311c6 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 1 May 2023 07:42:49 -0700 Subject: [PATCH 22/44] wip --- packages/product-core/src/Session/Base.ts | 12 --------- .../jbrowse-desktop/src/sessionModel/index.ts | 12 +++++++++ products/jbrowse-web/src/sessionModel/Base.ts | 27 ------------------- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index c6bf108702..e601c92080 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -66,18 +66,6 @@ export default function BaseSession(pluginManager: PluginManager) { get textSearchManager() { return this.root.textSearchManager }, - /** - * #getter - */ - get history() { - return this.root.history - }, - /** - * #getter - */ - get menus() { - return this.root.menus - }, })) .actions(self => ({ /** diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 07c2ca757f..0ac2fecd4f 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -38,6 +38,18 @@ export default function sessionModelFactory( SessionManagement(pluginManager), ) .views(self => ({ + /** + * #getter + */ + get history() { + return self.root.history + }, + /** + * #getter + */ + get menus() { + return self.root.menus + }, /** * #method */ diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts index 7c51dad090..25966b6912 100644 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -25,33 +25,6 @@ export function BaseSession(pluginManager: PluginManager) { sessionPlugins: types.array(types.frozen()), }) .views(self => ({ - /** - * #getter - */ - get jbrowse() { - return getParent(self).jbrowse - }, - })) - .views(self => ({ - /** - * #getter - */ - get rpcManager() { - return self.jbrowse.rpcManager as RpcManager - }, - /** - * #getter - */ - get configuration(): AnyConfigurationModel { - return self.jbrowse.configuration - }, - /** - * #getter - */ - get adminMode(): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return getParent(self).adminMode - }, /** * #getter */ From 249c49c1f1e8324b46c45dccceb70386f38ed838 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 1 May 2023 11:07:02 -0700 Subject: [PATCH 23/44] wip --- packages/core/util/types/util.ts | 17 ++- packages/product-core/src/RootModel/Base.ts | 5 +- .../src/RootModel/InternetAccounts.ts | 131 +++++++++++++++++- packages/product-core/src/RootModel/index.ts | 7 +- packages/product-core/src/Session/Base.ts | 13 +- .../product-core/src/Session/Connections.ts | 62 ++++----- .../src/Session/ReferenceManagement.ts | 4 + products/jbrowse-desktop/src/rootModel.ts | 123 ++-------------- products/jbrowse-web/src/sessionModel/Base.ts | 2 +- 9 files changed, 197 insertions(+), 167 deletions(-) diff --git a/packages/core/util/types/util.ts b/packages/core/util/types/util.ts index 8a92b437ed..501cab5fc8 100644 --- a/packages/core/util/types/util.ts +++ b/packages/core/util/types/util.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react' -import { IAnyModelType, Instance } from 'mobx-state-tree' +import { IAnyStateTreeNode, IModelType, Instance } from 'mobx-state-tree' import PluginManager from '../../PluginManager' /** @@ -22,7 +22,14 @@ export type AnyReactComponentType = React.ComponentType export type TypeTestedByPredicate boolean> = PREDICATE extends (thing: any) => thing is infer TYPE ? TYPE : never -/** get the type for an instance of an MST model in a PM factory function */ -export type InstanceOfModelReturnedBy< - FACTORY extends (pm: PluginManager) => IAnyModelType, -> = Instance> +/** type guard that inpects whether a MST model has an `afterCreate` action */ +export function isModelWithAfterCreate( + thing: IAnyStateTreeNode, +): thing is Instance void }>> { + return ( + typeof thing === 'object' && + thing !== null && + 'afterCreate' in thing && + typeof thing.afterCreate === 'function' + ) +} diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts index 4b057ac871..922ad799fe 100644 --- a/packages/product-core/src/RootModel/Base.ts +++ b/packages/product-core/src/RootModel/Base.ts @@ -9,7 +9,7 @@ import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' /** * factory function for the Base-level root model shared by all products */ -export function BaseRootModel< +export function BaseRoot< JBROWSE_MODEL_TYPE extends IAnyType, SESSION_MODEL_TYPE extends IAnyType, >( @@ -102,6 +102,7 @@ export function BaseRootModel< */ async renameCurrentSession(newName: string) { if (self.session) { + // @ts-expect-error this.setSession({ ...getSnapshot(self.session), name: newName }) } }, @@ -119,3 +120,5 @@ export function BaseRootModel< }, })) } + +export type BaseRootModelType = ReturnType diff --git a/packages/product-core/src/RootModel/InternetAccounts.ts b/packages/product-core/src/RootModel/InternetAccounts.ts index 814e956f85..4f40d108bf 100644 --- a/packages/product-core/src/RootModel/InternetAccounts.ts +++ b/packages/product-core/src/RootModel/InternetAccounts.ts @@ -1,5 +1,130 @@ import PluginManager from '@jbrowse/core/PluginManager' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { UriLocation, isModelWithAfterCreate } from '@jbrowse/core/util' +import { autorun } from 'mobx' +import { Instance, addDisposer, types } from 'mobx-state-tree' +import { BaseRootModelType } from './Base' -export default function InternetAccounts(pluginManager: PluginManager) { - -} \ No newline at end of file +export function InternetAccounts(pluginManager: PluginManager) { + return types + .model({ + /** + * #property + */ + internetAccounts: types.array( + pluginManager.pluggableMstType('internet account', 'stateModel'), + ), + }) + .actions(self => ({ + /** + * #action + */ + initializeInternetAccount( + internetAccountConfig: AnyConfigurationModel, + initialSnapshot = {}, + ) { + const internetAccountType = pluginManager.getInternetAccountType( + internetAccountConfig.type, + ) + if (!internetAccountType) { + throw new Error( + `unknown internet account type ${internetAccountConfig.type}`, + ) + } + + const length = self.internetAccounts.push({ + ...initialSnapshot, + type: internetAccountConfig.type, + configuration: internetAccountConfig, + }) + return self.internetAccounts[length - 1] + }, + + /** + * #action + */ + createEphemeralInternetAccount( + internetAccountId: string, + initialSnapshot = {}, + url: string, + ) { + let hostUri + + try { + hostUri = new URL(url).origin + } catch (e) { + // ignore + } + // id of a custom new internaccount is `${type}-${name}` + const internetAccountSplit = internetAccountId.split('-') + const configuration = { + type: internetAccountSplit[0], + internetAccountId: internetAccountId, + name: internetAccountSplit.slice(1).join('-'), + description: '', + domains: [hostUri], + } + const internetAccountType = pluginManager.getInternetAccountType( + configuration.type, + ) + const internetAccount = internetAccountType.stateModel.create({ + ...initialSnapshot, + type: configuration.type, + configuration, + }) + self.internetAccounts.push(internetAccount) + return internetAccount + }, + /** + * #action + */ + findAppropriateInternetAccount(location: UriLocation) { + // find the existing account selected from menu + const selectedId = location.internetAccountId + if (selectedId) { + const selectedAccount = self.internetAccounts.find(account => { + return account.internetAccountId === selectedId + }) + if (selectedAccount) { + return selectedAccount + } + } + + // if no existing account or not found, try to find working account + for (const account of self.internetAccounts) { + const handleResult = account.handlesLocation(location) + if (handleResult) { + return account + } + } + + // if still no existing account, create ephemeral config to use + return selectedId + ? this.createEphemeralInternetAccount(selectedId, {}, location.uri) + : null + }, + })) + .actions(self => { + const super_afterCreate = isModelWithAfterCreate(self) + ? self.afterCreate + : undefined + return { + afterCreate() { + if (super_afterCreate) { + super_afterCreate() + } + addDisposer( + self, + autorun(() => { + const { jbrowse } = self as typeof self & Instance + jbrowse.internetAccounts.forEach(account => { + self.initializeInternetAccount(account) + }) + }), + ) + }, + } + }) +} + +export type RootModelWithInternetAccounts = ReturnType diff --git a/packages/product-core/src/RootModel/index.ts b/packages/product-core/src/RootModel/index.ts index 1a3a70b0c2..c10fb9da1e 100644 --- a/packages/product-core/src/RootModel/index.ts +++ b/packages/product-core/src/RootModel/index.ts @@ -1,5 +1,2 @@ -import { Instance } from 'mobx-state-tree' -import { BaseRootModel as BaseRootModelF } from './Base' - -export { BaseRootModel } from './Base' -export type RootModel = Instance> +export * from './Base' +export * from './InternetAccounts' diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index e601c92080..4ba817194e 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -2,12 +2,15 @@ import shortid from 'shortid' import type PluginManager from '@jbrowse/core/PluginManager' import { Instance, getParent, types } from 'mobx-state-tree' -import { RootModel } from '../RootModel' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { BaseRootModelType } from '../RootModel' +import { AnyConfigurationSchemaType } from '@jbrowse/core/configuration' import { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager' /** base session shared by **all** JBrowse products. Be careful what you include here, everything will use it. */ -export default function BaseSession(pluginManager: PluginManager) { +export default function BaseSession< + ROOT_MODEL_TYPE extends BaseRootModelType = BaseRootModelType, + JB_CONFIG_SCHEMA extends AnyConfigurationSchemaType = AnyConfigurationSchemaType, +>(pluginManager: PluginManager) { return types .model({ /** @@ -30,7 +33,7 @@ export default function BaseSession(pluginManager: PluginManager) { })) .views(self => ({ get root() { - return getParent(self) + return getParent(self) }, /** * #getter @@ -47,7 +50,7 @@ export default function BaseSession(pluginManager: PluginManager) { /** * #getter */ - get configuration(): AnyConfigurationModel { + get configuration(): Instance { return this.jbrowse.configuration }, diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts index b21114edfd..ea41ccfce0 100644 --- a/packages/product-core/src/Session/Connections.ts +++ b/packages/product-core/src/Session/Connections.ts @@ -5,46 +5,32 @@ import { AnyConfigurationModel, readConfObject, } from '@jbrowse/core/configuration' -import { getParent, types } from 'mobx-state-tree' -import ReferenceManagement from './ReferenceManagement' -import { RootModel } from '../RootModel' +import { Instance, types } from 'mobx-state-tree' +import type { SessionWithReferenceManagement } from './ReferenceManagement' +import { BaseRootModelType } from '../RootModel' import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' import { BaseConnectionModel } from '@jbrowse/core/pluggableElementTypes/models/BaseConnectionModelFactory' export default function Connections(pluginManager: PluginManager) { - // connections: AnyConfigurationModel[] - // deleteConnection?: Function - // sessionConnections?: AnyConfigurationModel[] - // connectionInstances?: { - // name: string - // connectionId: string - // tracks: AnyConfigurationModel[] - // configuration: AnyConfigurationModel - // }[] - // makeConnection?: Function - return types - .compose( - 'ConnectionsManagementSessionMixin', - ReferenceManagement(pluginManager), - types.model({ - /** - * #property - */ - connectionInstances: types.array( - pluginManager.pluggableMstType( - 'connection', - 'stateModel', - ) as BaseConnectionModel, - ), - }), - ) + .model({ + /** + * #property + */ + connectionInstances: types.array( + pluginManager.pluggableMstType( + 'connection', + 'stateModel', + ) as BaseConnectionModel, + ), + }) .views(self => ({ /** * #getter */ get connections(): BaseConnectionConfigModel[] { - return getParent(self).jbrowse.connections + const { jbrowse } = self as typeof self & Instance + return jbrowse.connections }, })) .actions(self => ({ @@ -78,14 +64,16 @@ export default function Connections(pluginManager: PluginManager) { * #action */ prepareToBreakConnection(configuration: AnyConfigurationModel) { + const root = self as typeof self & + Instance const callbacksToDereferenceTrack: Function[] = [] const dereferenceTypeCount: Record = {} const name = readConfObject(configuration, 'name') const connection = self.connectionInstances.find(c => c.name === name) if (connection) { connection.tracks.forEach(track => { - const referring = self.getReferring(track) - self.removeReferring( + const referring = root.getReferring(track) + root.removeReferring( referring, track, callbacksToDereferenceTrack, @@ -117,18 +105,16 @@ export default function Connections(pluginManager: PluginManager) { * #action */ deleteConnection(configuration: AnyConfigurationModel) { - return getParent(self).jbrowse.deleteConnectionConf( - configuration, - ) + const { jbrowse } = self as typeof self & Instance + return jbrowse.deleteConnectionConf(configuration) }, /** * #action */ addConnectionConf(connectionConf: BaseConnectionConfigModel) { - return getParent(self).jbrowse.addConnectionConf( - connectionConf, - ) + const { jbrowse } = self as typeof self & Instance + return jbrowse.addConnectionConf(connectionConf) }, /** diff --git a/packages/product-core/src/Session/ReferenceManagement.ts b/packages/product-core/src/Session/ReferenceManagement.ts index 50abef2459..1de149c839 100644 --- a/packages/product-core/src/Session/ReferenceManagement.ts +++ b/packages/product-core/src/Session/ReferenceManagement.ts @@ -104,3 +104,7 @@ export default function ReferenceManagement(pluginManager: PluginManager) { }, })) } + +export type SessionWithReferenceManagement = ReturnType< + typeof ReferenceManagement +> diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts index 701a2bd9fc..ecd096dbbd 100644 --- a/products/jbrowse-desktop/src/rootModel.ts +++ b/products/jbrowse-desktop/src/rootModel.ts @@ -1,7 +1,6 @@ import { lazy } from 'react' import { addDisposer, - cast, getSnapshot, types, SnapshotIn, @@ -15,9 +14,8 @@ import RpcManager from '@jbrowse/core/rpc/RpcManager' import TimeTraveller from '@jbrowse/core/util/TimeTraveller' import { MenuItem } from '@jbrowse/core/ui' import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import { UriLocation } from '@jbrowse/core/util/types' -import { BaseRootModel } from '@jbrowse/product-core' +import { BaseRoot, InternetAccounts } from '@jbrowse/product-core' // icons import OpenIcon from '@mui/icons-material/FolderOpen' @@ -52,6 +50,8 @@ interface Menu { menuItems: MenuItem[] } +// IN PROGRESS: factoring more stuff out of this root model into core + /** * #stateModel JBrowseDesktopRootModel * note that many properties of the root model are available through the session, which @@ -61,12 +61,17 @@ export default function rootModelFactory(pluginManager: PluginManager) { const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) const JobsManager = jobsModelFactory(pluginManager) - return BaseRootModel( - pluginManager, - JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), - Session, - assemblyConfigSchema, - ) + return types + .compose( + 'JBrowseDesktopRootModel', + BaseRoot( + pluginManager, + JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), + Session, + assemblyConfigSchema, + ), + InternetAccounts(pluginManager), + ) .props({ /** * #property @@ -76,12 +81,6 @@ export default function rootModelFactory(pluginManager: PluginManager) { * #property */ savedSessionNames: types.maybe(types.array(types.string)), - /** - * #property - */ - internetAccounts: types.array( - pluginManager.pluggableMstType('internet account', 'stateModel'), - ), /** * #property */ @@ -119,92 +118,6 @@ export default function rootModelFactory(pluginManager: PluginManager) { self.setSession(snapshot) } }, - /** - * #action - */ - initializeInternetAccount( - internetAccountConfig: AnyConfigurationModel, - initialSnapshot = {}, - ) { - const internetAccountType = pluginManager.getInternetAccountType( - internetAccountConfig.type, - ) - if (!internetAccountType) { - throw new Error( - `unknown internet account type ${internetAccountConfig.type}`, - ) - } - - const length = self.internetAccounts.push({ - ...initialSnapshot, - type: internetAccountConfig.type, - configuration: internetAccountConfig, - }) - return self.internetAccounts[length - 1] - }, - /** - * #action - */ - createEphemeralInternetAccount( - internetAccountId: string, - initialSnapshot = {}, - url: string, - ) { - let hostUri - - try { - hostUri = new URL(url).origin - } catch (e) { - // ignore - } - // id of a custom new internaccount is `${type}-${name}` - const internetAccountSplit = internetAccountId.split('-') - const configuration = { - type: internetAccountSplit[0], - internetAccountId: internetAccountId, - name: internetAccountSplit.slice(1).join('-'), - description: '', - domains: [hostUri], - } - const internetAccountType = pluginManager.getInternetAccountType( - configuration.type, - ) - const internetAccount = internetAccountType.stateModel.create({ - ...initialSnapshot, - type: configuration.type, - configuration, - }) - self.internetAccounts.push(internetAccount) - return internetAccount - }, - /** - * #action - */ - findAppropriateInternetAccount(location: UriLocation) { - // find the existing account selected from menu - const selectedId = location.internetAccountId - if (selectedId) { - const selectedAccount = self.internetAccounts.find(account => { - return account.internetAccountId === selectedId - }) - if (selectedAccount) { - return selectedAccount - } - } - - // if no existing account or not found, try to find working account - for (const account of self.internetAccounts) { - const handleResult = account.handlesLocation(location) - if (handleResult) { - return account - } - } - - // if still no existing account, create ephemeral config to use - return selectedId - ? this.createEphemeralInternetAccount(selectedId, {}, location.uri) - : null - }, })) .volatile(self => ({ menus: [ @@ -600,14 +513,6 @@ export default function rootModelFactory(pluginManager: PluginManager) { } }), ) - addDisposer( - self, - autorun(() => { - self.jbrowse.internetAccounts.forEach(account => { - self.initializeInternetAccount(account) - }) - }), - ) addDisposer( self, autorun( diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts index 25966b6912..95aba1c4b1 100644 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -29,7 +29,7 @@ export function BaseSession(pluginManager: PluginManager) { * #getter */ get tracks(): AnyConfigurationModel[] { - return [...self.sessionTracks, ...getParent(self).jbrowse.tracks] + return [...self.sessionTracks, ...self.jbrowse.tracks] }, })) .actions(self => ({ From 6fe456e58f239f85c57f3f987521ee6d2c4c8d9a Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 1 May 2023 14:24:40 -0700 Subject: [PATCH 24/44] break up jb desktop root model more --- .../product-core/src/Session/DialogQueue.ts | 3 +- products/jbrowse-desktop/src/jbrowseModel.ts | 5 +- products/jbrowse-desktop/src/rootModel.ts | 536 ------------------ .../src/rootModel/HistoryManagement.ts | 50 ++ .../jbrowse-desktop/src/rootModel/Menus.ts | 385 +++++++++++++ .../jbrowse-desktop/src/rootModel/Sessions.ts | 99 ++++ .../__snapshots__/index.test.js.snap} | 0 .../index.test.js} | 6 +- .../jbrowse-desktop/src/rootModel/index.ts | 61 ++ products/jbrowse-web/src/sessionModel/Base.ts | 3 +- .../jbrowse-web/src/sessionModel/index.ts | 32 +- 11 files changed, 618 insertions(+), 562 deletions(-) delete mode 100644 products/jbrowse-desktop/src/rootModel.ts create mode 100644 products/jbrowse-desktop/src/rootModel/HistoryManagement.ts create mode 100644 products/jbrowse-desktop/src/rootModel/Menus.ts create mode 100644 products/jbrowse-desktop/src/rootModel/Sessions.ts rename products/jbrowse-desktop/src/{__snapshots__/rootModel.test.js.snap => rootModel/__snapshots__/index.test.js.snap} (100%) rename products/jbrowse-desktop/src/{rootModel.test.js => rootModel/index.test.js} (94%) create mode 100644 products/jbrowse-desktop/src/rootModel/index.ts diff --git a/packages/product-core/src/Session/DialogQueue.ts b/packages/product-core/src/Session/DialogQueue.ts index 5daab9634d..3f41c81807 100644 --- a/packages/product-core/src/Session/DialogQueue.ts +++ b/packages/product-core/src/Session/DialogQueue.ts @@ -57,4 +57,5 @@ export default function DialogQueue(pluginManager: PluginManager) { })) } -export type DialogQueueManager = Instance> +export type DialogQueueManagerType = ReturnType +export type DialogQueueManager = Instance diff --git a/products/jbrowse-desktop/src/jbrowseModel.ts b/products/jbrowse-desktop/src/jbrowseModel.ts index bc266d36ea..7efd0a21a1 100644 --- a/products/jbrowse-desktop/src/jbrowseModel.ts +++ b/products/jbrowse-desktop/src/jbrowseModel.ts @@ -1,8 +1,5 @@ import { readConfObject } from '@jbrowse/core/configuration' -import { - AnyConfigurationModel, - AnyConfigurationSchemaType, -} from '@jbrowse/core/configuration' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' import { PluginDefinition } from '@jbrowse/core/PluginLoader' import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' diff --git a/products/jbrowse-desktop/src/rootModel.ts b/products/jbrowse-desktop/src/rootModel.ts deleted file mode 100644 index ecd096dbbd..0000000000 --- a/products/jbrowse-desktop/src/rootModel.ts +++ /dev/null @@ -1,536 +0,0 @@ -import { lazy } from 'react' -import { - addDisposer, - getSnapshot, - types, - SnapshotIn, - Instance, -} from 'mobx-state-tree' -import { autorun } from 'mobx' -import makeWorkerInstance from './makeWorkerInstance' -import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' -import PluginManager from '@jbrowse/core/PluginManager' -import RpcManager from '@jbrowse/core/rpc/RpcManager' -import TimeTraveller from '@jbrowse/core/util/TimeTraveller' -import { MenuItem } from '@jbrowse/core/ui' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' - -import { BaseRoot, InternetAccounts } from '@jbrowse/product-core' - -// icons -import OpenIcon from '@mui/icons-material/FolderOpen' -import ExtensionIcon from '@mui/icons-material/Extension' -import AppsIcon from '@mui/icons-material/Apps' -import StorageIcon from '@mui/icons-material/Storage' -import SettingsIcon from '@mui/icons-material/Settings' -import MeetingRoomIcon from '@mui/icons-material/MeetingRoom' -import UndoIcon from '@mui/icons-material/Undo' -import RedoIcon from '@mui/icons-material/Redo' -import { Save, SaveAs, DNA, Cable } from '@jbrowse/core/ui/Icons' - -// locals -import sessionModelFactory from './sessionModel' -import jobsModelFactory from './indexJobsModel' -import JBrowseDesktop from './jbrowseModel' -import OpenSequenceDialog from './OpenSequenceDialog' - -const { ipcRenderer } = window.require('electron') - -const PreferencesDialog = lazy(() => import('./PreferencesDialog')) - -function getSaveSession(model: Instance>) { - return { - ...getSnapshot(model.jbrowse), - defaultSession: model.session ? getSnapshot(model.session) : {}, - } -} - -interface Menu { - label: string - menuItems: MenuItem[] -} - -// IN PROGRESS: factoring more stuff out of this root model into core - -/** - * #stateModel JBrowseDesktopRootModel - * note that many properties of the root model are available through the session, which - * may be preferable since using getSession() is better relied on than getRoot() - */ -export default function rootModelFactory(pluginManager: PluginManager) { - const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) - const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) - const JobsManager = jobsModelFactory(pluginManager) - return types - .compose( - 'JBrowseDesktopRootModel', - BaseRoot( - pluginManager, - JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), - Session, - assemblyConfigSchema, - ), - InternetAccounts(pluginManager), - ) - .props({ - /** - * #property - */ - jobsManager: types.maybe(JobsManager), - /** - * #property - */ - savedSessionNames: types.maybe(types.array(types.string)), - /** - * #property - */ - sessionPath: types.optional(types.string, ''), - /** - * #property - * used for undo/redo - */ - history: types.optional(TimeTraveller, { targetPath: '../session' }), - }) - .actions(self => ({ - /** - * #action - */ - async saveSession(val: unknown) { - if (self.sessionPath) { - await ipcRenderer.invoke('saveSession', self.sessionPath, val) - } - }, - /** - * #action - */ - duplicateCurrentSession() { - if (self.session) { - const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) - let newSnapshotName = `${self.session.name} (copy)` - if (self.jbrowse.savedSessionNames.includes(newSnapshotName)) { - let newSnapshotCopyNumber = 2 - do { - newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` - newSnapshotCopyNumber += 1 - } while (self.jbrowse.savedSessionNames.includes(newSnapshotName)) - } - snapshot.name = newSnapshotName - self.setSession(snapshot) - } - }, - })) - .volatile(self => ({ - menus: [ - { - label: 'File', - menuItems: [ - { - label: 'Open', - icon: OpenIcon, - onClick: async () => { - try { - const path = await ipcRenderer.invoke('promptOpenFile') - if (path) { - await self.openNewSessionCallback(path) - } - } catch (e) { - console.error(e) - self.session?.notify(`${e}`, 'error') - } - }, - }, - { - label: 'Save', - icon: Save, - 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, - 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') - } - }, - }, - { - type: 'divider', - }, - { - label: 'Open assembly...', - icon: DNA, - onClick: () => { - if (self.session) { - self.session.queueDialog(doneCallback => [ - OpenSequenceDialog, - { - model: self, - onClose: (confs: AnyConfigurationModel[]) => { - try { - confs?.forEach(conf => { - self.jbrowse.addAssemblyConf(conf) - }) - } catch (e) { - console.error(e) - self.session?.notify(`${e}`) - } - doneCallback() - }, - }, - ]) - } - }, - }, - { - 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 > 0) { - 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: Cable, - onClick: () => { - if (self.session) { - const widget = self.session.addWidget( - 'AddConnectionWidget', - 'addConnectionWidget', - ) - self.session.showWidget(widget) - } - }, - }, - { - type: 'divider', - }, - { - label: 'Return to start screen', - icon: AppsIcon, - onClick: () => { - self.setSession(undefined) - }, - }, - { - label: 'Exit', - icon: MeetingRoomIcon, - onClick: async () => { - await ipcRenderer.invoke('quit') - }, - }, - ], - }, - { - label: 'Add', - menuItems: [], - }, - { - label: 'Tools', - menuItems: [ - { - label: 'Undo', - icon: UndoIcon, - onClick: () => { - if (self.history.canUndo) { - self.history.undo() - } - }, - }, - { - label: 'Redo', - icon: RedoIcon, - onClick: () => { - if (self.history.canRedo) { - self.history.redo() - } - }, - }, - { type: 'divider' }, - { - label: 'Plugin store', - icon: ExtensionIcon, - onClick: () => { - if (self.session) { - const widget = self.session.addWidget( - 'PluginStoreWidget', - 'pluginStoreWidget', - ) - self.session.showWidget(widget) - } - }, - }, - { - label: 'Preferences', - icon: SettingsIcon, - onClick: () => { - if (self.session) { - self.session.queueDialog(handleClose => [ - PreferencesDialog, - { - session: self.session, - handleClose, - }, - ]) - } - }, - }, - { - label: 'Open assembly manager', - icon: SettingsIcon, - onClick: () => { - self.setAssemblyEditing(true) - }, - }, - ], - }, - ] as Menu[], - rpcManager: new RpcManager( - pluginManager, - self.jbrowse.configuration.rpc, - { - WebWorkerRpcDriver: { - makeWorkerInstance, - }, - MainThreadRpcDriver: {}, - }, - ), - adminMode: true, - })) - .actions(self => ({ - /** - * #action - */ - activateSession(sessionSnapshot: SnapshotIn) { - self.setSession(sessionSnapshot) - }, - /** - * #action - */ - setMenus(newMenus: Menu[]) { - self.menus = newMenus - }, - async setPluginsUpdated() { - if (self.session) { - await self.saveSession(getSaveSession(self as RootModel)) - } - await self.openNewSessionCallback(self.sessionPath) - }, - /** - * #action - * Add a top-level menu - * @param menuName - Name of the menu to insert. - * @returns The new length of the top-level menus array - */ - appendMenu(menuName: string) { - return self.menus.push({ label: menuName, menuItems: [] }) - }, - /** - * #action - * Insert a top-level menu - * @param menuName - Name of the menu to insert. - * @param position - Position to insert menu. If negative, counts from th - * end, e.g. `insertMenu('My Menu', -1)` will insert the menu as the - * second-to-last one. - * @returns The new length of the top-level menus array - */ - insertMenu(menuName: string, position: number) { - const insertPosition = - position < 0 ? self.menus.length + position : position - self.menus.splice(insertPosition, 0, { label: menuName, menuItems: [] }) - return self.menus.length - }, - /** - * #action - * Add a menu item to a top-level menu - * @param menuName - Name of the top-level menu to append to. - * @param menuItem - Menu item to append. - * @returns The new length of the menu - */ - appendToMenu(menuName: string, menuItem: MenuItem) { - const menu = self.menus.find(m => m.label === menuName) - if (!menu) { - self.menus.push({ label: menuName, menuItems: [menuItem] }) - return 1 - } - return menu.menuItems.push(menuItem) - }, - /** - * #action - * Insert a menu item into a top-level menu - * @param menuName - Name of the top-level menu to insert into - * @param menuItem - Menu item to insert - * @param position - Position to insert menu item. If negative, counts - * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as - * the second-to-last one. - * @returns The new length of the menu - */ - insertInMenu(menuName: string, menuItem: MenuItem, position: number) { - const menu = self.menus.find(m => m.label === menuName) - if (!menu) { - self.menus.push({ label: menuName, menuItems: [menuItem] }) - return 1 - } - const insertPosition = - position < 0 ? menu.menuItems.length + position : position - menu.menuItems.splice(insertPosition, 0, menuItem) - return menu.menuItems.length - }, - /** - * #action - * Add a menu item to a sub-menu - * @param menuPath - Path to the sub-menu to add to, starting with the - * top-level menu (e.g. `['File', 'Insert']`). - * @param menuItem - Menu item to append. - * @returns The new length of the sub-menu - */ - appendToSubMenu(menuPath: string[], menuItem: MenuItem) { - let topMenu = self.menus.find(m => m.label === menuPath[0]) - if (!topMenu) { - const idx = this.appendMenu(menuPath[0]) - topMenu = self.menus[idx - 1] - } - let { menuItems: subMenu } = topMenu - const pathSoFar = [menuPath[0]] - menuPath.slice(1).forEach(menuName => { - pathSoFar.push(menuName) - let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) - if (!sm) { - const idx = subMenu.push({ label: menuName, subMenu: [] }) - sm = subMenu[idx - 1] - } - if (!('subMenu' in sm)) { - throw new Error( - `"${menuName}" in path "${pathSoFar}" is not a subMenu`, - ) - } - subMenu = sm.subMenu - }) - return subMenu.push(menuItem) - }, - - /** - * #action - * Insert a menu item into a sub-menu - * @param menuPath - Path to the sub-menu to add to, starting with the - * top-level menu (e.g. `['File', 'Insert']`). - * @param menuItem - Menu item to insert. - * @param position - Position to insert menu item. If negative, counts - * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as - * the second-to-last one. - * @returns The new length of the sub-menu - */ - insertInSubMenu( - menuPath: string[], - menuItem: MenuItem, - position: number, - ) { - let topMenu = self.menus.find(m => m.label === menuPath[0]) - if (!topMenu) { - const idx = this.appendMenu(menuPath[0]) - topMenu = self.menus[idx - 1] - } - let { menuItems: subMenu } = topMenu - const pathSoFar = [menuPath[0]] - menuPath.slice(1).forEach(menuName => { - pathSoFar.push(menuName) - let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) - if (!sm) { - const idx = subMenu.push({ label: menuName, subMenu: [] }) - sm = subMenu[idx - 1] - } - if (!('subMenu' in sm)) { - throw new Error( - `"${menuName}" in path "${pathSoFar}" is not a subMenu`, - ) - } - subMenu = sm.subMenu - }) - subMenu.splice(position, 0, menuItem) - return subMenu.length - }, - - afterCreate() { - document.addEventListener('keydown', e => { - if ( - self.history.canRedo && - // ctrl+shift+z or cmd+shift+z - (((e.ctrlKey || e.metaKey) && e.shiftKey && e.code === 'KeyZ') || - // ctrl+y - (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) - ) { - self.history.redo() - } - if ( - self.history.canUndo && - // ctrl+z or cmd+z - (e.ctrlKey || e.metaKey) && - !e.shiftKey && - e.code === 'KeyZ' - ) { - self.history.undo() - } - }) - addDisposer( - self, - autorun(() => { - if (self.session) { - // we use a specific initialization routine after session is - // created to get it to start tracking itself sort of related - // issue here - // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 - self.history.initialize() - } - }), - ) - addDisposer( - self, - autorun( - async () => { - if (self.session) { - try { - await self.saveSession(getSaveSession(self as RootModel)) - } catch (e) { - console.error(e) - } - } - }, - { delay: 1000 }, - ), - ) - }, - })) -} - -export type RootModelType = ReturnType -export type RootModel = Instance diff --git a/products/jbrowse-desktop/src/rootModel/HistoryManagement.ts b/products/jbrowse-desktop/src/rootModel/HistoryManagement.ts new file mode 100644 index 0000000000..8ed4f6c2f1 --- /dev/null +++ b/products/jbrowse-desktop/src/rootModel/HistoryManagement.ts @@ -0,0 +1,50 @@ +import TimeTraveller from '@jbrowse/core/util/TimeTraveller' +import { BaseRootModelType } from '@jbrowse/product-core' +import { autorun } from 'mobx' +import { Instance, addDisposer, types } from 'mobx-state-tree' + +export const HistoryManagement = types + .model({ + /** + * #property + * used for undo/redo + */ + history: types.optional(TimeTraveller, { targetPath: '../session' }), + }) + .actions(self => ({ + afterCreate() { + document.addEventListener('keydown', e => { + if ( + self.history.canRedo && + // ctrl+shift+z or cmd+shift+z + (((e.ctrlKey || e.metaKey) && e.shiftKey && e.code === 'KeyZ') || + // ctrl+y + (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) + ) { + self.history.redo() + } + if ( + self.history.canUndo && + // ctrl+z or cmd+z + (e.ctrlKey || e.metaKey) && + !e.shiftKey && + e.code === 'KeyZ' + ) { + self.history.undo() + } + }) + addDisposer( + self, + autorun(() => { + const { session } = self as typeof self & Instance + if (session) { + // we use a specific initialization routine after session is + // created to get it to start tracking itself sort of related + // issue here + // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 + self.history.initialize() + } + }), + ) + }, + })) diff --git a/products/jbrowse-desktop/src/rootModel/Menus.ts b/products/jbrowse-desktop/src/rootModel/Menus.ts new file mode 100644 index 0000000000..4ff241befb --- /dev/null +++ b/products/jbrowse-desktop/src/rootModel/Menus.ts @@ -0,0 +1,385 @@ +import PluginManager from '@jbrowse/core/PluginManager' +import { Instance, types } from 'mobx-state-tree' +import { lazy } from 'react' + +// icons +import OpenIcon from '@mui/icons-material/FolderOpen' +import ExtensionIcon from '@mui/icons-material/Extension' +import AppsIcon from '@mui/icons-material/Apps' +import StorageIcon from '@mui/icons-material/Storage' +import SettingsIcon from '@mui/icons-material/Settings' +import MeetingRoomIcon from '@mui/icons-material/MeetingRoom' +import UndoIcon from '@mui/icons-material/Undo' +import RedoIcon from '@mui/icons-material/Redo' + +import type { MenuItem } from '@jbrowse/core/ui' +import { Save, SaveAs, DNA, Cable } from '@jbrowse/core/ui/Icons' +import type { AnyConfigurationModel } from '@jbrowse/core/configuration' + +import OpenSequenceDialog from '../OpenSequenceDialog' +import type { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' +import { getSaveSession } from './Sessions' +import { RootModel, RootModelType } from '.' + +const PreferencesDialog = lazy(() => import('../PreferencesDialog')) +const { ipcRenderer } = window.require('electron') + +export interface Menu { + label: string + menuItems: MenuItem[] +} + +export default function Menus(pluginManager: PluginManager) { + return types + .model({}) + .volatile(s => { + const self = s as RootModel + return { + menus: [ + { + label: 'File', + menuItems: [ + { + label: 'Open', + icon: OpenIcon, + onClick: async () => { + try { + const path = await ipcRenderer.invoke('promptOpenFile') + if (path) { + await self.openNewSessionCallback(path) + } + } catch (e) { + console.error(e) + self.session?.notify(`${e}`, 'error') + } + }, + }, + { + label: 'Save', + icon: Save, + onClick: async () => { + if (self.session) { + try { + await self.saveSession(getSaveSession(self)) + } catch (e) { + console.error(e) + self.session?.notify(`${e}`, 'error') + } + } + }, + }, + { + label: 'Save as...', + icon: SaveAs, + onClick: async () => { + try { + const saveAsPath = await ipcRenderer.invoke( + 'promptSessionSaveAs', + ) + self.setSessionPath(saveAsPath) + await self.saveSession(getSaveSession(self)) + } catch (e) { + console.error(e) + self.session?.notify(`${e}`, 'error') + } + }, + }, + { + type: 'divider', + }, + { + label: 'Open assembly...', + icon: DNA, + onClick: () => { + if (self.session) { + const session = self.session as DialogQueueManager + session.queueDialog(doneCallback => [ + OpenSequenceDialog, + { + model: self, + onClose: (confs: AnyConfigurationModel[]) => { + try { + confs?.forEach(conf => { + self.jbrowse.addAssemblyConf(conf) + }) + } catch (e) { + console.error(e) + self.session?.notify(`${e}`) + } + doneCallback() + }, + }, + ]) + } + }, + }, + { + 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 > 0) { + 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: Cable, + onClick: () => { + if (self.session) { + const widget = self.session.addWidget( + 'AddConnectionWidget', + 'addConnectionWidget', + ) + self.session.showWidget(widget) + } + }, + }, + { + type: 'divider', + }, + { + label: 'Return to start screen', + icon: AppsIcon, + onClick: () => { + self.setSession(undefined) + }, + }, + { + label: 'Exit', + icon: MeetingRoomIcon, + onClick: async () => { + await ipcRenderer.invoke('quit') + }, + }, + ], + }, + { + label: 'Add', + menuItems: [], + }, + { + label: 'Tools', + menuItems: [ + { + label: 'Undo', + icon: UndoIcon, + onClick: () => { + if (self.history.canUndo) { + self.history.undo() + } + }, + }, + { + label: 'Redo', + icon: RedoIcon, + onClick: () => { + if (self.history.canRedo) { + self.history.redo() + } + }, + }, + { type: 'divider' }, + { + label: 'Plugin store', + icon: ExtensionIcon, + onClick: () => { + if (self.session) { + const widget = self.session.addWidget( + 'PluginStoreWidget', + 'pluginStoreWidget', + ) + self.session.showWidget(widget) + } + }, + }, + { + label: 'Preferences', + icon: SettingsIcon, + onClick: () => { + if (self.session) { + const session = self.session as DialogQueueManager + session.queueDialog(handleClose => [ + PreferencesDialog, + { + session: self.session, + handleClose, + }, + ]) + } + }, + }, + { + label: 'Open assembly manager', + icon: SettingsIcon, + onClick: () => { + self.setAssemblyEditing(true) + }, + }, + ], + }, + ] as Menu[], + } + }) + .actions(self => ({ + /** + * #action + */ + setMenus(newMenus: Menu[]) { + self.menus = newMenus + }, + async setPluginsUpdated() { + const root = self as Instance + if (root.session) { + await root.saveSession(getSaveSession(root)) + } + await root.openNewSessionCallback(root.sessionPath) + }, + /** + * #action + * Add a top-level menu + * @param menuName - Name of the menu to insert. + * @returns The new length of the top-level menus array + */ + appendMenu(menuName: string) { + return self.menus.push({ label: menuName, menuItems: [] }) + }, + /** + * #action + * Insert a top-level menu + * @param menuName - Name of the menu to insert. + * @param position - Position to insert menu. If negative, counts from th + * end, e.g. `insertMenu('My Menu', -1)` will insert the menu as the + * second-to-last one. + * @returns The new length of the top-level menus array + */ + insertMenu(menuName: string, position: number) { + const insertPosition = + position < 0 ? self.menus.length + position : position + self.menus.splice(insertPosition, 0, { label: menuName, menuItems: [] }) + return self.menus.length + }, + /** + * #action + * Add a menu item to a top-level menu + * @param menuName - Name of the top-level menu to append to. + * @param menuItem - Menu item to append. + * @returns The new length of the menu + */ + appendToMenu(menuName: string, menuItem: MenuItem) { + const menu = self.menus.find(m => m.label === menuName) + if (!menu) { + self.menus.push({ label: menuName, menuItems: [menuItem] }) + return 1 + } + return menu.menuItems.push(menuItem) + }, + /** + * #action + * Insert a menu item into a top-level menu + * @param menuName - Name of the top-level menu to insert into + * @param menuItem - Menu item to insert + * @param position - Position to insert menu item. If negative, counts + * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as + * the second-to-last one. + * @returns The new length of the menu + */ + insertInMenu(menuName: string, menuItem: MenuItem, position: number) { + const menu = self.menus.find(m => m.label === menuName) + if (!menu) { + self.menus.push({ label: menuName, menuItems: [menuItem] }) + return 1 + } + const insertPosition = + position < 0 ? menu.menuItems.length + position : position + menu.menuItems.splice(insertPosition, 0, menuItem) + return menu.menuItems.length + }, + /** + * #action + * Add a menu item to a sub-menu + * @param menuPath - Path to the sub-menu to add to, starting with the + * top-level menu (e.g. `['File', 'Insert']`). + * @param menuItem - Menu item to append. + * @returns The new length of the sub-menu + */ + appendToSubMenu(menuPath: string[], menuItem: MenuItem) { + let topMenu = self.menus.find(m => m.label === menuPath[0]) + if (!topMenu) { + const idx = this.appendMenu(menuPath[0]) + topMenu = self.menus[idx - 1] + } + let { menuItems: subMenu } = topMenu + const pathSoFar = [menuPath[0]] + menuPath.slice(1).forEach(menuName => { + pathSoFar.push(menuName) + let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) + if (!sm) { + const idx = subMenu.push({ label: menuName, subMenu: [] }) + sm = subMenu[idx - 1] + } + if (!('subMenu' in sm)) { + throw new Error( + `"${menuName}" in path "${pathSoFar}" is not a subMenu`, + ) + } + subMenu = sm.subMenu + }) + return subMenu.push(menuItem) + }, + + /** + * #action + * Insert a menu item into a sub-menu + * @param menuPath - Path to the sub-menu to add to, starting with the + * top-level menu (e.g. `['File', 'Insert']`). + * @param menuItem - Menu item to insert. + * @param position - Position to insert menu item. If negative, counts + * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as + * the second-to-last one. + * @returns The new length of the sub-menu + */ + insertInSubMenu( + menuPath: string[], + menuItem: MenuItem, + position: number, + ) { + let topMenu = self.menus.find(m => m.label === menuPath[0]) + if (!topMenu) { + const idx = this.appendMenu(menuPath[0]) + topMenu = self.menus[idx - 1] + } + let { menuItems: subMenu } = topMenu + const pathSoFar = [menuPath[0]] + menuPath.slice(1).forEach(menuName => { + pathSoFar.push(menuName) + let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) + if (!sm) { + const idx = subMenu.push({ label: menuName, subMenu: [] }) + sm = subMenu[idx - 1] + } + if (!('subMenu' in sm)) { + throw new Error( + `"${menuName}" in path "${pathSoFar}" is not a subMenu`, + ) + } + subMenu = sm.subMenu + }) + subMenu.splice(position, 0, menuItem) + return subMenu.length + }, + })) +} diff --git a/products/jbrowse-desktop/src/rootModel/Sessions.ts b/products/jbrowse-desktop/src/rootModel/Sessions.ts new file mode 100644 index 0000000000..5b8d27b0ad --- /dev/null +++ b/products/jbrowse-desktop/src/rootModel/Sessions.ts @@ -0,0 +1,99 @@ +import PluginManager from '@jbrowse/core/PluginManager' +import { BaseRootModelType } from '@jbrowse/product-core' +import { autorun } from 'mobx' +import { + Instance, + SnapshotIn, + addDisposer, + getSnapshot, + types, +} from 'mobx-state-tree' +import { BaseSessionModel } from '../sessionModel/Base' + +const { ipcRenderer } = window.require('electron') + +export function getSaveSession(model: Instance) { + return { + ...getSnapshot(model.jbrowse), + defaultSession: model.session ? getSnapshot(model.session) : {}, + } +} + +export default function SessionManagement(pluginManager: PluginManager) { + return types + .model({ + /** + * #property + */ + savedSessionNames: types.maybe(types.array(types.string)), + /** + * #property + */ + sessionPath: types.optional(types.string, ''), + }) + .actions(s => { + const self = s as typeof s & Instance + return { + /** + * #action + */ + async saveSession(val: unknown) { + if (self.sessionPath) { + await ipcRenderer.invoke('saveSession', self.sessionPath, val) + } + }, + /** + * #action + */ + duplicateCurrentSession() { + if (self.session) { + const snapshot = JSON.parse( + JSON.stringify(getSnapshot(self.session)), + ) + let newSnapshotName = `${self.session.name} (copy)` + if (self.jbrowse.savedSessionNames.includes(newSnapshotName)) { + let newSnapshotCopyNumber = 2 + do { + newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` + newSnapshotCopyNumber += 1 + } while (self.jbrowse.savedSessionNames.includes(newSnapshotName)) + } + snapshot.name = newSnapshotName + self.setSession(snapshot) + } + }, + /** + * #action + */ + activateSession(sessionSnapshot: SnapshotIn) { + self.setSession(sessionSnapshot) + }, + } + }) + .actions(s => { + const self = s as typeof s & Instance + return { + afterCreate() { + addDisposer( + self, + autorun( + async () => { + if (self.session) { + try { + await self.saveSession( + getSaveSession(self as Instance), + ) + } catch (e) { + console.error(e) + } + } + }, + { delay: 1000 }, + ), + ) + }, + } + }) +} + +export type SessionManagerType = ReturnType diff --git a/products/jbrowse-desktop/src/__snapshots__/rootModel.test.js.snap b/products/jbrowse-desktop/src/rootModel/__snapshots__/index.test.js.snap similarity index 100% rename from products/jbrowse-desktop/src/__snapshots__/rootModel.test.js.snap rename to products/jbrowse-desktop/src/rootModel/__snapshots__/index.test.js.snap diff --git a/products/jbrowse-desktop/src/rootModel.test.js b/products/jbrowse-desktop/src/rootModel/index.test.js similarity index 94% rename from products/jbrowse-desktop/src/rootModel.test.js rename to products/jbrowse-desktop/src/rootModel/index.test.js index 890399f62c..460588f028 100644 --- a/products/jbrowse-desktop/src/rootModel.test.js +++ b/products/jbrowse-desktop/src/rootModel/index.test.js @@ -4,9 +4,9 @@ import electron from 'electron' import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' -import corePlugins from './corePlugins' -import rootModelFactory from './rootModel' -jest.mock('./makeWorkerInstance', () => () => {}) +import corePlugins from '../corePlugins' +import rootModelFactory from '.' +jest.mock('../makeWorkerInstance', () => () => {}) describe('Root MST model', () => { let rootModel diff --git a/products/jbrowse-desktop/src/rootModel/index.ts b/products/jbrowse-desktop/src/rootModel/index.ts new file mode 100644 index 0000000000..a1ae6c8541 --- /dev/null +++ b/products/jbrowse-desktop/src/rootModel/index.ts @@ -0,0 +1,61 @@ +import { types, Instance } from 'mobx-state-tree' +import makeWorkerInstance from '../makeWorkerInstance' +import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' +import PluginManager from '@jbrowse/core/PluginManager' +import RpcManager from '@jbrowse/core/rpc/RpcManager' + +import { BaseRoot, InternetAccounts } from '@jbrowse/product-core' + +// locals +import sessionModelFactory from '../sessionModel' +import jobsModelFactory from '../indexJobsModel' +import JBrowseDesktop from '../jbrowseModel' +import Menus from './Menus' +import SessionManagement from './Sessions' +import { HistoryManagement } from './HistoryManagement' + +/** + * #stateModel JBrowseDesktopRootModel + * note that many properties of the root model are available through the session, which + * may be preferable since using getSession() is better relied on than getRoot() + */ +export default function rootModelFactory(pluginManager: PluginManager) { + const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) + const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) + const JobsManager = jobsModelFactory(pluginManager) + return types + .compose( + 'JBrowseDesktopRootModel', + BaseRoot( + pluginManager, + JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), + Session, + assemblyConfigSchema, + ), + InternetAccounts(pluginManager), + Menus(pluginManager), + SessionManagement(pluginManager), + HistoryManagement, + ) + .props({ + /** + * #property + */ + jobsManager: types.maybe(JobsManager), + }) + .volatile(self => ({ + rpcManager: new RpcManager( + pluginManager, + self.jbrowse.configuration.rpc, + { + WebWorkerRpcDriver: { + makeWorkerInstance, + }, + MainThreadRpcDriver: {}, + }, + ), + })) +} + +export type RootModelType = ReturnType +export type RootModel = Instance diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts index 95aba1c4b1..d6a487a944 100644 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -1,8 +1,7 @@ -import { Instance, getParent, types } from 'mobx-state-tree' +import { Instance, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import RpcManager from '@jbrowse/core/rpc/RpcManager' import { Session as CoreSession } from '@jbrowse/product-core' diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 5a17db47fb..66ff862ad8 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -84,43 +84,43 @@ export default function sessionModelFactory( * #getter */ get textSearchManager(): TextSearchManager { - return getParent(self).textSearchManager + return self.root.textSearchManager }, /** * #getter */ get savedSessions() { - return getParent(self).savedSessions + return self.root.savedSessions }, /** * #getter */ get previousAutosaveId() { - return getParent(self).previousAutosaveId + return self.root.previousAutosaveId }, /** * #getter */ get savedSessionNames() { - return getParent(self).savedSessionNames + return self.root.savedSessionNames }, /** * #getter */ get history() { - return getParent(self).history + return self.root.history }, /** * #getter */ get menus() { - return getParent(self).menus + return self.root.menus }, /** * #getter */ get version() { - return getParent(self).version + return self.root.version }, /** @@ -210,62 +210,62 @@ export default function sessionModelFactory( * #action */ addSavedSession(sessionSnapshot: SnapshotIn) { - return getParent(self).addSavedSession(sessionSnapshot) + return self.root.addSavedSession(sessionSnapshot) }, /** * #action */ removeSavedSession(sessionSnapshot: any) { - return getParent(self).removeSavedSession(sessionSnapshot) + return self.root.removeSavedSession(sessionSnapshot) }, /** * #action */ renameCurrentSession(sessionName: string) { - return getParent(self).renameCurrentSession(sessionName) + return self.root.renameCurrentSession(sessionName) }, /** * #action */ duplicateCurrentSession() { - return getParent(self).duplicateCurrentSession() + return self.root.duplicateCurrentSession() }, /** * #action */ activateSession(sessionName: any) { - return getParent(self).activateSession(sessionName) + return self.root.activateSession(sessionName) }, /** * #action */ setDefaultSession() { - return getParent(self).setDefaultSession() + return self.root.setDefaultSession() }, /** * #action */ saveSessionToLocalStorage() { - return getParent(self).saveSessionToLocalStorage() + return self.root.saveSessionToLocalStorage() }, /** * #action */ loadAutosaveSession() { - return getParent(self).loadAutosaveSession() + return self.root.loadAutosaveSession() }, /** * #action */ setSession(sessionSnapshot: SnapshotIn) { - return getParent(self).setSession(sessionSnapshot) + return self.root.setSession(sessionSnapshot) }, } }) From bfa7beb437a230bcc8b5f933685754ff8175abaf Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Thu, 4 May 2023 10:23:18 -0700 Subject: [PATCH 25/44] wip --- packages/core/ui/AppToolbar.tsx | 2 +- packages/product-core/src/Session/Base.ts | 10 + .../product-core/src/Session/DialogQueue.ts | 18 +- .../Session/{Views.ts => MultipleViews.ts} | 0 .../product-core/src/Session/SessionTracks.ts | 76 ++++ packages/product-core/src/Session/Tracks.ts | 9 +- packages/product-core/src/Session/index.ts | 3 +- .../LinearGenomeView.test.tsx.snap | 4 +- .../jbrowse-desktop/src/sessionModel/index.ts | 2 +- .../package.json | 1 + .../src/createModel/createModel.ts | 3 + .../src/createModel/createSessionModel.ts | 371 +---------------- .../package.json | 1 + .../JBrowseLinearGenomeView.test.tsx.snap | 2 +- .../src/createModel/createModel.ts | 4 +- .../src/createModel/createSessionModel.ts | 392 +----------------- products/jbrowse-web/src/sessionModel/Base.ts | 7 +- .../jbrowse-web/src/sessionModel/index.ts | 214 ++++------ 18 files changed, 240 insertions(+), 879 deletions(-) rename packages/product-core/src/Session/{Views.ts => MultipleViews.ts} (100%) create mode 100644 packages/product-core/src/Session/SessionTracks.ts diff --git a/packages/core/ui/AppToolbar.tsx b/packages/core/ui/AppToolbar.tsx index 845431263e..75da08dbb9 100644 --- a/packages/core/ui/AppToolbar.tsx +++ b/packages/core/ui/AppToolbar.tsx @@ -61,7 +61,7 @@ const AppToolbar = observer(function ({ 'warning', ) } else { - session.renameCurrentSession(newName) + session.root.renameCurrentSession(newName) } } return ( diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index 4ba817194e..4e3dd4c0ee 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -21,6 +21,10 @@ export default function BaseSession< * #property */ name: types.string, + /** + * #property + */ + margin: 0, }) .volatile(() => ({ /** @@ -69,6 +73,12 @@ export default function BaseSession< get textSearchManager() { return this.root.textSearchManager }, + /** + * #getter + */ + get version() { + return this.root.version + }, })) .actions(self => ({ /** diff --git a/packages/product-core/src/Session/DialogQueue.ts b/packages/product-core/src/Session/DialogQueue.ts index 3f41c81807..1987acf250 100644 --- a/packages/product-core/src/Session/DialogQueue.ts +++ b/packages/product-core/src/Session/DialogQueue.ts @@ -14,9 +14,7 @@ export default function DialogQueue(pluginManager: PluginManager) { return types .model('DialogQueueSessionMixin', {}) .volatile(() => ({ - queueOfDialogs: observable.array( - [] as [DialogComponentType, Record][], - ), + queueOfDialogs: [] as [DialogComponentType, unknown][], })) .views(self => ({ /** @@ -41,18 +39,22 @@ export default function DialogQueue(pluginManager: PluginManager) { }, })) .actions(self => ({ + /** + * #action + */ + removeActiveDialog() { + self.queueOfDialogs = self.queueOfDialogs.slice(1) + }, /** * #action */ queueDialog( - callback: (doneCallback: () => void) => [DialogComponentType, any], + callback: (doneCallback: () => void) => [DialogComponentType, unknown], ): void { - // NOTE: this base implementation doesn't include the changes from #2469, - // hoping it's not needed anymore const [component, props] = callback(() => { - self.queueOfDialogs.shift() + this.removeActiveDialog() }) - self.queueOfDialogs.push([component, props]) + self.queueOfDialogs = [...self.queueOfDialogs, [component, props]] }, })) } diff --git a/packages/product-core/src/Session/Views.ts b/packages/product-core/src/Session/MultipleViews.ts similarity index 100% rename from packages/product-core/src/Session/Views.ts rename to packages/product-core/src/Session/MultipleViews.ts diff --git a/packages/product-core/src/Session/SessionTracks.ts b/packages/product-core/src/Session/SessionTracks.ts new file mode 100644 index 0000000000..9833e5c164 --- /dev/null +++ b/packages/product-core/src/Session/SessionTracks.ts @@ -0,0 +1,76 @@ +import { Instance, types } from 'mobx-state-tree' + +import PluginManager from '@jbrowse/core/PluginManager' +import { + AnyConfiguration, + AnyConfigurationModel, +} from '@jbrowse/core/configuration' +import Tracks from './Tracks' + +export default function SessionTracks(pluginManager: PluginManager) { + return Tracks(pluginManager) + .named('SessionTracksManagerSessionMixin') + .props({ + /** + * #property + */ + sessionTracks: types.array( + pluginManager.pluggableConfigSchemaType('track'), + ), + }) + .views(self => ({ + /** + * #getter + */ + get tracks(): AnyConfigurationModel[] { + return self.jbrowse.tracks + }, + })) + .actions(self => { + const super_addTrackConf = self.addTrackConf + const super_deletetrackConf = self.deleteTrackConf + return { + /** + * #action + */ + addTrackConf(trackConf: AnyConfiguration) { + if (self.adminMode) { + return super_addTrackConf(trackConf) + } + const { trackId, type } = trackConf as { + type: string + trackId: string + } + if (!type) { + throw new Error(`unknown track type ${type}`) + } + const track = self.sessionTracks.find(t => t.trackId === trackId) + if (track) { + return track + } + const length = self.sessionTracks.push(trackConf) + return self.sessionTracks[length - 1] + }, + + /** + * #action + */ + deleteTrackConf(trackConf: AnyConfigurationModel) { + // try to delete it in the main config if in admin mode + const found = super_deletetrackConf(trackConf) + if (found) { + return found + } + // if not found or not in admin mode, try to delete it in the sessionTracks + const { trackId } = trackConf + const idx = self.sessionTracks.findIndex(t => t.trackId === trackId) + if (idx === -1) { + return undefined + } + return self.sessionTracks.splice(idx, 1) + }, + } + }) +} + +export type TracksManager = Instance> diff --git a/packages/product-core/src/Session/Tracks.ts b/packages/product-core/src/Session/Tracks.ts index e7cf0a789c..9688312150 100644 --- a/packages/product-core/src/Session/Tracks.ts +++ b/packages/product-core/src/Session/Tracks.ts @@ -1,7 +1,10 @@ -import { Instance, getParent, types } from 'mobx-state-tree' +import { Instance, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { + AnyConfiguration, + AnyConfigurationModel, +} from '@jbrowse/core/configuration' import BaseSession from './Base' import ReferenceManagement from './ReferenceManagement' @@ -24,7 +27,7 @@ export default function Tracks(pluginManager: PluginManager) { /** * #action */ - addTrackConf(trackConf: any) { + addTrackConf(trackConf: AnyConfiguration) { return self.jbrowse.addTrackConf(trackConf) }, diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts index 1e4b81ae93..10b55c70c4 100644 --- a/packages/product-core/src/Session/index.ts +++ b/packages/product-core/src/Session/index.ts @@ -4,5 +4,6 @@ export { default as DrawerWidgets } from './DrawerWidgets' export { default as DialogQueue } from './DialogQueue' export { default as Themes } from './Themes' export { default as Tracks } from './Tracks' -export { default as Views } from './Views' +export { default as MultipleViews } from './MultipleViews' export { default as Base } from './Base' +export { default as SessionTracks } from './SessionTracks' diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap index 06f37dea25..79c012e959 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap @@ -157,7 +157,7 @@ exports[`renders one track, one region 1`] = ` />
@@ -731,7 +731,7 @@ exports[`renders two tracks, two regions 1`] = ` />
diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 0ac2fecd4f..03cf7b28dc 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -30,7 +30,7 @@ export default function sessionModelFactory( CoreSession.DialogQueue(pluginManager), CoreSession.Themes(pluginManager), CoreSession.Tracks(pluginManager), - CoreSession.Views(pluginManager), + CoreSession.MultipleViews(pluginManager), ), Base(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), diff --git a/products/jbrowse-react-circular-genome-view/package.json b/products/jbrowse-react-circular-genome-view/package.json index 2d48d3f2ea..a3b38ee0f3 100644 --- a/products/jbrowse-react-circular-genome-view/package.json +++ b/products/jbrowse-react-circular-genome-view/package.json @@ -50,6 +50,7 @@ "@jbrowse/plugin-sequence": "^2.5.0", "@jbrowse/plugin-variants": "^2.5.0", "@jbrowse/plugin-wiggle": "^2.5.0", + "@jbrowse/product-core": "^2.5.0", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.10.17", "mobx": "^6.6.0", diff --git a/products/jbrowse-react-circular-genome-view/src/createModel/createModel.ts b/products/jbrowse-react-circular-genome-view/src/createModel/createModel.ts index e27b69cf4f..e4bf4bf2d4 100644 --- a/products/jbrowse-react-circular-genome-view/src/createModel/createModel.ts +++ b/products/jbrowse-react-circular-genome-view/src/createModel/createModel.ts @@ -10,6 +10,7 @@ import { cast, getSnapshot, Instance, SnapshotIn, types } from 'mobx-state-tree' import corePlugins from '../corePlugins' import createConfigModel from './createConfigModel' import createSessionModel from './createSessionModel' +import { version } from '../version' export default function createModel(runtimePlugins: PluginConstructor[]) { const pluginManager = new PluginManager( @@ -33,6 +34,8 @@ export default function createModel(runtimePlugins: PluginConstructor[]) { }) .volatile(() => ({ error: undefined as Error | undefined, + adminMode: false, + version, })) .actions(self => ({ setSession(sessionSnapshot: SnapshotIn) { diff --git a/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts b/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts index 071eb8b088..d7ffcf5bd9 100644 --- a/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts +++ b/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts @@ -1,117 +1,48 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { - AbstractSessionModel, - TrackViewModel, - DialogComponentType, -} from '@jbrowse/core/util/types' -import { getContainingView } from '@jbrowse/core/util' +import { AbstractSessionModel } from '@jbrowse/core/util/types' import AboutDialog from '@jbrowse/core/ui/AboutDialog' -import { - getMembers, - getParent, - getSnapshot, - getType, - IAnyStateTreeNode, - isAlive, - isModelType, - isReferenceType, - types, - walk, - Instance, -} from 'mobx-state-tree' +import { getParent, types, Instance } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import { - readConfObject, - AnyConfigurationModel, -} from '@jbrowse/core/configuration' +import { readConfObject } from '@jbrowse/core/configuration' import InfoIcon from '@mui/icons-material/Info' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { ReferringNode } from '../types' -import { version } from '../version' + +import { Session as CoreSession } from '@jbrowse/product-core' /** * #stateModel JBrowseReactCGVSessionModel */ export default function sessionModelFactory(pluginManager: PluginManager) { const model = types - .model('ReactCircularGenomeViewSession', { - /** - * #property - */ - name: types.identifier, + .compose( + 'ReactCircularGenomeViewSession', + CoreSession.Base(pluginManager), + CoreSession.DrawerWidgets(pluginManager), + CoreSession.Connections(pluginManager), + CoreSession.DialogQueue(pluginManager), + CoreSession.Tracks(pluginManager), + CoreSession.ReferenceManagement(pluginManager), + ) + .props({ /** * #property */ view: pluginManager.getViewType('CircularView').stateModel, - /** - * #property - */ - widgets: types.map( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - ), - /** - * #property - */ - connectionInstances: types.array( - pluginManager.pluggableMstType('connection', 'stateModel'), - ), }) .volatile((/* self */) => ({ - /** - * this is the globally "selected" object. can be anything. - * code that wants to deal with this should examine it to see what - * kind of thing it is. - */ - selection: undefined, /** * this is the current "task" that is being performed in the UI. * this is usually an object of the form * `{ taskName: "configure", target: thing_being_configured }` */ task: undefined, - - queueOfDialogs: [] as [DialogComponentType, any][], })) .views(self => ({ /** * #getter */ - get DialogComponent() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[0] - } - return undefined - }, - /** - * #getter - */ - get DialogProps() { - if (self.queueOfDialogs.length) { - const firstInQueue = self.queueOfDialogs[0] - return firstInQueue && firstInQueue[1] - } - return undefined - }, - /** - * #getter - */ - get rpcManager() { - return getParent(self).rpcManager - }, - /** - * #getter - */ - get configuration() { - return getParent(self).config.configuration + get jbrowse() { + return self.root.config }, /** * #getter @@ -125,42 +56,18 @@ export default function sessionModelFactory(pluginManager: PluginManager) { get assemblyNames() { return [getParent(self).config.assemblyName] }, - /** - * #getter - */ - get tracks() { - return getParent(self).config.tracks - }, - /** - * #getter - */ - get aggregateTextSearchAdapters() { - return getParent(self).config.aggregateTextSearchAdapters - }, /** * #getter */ get connections() { return getParent(self).config.connections }, - /** - * #getter - */ - get adminMode() { - return false - }, /** * #getter */ get assemblyManager() { return getParent(self).assemblyManager }, - /** - * #getter - */ - get version() { - return version - }, /** * #getter */ @@ -171,166 +78,10 @@ export default function sessionModelFactory(pluginManager: PluginManager) { * #method */ renderProps() { - return { theme: readConfObject(this.configuration, 'theme') } - }, - - /** - * #getter - */ - get visibleWidget() { - if (isAlive(self)) { - // returns most recently added item in active widgets - return [...self.activeWidgets.values()][self.activeWidgets.size - 1] - } - return undefined - }, - /** - * #method - * See if any MST nodes currently have a types.reference to this object. - * @param object - object - * @returns An array where the first element is the node referring - * to the object and the second element is they property name the node is - * using to refer to the object - */ - getReferring(object: IAnyStateTreeNode) { - const refs: ReferringNode[] = [] - walk(getParent(self), node => { - if (isModelType(getType(node))) { - const members = getMembers(node) - Object.entries(members.properties).forEach(([key, value]) => { - // @ts-ignore - if (isReferenceType(value) && node[key] === object) { - refs.push({ node, key }) - } - }) - } - }) - return refs + return { theme: readConfObject(self.configuration, 'theme') } }, })) .actions(self => ({ - /** - * #action - */ - queueDialog( - callback: (doneCallback: () => void) => [DialogComponentType, any], - ): void { - const [component, props] = callback(() => { - this.removeActiveDialog() - }) - self.queueOfDialogs = [...self.queueOfDialogs, [component, props]] - }, - /** - * #action - */ - removeActiveDialog() { - self.queueOfDialogs = self.queueOfDialogs.slice(1) - }, - /** - * #action - */ - makeConnection( - configuration: AnyConfigurationModel, - initialSnapshot = {}, - ) { - const { type } = configuration - if (!type) { - throw new Error('track configuration has no `type` listed') - } - const name = readConfObject(configuration, 'name') - const connectionType = pluginManager.getConnectionType(type) - if (!connectionType) { - throw new Error(`unknown connection type ${type}`) - } - const connectionData = { - ...initialSnapshot, - name, - type, - configuration, - } - const length = self.connectionInstances.push(connectionData) - return self.connectionInstances[length - 1] - }, - - /** - * #action - */ - removeReferring( - referring: any, - track: any, - callbacks: Function[], - dereferenceTypeCount: Record, - ) { - referring.forEach(({ node }: ReferringNode) => { - let dereferenced = false - try { - // If a view is referring to the track config, remove the track - // from the view - const type = 'open track(s)' - const view = getContainingView(node) as TrackViewModel - callbacks.push(() => view.hideTrack(track.trackId)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } catch (err1) { - // ignore - } - if (this.hasWidget(node)) { - // If a configuration editor widget has the track config - // open, close the widget - const type = 'configuration editor widget(s)' - callbacks.push(() => this.hideWidget(node)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } - if (!dereferenced) { - throw new Error( - `Error when closing this connection, the following node is still referring to a track configuration: ${JSON.stringify( - getSnapshot(node), - )}`, - ) - } - }) - }, - - /** - * #action - */ - prepareToBreakConnection(configuration: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - connection.tracks.forEach((track: any) => { - const referring = self.getReferring(track) - this.removeReferring( - referring, - track, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - }) - const safelyBreakConnection = () => { - callbacksToDereferenceTrack.forEach(cb => cb()) - this.breakConnection(configuration) - } - return [safelyBreakConnection, dereferenceTypeCount] - }, - - /** - * #action - */ - breakConnection(configuration: AnyConfigurationModel) { - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - self.connectionInstances.remove(connection) - }, - /** * #action * replaces view in this case @@ -353,92 +104,6 @@ export default function sessionModelFactory(pluginManager: PluginManager) { * does nothing */ removeView() {}, - - /** - * #action - */ - addWidget( - typeName: string, - id: string, - initialState = {}, - conf?: unknown, - ) { - const typeDefinition = pluginManager.getElementType('widget', typeName) - if (!typeDefinition) { - throw new Error(`unknown widget type ${typeName}`) - } - const data = { - ...initialState, - id, - type: typeName, - configuration: conf || { type: typeName }, - } - self.widgets.set(id, data) - return self.widgets.get(id) - }, - - /** - * #action - */ - showWidget(widget: any) { - if (self.activeWidgets.has(widget.id)) { - self.activeWidgets.delete(widget.id) - } - self.activeWidgets.set(widget.id, widget) - }, - - /** - * #action - */ - hasWidget(widget: any) { - return self.activeWidgets.has(widget.id) - }, - - /** - * #action - */ - hideWidget(widget: any) { - self.activeWidgets.delete(widget.id) - }, - - /** - * #action - */ - hideAllWidgets() { - self.activeWidgets.clear() - }, - - /** - * #action - * set the global selection, i.e. the globally-selected object. - * can be a feature, a view, just about anything - * @param thing - - */ - setSelection(thing: any) { - self.selection = thing - }, - - /** - * #action - * clears the global selection - */ - clearSelection() { - self.selection = undefined - }, - - /** - * #action - */ - clearConnections() { - self.connectionInstances.length = 0 - }, - - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return getParent(self).renameCurrentSession(sessionName) - }, })) .views(self => ({ /** diff --git a/products/jbrowse-react-linear-genome-view/package.json b/products/jbrowse-react-linear-genome-view/package.json index a11b41dfcb..b5b916d6a7 100644 --- a/products/jbrowse-react-linear-genome-view/package.json +++ b/products/jbrowse-react-linear-genome-view/package.json @@ -65,6 +65,7 @@ "@jbrowse/plugin-trix": "^2.5.0", "@jbrowse/plugin-variants": "^2.5.0", "@jbrowse/plugin-wiggle": "^2.5.0", + "@jbrowse/product-core": "^2.5.0", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.10.17", "librpc-web-mod": "^1.1.5", diff --git a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap index 0fd291e2fc..fe55ab9a88 100644 --- a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap +++ b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap @@ -246,7 +246,7 @@ exports[` renders successfully 1`] = ` />
diff --git a/products/jbrowse-react-linear-genome-view/src/createModel/createModel.ts b/products/jbrowse-react-linear-genome-view/src/createModel/createModel.ts index d8085d31ac..e5c80619b6 100644 --- a/products/jbrowse-react-linear-genome-view/src/createModel/createModel.ts +++ b/products/jbrowse-react-linear-genome-view/src/createModel/createModel.ts @@ -10,6 +10,7 @@ import { cast, getSnapshot, Instance, SnapshotIn, types } from 'mobx-state-tree' import corePlugins from '../corePlugins' import createConfigModel from './createConfigModel' import createSessionModel from './createSessionModel' +import { version } from '../version' export default function createModel( runtimePlugins: PluginConstructor[], @@ -43,8 +44,9 @@ export default function createModel( MainThreadRpcDriver: {}, }), textSearchManager: new TextSearchManager(pluginManager), + adminMode: false, + version, })) - .actions(self => ({ setSession(sessionSnapshot: SnapshotIn) { self.session = cast(sessionSnapshot) diff --git a/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts b/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts index 87dd8e7fe2..ba1e431850 100644 --- a/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts +++ b/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts @@ -1,40 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // note: AboutDialog is imported statically instead of as a lazy component // due to vite failing to load it xref #2896 -import { - AbstractSessionModel, - TrackViewModel, - DialogComponentType, -} from '@jbrowse/core/util/types' +import { AbstractSessionModel } from '@jbrowse/core/util/types' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { getContainingView } from '@jbrowse/core/util' -import { - readConfObject, - AnyConfigurationModel, -} from '@jbrowse/core/configuration' -import { version } from '../version' -import { - cast, - getMembers, - getParent, - getSnapshot, - getType, - isAlive, - isModelType, - isReferenceType, - types, - walk, - Instance, - IAnyStateTreeNode, -} from 'mobx-state-tree' +import { readConfObject } from '@jbrowse/core/configuration' +import { cast, getParent, types, Instance } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import InfoIcon from '@mui/icons-material/Info' import AboutDialog from '@jbrowse/core/ui/AboutDialog' import { LinearGenomeViewStateModel } from '@jbrowse/plugin-linear-genome-view' +import { Session as CoreSession } from '@jbrowse/product-core' // locals -import { ReferringNode } from '../types' /** * #stateModel JBrowseReactLGVSessionModel @@ -42,40 +19,22 @@ import { ReferringNode } from '../types' */ export default function sessionModelFactory(pluginManager: PluginManager) { const model = types - .model('ReactLinearGenomeViewSession', { - /** - * #property - */ - name: types.identifier, - /** - * #property - */ - margin: 0, + .compose( + 'ReactLinearGenomeViewSession', + CoreSession.Base(pluginManager), + CoreSession.DrawerWidgets(pluginManager), + CoreSession.Connections(pluginManager), + CoreSession.DialogQueue(pluginManager), + CoreSession.Tracks(pluginManager), + CoreSession.ReferenceManagement(pluginManager), + CoreSession.SessionTracks(pluginManager), + ) + .props({ /** * #property */ view: pluginManager.getViewType('LinearGenomeView') .stateModel as LinearGenomeViewStateModel, - /** - * #property - */ - widgets: types.map( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - /** - * #property - */ - activeWidgets: types.map( - types.safeReference( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - ), - /** - * #property - */ - connectionInstances: types.array( - pluginManager.pluggableMstType('connection', 'stateModel'), - ), /** * #property */ @@ -83,57 +42,18 @@ export default function sessionModelFactory(pluginManager: PluginManager) { pluginManager.pluggableConfigSchemaType('track'), ), }) - .volatile((/* self */) => ({ - /** - * this is the globally "selected" object. can be anything. - * code that wants to deal with this should examine it to see what - * kind of thing it is. - */ - selection: undefined, - queueOfDialogs: [] as [DialogComponentType, any][], - })) .views(self => ({ /** * #getter */ - get disableAddTracks() { - return getParent(self).disableAddTracks - }, - /** - * #getter - */ - get DialogComponent() { - if (self.queueOfDialogs.length) { - return self.queueOfDialogs[0]?.[0] - } - return undefined + get jbrowse() { + return self.root.config }, /** * #getter */ - get DialogProps() { - if (self.queueOfDialogs.length) { - return self.queueOfDialogs[0]?.[1] - } - return undefined - }, - /** - * #getter - */ - get textSearchManager(): TextSearchManager { - return getParent(self).textSearchManager - }, - /** - * #getter - */ - get rpcManager() { - return getParent(self).rpcManager - }, - /** - * #getter - */ - get configuration() { - return getParent(self).config.configuration + get disableAddTracks() { + return getParent(self).disableAddTracks }, /** * #getter @@ -147,42 +67,18 @@ export default function sessionModelFactory(pluginManager: PluginManager) { get assemblyNames() { return [getParent(self).config.assemblyName] }, - /** - * #getter - */ - get tracks() { - return getParent(self).config.tracks - }, - /** - * #getter - */ - get aggregateTextSearchAdapters() { - return getParent(self).config.aggregateTextSearchAdapters - }, /** * #getter */ get connections() { return getParent(self).config.connections }, - /** - * #getter - */ - get adminMode() { - return false - }, /** * #getter */ get assemblyManager() { return getParent(self).assemblyManager }, - /** - * #getter - */ - get version() { - return version - }, /** * #getter */ @@ -193,179 +89,10 @@ export default function sessionModelFactory(pluginManager: PluginManager) { * #method */ renderProps() { - return { theme: readConfObject(this.configuration, 'theme') } - }, - /** - * #getter - */ - get visibleWidget() { - if (isAlive(self)) { - // returns most recently added item in active widgets - return [...self.activeWidgets.values()][self.activeWidgets.size - 1] - } - return undefined - }, - /** - * #method - * See if any MST nodes currently have a types.reference to this object. - * @param object - object - * @returns An array where the first element is the node referring - * to the object and the second element is they property name the node is - * using to refer to the object - */ - getReferring(object: IAnyStateTreeNode) { - const refs: ReferringNode[] = [] - walk(getParent(self), node => { - if (isModelType(getType(node))) { - const members = getMembers(node) - Object.entries(members.properties).forEach(([key, value]) => { - // @ts-ignore - if (isReferenceType(value) && node[key] === object) { - refs.push({ node, key }) - } - }) - } - }) - return refs + return { theme: readConfObject(self.configuration, 'theme') } }, })) .actions(self => ({ - /** - * #action - */ - addTrackConf(trackConf: AnyConfigurationModel) { - const { trackId, type } = trackConf - if (!type) { - throw new Error(`unknown track type ${type}`) - } - const track = self.sessionTracks.find((t: any) => t.trackId === trackId) - if (track) { - return track - } - const length = self.sessionTracks.push(trackConf) - return self.sessionTracks[length - 1] - }, - /** - * #action - */ - queueDialog( - callback: (doneCallback: () => void) => [DialogComponentType, any], - ): void { - const [component, props] = callback(() => { - this.removeActiveDialog() - }) - self.queueOfDialogs = [...self.queueOfDialogs, [component, props]] - }, - /** - * #action - */ - removeActiveDialog() { - self.queueOfDialogs = self.queueOfDialogs.slice(1) - }, - /** - * #action - */ - makeConnection( - configuration: AnyConfigurationModel, - initialSnapshot = {}, - ) { - const { type } = configuration - if (!type) { - throw new Error('track configuration has no `type` listed') - } - const name = readConfObject(configuration, 'name') - const connectionType = pluginManager.getConnectionType(type) - if (!connectionType) { - throw new Error(`unknown connection type ${type}`) - } - const connectionData = { - ...initialSnapshot, - name, - type, - configuration, - } - const length = self.connectionInstances.push(connectionData) - return self.connectionInstances[length - 1] - }, - /** - * #action - */ - - removeReferring( - referring: any, - track: any, - callbacks: Function[], - dereferenceTypeCount: Record, - ) { - referring.forEach(({ node }: ReferringNode) => { - let dereferenced = false - try { - // If a view is referring to the track config, remove the track - // from the view - const type = 'open track(s)' - const view = getContainingView(node) as TrackViewModel - callbacks.push(() => view.hideTrack(track.trackId)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } catch (err1) { - // ignore - } - if (this.hasWidget(node)) { - // If a configuration editor widget has the track config - // open, close the widget - const type = 'configuration editor widget(s)' - callbacks.push(() => this.hideWidget(node)) - dereferenced = true - if (!dereferenceTypeCount[type]) { - dereferenceTypeCount[type] = 0 - } - dereferenceTypeCount[type] += 1 - } - if (!dereferenced) { - throw new Error( - `Error when closing this connection, the following node is still referring to a track configuration: ${JSON.stringify( - getSnapshot(node), - )}`, - ) - } - }) - }, - - /** - * #action - */ - prepareToBreakConnection(configuration: AnyConfigurationModel) { - const callbacksToDereferenceTrack: Function[] = [] - const dereferenceTypeCount: Record = {} - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - connection.tracks.forEach((track: any) => { - const referring = self.getReferring(track) - this.removeReferring( - referring, - track, - callbacksToDereferenceTrack, - dereferenceTypeCount, - ) - }) - const safelyBreakConnection = () => { - callbacksToDereferenceTrack.forEach(cb => cb()) - this.breakConnection(configuration) - } - return [safelyBreakConnection, dereferenceTypeCount] - }, - - /** - * #action - */ - breakConnection(configuration: AnyConfigurationModel) { - const name = readConfObject(configuration, 'name') - const connection = self.connectionInstances.find(c => c.name === name) - self.connectionInstances.remove(connection) - }, /** * #action */ @@ -383,85 +110,6 @@ export default function sessionModelFactory(pluginManager: PluginManager) { }, removeView() {}, - /** - * #action - */ - addWidget( - typeName: string, - id: string, - initialState = {}, - conf?: unknown, - ) { - const typeDefinition = pluginManager.getElementType('widget', typeName) - if (!typeDefinition) { - throw new Error(`unknown widget type ${typeName}`) - } - const data = { - ...initialState, - id, - type: typeName, - configuration: conf || { type: typeName }, - } - self.widgets.set(id, data) - return self.widgets.get(id) - }, - /** - * #action - */ - showWidget(widget: any) { - if (self.activeWidgets.has(widget.id)) { - self.activeWidgets.delete(widget.id) - } - self.activeWidgets.set(widget.id, widget) - }, - /** - * #action - */ - hasWidget(widget: any) { - return self.activeWidgets.has(widget.id) - }, - /** - * #action - */ - hideWidget(widget: any) { - self.activeWidgets.delete(widget.id) - }, - /** - * #action - */ - hideAllWidgets() { - self.activeWidgets.clear() - }, - - /** - * #action - * set the global selection, i.e. the globally-selected object. - * can be a feature, a view, just about anything - * @param thing - - */ - setSelection(thing: any) { - self.selection = thing - }, - - /** - * #action - * clears the global selection - */ - clearSelection() { - self.selection = undefined - }, - /** - * #action - */ - clearConnections() { - self.connectionInstances.length = 0 - }, - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return getParent(self).renameCurrentSession(sessionName) - }, })) .views(self => ({ /** diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts index d6a487a944..d7be69e988 100644 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ b/products/jbrowse-web/src/sessionModel/Base.ts @@ -12,12 +12,7 @@ export function BaseSession(pluginManager: PluginManager) { * #property */ margin: 0, - /** - * #property - */ - sessionTracks: types.array( - pluginManager.pluggableConfigSchemaType('track'), - ), + /** * #property */ diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 66ff862ad8..28c08521a1 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -54,8 +54,8 @@ export default function sessionModelFactory( CoreSession.DrawerWidgets(pluginManager), CoreSession.DialogQueue(pluginManager), CoreSession.Themes(pluginManager), - CoreSession.Views(pluginManager), - CoreSession.Tracks(pluginManager), + CoreSession.MultipleViews(pluginManager), + CoreSession.SessionTracks(pluginManager), BaseSession(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), SessionConnections(pluginManager), @@ -132,143 +132,97 @@ export default function sessionModelFactory( } }, })) - .actions(self => { - const super_addTrackConf = self.addTrackConf - const super_deletetrackConf = self.deleteTrackConf - return { - /** - * #action - */ - addSessionPlugin(plugin: JBrowsePlugin) { - if (self.sessionPlugins.some(p => p.name === plugin.name)) { - throw new Error('session plugin cannot be installed twice') - } - self.sessionPlugins.push(plugin) - getRoot(self).setPluginsUpdated(true) - }, - - /** - * #action - */ - removeSessionPlugin(pluginDefinition: PluginDefinition) { - self.sessionPlugins = cast( - self.sessionPlugins.filter( - plugin => - plugin.url !== pluginDefinition.url || - plugin.umdUrl !== pluginDefinition.umdUrl || - plugin.cjsUrl !== pluginDefinition.cjsUrl || - plugin.esmUrl !== pluginDefinition.esmUrl, - ), - ) - const rootModel = getParent(self) - rootModel.setPluginsUpdated(true) - }, - - /** - * #action - */ - addTrackConf(trackConf: AnyConfiguration) { - if (self.adminMode) { - return super_addTrackConf(trackConf) - } - const { trackId, type } = trackConf as { - type: string - trackId: string - } - if (!type) { - throw new Error(`unknown track type ${type}`) - } - const track = self.sessionTracks.find( - (t: any) => t.trackId === trackId, - ) - if (track) { - return track - } - const length = self.sessionTracks.push(trackConf) - return self.sessionTracks[length - 1] - }, + .actions(self => ({ + /** + * #action + */ + addSessionPlugin(plugin: JBrowsePlugin) { + if (self.sessionPlugins.some(p => p.name === plugin.name)) { + throw new Error('session plugin cannot be installed twice') + } + self.sessionPlugins.push(plugin) + getRoot(self).setPluginsUpdated(true) + }, - /** - * #action - */ - deleteTrackConf(trackConf: AnyConfigurationModel) { - // try to delete it in the main config if in admin mode - const found = super_deletetrackConf(trackConf) - if (found) { - return found - } - // if not found or not in admin mode, try to delete it in the sessionTracks - const { trackId } = trackConf - const idx = self.sessionTracks.findIndex(t => t.trackId === trackId) - if (idx === -1) { - return undefined - } - return self.sessionTracks.splice(idx, 1) - }, + /** + * #action + */ + removeSessionPlugin(pluginDefinition: PluginDefinition) { + self.sessionPlugins = cast( + self.sessionPlugins.filter( + plugin => + plugin.url !== pluginDefinition.url || + plugin.umdUrl !== pluginDefinition.umdUrl || + plugin.cjsUrl !== pluginDefinition.cjsUrl || + plugin.esmUrl !== pluginDefinition.esmUrl, + ), + ) + const rootModel = getParent(self) + rootModel.setPluginsUpdated(true) + }, - /** - * #action - */ - addSavedSession(sessionSnapshot: SnapshotIn) { - return self.root.addSavedSession(sessionSnapshot) - }, + /** + * #action + */ + addSavedSession(sessionSnapshot: SnapshotIn) { + return self.root.addSavedSession(sessionSnapshot) + }, - /** - * #action - */ - removeSavedSession(sessionSnapshot: any) { - return self.root.removeSavedSession(sessionSnapshot) - }, + /** + * #action + */ + removeSavedSession(sessionSnapshot: any) { + return self.root.removeSavedSession(sessionSnapshot) + }, - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return self.root.renameCurrentSession(sessionName) - }, + /** + * #action + */ + renameCurrentSession(sessionName: string) { + return self.root.renameCurrentSession(sessionName) + }, - /** - * #action - */ - duplicateCurrentSession() { - return self.root.duplicateCurrentSession() - }, - /** - * #action - */ - activateSession(sessionName: any) { - return self.root.activateSession(sessionName) - }, + /** + * #action + */ + duplicateCurrentSession() { + return self.root.duplicateCurrentSession() + }, + /** + * #action + */ + activateSession(sessionName: any) { + return self.root.activateSession(sessionName) + }, - /** - * #action - */ - setDefaultSession() { - return self.root.setDefaultSession() - }, + /** + * #action + */ + setDefaultSession() { + return self.root.setDefaultSession() + }, - /** - * #action - */ - saveSessionToLocalStorage() { - return self.root.saveSessionToLocalStorage() - }, + /** + * #action + */ + saveSessionToLocalStorage() { + return self.root.saveSessionToLocalStorage() + }, - /** - * #action - */ - loadAutosaveSession() { - return self.root.loadAutosaveSession() - }, + /** + * #action + */ + loadAutosaveSession() { + return self.root.loadAutosaveSession() + }, - /** - * #action - */ - setSession(sessionSnapshot: SnapshotIn) { - return self.root.setSession(sessionSnapshot) - }, - } - }) + /** + * #action + */ + setSession(sessionSnapshot: SnapshotIn) { + return self.root.setSession(sessionSnapshot) + }, + })) .actions(self => ({ /** * #action From a7905846b79d93ffea304229ce5485a2092c565e Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Thu, 4 May 2023 10:55:51 -0700 Subject: [PATCH 26/44] remove session management from desktop session model --- .../src/sessionModel/SessionManagement.ts | 71 ------------------- .../jbrowse-desktop/src/sessionModel/index.ts | 1 - 2 files changed, 72 deletions(-) delete mode 100644 products/jbrowse-desktop/src/sessionModel/SessionManagement.ts diff --git a/products/jbrowse-desktop/src/sessionModel/SessionManagement.ts b/products/jbrowse-desktop/src/sessionModel/SessionManagement.ts deleted file mode 100644 index eba94cf614..0000000000 --- a/products/jbrowse-desktop/src/sessionModel/SessionManagement.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { SnapshotIn } from 'mobx-state-tree' - -import PluginManager from '@jbrowse/core/PluginManager' -import { Base } from '@jbrowse/product-core/src/Session' - -export default function SessionManagement(pluginManager: PluginManager) { - return Base(pluginManager) - .props({}) - .views(self => ({ - /** - * #getter - */ - get savedSessions() { - return self.jbrowse.savedSessions - }, - /** - * #getter - */ - get savedSessionNames() { - return self.jbrowse.savedSessionNames - }, - /** - * #action - */ - addSavedSession(sessionSnapshot: SnapshotIn) { - return self.root.addSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - removeSavedSession(sessionSnapshot: any) { - return self.root.removeSavedSession(sessionSnapshot) - }, - - /** - * #action - */ - renameCurrentSession(sessionName: string) { - return self.root.renameCurrentSession(sessionName) - }, - - /** - * #action - */ - duplicateCurrentSession() { - return self.root.duplicateCurrentSession() - }, - - /** - * #action - */ - activateSession(sessionName: any) { - return self.root.activateSession(sessionName) - }, - - /** - * #action - */ - setDefaultSession() { - return self.root.setDefaultSession() - }, - - /** - * #action - */ - setSession(sessionSnapshot: SnapshotIn) { - return self.root.setSession(sessionSnapshot) - }, - })) -} diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 03cf7b28dc..a039ab1b17 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -35,7 +35,6 @@ export default function sessionModelFactory( Base(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), TrackMenu(pluginManager), - SessionManagement(pluginManager), ) .views(self => ({ /** From 433e7009a7c1528bc8579476b3646ea35776405f Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Thu, 4 May 2023 10:56:24 -0700 Subject: [PATCH 27/44] fixup --- products/jbrowse-desktop/src/sessionModel/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index a039ab1b17..86d166cd06 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -10,7 +10,6 @@ import Base from './Base' import Assemblies from './Assemblies' import TrackMenu from './TrackMenu' import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' -import SessionManagement from './SessionManagement' /** * #stateModel JBrowseDesktopSessionModel From 746f06d72175a37b2164f85d26b2ff0a5a96f58a Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Fri, 5 May 2023 13:47:26 -0700 Subject: [PATCH 28/44] wip --- packages/core/ui/AppToolbar.tsx | 2 +- packages/product-core/src/RootModel/Base.ts | 30 +- .../src/RootModel/InternetAccounts.ts | 11 +- packages/product-core/src/RootModel/index.ts | 4 +- packages/product-core/src/Session/Base.ts | 7 +- .../product-core/src/Session/Connections.ts | 2 +- .../product-core/src/Session/SessionTracks.ts | 3 +- packages/product-core/src/index.ts | 2 +- products/jbrowse-desktop/src/JBrowse.tsx | 6 +- .../src/rootModel/HistoryManagement.ts | 6 +- .../jbrowse-desktop/src/rootModel/Menus.ts | 8 +- .../jbrowse-desktop/src/rootModel/Sessions.ts | 25 +- .../jbrowse-desktop/src/rootModel/index.ts | 21 +- .../src/sessionModel/TrackMenu.ts | 4 +- .../jbrowse-desktop/src/sessionModel/index.ts | 12 - .../__snapshots__/index.test.js.snap} | 0 .../index.test.js} | 6 +- .../src/{rootModel.ts => rootModel/index.ts} | 581 +++++++----------- .../src/sessionModel/Assemblies.ts | 168 ++--- products/jbrowse-web/src/sessionModel/Base.ts | 40 -- .../src/sessionModel/SessionConnections.ts | 7 +- .../jbrowse-web/src/sessionModel/index.ts | 34 +- 22 files changed, 430 insertions(+), 549 deletions(-) rename products/jbrowse-web/src/{__snapshots__/rootModel.test.js.snap => rootModel/__snapshots__/index.test.js.snap} (100%) rename products/jbrowse-web/src/{rootModel.test.js => rootModel/index.test.js} (97%) rename products/jbrowse-web/src/{rootModel.ts => rootModel/index.ts} (59%) delete mode 100644 products/jbrowse-web/src/sessionModel/Base.ts diff --git a/packages/core/ui/AppToolbar.tsx b/packages/core/ui/AppToolbar.tsx index 75da08dbb9..845431263e 100644 --- a/packages/core/ui/AppToolbar.tsx +++ b/packages/core/ui/AppToolbar.tsx @@ -61,7 +61,7 @@ const AppToolbar = observer(function ({ 'warning', ) } else { - session.root.renameCurrentSession(newName) + session.renameCurrentSession(newName) } } return ( diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts index 922ad799fe..a6ab37424d 100644 --- a/packages/product-core/src/RootModel/Base.ts +++ b/packages/product-core/src/RootModel/Base.ts @@ -3,13 +3,20 @@ import assemblyManagerFactory, { BaseAssemblyConfigSchema, } from '@jbrowse/core/assemblyManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' -import { IAnyType, SnapshotIn, cast, getSnapshot, types } from 'mobx-state-tree' +import { + IAnyType, + Instance, + SnapshotIn, + cast, + getSnapshot, + types, +} from 'mobx-state-tree' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' /** * factory function for the Base-level root model shared by all products */ -export function BaseRoot< +export default function BaseRootModelTypeF< JBROWSE_MODEL_TYPE extends IAnyType, SESSION_MODEL_TYPE extends IAnyType, >( @@ -44,9 +51,9 @@ export function BaseRoot< /** * #property */ - assemblyManager: assemblyManagerFactory( - assemblyConfigSchema, - pluginManager, + assemblyManager: types.optional( + assemblyManagerFactory(assemblyConfigSchema, pluginManager), + {}, ), }) .volatile(self => ({ @@ -63,13 +70,9 @@ export function BaseRoot< * Boolean indicating whether the session is in admin mode or not */ adminMode: true, - isAssemblyEditing: false, error: undefined as unknown, textSearchManager: new TextSearchManager(pluginManager), - openNewSessionCallback: async (_path: string) => { - console.error('openNewSessionCallback unimplemented') - }, pluginManager, })) .actions(self => ({ @@ -106,12 +109,6 @@ export function BaseRoot< this.setSession({ ...getSnapshot(self.session), name: newName }) } }, - /** - * #action - */ - setOpenNewSessionCallback(cb: (arg: string) => Promise) { - self.openNewSessionCallback = cb - }, /** * #action */ @@ -121,4 +118,5 @@ export function BaseRoot< })) } -export type BaseRootModelType = ReturnType +export type BaseRootModelType = ReturnType +export type BaseRootModel = Instance diff --git a/packages/product-core/src/RootModel/InternetAccounts.ts b/packages/product-core/src/RootModel/InternetAccounts.ts index 4f40d108bf..a54c9079b1 100644 --- a/packages/product-core/src/RootModel/InternetAccounts.ts +++ b/packages/product-core/src/RootModel/InternetAccounts.ts @@ -5,7 +5,7 @@ import { autorun } from 'mobx' import { Instance, addDisposer, types } from 'mobx-state-tree' import { BaseRootModelType } from './Base' -export function InternetAccounts(pluginManager: PluginManager) { +export default function InternetAccountsF(pluginManager: PluginManager) { return types .model({ /** @@ -116,7 +116,8 @@ export function InternetAccounts(pluginManager: PluginManager) { addDisposer( self, autorun(() => { - const { jbrowse } = self as typeof self & Instance + const { jbrowse } = self as typeof self & + Instance jbrowse.internetAccounts.forEach(account => { self.initializeInternetAccount(account) }) @@ -127,4 +128,8 @@ export function InternetAccounts(pluginManager: PluginManager) { }) } -export type RootModelWithInternetAccounts = ReturnType +export type RootModelWithInternetAccountsType = ReturnType< + typeof InternetAccountsF +> +export type RootModelWithInternetAccounts = + Instance diff --git a/packages/product-core/src/RootModel/index.ts b/packages/product-core/src/RootModel/index.ts index c10fb9da1e..4a252faadc 100644 --- a/packages/product-core/src/RootModel/index.ts +++ b/packages/product-core/src/RootModel/index.ts @@ -1,2 +1,2 @@ -export * from './Base' -export * from './InternetAccounts' +export { default as BaseRootModel } from './Base' +export { default as InternetAccounts } from './InternetAccounts' diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index 4e3dd4c0ee..854e0356c1 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -2,12 +2,12 @@ import shortid from 'shortid' import type PluginManager from '@jbrowse/core/PluginManager' import { Instance, getParent, types } from 'mobx-state-tree' -import { BaseRootModelType } from '../RootModel' +import type { BaseRootModelType } from '../RootModel/Base' import { AnyConfigurationSchemaType } from '@jbrowse/core/configuration' import { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager' /** base session shared by **all** JBrowse products. Be careful what you include here, everything will use it. */ -export default function BaseSession< +export default function BaseSessionFactory< ROOT_MODEL_TYPE extends BaseRootModelType = BaseRootModelType, JB_CONFIG_SCHEMA extends AnyConfigurationSchemaType = AnyConfigurationSchemaType, >(pluginManager: PluginManager) { @@ -101,4 +101,5 @@ export default function BaseSession< })) } -export type BaseSessionType = ReturnType +export type BaseSessionType = ReturnType +export type BaseSession = Instance diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts index ea41ccfce0..c3701688c9 100644 --- a/packages/product-core/src/Session/Connections.ts +++ b/packages/product-core/src/Session/Connections.ts @@ -7,7 +7,7 @@ import { } from '@jbrowse/core/configuration' import { Instance, types } from 'mobx-state-tree' import type { SessionWithReferenceManagement } from './ReferenceManagement' -import { BaseRootModelType } from '../RootModel' +import type { BaseRootModelType } from '../RootModel/Base' import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' import { BaseConnectionModel } from '@jbrowse/core/pluggableElementTypes/models/BaseConnectionModelFactory' diff --git a/packages/product-core/src/Session/SessionTracks.ts b/packages/product-core/src/Session/SessionTracks.ts index 9833e5c164..659345a759 100644 --- a/packages/product-core/src/Session/SessionTracks.ts +++ b/packages/product-core/src/Session/SessionTracks.ts @@ -73,4 +73,5 @@ export default function SessionTracks(pluginManager: PluginManager) { }) } -export type TracksManager = Instance> +export type SessionWithSessionTracksType = ReturnType +export type SessionWithSessionTracks = Instance diff --git a/packages/product-core/src/index.ts b/packages/product-core/src/index.ts index a99dd78d20..1714dac9c9 100644 --- a/packages/product-core/src/index.ts +++ b/packages/product-core/src/index.ts @@ -1,2 +1,2 @@ -export * from './RootModel' +export * as RootModel from './RootModel' export * as Session from './Session' diff --git a/products/jbrowse-desktop/src/JBrowse.tsx b/products/jbrowse-desktop/src/JBrowse.tsx index 9c84b24f71..6d6f97fd87 100644 --- a/products/jbrowse-desktop/src/JBrowse.tsx +++ b/products/jbrowse-desktop/src/JBrowse.tsx @@ -9,12 +9,12 @@ import PluginManager from '@jbrowse/core/PluginManager' import { AssemblyManager } from '@jbrowse/plugin-data-management' // locals -import { RootModel } from './rootModel' +import { DesktopRootModel } from './rootModel' const JBrowseNonNullRoot = observer(function ({ rootModel, }: { - rootModel: RootModel + rootModel: DesktopRootModel }) { const { session, error, isAssemblyEditing, setAssemblyEditing } = rootModel @@ -54,6 +54,6 @@ export default observer(function ({ }) { const { rootModel } = pluginManager return rootModel ? ( - + ) : null }) diff --git a/products/jbrowse-desktop/src/rootModel/HistoryManagement.ts b/products/jbrowse-desktop/src/rootModel/HistoryManagement.ts index 8ed4f6c2f1..b4189769db 100644 --- a/products/jbrowse-desktop/src/rootModel/HistoryManagement.ts +++ b/products/jbrowse-desktop/src/rootModel/HistoryManagement.ts @@ -1,7 +1,7 @@ import TimeTraveller from '@jbrowse/core/util/TimeTraveller' -import { BaseRootModelType } from '@jbrowse/product-core' +import type { BaseRootModel } from '@jbrowse/product-core/src/RootModel/Base' import { autorun } from 'mobx' -import { Instance, addDisposer, types } from 'mobx-state-tree' +import { addDisposer, types } from 'mobx-state-tree' export const HistoryManagement = types .model({ @@ -36,7 +36,7 @@ export const HistoryManagement = types addDisposer( self, autorun(() => { - const { session } = self as typeof self & Instance + const { session } = self as typeof self & BaseRootModel if (session) { // we use a specific initialization routine after session is // created to get it to start tracking itself sort of related diff --git a/products/jbrowse-desktop/src/rootModel/Menus.ts b/products/jbrowse-desktop/src/rootModel/Menus.ts index 4ff241befb..2bea2fe4e8 100644 --- a/products/jbrowse-desktop/src/rootModel/Menus.ts +++ b/products/jbrowse-desktop/src/rootModel/Menus.ts @@ -1,5 +1,5 @@ import PluginManager from '@jbrowse/core/PluginManager' -import { Instance, types } from 'mobx-state-tree' +import { types } from 'mobx-state-tree' import { lazy } from 'react' // icons @@ -19,7 +19,7 @@ import type { AnyConfigurationModel } from '@jbrowse/core/configuration' import OpenSequenceDialog from '../OpenSequenceDialog' import type { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' import { getSaveSession } from './Sessions' -import { RootModel, RootModelType } from '.' +import { DesktopRootModel } from '.' const PreferencesDialog = lazy(() => import('../PreferencesDialog')) const { ipcRenderer } = window.require('electron') @@ -33,7 +33,7 @@ export default function Menus(pluginManager: PluginManager) { return types .model({}) .volatile(s => { - const self = s as RootModel + const self = s as DesktopRootModel return { menus: [ { @@ -242,7 +242,7 @@ export default function Menus(pluginManager: PluginManager) { self.menus = newMenus }, async setPluginsUpdated() { - const root = self as Instance + const root = self as DesktopRootModel if (root.session) { await root.saveSession(getSaveSession(root)) } diff --git a/products/jbrowse-desktop/src/rootModel/Sessions.ts b/products/jbrowse-desktop/src/rootModel/Sessions.ts index 5b8d27b0ad..21ffc41057 100644 --- a/products/jbrowse-desktop/src/rootModel/Sessions.ts +++ b/products/jbrowse-desktop/src/rootModel/Sessions.ts @@ -1,20 +1,15 @@ import PluginManager from '@jbrowse/core/PluginManager' -import { BaseRootModelType } from '@jbrowse/product-core' import { autorun } from 'mobx' -import { - Instance, - SnapshotIn, - addDisposer, - getSnapshot, - types, -} from 'mobx-state-tree' -import { BaseSessionModel } from '../sessionModel/Base' +import { SnapshotIn, addDisposer, getSnapshot, types } from 'mobx-state-tree' +import type { BaseSessionModel } from '../sessionModel/Base' +import type { BaseRootModel } from '@jbrowse/product-core/src/RootModel/Base' const { ipcRenderer } = window.require('electron') -export function getSaveSession(model: Instance) { +export function getSaveSession(model: BaseRootModel) { + const snap = getSnapshot(model.jbrowse) return { - ...getSnapshot(model.jbrowse), + ...(snap as Record), defaultSession: model.session ? getSnapshot(model.session) : {}, } } @@ -32,7 +27,7 @@ export default function SessionManagement(pluginManager: PluginManager) { sessionPath: types.optional(types.string, ''), }) .actions(s => { - const self = s as typeof s & Instance + const self = s as typeof s & BaseRootModel return { /** * #action @@ -71,7 +66,7 @@ export default function SessionManagement(pluginManager: PluginManager) { } }) .actions(s => { - const self = s as typeof s & Instance + const self = s as typeof s & BaseRootModel return { afterCreate() { addDisposer( @@ -80,9 +75,7 @@ export default function SessionManagement(pluginManager: PluginManager) { async () => { if (self.session) { try { - await self.saveSession( - getSaveSession(self as Instance), - ) + await self.saveSession(getSaveSession(self)) } catch (e) { console.error(e) } diff --git a/products/jbrowse-desktop/src/rootModel/index.ts b/products/jbrowse-desktop/src/rootModel/index.ts index a1ae6c8541..9420d789c4 100644 --- a/products/jbrowse-desktop/src/rootModel/index.ts +++ b/products/jbrowse-desktop/src/rootModel/index.ts @@ -4,7 +4,7 @@ import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyC import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' -import { BaseRoot, InternetAccounts } from '@jbrowse/product-core' +import { RootModel } from '@jbrowse/product-core' // locals import sessionModelFactory from '../sessionModel' @@ -26,13 +26,13 @@ export default function rootModelFactory(pluginManager: PluginManager) { return types .compose( 'JBrowseDesktopRootModel', - BaseRoot( + RootModel.BaseRootModel( pluginManager, JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), Session, assemblyConfigSchema, ), - InternetAccounts(pluginManager), + RootModel.InternetAccounts(pluginManager), Menus(pluginManager), SessionManagement(pluginManager), HistoryManagement, @@ -54,8 +54,19 @@ export default function rootModelFactory(pluginManager: PluginManager) { MainThreadRpcDriver: {}, }, ), + openNewSessionCallback: async (_path: string) => { + console.error('openNewSessionCallback unimplemented') + }, + })) + .actions(self => ({ + /** + * #action + */ + setOpenNewSessionCallback(cb: (arg: string) => Promise) { + self.openNewSessionCallback = cb + }, })) } -export type RootModelType = ReturnType -export type RootModel = Instance +export type DesktopRootModelType = ReturnType +export type DesktopRootModel = Instance diff --git a/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts index 5aefc0bd86..b038f85f8c 100644 --- a/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts +++ b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts @@ -14,7 +14,7 @@ import { lazy } from 'react' import type { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' import type { TracksManager } from '@jbrowse/product-core/src/Session/Tracks' import type { DrawerWidgetManager } from '@jbrowse/product-core/src/Session/DrawerWidgets' -import { RootModel } from '../rootModel' +import { DesktopRootModel } from '../rootModel' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) @@ -69,7 +69,7 @@ export default function TrackMenu(pluginManager: PluginManager) { label: trackSnapshot.textSearching ? 'Re-index track' : 'Index track', disabled: !supportedIndexingAdapters(trackSnapshot.adapter.type), onClick: () => { - const rootModel = getParent(self) + const rootModel = getParent(self) const { jobsManager } = rootModel const { trackId, assemblyNames, textSearching, name } = trackSnapshot diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 86d166cd06..3e49d8047c 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -36,18 +36,6 @@ export default function sessionModelFactory( TrackMenu(pluginManager), ) .views(self => ({ - /** - * #getter - */ - get history() { - return self.root.history - }, - /** - * #getter - */ - get menus() { - return self.root.menus - }, /** * #method */ diff --git a/products/jbrowse-web/src/__snapshots__/rootModel.test.js.snap b/products/jbrowse-web/src/rootModel/__snapshots__/index.test.js.snap similarity index 100% rename from products/jbrowse-web/src/__snapshots__/rootModel.test.js.snap rename to products/jbrowse-web/src/rootModel/__snapshots__/index.test.js.snap diff --git a/products/jbrowse-web/src/rootModel.test.js b/products/jbrowse-web/src/rootModel/index.test.js similarity index 97% rename from products/jbrowse-web/src/rootModel.test.js rename to products/jbrowse-web/src/rootModel/index.test.js index 0a747d8b43..1ff752027b 100644 --- a/products/jbrowse-web/src/rootModel.test.js +++ b/products/jbrowse-web/src/rootModel/index.test.js @@ -1,9 +1,9 @@ // we use mainthread rpc so we mock the makeWorkerInstance to an empty file import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' -import corePlugins from './corePlugins' -import rootModelFactory from './rootModel' -jest.mock('./makeWorkerInstance', () => () => {}) +import corePlugins from '../corePlugins' +import rootModelFactory from '.' +jest.mock('../makeWorkerInstance', () => () => {}) describe('Root MST model', () => { let rootModel diff --git a/products/jbrowse-web/src/rootModel.ts b/products/jbrowse-web/src/rootModel/index.ts similarity index 59% rename from products/jbrowse-web/src/rootModel.ts rename to products/jbrowse-web/src/rootModel/index.ts index 5f03da1711..cbbe89530c 100644 --- a/products/jbrowse-web/src/rootModel.ts +++ b/products/jbrowse-web/src/rootModel/index.ts @@ -11,13 +11,12 @@ import { import { saveAs } from 'file-saver' import { observable, autorun } from 'mobx' -import assemblyManagerFactory from '@jbrowse/core/assemblyManager' import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import TimeTraveller from '@jbrowse/core/util/TimeTraveller' -import { UriLocation } from '@jbrowse/core/util/types' +import { isModelWithAfterCreate } from '@jbrowse/core/util' import { AbstractSessionModel, SessionWithWidgets } from '@jbrowse/core/util' import { MenuItem } from '@jbrowse/core/ui' @@ -37,14 +36,14 @@ import RedoIcon from '@mui/icons-material/Redo' import { Cable } from '@jbrowse/core/ui/Icons' // other -import makeWorkerInstance from './makeWorkerInstance' -import corePlugins from './corePlugins' -import jbrowseWebFactory from './jbrowseModel' -import sessionModelFactory from './sessionModel' -import { filterSessionInPlace } from './util' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import makeWorkerInstance from '../makeWorkerInstance' +import corePlugins from '../corePlugins' +import jbrowseWebFactory from '../jbrowseModel' +import sessionModelFactory from '../sessionModel' +import { filterSessionInPlace } from '../util' +import { RootModel as CoreRootModel } from '@jbrowse/product-core' -const PreferencesDialog = lazy(() => import('./PreferencesDialog')) +const PreferencesDialog = lazy(() => import('../PreferencesDialog')) interface Menu { label: string @@ -62,41 +61,21 @@ export default function RootModel( ) { const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) - const AssemblyManager = assemblyManagerFactory( - assemblyConfigSchema, - pluginManager, - ) return types - .model('Root', { - /** - * #property - * `jbrowse` is a mapping of the config.json into the in-memory state tree - */ - jbrowse: jbrowseWebFactory(pluginManager, Session, assemblyConfigSchema), + .compose( + CoreRootModel.BaseRootModel( + pluginManager, + jbrowseWebFactory(pluginManager, Session, assemblyConfigSchema), + Session, + assemblyConfigSchema, + ), + CoreRootModel.InternetAccounts(pluginManager), + ) + .props({ /** * #property */ configPath: types.maybe(types.string), - /** - * #property - * `session` encompasses the currently active state of the app, including - * views open, tracks open in those views, etc. - */ - session: types.maybe(Session), - /** - * #property - */ - assemblyManager: types.optional(AssemblyManager, {}), - /** - * #property - */ - version: types.maybe(types.string), - /** - * #property - */ - internetAccounts: types.array( - pluginManager.pluggableMstType('internet account', 'stateModel'), - ), /** * #property * used for undo/redo @@ -164,324 +143,242 @@ export default function RootModel( }, })) - .actions(self => ({ - afterCreate() { - document.addEventListener('keydown', e => { - const cm = e.ctrlKey || e.metaKey - if ( - self.history.canRedo && - // ctrl+shift+z or cmd+shift+z - ((cm && e.shiftKey && e.code === 'KeyZ') || - // ctrl+y - (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) - ) { - self.history.redo() + .actions(self => { + const super_afterCreate = isModelWithAfterCreate(self) + ? self.afterCreate + : undefined + return { + afterCreate() { + if (super_afterCreate) { + super_afterCreate() } - if ( - self.history.canUndo && // ctrl+z or cmd+z - cm && - !e.shiftKey && - e.code === 'KeyZ' - ) { - self.history.undo() - } - }) + document.addEventListener('keydown', e => { + const cm = e.ctrlKey || e.metaKey + if ( + self.history.canRedo && + // ctrl+shift+z or cmd+shift+z + ((cm && e.shiftKey && e.code === 'KeyZ') || + // ctrl+y + (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) + ) { + self.history.redo() + } + if ( + self.history.canUndo && // ctrl+z or cmd+z + cm && + !e.shiftKey && + e.code === 'KeyZ' + ) { + self.history.undo() + } + }) - for (const [key, val] of Object.entries(localStorage) - .filter(([key, _val]) => key.startsWith('localSaved-')) - .filter(([key]) => key.includes(self.configPath || 'undefined'))) { - try { - const { session } = JSON.parse(val) - self.savedSessionsVolatile.set(key, session) - } catch (e) { - console.error('bad session encountered', key, val) + for (const [key, val] of Object.entries(localStorage) + .filter(([key, _val]) => key.startsWith('localSaved-')) + .filter(([key]) => key.includes(self.configPath || 'undefined'))) { + try { + const { session } = JSON.parse(val) + self.savedSessionsVolatile.set(key, session) + } catch (e) { + console.error('bad session encountered', key, val) + } } - } - addDisposer( - self, - autorun(() => { - for (const [, val] of self.savedSessionsVolatile.entries()) { - try { - const key = self.localStorageId(val.name) - localStorage.setItem(key, JSON.stringify({ session: val })) - } catch (e) { - // @ts-expect-error - if (e.code === '22' || e.code === '1024') { - alert( - 'Local storage is full! Please use the "Open sessions" panel to remove old sessions', - ) + addDisposer( + self, + autorun(() => { + for (const [, val] of self.savedSessionsVolatile.entries()) { + try { + const key = self.localStorageId(val.name) + localStorage.setItem(key, JSON.stringify({ session: val })) + } catch (e) { + // @ts-expect-error + if (e.code === '22' || e.code === '1024') { + alert( + 'Local storage is full! Please use the "Open sessions" panel to remove old sessions', + ) + } } } - } - }), - ) + }), + ) - addDisposer( - self, - autorun(() => { - if (self.session) { - // we use a specific initialization routine after session is - // created to get it to start tracking itself sort of related - // issue here - // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 - self.history.initialize() - } - }), - ) - addDisposer( - self, - autorun( - () => { + addDisposer( + self, + autorun(() => { if (self.session) { - const noSession = { name: 'empty' } - const snapshot = getSnapshot(self.session) || noSession - sessionStorage.setItem( - 'current', - JSON.stringify({ session: snapshot }), - ) - - localStorage.setItem( - `autosave-${self.configPath}`, - JSON.stringify({ - session: { - ...snapshot, - name: `${snapshot.name}-autosaved`, - }, - }), - ) - if (self.pluginsUpdated) { - // reload app to get a fresh plugin manager - window.location.reload() - } + // we use a specific initialization routine after session is + // created to get it to start tracking itself sort of related + // issue here + // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 + self.history.initialize() } - }, - { delay: 400 }, - ), - ) - addDisposer( - self, - autorun(() => { - self.jbrowse.internetAccounts.forEach(account => { - this.initializeInternetAccount(account) - }) - }), - ) - }, - /** - * #action - */ - setSession(sessionSnapshot?: SnapshotIn) { - const oldSession = self.session - self.session = cast(sessionSnapshot) - if (self.session) { - // validate all references in the session snapshot - try { - filterSessionInPlace(self.session, getType(self.session)) - } catch (error) { - // throws error if session filtering failed - self.session = oldSession - throw error - } - } - }, - /** - * #action - */ - initializeInternetAccount( - internetAccountConfig: AnyConfigurationModel, - initialSnapshot = {}, - ) { - const internetAccountType = pluginManager.getInternetAccountType( - internetAccountConfig.type, - ) - if (!internetAccountType) { - throw new Error( - `unknown internet account type ${internetAccountConfig.type}`, + }), ) - } - - const length = self.internetAccounts.push({ - ...initialSnapshot, - type: internetAccountConfig.type, - configuration: internetAccountConfig, - }) - return self.internetAccounts[length - 1] - }, - /** - * #action - */ - createEphemeralInternetAccount( - internetAccountId: string, - initialSnapshot = {}, - url: string, - ) { - let hostUri - - try { - hostUri = new URL(url).origin - } catch (e) { - // ignore - } - // id of a custom new internaccount is `${type}-${name}` - const internetAccountSplit = internetAccountId.split('-') - const configuration = { - type: internetAccountSplit[0], - internetAccountId: internetAccountId, - name: internetAccountSplit.slice(1).join('-'), - description: '', - domains: hostUri ? [hostUri] : [], - } - const internetAccountType = pluginManager.getInternetAccountType( - configuration.type, - ) - const internetAccount = internetAccountType.stateModel.create({ - ...initialSnapshot, - type: configuration.type, - configuration, - }) - self.internetAccounts.push(internetAccount) - return internetAccount - }, - /** - * #action - */ - setAssemblyEditing(flag: boolean) { - self.isAssemblyEditing = flag - }, - /** - * #action - */ - setDefaultSessionEditing(flag: boolean) { - self.isDefaultSessionEditing = flag - }, - /** - * #action - */ - setPluginsUpdated(flag: boolean) { - self.pluginsUpdated = flag - }, - /** - * #action - */ - setDefaultSession() { - const { defaultSession } = self.jbrowse - const newSession = { - ...defaultSession, - name: `${defaultSession.name} ${new Date().toLocaleString()}`, - } + addDisposer( + self, + autorun( + () => { + if (self.session) { + const noSession = { name: 'empty' } + const snapshot = getSnapshot(self.session) || noSession + sessionStorage.setItem( + 'current', + JSON.stringify({ session: snapshot }), + ) - this.setSession(newSession) - }, - /** - * #action - */ - renameCurrentSession(sessionName: string) { - if (self.session) { - const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) - snapshot.name = sessionName - this.setSession(snapshot) - } - }, - /** - * #action - */ - addSavedSession(session: { name: string }) { - const key = self.localStorageId(session.name) - self.savedSessionsVolatile.set(key, session) - }, - /** - * #action - */ - removeSavedSession(session: { name: string }) { - const key = self.localStorageId(session.name) - localStorage.removeItem(key) - self.savedSessionsVolatile.delete(key) - }, - /** - * #action - */ - duplicateCurrentSession() { - if (self.session) { - const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) - let newSnapshotName = `${self.session.name} (copy)` - if (self.savedSessionNames.includes(newSnapshotName)) { - let newSnapshotCopyNumber = 2 - do { - newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` - newSnapshotCopyNumber += 1 - } while (self.savedSessionNames.includes(newSnapshotName)) - } - snapshot.name = newSnapshotName - this.setSession(snapshot) - } - }, - /** - * #action - */ - activateSession(name: string) { - const localId = self.localStorageId(name) - const newSessionSnapshot = localStorage.getItem(localId) - if (!newSessionSnapshot) { - throw new Error( - `Can't activate session ${name}, it is not in the savedSessions`, + localStorage.setItem( + `autosave-${self.configPath}`, + JSON.stringify({ + session: { + ...snapshot, + name: `${snapshot.name}-autosaved`, + }, + }), + ) + if (self.pluginsUpdated) { + // reload app to get a fresh plugin manager + window.location.reload() + } + } + }, + { delay: 400 }, + ), ) - } - - this.setSession(JSON.parse(newSessionSnapshot).session) - }, - /** - * #action - */ - saveSessionToLocalStorage() { - if (self.session) { - const key = self.localStorageId(self.session.name) - self.savedSessionsVolatile.set(key, getSnapshot(self.session)) - } - }, - loadAutosaveSession() { - const previousAutosave = localStorage.getItem(self.previousAutosaveId) - const autosavedSession = previousAutosave - ? JSON.parse(previousAutosave).session - : {} - const { name } = autosavedSession - autosavedSession.name = `${name.replace('-autosaved', '')}-restored` - this.setSession(autosavedSession) - }, - /** - * #action - */ - setError(error?: unknown) { - self.error = error - }, - /** - * #action - */ - findAppropriateInternetAccount(location: UriLocation) { - // find the existing account selected from menu - const selectedId = location.internetAccountId - if (selectedId) { - const selectedAccount = self.internetAccounts.find(account => { - return account.internetAccountId === selectedId - }) - if (selectedAccount) { - return selectedAccount + }, + /** + * #action + */ + setSession(sessionSnapshot?: SnapshotIn) { + const oldSession = self.session + self.session = cast(sessionSnapshot) + if (self.session) { + // validate all references in the session snapshot + try { + filterSessionInPlace(self.session, getType(self.session)) + } catch (error) { + // throws error if session filtering failed + self.session = oldSession + throw error + } + } + }, + /** + * #action + */ + setAssemblyEditing(flag: boolean) { + self.isAssemblyEditing = flag + }, + /** + * #action + */ + setDefaultSessionEditing(flag: boolean) { + self.isDefaultSessionEditing = flag + }, + /** + * #action + */ + setPluginsUpdated(flag: boolean) { + self.pluginsUpdated = flag + }, + /** + * #action + */ + setDefaultSession() { + const { defaultSession } = self.jbrowse + const newSession = { + ...defaultSession, + name: `${defaultSession.name} ${new Date().toLocaleString()}`, } - } - // if no existing account or not found, try to find working account - for (const account of self.internetAccounts) { - const handleResult = account.handlesLocation(location) - if (handleResult) { - return account + this.setSession(newSession) + }, + /** + * #action + */ + renameCurrentSession(sessionName: string) { + if (self.session) { + const snapshot = JSON.parse( + JSON.stringify(getSnapshot(self.session)), + ) + snapshot.name = sessionName + this.setSession(snapshot) + } + }, + /** + * #action + */ + addSavedSession(session: { name: string }) { + const key = self.localStorageId(session.name) + self.savedSessionsVolatile.set(key, session) + }, + /** + * #action + */ + removeSavedSession(session: { name: string }) { + const key = self.localStorageId(session.name) + localStorage.removeItem(key) + self.savedSessionsVolatile.delete(key) + }, + /** + * #action + */ + duplicateCurrentSession() { + if (self.session) { + const snapshot = JSON.parse( + JSON.stringify(getSnapshot(self.session)), + ) + let newSnapshotName = `${self.session.name} (copy)` + if (self.savedSessionNames.includes(newSnapshotName)) { + let newSnapshotCopyNumber = 2 + do { + newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` + newSnapshotCopyNumber += 1 + } while (self.savedSessionNames.includes(newSnapshotName)) + } + snapshot.name = newSnapshotName + this.setSession(snapshot) + } + }, + /** + * #action + */ + activateSession(name: string) { + const localId = self.localStorageId(name) + const newSessionSnapshot = localStorage.getItem(localId) + if (!newSessionSnapshot) { + throw new Error( + `Can't activate session ${name}, it is not in the savedSessions`, + ) } - } - // if still no existing account, create ephemeral config to use - return selectedId - ? this.createEphemeralInternetAccount(selectedId, {}, location.uri) - : null - }, - })) + this.setSession(JSON.parse(newSessionSnapshot).session) + }, + /** + * #action + */ + saveSessionToLocalStorage() { + if (self.session) { + const key = self.localStorageId(self.session.name) + self.savedSessionsVolatile.set(key, getSnapshot(self.session)) + } + }, + loadAutosaveSession() { + const previousAutosave = localStorage.getItem(self.previousAutosaveId) + const autosavedSession = previousAutosave + ? JSON.parse(previousAutosave).session + : {} + const { name } = autosavedSession + autosavedSession.name = `${name.replace('-autosaved', '')}-restored` + this.setSession(autosavedSession) + }, + /** + * #action + */ + setError(error?: unknown) { + self.error = error + }, + } + }) .volatile(self => ({ menus: [ { diff --git a/products/jbrowse-web/src/sessionModel/Assemblies.ts b/products/jbrowse-web/src/sessionModel/Assemblies.ts index 9142cae78f..6fb3b4afa8 100644 --- a/products/jbrowse-web/src/sessionModel/Assemblies.ts +++ b/products/jbrowse-web/src/sessionModel/Assemblies.ts @@ -1,4 +1,4 @@ -import { getParent, types } from 'mobx-state-tree' +import { types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { @@ -6,7 +6,7 @@ import { AnyConfigurationModel, readConfObject, } from '@jbrowse/core/configuration' -import { BaseSessionModel } from './Base' +import { BaseSession } from '@jbrowse/product-core/src/Session/Base' export default function Assemblies( pluginManager: PluginManager, @@ -23,86 +23,92 @@ export default function Assemblies( */ temporaryAssemblies: types.array(assemblyConfigSchemasType), }) - .views(self => ({ - /** - * #getter - */ - get assemblies(): AnyConfigurationModel[] { - return (self as typeof self & BaseSessionModel).jbrowse.assemblies - }, - /** - * #getter - */ - get assemblyNames(): string[] { - const { assemblyNames } = getParent(self).jbrowse - const sessionAssemblyNames = self.sessionAssemblies.map(assembly => - readConfObject(assembly, 'name'), - ) - return [...assemblyNames, ...sessionAssemblyNames] - }, - /** - * #getter - */ - get assemblyManager() { - return getParent(self).assemblyManager - }, - })) - .actions(self => ({ - /** - * #action - */ - addAssembly(conf: AnyConfiguration) { - const asm = self.sessionAssemblies.find(f => f.name === conf.name) - if (asm) { - console.warn(`Assembly ${conf.name} was already existing`) - return asm - } - const length = self.sessionAssemblies.push(conf) - return self.sessionAssemblies[length - 1] - }, + .views(s => { + const self = s as typeof s & BaseSession + return { + /** + * #getter + */ + get assemblies(): AnyConfigurationModel[] { + return self.jbrowse.assemblies + }, + /** + * #getter + */ + get assemblyNames(): string[] { + const { assemblyNames } = self.jbrowse + const sessionAssemblyNames = self.sessionAssemblies.map(assembly => + readConfObject(assembly, 'name'), + ) + return [...assemblyNames, ...sessionAssemblyNames] + }, + /** + * #getter + */ + get assemblyManager() { + return self.root.assemblyManager + }, + } + }) + .actions(s => { + const self = s as typeof s & BaseSession + return { + /** + * #action + */ + addAssembly(conf: AnyConfiguration) { + const asm = self.sessionAssemblies.find(f => f.name === conf.name) + if (asm) { + console.warn(`Assembly ${conf.name} was already existing`) + return asm + } + const length = self.sessionAssemblies.push(conf) + return self.sessionAssemblies[length - 1] + }, - /** - * #action - * used for read vs ref type assemblies. - */ - addTemporaryAssembly(conf: AnyConfiguration) { - const asm = self.sessionAssemblies.find(f => f.name === conf.name) - if (asm) { - console.warn(`Assembly ${conf.name} was already existing`) - return asm - } - const length = self.temporaryAssemblies.push(conf) - return self.temporaryAssemblies[length - 1] - }, + /** + * #action + * used for read vs ref type assemblies. + */ + addTemporaryAssembly(conf: AnyConfiguration) { + const asm = self.sessionAssemblies.find(f => f.name === conf.name) + if (asm) { + console.warn(`Assembly ${conf.name} was already existing`) + return asm + } + const length = self.temporaryAssemblies.push(conf) + return self.temporaryAssemblies[length - 1] + }, - /** - * #action - */ - addAssemblyConf(assemblyConf: AnyConfiguration) { - return getParent(self).jbrowse.addAssemblyConf(assemblyConf) - }, + /** + * #action + */ + addAssemblyConf(assemblyConf: AnyConfiguration) { + return self.jbrowse.addAssemblyConf(assemblyConf) + }, - /** - * #action - */ - removeAssembly(assemblyName: string) { - const index = self.sessionAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.sessionAssemblies.splice(index, 1) - } - }, - /** - * #action - */ - removeTemporaryAssembly(assemblyName: string) { - const index = self.temporaryAssemblies.findIndex( - asm => asm.name === assemblyName, - ) - if (index !== -1) { - self.temporaryAssemblies.splice(index, 1) - } - }, - })) + /** + * #action + */ + removeAssembly(assemblyName: string) { + const index = self.sessionAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.sessionAssemblies.splice(index, 1) + } + }, + /** + * #action + */ + removeTemporaryAssembly(assemblyName: string) { + const index = self.temporaryAssemblies.findIndex( + asm => asm.name === assemblyName, + ) + if (index !== -1) { + self.temporaryAssemblies.splice(index, 1) + } + }, + } + }) } diff --git a/products/jbrowse-web/src/sessionModel/Base.ts b/products/jbrowse-web/src/sessionModel/Base.ts deleted file mode 100644 index d7be69e988..0000000000 --- a/products/jbrowse-web/src/sessionModel/Base.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Instance, types } from 'mobx-state-tree' - -import PluginManager from '@jbrowse/core/PluginManager' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' - -import { Session as CoreSession } from '@jbrowse/product-core' - -export function BaseSession(pluginManager: PluginManager) { - const BaseSession = CoreSession.Base(pluginManager) - .props({ - /** - * #property - */ - margin: 0, - - /** - * #property - */ - sessionPlugins: types.array(types.frozen()), - }) - .views(self => ({ - /** - * #getter - */ - get tracks(): AnyConfigurationModel[] { - return [...self.sessionTracks, ...self.jbrowse.tracks] - }, - })) - .actions(self => ({ - /** - * #action - */ - setName(str: string) { - self.name = str - }, - })) - return BaseSession -} - -export type BaseSessionModel = Instance> diff --git a/products/jbrowse-web/src/sessionModel/SessionConnections.ts b/products/jbrowse-web/src/sessionModel/SessionConnections.ts index 59e33c5c25..349518956b 100644 --- a/products/jbrowse-web/src/sessionModel/SessionConnections.ts +++ b/products/jbrowse-web/src/sessionModel/SessionConnections.ts @@ -1,10 +1,12 @@ import { types } from 'mobx-state-tree' import { Session as CoreSession } from '@jbrowse/product-core' +import type { BaseSession } from '@jbrowse/product-core/src/Session/Base' + import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import type { BaseSessionModel } from './Base' import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' +import { SessionWithSessionTracks } from '@jbrowse/product-core/src/Session/SessionTracks' export default function SessionConnections(pluginManager: PluginManager) { return types @@ -21,8 +23,7 @@ export default function SessionConnections(pluginManager: PluginManager) { }), ) .actions(s => { - const self = s as typeof s & BaseSessionModel - + const self = s as typeof s & BaseSession & SessionWithSessionTracks const super_deleteConnection = self.deleteConnection const super_addConnectionConf = self.addConnectionConf return { diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 28c08521a1..4ce64cf4a5 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -2,11 +2,7 @@ import { lazy } from 'react' import clone from 'clone' import { PluginDefinition } from '@jbrowse/core/PluginLoader' -import { - getConf, - AnyConfigurationModel, - AnyConfiguration, -} from '@jbrowse/core/configuration' +import { getConf, AnyConfigurationModel } from '@jbrowse/core/configuration' import { AbstractSessionModel, JBrowsePlugin } from '@jbrowse/core/util/types' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' @@ -32,7 +28,6 @@ import CopyIcon from '@mui/icons-material/FileCopy' import DeleteIcon from '@mui/icons-material/Delete' import InfoIcon from '@mui/icons-material/Info' -import { BaseSession } from './Base' import Assemblies from './Assemblies' import SessionConnections from './SessionConnections' import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' @@ -56,10 +51,35 @@ export default function sessionModelFactory( CoreSession.Themes(pluginManager), CoreSession.MultipleViews(pluginManager), CoreSession.SessionTracks(pluginManager), - BaseSession(pluginManager), Assemblies(pluginManager, assemblyConfigSchemasType), SessionConnections(pluginManager), ) + .props({ + /** + * #property + */ + margin: 0, + /** + * #property + */ + sessionPlugins: types.array(types.frozen()), + }) + .views(self => ({ + /** + * #getter + */ + get tracks(): AnyConfigurationModel[] { + return [...self.sessionTracks, ...self.jbrowse.tracks] + }, + })) + .actions(self => ({ + /** + * #action + */ + setName(str: string) { + self.name = str + }, + })) .volatile((/* self */) => ({ /** * #volatile From 4fb58dce498a34363ff01b00d16044442ca8e014 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Fri, 5 May 2023 14:40:34 -0700 Subject: [PATCH 29/44] wip --- packages/product-core/src/RootModel/Base.ts | 5 +- .../jbrowse-desktop/src/rootModel/Menus.ts | 5 +- .../jbrowse-desktop/src/rootModel/index.ts | 2 +- .../jbrowse-desktop/src/sessionModel/Base.ts | 1 + .../jbrowse-desktop/src/sessionModel/index.ts | 25 + products/jbrowse-web/src/rootModel.ts | 799 ++++++++++++++++++ 6 files changed, 833 insertions(+), 4 deletions(-) create mode 100644 products/jbrowse-web/src/rootModel.ts diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts index a6ab37424d..cbf1ef9ea6 100644 --- a/packages/product-core/src/RootModel/Base.ts +++ b/packages/product-core/src/RootModel/Base.ts @@ -105,8 +105,9 @@ export default function BaseRootModelTypeF< */ async renameCurrentSession(newName: string) { if (self.session) { - // @ts-expect-error - this.setSession({ ...getSnapshot(self.session), name: newName }) + const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) + snapshot.name = newName + this.setSession(snapshot) } }, /** diff --git a/products/jbrowse-desktop/src/rootModel/Menus.ts b/products/jbrowse-desktop/src/rootModel/Menus.ts index 2bea2fe4e8..1e667fc375 100644 --- a/products/jbrowse-desktop/src/rootModel/Menus.ts +++ b/products/jbrowse-desktop/src/rootModel/Menus.ts @@ -1,5 +1,5 @@ import PluginManager from '@jbrowse/core/PluginManager' -import { types } from 'mobx-state-tree' +import { Instance, types } from 'mobx-state-tree' import { lazy } from 'react' // icons @@ -383,3 +383,6 @@ export default function Menus(pluginManager: PluginManager) { }, })) } + +export type DesktopMenusType = ReturnType +export type DesktopMenus = Instance \ No newline at end of file diff --git a/products/jbrowse-desktop/src/rootModel/index.ts b/products/jbrowse-desktop/src/rootModel/index.ts index 9420d789c4..15b4dc4fd2 100644 --- a/products/jbrowse-desktop/src/rootModel/index.ts +++ b/products/jbrowse-desktop/src/rootModel/index.ts @@ -69,4 +69,4 @@ export default function rootModelFactory(pluginManager: PluginManager) { } export type DesktopRootModelType = ReturnType -export type DesktopRootModel = Instance +export type DesktopRootModel = Instance \ No newline at end of file diff --git a/products/jbrowse-desktop/src/sessionModel/Base.ts b/products/jbrowse-desktop/src/sessionModel/Base.ts index 728ebd1a00..34e24d1188 100644 --- a/products/jbrowse-desktop/src/sessionModel/Base.ts +++ b/products/jbrowse-desktop/src/sessionModel/Base.ts @@ -1,6 +1,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import { Instance, types } from 'mobx-state-tree' import { Session as CoreSession } from '@jbrowse/product-core' +import { DesktopRootModel } from '../rootModel' export default function BaseSession(pluginManager: PluginManager) { return CoreSession.Base(pluginManager) diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 3e49d8047c..06bc95dc98 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -36,6 +36,25 @@ export default function sessionModelFactory( TrackMenu(pluginManager), ) .views(self => ({ + /** + * #getter + */ + get history() { + return self.root.history + }, + /** + * #getter + */ + get menus() { + return self.root.menus + }, + /** + * #getter + */ + get savedSessionNames() { + return self.root.savedSessionNames + }, + /** * #method */ @@ -44,6 +63,12 @@ export default function sessionModelFactory( }, })) .actions(self => ({ + /** + * #action + */ + renameCurrentSession(sessionName: string) { + return self.root.renameCurrentSession(sessionName) + }, /** * #action */ diff --git a/products/jbrowse-web/src/rootModel.ts b/products/jbrowse-web/src/rootModel.ts new file mode 100644 index 0000000000..fc7e34354d --- /dev/null +++ b/products/jbrowse-web/src/rootModel.ts @@ -0,0 +1,799 @@ +import { lazy } from 'react' +import { + addDisposer, + cast, + getSnapshot, + getType, + types, + IAnyStateTreeNode, + SnapshotIn, +} from 'mobx-state-tree' + +import { saveAs } from 'file-saver' +import { observable, autorun } from 'mobx' +import assemblyManagerFactory from '@jbrowse/core/assemblyManager' +import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' +import PluginManager from '@jbrowse/core/PluginManager' +import RpcManager from '@jbrowse/core/rpc/RpcManager' +import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' +import TimeTraveller from '@jbrowse/core/util/TimeTraveller' +import { UriLocation } from '@jbrowse/core/util/types' +import { AbstractSessionModel, SessionWithWidgets } from '@jbrowse/core/util' +import { MenuItem } from '@jbrowse/core/ui' + +// icons +import AddIcon from '@mui/icons-material/Add' +import SettingsIcon from '@mui/icons-material/Settings' +import AppsIcon from '@mui/icons-material/Apps' +import FileCopyIcon from '@mui/icons-material/FileCopy' +import FolderOpenIcon from '@mui/icons-material/FolderOpen' +import GetAppIcon from '@mui/icons-material/GetApp' +import PublishIcon from '@mui/icons-material/Publish' +import ExtensionIcon from '@mui/icons-material/Extension' +import StorageIcon from '@mui/icons-material/Storage' +import SaveIcon from '@mui/icons-material/Save' +import UndoIcon from '@mui/icons-material/Undo' +import RedoIcon from '@mui/icons-material/Redo' +import { Cable } from '@jbrowse/core/ui/Icons' + +// other +import makeWorkerInstance from './makeWorkerInstance' +import corePlugins from './corePlugins' +import jbrowseWebFactory from './jbrowseModel' +import sessionModelFactory from './sessionModelFactory' +import { filterSessionInPlace } from './util' +import { AnyConfigurationModel } from '@jbrowse/core/configuration' + +const PreferencesDialog = lazy(() => import('./PreferencesDialog')) + +interface Menu { + label: string + menuItems: MenuItem[] +} + +/** + * #stateModel JBrowseWebRootModel + * note that many properties of the root model are available through the session, which + * may be preferable since using getSession() is better relied on than getRoot() + */ +export default function RootModel( + pluginManager: PluginManager, + adminMode = false, +) { + const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) + const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) + const AssemblyManager = assemblyManagerFactory( + assemblyConfigSchema, + pluginManager, + ) + return types + .model('Root', { + /** + * #property + * `jbrowse` is a mapping of the config.json into the in-memory state tree + */ + jbrowse: jbrowseWebFactory(pluginManager, Session, assemblyConfigSchema), + /** + * #property + */ + configPath: types.maybe(types.string), + /** + * #property + * `session` encompasses the currently active state of the app, including + * views open, tracks open in those views, etc. + */ + session: types.maybe(Session), + /** + * #property + */ + assemblyManager: types.optional(AssemblyManager, {}), + /** + * #property + */ + version: types.maybe(types.string), + /** + * #property + */ + internetAccounts: types.array( + pluginManager.pluggableMstType('internet account', 'stateModel'), + ), + /** + * #property + * used for undo/redo + */ + history: types.optional(TimeTraveller, { targetPath: '../session' }), + }) + .volatile(self => ({ + isAssemblyEditing: false, + isDefaultSessionEditing: false, + pluginsUpdated: false, + rpcManager: new RpcManager( + pluginManager, + self.jbrowse.configuration.rpc, + { + WebWorkerRpcDriver: { + makeWorkerInstance, + }, + MainThreadRpcDriver: {}, + }, + ), + savedSessionsVolatile: observable.map({}), + textSearchManager: new TextSearchManager(pluginManager), + error: undefined as unknown, + })) + .views(self => ({ + /** + * #getter + */ + get savedSessions() { + return [...self.savedSessionsVolatile.values()] + }, + /** + * #method + */ + localStorageId(name: string) { + return `localSaved-${name}-${self.configPath}` + }, + /** + * #getter + */ + get autosaveId() { + return `autosave-${self.configPath}` + }, + /** + * #getter + */ + get previousAutosaveId() { + return `previousAutosave-${self.configPath}` + }, + })) + .views(self => ({ + /** + * #getter + */ + get savedSessionNames() { + return self.savedSessions.map(session => session.name) + }, + /** + * #getter + */ + get currentSessionId() { + const locationUrl = new URL(window.location.href) + const params = new URLSearchParams(locationUrl.search) + return params?.get('session')?.split('local-')[1] + }, + })) + + .actions(self => ({ + afterCreate() { + document.addEventListener('keydown', e => { + const cm = e.ctrlKey || e.metaKey + if ( + self.history.canRedo && + // ctrl+shift+z or cmd+shift+z + ((cm && e.shiftKey && e.code === 'KeyZ') || + // ctrl+y + (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) + ) { + self.history.redo() + } + if ( + self.history.canUndo && // ctrl+z or cmd+z + cm && + !e.shiftKey && + e.code === 'KeyZ' + ) { + self.history.undo() + } + }) + + for (const [key, val] of Object.entries(localStorage) + .filter(([key, _val]) => key.startsWith('localSaved-')) + .filter(([key]) => key.includes(self.configPath || 'undefined'))) { + try { + const { session } = JSON.parse(val) + self.savedSessionsVolatile.set(key, session) + } catch (e) { + console.error('bad session encountered', key, val) + } + } + addDisposer( + self, + autorun(() => { + for (const [, val] of self.savedSessionsVolatile.entries()) { + try { + const key = self.localStorageId(val.name) + localStorage.setItem(key, JSON.stringify({ session: val })) + } catch (e) { + // @ts-expect-error + if (e.code === '22' || e.code === '1024') { + alert( + 'Local storage is full! Please use the "Open sessions" panel to remove old sessions', + ) + } + } + } + }), + ) + + addDisposer( + self, + autorun(() => { + if (self.session) { + // we use a specific initialization routine after session is + // created to get it to start tracking itself sort of related + // issue here + // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 + self.history.initialize() + } + }), + ) + addDisposer( + self, + autorun( + () => { + if (self.session) { + const noSession = { name: 'empty' } + const snapshot = getSnapshot(self.session) || noSession + sessionStorage.setItem( + 'current', + JSON.stringify({ session: snapshot }), + ) + + localStorage.setItem( + `autosave-${self.configPath}`, + JSON.stringify({ + session: { + ...snapshot, + name: `${snapshot.name}-autosaved`, + }, + }), + ) + if (self.pluginsUpdated) { + // reload app to get a fresh plugin manager + window.location.reload() + } + } + }, + { delay: 400 }, + ), + ) + addDisposer( + self, + autorun(() => { + self.jbrowse.internetAccounts.forEach(account => { + this.initializeInternetAccount(account) + }) + }), + ) + }, + /** + * #action + */ + setSession(sessionSnapshot?: SnapshotIn) { + const oldSession = self.session + self.session = cast(sessionSnapshot) + if (self.session) { + // validate all references in the session snapshot + try { + filterSessionInPlace(self.session, getType(self.session)) + } catch (error) { + // throws error if session filtering failed + self.session = oldSession + throw error + } + } + }, + initializeInternetAccount( + internetAccountConfig: AnyConfigurationModel, + initialSnapshot = {}, + ) { + const internetAccountType = pluginManager.getInternetAccountType( + internetAccountConfig.type, + ) + if (!internetAccountType) { + throw new Error( + `unknown internet account type ${internetAccountConfig.type}`, + ) + } + + const length = self.internetAccounts.push({ + ...initialSnapshot, + type: internetAccountConfig.type, + configuration: internetAccountConfig, + }) + return self.internetAccounts[length - 1] + }, + createEphemeralInternetAccount( + internetAccountId: string, + initialSnapshot = {}, + url: string, + ) { + let hostUri + + try { + hostUri = new URL(url).origin + } catch (e) { + // ignore + } + // id of a custom new internaccount is `${type}-${name}` + const internetAccountSplit = internetAccountId.split('-') + const configuration = { + type: internetAccountSplit[0], + internetAccountId: internetAccountId, + name: internetAccountSplit.slice(1).join('-'), + description: '', + domains: hostUri ? [hostUri] : [], + } + const internetAccountType = pluginManager.getInternetAccountType( + configuration.type, + ) + const internetAccount = internetAccountType.stateModel.create({ + ...initialSnapshot, + type: configuration.type, + configuration, + }) + self.internetAccounts.push(internetAccount) + return internetAccount + }, + setAssemblyEditing(flag: boolean) { + self.isAssemblyEditing = flag + }, + setDefaultSessionEditing(flag: boolean) { + self.isDefaultSessionEditing = flag + }, + setPluginsUpdated(flag: boolean) { + self.pluginsUpdated = flag + }, + setDefaultSession() { + const { defaultSession } = self.jbrowse + const newSession = { + ...defaultSession, + name: `${defaultSession.name} ${new Date().toLocaleString()}`, + } + + this.setSession(newSession) + }, + renameCurrentSession(sessionName: string) { + if (self.session) { + const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) + snapshot.name = sessionName + this.setSession(snapshot) + } + }, + + addSavedSession(session: { name: string }) { + const key = self.localStorageId(session.name) + self.savedSessionsVolatile.set(key, session) + }, + + removeSavedSession(session: { name: string }) { + const key = self.localStorageId(session.name) + localStorage.removeItem(key) + self.savedSessionsVolatile.delete(key) + }, + + duplicateCurrentSession() { + if (self.session) { + const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) + let newSnapshotName = `${self.session.name} (copy)` + if (self.savedSessionNames.includes(newSnapshotName)) { + let newSnapshotCopyNumber = 2 + do { + newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` + newSnapshotCopyNumber += 1 + } while (self.savedSessionNames.includes(newSnapshotName)) + } + snapshot.name = newSnapshotName + this.setSession(snapshot) + } + }, + activateSession(name: string) { + const localId = self.localStorageId(name) + const newSessionSnapshot = localStorage.getItem(localId) + if (!newSessionSnapshot) { + throw new Error( + `Can't activate session ${name}, it is not in the savedSessions`, + ) + } + + this.setSession(JSON.parse(newSessionSnapshot).session) + }, + saveSessionToLocalStorage() { + if (self.session) { + const key = self.localStorageId(self.session.name) + self.savedSessionsVolatile.set(key, getSnapshot(self.session)) + } + }, + loadAutosaveSession() { + const previousAutosave = localStorage.getItem(self.previousAutosaveId) + const autosavedSession = previousAutosave + ? JSON.parse(previousAutosave).session + : {} + const { name } = autosavedSession + autosavedSession.name = `${name.replace('-autosaved', '')}-restored` + this.setSession(autosavedSession) + }, + + setError(error?: unknown) { + self.error = error + }, + findAppropriateInternetAccount(location: UriLocation) { + // find the existing account selected from menu + const selectedId = location.internetAccountId + if (selectedId) { + const selectedAccount = self.internetAccounts.find(account => { + return account.internetAccountId === selectedId + }) + if (selectedAccount) { + return selectedAccount + } + } + + // if no existing account or not found, try to find working account + for (const account of self.internetAccounts) { + const handleResult = account.handlesLocation(location) + if (handleResult) { + return account + } + } + + // if still no existing account, create ephemeral config to use + return selectedId + ? this.createEphemeralInternetAccount(selectedId, {}, location.uri) + : null + }, + })) + .volatile(self => ({ + menus: [ + { + label: 'File', + menuItems: [ + { + label: 'New session', + icon: AddIcon, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onClick: (session: any) => { + const lastAutosave = localStorage.getItem(self.autosaveId) + if (lastAutosave) { + localStorage.setItem(self.previousAutosaveId, lastAutosave) + } + session.setDefaultSession() + }, + }, + { + label: 'Import session…', + icon: PublishIcon, + onClick: (session: SessionWithWidgets) => { + const widget = session.addWidget( + 'ImportSessionWidget', + 'importSessionWidget', + ) + session.showWidget(widget) + }, + }, + { + label: 'Export session', + icon: GetAppIcon, + onClick: (session: IAnyStateTreeNode) => { + const sessionBlob = new Blob( + [JSON.stringify({ session: getSnapshot(session) }, null, 2)], + { type: 'text/plain;charset=utf-8' }, + ) + saveAs(sessionBlob, 'session.json') + }, + }, + { + label: 'Open session…', + icon: FolderOpenIcon, + onClick: (session: SessionWithWidgets) => { + const widget = session.addWidget( + 'SessionManager', + 'sessionManager', + ) + session.showWidget(widget) + }, + }, + { + label: 'Save session', + icon: SaveIcon, + onClick: (session: SessionWithWidgets) => { + self.saveSessionToLocalStorage() + session.notify(`Saved session "${session.name}"`, 'success') + }, + }, + { + label: 'Duplicate session', + icon: FileCopyIcon, + onClick: (session: AbstractSessionModel) => { + if (session.duplicateCurrentSession) { + session.duplicateCurrentSession() + } + }, + }, + { type: 'divider' }, + { + label: 'Open track...', + icon: StorageIcon, + onClick: (session: SessionWithWidgets) => { + if (session.views.length === 0) { + session.notify('Please open a view to add a track first') + } else if (session.views.length > 0) { + 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: Cable, + onClick: (session: SessionWithWidgets) => { + const widget = session.addWidget( + 'AddConnectionWidget', + 'addConnectionWidget', + ) + session.showWidget(widget) + }, + }, + { type: 'divider' }, + { + label: 'Return to splash screen', + icon: AppsIcon, + onClick: () => self.setSession(undefined), + }, + ], + }, + ...(adminMode + ? [ + { + label: 'Admin', + menuItems: [ + { + label: 'Open assembly manager', + onClick: () => self.setAssemblyEditing(true), + }, + { + label: 'Set default session', + onClick: () => self.setDefaultSessionEditing(true), + }, + ], + }, + ] + : []), + { + label: 'Add', + menuItems: [], + }, + { + label: 'Tools', + menuItems: [ + { + label: 'Undo', + icon: UndoIcon, + onClick: () => { + if (self.history.canUndo) { + self.history.undo() + } + }, + }, + { + label: 'Redo', + icon: RedoIcon, + onClick: () => { + if (self.history.canRedo) { + self.history.redo() + } + }, + }, + { type: 'divider' }, + { + label: 'Plugin store', + icon: ExtensionIcon, + onClick: () => { + if (self.session) { + const widget = self.session.addWidget( + 'PluginStoreWidget', + 'pluginStoreWidget', + ) + self.session.showWidget(widget) + } + }, + }, + { + label: 'Preferences', + icon: SettingsIcon, + onClick: () => { + if (self.session) { + self.session.queueDialog(handleClose => [ + PreferencesDialog, + { + session: self.session, + handleClose, + }, + ]) + } + }, + }, + ], + }, + ] as Menu[], + adminMode, + })) + .actions(self => ({ + /** + * #action + */ + setMenus(newMenus: Menu[]) { + self.menus = newMenus + }, + /** + * #action + * Add a top-level menu + * @param menuName - Name of the menu to insert. + * @returns The new length of the top-level menus array + */ + appendMenu(menuName: string) { + return self.menus.push({ label: menuName, menuItems: [] }) + }, + /** + * #action + * Insert a top-level menu + * @param menuName - Name of the menu to insert. + * @param position - Position to insert menu. If negative, counts from th + * end, e.g. `insertMenu('My Menu', -1)` will insert the menu as the + * second-to-last one. + * @returns The new length of the top-level menus array + */ + insertMenu(menuName: string, position: number) { + self.menus.splice( + (position < 0 ? self.menus.length : 0) + position, + 0, + { label: menuName, menuItems: [] }, + ) + return self.menus.length + }, + /** + * #action + * Add a menu item to a top-level menu + * @param menuName - Name of the top-level menu to append to. + * @param menuItem - Menu item to append. + * @returns The new length of the menu + */ + appendToMenu(menuName: string, menuItem: MenuItem) { + const menu = self.menus.find(m => m.label === menuName) + if (!menu) { + self.menus.push({ label: menuName, menuItems: [menuItem] }) + return 1 + } + return menu.menuItems.push(menuItem) + }, + /** + * #action + * Insert a menu item into a top-level menu + * @param menuName - Name of the top-level menu to insert into + * @param menuItem - Menu item to insert + * @param position - Position to insert menu item. If negative, counts + * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as + * the second-to-last one. + * @returns The new length of the menu + */ + insertInMenu(menuName: string, menuItem: MenuItem, position: number) { + const menu = self.menus.find(m => m.label === menuName) + if (!menu) { + self.menus.push({ label: menuName, menuItems: [menuItem] }) + return 1 + } + const insertPosition = + position < 0 ? menu.menuItems.length + position : position + menu.menuItems.splice(insertPosition, 0, menuItem) + return menu.menuItems.length + }, + /** + * #action + * Add a menu item to a sub-menu + * @param menuPath - Path to the sub-menu to add to, starting with the + * top-level menu (e.g. `['File', 'Insert']`). + * @param menuItem - Menu item to append. + * @returns The new length of the sub-menu + */ + appendToSubMenu(menuPath: string[], menuItem: MenuItem) { + let topMenu = self.menus.find(m => m.label === menuPath[0]) + if (!topMenu) { + const idx = this.appendMenu(menuPath[0]) + topMenu = self.menus[idx - 1] + } + let { menuItems: subMenu } = topMenu + const pathSoFar = [menuPath[0]] + menuPath.slice(1).forEach(menuName => { + pathSoFar.push(menuName) + let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) + if (!sm) { + const idx = subMenu.push({ label: menuName, subMenu: [] }) + sm = subMenu[idx - 1] + } + if (!('subMenu' in sm)) { + throw new Error( + `"${menuName}" in path "${pathSoFar}" is not a subMenu`, + ) + } + subMenu = sm.subMenu + }) + return subMenu.push(menuItem) + }, + /** + * #action + * Insert a menu item into a sub-menu + * @param menuPath - Path to the sub-menu to add to, starting with the + * top-level menu (e.g. `['File', 'Insert']`). + * @param menuItem - Menu item to insert. + * @param position - Position to insert menu item. If negative, counts + * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as + * the second-to-last one. + * @returns The new length of the sub-menu + */ + insertInSubMenu( + menuPath: string[], + menuItem: MenuItem, + position: number, + ) { + let topMenu = self.menus.find(m => m.label === menuPath[0]) + if (!topMenu) { + const idx = this.appendMenu(menuPath[0]) + topMenu = self.menus[idx - 1] + } + let { menuItems: subMenu } = topMenu + const pathSoFar = [menuPath[0]] + menuPath.slice(1).forEach(menuName => { + pathSoFar.push(menuName) + let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) + if (!sm) { + const idx = subMenu.push({ label: menuName, subMenu: [] }) + sm = subMenu[idx - 1] + } + if (!('subMenu' in sm)) { + throw new Error( + `"${menuName}" in path "${pathSoFar}" is not a subMenu`, + ) + } + subMenu = sm.subMenu + }) + subMenu.splice(position, 0, menuItem) + return subMenu.length + }, + })) +} + +export function createTestSession(snapshot = {}, adminMode = false) { + const pluginManager = new PluginManager(corePlugins.map(P => new P())) + pluginManager.createPluggableElements() + + const root = RootModel(pluginManager, adminMode).create( + { + jbrowse: { + configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, + }, + assemblyManager: {}, + }, + { pluginManager }, + ) + root.setSession({ + name: 'testSession', + ...snapshot, + }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const session = root.session! + session.views.map(view => view.setWidth(800)) + pluginManager.setRootModel(root) + pluginManager.configure() + return session +} From e2bc30fe2f1b205f031cd3e35cfdffe5d5563d70 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Fri, 5 May 2023 16:00:02 -0700 Subject: [PATCH 30/44] rework root and session model instantiation session can now know the type of its root model, but in exchange, the root model does not know the type of its session --- packages/core/util/index.ts | 4 + packages/product-core/src/RootModel/Base.ts | 11 +- packages/product-core/src/Session/Base.ts | 4 +- .../src/PluginStoreWidget/model.test.tsx | 2 +- .../src/DotplotView/model.test.ts | 2 +- products/jbrowse-desktop/src/JBrowse.test.tsx | 6 +- .../jbrowse-desktop/src/StartScreen/util.tsx | 3 +- products/jbrowse-desktop/src/jbrowseConfig.ts | 4 +- products/jbrowse-desktop/src/jbrowseModel.ts | 6 +- .../jbrowse-desktop/src/rootModel/Menus.ts | 2 +- .../jbrowse-desktop/src/rootModel/Sessions.ts | 4 +- .../jbrowse-desktop/src/rootModel/index.ts | 17 +- .../jbrowse-desktop/src/sessionModel/Base.ts | 3 +- .../jbrowse-desktop/src/sessionModel/index.ts | 12 +- products/jbrowse-web/src/JBrowse.tsx | 4 +- products/jbrowse-web/src/Loader.tsx | 11 +- products/jbrowse-web/src/jbrowseConfig.ts | 6 +- products/jbrowse-web/src/jbrowseModel.test.ts | 8 +- products/jbrowse-web/src/jbrowseModel.ts | 11 +- .../jbrowse-web/src/rootModel/index.test.js | 2 +- products/jbrowse-web/src/rootModel/index.ts | 737 +----------------- .../src/{ => rootModel}/rootModel.ts | 599 +++++++------- .../jbrowse-web/src/rootModel/test_util.ts | 30 + .../jbrowse-web/src/sessionModel/index.ts | 29 +- .../sessionModel/sessionModelFactory.test.js | 2 +- .../jbrowse-web/src/tests/JBrowse.test.tsx | 9 +- products/jbrowse-web/src/tests/util.tsx | 9 +- 27 files changed, 383 insertions(+), 1154 deletions(-) rename products/jbrowse-web/src/{ => rootModel}/rootModel.ts (56%) create mode 100644 products/jbrowse-web/src/rootModel/test_util.ts diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index 52ee811ede..9a2515a246 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -219,6 +219,10 @@ export function getSession(node: IAnyStateTreeNode) { } } +export function getJBrowseRoot(node: IAnyStateTreeNode) { + return getParent(getSession(node)) +} + /** get the state model of the view in the state tree that contains the given node */ export function getContainingView(node: IAnyStateTreeNode) { try { diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts index cbf1ef9ea6..121eaa23fa 100644 --- a/packages/product-core/src/RootModel/Base.ts +++ b/packages/product-core/src/RootModel/Base.ts @@ -16,13 +16,10 @@ import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' /** * factory function for the Base-level root model shared by all products */ -export default function BaseRootModelTypeF< - JBROWSE_MODEL_TYPE extends IAnyType, - SESSION_MODEL_TYPE extends IAnyType, ->( +export default function BaseRootModelTypeF( pluginManager: PluginManager, - jbrowseModelType: JBROWSE_MODEL_TYPE, - sessionModelType: SESSION_MODEL_TYPE, + jbrowseModelType: IAnyType, + sessionModelType: IAnyType, assemblyConfigSchema: BaseAssemblyConfigSchema, ) { return types @@ -85,7 +82,7 @@ export default function BaseRootModelTypeF< /** * #action */ - setSession(sessionSnapshot?: SnapshotIn) { + setSession(sessionSnapshot?: SnapshotIn) { self.session = cast(sessionSnapshot) }, /** diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index 854e0356c1..72254d3251 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -8,8 +8,8 @@ import { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager' /** base session shared by **all** JBrowse products. Be careful what you include here, everything will use it. */ export default function BaseSessionFactory< - ROOT_MODEL_TYPE extends BaseRootModelType = BaseRootModelType, - JB_CONFIG_SCHEMA extends AnyConfigurationSchemaType = AnyConfigurationSchemaType, + ROOT_MODEL_TYPE extends BaseRootModelType, + JB_CONFIG_SCHEMA extends AnyConfigurationSchemaType, >(pluginManager: PluginManager) { return types .model({ diff --git a/plugins/data-management/src/PluginStoreWidget/model.test.tsx b/plugins/data-management/src/PluginStoreWidget/model.test.tsx index 18be369308..5a3a44b62c 100644 --- a/plugins/data-management/src/PluginStoreWidget/model.test.tsx +++ b/plugins/data-management/src/PluginStoreWidget/model.test.tsx @@ -1,4 +1,4 @@ -import { createTestSession } from '@jbrowse/web/src/rootModel' +import { createTestSession } from '@jbrowse/web/src/rootModel/test_util' jest.mock('@jbrowse/web/src/makeWorkerInstance', () => () => {}) describe('PluginStoreModel', () => { diff --git a/plugins/dotplot-view/src/DotplotView/model.test.ts b/plugins/dotplot-view/src/DotplotView/model.test.ts index 8203b82410..fe33d1fc15 100644 --- a/plugins/dotplot-view/src/DotplotView/model.test.ts +++ b/plugins/dotplot-view/src/DotplotView/model.test.ts @@ -1,4 +1,4 @@ -import { createTestSession } from '@jbrowse/web/src/rootModel' +import { createTestSession } from '@jbrowse/web/src/rootModel/test_util' import { getEnv } from 'mobx-state-tree' jest.mock('@jbrowse/web/src/makeWorkerInstance', () => () => {}) diff --git a/products/jbrowse-desktop/src/JBrowse.test.tsx b/products/jbrowse-desktop/src/JBrowse.test.tsx index 68c8c228f3..fa6a0d900a 100644 --- a/products/jbrowse-desktop/src/JBrowse.test.tsx +++ b/products/jbrowse-desktop/src/JBrowse.test.tsx @@ -12,6 +12,7 @@ import corePlugins from './corePlugins' import JBrowse from './JBrowse' import JBrowseRootModelFactory from './rootModel' import configSnapshot from '../test_data/volvox/config.json' +import sessionModelFactory from './sessionModel' jest.mock('./makeWorkerInstance', () => () => {}) @@ -27,7 +28,10 @@ function getPluginManager(initialState?: SnapshotIn) { const pluginManager = new PluginManager(corePlugins.map(P => new P())) pluginManager.createPluggableElements() - const JBrowseRootModel = JBrowseRootModelFactory(pluginManager) + const JBrowseRootModel = JBrowseRootModelFactory( + pluginManager, + sessionModelFactory, + ) const rootModel = JBrowseRootModel.create( { jbrowse: initialState || configSnapshot, diff --git a/products/jbrowse-desktop/src/StartScreen/util.tsx b/products/jbrowse-desktop/src/StartScreen/util.tsx index 7ad2b71c08..ec02dd8924 100644 --- a/products/jbrowse-desktop/src/StartScreen/util.tsx +++ b/products/jbrowse-desktop/src/StartScreen/util.tsx @@ -13,6 +13,7 @@ import { import JBrowseRootModelFactory from '../rootModel' import corePlugins from '../corePlugins' import packageJSON from '../../package.json' +import sessionModelFactory from '../sessionModel' const { ipcRenderer } = window.require('electron') @@ -118,7 +119,7 @@ export async function createPluginManager( ]) pm.createPluggableElements() - const JBrowseRootModel = JBrowseRootModelFactory(pm) + const JBrowseRootModel = JBrowseRootModelFactory(pm, sessionModelFactory) const jbrowse = deepmerge(configSnapshot, { internetAccounts: defaultInternetAccounts, diff --git a/products/jbrowse-desktop/src/jbrowseConfig.ts b/products/jbrowse-desktop/src/jbrowseConfig.ts index a2b64fbbdc..a48301605e 100644 --- a/products/jbrowse-desktop/src/jbrowseConfig.ts +++ b/products/jbrowse-desktop/src/jbrowseConfig.ts @@ -7,7 +7,6 @@ import { PluginDefinition } from '@jbrowse/core/PluginLoader' import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' import { types } from 'mobx-state-tree' -import { SessionStateModelType } from './sessionModel' /** * #config JBrowseDesktopConfiguration @@ -17,7 +16,6 @@ function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars export default function JBrowseConfigF( pluginManager: PluginManager, - Session: SessionStateModelType, assemblyConfigSchemasType: AnyConfigurationSchemaType, ) { return types.model('JBrowseDesktop', { @@ -110,7 +108,7 @@ export default function JBrowseConfigF( /** * #slot */ - defaultSession: types.optional(types.frozen(Session), { + defaultSession: types.optional(types.frozen(), { name: `New Session`, }), }) diff --git a/products/jbrowse-desktop/src/jbrowseModel.ts b/products/jbrowse-desktop/src/jbrowseModel.ts index 7efd0a21a1..f7a110a675 100644 --- a/products/jbrowse-desktop/src/jbrowseModel.ts +++ b/products/jbrowse-desktop/src/jbrowseModel.ts @@ -10,7 +10,6 @@ import { resolveIdentifier, } from 'mobx-state-tree' import JBrowseConfigF from './jbrowseConfig' -import { SessionStateModelType } from './sessionModel' import { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager' // poke some things for testing (this stuff will eventually be removed) @@ -24,14 +23,11 @@ window.resolveIdentifier = resolveIdentifier * #stateModel JBrowseDesktopModel * the rootModel.jbrowse state model for JBrowse Desktop */ -function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars - export default function JBrowseDesktop( pluginManager: PluginManager, - Session: SessionStateModelType, assemblyConfigSchemasType: BaseAssemblyConfigSchema, ) { - return JBrowseConfigF(pluginManager, Session, assemblyConfigSchemasType) + return JBrowseConfigF(pluginManager, assemblyConfigSchemasType) .views(self => ({ /** * #getter diff --git a/products/jbrowse-desktop/src/rootModel/Menus.ts b/products/jbrowse-desktop/src/rootModel/Menus.ts index 1e667fc375..8b04e6091e 100644 --- a/products/jbrowse-desktop/src/rootModel/Menus.ts +++ b/products/jbrowse-desktop/src/rootModel/Menus.ts @@ -385,4 +385,4 @@ export default function Menus(pluginManager: PluginManager) { } export type DesktopMenusType = ReturnType -export type DesktopMenus = Instance \ No newline at end of file +export type DesktopMenus = Instance diff --git a/products/jbrowse-desktop/src/rootModel/Sessions.ts b/products/jbrowse-desktop/src/rootModel/Sessions.ts index 21ffc41057..010496b607 100644 --- a/products/jbrowse-desktop/src/rootModel/Sessions.ts +++ b/products/jbrowse-desktop/src/rootModel/Sessions.ts @@ -1,8 +1,8 @@ import PluginManager from '@jbrowse/core/PluginManager' import { autorun } from 'mobx' import { SnapshotIn, addDisposer, getSnapshot, types } from 'mobx-state-tree' -import type { BaseSessionModel } from '../sessionModel/Base' import type { BaseRootModel } from '@jbrowse/product-core/src/RootModel/Base' +import { BaseSession } from '@jbrowse/product-core/src/Session/Base' const { ipcRenderer } = window.require('electron') @@ -60,7 +60,7 @@ export default function SessionManagement(pluginManager: PluginManager) { /** * #action */ - activateSession(sessionSnapshot: SnapshotIn) { + activateSession(sessionSnapshot: SnapshotIn) { self.setSession(sessionSnapshot) }, } diff --git a/products/jbrowse-desktop/src/rootModel/index.ts b/products/jbrowse-desktop/src/rootModel/index.ts index 15b4dc4fd2..97a26857d7 100644 --- a/products/jbrowse-desktop/src/rootModel/index.ts +++ b/products/jbrowse-desktop/src/rootModel/index.ts @@ -1,4 +1,4 @@ -import { types, Instance } from 'mobx-state-tree' +import { types, Instance, IAnyType } from 'mobx-state-tree' import makeWorkerInstance from '../makeWorkerInstance' import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' import PluginManager from '@jbrowse/core/PluginManager' @@ -7,19 +7,26 @@ import RpcManager from '@jbrowse/core/rpc/RpcManager' import { RootModel } from '@jbrowse/product-core' // locals -import sessionModelFactory from '../sessionModel' import jobsModelFactory from '../indexJobsModel' import JBrowseDesktop from '../jbrowseModel' import Menus from './Menus' import SessionManagement from './Sessions' import { HistoryManagement } from './HistoryManagement' +type SessionModelFactory = ( + pm: PluginManager, + assemblyConfigSchema: ReturnType, +) => IAnyType + /** * #stateModel JBrowseDesktopRootModel * note that many properties of the root model are available through the session, which * may be preferable since using getSession() is better relied on than getRoot() */ -export default function rootModelFactory(pluginManager: PluginManager) { +export default function rootModelFactory( + pluginManager: PluginManager, + sessionModelFactory: SessionModelFactory, +) { const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) const JobsManager = jobsModelFactory(pluginManager) @@ -28,7 +35,7 @@ export default function rootModelFactory(pluginManager: PluginManager) { 'JBrowseDesktopRootModel', RootModel.BaseRootModel( pluginManager, - JBrowseDesktop(pluginManager, Session, assemblyConfigSchema), + JBrowseDesktop(pluginManager, assemblyConfigSchema), Session, assemblyConfigSchema, ), @@ -69,4 +76,4 @@ export default function rootModelFactory(pluginManager: PluginManager) { } export type DesktopRootModelType = ReturnType -export type DesktopRootModel = Instance \ No newline at end of file +export type DesktopRootModel = Instance diff --git a/products/jbrowse-desktop/src/sessionModel/Base.ts b/products/jbrowse-desktop/src/sessionModel/Base.ts index 34e24d1188..297debd0b2 100644 --- a/products/jbrowse-desktop/src/sessionModel/Base.ts +++ b/products/jbrowse-desktop/src/sessionModel/Base.ts @@ -1,7 +1,6 @@ import PluginManager from '@jbrowse/core/PluginManager' -import { Instance, types } from 'mobx-state-tree' +import { Instance } from 'mobx-state-tree' import { Session as CoreSession } from '@jbrowse/product-core' -import { DesktopRootModel } from '../rootModel' export default function BaseSession(pluginManager: PluginManager) { return CoreSession.Base(pluginManager) diff --git a/products/jbrowse-desktop/src/sessionModel/index.ts b/products/jbrowse-desktop/src/sessionModel/index.ts index 06bc95dc98..e11e41fd4a 100644 --- a/products/jbrowse-desktop/src/sessionModel/index.ts +++ b/products/jbrowse-desktop/src/sessionModel/index.ts @@ -1,6 +1,6 @@ import { readConfObject } from '@jbrowse/core/configuration' import addSnackbarToModel from '@jbrowse/core/ui/SnackbarModel' -import { types, Instance } from 'mobx-state-tree' +import { types, Instance, getParent } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { Session as CoreSession } from '@jbrowse/product-core' @@ -10,6 +10,7 @@ import Base from './Base' import Assemblies from './Assemblies' import TrackMenu from './TrackMenu' import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' +import { DesktopRootModel } from '../rootModel' /** * #stateModel JBrowseDesktopSessionModel @@ -36,23 +37,26 @@ export default function sessionModelFactory( TrackMenu(pluginManager), ) .views(self => ({ + get root() { + return getParent(self) + }, /** * #getter */ get history() { - return self.root.history + return this.root.history }, /** * #getter */ get menus() { - return self.root.menus + return this.root.menus }, /** * #getter */ get savedSessionNames() { - return self.root.savedSessionNames + return this.root.savedSessionNames }, /** diff --git a/products/jbrowse-web/src/JBrowse.tsx b/products/jbrowse-web/src/JBrowse.tsx index f00cee7c22..062b301a4f 100644 --- a/products/jbrowse-web/src/JBrowse.tsx +++ b/products/jbrowse-web/src/JBrowse.tsx @@ -11,7 +11,7 @@ import PluginManager from '@jbrowse/core/PluginManager' // locals import ShareButton from './ShareButton' import AdminComponent from './AdminComponent' -import { SessionModel } from './sessionModel' +import { WebSessionModel } from './sessionModel' export default observer(function ({ pluginManager, @@ -24,7 +24,7 @@ export default observer(function ({ const [, setSessionId] = useQueryParam('session', StringParam) const { rootModel } = pluginManager const { error, jbrowse } = rootModel || {} - const session = rootModel?.session as SessionModel + const session = rootModel?.session as WebSessionModel const currentSessionId = session.id useEffect(() => { diff --git a/products/jbrowse-web/src/Loader.tsx b/products/jbrowse-web/src/Loader.tsx index 06b9476160..b64afb3ff7 100644 --- a/products/jbrowse-web/src/Loader.tsx +++ b/products/jbrowse-web/src/Loader.tsx @@ -19,13 +19,14 @@ import { doAnalytics } from '@jbrowse/core/util/analytics' import Loading from './Loading' import corePlugins from './corePlugins' import JBrowse from './JBrowse' -import JBrowseRootModelFactory from './rootModel' +import JBrowseRootModelFactory from './rootModel/rootModel' import packageJSON from '../package.json' import factoryReset from './factoryReset' import SessionLoader, { SessionLoaderModel, loadSessionSpec, } from './SessionLoader' +import sessionModelFactory from './sessionModel' // lazy components const SessionWarningDialog = lazy(() => import('./SessionWarningDialog')) @@ -278,7 +279,11 @@ const Renderer = observer( })), ]) pluginManager.createPluggableElements() - const RootModel = JBrowseRootModelFactory(pluginManager, !!adminKey) + const RootModel = JBrowseRootModelFactory( + pluginManager, + sessionModelFactory, + !!adminKey, + ) if (configSnapshot) { const rootModel = RootModel.create( @@ -304,7 +309,7 @@ const Renderer = observer( if (sessionError) { rootModel.setDefaultSession() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rootModel.session!.notify( + rootModel.session.notify( `Error loading session: ${sessionError}. If you received this URL from another user, request that they send you a session generated with the "Share" button instead of copying diff --git a/products/jbrowse-web/src/jbrowseConfig.ts b/products/jbrowse-web/src/jbrowseConfig.ts index be049d3078..09972eb0bc 100644 --- a/products/jbrowse-web/src/jbrowseConfig.ts +++ b/products/jbrowse-web/src/jbrowseConfig.ts @@ -6,18 +6,14 @@ import RpcManager from '@jbrowse/core/rpc/RpcManager' import PluginManager from '@jbrowse/core/PluginManager' import { PluginDefinition } from '@jbrowse/core/PluginLoader' import { types } from 'mobx-state-tree' -import { SessionStateModel } from './sessionModel' /** * #config JBrowseWebConfiguration * configuration in a config.json */ -function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars - export default function JBrowseConfigF( pluginManager: PluginManager, assemblyConfigSchemasType: AnyConfigurationSchemaType, - Session: SessionStateModel, ) { return types.model('JBrowseWeb', { configuration: ConfigurationSchema('Root', { @@ -158,7 +154,7 @@ export default function JBrowseConfigF( /** * #slot */ - defaultSession: types.optional(types.frozen(Session), { + defaultSession: types.optional(types.frozen(), { name: `New session`, }), }) diff --git a/products/jbrowse-web/src/jbrowseModel.test.ts b/products/jbrowse-web/src/jbrowseModel.test.ts index 5741acccb2..99977ee500 100644 --- a/products/jbrowse-web/src/jbrowseModel.test.ts +++ b/products/jbrowse-web/src/jbrowseModel.test.ts @@ -4,7 +4,6 @@ import assemblyConfigSchemasFactory from '@jbrowse/core/assemblyManager/assembly import configSnapshot from '../test_data/volvox/config.json' import corePlugins from './corePlugins' import jbrowseModelFactory from './jbrowseModel' -import sessionModelFactory from './sessionModelFactory' type JBrowseModelType = ReturnType @@ -16,12 +15,7 @@ describe('JBrowse model', () => { .configure() const assemblyConfigSchema = assemblyConfigSchemasFactory(pluginManager) - const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) - JBrowseModel = jbrowseModelFactory( - pluginManager, - Session, - assemblyConfigSchema, - ) + JBrowseModel = jbrowseModelFactory(pluginManager, assemblyConfigSchema) }) it('creates with empty snapshot', () => { diff --git a/products/jbrowse-web/src/jbrowseModel.ts b/products/jbrowse-web/src/jbrowseModel.ts index c4e1eac560..977a488cb3 100644 --- a/products/jbrowse-web/src/jbrowseModel.ts +++ b/products/jbrowse-web/src/jbrowseModel.ts @@ -19,7 +19,6 @@ import clone from 'clone' // locals import JBrowseConfigF from './jbrowseConfig' import RpcManager from '@jbrowse/core/rpc/RpcManager' -import { SessionStateModel } from './sessionModel' // poke some things for testing (this stuff will eventually be removed) // @ts-expect-error @@ -42,18 +41,11 @@ function removeAttr(obj: Record, attr: string) { * #stateModel JBrowseWebModel * the rootModel.jbrowse state model for JBrowse Web */ -function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars - export default function JBrowseWeb( pluginManager: PluginManager, - Session: SessionStateModel, assemblyConfigSchemasType: AnyConfigurationSchemaType, ) { - const JBrowseModel = JBrowseConfigF( - pluginManager, - assemblyConfigSchemasType, - Session, - ) + const JBrowseModel = JBrowseConfigF(pluginManager, assemblyConfigSchemasType) .views(self => ({ /** * #getter @@ -196,7 +188,6 @@ export default function JBrowseWeb( throw new Error(`unable to set default session to ${newDefault.name}`) } - // @ts-expect-error complains about name missing, but above line checks this self.defaultSession = cast(newDefault) }, /** diff --git a/products/jbrowse-web/src/rootModel/index.test.js b/products/jbrowse-web/src/rootModel/index.test.js index 1ff752027b..055dbba3e2 100644 --- a/products/jbrowse-web/src/rootModel/index.test.js +++ b/products/jbrowse-web/src/rootModel/index.test.js @@ -2,7 +2,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' import corePlugins from '../corePlugins' -import rootModelFactory from '.' +import rootModelFactory from './rootModel' jest.mock('../makeWorkerInstance', () => () => {}) describe('Root MST model', () => { diff --git a/products/jbrowse-web/src/rootModel/index.ts b/products/jbrowse-web/src/rootModel/index.ts index cbbe89530c..d61b1ec729 100644 --- a/products/jbrowse-web/src/rootModel/index.ts +++ b/products/jbrowse-web/src/rootModel/index.ts @@ -1,734 +1,3 @@ -import { lazy } from 'react' -import { - addDisposer, - cast, - getSnapshot, - getType, - types, - IAnyStateTreeNode, - SnapshotIn, -} from 'mobx-state-tree' - -import { saveAs } from 'file-saver' -import { observable, autorun } from 'mobx' -import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' -import PluginManager from '@jbrowse/core/PluginManager' -import RpcManager from '@jbrowse/core/rpc/RpcManager' -import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' -import TimeTraveller from '@jbrowse/core/util/TimeTraveller' -import { isModelWithAfterCreate } from '@jbrowse/core/util' -import { AbstractSessionModel, SessionWithWidgets } from '@jbrowse/core/util' -import { MenuItem } from '@jbrowse/core/ui' - -// icons -import AddIcon from '@mui/icons-material/Add' -import SettingsIcon from '@mui/icons-material/Settings' -import AppsIcon from '@mui/icons-material/Apps' -import FileCopyIcon from '@mui/icons-material/FileCopy' -import FolderOpenIcon from '@mui/icons-material/FolderOpen' -import GetAppIcon from '@mui/icons-material/GetApp' -import PublishIcon from '@mui/icons-material/Publish' -import ExtensionIcon from '@mui/icons-material/Extension' -import StorageIcon from '@mui/icons-material/Storage' -import SaveIcon from '@mui/icons-material/Save' -import UndoIcon from '@mui/icons-material/Undo' -import RedoIcon from '@mui/icons-material/Redo' -import { Cable } from '@jbrowse/core/ui/Icons' - -// other -import makeWorkerInstance from '../makeWorkerInstance' -import corePlugins from '../corePlugins' -import jbrowseWebFactory from '../jbrowseModel' -import sessionModelFactory from '../sessionModel' -import { filterSessionInPlace } from '../util' -import { RootModel as CoreRootModel } from '@jbrowse/product-core' - -const PreferencesDialog = lazy(() => import('../PreferencesDialog')) - -interface Menu { - label: string - menuItems: MenuItem[] -} - -/** - * #stateModel JBrowseWebRootModel - * note that many properties of the root model are available through the session, which - * may be preferable since using getSession() is better relied on than getRoot() - */ -export default function RootModel( - pluginManager: PluginManager, - adminMode = false, -) { - const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) - const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) - return types - .compose( - CoreRootModel.BaseRootModel( - pluginManager, - jbrowseWebFactory(pluginManager, Session, assemblyConfigSchema), - Session, - assemblyConfigSchema, - ), - CoreRootModel.InternetAccounts(pluginManager), - ) - .props({ - /** - * #property - */ - configPath: types.maybe(types.string), - /** - * #property - * used for undo/redo - */ - history: types.optional(TimeTraveller, { targetPath: '../session' }), - }) - .volatile(self => ({ - isAssemblyEditing: false, - isDefaultSessionEditing: false, - pluginsUpdated: false, - rpcManager: new RpcManager( - pluginManager, - self.jbrowse.configuration.rpc, - { - WebWorkerRpcDriver: { - makeWorkerInstance, - }, - MainThreadRpcDriver: {}, - }, - ), - savedSessionsVolatile: observable.map({}), - textSearchManager: new TextSearchManager(pluginManager), - error: undefined as unknown, - })) - .views(self => ({ - /** - * #getter - */ - get savedSessions() { - return [...self.savedSessionsVolatile.values()] - }, - /** - * #method - */ - localStorageId(name: string) { - return `localSaved-${name}-${self.configPath}` - }, - /** - * #getter - */ - get autosaveId() { - return `autosave-${self.configPath}` - }, - /** - * #getter - */ - get previousAutosaveId() { - return `previousAutosave-${self.configPath}` - }, - })) - .views(self => ({ - /** - * #getter - */ - get savedSessionNames() { - return self.savedSessions.map(session => session.name) - }, - /** - * #getter - */ - get currentSessionId() { - const locationUrl = new URL(window.location.href) - const params = new URLSearchParams(locationUrl.search) - return params?.get('session')?.split('local-')[1] - }, - })) - - .actions(self => { - const super_afterCreate = isModelWithAfterCreate(self) - ? self.afterCreate - : undefined - return { - afterCreate() { - if (super_afterCreate) { - super_afterCreate() - } - document.addEventListener('keydown', e => { - const cm = e.ctrlKey || e.metaKey - if ( - self.history.canRedo && - // ctrl+shift+z or cmd+shift+z - ((cm && e.shiftKey && e.code === 'KeyZ') || - // ctrl+y - (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) - ) { - self.history.redo() - } - if ( - self.history.canUndo && // ctrl+z or cmd+z - cm && - !e.shiftKey && - e.code === 'KeyZ' - ) { - self.history.undo() - } - }) - - for (const [key, val] of Object.entries(localStorage) - .filter(([key, _val]) => key.startsWith('localSaved-')) - .filter(([key]) => key.includes(self.configPath || 'undefined'))) { - try { - const { session } = JSON.parse(val) - self.savedSessionsVolatile.set(key, session) - } catch (e) { - console.error('bad session encountered', key, val) - } - } - addDisposer( - self, - autorun(() => { - for (const [, val] of self.savedSessionsVolatile.entries()) { - try { - const key = self.localStorageId(val.name) - localStorage.setItem(key, JSON.stringify({ session: val })) - } catch (e) { - // @ts-expect-error - if (e.code === '22' || e.code === '1024') { - alert( - 'Local storage is full! Please use the "Open sessions" panel to remove old sessions', - ) - } - } - } - }), - ) - - addDisposer( - self, - autorun(() => { - if (self.session) { - // we use a specific initialization routine after session is - // created to get it to start tracking itself sort of related - // issue here - // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 - self.history.initialize() - } - }), - ) - addDisposer( - self, - autorun( - () => { - if (self.session) { - const noSession = { name: 'empty' } - const snapshot = getSnapshot(self.session) || noSession - sessionStorage.setItem( - 'current', - JSON.stringify({ session: snapshot }), - ) - - localStorage.setItem( - `autosave-${self.configPath}`, - JSON.stringify({ - session: { - ...snapshot, - name: `${snapshot.name}-autosaved`, - }, - }), - ) - if (self.pluginsUpdated) { - // reload app to get a fresh plugin manager - window.location.reload() - } - } - }, - { delay: 400 }, - ), - ) - }, - /** - * #action - */ - setSession(sessionSnapshot?: SnapshotIn) { - const oldSession = self.session - self.session = cast(sessionSnapshot) - if (self.session) { - // validate all references in the session snapshot - try { - filterSessionInPlace(self.session, getType(self.session)) - } catch (error) { - // throws error if session filtering failed - self.session = oldSession - throw error - } - } - }, - /** - * #action - */ - setAssemblyEditing(flag: boolean) { - self.isAssemblyEditing = flag - }, - /** - * #action - */ - setDefaultSessionEditing(flag: boolean) { - self.isDefaultSessionEditing = flag - }, - /** - * #action - */ - setPluginsUpdated(flag: boolean) { - self.pluginsUpdated = flag - }, - /** - * #action - */ - setDefaultSession() { - const { defaultSession } = self.jbrowse - const newSession = { - ...defaultSession, - name: `${defaultSession.name} ${new Date().toLocaleString()}`, - } - - this.setSession(newSession) - }, - /** - * #action - */ - renameCurrentSession(sessionName: string) { - if (self.session) { - const snapshot = JSON.parse( - JSON.stringify(getSnapshot(self.session)), - ) - snapshot.name = sessionName - this.setSession(snapshot) - } - }, - /** - * #action - */ - addSavedSession(session: { name: string }) { - const key = self.localStorageId(session.name) - self.savedSessionsVolatile.set(key, session) - }, - /** - * #action - */ - removeSavedSession(session: { name: string }) { - const key = self.localStorageId(session.name) - localStorage.removeItem(key) - self.savedSessionsVolatile.delete(key) - }, - /** - * #action - */ - duplicateCurrentSession() { - if (self.session) { - const snapshot = JSON.parse( - JSON.stringify(getSnapshot(self.session)), - ) - let newSnapshotName = `${self.session.name} (copy)` - if (self.savedSessionNames.includes(newSnapshotName)) { - let newSnapshotCopyNumber = 2 - do { - newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` - newSnapshotCopyNumber += 1 - } while (self.savedSessionNames.includes(newSnapshotName)) - } - snapshot.name = newSnapshotName - this.setSession(snapshot) - } - }, - /** - * #action - */ - activateSession(name: string) { - const localId = self.localStorageId(name) - const newSessionSnapshot = localStorage.getItem(localId) - if (!newSessionSnapshot) { - throw new Error( - `Can't activate session ${name}, it is not in the savedSessions`, - ) - } - - this.setSession(JSON.parse(newSessionSnapshot).session) - }, - /** - * #action - */ - saveSessionToLocalStorage() { - if (self.session) { - const key = self.localStorageId(self.session.name) - self.savedSessionsVolatile.set(key, getSnapshot(self.session)) - } - }, - loadAutosaveSession() { - const previousAutosave = localStorage.getItem(self.previousAutosaveId) - const autosavedSession = previousAutosave - ? JSON.parse(previousAutosave).session - : {} - const { name } = autosavedSession - autosavedSession.name = `${name.replace('-autosaved', '')}-restored` - this.setSession(autosavedSession) - }, - /** - * #action - */ - setError(error?: unknown) { - self.error = error - }, - } - }) - .volatile(self => ({ - menus: [ - { - label: 'File', - menuItems: [ - { - label: 'New session', - icon: AddIcon, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onClick: (session: any) => { - const lastAutosave = localStorage.getItem(self.autosaveId) - if (lastAutosave) { - localStorage.setItem(self.previousAutosaveId, lastAutosave) - } - session.setDefaultSession() - }, - }, - { - label: 'Import session…', - icon: PublishIcon, - onClick: (session: SessionWithWidgets) => { - const widget = session.addWidget( - 'ImportSessionWidget', - 'importSessionWidget', - ) - session.showWidget(widget) - }, - }, - { - label: 'Export session', - icon: GetAppIcon, - onClick: (session: IAnyStateTreeNode) => { - const sessionBlob = new Blob( - [JSON.stringify({ session: getSnapshot(session) }, null, 2)], - { type: 'text/plain;charset=utf-8' }, - ) - saveAs(sessionBlob, 'session.json') - }, - }, - { - label: 'Open session…', - icon: FolderOpenIcon, - onClick: (session: SessionWithWidgets) => { - const widget = session.addWidget( - 'SessionManager', - 'sessionManager', - ) - session.showWidget(widget) - }, - }, - { - label: 'Save session', - icon: SaveIcon, - onClick: (session: SessionWithWidgets) => { - self.saveSessionToLocalStorage() - session.notify(`Saved session "${session.name}"`, 'success') - }, - }, - { - label: 'Duplicate session', - icon: FileCopyIcon, - onClick: (session: AbstractSessionModel) => { - if (session.duplicateCurrentSession) { - session.duplicateCurrentSession() - } - }, - }, - { type: 'divider' }, - { - label: 'Open track...', - icon: StorageIcon, - onClick: (session: SessionWithWidgets) => { - if (session.views.length === 0) { - session.notify('Please open a view to add a track first') - } else if (session.views.length > 0) { - 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: Cable, - onClick: (session: SessionWithWidgets) => { - const widget = session.addWidget( - 'AddConnectionWidget', - 'addConnectionWidget', - ) - session.showWidget(widget) - }, - }, - { type: 'divider' }, - { - label: 'Return to splash screen', - icon: AppsIcon, - onClick: () => self.setSession(undefined), - }, - ], - }, - ...(adminMode - ? [ - { - label: 'Admin', - menuItems: [ - { - label: 'Open assembly manager', - onClick: () => self.setAssemblyEditing(true), - }, - { - label: 'Set default session', - onClick: () => self.setDefaultSessionEditing(true), - }, - ], - }, - ] - : []), - { - label: 'Add', - menuItems: [], - }, - { - label: 'Tools', - menuItems: [ - { - label: 'Undo', - icon: UndoIcon, - onClick: () => { - if (self.history.canUndo) { - self.history.undo() - } - }, - }, - { - label: 'Redo', - icon: RedoIcon, - onClick: () => { - if (self.history.canRedo) { - self.history.redo() - } - }, - }, - { type: 'divider' }, - { - label: 'Plugin store', - icon: ExtensionIcon, - onClick: () => { - if (self.session) { - const widget = self.session.addWidget( - 'PluginStoreWidget', - 'pluginStoreWidget', - ) - self.session.showWidget(widget) - } - }, - }, - { - label: 'Preferences', - icon: SettingsIcon, - onClick: () => { - if (self.session) { - self.session.queueDialog(handleClose => [ - PreferencesDialog, - { - session: self.session, - handleClose, - }, - ]) - } - }, - }, - ], - }, - ] as Menu[], - adminMode, - })) - .actions(self => ({ - /** - * #action - */ - setMenus(newMenus: Menu[]) { - self.menus = newMenus - }, - /** - * #action - * Add a top-level menu - * @param menuName - Name of the menu to insert. - * @returns The new length of the top-level menus array - */ - appendMenu(menuName: string) { - return self.menus.push({ label: menuName, menuItems: [] }) - }, - /** - * #action - * Insert a top-level menu - * @param menuName - Name of the menu to insert. - * @param position - Position to insert menu. If negative, counts from th - * end, e.g. `insertMenu('My Menu', -1)` will insert the menu as the - * second-to-last one. - * @returns The new length of the top-level menus array - */ - insertMenu(menuName: string, position: number) { - self.menus.splice( - (position < 0 ? self.menus.length : 0) + position, - 0, - { label: menuName, menuItems: [] }, - ) - return self.menus.length - }, - /** - * #action - * Add a menu item to a top-level menu - * @param menuName - Name of the top-level menu to append to. - * @param menuItem - Menu item to append. - * @returns The new length of the menu - */ - appendToMenu(menuName: string, menuItem: MenuItem) { - const menu = self.menus.find(m => m.label === menuName) - if (!menu) { - self.menus.push({ label: menuName, menuItems: [menuItem] }) - return 1 - } - return menu.menuItems.push(menuItem) - }, - /** - * #action - * Insert a menu item into a top-level menu - * @param menuName - Name of the top-level menu to insert into - * @param menuItem - Menu item to insert - * @param position - Position to insert menu item. If negative, counts - * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as - * the second-to-last one. - * @returns The new length of the menu - */ - insertInMenu(menuName: string, menuItem: MenuItem, position: number) { - const menu = self.menus.find(m => m.label === menuName) - if (!menu) { - self.menus.push({ label: menuName, menuItems: [menuItem] }) - return 1 - } - const insertPosition = - position < 0 ? menu.menuItems.length + position : position - menu.menuItems.splice(insertPosition, 0, menuItem) - return menu.menuItems.length - }, - /** - * #action - * Add a menu item to a sub-menu - * @param menuPath - Path to the sub-menu to add to, starting with the - * top-level menu (e.g. `['File', 'Insert']`). - * @param menuItem - Menu item to append. - * @returns The new length of the sub-menu - */ - appendToSubMenu(menuPath: string[], menuItem: MenuItem) { - let topMenu = self.menus.find(m => m.label === menuPath[0]) - if (!topMenu) { - const idx = this.appendMenu(menuPath[0]) - topMenu = self.menus[idx - 1] - } - let { menuItems: subMenu } = topMenu - const pathSoFar = [menuPath[0]] - menuPath.slice(1).forEach(menuName => { - pathSoFar.push(menuName) - let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) - if (!sm) { - const idx = subMenu.push({ label: menuName, subMenu: [] }) - sm = subMenu[idx - 1] - } - if (!('subMenu' in sm)) { - throw new Error( - `"${menuName}" in path "${pathSoFar}" is not a subMenu`, - ) - } - subMenu = sm.subMenu - }) - return subMenu.push(menuItem) - }, - /** - * #action - * Insert a menu item into a sub-menu - * @param menuPath - Path to the sub-menu to add to, starting with the - * top-level menu (e.g. `['File', 'Insert']`). - * @param menuItem - Menu item to insert. - * @param position - Position to insert menu item. If negative, counts - * from the end, e.g. `insertMenu('My Menu', -1)` will insert the menu as - * the second-to-last one. - * @returns The new length of the sub-menu - */ - insertInSubMenu( - menuPath: string[], - menuItem: MenuItem, - position: number, - ) { - let topMenu = self.menus.find(m => m.label === menuPath[0]) - if (!topMenu) { - const idx = this.appendMenu(menuPath[0]) - topMenu = self.menus[idx - 1] - } - let { menuItems: subMenu } = topMenu - const pathSoFar = [menuPath[0]] - menuPath.slice(1).forEach(menuName => { - pathSoFar.push(menuName) - let sm = subMenu.find(mi => 'label' in mi && mi.label === menuName) - if (!sm) { - const idx = subMenu.push({ label: menuName, subMenu: [] }) - sm = subMenu[idx - 1] - } - if (!('subMenu' in sm)) { - throw new Error( - `"${menuName}" in path "${pathSoFar}" is not a subMenu`, - ) - } - subMenu = sm.subMenu - }) - subMenu.splice(position, 0, menuItem) - return subMenu.length - }, - })) -} - -export function createTestSession(snapshot = {}, adminMode = false) { - const pluginManager = new PluginManager(corePlugins.map(P => new P())) - pluginManager.createPluggableElements() - - const root = RootModel(pluginManager, adminMode).create( - { - jbrowse: { - configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, - }, - assemblyManager: {}, - }, - { pluginManager }, - ) - root.setSession({ - name: 'testSession', - ...snapshot, - }) - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const session = root.session! - session.views.map(view => view.setWidth(800)) - pluginManager.setRootModel(root) - pluginManager.configure() - return session -} +export default './rootModel' +export { createTestSession } from './test_util' +export type * from './rootModel' diff --git a/products/jbrowse-web/src/rootModel.ts b/products/jbrowse-web/src/rootModel/rootModel.ts similarity index 56% rename from products/jbrowse-web/src/rootModel.ts rename to products/jbrowse-web/src/rootModel/rootModel.ts index fc7e34354d..a8595dc728 100644 --- a/products/jbrowse-web/src/rootModel.ts +++ b/products/jbrowse-web/src/rootModel/rootModel.ts @@ -7,17 +7,18 @@ import { types, IAnyStateTreeNode, SnapshotIn, + Instance, + IAnyType, } from 'mobx-state-tree' import { saveAs } from 'file-saver' import { observable, autorun } from 'mobx' -import assemblyManagerFactory from '@jbrowse/core/assemblyManager' import assemblyConfigSchemaFactory from '@jbrowse/core/assemblyManager/assemblyConfigSchema' import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import TimeTraveller from '@jbrowse/core/util/TimeTraveller' -import { UriLocation } from '@jbrowse/core/util/types' +import { isModelWithAfterCreate } from '@jbrowse/core/util' import { AbstractSessionModel, SessionWithWidgets } from '@jbrowse/core/util' import { MenuItem } from '@jbrowse/core/ui' @@ -37,16 +38,19 @@ import RedoIcon from '@mui/icons-material/Redo' import { Cable } from '@jbrowse/core/ui/Icons' // other -import makeWorkerInstance from './makeWorkerInstance' -import corePlugins from './corePlugins' -import jbrowseWebFactory from './jbrowseModel' -import sessionModelFactory from './sessionModelFactory' -import { filterSessionInPlace } from './util' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import makeWorkerInstance from '../makeWorkerInstance' +import jbrowseWebFactory from '../jbrowseModel' +import { filterSessionInPlace } from '../util' +import { RootModel as CoreRootModel } from '@jbrowse/product-core' +import { + BaseSession, + BaseSessionType, +} from '@jbrowse/product-core/src/Session/Base' +import { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' -const PreferencesDialog = lazy(() => import('./PreferencesDialog')) +const PreferencesDialog = lazy(() => import('../PreferencesDialog')) -interface Menu { +export interface Menu { label: string menuItems: MenuItem[] } @@ -58,45 +62,28 @@ interface Menu { */ export default function RootModel( pluginManager: PluginManager, + sessionModelFactory: ( + p: PluginManager, + assemblyConfigSchema: ReturnType, + ) => IAnyType, adminMode = false, ) { const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) - const Session = sessionModelFactory(pluginManager, assemblyConfigSchema) - const AssemblyManager = assemblyManagerFactory( - assemblyConfigSchema, - pluginManager, - ) return types - .model('Root', { - /** - * #property - * `jbrowse` is a mapping of the config.json into the in-memory state tree - */ - jbrowse: jbrowseWebFactory(pluginManager, Session, assemblyConfigSchema), + .compose( + CoreRootModel.BaseRootModel( + pluginManager, + jbrowseWebFactory(pluginManager, assemblyConfigSchema), + sessionModelFactory(pluginManager, assemblyConfigSchema), + assemblyConfigSchema, + ), + CoreRootModel.InternetAccounts(pluginManager), + ) + .props({ /** * #property */ configPath: types.maybe(types.string), - /** - * #property - * `session` encompasses the currently active state of the app, including - * views open, tracks open in those views, etc. - */ - session: types.maybe(Session), - /** - * #property - */ - assemblyManager: types.optional(AssemblyManager, {}), - /** - * #property - */ - version: types.maybe(types.string), - /** - * #property - */ - internetAccounts: types.array( - pluginManager.pluggableMstType('internet account', 'stateModel'), - ), /** * #property * used for undo/redo @@ -164,286 +151,243 @@ export default function RootModel( }, })) - .actions(self => ({ - afterCreate() { - document.addEventListener('keydown', e => { - const cm = e.ctrlKey || e.metaKey - if ( - self.history.canRedo && - // ctrl+shift+z or cmd+shift+z - ((cm && e.shiftKey && e.code === 'KeyZ') || - // ctrl+y - (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) - ) { - self.history.redo() + .actions(self => { + const super_afterCreate = isModelWithAfterCreate(self) + ? self.afterCreate + : undefined + return { + afterCreate() { + if (super_afterCreate) { + super_afterCreate() } - if ( - self.history.canUndo && // ctrl+z or cmd+z - cm && - !e.shiftKey && - e.code === 'KeyZ' - ) { - self.history.undo() - } - }) + document.addEventListener('keydown', e => { + const cm = e.ctrlKey || e.metaKey + if ( + self.history.canRedo && + // ctrl+shift+z or cmd+shift+z + ((cm && e.shiftKey && e.code === 'KeyZ') || + // ctrl+y + (e.ctrlKey && !e.shiftKey && e.code === 'KeyY')) + ) { + self.history.redo() + } + if ( + self.history.canUndo && // ctrl+z or cmd+z + cm && + !e.shiftKey && + e.code === 'KeyZ' + ) { + self.history.undo() + } + }) - for (const [key, val] of Object.entries(localStorage) - .filter(([key, _val]) => key.startsWith('localSaved-')) - .filter(([key]) => key.includes(self.configPath || 'undefined'))) { - try { - const { session } = JSON.parse(val) - self.savedSessionsVolatile.set(key, session) - } catch (e) { - console.error('bad session encountered', key, val) + for (const [key, val] of Object.entries(localStorage) + .filter(([key, _val]) => key.startsWith('localSaved-')) + .filter(([key]) => key.includes(self.configPath || 'undefined'))) { + try { + const { session } = JSON.parse(val) + self.savedSessionsVolatile.set(key, session) + } catch (e) { + console.error('bad session encountered', key, val) + } } - } - addDisposer( - self, - autorun(() => { - for (const [, val] of self.savedSessionsVolatile.entries()) { - try { - const key = self.localStorageId(val.name) - localStorage.setItem(key, JSON.stringify({ session: val })) - } catch (e) { - // @ts-expect-error - if (e.code === '22' || e.code === '1024') { - alert( - 'Local storage is full! Please use the "Open sessions" panel to remove old sessions', - ) + addDisposer( + self, + autorun(() => { + for (const [, val] of self.savedSessionsVolatile.entries()) { + try { + const key = self.localStorageId(val.name) + localStorage.setItem(key, JSON.stringify({ session: val })) + } catch (e) { + // @ts-expect-error + if (e.code === '22' || e.code === '1024') { + alert( + 'Local storage is full! Please use the "Open sessions" panel to remove old sessions', + ) + } } } - } - }), - ) + }), + ) - addDisposer( - self, - autorun(() => { - if (self.session) { - // we use a specific initialization routine after session is - // created to get it to start tracking itself sort of related - // issue here - // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 - self.history.initialize() - } - }), - ) - addDisposer( - self, - autorun( - () => { + addDisposer( + self, + autorun(() => { if (self.session) { - const noSession = { name: 'empty' } - const snapshot = getSnapshot(self.session) || noSession - sessionStorage.setItem( - 'current', - JSON.stringify({ session: snapshot }), - ) - - localStorage.setItem( - `autosave-${self.configPath}`, - JSON.stringify({ - session: { - ...snapshot, - name: `${snapshot.name}-autosaved`, - }, - }), - ) - if (self.pluginsUpdated) { - // reload app to get a fresh plugin manager - window.location.reload() - } + // we use a specific initialization routine after session is + // created to get it to start tracking itself sort of related + // issue here + // https://github.com/mobxjs/mobx-state-tree/issues/1089#issuecomment-441207911 + self.history.initialize() } - }, - { delay: 400 }, - ), - ) - addDisposer( - self, - autorun(() => { - self.jbrowse.internetAccounts.forEach(account => { - this.initializeInternetAccount(account) - }) - }), - ) - }, - /** - * #action - */ - setSession(sessionSnapshot?: SnapshotIn) { - const oldSession = self.session - self.session = cast(sessionSnapshot) - if (self.session) { - // validate all references in the session snapshot - try { - filterSessionInPlace(self.session, getType(self.session)) - } catch (error) { - // throws error if session filtering failed - self.session = oldSession - throw error - } - } - }, - initializeInternetAccount( - internetAccountConfig: AnyConfigurationModel, - initialSnapshot = {}, - ) { - const internetAccountType = pluginManager.getInternetAccountType( - internetAccountConfig.type, - ) - if (!internetAccountType) { - throw new Error( - `unknown internet account type ${internetAccountConfig.type}`, + }), ) - } - - const length = self.internetAccounts.push({ - ...initialSnapshot, - type: internetAccountConfig.type, - configuration: internetAccountConfig, - }) - return self.internetAccounts[length - 1] - }, - createEphemeralInternetAccount( - internetAccountId: string, - initialSnapshot = {}, - url: string, - ) { - let hostUri - - try { - hostUri = new URL(url).origin - } catch (e) { - // ignore - } - // id of a custom new internaccount is `${type}-${name}` - const internetAccountSplit = internetAccountId.split('-') - const configuration = { - type: internetAccountSplit[0], - internetAccountId: internetAccountId, - name: internetAccountSplit.slice(1).join('-'), - description: '', - domains: hostUri ? [hostUri] : [], - } - const internetAccountType = pluginManager.getInternetAccountType( - configuration.type, - ) - const internetAccount = internetAccountType.stateModel.create({ - ...initialSnapshot, - type: configuration.type, - configuration, - }) - self.internetAccounts.push(internetAccount) - return internetAccount - }, - setAssemblyEditing(flag: boolean) { - self.isAssemblyEditing = flag - }, - setDefaultSessionEditing(flag: boolean) { - self.isDefaultSessionEditing = flag - }, - setPluginsUpdated(flag: boolean) { - self.pluginsUpdated = flag - }, - setDefaultSession() { - const { defaultSession } = self.jbrowse - const newSession = { - ...defaultSession, - name: `${defaultSession.name} ${new Date().toLocaleString()}`, - } - - this.setSession(newSession) - }, - renameCurrentSession(sessionName: string) { - if (self.session) { - const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) - snapshot.name = sessionName - this.setSession(snapshot) - } - }, - - addSavedSession(session: { name: string }) { - const key = self.localStorageId(session.name) - self.savedSessionsVolatile.set(key, session) - }, - - removeSavedSession(session: { name: string }) { - const key = self.localStorageId(session.name) - localStorage.removeItem(key) - self.savedSessionsVolatile.delete(key) - }, + addDisposer( + self, + autorun( + () => { + if (self.session) { + const noSession = { name: 'empty' } + const snapshot = + getSnapshot(self.session as BaseSession) || noSession + sessionStorage.setItem( + 'current', + JSON.stringify({ session: snapshot }), + ) - duplicateCurrentSession() { - if (self.session) { - const snapshot = JSON.parse(JSON.stringify(getSnapshot(self.session))) - let newSnapshotName = `${self.session.name} (copy)` - if (self.savedSessionNames.includes(newSnapshotName)) { - let newSnapshotCopyNumber = 2 - do { - newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` - newSnapshotCopyNumber += 1 - } while (self.savedSessionNames.includes(newSnapshotName)) - } - snapshot.name = newSnapshotName - this.setSession(snapshot) - } - }, - activateSession(name: string) { - const localId = self.localStorageId(name) - const newSessionSnapshot = localStorage.getItem(localId) - if (!newSessionSnapshot) { - throw new Error( - `Can't activate session ${name}, it is not in the savedSessions`, + localStorage.setItem( + `autosave-${self.configPath}`, + JSON.stringify({ + session: { + ...snapshot, + name: `${snapshot.name}-autosaved`, + }, + }), + ) + if (self.pluginsUpdated) { + // reload app to get a fresh plugin manager + window.location.reload() + } + } + }, + { delay: 400 }, + ), ) - } - - this.setSession(JSON.parse(newSessionSnapshot).session) - }, - saveSessionToLocalStorage() { - if (self.session) { - const key = self.localStorageId(self.session.name) - self.savedSessionsVolatile.set(key, getSnapshot(self.session)) - } - }, - loadAutosaveSession() { - const previousAutosave = localStorage.getItem(self.previousAutosaveId) - const autosavedSession = previousAutosave - ? JSON.parse(previousAutosave).session - : {} - const { name } = autosavedSession - autosavedSession.name = `${name.replace('-autosaved', '')}-restored` - this.setSession(autosavedSession) - }, - - setError(error?: unknown) { - self.error = error - }, - findAppropriateInternetAccount(location: UriLocation) { - // find the existing account selected from menu - const selectedId = location.internetAccountId - if (selectedId) { - const selectedAccount = self.internetAccounts.find(account => { - return account.internetAccountId === selectedId - }) - if (selectedAccount) { - return selectedAccount + }, + /** + * #action + */ + setSession(sessionSnapshot?: SnapshotIn) { + const oldSession = self.session + self.session = cast(sessionSnapshot) + if (self.session) { + // validate all references in the session snapshot + try { + filterSessionInPlace(self.session, getType(self.session)) + } catch (error) { + // throws error if session filtering failed + self.session = oldSession + throw error + } + } + }, + /** + * #action + */ + setAssemblyEditing(flag: boolean) { + self.isAssemblyEditing = flag + }, + /** + * #action + */ + setDefaultSessionEditing(flag: boolean) { + self.isDefaultSessionEditing = flag + }, + /** + * #action + */ + setPluginsUpdated(flag: boolean) { + self.pluginsUpdated = flag + }, + /** + * #action + */ + setDefaultSession() { + const { defaultSession } = self.jbrowse + const newSession = { + ...defaultSession, + name: `${defaultSession.name} ${new Date().toLocaleString()}`, } - } - // if no existing account or not found, try to find working account - for (const account of self.internetAccounts) { - const handleResult = account.handlesLocation(location) - if (handleResult) { - return account + this.setSession(newSession) + }, + /** + * #action + */ + renameCurrentSession(sessionName: string) { + if (self.session) { + const snapshot = JSON.parse( + JSON.stringify(getSnapshot(self.session)), + ) + snapshot.name = sessionName + this.setSession(snapshot) + } + }, + /** + * #action + */ + addSavedSession(session: { name: string }) { + const key = self.localStorageId(session.name) + self.savedSessionsVolatile.set(key, session) + }, + /** + * #action + */ + removeSavedSession(session: { name: string }) { + const key = self.localStorageId(session.name) + localStorage.removeItem(key) + self.savedSessionsVolatile.delete(key) + }, + /** + * #action + */ + duplicateCurrentSession() { + if (self.session) { + const snapshot = JSON.parse( + JSON.stringify(getSnapshot(self.session)), + ) + let newSnapshotName = `${self.session.name} (copy)` + if (self.savedSessionNames.includes(newSnapshotName)) { + let newSnapshotCopyNumber = 2 + do { + newSnapshotName = `${self.session.name} (copy ${newSnapshotCopyNumber})` + newSnapshotCopyNumber += 1 + } while (self.savedSessionNames.includes(newSnapshotName)) + } + snapshot.name = newSnapshotName + this.setSession(snapshot) + } + }, + /** + * #action + */ + activateSession(name: string) { + const localId = self.localStorageId(name) + const newSessionSnapshot = localStorage.getItem(localId) + if (!newSessionSnapshot) { + throw new Error( + `Can't activate session ${name}, it is not in the savedSessions`, + ) } - } - // if still no existing account, create ephemeral config to use - return selectedId - ? this.createEphemeralInternetAccount(selectedId, {}, location.uri) - : null - }, - })) + this.setSession(JSON.parse(newSessionSnapshot).session) + }, + /** + * #action + */ + saveSessionToLocalStorage() { + if (self.session) { + const key = self.localStorageId(self.session.name) + self.savedSessionsVolatile.set(key, getSnapshot(self.session)) + } + }, + loadAutosaveSession() { + const previousAutosave = localStorage.getItem(self.previousAutosaveId) + const autosavedSession = previousAutosave + ? JSON.parse(previousAutosave).session + : {} + const { name } = autosavedSession + autosavedSession.name = `${name.replace('-autosaved', '')}-restored` + this.setSession(autosavedSession) + }, + /** + * #action + */ + setError(error?: unknown) { + self.error = error + }, + } + }) .volatile(self => ({ menus: [ { @@ -613,13 +557,15 @@ export default function RootModel( icon: SettingsIcon, onClick: () => { if (self.session) { - self.session.queueDialog(handleClose => [ - PreferencesDialog, - { - session: self.session, - handleClose, - }, - ]) + ;(self.session as DialogQueueManager).queueDialog( + handleClose => [ + PreferencesDialog, + { + session: self.session, + handleClose, + }, + ], + ) } }, }, @@ -772,28 +718,5 @@ export default function RootModel( })) } -export function createTestSession(snapshot = {}, adminMode = false) { - const pluginManager = new PluginManager(corePlugins.map(P => new P())) - pluginManager.createPluggableElements() - - const root = RootModel(pluginManager, adminMode).create( - { - jbrowse: { - configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, - }, - assemblyManager: {}, - }, - { pluginManager }, - ) - root.setSession({ - name: 'testSession', - ...snapshot, - }) - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const session = root.session! - session.views.map(view => view.setWidth(800)) - pluginManager.setRootModel(root) - pluginManager.configure() - return session -} +export type WebRootModelType = ReturnType +export type WebRootModel = Instance diff --git a/products/jbrowse-web/src/rootModel/test_util.ts b/products/jbrowse-web/src/rootModel/test_util.ts new file mode 100644 index 0000000000..1c9f6c5ace --- /dev/null +++ b/products/jbrowse-web/src/rootModel/test_util.ts @@ -0,0 +1,30 @@ +import PluginManager from '@jbrowse/core/PluginManager' +import corePlugins from '../corePlugins' +import RootModel from './rootModel' +import sessionModelFactory, { WebSessionModel } from '../sessionModel' + +export function createTestSession(snapshot = {}, adminMode = false) { + const pluginManager = new PluginManager(corePlugins.map(P => new P())) + pluginManager.createPluggableElements() + + const root = RootModel(pluginManager, sessionModelFactory, adminMode).create( + { + jbrowse: { + configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, + }, + assemblyManager: {}, + }, + { pluginManager }, + ) + root.setSession({ + name: 'testSession', + ...snapshot, + }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const session = root.session as WebSessionModel + session.views.map(view => view.setWidth(800)) + pluginManager.setRootModel(root) + pluginManager.configure() + return session +} diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 4ce64cf4a5..8c0a8bd05e 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -11,7 +11,6 @@ import { addDisposer, cast, getParent, - getRoot, getSnapshot, types, Instance, @@ -31,6 +30,7 @@ import InfoIcon from '@mui/icons-material/Info' import Assemblies from './Assemblies' import SessionConnections from './SessionConnections' import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' +import { WebRootModel } from '../rootModel/rootModel' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) @@ -94,6 +94,9 @@ export default function sessionModelFactory( task: undefined, })) .views(self => ({ + get root() { + return getParent(self) + }, /** * #getter */ @@ -104,43 +107,43 @@ export default function sessionModelFactory( * #getter */ get textSearchManager(): TextSearchManager { - return self.root.textSearchManager + return this.root.textSearchManager }, /** * #getter */ get savedSessions() { - return self.root.savedSessions + return this.root.savedSessions }, /** * #getter */ get previousAutosaveId() { - return self.root.previousAutosaveId + return this.root.previousAutosaveId }, /** * #getter */ get savedSessionNames() { - return self.root.savedSessionNames + return this.root.savedSessionNames }, /** * #getter */ get history() { - return self.root.history + return this.root.history }, /** * #getter */ get menus() { - return self.root.menus + return this.root.menus }, /** * #getter */ get version() { - return self.root.version + return this.root.version }, /** @@ -161,7 +164,7 @@ export default function sessionModelFactory( throw new Error('session plugin cannot be installed twice') } self.sessionPlugins.push(plugin) - getRoot(self).setPluginsUpdated(true) + self.root.setPluginsUpdated(true) }, /** @@ -332,10 +335,8 @@ export default function sessionModelFactory( ) as typeof sessionModel return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { - // @ts-expect-error preProcessor(snapshot) { if (snapshot) { - // @ts-expect-error const { connectionInstances, ...rest } = snapshot || {} // connectionInstances schema changed from object to an array, so any // old connectionInstances as object is in snapshot, filter it out @@ -349,11 +350,11 @@ export default function sessionModelFactory( }) } -export type SessionStateModel = ReturnType -export type SessionModel = Instance +export type WebSessionModelType = ReturnType +export type WebSessionModel = Instance // eslint-disable-next-line @typescript-eslint/no-unused-vars -function z(x: Instance): AbstractSessionModel { +function z(x: Instance): AbstractSessionModel { // this function's sole purpose is to get typescript to check // that the session model implements all of AbstractSessionModel return x diff --git a/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js index b73246f2a3..07d8a0d7ca 100644 --- a/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js +++ b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js @@ -2,7 +2,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' import { configure } from 'mobx' -import { createTestSession } from '../rootModel' +import { createTestSession } from '../rootModel/rootModel' import sessionModelFactory from '.' jest.mock('../makeWorkerInstance', () => () => {}) diff --git a/products/jbrowse-web/src/tests/JBrowse.test.tsx b/products/jbrowse-web/src/tests/JBrowse.test.tsx index 26d414b845..c5c3153bbb 100644 --- a/products/jbrowse-web/src/tests/JBrowse.test.tsx +++ b/products/jbrowse-web/src/tests/JBrowse.test.tsx @@ -5,12 +5,13 @@ import { readConfObject, getConf } from '@jbrowse/core/configuration' import PluginManager from '@jbrowse/core/PluginManager' // locals -import JBrowseRootModelFactory from '../rootModel' +import JBrowseRootModelFactory from '../rootModel/rootModel' import corePlugins from '../corePlugins' import * as sessionSharing from '../sessionSharing' import volvoxConfigSnapshot from '../../test_data/volvox/config.json' import { doBeforeEach, setup, createView, hts } from './util' import TestPlugin from './TestPlugin' +import sessionModelFactory from '../sessionModel' jest.mock('../makeWorkerInstance', () => () => {}) @@ -45,7 +46,11 @@ test('lollipop track test', async () => { test('toplevel configuration', () => { const pm = new PluginManager([...corePlugins, TestPlugin].map(P => new P())) pm.createPluggableElements() - const rootModel = JBrowseRootModelFactory(pm, true).create({ + const rootModel = JBrowseRootModelFactory( + pm, + sessionModelFactory, + true, + ).create({ jbrowse: volvoxConfigSnapshot, assemblyManager: {}, }) diff --git a/products/jbrowse-web/src/tests/util.tsx b/products/jbrowse-web/src/tests/util.tsx index 4a6a295711..8d0edbbf8c 100644 --- a/products/jbrowse-web/src/tests/util.tsx +++ b/products/jbrowse-web/src/tests/util.tsx @@ -16,7 +16,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import { QueryParamProvider } from 'use-query-params' import JBrowseWithoutQueryParamProvider from '../JBrowse' -import JBrowseRootModelFactory from '../rootModel' +import JBrowseRootModelFactory from '../rootModel/rootModel' import configSnapshot from '../../test_data/volvox/config.json' import corePlugins from '../corePlugins' @@ -25,6 +25,7 @@ import { Image, createCanvas } from 'canvas' import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' import { WindowHistoryAdapter } from 'use-query-params/adapters/window' +import sessionModelFactory from '../sessionModel' type LGV = LinearGenomeViewModel @@ -40,7 +41,11 @@ export function getPluginManager(initialState?: any, adminMode = true) { const pluginManager = new PluginManager(corePlugins.map(P => new P())) pluginManager.createPluggableElements() - const JBrowseRootModel = JBrowseRootModelFactory(pluginManager, adminMode) + const JBrowseRootModel = JBrowseRootModelFactory( + pluginManager, + sessionModelFactory, + adminMode, + ) const rootModel = JBrowseRootModel.create( { jbrowse: initialState || configSnapshot, From bdcece8aaf8a90c223650eb5a322ee69ae02ea6e Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 6 May 2023 09:03:51 -0700 Subject: [PATCH 31/44] fix last ts errors --- packages/product-core/src/RootModel/InternetAccounts.ts | 4 +--- .../src/createModel/createSessionModel.ts | 6 ------ .../src/createModel/createSessionModel.ts | 6 ------ products/jbrowse-web/src/sessionModel/index.ts | 2 ++ 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/product-core/src/RootModel/InternetAccounts.ts b/packages/product-core/src/RootModel/InternetAccounts.ts index a54c9079b1..573dc42095 100644 --- a/packages/product-core/src/RootModel/InternetAccounts.ts +++ b/packages/product-core/src/RootModel/InternetAccounts.ts @@ -118,9 +118,7 @@ export default function InternetAccountsF(pluginManager: PluginManager) { autorun(() => { const { jbrowse } = self as typeof self & Instance - jbrowse.internetAccounts.forEach(account => { - self.initializeInternetAccount(account) - }) + jbrowse.internetAccounts.forEach(self.initializeInternetAccount) }), ) }, diff --git a/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts b/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts index d7ffcf5bd9..5b88e958c4 100644 --- a/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts +++ b/products/jbrowse-react-circular-genome-view/src/createModel/createSessionModel.ts @@ -38,12 +38,6 @@ export default function sessionModelFactory(pluginManager: PluginManager) { task: undefined, })) .views(self => ({ - /** - * #getter - */ - get jbrowse() { - return self.root.config - }, /** * #getter */ diff --git a/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts b/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts index ba1e431850..b8190786b1 100644 --- a/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts +++ b/products/jbrowse-react-linear-genome-view/src/createModel/createSessionModel.ts @@ -43,12 +43,6 @@ export default function sessionModelFactory(pluginManager: PluginManager) { ), }) .views(self => ({ - /** - * #getter - */ - get jbrowse() { - return self.root.config - }, /** * #getter */ diff --git a/products/jbrowse-web/src/sessionModel/index.ts b/products/jbrowse-web/src/sessionModel/index.ts index 8c0a8bd05e..f3ab6a8cb4 100644 --- a/products/jbrowse-web/src/sessionModel/index.ts +++ b/products/jbrowse-web/src/sessionModel/index.ts @@ -335,8 +335,10 @@ export default function sessionModelFactory( ) as typeof sessionModel return types.snapshotProcessor(addSnackbarToModel(extendedSessionModel), { + // @ts-expect-error preProcessor(snapshot) { if (snapshot) { + // @ts-expect-error const { connectionInstances, ...rest } = snapshot || {} // connectionInstances schema changed from object to an array, so any // old connectionInstances as object is in snapshot, filter it out From b9b8704bcffd5dafd0757bd1341d9ee4dade5056 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 6 May 2023 09:11:09 -0700 Subject: [PATCH 32/44] fix lint --- .../pluggableElementTypes/models/index.ts | 5 ++++- .../product-core/src/Session/DialogQueue.ts | 1 - .../product-core/src/Session/DrawerWidgets.ts | 21 +++++++++---------- .../src/sessionModel/Assemblies.ts | 4 ++-- products/jbrowse-web/src/Loader.tsx | 1 - .../jbrowse-web/src/rootModel/test_util.ts | 1 - 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/core/pluggableElementTypes/models/index.ts b/packages/core/pluggableElementTypes/models/index.ts index 2930bac85d..4c2c31d861 100644 --- a/packages/core/pluggableElementTypes/models/index.ts +++ b/packages/core/pluggableElementTypes/models/index.ts @@ -16,4 +16,7 @@ export { BaseInternetAccountConfig } from './baseInternetAccountConfig' export { createBaseTrackModel } from './BaseTrackModel' export type { BaseTrackModel, BaseTrackStateModel } from './BaseTrackModel' export { createBaseTrackConfig } from './baseTrackConfig' -export type { BaseTrackConfig, BaseTrackConfigSchema as BaseTrackConfigModel } from './baseTrackConfig' +export type { + BaseTrackConfig, + BaseTrackConfigSchema as BaseTrackConfigModel, +} from './baseTrackConfig' diff --git a/packages/product-core/src/Session/DialogQueue.ts b/packages/product-core/src/Session/DialogQueue.ts index 1987acf250..bf723f846b 100644 --- a/packages/product-core/src/Session/DialogQueue.ts +++ b/packages/product-core/src/Session/DialogQueue.ts @@ -2,7 +2,6 @@ import PluginManager from '@jbrowse/core/PluginManager' import { DialogComponentType } from '@jbrowse/core/util' -import { observable } from 'mobx' import { IAnyStateTreeNode, Instance, types } from 'mobx-state-tree' export interface ReferringNode { diff --git a/packages/product-core/src/Session/DrawerWidgets.ts b/packages/product-core/src/Session/DrawerWidgets.ts index c342b541b3..bfc09e8223 100644 --- a/packages/product-core/src/Session/DrawerWidgets.ts +++ b/packages/product-core/src/Session/DrawerWidgets.ts @@ -11,6 +11,11 @@ import { const minDrawerWidth = 128 export default function DrawerWidgets(pluginManager: PluginManager) { + const widgetStateModelType = pluginManager.pluggableMstType( + 'widget', + 'stateModel', + ) + type WidgetStateModel = Instance return types .model({ /** @@ -30,17 +35,11 @@ export default function DrawerWidgets(pluginManager: PluginManager) { /** * #property */ - widgets: types.map( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), + widgets: types.map(widgetStateModelType), /** * #property */ - activeWidgets: types.map( - types.safeReference( - pluginManager.pluggableMstType('widget', 'stateModel'), - ), - ), + activeWidgets: types.map(types.safeReference(widgetStateModelType)), /** * #property @@ -122,7 +121,7 @@ export default function DrawerWidgets(pluginManager: PluginManager) { /** * #action */ - showWidget(widget: any) { + showWidget(widget: WidgetStateModel) { if (self.activeWidgets.has(widget.id)) { self.activeWidgets.delete(widget.id) } @@ -133,14 +132,14 @@ export default function DrawerWidgets(pluginManager: PluginManager) { /** * #action */ - hasWidget(widget: any) { + hasWidget(widget: WidgetStateModel) { return self.activeWidgets.has(widget.id) }, /** * #action */ - hideWidget(widget: any) { + hideWidget(widget: WidgetStateModel) { self.activeWidgets.delete(widget.id) }, diff --git a/products/jbrowse-desktop/src/sessionModel/Assemblies.ts b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts index ff41749f41..3ff73dfc76 100644 --- a/products/jbrowse-desktop/src/sessionModel/Assemblies.ts +++ b/products/jbrowse-desktop/src/sessionModel/Assemblies.ts @@ -1,4 +1,4 @@ -import { Instance, getParent, types } from 'mobx-state-tree' +import { Instance, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfigurationModel } from '@jbrowse/core/configuration' @@ -84,7 +84,7 @@ export default function Assemblies( /** * #action */ - addAssemblyConf(assemblyConf: any) { + addAssemblyConf(assemblyConf: AnyConfigurationModel) { return self.jbrowse.addAssemblyConf(assemblyConf) }, })) diff --git a/products/jbrowse-web/src/Loader.tsx b/products/jbrowse-web/src/Loader.tsx index b64afb3ff7..4b7f9a309b 100644 --- a/products/jbrowse-web/src/Loader.tsx +++ b/products/jbrowse-web/src/Loader.tsx @@ -308,7 +308,6 @@ const Renderer = observer( try { if (sessionError) { rootModel.setDefaultSession() - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion rootModel.session.notify( `Error loading session: ${sessionError}. If you received this URL from another user, request that they send you diff --git a/products/jbrowse-web/src/rootModel/test_util.ts b/products/jbrowse-web/src/rootModel/test_util.ts index 1c9f6c5ace..bbca0ae0ca 100644 --- a/products/jbrowse-web/src/rootModel/test_util.ts +++ b/products/jbrowse-web/src/rootModel/test_util.ts @@ -21,7 +21,6 @@ export function createTestSession(snapshot = {}, adminMode = false) { ...snapshot, }) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const session = root.session as WebSessionModel session.views.map(view => view.setWidth(800)) pluginManager.setRootModel(root) From b5a0b642487c3eacacf2bc4cf72d1e5609b9fd39 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 6 May 2023 09:16:00 -0700 Subject: [PATCH 33/44] ts-ify web rootmodel test --- .eslintrc.json | 3 ++- .../{index.test.js => index.test.ts} | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) rename products/jbrowse-web/src/rootModel/{index.test.js => index.test.ts} (90%) diff --git a/.eslintrc.json b/.eslintrc.json index 3ad5644dfb..434cdb49ef 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -154,7 +154,8 @@ "test": true }, "rules": { - "import/no-extraneous-dependencies": "off" + "import/no-extraneous-dependencies": "off", + "@typescript-eslint/no-non-null-assertion": "off" } }, { diff --git a/products/jbrowse-web/src/rootModel/index.test.js b/products/jbrowse-web/src/rootModel/index.test.ts similarity index 90% rename from products/jbrowse-web/src/rootModel/index.test.js rename to products/jbrowse-web/src/rootModel/index.test.ts index 055dbba3e2..f13a4e1e68 100644 --- a/products/jbrowse-web/src/rootModel/index.test.js +++ b/products/jbrowse-web/src/rootModel/index.test.ts @@ -2,17 +2,18 @@ import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' import corePlugins from '../corePlugins' -import rootModelFactory from './rootModel' +import rootModelFactory, { WebRootModelType } from './rootModel' +import sessionModelFactory from '../sessionModel' jest.mock('../makeWorkerInstance', () => () => {}) describe('Root MST model', () => { - let rootModel + let rootModel: WebRootModelType | undefined beforeAll(() => { const pluginManager = new PluginManager(corePlugins.map(P => new P())) pluginManager.createPluggableElements() pluginManager.configure() - rootModel = rootModelFactory(pluginManager) + rootModel = rootModelFactory(pluginManager, sessionModelFactory) }) afterEach(() => { @@ -21,7 +22,7 @@ describe('Root MST model', () => { }) it('creates with defaults', () => { - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, @@ -35,7 +36,7 @@ describe('Root MST model', () => { }) it('creates with a minimal session', () => { - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, @@ -51,7 +52,7 @@ describe('Root MST model', () => { Storage.prototype.getItem = jest.fn( () => `{"session": {"name": "testSession"}}`, ) - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, @@ -63,7 +64,7 @@ describe('Root MST model', () => { }) it('adds track and connection configs to an assembly', () => { - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, assemblies: [ @@ -110,7 +111,7 @@ describe('Root MST model', () => { it('throws if session is invalid', () => { expect(() => - rootModel.create({ + rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, @@ -121,7 +122,7 @@ describe('Root MST model', () => { }) it('throws if session snapshot is invalid', () => { - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, @@ -131,7 +132,7 @@ describe('Root MST model', () => { }) it('adds menus', () => { - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, From 801b8cb58ac4f059a914fb3919a33c9b8ca9ce14 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sat, 6 May 2023 09:21:23 -0700 Subject: [PATCH 34/44] rootmodel tests fixed --- ...{index.test.js.snap => index.test.ts.snap} | 0 .../{index.test.js => index.test.ts} | 13 +- .../__snapshots__/index.test.ts.snap | 453 ++++++++++++++++++ 3 files changed, 460 insertions(+), 6 deletions(-) rename products/jbrowse-desktop/src/rootModel/__snapshots__/{index.test.js.snap => index.test.ts.snap} (100%) rename products/jbrowse-desktop/src/rootModel/{index.test.js => index.test.ts} (86%) create mode 100644 products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap diff --git a/products/jbrowse-desktop/src/rootModel/__snapshots__/index.test.js.snap b/products/jbrowse-desktop/src/rootModel/__snapshots__/index.test.ts.snap similarity index 100% rename from products/jbrowse-desktop/src/rootModel/__snapshots__/index.test.js.snap rename to products/jbrowse-desktop/src/rootModel/__snapshots__/index.test.ts.snap diff --git a/products/jbrowse-desktop/src/rootModel/index.test.js b/products/jbrowse-desktop/src/rootModel/index.test.ts similarity index 86% rename from products/jbrowse-desktop/src/rootModel/index.test.js rename to products/jbrowse-desktop/src/rootModel/index.test.ts index 460588f028..0404c96437 100644 --- a/products/jbrowse-desktop/src/rootModel/index.test.js +++ b/products/jbrowse-desktop/src/rootModel/index.test.ts @@ -1,25 +1,26 @@ // import electron first, important, because the electron mock creates // window.require // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars -import electron from 'electron' +import 'electron' import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' import corePlugins from '../corePlugins' -import rootModelFactory from '.' +import rootModelFactory, { DesktopRootModelType } from '.' +import sessionModelFactory from '../sessionModel' jest.mock('../makeWorkerInstance', () => () => {}) describe('Root MST model', () => { - let rootModel + let rootModel: DesktopRootModelType | undefined beforeAll(() => { const pluginManager = new PluginManager(corePlugins.map(P => new P())) pluginManager.createPluggableElements() pluginManager.configure() - rootModel = rootModelFactory(pluginManager) + rootModel = rootModelFactory(pluginManager, sessionModelFactory) }) it('creates with defaults', () => { - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, @@ -33,7 +34,7 @@ describe('Root MST model', () => { }) it('adds menus', () => { - const root = rootModel.create({ + const root = rootModel!.create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, }, diff --git a/products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap b/products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000000..80662756d7 --- /dev/null +++ b/products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap @@ -0,0 +1,453 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Root MST model adds menus 1`] = ` +[ + { + "label": "File", + "menuItems": [ + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "New session", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Import session…", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Export session", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Open session…", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Save session", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Duplicate session", + "onClick": [Function], + }, + { + "type": "divider", + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Open track...", + "onClick": [Function], + }, + { + "icon": [Function], + "label": "Open connection...", + "onClick": [Function], + }, + { + "type": "divider", + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Return to splash screen", + "onClick": [Function], + }, + ], + }, + { + "label": "Add", + "menuItems": [], + }, + { + "label": "Tools", + "menuItems": [ + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Undo", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Redo", + "onClick": [Function], + }, + { + "type": "divider", + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Plugin store", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Preferences", + "onClick": [Function], + }, + ], + }, +] +`; + +exports[`Root MST model adds menus 2`] = ` +[ + { + "label": "File", + "menuItems": [ + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "New session", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Import session…", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Export session", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Open session…", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Save session", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Duplicate session", + "onClick": [Function], + }, + { + "type": "divider", + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Open track...", + "onClick": [Function], + }, + { + "icon": [Function], + "label": "Open connection...", + "onClick": [Function], + }, + { + "type": "divider", + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Return to splash screen", + "onClick": [Function], + }, + ], + }, + { + "label": "Add", + "menuItems": [], + }, + { + "label": "Tools", + "menuItems": [ + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Undo", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Redo", + "onClick": [Function], + }, + { + "type": "divider", + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Plugin store", + "onClick": [Function], + }, + { + "icon": { + "$$typeof": Symbol(react.memo), + "compare": null, + "type": { + "$$typeof": Symbol(react.forward_ref), + "render": [Function], + }, + }, + "label": "Preferences", + "onClick": [Function], + }, + ], + }, + { + "label": "Second Menu", + "menuItems": [ + { + "label": "First Menu Item", + "onClick": [Function], + }, + { + "label": "Second Menu Item", + "onClick": [Function], + }, + { + "label": "First Sub Menu", + "subMenu": [ + { + "label": "First Sub Menu Item", + "onClick": [Function], + }, + { + "label": "Second Sub Menu Item", + "onClick": [Function], + }, + ], + }, + ], + }, + { + "label": "Third Menu", + "menuItems": [], + }, +] +`; + +exports[`Root MST model adds track and connection configs to an assembly 1`] = ` +{ + "aliases": [ + "assemblyA", + ], + "name": "assembly1", + "sequence": { + "adapter": { + "adapterId": "sequenceConfigAdapterId", + "features": [ + { + "end": 10, + "refName": "ctgA", + "seq": "cattgttgcg", + "start": 0, + "uniqueId": "firstId", + }, + ], + "type": "FromConfigSequenceAdapter", + }, + "displays": [ + { + "displayId": "sequenceConfigId-LinearReferenceSequenceDisplay", + "type": "LinearReferenceSequenceDisplay", + }, + { + "displayId": "sequenceConfigId-LinearGCContentDisplay", + "type": "LinearGCContentDisplay", + }, + ], + "trackId": "sequenceConfigId", + "type": "ReferenceSequenceTrack", + }, +} +`; + +exports[`Root MST model adds track and connection configs to an assembly 2`] = ` +{ + "displays": [ + { + "displayId": "trackId0-LinearBasicDisplay", + "type": "LinearBasicDisplay", + }, + { + "displayId": "trackId0-LinearArcDisplay", + "type": "LinearArcDisplay", + }, + ], + "trackId": "trackId0", + "type": "FeatureTrack", +} +`; + +exports[`Root MST model adds track and connection configs to an assembly 3`] = ` +{ + "connectionId": "connectionId0", + "dataDirLocation": { + "internetAccountId": undefined, + "internetAccountPreAuthorization": undefined, + "locationType": "UriLocation", + "uri": "http://mysite.com/jbrowse/data/", + }, + "type": "JBrowse1Connection", +} +`; + +exports[`Root MST model creates with defaults 1`] = `{}`; From 856cd699ae72923a0f9145bc50303014e6663354 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sun, 7 May 2023 10:09:21 -0700 Subject: [PATCH 35/44] some simple test fixes --- .../LinearGenomeView.test.tsx.snap | 4 +- .../__snapshots__/index.test.js.snap | 453 ------------------ .../sessionModel/sessionModelFactory.test.js | 2 +- 3 files changed, 3 insertions(+), 456 deletions(-) delete mode 100644 products/jbrowse-web/src/rootModel/__snapshots__/index.test.js.snap diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap index 79c012e959..06f37dea25 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap @@ -157,7 +157,7 @@ exports[`renders one track, one region 1`] = ` />
@@ -731,7 +731,7 @@ exports[`renders two tracks, two regions 1`] = ` />
diff --git a/products/jbrowse-web/src/rootModel/__snapshots__/index.test.js.snap b/products/jbrowse-web/src/rootModel/__snapshots__/index.test.js.snap deleted file mode 100644 index 80662756d7..0000000000 --- a/products/jbrowse-web/src/rootModel/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,453 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Root MST model adds menus 1`] = ` -[ - { - "label": "File", - "menuItems": [ - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "New session", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Import session…", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Export session", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Open session…", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Save session", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Duplicate session", - "onClick": [Function], - }, - { - "type": "divider", - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Open track...", - "onClick": [Function], - }, - { - "icon": [Function], - "label": "Open connection...", - "onClick": [Function], - }, - { - "type": "divider", - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Return to splash screen", - "onClick": [Function], - }, - ], - }, - { - "label": "Add", - "menuItems": [], - }, - { - "label": "Tools", - "menuItems": [ - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Undo", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Redo", - "onClick": [Function], - }, - { - "type": "divider", - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Plugin store", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Preferences", - "onClick": [Function], - }, - ], - }, -] -`; - -exports[`Root MST model adds menus 2`] = ` -[ - { - "label": "File", - "menuItems": [ - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "New session", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Import session…", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Export session", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Open session…", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Save session", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Duplicate session", - "onClick": [Function], - }, - { - "type": "divider", - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Open track...", - "onClick": [Function], - }, - { - "icon": [Function], - "label": "Open connection...", - "onClick": [Function], - }, - { - "type": "divider", - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Return to splash screen", - "onClick": [Function], - }, - ], - }, - { - "label": "Add", - "menuItems": [], - }, - { - "label": "Tools", - "menuItems": [ - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Undo", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Redo", - "onClick": [Function], - }, - { - "type": "divider", - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Plugin store", - "onClick": [Function], - }, - { - "icon": { - "$$typeof": Symbol(react.memo), - "compare": null, - "type": { - "$$typeof": Symbol(react.forward_ref), - "render": [Function], - }, - }, - "label": "Preferences", - "onClick": [Function], - }, - ], - }, - { - "label": "Second Menu", - "menuItems": [ - { - "label": "First Menu Item", - "onClick": [Function], - }, - { - "label": "Second Menu Item", - "onClick": [Function], - }, - { - "label": "First Sub Menu", - "subMenu": [ - { - "label": "First Sub Menu Item", - "onClick": [Function], - }, - { - "label": "Second Sub Menu Item", - "onClick": [Function], - }, - ], - }, - ], - }, - { - "label": "Third Menu", - "menuItems": [], - }, -] -`; - -exports[`Root MST model adds track and connection configs to an assembly 1`] = ` -{ - "aliases": [ - "assemblyA", - ], - "name": "assembly1", - "sequence": { - "adapter": { - "adapterId": "sequenceConfigAdapterId", - "features": [ - { - "end": 10, - "refName": "ctgA", - "seq": "cattgttgcg", - "start": 0, - "uniqueId": "firstId", - }, - ], - "type": "FromConfigSequenceAdapter", - }, - "displays": [ - { - "displayId": "sequenceConfigId-LinearReferenceSequenceDisplay", - "type": "LinearReferenceSequenceDisplay", - }, - { - "displayId": "sequenceConfigId-LinearGCContentDisplay", - "type": "LinearGCContentDisplay", - }, - ], - "trackId": "sequenceConfigId", - "type": "ReferenceSequenceTrack", - }, -} -`; - -exports[`Root MST model adds track and connection configs to an assembly 2`] = ` -{ - "displays": [ - { - "displayId": "trackId0-LinearBasicDisplay", - "type": "LinearBasicDisplay", - }, - { - "displayId": "trackId0-LinearArcDisplay", - "type": "LinearArcDisplay", - }, - ], - "trackId": "trackId0", - "type": "FeatureTrack", -} -`; - -exports[`Root MST model adds track and connection configs to an assembly 3`] = ` -{ - "connectionId": "connectionId0", - "dataDirLocation": { - "internetAccountId": undefined, - "internetAccountPreAuthorization": undefined, - "locationType": "UriLocation", - "uri": "http://mysite.com/jbrowse/data/", - }, - "type": "JBrowse1Connection", -} -`; - -exports[`Root MST model creates with defaults 1`] = `{}`; diff --git a/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js index 07d8a0d7ca..b73246f2a3 100644 --- a/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js +++ b/products/jbrowse-web/src/sessionModel/sessionModelFactory.test.js @@ -2,7 +2,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' import { configure } from 'mobx' -import { createTestSession } from '../rootModel/rootModel' +import { createTestSession } from '../rootModel' import sessionModelFactory from '.' jest.mock('../makeWorkerInstance', () => () => {}) From d81ad4321e7071989e7be95423b7ddb0448006b1 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sun, 7 May 2023 10:46:13 -0700 Subject: [PATCH 36/44] fix jbweb internet account tests I forgot that MST automatically chains lifecycle hooks like `afterCreate` when composing types --- packages/core/util/types/util.ts | 13 ------- .../src/RootModel/InternetAccounts.ts | 36 ++++++++----------- .../jbrowse-web/src/rootModel/rootModel.ts | 7 ---- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/packages/core/util/types/util.ts b/packages/core/util/types/util.ts index 501cab5fc8..d6148ab273 100644 --- a/packages/core/util/types/util.ts +++ b/packages/core/util/types/util.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React from 'react' -import { IAnyStateTreeNode, IModelType, Instance } from 'mobx-state-tree' import PluginManager from '../../PluginManager' /** @@ -21,15 +20,3 @@ export type AnyReactComponentType = React.ComponentType /** get the type that a predicate asserts */ export type TypeTestedByPredicate boolean> = PREDICATE extends (thing: any) => thing is infer TYPE ? TYPE : never - -/** type guard that inpects whether a MST model has an `afterCreate` action */ -export function isModelWithAfterCreate( - thing: IAnyStateTreeNode, -): thing is Instance void }>> { - return ( - typeof thing === 'object' && - thing !== null && - 'afterCreate' in thing && - typeof thing.afterCreate === 'function' - ) -} diff --git a/packages/product-core/src/RootModel/InternetAccounts.ts b/packages/product-core/src/RootModel/InternetAccounts.ts index 573dc42095..99d85878f7 100644 --- a/packages/product-core/src/RootModel/InternetAccounts.ts +++ b/packages/product-core/src/RootModel/InternetAccounts.ts @@ -1,6 +1,6 @@ import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfigurationModel } from '@jbrowse/core/configuration' -import { UriLocation, isModelWithAfterCreate } from '@jbrowse/core/util' +import { UriLocation } from '@jbrowse/core/util' import { autorun } from 'mobx' import { Instance, addDisposer, types } from 'mobx-state-tree' import { BaseRootModelType } from './Base' @@ -62,7 +62,7 @@ export default function InternetAccountsF(pluginManager: PluginManager) { internetAccountId: internetAccountId, name: internetAccountSplit.slice(1).join('-'), description: '', - domains: [hostUri], + domains: hostUri ? [hostUri] : [], } const internetAccountType = pluginManager.getInternetAccountType( configuration.type, @@ -104,26 +104,18 @@ export default function InternetAccountsF(pluginManager: PluginManager) { : null }, })) - .actions(self => { - const super_afterCreate = isModelWithAfterCreate(self) - ? self.afterCreate - : undefined - return { - afterCreate() { - if (super_afterCreate) { - super_afterCreate() - } - addDisposer( - self, - autorun(() => { - const { jbrowse } = self as typeof self & - Instance - jbrowse.internetAccounts.forEach(self.initializeInternetAccount) - }), - ) - }, - } - }) + .actions(self => ({ + afterCreate() { + addDisposer( + self, + autorun(() => { + const { jbrowse } = self as typeof self & + Instance + jbrowse.internetAccounts.forEach(self.initializeInternetAccount) + }), + ) + }, + })) } export type RootModelWithInternetAccountsType = ReturnType< diff --git a/products/jbrowse-web/src/rootModel/rootModel.ts b/products/jbrowse-web/src/rootModel/rootModel.ts index a8595dc728..3e32384c98 100644 --- a/products/jbrowse-web/src/rootModel/rootModel.ts +++ b/products/jbrowse-web/src/rootModel/rootModel.ts @@ -18,7 +18,6 @@ import PluginManager from '@jbrowse/core/PluginManager' import RpcManager from '@jbrowse/core/rpc/RpcManager' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' import TimeTraveller from '@jbrowse/core/util/TimeTraveller' -import { isModelWithAfterCreate } from '@jbrowse/core/util' import { AbstractSessionModel, SessionWithWidgets } from '@jbrowse/core/util' import { MenuItem } from '@jbrowse/core/ui' @@ -152,14 +151,8 @@ export default function RootModel( })) .actions(self => { - const super_afterCreate = isModelWithAfterCreate(self) - ? self.afterCreate - : undefined return { afterCreate() { - if (super_afterCreate) { - super_afterCreate() - } document.addEventListener('keydown', e => { const cm = e.ctrlKey || e.metaKey if ( From f41b88f1150b0967c6ebe9701aef80a1f7f05646 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sun, 7 May 2023 10:52:04 -0700 Subject: [PATCH 37/44] update snap --- .../__snapshots__/JBrowseLinearGenomeView.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap index fe55ab9a88..0fd291e2fc 100644 --- a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap +++ b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap @@ -246,7 +246,7 @@ exports[` renders successfully 1`] = ` />
From 24431d4c9d57262ea1c6b38ed267cebb9463bfca Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Sun, 7 May 2023 12:28:25 -0700 Subject: [PATCH 38/44] snaps --- .../components/__snapshots__/LinearGenomeView.test.tsx.snap | 4 ++-- .../__snapshots__/JBrowseLinearGenomeView.test.tsx.snap | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap index 06f37dea25..79c012e959 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.tsx.snap @@ -157,7 +157,7 @@ exports[`renders one track, one region 1`] = ` />
@@ -731,7 +731,7 @@ exports[`renders two tracks, two regions 1`] = ` />
diff --git a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap index 0fd291e2fc..fe55ab9a88 100644 --- a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap +++ b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/__snapshots__/JBrowseLinearGenomeView.test.tsx.snap @@ -246,7 +246,7 @@ exports[` renders successfully 1`] = ` />
From 19d0848c6cb1d3138edc948d728ca40012b59a3d Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 8 May 2023 11:26:04 -0700 Subject: [PATCH 39/44] fix build, make session type names and guards consistent --- packages/product-core/src/Session/Base.ts | 21 ++++++++++++++- .../product-core/src/Session/Connections.ts | 20 +++++++++++--- .../product-core/src/Session/DialogQueue.ts | 15 +++++++++-- .../product-core/src/Session/DrawerWidgets.ts | 26 +++++++++++++++++-- .../product-core/src/Session/MultipleViews.ts | 19 +++++++++++--- .../src/Session/ReferenceManagement.ts | 22 +++++++++++++++- .../product-core/src/Session/SessionTracks.ts | 13 +++++++++- packages/product-core/src/Session/Themes.ts | 21 ++++++++++++--- packages/product-core/src/Session/Tracks.ts | 17 +++++++++--- packages/product-core/src/Session/index.ts | 1 + packages/product-core/src/index.ts | 1 + .../jbrowse-desktop/src/rootModel/Menus.ts | 6 ++--- .../src/sessionModel/TrackMenu.ts | 12 ++++----- .../jbrowse-web/src/rootModel/rootModel.ts | 6 ++--- 14 files changed, 168 insertions(+), 32 deletions(-) diff --git a/packages/product-core/src/Session/Base.ts b/packages/product-core/src/Session/Base.ts index 72254d3251..ec635fd0f3 100644 --- a/packages/product-core/src/Session/Base.ts +++ b/packages/product-core/src/Session/Base.ts @@ -1,7 +1,13 @@ import shortid from 'shortid' import type PluginManager from '@jbrowse/core/PluginManager' -import { Instance, getParent, types } from 'mobx-state-tree' +import { + IAnyStateTreeNode, + Instance, + getParent, + isStateTreeNode, + types, +} from 'mobx-state-tree' import type { BaseRootModelType } from '../RootModel/Base' import { AnyConfigurationSchemaType } from '@jbrowse/core/configuration' import { BaseAssemblyConfigSchema } from '@jbrowse/core/assemblyManager' @@ -101,5 +107,18 @@ export default function BaseSessionFactory< })) } +/** Session mixin MST type for the most basic session */ export type BaseSessionType = ReturnType + +/** Instance of the most basic possible session */ export type BaseSession = Instance + +/** Type guard for BaseSession */ +export function isBaseSession(thing: IAnyStateTreeNode): thing is BaseSession { + return 'id' in thing && 'name' in thing && 'root' in thing +} + +/** Type guard for whether a thing is JBrowse session */ +export function isSession(thing: unknown): thing is BaseSession { + return isStateTreeNode(thing) && isBaseSession(thing) +} diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts index c3701688c9..54ba823280 100644 --- a/packages/product-core/src/Session/Connections.ts +++ b/packages/product-core/src/Session/Connections.ts @@ -5,11 +5,12 @@ import { AnyConfigurationModel, readConfObject, } from '@jbrowse/core/configuration' -import { Instance, types } from 'mobx-state-tree' -import type { SessionWithReferenceManagement } from './ReferenceManagement' +import { IAnyStateTreeNode, Instance, isStateTreeNode, types } from 'mobx-state-tree' +import type { SessionWithReferenceManagementType } from './ReferenceManagement' import type { BaseRootModelType } from '../RootModel/Base' import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' import { BaseConnectionModel } from '@jbrowse/core/pluggableElementTypes/models/BaseConnectionModelFactory' +import { isBaseSession } from './Base' export default function Connections(pluginManager: PluginManager) { return types @@ -65,7 +66,7 @@ export default function Connections(pluginManager: PluginManager) { */ prepareToBreakConnection(configuration: AnyConfigurationModel) { const root = self as typeof self & - Instance + Instance const callbacksToDereferenceTrack: Function[] = [] const dereferenceTypeCount: Record = {} const name = readConfObject(configuration, 'name') @@ -125,3 +126,16 @@ export default function Connections(pluginManager: PluginManager) { }, })) } + +/** Session mixin MST type for a session that has connections */ +export type SessionWithConnectionsType = ReturnType + +/** Instance of a session that has connections: `connectionInstances`, `makeConnection()`, etc. */ +export type SessionWithConnections = Instance + +/** Type guard for SessionWithConnections */ +export function isSessionWithConnections( + session: IAnyStateTreeNode, +): session is SessionWithConnections { + return isBaseSession(session) && 'connectionInstances' in session +} diff --git a/packages/product-core/src/Session/DialogQueue.ts b/packages/product-core/src/Session/DialogQueue.ts index bf723f846b..153268fba5 100644 --- a/packages/product-core/src/Session/DialogQueue.ts +++ b/packages/product-core/src/Session/DialogQueue.ts @@ -3,6 +3,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import { DialogComponentType } from '@jbrowse/core/util' import { IAnyStateTreeNode, Instance, types } from 'mobx-state-tree' +import { isBaseSession } from './Base' export interface ReferringNode { node: IAnyStateTreeNode @@ -58,5 +59,15 @@ export default function DialogQueue(pluginManager: PluginManager) { })) } -export type DialogQueueManagerType = ReturnType -export type DialogQueueManager = Instance +/** Session mixin MST type for a session that has `queueOfDialogs`, etc. */ +export type SessionWithDialogsType = ReturnType + +/** Instance of a session that has dialogs */ +export type SessionWithDialogs = Instance + +/** Type guard for SessionWithDialogs */ +export function isSessionWithDialogs( + session: IAnyStateTreeNode, +): session is SessionWithDialogs { + return isBaseSession(session) && 'queueOfDialogs' in session +} diff --git a/packages/product-core/src/Session/DrawerWidgets.ts b/packages/product-core/src/Session/DrawerWidgets.ts index bfc09e8223..76b106e8ad 100644 --- a/packages/product-core/src/Session/DrawerWidgets.ts +++ b/packages/product-core/src/Session/DrawerWidgets.ts @@ -1,4 +1,10 @@ -import { Instance, addDisposer, isAlive, types } from 'mobx-state-tree' +import { + IAnyStateTreeNode, + Instance, + addDisposer, + isAlive, + types, +} from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' @@ -7,6 +13,7 @@ import { AnyConfigurationModel, isConfigurationModel, } from '@jbrowse/core/configuration' +import { isBaseSession } from './Base' const minDrawerWidth = 128 @@ -193,4 +200,19 @@ export default function DrawerWidgets(pluginManager: PluginManager) { })) } -export type DrawerWidgetManager = Instance> +/** Session mixin MST type for a session that manages drawer widgets */ +export type SessionWithDrawerWidgetsType = ReturnType + +/** Instance of a session that manages drawer widgets */ +export type SessionWithDrawerWidgets = Instance + +/** Type guard for SessionWithDrawerWidgets */ +export function isSessionWithDrawerWidgets( + session: IAnyStateTreeNode, +): session is SessionWithDrawerWidgets { + return ( + isBaseSession(session) && + 'widgets' in session && + 'drawerPosition' in session + ) +} diff --git a/packages/product-core/src/Session/MultipleViews.ts b/packages/product-core/src/Session/MultipleViews.ts index 4c6b5e0487..ca2a790c3d 100644 --- a/packages/product-core/src/Session/MultipleViews.ts +++ b/packages/product-core/src/Session/MultipleViews.ts @@ -1,4 +1,4 @@ -import { getSnapshot, types } from 'mobx-state-tree' +import { IAnyStateTreeNode, Instance, getSnapshot, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { readConfObject } from '@jbrowse/core/configuration' @@ -6,9 +6,9 @@ import { Region } from '@jbrowse/core/util' import DrawerWidgets from './DrawerWidgets' import { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes' import { IBaseViewModelWithDisplayedRegions } from '@jbrowse/core/pluggableElementTypes/models/BaseViewModel' -import Base from './Base' +import Base, { isBaseSession } from './Base' -export default function Views(pluginManager: PluginManager) { +export default function MultipleViews(pluginManager: PluginManager) { return types .compose(Base(pluginManager), DrawerWidgets(pluginManager)) .props({ @@ -121,3 +121,16 @@ export default function Views(pluginManager: PluginManager) { }, })) } + +/** Session mixin MST type for a session that manages multiple views */ +export type SessionWithMultipleViewsType = ReturnType + +/** Instance of a session with multiple views */ +export type SessionWithMultipleViews = Instance + +/** Type guard for SessionWithMultipleViews */ +export function isSessionWithMultipleViews( + session: IAnyStateTreeNode, +): session is SessionWithMultipleViews { + return isBaseSession(session) && 'views' in session +} diff --git a/packages/product-core/src/Session/ReferenceManagement.ts b/packages/product-core/src/Session/ReferenceManagement.ts index 1de149c839..9813373a7f 100644 --- a/packages/product-core/src/Session/ReferenceManagement.ts +++ b/packages/product-core/src/Session/ReferenceManagement.ts @@ -8,6 +8,7 @@ import { } from '@jbrowse/core/util' import { IAnyStateTreeNode, + Instance, getMembers, getParent, getSnapshot, @@ -19,6 +20,7 @@ import { } from 'mobx-state-tree' import type { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' +import { isBaseSession } from './Base' export interface ReferringNode { node: IAnyStateTreeNode @@ -105,6 +107,24 @@ export default function ReferenceManagement(pluginManager: PluginManager) { })) } -export type SessionWithReferenceManagement = ReturnType< +/** Session mixin MST type for a session that manages multiple views */ +export type SessionWithReferenceManagementType = ReturnType< typeof ReferenceManagement > + +/** Instance of a session with MST reference management (`getReferring()`, `removeReferring()`) */ +export type SessionWithReferenceManagement = + Instance + +/** Type guard for SessionWithReferenceManagement */ +export function isSessionWithReferenceManagement( + thing: IAnyStateTreeNode, +): thing is SessionWithReferenceManagement { + return ( + isBaseSession(thing) && + 'getReferring' in thing && + typeof thing.getReferring === 'function' && + 'removeReferring' in thing && + typeof thing.removeReferring === 'function' + ) +} diff --git a/packages/product-core/src/Session/SessionTracks.ts b/packages/product-core/src/Session/SessionTracks.ts index 659345a759..9bc68d368c 100644 --- a/packages/product-core/src/Session/SessionTracks.ts +++ b/packages/product-core/src/Session/SessionTracks.ts @@ -1,4 +1,4 @@ -import { Instance, types } from 'mobx-state-tree' +import { IAnyStateTreeNode, Instance, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { @@ -6,6 +6,7 @@ import { AnyConfigurationModel, } from '@jbrowse/core/configuration' import Tracks from './Tracks' +import { isBaseSession } from './Base' export default function SessionTracks(pluginManager: PluginManager) { return Tracks(pluginManager) @@ -73,5 +74,15 @@ export default function SessionTracks(pluginManager: PluginManager) { }) } +/** Session mixin MST type for a session that has `sessionTracks` */ export type SessionWithSessionTracksType = ReturnType + +/** Instance of a session that has `sessionTracks` */ export type SessionWithSessionTracks = Instance + +/** Type guard for SessionWithSessionTracks */ +export function isSessionWithSessionTracks( + thing: IAnyStateTreeNode, +): thing is SessionWithSessionTracks { + return isBaseSession(thing) && 'sessionTracks' in thing +} diff --git a/packages/product-core/src/Session/Themes.ts b/packages/product-core/src/Session/Themes.ts index b5c1685f32..06bf69a85e 100644 --- a/packages/product-core/src/Session/Themes.ts +++ b/packages/product-core/src/Session/Themes.ts @@ -1,12 +1,12 @@ -import { addDisposer, types } from 'mobx-state-tree' +import { IAnyStateTreeNode, Instance, addDisposer, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { getConf } from '@jbrowse/core/configuration' import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' -import type { BaseSessionModel } from '../../../../products/jbrowse-desktop/src/sessionModel/Base' import { ThemeOptions } from '@mui/material' import { autorun } from 'mobx' +import { BaseSession } from './Base' type ThemeMap = { [key: string]: ThemeOptions } @@ -21,7 +21,7 @@ export default function Themes(pluginManager: PluginManager) { * #method */ allThemes(): ThemeMap { - const self = s as typeof s & BaseSessionModel + const self = s as typeof s & BaseSession const extraThemes = getConf(self.jbrowse, 'extraThemes') return { ...defaultThemes, ...extraThemes } }, @@ -37,7 +37,7 @@ export default function Themes(pluginManager: PluginManager) { * #getter */ get theme() { - const self = s as typeof s & BaseSessionModel + const self = s as typeof s & BaseSession const configTheme = getConf(self.jbrowse, 'theme') const all = this.allThemes() return createJBrowseTheme(configTheme, all, this.themeName) @@ -60,3 +60,16 @@ export default function Themes(pluginManager: PluginManager) { }, })) } + +/** Session mixin MST type for a session that supports theming */ +export type SessionWithThemesType = ReturnType + +/** Instance of a session that has theming support */ +export type SessionWithThemes = Instance + +/** Type guard for SessionWithThemes */ +export function isSessionWithThemes( + session: IAnyStateTreeNode, +): session is SessionWithThemes { + return 'theme' in session +} diff --git a/packages/product-core/src/Session/Tracks.ts b/packages/product-core/src/Session/Tracks.ts index 9688312150..bac54b447e 100644 --- a/packages/product-core/src/Session/Tracks.ts +++ b/packages/product-core/src/Session/Tracks.ts @@ -1,11 +1,11 @@ -import { Instance, types } from 'mobx-state-tree' +import { IAnyStateTreeNode, Instance, types } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { AnyConfiguration, AnyConfigurationModel, } from '@jbrowse/core/configuration' -import BaseSession from './Base' +import BaseSession, { isBaseSession } from './Base' import ReferenceManagement from './ReferenceManagement' export default function Tracks(pluginManager: PluginManager) { @@ -52,4 +52,15 @@ export default function Tracks(pluginManager: PluginManager) { })) } -export type TracksManager = Instance> +/** Session mixin MST type for a session that has tracks */ +export type SessionWithTracksType = ReturnType + +/** Instance of a session that has tracks */ +export type SessionWithTracks = Instance + +/** Type guard for SessionWithTracks */ +export function isSessionWithTracks( + thing: IAnyStateTreeNode, +): thing is SessionWithTracks { + return isBaseSession(thing) && 'tracks' in thing +} diff --git a/packages/product-core/src/Session/index.ts b/packages/product-core/src/Session/index.ts index 10b55c70c4..e903998a70 100644 --- a/packages/product-core/src/Session/index.ts +++ b/packages/product-core/src/Session/index.ts @@ -7,3 +7,4 @@ export { default as Tracks } from './Tracks' export { default as MultipleViews } from './MultipleViews' export { default as Base } from './Base' export { default as SessionTracks } from './SessionTracks' +export { isSession } from './Base' diff --git a/packages/product-core/src/index.ts b/packages/product-core/src/index.ts index 1714dac9c9..ac4aaebfcb 100644 --- a/packages/product-core/src/index.ts +++ b/packages/product-core/src/index.ts @@ -1,2 +1,3 @@ export * as RootModel from './RootModel' export * as Session from './Session' +export { isSession } from './Session' diff --git a/products/jbrowse-desktop/src/rootModel/Menus.ts b/products/jbrowse-desktop/src/rootModel/Menus.ts index 8b04e6091e..1cef04f717 100644 --- a/products/jbrowse-desktop/src/rootModel/Menus.ts +++ b/products/jbrowse-desktop/src/rootModel/Menus.ts @@ -17,7 +17,7 @@ import { Save, SaveAs, DNA, Cable } from '@jbrowse/core/ui/Icons' import type { AnyConfigurationModel } from '@jbrowse/core/configuration' import OpenSequenceDialog from '../OpenSequenceDialog' -import type { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' +import type { SessionWithDialogs } from '@jbrowse/product-core/src/Session/DialogQueue' import { getSaveSession } from './Sessions' import { DesktopRootModel } from '.' @@ -92,7 +92,7 @@ export default function Menus(pluginManager: PluginManager) { icon: DNA, onClick: () => { if (self.session) { - const session = self.session as DialogQueueManager + const session = self.session as SessionWithDialogs session.queueDialog(doneCallback => [ OpenSequenceDialog, { @@ -211,7 +211,7 @@ export default function Menus(pluginManager: PluginManager) { icon: SettingsIcon, onClick: () => { if (self.session) { - const session = self.session as DialogQueueManager + const session = self.session as SessionWithDialogs session.queueDialog(handleClose => [ PreferencesDialog, { diff --git a/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts index b038f85f8c..60bd240a03 100644 --- a/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts +++ b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts @@ -11,9 +11,9 @@ import { BaseTrackConfig } from '@jbrowse/core/pluggableElementTypes' import { supportedIndexingAdapters } from '@jbrowse/text-indexing' import { lazy } from 'react' -import type { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' -import type { TracksManager } from '@jbrowse/product-core/src/Session/Tracks' -import type { DrawerWidgetManager } from '@jbrowse/product-core/src/Session/DrawerWidgets' +import type { SessionWithDialogs } from '@jbrowse/product-core/src/Session/DialogQueue' +import type { SessionWithTracks } from '@jbrowse/product-core/src/Session/Tracks' +import type { SessionWithDrawerWidgets } from '@jbrowse/product-core/src/Session/DrawerWidgets' import { DesktopRootModel } from '../rootModel' const AboutDialog = lazy(() => import('@jbrowse/core/ui/AboutDialog')) @@ -24,9 +24,9 @@ export default function TrackMenu(pluginManager: PluginManager) { * #method */ getTrackActionMenuItems(trackConfig: BaseTrackConfig) { - const session = self as DialogQueueManager & - TracksManager & - DrawerWidgetManager + const session = self as SessionWithDialogs & + SessionWithTracks & + SessionWithDrawerWidgets const trackSnapshot = JSON.parse(JSON.stringify(getSnapshot(trackConfig))) return [ { diff --git a/products/jbrowse-web/src/rootModel/rootModel.ts b/products/jbrowse-web/src/rootModel/rootModel.ts index 3e32384c98..ae8e1fee28 100644 --- a/products/jbrowse-web/src/rootModel/rootModel.ts +++ b/products/jbrowse-web/src/rootModel/rootModel.ts @@ -41,11 +41,11 @@ import makeWorkerInstance from '../makeWorkerInstance' import jbrowseWebFactory from '../jbrowseModel' import { filterSessionInPlace } from '../util' import { RootModel as CoreRootModel } from '@jbrowse/product-core' -import { +import type { BaseSession, BaseSessionType, } from '@jbrowse/product-core/src/Session/Base' -import { DialogQueueManager } from '@jbrowse/product-core/src/Session/DialogQueue' +import type { SessionWithDialogs } from '@jbrowse/product-core/src/Session/DialogQueue' const PreferencesDialog = lazy(() => import('../PreferencesDialog')) @@ -550,7 +550,7 @@ export default function RootModel( icon: SettingsIcon, onClick: () => { if (self.session) { - ;(self.session as DialogQueueManager).queueDialog( + ;(self.session as SessionWithDialogs).queueDialog( handleClose => [ PreferencesDialog, { From 9e329d82ff8c976068dc1bc7bd938243c8120fb6 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 8 May 2023 11:28:32 -0700 Subject: [PATCH 40/44] add type guard for jb root model --- packages/product-core/src/RootModel/Base.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/product-core/src/RootModel/Base.ts b/packages/product-core/src/RootModel/Base.ts index 121eaa23fa..7ca6916026 100644 --- a/packages/product-core/src/RootModel/Base.ts +++ b/packages/product-core/src/RootModel/Base.ts @@ -9,6 +9,7 @@ import { SnapshotIn, cast, getSnapshot, + isStateTreeNode, types, } from 'mobx-state-tree' import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager' @@ -118,3 +119,13 @@ export default function BaseRootModelTypeF( export type BaseRootModelType = ReturnType export type BaseRootModel = Instance + +/** Type guard for checking if something is a JB root model */ +export function isRootModel(thing: unknown): thing is BaseRootModelType { + return ( + isStateTreeNode(thing) && + 'session' in thing && + 'jbrowse' in thing && + 'assemblyManager' in thing + ) +} From f0f8d58fd5bc5b48117290c6c6e16ff7978a1de0 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 8 May 2023 13:12:32 -0700 Subject: [PATCH 41/44] lint --- packages/product-core/src/Session/Connections.ts | 2 +- packages/product-core/src/Session/MultipleViews.ts | 7 ++++++- packages/product-core/src/Session/Themes.ts | 7 ++++++- products/jbrowse-desktop/src/rootModel/index.test.ts | 1 - products/jbrowse-web/src/tests/JBrowse.test.tsx | 1 - products/jbrowse-web/src/tests/StartScreen.test.tsx | 1 - 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/product-core/src/Session/Connections.ts b/packages/product-core/src/Session/Connections.ts index 54ba823280..7e511e8cb0 100644 --- a/packages/product-core/src/Session/Connections.ts +++ b/packages/product-core/src/Session/Connections.ts @@ -5,7 +5,7 @@ import { AnyConfigurationModel, readConfObject, } from '@jbrowse/core/configuration' -import { IAnyStateTreeNode, Instance, isStateTreeNode, types } from 'mobx-state-tree' +import { IAnyStateTreeNode, Instance, types } from 'mobx-state-tree' import type { SessionWithReferenceManagementType } from './ReferenceManagement' import type { BaseRootModelType } from '../RootModel/Base' import { BaseConnectionConfigModel } from '@jbrowse/core/pluggableElementTypes/models/baseConnectionConfig' diff --git a/packages/product-core/src/Session/MultipleViews.ts b/packages/product-core/src/Session/MultipleViews.ts index ca2a790c3d..65cd427e66 100644 --- a/packages/product-core/src/Session/MultipleViews.ts +++ b/packages/product-core/src/Session/MultipleViews.ts @@ -1,4 +1,9 @@ -import { IAnyStateTreeNode, Instance, getSnapshot, types } from 'mobx-state-tree' +import { + IAnyStateTreeNode, + Instance, + getSnapshot, + types, +} from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { readConfObject } from '@jbrowse/core/configuration' diff --git a/packages/product-core/src/Session/Themes.ts b/packages/product-core/src/Session/Themes.ts index 06bf69a85e..c8dfd7fb66 100644 --- a/packages/product-core/src/Session/Themes.ts +++ b/packages/product-core/src/Session/Themes.ts @@ -1,4 +1,9 @@ -import { IAnyStateTreeNode, Instance, addDisposer, types } from 'mobx-state-tree' +import { + IAnyStateTreeNode, + Instance, + addDisposer, + types, +} from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' import { getConf } from '@jbrowse/core/configuration' diff --git a/products/jbrowse-desktop/src/rootModel/index.test.ts b/products/jbrowse-desktop/src/rootModel/index.test.ts index 0404c96437..9879a280f6 100644 --- a/products/jbrowse-desktop/src/rootModel/index.test.ts +++ b/products/jbrowse-desktop/src/rootModel/index.test.ts @@ -1,6 +1,5 @@ // import electron first, important, because the electron mock creates // window.require -// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars import 'electron' import PluginManager from '@jbrowse/core/PluginManager' import { getSnapshot } from 'mobx-state-tree' diff --git a/products/jbrowse-web/src/tests/JBrowse.test.tsx b/products/jbrowse-web/src/tests/JBrowse.test.tsx index c5c3153bbb..df7f884e0b 100644 --- a/products/jbrowse-web/src/tests/JBrowse.test.tsx +++ b/products/jbrowse-web/src/tests/JBrowse.test.tsx @@ -58,7 +58,6 @@ test('toplevel configuration', () => { pm.setRootModel(rootModel) pm.configure() const state = pm.rootModel - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { jbrowse } = state! const { configuration } = jbrowse // test reading top level configurations added by Test Plugin diff --git a/products/jbrowse-web/src/tests/StartScreen.test.tsx b/products/jbrowse-web/src/tests/StartScreen.test.tsx index ebe4a850d5..0bc1d53489 100644 --- a/products/jbrowse-web/src/tests/StartScreen.test.tsx +++ b/products/jbrowse-web/src/tests/StartScreen.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import React from 'react' import { fireEvent, render } from '@testing-library/react' From eb66f7fe8590b98565911df272ae7db4c2df3823 Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 8 May 2023 13:14:34 -0700 Subject: [PATCH 42/44] fix product-core version --- packages/product-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/product-core/package.json b/packages/product-core/package.json index 6023851cf7..ef2c99eb9f 100644 --- a/packages/product-core/package.json +++ b/packages/product-core/package.json @@ -1,6 +1,6 @@ { "name": "@jbrowse/product-core", - "version": "2.4.2", + "version": "2.5.0", "description": "JBrowse 2 code shared between products but not used by plugins", "keywords": [ "jbrowse", From 5f9e9d8a5fccf4bc752e9df8d4190e9b8b9e2d3d Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 8 May 2023 14:33:00 -0700 Subject: [PATCH 43/44] add built-package resolutions for product-core in lgv and cgv built-embedded tests --- component_tests/cgv/package.json | 1 + component_tests/lgv/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/component_tests/cgv/package.json b/component_tests/cgv/package.json index 17ced85dbc..79449262a3 100644 --- a/component_tests/cgv/package.json +++ b/component_tests/cgv/package.json @@ -41,6 +41,7 @@ "@jbrowse/plugin-trix": "file:./packed/jbrowse-plugin-trix.tgz", "@jbrowse/plugin-variants": "file:./packed/jbrowse-plugin-variants.tgz", "@jbrowse/plugin-wiggle": "file:./packed/jbrowse-plugin-wiggle.tgz", + "@jbrowse/product-core": "file:./packed/jbrowse-product-core.tgz", "@jbrowse/react-linear-genome-view": "file:./packed/jbrowse-react-linear-genome-view.tgz", "@jbrowse/react-circular-genome-view": "file:./packed/jbrowse-react-circular-genome-view.tgz" }, diff --git a/component_tests/lgv/package.json b/component_tests/lgv/package.json index 81ca5e6de6..0c33c45e02 100644 --- a/component_tests/lgv/package.json +++ b/component_tests/lgv/package.json @@ -40,6 +40,7 @@ "@jbrowse/plugin-trix": "file:./packed/jbrowse-plugin-trix.tgz", "@jbrowse/plugin-variants": "file:./packed/jbrowse-plugin-variants.tgz", "@jbrowse/plugin-wiggle": "file:./packed/jbrowse-plugin-wiggle.tgz", + "@jbrowse/product-core": "file:./packed/jbrowse-product-core.tgz", "@jbrowse/react-linear-genome-view": "file:./packed/jbrowse-react-linear-genome-view.tgz" }, "scripts": { From acec95e27b4f1ed483d42e2bef2bfd331aaf541f Mon Sep 17 00:00:00 2001 From: Robert Buels Date: Mon, 8 May 2023 14:35:48 -0700 Subject: [PATCH 44/44] remove unused getJBrowseRoot --- packages/core/util/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index 9a2515a246..52ee811ede 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -219,10 +219,6 @@ export function getSession(node: IAnyStateTreeNode) { } } -export function getJBrowseRoot(node: IAnyStateTreeNode) { - return getParent(getSession(node)) -} - /** get the state model of the view in the state tree that contains the given node */ export function getContainingView(node: IAnyStateTreeNode) { try {