Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix navigation through DOM views with pages #1565

Merged
merged 15 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/toolpad-app/src/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const APP_PAGE_ROUTE = '/app/:appId/pages/:nodeId';
export const APP_API_ROUTE = '/app/:appId/apis/:nodeId';
export const APP_CODE_COMPONENT_ROUTE = '/app/:appId/codeComponents/:nodeId';
export const APP_CONNECTION_ROUTE = '/app/:appId/connections/:nodeId';
6 changes: 3 additions & 3 deletions packages/toolpad-app/src/toolpad/AppEditor/AppEditorShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';

import * as React from 'react';
import { useForm } from 'react-hook-form';
import { Outlet } from 'react-router-dom';
import invariant from 'invariant';
import DialogForm from '../../components/DialogForm';
import { DomLoader, useDomLoader } from '../DomLoader';
Expand Down Expand Up @@ -140,9 +139,10 @@ function getSaveState(domLoader: DomLoader): React.ReactNode {
export interface ToolpadShellProps {
appId: string;
actions?: React.ReactNode;
children: React.ReactNode;
}

export default function AppEditorShell({ appId, ...props }: ToolpadShellProps) {
export default function AppEditorShell({ appId, children, ...props }: ToolpadShellProps) {
const domLoader = useDomLoader();
const release = client.useQuery('findLastRelease', [appId]);

Expand Down Expand Up @@ -229,7 +229,7 @@ export default function AppEditorShell({ appId, ...props }: ToolpadShellProps) {
position: 'relative',
}}
>
<Outlet />
{children}
</Box>

<CreateReleaseDialog
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Box, Button, Stack, styled, TextField, Toolbar, Typography } from '@mui/material';
import { useParams } from 'react-router-dom';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import * as ReactDOM from 'react-dom';
Expand Down Expand Up @@ -309,12 +308,12 @@ function CodeComponentEditorContent({ codeComponentNode }: CodeComponentEditorCo

interface CodeComponentEditorProps {
appId: string;
nodeId?: NodeId;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function CodeComponentEditor({ appId }: CodeComponentEditorProps) {
export default function CodeComponentEditor({ appId, nodeId }: CodeComponentEditorProps) {
const { dom } = useDom();
const { nodeId } = useParams();
const codeComponentNode = appDom.getMaybeNode(dom, nodeId as NodeId, 'codeComponent');

useUndoRedo();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import { Box, Container, Stack, Typography } from '@mui/material';
import { useParams } from 'react-router-dom';
import { NodeId } from '@mui/toolpad-core';
import invariant from 'invariant';
import { ConnectionEditorProps, ClientDataSource } from '../../../types';
Expand Down Expand Up @@ -103,11 +102,11 @@ function ConnectionEditorContent<P>({

export interface ConnectionProps {
appId: string;
nodeId?: NodeId;
}

export default function ConnectionEditor({ appId }: ConnectionProps) {
export default function ConnectionEditor({ appId, nodeId }: ConnectionProps) {
const { dom } = useDom();
const { nodeId } = useParams();
const connectionNode = appDom.getMaybeNode(dom, nodeId as NodeId, 'connection');

useUndoRedo();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import AddIcon from '@mui/icons-material/Add';
import { useLocation, matchRoutes, Location } from 'react-router-dom';
import { NodeId } from '@mui/toolpad-core';
import clsx from 'clsx';
import invariant from 'invariant';
import * as appDom from '../../../appDom';
import { useDom, useDomApi, DomView } from '../../DomLoader';
import { useDom, useDomApi } from '../../DomLoader';
import CreatePageNodeDialog from './CreatePageNodeDialog';
import CreateCodeComponentNodeDialog from './CreateCodeComponentNodeDialog';
import CreateConnectionNodeDialog from './CreateConnectionNodeDialog';
import useLocalStorageState from '../../../utils/useLocalStorageState';
import NodeMenu from '../NodeMenu';
import config from '../../../config';
import { DomView } from '../../../utils/domView';

const HierarchyExplorerRoot = styled('div')({
overflow: 'auto',
Expand All @@ -41,22 +41,6 @@ const StyledTreeItem = styled(TreeItem)({
},
});

function getActiveNodeId(location: Location): NodeId | null {
const match =
matchRoutes(
[
{ path: `/app/:appId/pages/:activeNodeId` },
{ path: `/app/:appId/apis/:activeNodeId` },
{ path: `/app/:appId/codeComponents/:activeNodeId` },
{ path: `/app/:appId/connections/:activeNodeId` },
],
location,
) || [];

const selected: NodeId[] = match.map((route) => route.params.activeNodeId as NodeId);
return selected.length > 0 ? selected[0] : null;
}

type StyledTreeItemProps = TreeItemProps & {
onDeleteNode?: (nodeId: NodeId) => void;
onDuplicateNode?: (nodeId: NodeId) => void;
Expand Down Expand Up @@ -142,7 +126,7 @@ export interface HierarchyExplorerProps {
}

export default function HierarchyExplorer({ appId, className }: HierarchyExplorerProps) {
const { dom } = useDom();
const { dom, currentView } = useDom();
const domApi = useDomApi();

const app = appDom.getApp(dom);
Expand All @@ -153,9 +137,7 @@ export default function HierarchyExplorer({ appId, className }: HierarchyExplore
[':connections', ':pages', ':codeComponents'],
);

const location = useLocation();

const activeNode = getActiveNodeId(location);
const activeNode = currentView.nodeId || null;

const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
setExpanded(nodeIds as NodeId[]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,19 @@ export default function PageModuleEditor({ pageNodeId }: PageModuleEditorProps)
const [dialogOpen, setDialogOpen] = React.useState(false);

const handleButtonClick = React.useCallback(() => {
domApi.setView({ kind: 'pageModule', nodeId: pageNodeId });
domApi.setView({
kind: 'page',
nodeId: pageNodeId,
view: { kind: 'pageModule' },
});
}, [domApi, pageNodeId]);

const handleDialogClose = React.useCallback(() => {
domApi.setView({ kind: 'page', nodeId: pageNodeId });
}, [domApi, pageNodeId]);

React.useEffect(() => {
setDialogOpen(currentView.kind === 'pageModule');
setDialogOpen(currentView.kind === 'page' && currentView.view?.kind === 'pageModule');
}, [currentView]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default function QueryEditor() {
domApi.saveNode(node);
} else {
domApi.update((draft) => appDom.addNode(draft, node, page, 'queries'), {
view: { kind: 'query', nodeId: node.id },
view: { kind: 'page', nodeId: page.id, view: { kind: 'query', nodeId: node.id } },
});
}
},
Expand Down Expand Up @@ -176,8 +176,8 @@ export default function QueryEditor() {

React.useEffect(() => {
setDialogState(() => {
if (currentView.kind === 'query') {
const node = appDom.getNode(dom, currentView.nodeId, 'query');
if (currentView.kind === 'page' && currentView.view?.kind === 'query') {
const node = appDom.getNode(dom, currentView.view?.nodeId, 'query');
return { node, isDraft: false };
}

Expand All @@ -197,7 +197,11 @@ export default function QueryEditor() {
key={queryNode.id}
disablePadding
onClick={() => {
domApi.setView({ kind: 'query', nodeId: queryNode.id });
domApi.setView({
kind: 'page',
nodeId: page.id,
view: { kind: 'query', nodeId: queryNode.id },
});
}}
secondaryAction={
<NodeMenu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export default function UrlQueryEditor({ pageNodeId }: UrlQueryEditorProps) {
}, [isDialogOpen, value]);

const handleButtonClick = React.useCallback(() => {
domApi.setView({ kind: 'pageParameters', nodeId: pageNodeId });
domApi.setView({
kind: 'page',
nodeId: pageNodeId,
view: { kind: 'pageParameters' },
});
}, [domApi, pageNodeId]);

const handleDialogClose = React.useCallback(() => {
Expand All @@ -56,7 +60,7 @@ export default function UrlQueryEditor({ pageNodeId }: UrlQueryEditorProps) {
}, [domApi, handleDialogClose, input, page]);

React.useEffect(() => {
if (currentView.kind === 'pageParameters') {
if (currentView.kind === 'page' && currentView.view?.kind === 'pageParameters') {
openDialog();
} else {
closeDialog();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import { styled } from '@mui/material';
import { useParams } from 'react-router-dom';
import { NodeId } from '@mui/toolpad-core';
import SplitPane from '../../../components/SplitPane';
import RenderPanel from './RenderPanel';
Expand Down Expand Up @@ -67,11 +66,11 @@ function PageEditorContent({ appId, node }: PageEditorContentProps) {

interface PageEditorProps {
appId: string;
nodeId?: NodeId;
}

export default function PageEditor({ appId }: PageEditorProps) {
export default function PageEditor({ appId, nodeId }: PageEditorProps) {
const { dom } = useDom();
const { nodeId } = useParams();
const pageNode = appDom.getMaybeNode(dom, nodeId as NodeId, 'page');

useUndoRedo();
Expand Down
53 changes: 18 additions & 35 deletions packages/toolpad-app/src/toolpad/AppEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as React from 'react';
import { styled } from '@mui/material';
import { Route, Routes, useParams, Navigate, useNavigate } from 'react-router-dom';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { JsRuntimeProvider } from '@mui/toolpad-core/jsServerRuntime';
import PageEditor from './PageEditor';
import DomProvider, { useDom } from '../DomLoader';
import * as appDom from '../../appDom';
import CodeComponentEditor from './CodeComponentEditor';
import ConnectionEditor from './ConnectionEditor';
import AppEditorShell from './AppEditorShell';
import CodeComponentEditor from './CodeComponentEditor';
import NoPageFound from './NoPageFound';
import { getPathnameFromView } from '../../utils/domView';

const classes = {
content: 'Toolpad_Content',
Expand Down Expand Up @@ -43,49 +43,32 @@ interface FileEditorProps {
}

function FileEditor({ appId }: FileEditorProps) {
const { dom, currentView } = useDom();

const app = appDom.getApp(dom);
const { pages = [] } = appDom.getChildNodes(dom, app);
const { currentView } = useDom();

const location = useLocation();
const navigate = useNavigate();

const firstPage = pages.length > 0 ? pages[0] : null;

React.useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this could be a bit DRYer if written as:

function getPathnameForView (appId, view) {
  switch (view.kind) {
    case 'page': return `/app/${appId}/pages/${view.nodeId}`;
    // ...
  }
}

React.useEffect(() => {
  const desiredPath = getPathnameForView(currentView)
  if (desiredPath !== location.pathname) {
    navigate({  pathname: desiredPath }, { replace: true });
  }
}, [appId, currentView, location.pathname]

const newPathname = getPathnameFromView(appId, currentView);
if (newPathname !== location.pathname) {
navigate({ pathname: newPathname }, { replace: true });
}
}, [appId, currentView, location.pathname, navigate]);

const currentViewContent = React.useMemo(() => {
switch (currentView.kind) {
case 'page':
navigate(`/app/${appId}/pages/${currentView.nodeId || firstPage?.id}`, { replace: true });
break;
return <PageEditor appId={appId} nodeId={currentView.nodeId} />;
case 'connection':
navigate(`/app/${appId}/connections/${currentView.nodeId}`, { replace: true });
break;
return <ConnectionEditor appId={appId} nodeId={currentView.nodeId} />;
case 'codeComponent':
navigate(`/app/${appId}/codeComponents/${currentView.nodeId}`);
break;
return <CodeComponentEditor appId={appId} nodeId={currentView.nodeId} />;
default:
return <NoPageFound appId={appId} />;
}
}, [appId, currentView.kind, currentView.nodeId, firstPage?.id, navigate]);
}, [appId, currentView.kind, currentView.nodeId]);

return (
<Routes>
<Route element={<AppEditorShell appId={appId} />}>
<Route path="connections/:nodeId" element={<ConnectionEditor appId={appId} />} />
<Route path="pages/:nodeId" element={<PageEditor appId={appId} />} />
<Route path="codeComponents/:nodeId" element={<CodeComponentEditor appId={appId} />} />
<Route
index
element={
firstPage ? (
<Navigate to={`pages/${firstPage.id}`} replace />
) : (
<NoPageFound appId={appId} />
)
}
/>
</Route>
</Routes>
);
return <AppEditorShell appId={appId}>{currentViewContent}</AppEditorShell>;
}

export interface EditorContentProps {
Expand Down
25 changes: 15 additions & 10 deletions packages/toolpad-app/src/toolpad/DomLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NodeId } from '@mui/toolpad-core';
import { createProvidedContext } from '@mui/toolpad-core/utils/react';
import invariant from 'invariant';
import { debounce, DebouncedFunc } from 'lodash-es';
import { useLocation } from 'react-router-dom';
import * as appDom from '../appDom';
import { update } from '../utils/immutability';
import client from '../api';
Expand All @@ -13,14 +14,7 @@ import insecureHash from '../utils/insecureHash';
import useEvent from '../utils/useEvent';
import { NodeHashes } from '../types';
import { hasFieldFocus } from '../utils/fields';

export type DomView =
| { kind: 'page'; nodeId?: NodeId }
| { kind: 'query'; nodeId: NodeId }
| { kind: 'pageModule'; nodeId: NodeId }
| { kind: 'pageParameters'; nodeId: NodeId }
| { kind: 'connection'; nodeId: NodeId }
| { kind: 'codeComponent'; nodeId: NodeId };
import { DomView, getViewFromPathname } from '../utils/domView';

export type ComponentPanelTab = 'component' | 'theme';

Expand Down Expand Up @@ -367,20 +361,31 @@ export default function DomProvider({ appId, children }: DomContextProps) {

invariant(dom, `Suspense should load the dom`);

const location = useLocation();

const app = appDom.getApp(dom);
const { pages = [] } = appDom.getChildNodes(dom, app);
const firstPage = pages.length > 0 ? pages[0] : null;

const initialView = getViewFromPathname(location.pathname) || {
kind: 'page',
nodeId: firstPage?.id,
};

const [state, dispatch] = React.useReducer(domLoaderReducer, {
saving: false,
unsavedChanges: 0,
saveError: null,
savedDom: dom,
dom,
selectedNodeId: null,
currentView: { kind: 'page' },
currentView: initialView,
currentTab: 'component',
undoStack: [
{
dom,
selectedNodeId: null,
view: { kind: 'page' },
view: initialView,
tab: 'component',
timestamp: Date.now(),
},
Expand Down
Loading