From 7964199be1f812f87f50e92bd82ad3a302ea4006 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 9 Feb 2024 10:43:59 -0500 Subject: [PATCH] Squash updates Remove clone. It seems to be unneeded, even though it will mutate the original object, this doesn't seem like it should matter Another optimization to the track selector Use structuredClone Misc lint fixes Misc --- packages/app-core/src/JBrowseConfig/index.ts | 8 +- packages/app-core/src/JBrowseModel/index.ts | 23 +++- .../core/configuration/configurationSchema.ts | 50 +++++++++ packages/core/util/tracks.ts | 103 +++++++++++++++++- packages/core/util/types/index.ts | 5 +- packages/product-core/src/Session/Tracks.ts | 7 ++ packages/product-core/src/ui/AboutDialog.tsx | 14 ++- .../src/ui/AboutDialogContents.tsx | 11 +- .../product-core/src/ui/FileInfoPanel.tsx | 5 +- .../product-core/src/ui/RefNameInfoDialog.tsx | 5 +- packages/web-core/src/BaseWebSession/index.ts | 8 +- .../src/CircularView/models/model.ts | 44 ++------ .../HierarchicalTrackSelector.test.tsx.snap | 20 ++-- .../components/tree/TrackListNode.tsx | 18 +-- .../components/tree/util.ts | 46 ++++++++ .../facetedModel.ts | 5 +- .../facetedUtil.ts | 2 +- .../filterTracks.ts | 12 +- .../generateHierarchy.ts | 19 ++-- plugins/dotplot-view/src/DotplotView/model.ts | 47 ++------ .../src/LinearComparativeView/model.ts | 55 ++-------- .../src/LinearGenomeView/model.ts | 76 +++---------- .../SvInspectorView/models/SvInspectorView.ts | 2 +- products/jbrowse-desktop/src/jbrowseModel.ts | 3 +- .../jbrowse-react-app/src/jbrowseModel.ts | 4 +- products/jbrowse-web/src/jbrowseModel.test.ts | 1 + products/jbrowse-web/src/jbrowseModel.ts | 12 +- .../__snapshots__/index.test.ts.snap | 10 -- .../jbrowse-web/src/rootModel/index.test.ts | 16 +-- .../jbrowse-web/src/rootModel/rootModel.ts | 1 + 30 files changed, 364 insertions(+), 268 deletions(-) create mode 100644 plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/util.ts diff --git a/packages/app-core/src/JBrowseConfig/index.ts b/packages/app-core/src/JBrowseConfig/index.ts index ecb90078e7..f6cc8391d2 100644 --- a/packages/app-core/src/JBrowseConfig/index.ts +++ b/packages/app-core/src/JBrowseConfig/index.ts @@ -36,9 +36,11 @@ import { types } from 'mobx-state-tree' export function JBrowseConfigF({ pluginManager, assemblyConfigSchema, + adminMode, }: { pluginManager: PluginManager assemblyConfigSchema: AnyConfigurationSchemaType + adminMode: boolean }) { return types.model('JBrowseConfig', { configuration: ConfigurationSchema('Root', { @@ -121,7 +123,11 @@ export function JBrowseConfigF({ * track configuration is an array of track config schemas. multiple * instances of a track can exist that use the same configuration */ - tracks: types.array(pluginManager.pluggableConfigSchemaType('track')), + tracks: + // @ts-expect-error + adminMode || globalThis.disableFrozenTracks + ? types.array(pluginManager.pluggableConfigSchemaType('track')) + : types.frozen([] as { trackId: string; [key: string]: unknown }[]), /** * #slot * configuration for internet accounts, see InternetAccounts diff --git a/packages/app-core/src/JBrowseModel/index.ts b/packages/app-core/src/JBrowseModel/index.ts index bb5bce0c41..64b2b42cbb 100644 --- a/packages/app-core/src/JBrowseModel/index.ts +++ b/packages/app-core/src/JBrowseModel/index.ts @@ -24,13 +24,15 @@ import { JBrowseConfigF } from '../JBrowseConfig' */ export function JBrowseModelF({ + adminMode, pluginManager, assemblyConfigSchema, }: { + adminMode: boolean pluginManager: PluginManager assemblyConfigSchema: BaseAssemblyConfigSchema }) { - return JBrowseConfigF({ pluginManager, assemblyConfigSchema }) + return JBrowseConfigF({ pluginManager, assemblyConfigSchema, adminMode }) .views(self => ({ /** * #getter @@ -81,13 +83,17 @@ export function JBrowseModelF({ /** * #action */ - addTrackConf(trackConf: AnyConfigurationModel) { + addTrackConf(trackConf: { trackId: string; type: string }) { const { type } = trackConf if (!type) { throw new Error(`unknown track type ${type}`) } - const length = self.tracks.push(trackConf) - return self.tracks[length - 1] + if (adminMode) { + self.tracks.push(trackConf) + } else { + self.tracks = [...self.tracks, trackConf] + } + return self.tracks.at(-1) }, /** * #action @@ -111,8 +117,13 @@ export function JBrowseModelF({ * #action */ deleteTrackConf(trackConf: AnyConfigurationModel) { - const elt = self.tracks.find(t => t.trackId === trackConf.trackId) - return self.tracks.remove(elt) + if (adminMode) { + const elt = self.tracks.find(t => t.trackId === trackConf.trackId) + // @ts-expect-error + return self.tracks.remove(elt) + } else { + return self.tracks.filter(f => f.trackId !== trackConf.trackId) + } }, /** * #action diff --git a/packages/core/configuration/configurationSchema.ts b/packages/core/configuration/configurationSchema.ts index 43ba52758c..2dca391b8e 100644 --- a/packages/core/configuration/configurationSchema.ts +++ b/packages/core/configuration/configurationSchema.ts @@ -6,6 +6,9 @@ import { getSnapshot, IAnyType, SnapshotOut, + getEnv, + getRoot, + resolveIdentifier, } from 'mobx-state-tree' import { ElementId } from '../util/types/mst' @@ -13,6 +16,7 @@ import { ElementId } from '../util/types/mst' import ConfigSlot, { ConfigSlotDefinition } from './configurationSlot' import { isConfigurationSchemaType } from './util' import { AnyConfigurationSchemaType } from './types' +import { getContainingTrack, getSession } from '../util' export type { AnyConfigurationSchemaType, @@ -276,9 +280,55 @@ export function ConfigurationSchema< return schemaType } +export function TrackConfigurationReference(schemaType: IAnyType) { + const trackRef = types.reference(schemaType, { + get(id, parent) { + let ret = getSession(parent).tracksById[id] + if (!ret) { + // @ts-expect-error + ret = resolveIdentifier(schemaType, getRoot(parent), id) + } + if (!ret) { + throw new Error(`${id} not found`) + } + return isStateTreeNode(ret) ? ret : schemaType.create(ret, getEnv(parent)) + }, + set(value) { + return value.trackId + }, + }) + return types.union(trackRef, schemaType) +} + +export function DisplayConfigurationReference(schemaType: IAnyType) { + const displayRef = types.reference(schemaType, { + get(id, parent) { + const track = getContainingTrack(parent) + let ret = track.configuration.displays.find(u => u.displayId === id) + if (!ret) { + // @ts-expect-error + ret = resolveIdentifier(schemaType, getRoot(parent), id) + } + if (!ret) { + throw new Error(`${id} not found`) + } + return ret + }, + set(value) { + return value.displayId + }, + }) + return types.union(displayRef, schemaType) +} + export function ConfigurationReference< SCHEMATYPE extends AnyConfigurationSchemaType, >(schemaType: SCHEMATYPE) { + if (schemaType.name.endsWith('TrackConfigurationSchema')) { + return TrackConfigurationReference(schemaType) + } else if (schemaType.name.endsWith('DisplayConfigurationSchema')) { + return DisplayConfigurationReference(schemaType) + } // we cast this to SCHEMATYPE, because the reference *should* behave just // like the object it points to. It won't be undefined (this is a // `reference`, not a `safeReference`) diff --git a/packages/core/util/tracks.ts b/packages/core/util/tracks.ts index b8850cfc6d..6828ce445d 100644 --- a/packages/core/util/tracks.ts +++ b/packages/core/util/tracks.ts @@ -1,4 +1,13 @@ -import { getParent, isRoot, IAnyStateTreeNode } from 'mobx-state-tree' +import { + getParent, + getRoot, + isRoot, + resolveIdentifier, + types, + IAnyStateTreeNode, + Instance, + IAnyType, +} from 'mobx-state-tree' import { getSession, objectHash, getEnv } from './index' import { PreFileLocation, FileLocation } from './types' import { readConfObject, AnyConfigurationModel } from '../configuration' @@ -268,3 +277,95 @@ export function getTrackName( } return trackName } + +type MSTArray = Instance>> + +interface MinimalTrack extends IAnyType { + configuration: { trackId: string } +} + +interface GenericView { + type: string + tracks: MSTArray +} + +export function showTrackGeneric( + self: GenericView, + trackId: string, + initialSnapshot = {}, + displayInitialSnapshot = {}, +) { + const { pluginManager } = getEnv(self) + const session = getSession(self) + let conf = session.tracks.find(t => t.trackId === trackId) + if (!conf) { + const schema = pluginManager.pluggableConfigSchemaType('track') + conf = resolveIdentifier(schema, getRoot(self), trackId) + } + if (!conf) { + throw new Error(`Could not resolve identifier "${trackId}"`) + } + const trackType = pluginManager.getTrackType(conf.type) + if (!trackType) { + throw new Error(`Unknown track type ${conf.type}`) + } + const viewType = pluginManager.getViewType(self.type)! + const supportedDisplays = new Set(viewType.displayTypes.map(d => d.name)) + + const { displays = [] } = conf + const displayTypes = new Set() + + displays.forEach((d: any) => d && displayTypes.add(d.type)) + trackType.displayTypes.forEach(displayType => { + if (!displayTypes.has(displayType.name)) { + displays.push({ + displayId: `${trackId}-${displayType.name}`, + type: displayType.name, + }) + } + }) + + const displayConf = displays?.find((d: AnyConfigurationModel) => + supportedDisplays.has(d.type), + ) + if (!displayConf) { + throw new Error( + `Could not find a compatible display for view type ${self.type}`, + ) + } + + const found = self.tracks.find(t => t.configuration.trackId === trackId) + if (!found) { + const track = trackType.stateModel.create({ + ...initialSnapshot, + type: conf.type, + configuration: conf, + displays: [ + { + type: displayConf.type, + configuration: displayConf, + ...displayInitialSnapshot, + }, + ], + }) + self.tracks.push(track) + return track + } + return found +} + +export function hideTrackGeneric(self: GenericView, trackId: string) { + const t = self.tracks.find(t => t.configuration.trackId === trackId) + if (t) { + self.tracks.remove(t) + return 1 + } + return 0 +} + +export function toggleTrackGeneric(self: GenericView, trackId: string) { + const hiddenCount = hideTrackGeneric(self, trackId) + if (!hiddenCount) { + showTrackGeneric(self, trackId) + } +} diff --git a/packages/core/util/types/index.ts b/packages/core/util/types/index.ts index 11b1471a9d..6de920c47c 100644 --- a/packages/core/util/types/index.ts +++ b/packages/core/util/types/index.ts @@ -81,6 +81,7 @@ export type DialogComponentType = /** minimum interface that all session state models must implement */ export interface AbstractSessionModel extends AbstractViewContainer { + tracksById: Record jbrowse: IAnyStateTreeNode drawerPosition?: string configuration: AnyConfigurationModel @@ -295,9 +296,11 @@ export function isViewModel(thing: unknown): thing is AbstractViewModel { ) } +type Display = { displayId: string } & AnyConfigurationModel + export interface AbstractTrackModel { displays: AbstractDisplayModel[] - configuration: AnyConfigurationModel + configuration: AnyConfigurationModel & { displays: Display[] } } export function isTrackModel(thing: unknown): thing is AbstractTrackModel { diff --git a/packages/product-core/src/Session/Tracks.ts b/packages/product-core/src/Session/Tracks.ts index 018959b386..c24d411429 100644 --- a/packages/product-core/src/Session/Tracks.ts +++ b/packages/product-core/src/Session/Tracks.ts @@ -28,6 +28,13 @@ export function TracksManagerSessionMixin(pluginManager: PluginManager) { get tracks(): AnyConfigurationModel[] { return self.jbrowse.tracks }, + + /** + * #getter + */ + get tracksById(): Record { + return Object.fromEntries(this.tracks.map(t => [t.trackId, t])) + }, })) .actions(self => ({ /** diff --git a/packages/product-core/src/ui/AboutDialog.tsx b/packages/product-core/src/ui/AboutDialog.tsx index d0793da591..b358e141a8 100644 --- a/packages/product-core/src/ui/AboutDialog.tsx +++ b/packages/product-core/src/ui/AboutDialog.tsx @@ -1,18 +1,21 @@ import React from 'react' import { AnyConfigurationModel } from '@jbrowse/core/configuration' import Dialog from '@jbrowse/core/ui/Dialog' -import { getSession, getEnv } from '@jbrowse/core/util' +import { getEnv, AbstractSessionModel } from '@jbrowse/core/util' import { getTrackName } from '@jbrowse/core/util/tracks' + +// locals import AboutContents from './AboutDialogContents' export function AboutDialog({ config, + session, handleClose, }: { config: AnyConfigurationModel + session: AbstractSessionModel handleClose: () => void }) { - const session = getSession(config) const trackName = getTrackName(config, session) const { pluginManager } = getEnv(session) @@ -20,11 +23,14 @@ export function AboutDialog({ 'Core-replaceAbout', AboutContents, { session, config }, - ) as React.FC + ) as React.FC<{ + config: AnyConfigurationModel + session: AbstractSessionModel + }> return ( - + ) } diff --git a/packages/product-core/src/ui/AboutDialogContents.tsx b/packages/product-core/src/ui/AboutDialogContents.tsx index 6596648c04..7789ff72e1 100644 --- a/packages/product-core/src/ui/AboutDialogContents.tsx +++ b/packages/product-core/src/ui/AboutDialogContents.tsx @@ -9,13 +9,14 @@ import { readConfObject, AnyConfigurationModel, } from '@jbrowse/core/configuration' -import { getSession, getEnv } from '@jbrowse/core/util' +import { getEnv, AbstractSessionModel } from '@jbrowse/core/util' import { BaseCard, Attributes, } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail' import FileInfoPanel from './FileInfoPanel' import RefNameInfoDialog from './RefNameInfoDialog' +import { isStateTreeNode } from 'mobx-state-tree' const useStyles = makeStyles()({ content: { @@ -39,12 +40,13 @@ function removeAttr(obj: Record, attr: string) { const AboutDialogContents = observer(function ({ config, + session, }: { config: AnyConfigurationModel + session: AbstractSessionModel }) { const [copied, setCopied] = useState(false) - const conf = readConfObject(config) - const session = getSession(config) + const conf = isStateTreeNode(config) ? readConfObject(config) : config const { classes } = useStyles() const [showRefNames, setShowRefNames] = useState(false) @@ -112,9 +114,10 @@ const AboutDialogContents = observer(function ({ ) : null} - + {showRefNames ? ( { setShowRefNames(false) diff --git a/packages/product-core/src/ui/FileInfoPanel.tsx b/packages/product-core/src/ui/FileInfoPanel.tsx index 365e6ecedd..033b67bedd 100644 --- a/packages/product-core/src/ui/FileInfoPanel.tsx +++ b/packages/product-core/src/ui/FileInfoPanel.tsx @@ -3,7 +3,7 @@ import { readConfObject, AnyConfigurationModel, } from '@jbrowse/core/configuration' -import { getSession } from '@jbrowse/core/util' +import { AbstractSessionModel } from '@jbrowse/core/util' import { BaseCard, Attributes, @@ -14,12 +14,13 @@ type FileInfo = Record | string export default function FileInfoPanel({ config, + session, }: { config: AnyConfigurationModel + session: AbstractSessionModel }) { const [error, setError] = useState() const [info, setInfo] = useState() - const session = getSession(config) const { rpcManager } = session useEffect(() => { diff --git a/packages/product-core/src/ui/RefNameInfoDialog.tsx b/packages/product-core/src/ui/RefNameInfoDialog.tsx index 7382d0e9e4..d801a07948 100644 --- a/packages/product-core/src/ui/RefNameInfoDialog.tsx +++ b/packages/product-core/src/ui/RefNameInfoDialog.tsx @@ -5,7 +5,7 @@ import { AnyConfigurationModel, } from '@jbrowse/core/configuration' import { Dialog, LoadingEllipses } from '@jbrowse/core/ui' -import { getSession } from '@jbrowse/core/util' +import { AbstractSessionModel } from '@jbrowse/core/util' import { getConfAssemblyNames } from '@jbrowse/core/util/tracks' import { observer } from 'mobx-react' import { makeStyles } from 'tss-react/mui' @@ -28,16 +28,17 @@ const useStyles = makeStyles()(theme => ({ const RefNameInfoDialog = observer(function ({ config, + session, onClose, }: { config: AnyConfigurationModel + session: AbstractSessionModel onClose: () => void }) { const [error, setError] = useState() const [refNames, setRefNames] = useState>() const [copied, setCopied] = useState(false) const { classes } = useStyles() - const session = getSession(config) const { rpcManager } = session useEffect(() => { diff --git a/packages/web-core/src/BaseWebSession/index.ts b/packages/web-core/src/BaseWebSession/index.ts index 948d3bc05c..117dc860da 100644 --- a/packages/web-core/src/BaseWebSession/index.ts +++ b/packages/web-core/src/BaseWebSession/index.ts @@ -118,6 +118,12 @@ export function BaseWebSession({ task: undefined, })) .views(self => ({ + /** + * #getter + */ + get tracksById(): Record { + return Object.fromEntries(this.tracks.map(t => [t.trackId, t])) + }, /** * #getter */ @@ -359,7 +365,7 @@ export function BaseWebSession({ onClick: () => { self.queueDialog(handleClose => [ AboutDialog, - { config, handleClose }, + { config, session: self, handleClose }, ]) }, icon: InfoIcon, diff --git a/plugins/circular-view/src/CircularView/models/model.ts b/plugins/circular-view/src/CircularView/models/model.ts index a47c395b4e..01ec8868d2 100644 --- a/plugins/circular-view/src/CircularView/models/model.ts +++ b/plugins/circular-view/src/CircularView/models/model.ts @@ -2,14 +2,11 @@ import React, { lazy } from 'react' import PluginManager from '@jbrowse/core/PluginManager' import { cast, - getRoot, - resolveIdentifier, types, SnapshotOrInstance, Instance, } from 'mobx-state-tree' import { Region } from '@jbrowse/core/util/types/mst' -import { transaction } from 'mobx' import { saveAs } from 'file-saver' import { AnyConfigurationModel, @@ -31,6 +28,11 @@ import PhotoCameraIcon from '@mui/icons-material/PhotoCamera' // locals import { calculateStaticSlices, sliceIsVisible, SliceRegion } from './slices' import { viewportVisibleSection } from './viewportVisibleRegion' +import { + hideTrackGeneric, + showTrackGeneric, + toggleTrackGeneric, +} from '@jbrowse/core/util/tracks' // lazies const ExportSvgDialog = lazy(() => import('../components/ExportSvgDialog')) @@ -490,12 +492,7 @@ function stateModelFactory(pluginManager: PluginManager) { * #action */ toggleTrack(trackId: string) { - const hiddenCount = this.hideTrack(trackId) - if (!hiddenCount) { - this.showTrack(trackId) - return true - } - return false + toggleTrackGeneric(self, trackId) }, /** @@ -509,26 +506,7 @@ function stateModelFactory(pluginManager: PluginManager) { * #action */ showTrack(trackId: string, initialSnapshot = {}) { - const schema = pluginManager.pluggableConfigSchemaType('track') - const conf = resolveIdentifier(schema, getRoot(self), trackId) - const trackType = pluginManager.getTrackType(conf.type) - if (!trackType) { - throw new Error(`unknown track type ${conf.type}`) - } - const viewType = pluginManager.getViewType(self.type)! - const supportedDisplays = new Set( - viewType.displayTypes.map(d => d.name), - ) - const displayConf = conf.displays.find((d: AnyConfigurationModel) => - supportedDisplays.has(d.type), - ) - const track = trackType.stateModel.create({ - ...initialSnapshot, - type: conf.type, - configuration: conf, - displays: [{ type: displayConf.type, configuration: displayConf }], - }) - self.tracks.push(track) + showTrackGeneric(self, trackId, initialSnapshot) }, /** @@ -563,13 +541,7 @@ function stateModelFactory(pluginManager: PluginManager) { * #action */ hideTrack(trackId: string) { - const schema = pluginManager.pluggableConfigSchemaType('track') - const conf = resolveIdentifier(schema, getRoot(self), trackId) - const t = self.tracks.filter(t => t.configuration === conf) - transaction(() => { - t.forEach(t => self.tracks.remove(t)) - }) - return t.length + hideTrackGeneric(self, trackId) }, /** diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.tsx.snap b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.tsx.snap index c5280685f2..e68887360b 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.tsx.snap +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.tsx.snap @@ -44,16 +44,6 @@ exports[`localstorage preference - sorting categories 1`] = ` exports[`localstorage preference - sorting track names 1`] = ` [ - "1 toplevel", - "10 toplevel", - "2 toplevel", - "3 toplevel", - "4 toplevel", - "5 toplevel", - "6 toplevel", - "7 toplevel", - "8 toplevel", - "9 toplevel", "1 cat1", "10 cat1", "2 cat1", @@ -84,6 +74,16 @@ exports[`localstorage preference - sorting track names 1`] = ` "3 cat3 cat4", "4 cat3 cat4", "5 cat3 cat4", + "1 toplevel", + "10 toplevel", + "2 toplevel", + "3 toplevel", + "4 toplevel", + "5 toplevel", + "6 toplevel", + "7 toplevel", + "8 toplevel", + "9 toplevel", ] `; diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackListNode.tsx b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackListNode.tsx index de5ea2478b..a106b0c6ae 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackListNode.tsx +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackListNode.tsx @@ -33,6 +33,10 @@ const useStyles = makeStyles()(theme => ({ display: 'flex', paddingLeft: 5, }, + wrapper: { + whiteSpace: 'nowrap', + width: '100%', + }, })) // An individual node in the track selector. Note: manually sets cursor: @@ -49,8 +53,7 @@ export default function Node({ setOpen: (arg: boolean) => void }) { const { isLeaf, nestingLevel } = data - - const { classes } = useStyles() + const { classes, cx } = useStyles() const width = 10 const marginLeft = nestingLevel * width + (isLeaf ? width : 0) @@ -65,12 +68,11 @@ export default function Node({ /> ))}
{!isLeaf ? ( diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/util.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/util.ts new file mode 100644 index 0000000000..2df9c1300e --- /dev/null +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/util.ts @@ -0,0 +1,46 @@ +import { AnyConfigurationModel } from '@jbrowse/core/configuration' + +// locals +import { TreeNode } from '../../generateHierarchy' +import { HierarchicalTrackSelectorModel } from '../../model' + +export function getNodeData( + node: TreeNode, + nestingLevel: number, + extra: Record, + selection: Record, +) { + const isLeaf = node.type === 'track' + const selected = isLeaf ? selection[node.trackId] : false + return { + data: { + defaultHeight: isLeaf ? 22 : 40, + isLeaf, + isOpenByDefault: true, + nestingLevel, + selected, + ...node, + ...extra, + }, + nestingLevel, + node, + } +} + +export interface NodeData { + nestingLevel: number + checked: boolean + conf: AnyConfigurationModel + drawerPosition: unknown + id: string + isLeaf: boolean + name: string + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + onChange: Function + toggleCollapse: (arg: string) => void + tree: TreeNode + selected: boolean + model: HierarchicalTrackSelectorModel +} + +export type NodeEntry = ReturnType diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedModel.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedModel.ts index 5e8b5f7792..f7e4e07e74 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedModel.ts +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedModel.ts @@ -126,10 +126,7 @@ export function facetedStateTreeF() { category: readConfObject(track, 'category')?.join(', ') as string, adapter: readConfObject(track, 'adapter')?.type as string, description: readConfObject(track, 'description') as string, - metadata: readConfObject(track, 'metadata') as Record< - string, - unknown - >, + metadata: (track.metadata || {}) as Record, } as const }) }, diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedUtil.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedUtil.ts index cdc7e1a587..0be7fe8986 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedUtil.ts +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/facetedUtil.ts @@ -9,5 +9,5 @@ export function findNonSparseKeys( export function getRootKeys(obj: Record) { return Object.entries(obj) .map(([key, val]) => (typeof val === 'string' ? key : '')) - .filter(f => !!f) + .filter((f): f is string => !!f) } diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/filterTracks.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/filterTracks.ts index f6de73545f..8e91a2f35e 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/filterTracks.ts +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/filterTracks.ts @@ -25,6 +25,8 @@ export function filterTracks( const trackListAssemblies = self.assemblyNames .map(a => assemblyManager.get(a)) .filter(notEmpty) + const { displayTypes } = pluginManager.getViewType(view.type)! + const viewDisplays = displayTypes.map((d: { name: string }) => d.name) return tracks .filter(c => { const trackAssemblyNames = readConfObject(c, 'assemblyNames') as @@ -37,10 +39,10 @@ export function filterTracks( ? hasAnyOverlap(trackAssemblies, trackListAssemblies) : hasAllOverlap(trackAssemblies, trackListAssemblies) }) - .filter(c => { - const { displayTypes } = pluginManager.getViewType(view.type)! - const compatDisplays = displayTypes.map(d => d.name) - const trackDisplays = c.displays.map((d: { type: string }) => d.type) - return hasAnyOverlap(compatDisplays, trackDisplays) + + .filter(conf => { + const trackType = pluginManager.getTrackType(conf.type)! + const trackDisplays = trackType.displayTypes.map(d => d.name) + return hasAnyOverlap(viewDisplays, trackDisplays) }) } diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts index 27047339ad..61f359e83b 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts @@ -4,10 +4,10 @@ import { } from '@jbrowse/core/configuration' import { getSession } from '@jbrowse/core/util' import { getTrackName } from '@jbrowse/core/util/tracks' +import { MenuItem } from '@jbrowse/core/ui' // locals import { matches } from './util' -import { MenuItem } from '@jbrowse/core/ui' function sortConfs( confs: AnyConfigurationModel[], @@ -76,7 +76,9 @@ export function generateHierarchy({ activeSortCategories: boolean collapsed: Map view?: { - tracks: { configuration: AnyConfigurationModel }[] + tracks: { + configuration: AnyConfigurationModel + }[] } } noCategories?: boolean @@ -84,7 +86,7 @@ export function generateHierarchy({ trackConfs: AnyConfigurationModel[] extra?: string }): TreeNode[] { - const hierarchy = { children: [] as TreeNode[] } as TreeNode + const hierarchy = { children: [] as TreeNode[] } const { collapsed, filterText, @@ -96,8 +98,8 @@ export function generateHierarchy({ return [] } const session = getSession(model) - const viewTracks = view.tracks const confs = trackConfs.filter(conf => matches(filterText, conf, session)) + const viewTrackIds = new Set(view.tracks.map(f => f.configuration.trackId)) // uses getConf for (const conf of sortConfs( @@ -141,17 +143,14 @@ export function generateHierarchy({ } } - // uses splice to try to put all leaf nodes above "category nodes" if you - // change the splice to a simple push and open - // test_data/test_order/config.json you will see the weirdness - const r = currLevel.children.findIndex(elt => elt.children.length) - const idx = r === -1 ? currLevel.children.length : r + const r = currLevel.children.findLastIndex(elt => !elt.children.length) + const idx = r === -1 ? currLevel.children.length : r + 1 currLevel.children.splice(idx, 0, { id: [extra, conf.trackId].filter(f => !!f).join(','), trackId: conf.trackId, name: getTrackName(conf, session), conf, - checked: viewTracks.some(f => f.configuration === conf), + checked: viewTrackIds.has(conf.trackId), children: [], type: 'track' as const, }) diff --git a/plugins/dotplot-view/src/DotplotView/model.ts b/plugins/dotplot-view/src/DotplotView/model.ts index 40f417f7dd..23e9117347 100644 --- a/plugins/dotplot-view/src/DotplotView/model.ts +++ b/plugins/dotplot-view/src/DotplotView/model.ts @@ -3,9 +3,7 @@ import { addDisposer, cast, getParent, - getRoot, getSnapshot, - resolveIdentifier, types, Instance, SnapshotIn, @@ -13,7 +11,12 @@ import { import { saveAs } from 'file-saver' import { autorun, transaction } from 'mobx' -import { getParentRenderProps } from '@jbrowse/core/util/tracks' +import { + getParentRenderProps, + hideTrackGeneric, + showTrackGeneric, + toggleTrackGeneric, +} from '@jbrowse/core/util/tracks' import { BaseTrackStateModel } from '@jbrowse/core/pluggableElementTypes/models' import BaseViewModel from '@jbrowse/core/pluggableElementTypes/models/BaseViewModel' import { Base1DViewModel } from '@jbrowse/core/util/Base1DViewModel' @@ -373,52 +376,20 @@ export default function stateModelFactory(pm: PluginManager) { * #action */ showTrack(trackId: string, initialSnapshot = {}) { - const schema = pm.pluggableConfigSchemaType('track') - const conf = resolveIdentifier(schema, getRoot(self), trackId) - const trackType = pm.getTrackType(conf.type) - if (!trackType) { - throw new Error(`unknown track type ${conf.type}`) - } - const viewType = pm.getViewType(self.type)! - const displayConf = conf.displays.find((d: AnyConfigurationModel) => - viewType.displayTypes.find(type => type.name === d.type), - ) - if (!displayConf) { - throw new Error( - `could not find a compatible display for view type ${self.type}`, - ) - } - const track = trackType.stateModel.create({ - ...initialSnapshot, - type: conf.type, - configuration: conf, - displays: [{ type: displayConf.type, configuration: displayConf }], - }) - self.tracks.push(track) + return showTrackGeneric(self, trackId, initialSnapshot) }, /** * #action */ hideTrack(trackId: string) { - const schema = pm.pluggableConfigSchemaType('track') - const conf = resolveIdentifier(schema, getRoot(self), trackId) - const t = self.tracks.filter(t => t.configuration === conf) - transaction(() => { - t.forEach(t => self.tracks.remove(t)) - }) - return t.length + return hideTrackGeneric(self, trackId) }, /** * #action */ toggleTrack(trackId: string) { - const hiddenCount = this.hideTrack(trackId) - if (!hiddenCount) { - this.showTrack(trackId) - return true - } - return false + toggleTrackGeneric(self, trackId) }, /** * #action diff --git a/plugins/linear-comparative-view/src/LinearComparativeView/model.ts b/plugins/linear-comparative-view/src/LinearComparativeView/model.ts index edc9b796ad..ce0d23e932 100644 --- a/plugins/linear-comparative-view/src/LinearComparativeView/model.ts +++ b/plugins/linear-comparative-view/src/LinearComparativeView/model.ts @@ -3,20 +3,17 @@ import { addDisposer, cast, getPath, - getRoot, - resolveIdentifier, types, Instance, SnapshotIn, } from 'mobx-state-tree' -import { autorun, transaction } from 'mobx' +import { autorun } from 'mobx' // jbrowse import BaseViewModel from '@jbrowse/core/pluggableElementTypes/models/BaseViewModel' import { MenuItem } from '@jbrowse/core/ui' import { getSession, isSessionModelWithWidgets, avg } from '@jbrowse/core/util' import PluginManager from '@jbrowse/core/PluginManager' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' import { ElementId } from '@jbrowse/core/util/types/mst' import { LinearGenomeViewModel, @@ -26,6 +23,11 @@ import { // icons import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons' import FolderOpenIcon from '@mui/icons-material/FolderOpen' +import { + hideTrackGeneric, + showTrackGeneric, + toggleTrackGeneric, +} from '@jbrowse/core/util/tracks' // lazies const ReturnToImportFormDialog = lazy( @@ -207,60 +209,21 @@ function stateModelFactory(pluginManager: PluginManager) { * #action */ toggleTrack(trackId: string) { - const hiddenCount = this.hideTrack(trackId) - if (!hiddenCount) { - this.showTrack(trackId) - return true - } - return false + toggleTrackGeneric(self, trackId) }, /** * #action */ showTrack(trackId: string, initialSnapshot = {}) { - const schema = pluginManager.pluggableConfigSchemaType('track') - const configuration = resolveIdentifier(schema, getRoot(self), trackId) - if (!configuration) { - throw new Error(`track not found ${trackId}`) - } - const trackType = pluginManager.getTrackType(configuration.type) - if (!trackType) { - throw new Error(`unknown track type ${configuration.type}`) - } - const viewType = pluginManager.getViewType(self.type)! - const supportedDisplays = new Set( - viewType.displayTypes.map(d => d.name), - ) - const displayConf = configuration.displays.find( - (d: AnyConfigurationModel) => supportedDisplays.has(d.type), - ) - if (!displayConf) { - throw new Error( - `could not find a compatible display for view type ${self.type}`, - ) - } - self.tracks.push( - trackType.stateModel.create({ - ...initialSnapshot, - type: configuration.type, - configuration, - displays: [{ type: displayConf.type, configuration: displayConf }], - }), - ) + return showTrackGeneric(self, trackId, initialSnapshot) }, /** * #action */ hideTrack(trackId: string) { - const schema = pluginManager.pluggableConfigSchemaType('track') - const config = resolveIdentifier(schema, getRoot(self), trackId) - const shownTracks = self.tracks.filter(t => t.configuration === config) - transaction(() => { - shownTracks.forEach(t => self.tracks.remove(t)) - }) - return shownTracks.length + return hideTrackGeneric(self, trackId) }, /** * #action diff --git a/plugins/linear-genome-view/src/LinearGenomeView/model.ts b/plugins/linear-genome-view/src/LinearGenomeView/model.ts index dd323f1895..3f61740668 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/model.ts +++ b/plugins/linear-genome-view/src/LinearGenomeView/model.ts @@ -1,5 +1,5 @@ import React, { lazy } from 'react' -import { getConf, AnyConfigurationModel } from '@jbrowse/core/configuration' +import { getConf } from '@jbrowse/core/configuration' import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models' import { Region } from '@jbrowse/core/util/types' import { ElementId } from '@jbrowse/core/util/types/mst' @@ -23,14 +23,17 @@ import BaseResult from '@jbrowse/core/TextSearch/BaseResults' import { BlockSet, BaseBlock } from '@jbrowse/core/util/blockTypes' import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks' import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks' -import { getParentRenderProps } from '@jbrowse/core/util/tracks' -import { when, transaction, autorun } from 'mobx' +import { + getParentRenderProps, + hideTrackGeneric, + showTrackGeneric, + toggleTrackGeneric, +} from '@jbrowse/core/util/tracks' +import { when, autorun } from 'mobx' import { addDisposer, cast, getSnapshot, - getRoot, - resolveIdentifier, types, Instance, getParent, @@ -350,7 +353,6 @@ export function stateModelFactory(pluginManager: PluginManager) { /** * #method */ - MiniControlsComponent(): React.FC { return MiniControls }, @@ -358,7 +360,6 @@ export function stateModelFactory(pluginManager: PluginManager) { /** * #method */ - HeaderComponent(): React.FC { return Header }, @@ -747,58 +748,18 @@ export function stateModelFactory(pluginManager: PluginManager) { initialSnapshot = {}, displayInitialSnapshot = {}, ) { - const schema = pluginManager.pluggableConfigSchemaType('track') - const conf = resolveIdentifier(schema, getRoot(self), trackId) - if (!conf) { - throw new Error(`Could not resolve identifier "${trackId}"`) - } - const trackType = pluginManager.getTrackType(conf?.type) - if (!trackType) { - throw new Error(`Unknown track type ${conf.type}`) - } - const viewType = pluginManager.getViewType(self.type)! - const supportedDisplays = new Set( - viewType.displayTypes.map(d => d.name), - ) - const displayConf = conf.displays.find((d: AnyConfigurationModel) => - supportedDisplays.has(d.type), + return showTrackGeneric( + self, + trackId, + initialSnapshot, + displayInitialSnapshot, ) - if (!displayConf) { - throw new Error( - `Could not find a compatible display for view type ${self.type}`, - ) - } - - const t = self.tracks.filter(t => t.configuration === conf) - if (t.length === 0) { - const track = trackType.stateModel.create({ - ...initialSnapshot, - type: conf.type, - configuration: conf, - displays: [ - { - type: displayConf.type, - configuration: displayConf, - ...displayInitialSnapshot, - }, - ], - }) - self.tracks.push(track) - return track - } - return t[0] }, /** * #action */ hideTrack(trackId: string) { - const schema = pluginManager.pluggableConfigSchemaType('track') - const conf = resolveIdentifier(schema, getRoot(self), trackId) - const t = self.tracks.filter(t => t.configuration === conf) - transaction(() => { - t.forEach(t => self.tracks.remove(t)) - }) - return t.length + return hideTrackGeneric(self, trackId) }, })) .actions(self => ({ @@ -866,14 +827,7 @@ export function stateModelFactory(pluginManager: PluginManager) { * #action */ toggleTrack(trackId: string) { - // if we have any tracks with that configuration, turn them off - const hiddenCount = self.hideTrack(trackId) - // if none had that configuration, turn one on - if (!hiddenCount) { - self.showTrack(trackId) - return true - } - return false + toggleTrackGeneric(self, trackId) }, /** diff --git a/plugins/sv-inspector/src/SvInspectorView/models/SvInspectorView.ts b/plugins/sv-inspector/src/SvInspectorView/models/SvInspectorView.ts index 31643ab581..346837589c 100644 --- a/plugins/sv-inspector/src/SvInspectorView/models/SvInspectorView.ts +++ b/plugins/sv-inspector/src/SvInspectorView/models/SvInspectorView.ts @@ -341,7 +341,7 @@ function SvInspectorViewF(pluginManager: PluginManager) { const { circularView } = self // hide any visible tracks circularView.tracks.forEach(t => - circularView.hideTrack(t.configuration.trackId), + { circularView.hideTrack(t.configuration.trackId) }, ) // put our track in as the only track diff --git a/products/jbrowse-desktop/src/jbrowseModel.ts b/products/jbrowse-desktop/src/jbrowseModel.ts index 844b99d215..8e31f58d46 100644 --- a/products/jbrowse-desktop/src/jbrowseModel.ts +++ b/products/jbrowse-desktop/src/jbrowseModel.ts @@ -17,6 +17,7 @@ window.resolveIdentifier = resolveIdentifier export default function JBrowseDesktop( pluginManager: PluginManager, assemblyConfigSchema: BaseAssemblyConfigSchema, + adminMode = true, ) { - return JBrowseModelF({ pluginManager, assemblyConfigSchema }) + return JBrowseModelF({ pluginManager, assemblyConfigSchema, adminMode }) } diff --git a/products/jbrowse-react-app/src/jbrowseModel.ts b/products/jbrowse-react-app/src/jbrowseModel.ts index c19c477f73..aaf4affa8f 100644 --- a/products/jbrowse-react-app/src/jbrowseModel.ts +++ b/products/jbrowse-react-app/src/jbrowseModel.ts @@ -10,9 +10,11 @@ import { JBrowseModelF } from '@jbrowse/app-core' export default function JBrowseWeb({ pluginManager, assemblyConfigSchema, + adminMode = false, }: { pluginManager: PluginManager assemblyConfigSchema: AnyConfigurationSchemaType + adminMode?: boolean }) { - return JBrowseModelF({ pluginManager, assemblyConfigSchema }) + return JBrowseModelF({ pluginManager, assemblyConfigSchema, adminMode }) } diff --git a/products/jbrowse-web/src/jbrowseModel.test.ts b/products/jbrowse-web/src/jbrowseModel.test.ts index 12c0ed7443..9c16bbca6d 100644 --- a/products/jbrowse-web/src/jbrowseModel.test.ts +++ b/products/jbrowse-web/src/jbrowseModel.test.ts @@ -15,6 +15,7 @@ describe('JBrowse model', () => { .configure() JBrowseModel = jbrowseModelFactory({ + adminMode: false, pluginManager, assemblyConfigSchema: assemblyConfigSchemasFactory(pluginManager), }) diff --git a/products/jbrowse-web/src/jbrowseModel.ts b/products/jbrowse-web/src/jbrowseModel.ts index 0bd2de798a..a8b83fe05f 100644 --- a/products/jbrowse-web/src/jbrowseModel.ts +++ b/products/jbrowse-web/src/jbrowseModel.ts @@ -2,9 +2,6 @@ import { AnyConfigurationSchemaType } from '@jbrowse/core/configuration' import PluginManager from '@jbrowse/core/PluginManager' import { JBrowseModelF } from '@jbrowse/app-core' import { getSnapshot, resolveIdentifier, types } from 'mobx-state-tree' -import clone from 'clone' - -// locals import { removeAttr } from './util' // poke some things for testing (this stuff will eventually be removed) @@ -21,15 +18,18 @@ window.resolveIdentifier = resolveIdentifier export default function JBrowseWeb({ pluginManager, assemblyConfigSchema, + adminMode, }: { pluginManager: PluginManager assemblyConfigSchema: AnyConfigurationSchemaType + adminMode: boolean }) { return types.snapshotProcessor( - JBrowseModelF({ pluginManager, assemblyConfigSchema }), + JBrowseModelF({ pluginManager, assemblyConfigSchema, adminMode }), { - postProcessor(snapshot: Record) { - return removeAttr(clone(snapshot), 'baseUri') + postProcessor(snapshot) { + // @ts-expect-error + return removeAttr(structuredClone(snapshot), 'baseUri') }, }, ) diff --git a/products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap b/products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap index df3c264f5b..a686a14599 100644 --- a/products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap +++ b/products/jbrowse-web/src/rootModel/__snapshots__/index.test.ts.snap @@ -422,16 +422,6 @@ exports[`adds track and connection configs to an assembly 1`] = ` exports[`adds track and connection configs to an assembly 2`] = ` { - "displays": [ - { - "displayId": "trackId0-LinearBasicDisplay", - "type": "LinearBasicDisplay", - }, - { - "displayId": "trackId0-LinearArcDisplay", - "type": "LinearArcDisplay", - }, - ], "trackId": "trackId0", "type": "FeatureTrack", } diff --git a/products/jbrowse-web/src/rootModel/index.test.ts b/products/jbrowse-web/src/rootModel/index.test.ts index a53cf42d42..d29828ee65 100644 --- a/products/jbrowse-web/src/rootModel/index.test.ts +++ b/products/jbrowse-web/src/rootModel/index.test.ts @@ -19,7 +19,7 @@ afterEach(() => { sessionStorage.clear() }) -test('creates with defaults', () => { +it('creates with defaults', () => { const root = getRootModel().create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, @@ -32,7 +32,7 @@ test('creates with defaults', () => { expect(getSnapshot(root.jbrowse.configuration)).toMatchSnapshot() }) -test('creates with a minimal session', () => { +it('creates with a minimal session', () => { const root = getRootModel().create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, @@ -42,7 +42,7 @@ test('creates with a minimal session', () => { expect(root.session).toBeTruthy() }) -test('activates a session snapshot', () => { +it('activates a session snapshot', () => { const session = { name: 'testSession' } localStorage.setItem('localSaved-123', JSON.stringify({ session })) Storage.prototype.getItem = jest.fn( @@ -58,7 +58,7 @@ test('activates a session snapshot', () => { expect(root.session).toBeTruthy() }) -test('adds track and connection configs to an assembly', () => { +it('adds track and connection configs to an assembly', () => { const root = getRootModel().create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, @@ -93,7 +93,7 @@ test('adds track and connection configs to an assembly', () => { type: 'FeatureTrack', trackId: 'trackId0', }) - expect(getSnapshot(newTrackConf)).toMatchSnapshot() + expect(newTrackConf).toMatchSnapshot() expect(root.jbrowse.tracks.length).toBe(1) const newConnectionConf = root.jbrowse.addConnectionConf({ type: 'JBrowse1Connection', @@ -103,7 +103,7 @@ test('adds track and connection configs to an assembly', () => { expect(root.jbrowse.connections.length).toBe(1) }) -test('throws if session is invalid', () => { +it('throws if session is invalid', () => { expect(() => getRootModel().create({ jbrowse: { @@ -114,7 +114,7 @@ test('throws if session is invalid', () => { ).toThrow() }) -test('throws if session snapshot is invalid', () => { +it('throws if session snapshot is invalid', () => { const root = getRootModel().create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, @@ -125,7 +125,7 @@ test('throws if session snapshot is invalid', () => { }).toThrow() }) -test('adds menus', () => { +it('adds menus', () => { const root = getRootModel().create({ jbrowse: { configuration: { rpc: { defaultDriver: 'MainThreadRpcDriver' } }, diff --git a/products/jbrowse-web/src/rootModel/rootModel.ts b/products/jbrowse-web/src/rootModel/rootModel.ts index aeee671146..c35e70a8fd 100644 --- a/products/jbrowse-web/src/rootModel/rootModel.ts +++ b/products/jbrowse-web/src/rootModel/rootModel.ts @@ -90,6 +90,7 @@ export default function RootModel({ }) { const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) const jbrowseModelType = jbrowseWebFactory({ + adminMode, pluginManager, assemblyConfigSchema, })