diff --git a/packages/toolpad-app/pages/app/[appId]/[version]/[[...path]].tsx b/packages/toolpad-app/pages/app/[appId]/[version]/[[...path]].tsx index b92fb436a8a..b8f3d6b3f35 100644 --- a/packages/toolpad-app/pages/app/[appId]/[version]/[[...path]].tsx +++ b/packages/toolpad-app/pages/app/[appId]/[version]/[[...path]].tsx @@ -2,9 +2,10 @@ import type { GetServerSideProps, NextPage } from 'next'; import * as React from 'react'; import { asArray } from '../../../../src/utils/collections'; import ToolpadApp, { ToolpadAppProps } from '../../../../src/runtime/ToolpadApp'; +import { createRenderTree } from '../../../../src/appDom'; export const getServerSideProps: GetServerSideProps = async (context) => { - const { loadVersionedDom, parseVersion } = await import('../../../../src/server/data'); + const { loadDom, parseVersion } = await import('../../../../src/server/data'); const [appId] = asArray(context.query.appId); const version = parseVersion(context.query.version); @@ -14,12 +15,12 @@ export const getServerSideProps: GetServerSideProps = async (co }; } - const dom = await loadVersionedDom(appId, version); + const dom = await loadDom(appId, version); return { props: { appId, - dom, + dom: createRenderTree(dom), version, basename: `/app/${appId}/${version}`, }, diff --git a/packages/toolpad-app/pages/deploy/[appId]/[[...path]].tsx b/packages/toolpad-app/pages/deploy/[appId]/[[...path]].tsx index 5d6ebac8416..7e68ab02610 100644 --- a/packages/toolpad-app/pages/deploy/[appId]/[[...path]].tsx +++ b/packages/toolpad-app/pages/deploy/[appId]/[[...path]].tsx @@ -2,9 +2,10 @@ import type { GetServerSideProps, NextPage } from 'next'; import * as React from 'react'; import { asArray } from '../../../src/utils/collections'; import ToolpadApp, { ToolpadAppProps } from '../../../src/runtime/ToolpadApp'; +import { createRenderTree } from '../../../src/appDom'; export const getServerSideProps: GetServerSideProps = async (context) => { - const { loadVersionedDom, findActiveDeployment } = await import('../../../src/server/data'); + const { loadDom, findActiveDeployment } = await import('../../../src/server/data'); const [appId] = asArray(context.query.appId); @@ -24,12 +25,12 @@ export const getServerSideProps: GetServerSideProps = async (co const { version } = activeDeployment; - const dom = await loadVersionedDom(appId, version); + const dom = await loadDom(appId, version); return { props: { appId, - dom, + dom: createRenderTree(dom), version, basename: `/deploy/${appId}`, }, diff --git a/packages/toolpad-app/src/appDom.ts b/packages/toolpad-app/src/appDom.ts index 5b6e33bcfc1..ddf72736371 100644 --- a/packages/toolpad-app/src/appDom.ts +++ b/packages/toolpad-app/src/appDom.ts @@ -768,15 +768,26 @@ export function getNewParentIndexAfterNode( return createFractionalIndex(node.parentIndex, nodeAfter?.parentIndex || null); } +const RENDERTREE_NODES = ['app', 'page', 'element', 'query', 'theme', 'codeComponent'] as const; + +export type RenderTreeNodeType = typeof RENDERTREE_NODES[number]; +export type RenderTreeNode = { [K in RenderTreeNodeType]: AppDomNodeOfType }[RenderTreeNodeType]; +export type RenderTreeNodes = Record; + +export interface RenderTree { + root: NodeId; + nodes: RenderTreeNodes; +} + /** * We need to make sure no secrets end up in the frontend html, so let's only send the * nodes that we need to build frontend, and that we know don't contain secrets. * TODO: Would it make sense to create a separate datastructure that represents the render tree? */ -export function createRenderTree(dom: AppDom): AppDom { - const frontendNodes = new Set(['app', 'page', 'element', 'query', 'theme', 'codeComponent']); +export function createRenderTree(dom: AppDom): RenderTree { + const frontendNodes = new Set(RENDERTREE_NODES); return { ...dom, - nodes: filterValues(dom.nodes, (node) => frontendNodes.has(node.type)) as AppDomNodes, + nodes: filterValues(dom.nodes, (node) => frontendNodes.has(node.type)) as RenderTreeNodes, }; } diff --git a/packages/toolpad-app/src/components/AppEditor/CodeComponentEditor/index.tsx b/packages/toolpad-app/src/components/AppEditor/CodeComponentEditor/index.tsx index 3586e52b229..6dfaa5e678a 100644 --- a/packages/toolpad-app/src/components/AppEditor/CodeComponentEditor/index.tsx +++ b/packages/toolpad-app/src/components/AppEditor/CodeComponentEditor/index.tsx @@ -64,12 +64,12 @@ const EXTRA_LIBS_HTTP_MODULES = [ ]; interface CodeComponentEditorContentProps { - theme?: appDom.ThemeNode; codeComponentNode: appDom.CodeComponentNode; } -function CodeComponentEditorContent({ theme, codeComponentNode }: CodeComponentEditorContentProps) { +function CodeComponentEditorContent({ codeComponentNode }: CodeComponentEditorContentProps) { const domApi = useDomApi(); + const dom = useDom(); const { data: typings } = useQuery>(['/typings.json'], async () => { return fetch('/typings.json').then((res) => res.json()); @@ -180,7 +180,7 @@ function CodeComponentEditorContent({ theme, codeComponentNode }: CodeComponentE resetKeys={[CodeComponent]} fallbackRender={({ error: runtimeError }) => } > - + @@ -203,14 +203,8 @@ export default function CodeComponentEditor({ appId }: CodeComponentEditorProps) const dom = useDom(); const { nodeId } = useParams(); const codeComponentNode = appDom.getMaybeNode(dom, nodeId as NodeId, 'codeComponent'); - const root = appDom.getApp(dom); - const { themes = [] } = appDom.getChildNodes(dom, root); return codeComponentNode ? ( - + ) : ( Non-existing Code Component "{nodeId}" ); diff --git a/packages/toolpad-app/src/runtime/AppCanvas/index.tsx b/packages/toolpad-app/src/runtime/AppCanvas/index.tsx index 5d79b30c244..85de647af67 100644 --- a/packages/toolpad-app/src/runtime/AppCanvas/index.tsx +++ b/packages/toolpad-app/src/runtime/AppCanvas/index.tsx @@ -5,7 +5,7 @@ import * as appDom from '../../appDom'; export interface AppCanvasState { appId: string; - dom: appDom.AppDom; + dom: appDom.RenderTree; } export interface ToolpadBridge { diff --git a/packages/toolpad-app/src/runtime/AppThemeProvider.tsx b/packages/toolpad-app/src/runtime/AppThemeProvider.tsx index d505bd3a552..50711430953 100644 --- a/packages/toolpad-app/src/runtime/AppThemeProvider.tsx +++ b/packages/toolpad-app/src/runtime/AppThemeProvider.tsx @@ -32,11 +32,17 @@ export function createToolpadTheme(themeNode?: appDom.ThemeNode | null): Theme { } export interface ThemeProviderProps { - node?: appDom.ThemeNode | null; + dom: appDom.AppDom; children?: React.ReactNode; } -export default function AppThemeProvider({ node, children }: ThemeProviderProps) { - const theme = React.useMemo(() => createToolpadTheme(node), [node]); +export default function AppThemeProvider({ dom, children }: ThemeProviderProps) { + const theme = React.useMemo(() => { + const root = appDom.getApp(dom); + const { themes = [] } = appDom.getChildNodes(dom, root); + const themeNode = themes.length > 0 ? themes[0] : null; + return createToolpadTheme(themeNode); + }, [dom]); + return {children}; } diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index aee502662d0..6a145bbadf2 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -551,6 +551,29 @@ function RenderedPage({ nodeId }: RenderedNodeProps) { ); } +interface RenderedPagesProps { + dom: appDom.AppDom; +} + +function RenderedPages({ dom }: RenderedPagesProps) { + const root = appDom.getApp(dom); + const { pages = [] } = appDom.getChildNodes(dom, root); + + return ( + + } /> + } /> + {pages.map((page) => ( + } + /> + ))} + + ); +} + const FullPageCentered = styled('div')({ width: '100%', height: '100%', @@ -574,6 +597,14 @@ function AppError({ error }: FallbackProps) { ); } +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + export interface ToolpadAppProps { hidePreviewBanner?: boolean; basename: string; @@ -589,25 +620,8 @@ export default function ToolpadApp({ dom, hidePreviewBanner, }: ToolpadAppProps) { - const root = appDom.getApp(dom); - const { pages = [], themes = [] } = appDom.getChildNodes(dom, root); - - const theme = themes.length > 0 ? themes[0] : null; - const appContext = React.useMemo(() => ({ appId, version }), [appId, version]); - const queryClient = React.useMemo( - () => - new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }), - [], - ); - const [resetNodeErrorsKey, setResetNodeErrorsKey] = React.useState(0); React.useEffect(() => setResetNodeErrorsKey((key) => key + 1), [dom]); @@ -616,7 +630,7 @@ export default function ToolpadApp({ - + {version === 'preview' && !hidePreviewBanner ? ( This is a preview version of the application. @@ -629,17 +643,7 @@ export default function ToolpadApp({ - - } /> - } /> - {pages.map((page) => ( - } - /> - ))} - + diff --git a/packages/toolpad-app/src/server/data.ts b/packages/toolpad-app/src/server/data.ts index f7de3373c27..2f0b8d7bccb 100644 --- a/packages/toolpad-app/src/server/data.ts +++ b/packages/toolpad-app/src/server/data.ts @@ -96,7 +96,7 @@ export async function saveDom(appId: string, app: appDom.AppDom): Promise ]); } -export async function loadDom(appId: string): Promise { +async function loadPreviewDom(appId: string): Promise { const dbNodes = await prisma.domNode.findMany({ where: { appId }, include: { attributes: true }, @@ -264,7 +264,7 @@ export async function createRelease( appId: string, { description }: CreateReleaseParams, ): Promise> { - const currentDom = await loadDom(appId); + const currentDom = await loadPreviewDom(appId); const snapshot = Buffer.from(JSON.stringify(currentDom), 'utf-8'); const lastRelease = await findLastReleaseInternal(appId); @@ -339,7 +339,7 @@ async function getConnection

( appId: string, id: string, ): Promise> { - const dom = await loadDom(appId); + const dom = await loadPreviewDom(appId); return appDom.getNode(dom, id as NodeId, 'connection') as appDom.ConnectionNode

; } @@ -347,7 +347,7 @@ export async function getConnectionParams

( appId: string, id: string, ): Promise

{ - const dom = await loadDom(appId); + const dom = await loadPreviewDom(appId); const node = appDom.getNode(dom, id as NodeId, 'connection') as appDom.ConnectionNode

; return node.attributes.params.value; } @@ -357,7 +357,7 @@ export async function setConnectionParams

( connectionId: NodeId, params: P, ): Promise { - let dom = await loadDom(appId); + let dom = await loadPreviewDom(appId); const existing = appDom.getNode(dom, connectionId, 'connection'); dom = appDom.setNodeNamespacedProp( @@ -439,6 +439,6 @@ export function parseVersion(param?: string | string[]): VersionOrPreview | null return Number.isNaN(parsed) ? null : parsed; } -export async function loadVersionedDom(appId: string, version: VersionOrPreview) { - return version === 'preview' ? loadDom(appId) : loadReleaseDom(appId, version); +export async function loadDom(appId: string, version: VersionOrPreview = 'preview') { + return version === 'preview' ? loadPreviewDom(appId) : loadReleaseDom(appId, version); } diff --git a/packages/toolpad-app/src/server/handleDataRequest.ts b/packages/toolpad-app/src/server/handleDataRequest.ts index 8676dd5e0c6..cbbcf8cb7af 100644 --- a/packages/toolpad-app/src/server/handleDataRequest.ts +++ b/packages/toolpad-app/src/server/handleDataRequest.ts @@ -1,7 +1,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import Cors from 'cors'; import { NodeId } from '@mui/toolpad-core'; -import { execQuery, loadVersionedDom } from './data'; +import { execQuery, loadDom } from './data'; import initMiddleware from './initMiddleware'; import { ApiResult, VersionOrPreview } from '../types'; import * as appDom from '../appDom'; @@ -27,7 +27,7 @@ export default async ( ) => { await cors(req, res); const queryNodeId = req.query.queryId as NodeId; - const dom = await loadVersionedDom(appId, version); + const dom = await loadDom(appId, version); const query = appDom.getNode(dom, queryNodeId, 'query'); const result = await execQuery(