From e6d30a82e225baa7a13ca14541f5c51bcf62ee6a Mon Sep 17 00:00:00 2001 From: Colin Diesh Date: Thu, 14 Dec 2023 12:40:13 -0500 Subject: [PATCH] Fix "Export SVG" feature in next 14 (#4136) --- packages/core/util/index.ts | 32 +++++++++--- .../src/BreakpointSplitView/model.ts | 3 +- .../svgcomponents/SVGBreakpointSplitView.tsx | 49 +++---------------- .../BreakpointSplitView/svgcomponents/util.ts | 35 +++++++++++++ .../src/BaseChordDisplay/models/model.tsx | 2 +- .../BaseChordDisplay/models/renderReaction.ts | 2 +- .../CircularView/components/CircularView.tsx | 2 +- .../src/CircularView/components/Controls.tsx | 2 +- .../components/ExportSvgDialog.tsx | 2 +- .../src/CircularView/components/Ruler.tsx | 2 +- .../circular-view/src/CircularView/index.ts | 2 +- .../models/{CircularView.ts => model.ts} | 3 +- .../svgcomponents/SVGCircularView.tsx | 9 ++-- .../src/LaunchCircularView/index.ts | 2 +- plugins/circular-view/src/index.ts | 2 +- plugins/dotplot-view/src/DotplotView/model.ts | 3 +- .../svgcomponents/SVGDotplotView.tsx | 7 ++- .../src/LinearSyntenyView/model.ts | 3 +- .../svgcomponents/SVGLinearSyntenyView.tsx | 10 ++-- .../src/LinearGenomeView/model.ts | 3 +- .../svgcomponents/SVGLinearGenomeView.tsx | 40 ++++++--------- .../LinearGenomeView/svgcomponents/util.ts | 22 +++++++++ plugins/linear-genome-view/src/index.ts | 2 +- .../jbrowse-desktop/src/rootModel/index.ts | 3 +- products/jbrowse-img/src/renderRegion.tsx | 6 ++- products/jbrowse-react-app/src/createModel.ts | 4 ++ .../jbrowse-react-app/src/createViewState.ts | 13 ++++- .../jbrowse-react-app/src/rootModel/index.ts | 5 ++ .../src/createModel/createModel.ts | 4 ++ .../src/createViewState.ts | 5 ++ .../JBrowseLinearGenomeView.test.tsx | 7 ++- .../src/createModel/createModel.ts | 4 ++ .../src/createViewState.ts | 5 ++ .../stories/React18.mdx | 11 +++-- .../stories/examples/WithReact18.tsx | 10 ++-- .../jbrowse-web/src/rootModel/rootModel.ts | 3 +- .../breakpoint_split_view_snapshot.svg | 2 +- .../__image_snapshots__/circular_snapshot.svg | 2 +- .../__image_snapshots__/lgv_snapshot.svg | 2 +- .../__snapshots__/ExportSvg.test.tsx.snap | 4 +- ...ExportSvgBreakpointSplitView.test.tsx.snap | 2 +- 41 files changed, 205 insertions(+), 126 deletions(-) create mode 100644 plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/util.ts rename plugins/circular-view/src/CircularView/models/{CircularView.ts => model.ts} (99%) create mode 100644 plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/util.ts diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index 1160769db6..2c0820d566 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import isObject from 'is-object' import PluginManager from '../PluginManager' import { @@ -33,6 +33,8 @@ import { isUriLocation } from './types' // has to be the full path and not the relative path to get the jest mock import useMeasure from '@jbrowse/core/util/useMeasure' import { colord } from './colord' +// eslint-disable-next-line react/no-deprecated +import { flushSync, render } from 'react-dom' export * from './types' export * from './aborting' export * from './when' @@ -1374,6 +1376,29 @@ export function gatherOverlaps(regions: BasicFeature[]) { ) } +export function stripAlpha(str: string) { + const c = colord(str) + return c.alpha(1).toHex() +} + +// https://react.dev/reference/react-dom/server/renderToString#removing-rendertostring-from-the-client-code +export function renderToStaticMarkup( + node: React.ReactElement, + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + }, +) { + const div = document.createElement('div') + flushSync(() => { + if (createRootFn) { + createRootFn(div).render(node) + } else { + render(node, div) + } + }) + return div.innerHTML.replace('>', '>').replace('<', '<') +} + export { default as SimpleFeature, type Feature, @@ -1381,9 +1406,4 @@ export { isFeature, } from './simpleFeature' -export function stripAlpha(str: string) { - const c = colord(str) - return c.alpha(1).toHex() -} - export { blobToDataURL } from './blobToDataURL' diff --git a/plugins/breakpoint-split-view/src/BreakpointSplitView/model.ts b/plugins/breakpoint-split-view/src/BreakpointSplitView/model.ts index 8051187824..b31d402959 100644 --- a/plugins/breakpoint-split-view/src/BreakpointSplitView/model.ts +++ b/plugins/breakpoint-split-view/src/BreakpointSplitView/model.ts @@ -45,8 +45,7 @@ function calc(track: Track, f: Feature) { export interface ExportSvgOptions { rasterizeLayers?: boolean filename?: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Wrapper?: React.FC + Wrapper?: React.FC<{ children: React.ReactNode }> fontSize?: number rulerHeight?: number textHeight?: number diff --git a/plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/SVGBreakpointSplitView.tsx b/plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/SVGBreakpointSplitView.tsx index f1606ecae6..66c933e627 100644 --- a/plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/SVGBreakpointSplitView.tsx +++ b/plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/SVGBreakpointSplitView.tsx @@ -1,60 +1,23 @@ import React from 'react' -import { renderToStaticMarkup } from 'react-dom/server' import { when } from 'mobx' -import { - AbstractSessionModel, - getSession, - max, - measureText, - sum, -} from '@jbrowse/core/util' +import { getSession, renderToStaticMarkup, sum } from '@jbrowse/core/util' import { ThemeProvider } from '@mui/material' import { createJBrowseTheme } from '@jbrowse/core/ui' - -// locals +import { getRoot } from 'mobx-state-tree' import { SVGTracks, SVGRuler, totalHeight, - LinearGenomeViewModel, } from '@jbrowse/plugin-linear-genome-view' // locals import SVGBackground from './SVGBackground' import { ExportSvgOptions, BreakpointViewModel } from '../model' -import { getTrackName } from '@jbrowse/core/util/tracks' import Overlay from '../components/Overlay' +import { getTrackNameMaxLen, getTrackOffsets } from './util' type BSV = BreakpointViewModel -function getTrackNameMaxLen( - views: LinearGenomeViewModel[], - fontSize: number, - session: AbstractSessionModel, -) { - return max( - views.flatMap(view => - view.tracks.map(t => - measureText(getTrackName(t.configuration, session), fontSize), - ), - ), - 0, - ) -} -function getTrackOffsets( - view: LinearGenomeViewModel, - textOffset: number, - extra = 0, -) { - const offsets = {} as Record - let curr = textOffset - for (const track of view.tracks) { - offsets[track.configuration.trackId] = curr + extra - curr += track.displays[0].height + textOffset - } - return offsets -} - // render LGV to SVG export async function renderToSvg(model: BSV, opts: ExportSvgOptions) { const { @@ -63,10 +26,11 @@ export async function renderToSvg(model: BSV, opts: ExportSvgOptions) { rulerHeight = 30, fontSize = 13, trackLabels = 'offset', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Wrapper = ({ children }: any) => <>{children}, + Wrapper = ({ children }) => <>{children}, themeName = 'default', } = opts + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { createRootFn } = getRoot(model) const session = getSession(model) const theme = session.allThemes?.()[themeName] const { width, views } = model @@ -174,5 +138,6 @@ export async function renderToSvg(model: BSV, opts: ExportSvgOptions) { , + createRootFn, ) } diff --git a/plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/util.ts b/plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/util.ts new file mode 100644 index 0000000000..7cdc2a7f69 --- /dev/null +++ b/plugins/breakpoint-split-view/src/BreakpointSplitView/svgcomponents/util.ts @@ -0,0 +1,35 @@ +import { AbstractSessionModel, max, measureText } from '@jbrowse/core/util' + +// locals +import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' + +// locals +import { getTrackName } from '@jbrowse/core/util/tracks' + +export function getTrackNameMaxLen( + views: LinearGenomeViewModel[], + fontSize: number, + session: AbstractSessionModel, +) { + return max( + views.flatMap(view => + view.tracks.map(t => + measureText(getTrackName(t.configuration, session), fontSize), + ), + ), + 0, + ) +} +export function getTrackOffsets( + view: LinearGenomeViewModel, + textOffset: number, + extra = 0, +) { + const offsets = {} as Record + let curr = textOffset + for (const track of view.tracks) { + offsets[track.configuration.trackId] = curr + extra + curr += track.displays[0].height + textOffset + } + return offsets +} diff --git a/plugins/circular-view/src/BaseChordDisplay/models/model.tsx b/plugins/circular-view/src/BaseChordDisplay/models/model.tsx index e5b7157e4a..81d4f5d793 100644 --- a/plugins/circular-view/src/BaseChordDisplay/models/model.tsx +++ b/plugins/circular-view/src/BaseChordDisplay/models/model.tsx @@ -28,7 +28,7 @@ import { renderReactionData, renderReactionEffect } from './renderReaction' import { CircularViewModel, ExportSvgOptions, -} from '../../CircularView/models/CircularView' +} from '../../CircularView/models/model' import { ThemeOptions } from '@mui/material' import { baseChordDisplayConfig } from './configSchema' diff --git a/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts b/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts index 2de175d688..0c79e43f00 100644 --- a/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts +++ b/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts @@ -1,7 +1,7 @@ import clone from 'clone' import { getRpcSessionId } from '@jbrowse/core/util/tracks' import { getSession, getContainingView } from '@jbrowse/core/util' -import { CircularViewModel } from '../../CircularView/models/CircularView' +import { CircularViewModel } from '../../CircularView/models/model' // eslint-disable-next-line @typescript-eslint/no-explicit-any export function renderReactionData(self: any) { diff --git a/plugins/circular-view/src/CircularView/components/CircularView.tsx b/plugins/circular-view/src/CircularView/components/CircularView.tsx index 1056febf7b..bc2c81e333 100644 --- a/plugins/circular-view/src/CircularView/components/CircularView.tsx +++ b/plugins/circular-view/src/CircularView/components/CircularView.tsx @@ -8,7 +8,7 @@ import { makeStyles } from 'tss-react/mui' import Ruler from './Ruler' import Controls from './Controls' import ImportForm from './ImportForm' -import { CircularViewModel } from '../models/CircularView' +import { CircularViewModel } from '../models/model' const dragHandleHeight = 3 diff --git a/plugins/circular-view/src/CircularView/components/Controls.tsx b/plugins/circular-view/src/CircularView/components/Controls.tsx index ff039339a0..8b7270ea15 100644 --- a/plugins/circular-view/src/CircularView/components/Controls.tsx +++ b/plugins/circular-view/src/CircularView/components/Controls.tsx @@ -16,7 +16,7 @@ import MoreVert from '@mui/icons-material/MoreVert' import { TrackSelector as TrackSelectorIcon } from '@jbrowse/core/ui/Icons' // locals -import { CircularViewModel } from '../models/CircularView' +import { CircularViewModel } from '../models/model' import { getSession } from '@jbrowse/core/util' import ExportSvgDlg from './ExportSvgDialog' diff --git a/plugins/circular-view/src/CircularView/components/ExportSvgDialog.tsx b/plugins/circular-view/src/CircularView/components/ExportSvgDialog.tsx index 5acca6ca2c..4d2880038d 100644 --- a/plugins/circular-view/src/CircularView/components/ExportSvgDialog.tsx +++ b/plugins/circular-view/src/CircularView/components/ExportSvgDialog.tsx @@ -13,7 +13,7 @@ import { import { Dialog, ErrorMessage } from '@jbrowse/core/ui' // locals -import { ExportSvgOptions } from '../models/CircularView' +import { ExportSvgOptions } from '../models/model' import { getSession, useLocalStorage } from '@jbrowse/core/util' function LoadingMessage() { diff --git a/plugins/circular-view/src/CircularView/components/Ruler.tsx b/plugins/circular-view/src/CircularView/components/Ruler.tsx index 5c376c4675..9e21316eb3 100644 --- a/plugins/circular-view/src/CircularView/components/Ruler.tsx +++ b/plugins/circular-view/src/CircularView/components/Ruler.tsx @@ -16,7 +16,7 @@ import { SliceElidedRegion, SliceNonElidedRegion, } from '../models/slices' -import { CircularViewModel } from '../models/CircularView' +import { CircularViewModel } from '../models/model' const useStyles = makeStyles()({ rulerLabel: { diff --git a/plugins/circular-view/src/CircularView/index.ts b/plugins/circular-view/src/CircularView/index.ts index 538c6102f0..0f6b6b7291 100644 --- a/plugins/circular-view/src/CircularView/index.ts +++ b/plugins/circular-view/src/CircularView/index.ts @@ -1,7 +1,7 @@ import { lazy } from 'react' import PluginManager from '@jbrowse/core/PluginManager' import ViewType from '@jbrowse/core/pluggableElementTypes/ViewType' -import stateModelFactory from './models/CircularView' +import stateModelFactory from './models/model' export default (pluginManager: PluginManager) => { pluginManager.addViewType( diff --git a/plugins/circular-view/src/CircularView/models/CircularView.ts b/plugins/circular-view/src/CircularView/models/model.ts similarity index 99% rename from plugins/circular-view/src/CircularView/models/CircularView.ts rename to plugins/circular-view/src/CircularView/models/model.ts index c181d00971..e2b57dbf03 100644 --- a/plugins/circular-view/src/CircularView/models/CircularView.ts +++ b/plugins/circular-view/src/CircularView/models/model.ts @@ -39,8 +39,7 @@ const ExportSvgDialog = lazy(() => import('../components/ExportSvgDialog')) export interface ExportSvgOptions { rasterizeLayers?: boolean filename?: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Wrapper?: React.FC + Wrapper?: React.FC<{ children: React.ReactNode }> themeName?: string } diff --git a/plugins/circular-view/src/CircularView/svgcomponents/SVGCircularView.tsx b/plugins/circular-view/src/CircularView/svgcomponents/SVGCircularView.tsx index d5c495c9fe..8448f7f018 100644 --- a/plugins/circular-view/src/CircularView/svgcomponents/SVGCircularView.tsx +++ b/plugins/circular-view/src/CircularView/svgcomponents/SVGCircularView.tsx @@ -1,12 +1,12 @@ import React from 'react' import { ThemeProvider } from '@mui/material' -import { renderToStaticMarkup } from 'react-dom/server' import { when } from 'mobx' -import { getSession, radToDeg } from '@jbrowse/core/util' +import { getSession, radToDeg, renderToStaticMarkup } from '@jbrowse/core/util' import { createJBrowseTheme } from '@jbrowse/core/ui' +import { getRoot } from 'mobx-state-tree' // locals -import { ExportSvgOptions, CircularViewModel } from '../models/CircularView' +import { ExportSvgOptions, CircularViewModel } from '../models/model' import SVGBackground from './SVGBackground' import Ruler from '../components/Ruler' @@ -18,6 +18,8 @@ export async function renderToSvg(model: CGV, opts: ExportSvgOptions) { opts const session = getSession(model) const theme = session.allThemes?.()[themeName] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { createRootFn } = getRoot(model) const { width, tracks, height } = model const shift = 50 const displayResults = await Promise.all( @@ -54,5 +56,6 @@ export async function renderToSvg(model: CGV, opts: ExportSvgOptions) { , + createRootFn, ) } diff --git a/plugins/circular-view/src/LaunchCircularView/index.ts b/plugins/circular-view/src/LaunchCircularView/index.ts index 2d444e3d7e..d01d947078 100644 --- a/plugins/circular-view/src/LaunchCircularView/index.ts +++ b/plugins/circular-view/src/LaunchCircularView/index.ts @@ -3,7 +3,7 @@ import { AbstractSessionModel } from '@jbrowse/core/util' import PluginManager from '@jbrowse/core/PluginManager' // locals -import { CircularViewModel } from '../CircularView/models/CircularView' +import { CircularViewModel } from '../CircularView/models/model' type CGV = CircularViewModel diff --git a/plugins/circular-view/src/index.ts b/plugins/circular-view/src/index.ts index 76e9986924..da8fd7842b 100644 --- a/plugins/circular-view/src/index.ts +++ b/plugins/circular-view/src/index.ts @@ -40,4 +40,4 @@ export { export { type CircularViewModel, type CircularViewStateModel, -} from './CircularView/models/CircularView' +} from './CircularView/models/model' diff --git a/plugins/dotplot-view/src/DotplotView/model.ts b/plugins/dotplot-view/src/DotplotView/model.ts index d05d3f84c9..e40744139f 100644 --- a/plugins/dotplot-view/src/DotplotView/model.ts +++ b/plugins/dotplot-view/src/DotplotView/model.ts @@ -49,8 +49,7 @@ type Coord = [number, number] export interface ExportSvgOptions { rasterizeLayers?: boolean filename?: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Wrapper?: React.FC + Wrapper?: React.FC<{ children: React.ReactNode }> themeName?: string } diff --git a/plugins/dotplot-view/src/DotplotView/svgcomponents/SVGDotplotView.tsx b/plugins/dotplot-view/src/DotplotView/svgcomponents/SVGDotplotView.tsx index 3987c83bbf..d245749971 100644 --- a/plugins/dotplot-view/src/DotplotView/svgcomponents/SVGDotplotView.tsx +++ b/plugins/dotplot-view/src/DotplotView/svgcomponents/SVGDotplotView.tsx @@ -1,9 +1,9 @@ import React from 'react' -import { renderToStaticMarkup } from 'react-dom/server' import { when } from 'mobx' -import { getSession } from '@jbrowse/core/util' +import { getSession, renderToStaticMarkup } from '@jbrowse/core/util' import { ThemeProvider } from '@mui/material' import { createJBrowseTheme } from '@jbrowse/core/ui' +import { getRoot } from 'mobx-state-tree' // locals import { DotplotViewModel, ExportSvgOptions } from '../model' @@ -19,6 +19,8 @@ export async function renderToSvg( await when(() => model.initialized) const { themeName = 'default', Wrapper = ({ children }) => <>{children} } = opts + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { createRootFn } = getRoot(model) const session = getSession(model) const theme = session.allThemes?.()[themeName] const { width, borderX, viewWidth, viewHeight, tracks, height } = model @@ -64,5 +66,6 @@ export async function renderToSvg( , + createRootFn, ) } diff --git a/plugins/linear-comparative-view/src/LinearSyntenyView/model.ts b/plugins/linear-comparative-view/src/LinearSyntenyView/model.ts index 5f0cd0ef1d..d030c4632e 100644 --- a/plugins/linear-comparative-view/src/LinearSyntenyView/model.ts +++ b/plugins/linear-comparative-view/src/LinearSyntenyView/model.ts @@ -22,8 +22,7 @@ export interface ExportSvgOptions { rasterizeLayers?: boolean scale?: number filename?: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Wrapper?: React.FC + Wrapper?: React.FC<{ children: React.ReactNode }> fontSize?: number rulerHeight?: number textHeight?: number diff --git a/plugins/linear-comparative-view/src/LinearSyntenyView/svgcomponents/SVGLinearSyntenyView.tsx b/plugins/linear-comparative-view/src/LinearSyntenyView/svgcomponents/SVGLinearSyntenyView.tsx index e161747f99..40961bdc23 100644 --- a/plugins/linear-comparative-view/src/LinearSyntenyView/svgcomponents/SVGLinearSyntenyView.tsx +++ b/plugins/linear-comparative-view/src/LinearSyntenyView/svgcomponents/SVGLinearSyntenyView.tsx @@ -1,6 +1,6 @@ import React from 'react' import { ThemeProvider } from '@mui/material' -import { renderToStaticMarkup } from 'react-dom/server' +import { getRoot } from 'mobx-state-tree' import { when } from 'mobx' import { getSession, @@ -9,6 +9,7 @@ import { measureText, ReactRendering, renderToAbstractCanvas, + renderToStaticMarkup, sum, } from '@jbrowse/core/util' import { getTrackName } from '@jbrowse/core/util/tracks' @@ -35,8 +36,7 @@ export async function renderToSvg(model: LSV, opts: ExportSvgOptions) { rulerHeight = 30, fontSize = 13, trackLabels = 'offset', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Wrapper = ({ children }: any) => <>{children}, + Wrapper = ({ children }) => <>{children}, themeName = 'default', } = opts const session = getSession(model) @@ -44,7 +44,8 @@ export async function renderToSvg(model: LSV, opts: ExportSvgOptions) { const { width, views, middleComparativeHeight: synH, tracks } = model const shift = 50 const offset = headerHeight + rulerHeight - + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { createRootFn } = getRoot(model) const heights = views.map( v => totalHeight(v.tracks, textHeight, trackLabels) + offset, ) @@ -172,5 +173,6 @@ export async function renderToSvg(model: LSV, opts: ExportSvgOptions) { , + createRootFn, ) } diff --git a/plugins/linear-genome-view/src/LinearGenomeView/model.ts b/plugins/linear-genome-view/src/LinearGenomeView/model.ts index f414e1f041..e30276001a 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/model.ts +++ b/plugins/linear-genome-view/src/LinearGenomeView/model.ts @@ -83,8 +83,7 @@ export interface BpOffset { export interface ExportSvgOptions { rasterizeLayers?: boolean filename?: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Wrapper?: React.FC + Wrapper?: React.FC<{ children: React.ReactNode }> fontSize?: number rulerHeight?: number textHeight?: number diff --git a/plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/SVGLinearGenomeView.tsx b/plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/SVGLinearGenomeView.tsx index 2e72a2a3c7..52d135938c 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/SVGLinearGenomeView.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/SVGLinearGenomeView.tsx @@ -1,42 +1,26 @@ /* eslint-disable react-refresh/only-export-components */ import React from 'react' -import { renderToStaticMarkup } from 'react-dom/server' import { when } from 'mobx' -import { getSession, max, measureText, sum } from '@jbrowse/core/util' +import { + getSession, + max, + measureText, + renderToStaticMarkup, +} from '@jbrowse/core/util' import { ThemeProvider } from '@mui/material' import { createJBrowseTheme } from '@jbrowse/core/ui' +import { getTrackName } from '@jbrowse/core/util/tracks' +import { getRoot } from 'mobx-state-tree' // locals import { LinearGenomeViewModel, ExportSvgOptions } from '..' import SVGBackground from './SVGBackground' import SVGTracks from './SVGTracks' import SVGHeader from './SVGHeader' - -import { getTrackName } from '@jbrowse/core/util/tracks' +import { totalHeight } from './util' type LGV = LinearGenomeViewModel -interface Display { - height: number -} -interface Track { - displays: Display[] -} - -export function totalHeight( - tracks: Track[], - textHeight: number, - trackLabels: string, -) { - return sum( - tracks.map( - t => - t.displays[0].height + - (['none', 'left'].includes(trackLabels) ? 0 : textHeight), - ), - ) -} - // render LGV to SVG export async function renderToSvg(model: LGV, opts: ExportSvgOptions) { await when(() => model.initialized) @@ -51,7 +35,10 @@ export async function renderToSvg(model: LGV, opts: ExportSvgOptions) { Wrapper = ({ children }) => <>{children}, } = opts const session = getSession(model) - const theme = session.allThemes?.()[themeName] + const { allThemes } = session + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { createRootFn } = getRoot(model) + const theme = allThemes?.()[themeName] const { width, tracks, showCytobands } = model const shift = 50 const c = +showCytobands * cytobandHeight @@ -108,6 +95,7 @@ export async function renderToSvg(model: LGV, opts: ExportSvgOptions) { , + createRootFn, ) } diff --git a/plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/util.ts b/plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/util.ts new file mode 100644 index 0000000000..50d1119671 --- /dev/null +++ b/plugins/linear-genome-view/src/LinearGenomeView/svgcomponents/util.ts @@ -0,0 +1,22 @@ +import { sum } from '@jbrowse/core/util' + +interface Display { + height: number +} +interface Track { + displays: Display[] +} + +export function totalHeight( + tracks: Track[], + textHeight: number, + trackLabels: string, +) { + return sum( + tracks.map( + t => + t.displays[0].height + + (['none', 'left'].includes(trackLabels) ? 0 : textHeight), + ), + ) +} diff --git a/plugins/linear-genome-view/src/index.ts b/plugins/linear-genome-view/src/index.ts index 07852b5d36..d1d34986d1 100644 --- a/plugins/linear-genome-view/src/index.ts +++ b/plugins/linear-genome-view/src/index.ts @@ -102,9 +102,9 @@ export { export { renderToSvg, SVGTracks, - totalHeight, SVGRuler, } from './LinearGenomeView/svgcomponents/SVGLinearGenomeView' +export { totalHeight } from './LinearGenomeView/svgcomponents/util' export { configSchema as linearBasicDisplayConfigSchemaFactory, modelFactory as linearBasicDisplayModelFactory, diff --git a/products/jbrowse-desktop/src/rootModel/index.ts b/products/jbrowse-desktop/src/rootModel/index.ts index 6b5a4fb759..157bf6cfb4 100644 --- a/products/jbrowse-desktop/src/rootModel/index.ts +++ b/products/jbrowse-desktop/src/rootModel/index.ts @@ -11,7 +11,7 @@ import { InternetAccountsRootModelMixin, } from '@jbrowse/product-core' import { HistoryManagementMixin, RootAppMenuMixin } from '@jbrowse/app-core' -import { hydrateRoot } from 'react-dom/client' +import { createRoot, hydrateRoot } from 'react-dom/client' // locals import jobsModelFactory from '../indexJobsModel' @@ -79,6 +79,7 @@ export default function rootModelFactory({ version: packageJSON.version, adminMode: true, hydrateFn: hydrateRoot, + createRootFn: createRoot, rpcManager: new RpcManager( pluginManager, self.jbrowse.configuration.rpc, diff --git a/products/jbrowse-img/src/renderRegion.tsx b/products/jbrowse-img/src/renderRegion.tsx index f2cccc305c..f62d4ecb5e 100644 --- a/products/jbrowse-img/src/renderRegion.tsx +++ b/products/jbrowse-img/src/renderRegion.tsx @@ -12,6 +12,7 @@ import fs from 'fs' // local import { Entry } from './parseArgv' import { booleanize } from './util' +import { createRoot } from 'react-dom/client' export interface Opts { noRasterize?: boolean @@ -416,7 +417,10 @@ function process( } export async function renderRegion(opts: Opts) { - const model = createViewState(readData(opts)) + const model = createViewState({ + ...readData(opts), + createRootFn: createRoot, + }) const { loc, width = 1500, diff --git a/products/jbrowse-react-app/src/createModel.ts b/products/jbrowse-react-app/src/createModel.ts index 3561595c2c..71fe9719b9 100644 --- a/products/jbrowse-react-app/src/createModel.ts +++ b/products/jbrowse-react-app/src/createModel.ts @@ -18,6 +18,9 @@ export default function createModel( initialChildren: React.ReactNode, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any, + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + }, ) { const pluginManager = new PluginManager([ ...corePlugins.map(P => ({ plugin: new P(), metadata: { isCore: true } })), @@ -30,6 +33,7 @@ export default function createModel( sessionModelFactory, makeWorkerInstance, hydrateFn, + createRootFn, }), pluginManager, } diff --git a/products/jbrowse-react-app/src/createViewState.ts b/products/jbrowse-react-app/src/createViewState.ts index f07b72c93a..80101d483c 100644 --- a/products/jbrowse-react-app/src/createViewState.ts +++ b/products/jbrowse-react-app/src/createViewState.ts @@ -41,13 +41,24 @@ export default function createViewState(opts: { initialChildren: React.ReactNode, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + } }) { - const { config, plugins = [], onChange, makeWorkerInstance, hydrateFn } = opts + const { + config, + plugins = [], + onChange, + makeWorkerInstance, + hydrateFn, + createRootFn, + } = opts const { defaultSession = { name: 'NewSession' } } = config const { model, pluginManager } = createModel( plugins, makeWorkerInstance, hydrateFn, + createRootFn, ) const stateTree = model.create( { diff --git a/products/jbrowse-react-app/src/rootModel/index.ts b/products/jbrowse-react-app/src/rootModel/index.ts index d4448bbeeb..3c07caf667 100644 --- a/products/jbrowse-react-app/src/rootModel/index.ts +++ b/products/jbrowse-react-app/src/rootModel/index.ts @@ -67,6 +67,7 @@ export default function RootModel({ throw new Error('no makeWorkerInstance supplied') }, hydrateFn, + createRootFn, }: { pluginManager: PluginManager sessionModelFactory: SessionModelFactory @@ -76,6 +77,9 @@ export default function RootModel({ initialChildren: React.ReactNode, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + } }) { const assemblyConfigSchema = assemblyConfigSchemaFactory(pluginManager) return types @@ -110,6 +114,7 @@ export default function RootModel({ }, ), hydrateFn, + createRootFn, textSearchManager: new TextSearchManager(pluginManager), error: undefined as unknown, })) 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 713c23e325..054c57a59c 100644 --- a/products/jbrowse-react-circular-genome-view/src/createModel/createModel.ts +++ b/products/jbrowse-react-circular-genome-view/src/createModel/createModel.ts @@ -26,6 +26,9 @@ export default function createModel( initialChildren: React.ReactNode, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any, + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + }, ) { const pluginManager = new PluginManager( [...corePlugins, ...runtimePlugins].map(P => new P()), @@ -143,6 +146,7 @@ export default function createModel( MainThreadRpcDriver: {}, }), hydrateFn, + createRootFn, textSearchManager: new TextSearchManager(pluginManager), })) return { model: rootModel, pluginManager } diff --git a/products/jbrowse-react-circular-genome-view/src/createViewState.ts b/products/jbrowse-react-circular-genome-view/src/createViewState.ts index 593b810b54..dfdd7c5b5c 100644 --- a/products/jbrowse-react-circular-genome-view/src/createViewState.ts +++ b/products/jbrowse-react-circular-genome-view/src/createViewState.ts @@ -27,6 +27,9 @@ interface ViewStateOptions { initialChildren: React.ReactNode, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + } defaultSession?: SessionSnapshot onChange?: (patch: IJsonPatch, reversePatch: IJsonPatch) => void } @@ -40,6 +43,7 @@ export default function createViewState(opts: ViewStateOptions) { aggregateTextSearchAdapters, plugins, hydrateFn, + createRootFn, makeWorkerInstance, onChange, } = opts @@ -47,6 +51,7 @@ export default function createViewState(opts: ViewStateOptions) { plugins || [], makeWorkerInstance, hydrateFn, + createRootFn, ) let { defaultSession } = opts if (!defaultSession) { diff --git a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/JBrowseLinearGenomeView.test.tsx b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/JBrowseLinearGenomeView.test.tsx index 9861ea8935..c7af1a8963 100644 --- a/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/JBrowseLinearGenomeView.test.tsx +++ b/products/jbrowse-react-linear-genome-view/src/JBrowseLinearGenomeView/JBrowseLinearGenomeView.test.tsx @@ -65,11 +65,16 @@ test(' renders successfully', async () => { tracks: [], defaultSession, }) - const { container, getAllByTestId } = render( + const { container, getAllByTestId, getByPlaceholderText } = render( , ) + + const getInputValue = () => + (getByPlaceholderText('Search for location') as HTMLInputElement).value await waitFor(() => expect(getAllByTestId('sequence_track').length).toBe(2), { timeout, }) + await waitFor(() => expect(getInputValue()).toBe('ctgA:1..40'), { timeout }) + expect(container).toMatchSnapshot() }, 40000) 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 fe4533ebae..abc8fc9813 100644 --- a/products/jbrowse-react-linear-genome-view/src/createModel/createModel.ts +++ b/products/jbrowse-react-linear-genome-view/src/createModel/createModel.ts @@ -27,6 +27,9 @@ export default function createModel( initialChildren: React.ReactNode, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any, + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + }, ) { const pluginManager = new PluginManager( [...corePlugins, ...runtimePlugins].map(P => new P()), @@ -69,6 +72,7 @@ export default function createModel( MainThreadRpcDriver: {}, }), hydrateFn, + createRootFn, textSearchManager: new TextSearchManager(pluginManager), adminMode: false, version, diff --git a/products/jbrowse-react-linear-genome-view/src/createViewState.ts b/products/jbrowse-react-linear-genome-view/src/createViewState.ts index 3a5c5128ad..2f48bccc71 100644 --- a/products/jbrowse-react-linear-genome-view/src/createViewState.ts +++ b/products/jbrowse-react-linear-genome-view/src/createViewState.ts @@ -38,6 +38,9 @@ interface ViewStateOptions { initialChildren: React.ReactNode, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any + createRootFn?: (elt: Element | DocumentFragment) => { + render: (node: React.ReactElement) => unknown + } } export default function createViewState(opts: ViewStateOptions) { @@ -53,11 +56,13 @@ export default function createViewState(opts: ViewStateOptions) { disableAddTracks = false, makeWorkerInstance, hydrateFn, + createRootFn, } = opts const { model, pluginManager } = createModel( plugins || [], makeWorkerInstance, hydrateFn, + createRootFn, ) let { defaultSession } = opts if (!defaultSession) { diff --git a/products/jbrowse-react-linear-genome-view/stories/React18.mdx b/products/jbrowse-react-linear-genome-view/stories/React18.mdx index f9ba46b7bd..9e788f57fd 100644 --- a/products/jbrowse-react-linear-genome-view/stories/React18.mdx +++ b/products/jbrowse-react-linear-genome-view/stories/React18.mdx @@ -11,12 +11,13 @@ If you are using: - AND React 18+ - AND the createRoot API -then you will also want to pass a hydrateFn to createViewState that corresponds -to react-dom/client::hydrateRoot. JBrowse will fallback to normal -react-dom::hydrate if it is not specified, but you will likely get more -consistent react 18 behavior by specifying this. +then you will also want to pass a hydrateFn and createRootFn to createViewState +that corresponds to react-dom/client::hydrateRoot. -the reason for all this is that JBrowse uses the hydrate function, but cannot +JBrowse will fallback to normal react-dom::hydrate if it is not specified, but +you will likely get more consistent react 18 behavior by specifying this. + +The reason for all this is that JBrowse uses the hydrate function, but cannot internally try to import 'react-dom/client' without causing chaos for users who still use e.g. React v17 diff --git a/products/jbrowse-react-linear-genome-view/stories/examples/WithReact18.tsx b/products/jbrowse-react-linear-genome-view/stories/examples/WithReact18.tsx index 1d834c3646..ef563d976e 100644 --- a/products/jbrowse-react-linear-genome-view/stories/examples/WithReact18.tsx +++ b/products/jbrowse-react-linear-genome-view/stories/examples/WithReact18.tsx @@ -4,7 +4,7 @@ import React from 'react' import { createViewState, JBrowseLinearGenomeView } from '../../src' import makeWorkerInstance from '../../src/makeWorkerInstance' import { getVolvoxConfig } from './util' -import { hydrateRoot } from 'react-dom/client' +import { createRoot, hydrateRoot } from 'react-dom/client' export const WithReact18 = () => { const { assembly, tracks } = getVolvoxConfig() @@ -18,12 +18,8 @@ export const WithReact18 = () => { }, makeWorkerInstance, - // can just say hydrateRoot:hydrateFn in your code - hydrateFn: (...args) => { - // eslint-disable-next-line no-console - console.log('calling your custom hydrate fn') - hydrateRoot(...args) - }, + hydrateFn: hydrateRoot, + createRootFn: createRoot, }) return (
diff --git a/products/jbrowse-web/src/rootModel/rootModel.ts b/products/jbrowse-web/src/rootModel/rootModel.ts index d9bc7a4e51..e7be402141 100644 --- a/products/jbrowse-web/src/rootModel/rootModel.ts +++ b/products/jbrowse-web/src/rootModel/rootModel.ts @@ -48,7 +48,7 @@ import { BaseRootModelFactory, } from '@jbrowse/product-core' import { HistoryManagementMixin, RootAppMenuMixin } from '@jbrowse/app-core' -import { hydrateRoot } from 'react-dom/client' +import { hydrateRoot, createRoot } from 'react-dom/client' import { AssemblyManager } from '@jbrowse/plugin-data-management' // locals @@ -118,6 +118,7 @@ export default function RootModel({ .volatile(self => ({ version: packageJSON.version, hydrateFn: hydrateRoot, + createRootFn: createRoot, pluginsUpdated: false, rpcManager: new RpcManager( pluginManager, diff --git a/products/jbrowse-web/src/tests/__image_snapshots__/breakpoint_split_view_snapshot.svg b/products/jbrowse-web/src/tests/__image_snapshots__/breakpoint_split_view_snapshot.svg index a1f53942f6..546a9a0109 100644 --- a/products/jbrowse-web/src/tests/__image_snapshots__/breakpoint_split_view_snapshot.svg +++ b/products/jbrowse-web/src/tests/__image_snapshots__/breakpoint_split_view_snapshot.svg @@ -1 +1 @@ -hg19chr3186,698,000186,700,000chr3186,702,000186,704,0000077HG002.hs37d5.11kbpbsv.BND.3:186700648-6:56758392T[6:56758392[pbsv.BND.3:186700648-6:56758392T[6:56758392[HG002.hs37d5.bndshg19chr656,754,00056,756,00056,758,000chr656,758,00056,760,00056,762,0000088HG002.hs37d5.11kbpbsv.BND.6:56758392-3:186700648]3:186700648]Tpbsv.BND.6:56758392-3:186700648]3:186700648]THG002.hs37d5.bnds \ No newline at end of file +hg19chr3186,698,000186,700,000chr3186,702,000186,704,0000077HG002.hs37d5.11kbpbsv.BND.3:186700648-6:56758392T[6:56758392[pbsv.BND.3:186700648-6:56758392T[6:56758392[HG002.hs37d5.bndshg19chr656,754,00056,756,00056,758,000chr656,758,00056,760,00056,762,0000088HG002.hs37d5.11kbpbsv.BND.6:56758392-3:186700648]3:186700648]Tpbsv.BND.6:56758392-3:186700648]3:186700648]THG002.hs37d5.bnds \ No newline at end of file diff --git a/products/jbrowse-web/src/tests/__image_snapshots__/circular_snapshot.svg b/products/jbrowse-web/src/tests/__image_snapshots__/circular_snapshot.svg index ce468d7534..db5b034c3f 100644 --- a/products/jbrowse-web/src/tests/__image_snapshots__/circular_snapshot.svg +++ b/products/jbrowse-web/src/tests/__image_snapshots__/circular_snapshot.svg @@ -1,4 +1,4 @@ -ctgActgActgBctgB