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

Add app navigation sidebar #1819

Merged
merged 21 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
38c3cd7
App navigation
apedroferreira Mar 27, 2023
305c275
Merge remote-tracking branch 'origin/master' into add-app-navigation
apedroferreira Mar 27, 2023
3ce2600
Fix tests, remove right-side preview, open current page from top, add…
apedroferreira Mar 28, 2023
fec9be8
Merge remote-tracking branch 'origin/master' into add-app-navigation
apedroferreira Mar 28, 2023
2c34cf5
Fix navigation overlaying app content
apedroferreira Mar 28, 2023
0d5cc96
Improve go to page test method
apedroferreira Mar 28, 2023
37c008c
Merge remote-tracking branch 'origin/master' into add-app-navigation
apedroferreira Mar 29, 2023
b811615
Open respective page in editor from preview
apedroferreira Mar 29, 2023
f144507
unused import
apedroferreira Mar 29, 2023
d2926a2
Better fallback
apedroferreira Mar 29, 2023
2297d2a
Merge remote-tracking branch 'origin/master' into add-app-navigation
apedroferreira Mar 29, 2023
56f1685
Merge remote-tracking branch 'origin/master' into add-app-navigation
apedroferreira Mar 30, 2023
f5c481a
Merge remote-tracking branch 'origin/master' into add-app-navigation
apedroferreira Mar 30, 2023
8345268
Add page parameter for default page display
apedroferreira Mar 30, 2023
49b5c1c
Fixed default value
apedroferreira Mar 30, 2023
7e69087
Keep display param between routes
apedroferreira Mar 30, 2023
618d9b3
Set initial page display only
apedroferreira Mar 31, 2023
96cc127
Merge remote-tracking branch 'origin/master' into add-app-navigation
apedroferreira Mar 31, 2023
8332f20
Revert previous changes and rename defaultDisplay to display
apedroferreira Mar 31, 2023
5de6db7
Not sure what happened here
apedroferreira Mar 31, 2023
8d846cd
Remove uneeded URL params
apedroferreira Mar 31, 2023
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/appDom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,15 @@ export interface ConnectionNode<P = unknown> extends AppDomNodeBase {
};
}

export type PageDisplayMode = 'standalone' | 'shell';

export interface PageNode extends AppDomNodeBase {
readonly type: 'page';
readonly attributes: {
readonly title: ConstantAttrValue<string>;
readonly parameters?: ConstantAttrValue<[string, string][]>;
readonly module?: ConstantAttrValue<string>;
readonly display?: ConstantAttrValue<PageDisplayMode>;
};
}

Expand Down Expand Up @@ -1105,6 +1108,7 @@ export function createDefaultDom(): AppDom {
name: 'Page 1',
attributes: {
title: createConst('Page 1'),
display: createConst('shell'),
},
});

Expand Down
4 changes: 2 additions & 2 deletions packages/toolpad-app/src/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ export default function AppCanvas({ catalog, basename, initialState = null }: Ap
<ToolpadApp
rootRef={onAppRoot}
catalog={catalog}
hidePreviewBanner
version="preview"
hasShell={false}
version="development"
basename={basename}
state={state}
/>
Expand Down
2 changes: 2 additions & 0 deletions packages/toolpad-app/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const APP_PAGE_ROUTE = '/app/pages/:nodeId';
export const APP_API_ROUTE = '/app/apis/:nodeId';
export const APP_CODE_COMPONENT_ROUTE = '/app/codeComponents/:nodeId';
export const APP_CONNECTION_ROUTE = '/app/connections/:nodeId';

export const PREVIEW_PAGE_ROUTE = '/preview/pages/:nodeId';
4 changes: 2 additions & 2 deletions packages/toolpad-app/src/runtime/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';
import { useNonNullableContext } from '@mui/toolpad-core/utils/react';
import { VersionOrPreview } from '../types';
import { AppVersion } from '../types';

export interface AppContext {
version: VersionOrPreview;
version: AppVersion;
}

const Context = React.createContext<AppContext | null>(null);
Expand Down
72 changes: 72 additions & 0 deletions packages/toolpad-app/src/runtime/AppNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as React from 'react';
import {
Drawer,
Box,
List,
ListSubheader,
ListItem,
ListItemButton,
ListItemText,
Toolbar,
} from '@mui/material';
import { useNavigate, useLocation, useHref } from 'react-router-dom';
import * as appDom from '../appDom';

const DRAWER_WIDTH = 250; // px

interface AppNavigationProps {
pages: appDom.PageNode[];
isPreview?: boolean;
}

export default function AppNavigation({ pages, isPreview = false }: AppNavigationProps) {
const navigate = useNavigate();
const location = useLocation();
const { search } = location;
const href = useHref('');

const handlePageClick = React.useCallback(
(page: appDom.PageNode) => () => {
navigate(`pages/${page.id}${search}`);
},
[navigate, search],
);

const activePagePath = location.pathname.replace(href, '');

const navListSubheaderId = React.useId();

return (
<Drawer
variant="permanent"
anchor="left"
open
sx={{
width: DRAWER_WIDTH,
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: DRAWER_WIDTH, boxSizing: 'border-box' },
}}
>
{isPreview ? <Toolbar /> : null}
<Box>
<List
component="nav"
subheader={
<ListSubheader id={navListSubheaderId} sx={{ px: 4 }}>
Pages
</ListSubheader>
}
aria-labelledby={navListSubheaderId}
>
{pages.map((page) => (
<ListItem key={page.id} onClick={handlePageClick(page)} disablePadding>
<ListItemButton selected={activePagePath === `/pages/${page.id}`}>
<ListItemText primary={page.name} sx={{ ml: 2 }} />
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Drawer>
);
}
56 changes: 0 additions & 56 deletions packages/toolpad-app/src/runtime/AppOverview.tsx

This file was deleted.

111 changes: 75 additions & 36 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
Navigate,
Location as RouterLocation,
useNavigate,
matchPath,
} from 'react-router-dom';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import {
Expand All @@ -51,14 +52,13 @@ import ErrorIcon from '@mui/icons-material/Error';
import EditIcon from '@mui/icons-material/Edit';
import { useBrowserJsRuntime } from '@mui/toolpad-core/jsBrowserRuntime';
import * as appDom from '../appDom';
import { RuntimeState, VersionOrPreview } from '../types';
import { RuntimeState, AppVersion } from '../types';
import {
getElementNodeComponentId,
isPageLayoutComponent,
isPageRow,
PAGE_ROW_COMPONENT_ID,
} from '../toolpadComponents';
import AppOverview from './AppOverview';
import AppThemeProvider from './AppThemeProvider';
import evalJsBindings, {
buildGlobalScope,
Expand All @@ -81,6 +81,8 @@ import { errorFrom } from '../utils/errors';
import Header from '../toolpad/ToolpadShell/Header';
import { ThemeProvider } from '../ThemeContext';
import { BridgeContext } from '../canvas/BridgeContext';
import AppNavigation from './AppNavigation';
import { PREVIEW_PAGE_ROUTE } from '../routes';

const ReactQueryDevtoolsProduction = React.lazy(() =>
import('@tanstack/react-query-devtools/build/lib/index.prod.js').then((d) => ({
Expand Down Expand Up @@ -1078,17 +1080,13 @@ function RenderedPage({ nodeId }: RenderedNodeProps) {
}

interface RenderedPagesProps {
dom: appDom.AppDom;
pages: appDom.PageNode[];
defaultPage: appDom.PageNode;
}

function RenderedPages({ dom }: RenderedPagesProps) {
const root = appDom.getApp(dom);
const { pages = [] } = appDom.getChildNodes(dom, root);

function RenderedPages({ pages, defaultPage }: RenderedPagesProps) {
return (
<Routes>
<Route path="/" element={<Navigate replace to="/pages" />} />
<Route path="/pages" element={<AppOverview dom={dom} />} />
{pages.map((page) => (
<Route
key={page.id}
Expand All @@ -1103,6 +1101,7 @@ function RenderedPages({ dom }: RenderedPagesProps) {
}
/>
))}
<Route path="/" element={<Navigate to={`pages/${defaultPage.id}`} replace />} />
</Routes>
);
}
Expand Down Expand Up @@ -1139,12 +1138,75 @@ const queryClient = new QueryClient({
},
});

export interface ToolpadAppLayoutProps {
dom: appDom.RenderTree;
hasShell?: boolean;
version: AppVersion;
}

function ToolpadAppLayout({ dom, version, hasShell: hasShellProp = true }: ToolpadAppLayoutProps) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved these to their own component so that we can use React Router hooks.

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

const location = useLocation();
const { pathname, search } = location;
const urlParams = React.useMemo(() => new URLSearchParams(search), [search]);

const pageMatch = matchPath(PREVIEW_PAGE_ROUTE, `/preview${pathname}`);
const pageId = pageMatch?.params.nodeId;

const defaultPage = pages[0];
const page = pageId
? (appDom.getNode<'page'>(dom, pageId as NodeId) as appDom.PageNode)
: defaultPage;

const pageDisplay = urlParams.get('toolpad-display') || page.attributes.display?.value;

const hasShell = hasShellProp && pageDisplay !== 'standalone';

const isPreview = version === 'preview';

return (
<React.Fragment>
{isPreview ? (
<ThemeProvider>
<Header
enableUserFeedback={false}
actions={
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" sx={{ color: 'primary.main' }}>
This is a preview version of the application.
</Typography>
<Button
variant="outlined"
endIcon={<EditIcon />}
color="primary"
component="a"
href={pageId ? `/_toolpad/app/pages/${pageId}` : '/_toolpad/app'}
>
Edit
</Button>
</Stack>
}
/>
</ThemeProvider>
) : null}
<Box sx={{ display: 'flex' }}>
{hasShell && pages.length > 0 ? (
<AppNavigation pages={pages} isPreview={isPreview} />
) : null}
<RenderedPages pages={pages} defaultPage={defaultPage} />
</Box>
</React.Fragment>
);
}

export interface ToolpadAppProps {
rootRef?: React.Ref<HTMLDivElement>;
catalog?: Record<string, ToolpadComponent>;
hidePreviewBanner?: boolean;
hasShell?: boolean;
basename: string;
version: VersionOrPreview;
version: AppVersion;
state: RuntimeState;
}

Expand All @@ -1153,7 +1215,7 @@ export default function ToolpadApp({
catalog,
basename,
version,
hidePreviewBanner,
hasShell = true,
state,
}: ToolpadAppProps) {
const { dom } = state;
Expand All @@ -1175,29 +1237,6 @@ export default function ToolpadApp({
<DomContextProvider value={dom}>
<AppThemeProvider dom={dom}>
<CssBaseline enableColorScheme />
{version === 'preview' && !hidePreviewBanner ? (
<ThemeProvider>
<Header
enableUserFeedback={false}
actions={
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" sx={{ color: 'primary.main' }}>
This is a preview version of the application.
</Typography>
<Button
variant="outlined"
endIcon={<EditIcon />}
color="primary"
component="a"
href={`/_toolpad/app`}
>
Edit
</Button>
</Stack>
}
/>
</ThemeProvider>
) : null}
<ErrorBoundary FallbackComponent={AppError}>
<ResetNodeErrorsKeyProvider value={resetNodeErrorsKey}>
<React.Suspense fallback={<AppLoading />}>
Expand All @@ -1206,7 +1245,7 @@ export default function ToolpadApp({
<AppContextProvider value={appContext}>
<QueryClientProvider client={queryClient}>
<BrowserRouter basename={basename}>
<RenderedPages dom={dom} />
<ToolpadAppLayout dom={dom} version={version} hasShell={hasShell} />
</BrowserRouter>
{showDevtools ? (
<ReactQueryDevtoolsProduction initialIsOpen={false} />
Expand Down
Loading