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 374ed87918..62576f1484 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 847701b6be..7781bfe31e 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 Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes' import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard' // locals 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 bc7959fcc4..350fae27e0 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 Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes' import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard' import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui' @@ -12,12 +12,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 55c8817297..7c4895e818 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, ErrorMessage, 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' @@ -27,16 +27,18 @@ const useStyles = makeStyles()(theme => ({ const RefNameInfoDialog = observer(function ({ config, + session, onClose, }: { config: AnyConfigurationModel + session: AbstractSessionModel onClose: () => void }) { const { classes } = useStyles() const [error, setError] = useState() const [refNames, setRefNames] = useState>() const [copied, setCopied] = useState(false) - const { rpcManager } = getSession(config) + const { rpcManager } = session useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/web-core/src/BaseWebSession/index.ts b/packages/web-core/src/BaseWebSession/index.ts index 8ea0bafc5b..4341f5b552 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 */ @@ -360,7 +366,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..6f95cbcd02 100644 --- a/plugins/circular-view/src/CircularView/models/model.ts +++ b/plugins/circular-view/src/CircularView/models/model.ts @@ -1,15 +1,7 @@ import React, { lazy } from 'react' import PluginManager from '@jbrowse/core/PluginManager' -import { - cast, - getRoot, - resolveIdentifier, - types, - SnapshotOrInstance, - Instance, -} from 'mobx-state-tree' +import { cast, 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 +23,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 +487,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 +501,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 +536,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 2b39ae20cd..39415b286d 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/LinearSyntenyViewHelper/stateModelFactory.ts b/plugins/linear-comparative-view/src/LinearSyntenyViewHelper/stateModelFactory.ts index 1480fa52fd..c385dbcc47 100644 --- a/plugins/linear-comparative-view/src/LinearSyntenyViewHelper/stateModelFactory.ts +++ b/plugins/linear-comparative-view/src/LinearSyntenyViewHelper/stateModelFactory.ts @@ -1,15 +1,12 @@ -import { - getRoot, - resolveIdentifier, - types, - Instance, - getParent, -} from 'mobx-state-tree' +import { types, Instance, getParent } from 'mobx-state-tree' import PluginManager from '@jbrowse/core/PluginManager' -import { transaction } from 'mobx' -import { AnyConfigurationModel } from '@jbrowse/core/configuration' import { ElementId } from '@jbrowse/core/util/types/mst' import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' +import { + hideTrackGeneric, + showTrackGeneric, + toggleTrackGeneric, +} from '@jbrowse/core/util/tracks' export function linearSyntenyViewHelperModelFactory( pluginManager: PluginManager, @@ -52,67 +49,20 @@ export function linearSyntenyViewHelperModelFactory( * #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 */ toggleTrack(trackId: string) { - const hiddenCount = this.hideTrack(trackId) - if (!hiddenCount) { - this.showTrack(trackId) - return true - } - return false + toggleTrackGeneric(self, trackId) }, })) .views(self => ({ diff --git a/plugins/linear-genome-view/src/LinearGenomeView/model.ts b/plugins/linear-genome-view/src/LinearGenomeView/model.ts index dd3740bb2e..ca96305ce2 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, @@ -349,7 +352,6 @@ export function stateModelFactory(pluginManager: PluginManager) { /** * #method */ - MiniControlsComponent(): React.FC { return MiniControls }, @@ -357,7 +359,6 @@ export function stateModelFactory(pluginManager: PluginManager) { /** * #method */ - HeaderComponent(): React.FC { return Header }, @@ -746,58 +747,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 => ({ @@ -865,14 +826,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 58954bfdd6..bf2afa235f 100644 --- a/plugins/sv-inspector/src/SvInspectorView/models/SvInspectorView.ts +++ b/plugins/sv-inspector/src/SvInspectorView/models/SvInspectorView.ts @@ -340,9 +340,9 @@ function SvInspectorViewF(pluginManager: PluginManager) { const { assemblyName, generatedTrackConf } = data const { circularView } = self // hide any visible tracks - circularView.tracks.forEach(t => - circularView.hideTrack(t.configuration.trackId), - ) + circularView.tracks.forEach(t => { + circularView.hideTrack(t.configuration.trackId) + }) // put our track in as the only track if (assemblyName) { 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, })