diff --git a/docs/Admin.md b/docs/Admin.md index 03917b22afa..3c67090b564 100644 --- a/docs/Admin.md +++ b/docs/Admin.md @@ -595,10 +595,15 @@ Layout components can be customized via props. For instance, you can pass a cust ```tsx // in src/MyLayout.js +import type { ReactNode } from 'react'; import { Layout } from 'react-admin'; import MyMenu from './MyMenu'; -export const MyLayout = (props) => ; +export const MyLayout = ({ children }: { children: ReactNode }) => ( + + {children} + +); ``` Then, pass it to the `` component as the `layout` prop: @@ -617,7 +622,31 @@ const App = () => ( Refer to each layout component documentation to understand the props it accepts. -Finally, you can also pass a custom component as the `layout` prop. It must contain a `{children}` placeholder, where react-admin will render the page content. Check [the custom layout documentation](./Layout.md#writing-a-layout-from-scratch) for examples, and use the [default ``](https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/layout/Layout.tsx) as a starting point. +Finally, you can also pass a custom component as the `layout` prop. Your custom layout will receive the page content as `children`, so it should render it somewhere. + +```tsx +// in src/MyLayout.js +import type { ReactNode } from 'react'; +export const MyLayout = ({ children }: { children: ReactNode }) => ( +
+

My App

+
{children}
+
+); + +// in src/App.js +import { Admin } from 'react-admin'; +import { dataProvider } from './dataProvider'; +import { MyLayout } from './MyLayout'; + +const App = () => ( + + // ... + +); +``` + +Check [the custom layout documentation](./Layout.md#writing-a-layout-from-scratch) for examples, and use the [default ``](https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/layout/Layout.tsx) as a starting point. ## `loginPage` @@ -878,6 +907,17 @@ const App = () => ( ); ``` +If you need to display this application title somewhere in your app, use the `useDefaultTitle` hook: + +```tsx +import { useDefaultTitle } from 'react-admin'; + +const MyTitle = () => { + const defaultTitle = useDefaultTitle(); + return {defaultTitle}; // My Custom Admin +}; +``` + ## Adding Custom Pages The [`children`](#children) prop of the `` component define the routes of the application. diff --git a/docs/AppBar.md b/docs/AppBar.md index 6d34cdbabeb..33e88bcde9b 100644 --- a/docs/AppBar.md +++ b/docs/AppBar.md @@ -43,10 +43,13 @@ Then, create a custom layout based on react-admin's ``: ```jsx // in src/MyLayout.js import { Layout } from 'react-admin'; - import { MyAppBar } from './MyAppBar'; -export const MyLayout = props => ; +export const MyLayout = ({ children }) => ( + + {children} + +); ``` Then pass this custom layout to the `` component: @@ -463,11 +466,12 @@ Then, use your custom app bar in a custom `` component: ```jsx // in src/MyLayout.js import { Layout } from 'react-admin'; - import { MyAppBar } from './MyAppBar'; -export const MyLayout = (props) => ( - +export const MyLayout = ({ children }) => ( + + {children} + ); ``` diff --git a/docs/AppTheme.md b/docs/AppTheme.md index 4f624221f47..4772756145b 100644 --- a/docs/AppTheme.md +++ b/docs/AppTheme.md @@ -350,7 +350,11 @@ const MySidebar = (props) => ( /> ); -const MyLayout = props => +const MyLayout = ({ children }) => ( + + {children} + +); ``` {% endraw %} diff --git a/docs/Architecture.md b/docs/Architecture.md index b174a44478c..11f46e9fefb 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -200,7 +200,11 @@ The trade-off with this approach is that sometimes react-admin may require you t import { Layout } from 'react-admin'; import { Menu } from './Menu'; -export const Layout = (props) => ; +export const Layout = ({ children }) => ( + + {children} + +); // in src/App.js import { Layout } from './Layout'; diff --git a/docs/AuthRBAC.md b/docs/AuthRBAC.md index 6f37cd27ead..eb7f7433d0b 100644 --- a/docs/AuthRBAC.md +++ b/docs/AuthRBAC.md @@ -640,7 +640,11 @@ const authProvider= { }), }; -const CustomLayout = props => ; +const CustomLayout = ({ children }) => ( + + {children} + +); const App = () => ( diff --git a/docs/Authentication.md b/docs/Authentication.md index b3f003c6f13..95d184bfa96 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -396,7 +396,11 @@ const MyUserMenu = () => ; const MyAppBar = () => } />; -const MyLayout = (props) => ; +const MyLayout = ({ children }) => ( + + {children} + +); const App = () => ( diff --git a/docs/Buttons.md b/docs/Buttons.md index f7169dae28a..b60f5b844f3 100644 --- a/docs/Buttons.md +++ b/docs/Buttons.md @@ -800,7 +800,11 @@ To use this custom menu component, pass it to a custom Layout: import { Layout } from 'react-admin'; import { Menu } from './Menu'; -export const Layout = (props) => ; +export const Layout = ({ children }) => ( + + {children} + +); ``` Then, use this layout in the `` `layout` prop: diff --git a/docs/CheckForApplicationUpdate.md b/docs/CheckForApplicationUpdate.md index e78b1647dfa..7ac7660792b 100644 --- a/docs/CheckForApplicationUpdate.md +++ b/docs/CheckForApplicationUpdate.md @@ -17,10 +17,11 @@ Include this component in a custom layout: ```tsx // in src/MyLayout.tsx -import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; +import type { ReactNode } from 'react'; +import { CheckForApplicationUpdate, Layout } from 'react-admin'; -export const MyLayout = ({ children, ...props }: LayoutProps) => ( - +export const MyLayout = ({ children }: { children: ReactNode}) => ( + {children} @@ -56,12 +57,13 @@ You can customize the interval between each check by providing the `interval` pr ```tsx // in src/MyLayout.tsx -import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; +import type { ReactNode } from 'react'; +import { CheckForApplicationUpdate, Layout } from 'react-admin'; const HALF_HOUR = 30 * 60 * 1000; -export const MyLayout = ({ children, ...props }: LayoutProps) => ( - +export const MyLayout = ({ children }: { children: ReactNode}) => ( + {children} @@ -74,10 +76,11 @@ You can dynamically disable the automatic application update detection by provid ```tsx // in src/MyLayout.tsx -import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; +import type { ReactNode } from 'react'; +import { CheckForApplicationUpdate, Layout } from 'react-admin'; -export const MyLayout = ({ children, ...props }: LayoutProps) => ( - +export const MyLayout = ({ children }: { children: ReactNode}) => ( + {children} @@ -91,7 +94,7 @@ Note that you must wrap your component with `forwardRef`. ```tsx // in src/MyLayout.tsx -import { forwardRef } from 'react'; +import { forwardRef, ReactNode } from 'react'; import { Layout, CheckForApplicationUpdate } from 'react-admin'; const CustomAppUpdatedNotification = forwardRef((props, ref) => ( @@ -112,8 +115,8 @@ const CustomAppUpdatedNotification = forwardRef((props, ref) => ( )); -const MyLayout = ({ children, ...props }) => ( - +const MyLayout = ({ children }: { children: ReactNode}) => ( + {children} }/> @@ -163,12 +166,13 @@ You can customize the URL fetched to detect updates by providing the `url` prop. ```tsx // in src/MyLayout.tsx -import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; +import type { ReactNode } from 'react'; +import { CheckForApplicationUpdate, Layout } from 'react-admin'; const MY_APP_ROOT_URL = 'https://admin.mycompany.com'; -export const MyLayout = ({ children, ...props }: LayoutProps) => ( - +export const MyLayout = ({ children }: { children: ReactNode}) => ( + {children} diff --git a/docs/CreateReactApp.md b/docs/CreateReactApp.md index 5f42f94f857..dde3499e189 100644 --- a/docs/CreateReactApp.md +++ b/docs/CreateReactApp.md @@ -77,10 +77,11 @@ To enable it, start by creating a custom layout: ```tsx // in src/admin/MyLayout.tsx -import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; +import type { ReactNode } from 'react'; +import { CheckForApplicationUpdate, Layout } from 'react-admin'; -export const MyLayout = ({ children, ...props }: LayoutProps) => ( - +export const MyLayout = ({ children }: { children: ReactNode}) => ( + {children} diff --git a/docs/CustomRoutes.md b/docs/CustomRoutes.md index 645093dfed5..e8eb0929bb7 100644 --- a/docs/CustomRoutes.md +++ b/docs/CustomRoutes.md @@ -158,7 +158,11 @@ Next, pass the custom menu to a custom `` component: import { Layout } from 'react-admin'; import { MyMenu } from './MyMenu'; -export const MyLayout = (props) => ; +export const MyLayout = ({ children }) => ( + + {children} + +); ``` Finally, pass the custom `` component to ``: diff --git a/docs/DataProviders.md b/docs/DataProviders.md index 5c7f10b8d1d..644cf9a3ac3 100644 --- a/docs/DataProviders.md +++ b/docs/DataProviders.md @@ -124,11 +124,11 @@ To enable these devtools, add the `` component to a custom L import { Layout } from 'react-admin'; import { ReactQueryDevtools } from 'react-query/devtools'; -export const MyLayout = props => ( - <> - +export const MyLayout = ({ children }) => ( + + {children} - + ); ``` diff --git a/docs/Features.md b/docs/Features.md index ebb0f9c49bb..c22131a5055 100644 --- a/docs/Features.md +++ b/docs/Features.md @@ -1191,8 +1191,10 @@ import { MenuLive } from '@react-admin/ra-realtime'; import { PostList, PostShow, PostEdit, realTimeDataProvider } from '.'; -const CustomLayout = (props) => ( - +const CustomLayout = ({ children }) => ( + + {children} + ); const MyReactAdmin = () => ( diff --git a/docs/IconMenu.md b/docs/IconMenu.md index b93f7b3a71d..b48e76cf18a 100644 --- a/docs/IconMenu.md +++ b/docs/IconMenu.md @@ -45,9 +45,11 @@ import { AppLocationContext } from '@react-admin/ra-navigation'; import { MyMenu } from './MyMenu'; -export const MyLayout = (props) => ( +export const MyLayout = ({ children }) => ( - + + {children} + ); ``` diff --git a/docs/Layout.md b/docs/Layout.md index d4f43aa49ec..59b524ca659 100644 --- a/docs/Layout.md +++ b/docs/Layout.md @@ -13,7 +13,6 @@ The default react-admin layout renders a horizontal app bar at the top, a naviga Your browser does not support the video tag. - In addition, the layout renders the menu as a dropdown on mobile. - React-admin lets you override the app layout using [the `` prop](./Admin.md#layout). You can use any component you want as layout ; but if you just need to tweak the default layout, you can use the `` component. ## Usage -Create a custom layout overriding some of the components of the default layout: +Create a custom layout overriding some of the props of the default layout. Remember to pass down the `children` prop: ```jsx // in src/MyLayout.js @@ -35,7 +33,11 @@ import { Layout } from 'react-admin'; import { MyAppBar } from './MyAppBar'; -export const MyLayout = props => ; +export const MyLayout = ({ children }) => ( + + {children} + +); ``` Then pass this custom layout to the `` component: @@ -57,6 +59,7 @@ const App = () => ( | Prop | Required | Type | Default | Description | | ---------------- | -------- | ----------- | -------- | ----------------------------------------------------------------------- | +| `children` | Required | `Element` | - | The content of the layout | | `appBar` | Optional | `Component` | - | A React component rendered at the top of the layout | | `appBarAlwaysOn` | Optional | `boolean` | - | When true, the app bar is always visible | | `className` | Optional | `string` | - | Passed to the root `
` component | @@ -65,23 +68,6 @@ const App = () => ( | `sidebar` | Optional | `Component` | - | A React component responsible for rendering the menu (e.g. in a drawer) | | `sx` | Optional | `SxProps` | - | Style overrides, powered by MUI System | -React-admin injects more props at runtime based on the `` props: - -* `dashboard`: The dashboard component. Used to enable the dahboard link in the menu -* `title`: The default page tile, rendered in the AppBar for error pages -* `children`: The main content of the page - -Any value set for these props in a custom layout will be ignored. That's why you're supposed to pass down the props when creating a layout based on ``: - -```jsx -// in src/MyLayout.js -import { Layout } from 'react-admin'; - -import { MyAppBar } from './MyAppBar'; - -export const MyLayout = props => ; -``` - ## `appBar` Lets you override the top App Bar. @@ -93,7 +79,11 @@ import { Layout } from 'react-admin'; import { MyAppBar } from './MyAppBar'; -export const MyLayout = (props) => ; +export const MyLayout = ({ children }) => ( + + {children} + +); ``` You can use [react-admin's `` component](./AppBar.md) as a base for your custom app bar, or the component of your choice. @@ -135,7 +125,11 @@ By default, the app bar is hidden when the user scrolls down the page. This is u import * as React from 'react'; import { Layout } from 'react-admin'; -export const MyLayout = (props) => ; +export const MyLayout = ({ children }) => ( + + {children} + +); ``` ## `className` @@ -153,10 +147,13 @@ If you want to customize this page, or log the error to a third-party service, c ```jsx // in src/MyLayout.js import { Layout } from 'react-admin'; - import { MyError } from './MyError'; -export const MyLayout = (props) => ; +export const MyLayout = ({ children }) => ( + + {children} + +); ``` The following snippet is a simplified version of the react-admin `Error` component, that you can use as a base for your own: @@ -167,7 +164,7 @@ import * as React from 'react'; import Button from '@mui/material/Button'; import ErrorIcon from '@mui/icons-material/Report'; import History from '@mui/icons-material/History'; -import { Title, useTranslate } from 'react-admin'; +import { Title, useTranslate, useDefaultTitle } from 'react-admin'; import { useLocation } from 'react-router-dom'; export const MyError = ({ @@ -186,9 +183,10 @@ export const MyError = ({ }, [pathname, resetErrorBoundary]); const translate = useTranslate(); + const defaultTitle = useDefaultTitle(); return (
- + <Title title={`${defaultTitle}: Error`} /> <h1><ErrorIcon /> Something Went Wrong </h1> <div>A client error occurred and your request couldn't be completed.</div> {process.env.NODE_ENV !== 'production' && ( @@ -220,10 +218,13 @@ Lets you override the menu. ```jsx // in src/Layout.js import { Layout } from 'react-admin'; - import { MyMenu } from './MyMenu'; -export const Layout = (props) => <Layout {...props} menu={MyMenu} />; +export const Layout = ({ children }) => ( + <Layout menu={MyMenu}> + {children} + </Layout> +); ``` You can create a custom menu component using [react-admin's `<Menu>` component](./Menu.md): @@ -276,8 +277,11 @@ import { Layout } from 'react-admin'; import { MySidebar } from './MySidebar'; -export const Layout = (props) => <Layout {...props} sidebar={MySidebar} />; - +export const Layout = ({ children }) => ( + <Layout sidebar={MySidebar}> + {children} + </Layout> +); // in src/MySidebar.js import * as React from 'react'; @@ -340,7 +344,11 @@ const MySidebar = (props) => ( /> ); -const MyLayout = props => <Layout {...props} sidebar={MySidebar} /> +const MyLayout = ({ children }) => ( + <Layout sidebar={MySidebar}> + {children} + </Layout> +); ``` {% endraw %} @@ -350,8 +358,10 @@ Pass an `sx` prop to customize the style of the main component and the underlyin {% raw %} ```jsx -export const MyLayout = (props) => ( - <Layout sx={{ '& .RaLayout-appFrame': { marginTop: 55 } }} {...props} /> +export const MyLayout = ({ children }) => ( + <Layout sx={{ '& .RaLayout-appFrame': { marginTop: 55 } }}> + {children} + </Layout> ); ``` {% endraw %} @@ -385,9 +395,11 @@ const getCookie = (name) => document.cookie .find(row => row.startsWith(`${name}=`)) ?.split('=')[1]; -export const MyLayout = (props) => ( +export const MyLayout = ({ children }) => ( <TenantContext.Provider value={getCookie('tenant')}> - <Layout {...props} /> + <Layout> + {children} + </Layout> </TenantContext.Provider> ); ``` @@ -401,11 +413,11 @@ A custom layout is also the ideal place to add debug tools, e.g. [react-query de import { Layout } from 'react-admin'; import { ReactQueryDevtools } from 'react-query/devtools' -export const MyLayout = (props) => ( - <> - <Layout {...props} /> +export const MyLayout = ({ children }) => ( + <Layout> + {children} <ReactQueryDevtools /> - </> + </Layout> ); ``` @@ -443,16 +455,28 @@ You can also write your own layout component from scratch (see below). ## Writing A Layout From Scratch -For more custom layouts, write a component from scratch. It must contain a `{children}` placeholder, where react-admin will render the resources. Use the [default layout](https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/layout/Layout.tsx) as a starting point. Here is a simplified version (with no responsive support): +For more custom layouts, write a component from scratch. Your custom layout will receive the page content as `children`, so it should render it somewhere. + +In its simplest form, a custom layout is just a component that renders its children: + +```tsx +const MyLayout = ({ children }) => ( + <div> + <h1>My App</h1> + <main>{children}</main> + </div> +); +``` + +You can use the [default layout](https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/layout/Layout.tsx) as a starting point for your custom layout. Here is a simplified version (with no responsive support): {% raw %} ```jsx // in src/MyLayout.js -import * as React from 'react'; import { Box } from '@mui/material'; import { AppBar, Menu, Sidebar } from 'react-admin'; -const MyLayout = ({ children, dashboard }) => ( +const MyLayout = ({ children }) => ( <Box display="flex" flexDirection="column" @@ -469,7 +493,7 @@ const MyLayout = ({ children, dashboard }) => ( <AppBar /> <Box display="flex" flexGrow={1}> <Sidebar> - <Menu hasDashboard={!!dashboard} /> + <Menu /> </Sidebar> <Box display="flex" diff --git a/docs/LocalesMenuButton.md b/docs/LocalesMenuButton.md index b4d5dd9495e..c8035c2c4a9 100644 --- a/docs/LocalesMenuButton.md +++ b/docs/LocalesMenuButton.md @@ -46,7 +46,11 @@ import { Admin, Resource, Layout } from 'react-admin'; import { MyAppBar } from './MyAppBar'; -const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />; +const MyLayout = ({ children }) => ( + <Layout appBar={MyAppBar}> + {children} + </Layout> +); const i18nProvider = polyglotI18nProvider( locale => (locale === 'fr' ? frenchMessages : englishMessages), diff --git a/docs/Menu.md b/docs/Menu.md index 154df47384e..20aa37cc3a0 100644 --- a/docs/Menu.md +++ b/docs/Menu.md @@ -34,10 +34,13 @@ Then, create a custom layout using [the `<Layout>` component](./Layout.md) and p ```jsx // in src/MyLayout.js import { Layout } from 'react-admin'; - import { MyMenu } from './MyMenu'; -export const MyLayout = props => <Layout {...props} menu={MyMenu} />; +export const MyLayout = ({ children }) => ( + <Layout menu={MyMenu}> + {children} + </Layout> +); ``` Finally, pass this custom layout to the `<Admin>` component: @@ -79,7 +82,11 @@ import PeopleIcon from '@mui/icons-material/People'; import { dataProvider } from './dataProvider'; const MyMenu = () => <Menu />; -const MyLayout = (props) => <Layout {...props} menu={MyMenu} /> +const MyLayout = ({ children }) => ( + <Layout menu={MyMenu}> + {children} + </Layout> +); const App = () => ( <Admin dataProvider={dataProvider} layout={MyLayout} dashboard={MyDashboard}> @@ -91,7 +98,7 @@ const App = () => ( ); ``` -Renders the following menu: +Renders the following menu: ![standard menu with dashboard](./img/menu-with-dashboard.webp) @@ -416,12 +423,15 @@ If you need to display a menu item with a submenu, you should use [the `<MultiLe You can display a badge on the menu item to indicate that new data is available. Use [the `<MenuLive>` component](./MenuLive.md) instead of `<Menu>` to enable this feature. ```tsx -import { Admin, Layout, LayoutProps, Resource } from 'react-admin'; +import type { ReactNode } from 'react'; +import { Admin, Layout, Resource } from 'react-admin'; import { MenuLive } from '@react-admin/ra-realtime'; import { PostList, PostShow, PostEdit, realTimeDataProvider } from '.'; -const CustomLayout = (props: LayoutProps) => ( - <Layout {...props} menu={MenuLive} /> +const CustomLayout = ({ children}: { children: ReactNode }) => ( + <Layout menu={MenuLive}> + {children} + </Layout> ); const MyReactAdmin = () => ( diff --git a/docs/MenuLive.md b/docs/MenuLive.md index 624e17cc495..c868bef127a 100644 --- a/docs/MenuLive.md +++ b/docs/MenuLive.md @@ -14,12 +14,15 @@ title: "The MenuLive Component" Use `<MenuLive>` instead of `<Menu>` in a custom layout: ```tsx -import { Admin, Layout, LayoutProps, Resource } from 'react-admin'; +import type { ReactNode } from 'react'; +import { Admin, Layout, Resource } from 'react-admin'; import { MenuLive } from '@react-admin/ra-realtime'; import { PostList, PostShow, PostEdit, realTimeDataProvider } from '.'; -const CustomLayout = (props: LayoutProps) => ( - <Layout {...props} menu={MenuLive} /> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <Layout menu={MenuLive}> + {children} + </Layout> ); const MyReactAdmin = () => ( diff --git a/docs/MultiLevelMenu.md b/docs/MultiLevelMenu.md index 5014e3c72ee..410836feb63 100644 --- a/docs/MultiLevelMenu.md +++ b/docs/MultiLevelMenu.md @@ -50,7 +50,7 @@ const MyMenu = () => ( Note that each `<MultiLevelMenu.Item>` requires a unique `name` attribute. -Then, create a custom layout using [the `<Layout>` component](./Layout.md) and pass your custom menu component to it. Make sure you wrap the layout with the `<AppLocationContext>` component. +Then, create a custom layout using [the `<Layout>` component](./Layout.md) and pass your custom menu component to it. Make sure you wrap the layout with the `<AppLocationContext>` component. ```jsx // in src/MyLayout.js @@ -59,9 +59,11 @@ import { AppLocationContext } from '@react-admin/ra-navigation'; import { MyMenu } from './MyMenu'; -export const MyLayout = (props) => ( +export const MyLayout = ({ children }) => ( <AppLocationContext> - <Layout {...props} menu={MyMenu} /> + <Layout menu={MyMenu}> + {children} + </Layout> </AppLocationContext> ); ``` diff --git a/docs/Realtime.md b/docs/Realtime.md index fe770fe6c4f..6d94b131f96 100644 --- a/docs/Realtime.md +++ b/docs/Realtime.md @@ -218,8 +218,10 @@ import { MenuLive } from '@react-admin/ra-realtime'; import { PostList, PostShow, PostEdit, realTimeDataProvider } from '.'; -const CustomLayout = (props) => ( - <Layout {...props} menu={MenuLive} /> +const CustomLayout = ({ children }) => ( + <Layout menu={MenuLive}> + {children} + </Layout> ); const MyReactAdmin = () => ( diff --git a/docs/Routing.md b/docs/Routing.md index 2c66a38d0b9..35d69de6eea 100644 --- a/docs/Routing.md +++ b/docs/Routing.md @@ -83,9 +83,9 @@ import { Layout } from 'react-admin'; import { usePageTracking } from './usePageTracking'; -export const MyLayout = (props) => { +export const MyLayout = ({ children }) => { usePageTracking(); - return <Layout {...props} />; + return <Layout>{children}</Layout>; } ``` diff --git a/docs/SX.md b/docs/SX.md index fee4065e2f6..d06acedfa14 100644 --- a/docs/SX.md +++ b/docs/SX.md @@ -366,10 +366,11 @@ Sometimes you want the format to depend on the value. Use `useRecordContext` to The following example shows how to create a new `<ColoredNumberField>` component, which renders with red text when its value is less than 0. {% raw %} -```jsx +```tsx import { useRecordContext, NumberField, List, Datagrid, TextField, EditButton } from 'react-admin'; +import type { NumberFieldProps } from 'react-admin'; -const ColoredNumberField = (props) => { +const ColoredNumberField = (props: NumberFieldProps) => { const record = useRecordContext(); return ( <NumberField diff --git a/docs/Search.md b/docs/Search.md index a3653655e9d..dd382e1d06c 100644 --- a/docs/Search.md +++ b/docs/Search.md @@ -102,7 +102,11 @@ Include that AppBar in [a custom layout component](./Layout.md): import { Layout } from "react-admin"; import { MyAppbar } from "./MyAppBar"; -export const MyLayout = (props) => <Layout {...props} appBar={MyAppbar} />; +export const MyLayout = ({ children }) => ( + <Layout appBar={MyAppbar}> + {children} + </Layout> +); ``` Finally, include that custom layout in the `<Admin>`. @@ -133,8 +137,10 @@ If you're using [the `<ContainerLayout>` component](./ContainerLayout.md), you c import { ContainerLayout } from "@react-admin/ra-navigation"; import { Search } from "@react-admin/ra-search"; -const MyLayout = (props: any) => ( - <ContainerLayout {...props} maxWidth="xl" toolbar={<Search />} /> +const MyLayout = ({ children }) => ( + <ContainerLayout maxWidth="xl" toolbar={<Search />}> + {children} + </ContainerLayout> ); ``` @@ -200,7 +206,11 @@ const MyAppBar = () => ( </AppBar> ); -const MyLayout = props => <Layout {...props} appBar={MyAppBar} />; +const MyLayout = ({ children }) => ( + <Layout appBar={MyAppBar}> + {children} + </Layout> +); export const App = () => ( <Admin dataProvider={searchDataProvider} layout={MyLayout}> diff --git a/docs/SearchWithResult.md b/docs/SearchWithResult.md index f2bf8ea520a..47d8b64938c 100644 --- a/docs/SearchWithResult.md +++ b/docs/SearchWithResult.md @@ -358,6 +358,7 @@ Here is an implementation example: {% raw %} ```tsx +import type { ReactNode } from 'react'; import { Admin } from 'react-admin'; import { Box } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; @@ -365,15 +366,16 @@ import AlbumIcon from '@mui/icons-material/Album'; import Groups3Icon from '@mui/icons-material/Groups3'; import { SolarLayout, - SolarLayoutProps, SolarMenu, useSolarSidebarActiveMenu, } from '@react-admin/ra-navigation'; import { SearchWithResult } from '@react-admin/ra-search'; import { searchDataProvider } from './searchDataProvider'; -const MySolarLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} menu={MySolarMenu} /> +const MySolarLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout menu={MySolarMenu}> + {children} + </SolarLayout> ); const MySolarMenu = () => ( diff --git a/docs/SolarLayout.md b/docs/SolarLayout.md index 7b7972cc419..6f5c4e3cd72 100644 --- a/docs/SolarLayout.md +++ b/docs/SolarLayout.md @@ -71,10 +71,10 @@ You can customize the AppBar that appears on Mobile by setting the `appBar` prop {% raw %} ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; import { SolarAppBar, - SolarLayoutProps, SolarLayout, } from '@react-admin/ra-navigation'; @@ -96,8 +96,10 @@ const CustomAppBar = () => ( /> ); -const CustomLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} appBar={CustomAppBar} /> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout appBar={CustomAppBar}> + {children} + </SolarLayout> ); export const App = () => ( @@ -121,10 +123,15 @@ If you want to customize this page, or log the error to a third-party service, c ```tsx // in src/MyLayout.tsx +import type { ReactNode } from 'react'; import { Layout } from 'react-admin'; import { MyError } from './MyError'; -export const MyLayout = props => <Layout {...props} error={MyError} />; +export const MyLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout error={MyError}> + {children} + </SolarLayout> +); ``` The following snippet is a simplified version of the react-admin `Error` component, that you can use as a base for your own: @@ -193,13 +200,16 @@ export const MyError = ({ You can customize the icon of the dashboard menu item of the default menu by setting the `logo` prop: ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; -import { SolarLayoutProps, SolarLayout } from '@react-admin/ra-navigation'; +import { SolarLayout } from '@react-admin/ra-navigation'; import { Dashboard } from './Dashboard'; import { Logo } from './Logo'; -const CustomLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} logo={<Logo />} /> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout logo={<Logo />}> + {children} + </SolarLayout> ); export const WithDashboardAndCustomLogo = () => ( @@ -215,12 +225,9 @@ export const WithDashboardAndCustomLogo = () => ( If you need a customized menu, pass it to the `menu` prop. It's recommended to pass a customized [`<SolarMenu>`](#solarmenu) to leverage this layout. This is useful to organize many resources into categories or to provide shortcuts to filtered lists: ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; -import { - SolarLayoutProps, - SolarLayout, - SolarMenu, -} from '@react-admin/ra-navigation'; +import { SolarLayout, SolarMenu } from '@react-admin/ra-navigation'; export const App = () => ( <Admin @@ -233,8 +240,8 @@ export const App = () => ( </Admin> ); -const CustomLayout = ({ children, ...props }: SolarLayoutProps) => ( - <SolarLayout {...props} menu={CustomMenu}> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout menu={CustomMenu}> {children} </SolarLayout> ); @@ -325,11 +332,14 @@ The `sx` prop allows you to customize the layout styles using a MUI [SX](./SX.md {% raw %} ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; -import { SolarLayoutProps, SolarLayout } from '@react-admin/ra-navigation'; +import { SolarLayout } from '@react-admin/ra-navigation'; -const CustomLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} sx={{ bgcolor: 'white' }} /> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout sx={{ bgcolor: 'white' }}> + {children} + </SolarLayout> ); export const App = () => ( @@ -475,19 +485,18 @@ const CustomMenu = () => ( Set the `dense` prop to `true` to reduce the vertical space between items: ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; -import { - SolarLayoutProps, - SolarLayout, - SolarMenu, -} from '@react-admin/ra-navigation'; +import { SolarLayout, SolarMenu } from '@react-admin/ra-navigation'; import { ListItemButton } from '@mui/material'; import { dataProvider } from './dataProvider'; const CustomMenu = () => <SolarMenu dense />; -const CustomLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} menu={CustomMenu} /> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout menu={CustomMenu}> + {children} + </SolarLayout> ); export const App = () => ( @@ -518,12 +527,9 @@ You can customize it by passing your own content to the `userMenu` prop. For instance, here's how to only show a logout button: ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; -import { - SolarLayoutProps, - SolarLayout, - SolarMenu, -} from '@react-admin/ra-navigation'; +import { SolarLayout, SolarMenu } from '@react-admin/ra-navigation'; import { ListItemButton } from '@mui/material'; import { dataProvider } from './dataProvider'; @@ -539,8 +545,8 @@ const CustomUserMenu = () => { const CustomMenu = () => <SolarMenu userMenu={<CustomUserMenu />} />; -const CustomLayout = ({ children, ...props }: SolarLayoutProps) => ( - <SolarLayout {...props} menu={CustomMenu}> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout menu={CustomMenu}> {children} </SolarLayout> ); @@ -566,12 +572,9 @@ You can customize it by passing your own content to the `bottomToolbar` prop. For instance, here's how to show a settings menu item in addition to the existing bottom menu items: ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; -import { - SolarLayoutProps, - SolarLayout, - SolarMenu, -} from '@react-admin/ra-navigation'; +import { SolarLayout, SolarMenu } from '@react-admin/ra-navigation'; import { ListItemButton } from '@mui/material'; import { dataProvider } from './dataProvider'; @@ -590,8 +593,10 @@ const CustomBottomToolbar = () => ( const CustomMenu = () => <SolarMenu bottomToolbar={<CustomBottomToolbar />} />; -const CustomLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} menu={CustomMenu} /> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout menu={CustomMenu}> + {children} + </SolarLayout> ); export const App = () => ( @@ -612,8 +617,9 @@ For instance, here is how to change the background color of the menu: {% raw %} ```tsx +import type { ReactNode } from 'react'; import { Admin, Resource, ListGuesser } from 'react-admin'; -import { SolarLayoutProps, SolarLayout, SolarMenu, SolarMenuProps } from '@react-admin/ra-navigation'; +import { SolarLayout, SolarMenu, SolarMenuProps } from '@react-admin/ra-navigation'; const CustomMenu = (props: SolarMenuProps) => ( <SolarMenu @@ -634,8 +640,10 @@ const CustomMenu = (props: SolarMenuProps) => ( /> ); -const CustomLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} menu={CustomMenu} /> +const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout menu={CustomMenu}> + {children} + </SolarLayout> ); export const App = () => ( @@ -824,12 +832,9 @@ An AppBar alternative for the SolarLayout that is only shown on small devices. I You can customize it by passing children: ```tsx +import type { ReactNode } from 'react'; import { Admin, AppBarProps, Resource, LoadingIndicator } from 'react-admin'; -import { - SolarAppBar, - SolarLayout, - SolarLayoutProps, -} from '@react-admin/ra-navigation'; +import { SolarAppBar, SolarLayout } from '@react-admin/ra-navigation'; import { Search } from '@react-admin/ra-search'; const CustomAppBar = () => ( @@ -839,8 +844,10 @@ const CustomAppBar = () => ( </SolarAppBar> ); -export const CustomLayout = (props: SolarLayoutProps) => ( - <SolarLayout {...props} appBar={CustomAppBar} /> +export const CustomLayout = ({ children }: { children: ReactNode }) => ( + <SolarLayout appBar={CustomAppBar}> + {children} + </SolarLayout> ); export const App = () => ( diff --git a/docs/ToggleThemeButton.md b/docs/ToggleThemeButton.md index b906247fe62..c05e78577d3 100644 --- a/docs/ToggleThemeButton.md +++ b/docs/ToggleThemeButton.md @@ -33,10 +33,13 @@ Then, pass the custom App Bar in a custom `<Layout>`, and the `<Layout>` to your {% raw %} ```jsx import { Admin, Layout } from 'react-admin'; - import { MyAppBar } from './MyAppBar'; -const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />; +const MyLayout = ({ children }) => ( + <Layout appBar={MyAppBar}> + {children} + </Layout> +); const App = () => ( <Admin diff --git a/docs/Upgrade.md b/docs/Upgrade.md index f3157becb41..0c74b23988b 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -23,7 +23,7 @@ The React team has published a [migration guide](https://react.dev/blog/2022/03/ React 18 adds out-of-the-box performance improvements by doing more batching by default. -## Drop support for IE11 +## IE11 Is No Longer Supported React-admin v5 uses React 18, which dropped support for Internet Explorer. If you need to support IE11, you'll have to stay on react-admin v4. @@ -133,6 +133,144 @@ Here are the available codemods you may need to run on your codebase: Check out React Query [codemod documentation](https://tanstack.com/query/latest/docs/react/guides/migrating-to-v5#codemod) for more information. +## `<Admin menu>` Is No Longer Supported + +The `<Admin menu>` prop was deprecated since 4.0. It's no longer supported. If you want to customize the application menu, you'll have to do it in a custom Layout instead: + +```diff +-import { Admin } from 'react-admin'; ++import { Admin, Layout } from 'react-admin'; +import { MyMenu } from './MyMenu'; + ++const MyLayout = ({ children }) => ( ++ <Layout menu={MyMenu}>{children}</Layout> ++); + +const App = () => ( +- <Admin menu={MyMenu} dataProvider={dataProvider}> ++ <Admin layout={MyLayout} dataProvider={dataProvider}> + ... + </Admin> +); +``` + +## Custom Layout No Longer Receives Props + +React-admin used to inject 4 props to [custom layouts](https://marmelab.com/react-admin/Admin.html#layout): `children`, `dashboard`, `menu`, and `title`. In react-admin v5, only the `children` prop is injected. + +This means that you'll need to use hooks to get the other props: + +```diff ++import { useHasDashboard, useDefaultTitle } from 'react-admin'; + +-const MyLayout = ({ children, dashboard, title }) => ( ++const MyLayout = ({ children }) => { +- const hasDashboard = !!dashboard; ++ const hasDashboard = useHasDashboard(); ++ const title = useDefaultTitle(); + // ... +} + +const App = () => ( + <Admin layout={MyLayout} dataProvider={dataProvider}> + ... + </Admin> +); +``` + +As for the `menu` prop, it's no longer injected by react-admin because the `<Admin menu>` prop is no longer supported. But you can still customize the menu of the default Layout as before: + +```tsx +import { Layout } from 'react-admin'; +import { MyMenu } from './MyMenu'; + +const MyLayout = ({ children }) => ( + <Layout menu={MyMenu}>{children}</Layout> +); + +const App = () => ( + <Admin layout={MyLayout} dataProvider={dataProvider}> + ... + </Admin> +); +``` + +## Custom App Bars No Longer Receive Props + +React-admin used to inject 2 props to [custom app bars](https://marmelab.com/react-admin/Layout.html#appbar): `open`, and `title`. These deprecated props are no longer injected in v5. If you need them, you'll have to use hooks: + +```diff ++import { useSidebarState, useDefaultTitle } from 'react-admin'; + +-const MyAppBar = ({ open, title }) => ( ++const MyAppBar = () => { ++ const [open] = useSidebarState(); ++ const title = useDefaultTitle(); + // ... +} + +const MyLayout = ({ children }) => ( + <Layout appBar={MyAppBar}>{children}</Layout> +); +``` + +## Custom Menu No Longer Receive Props + +React-admin used to inject one prop to [custom menus](https://marmelab.com/react-admin/Layout.html#menu): `hasDashboard`. This deprecated prop is no longer injected in v5. If you need it, you'll have to use the `useHasDashboard` hook instead: + +```diff ++import { useHasDashboard } from 'react-admin'; + +-const MyMenu = ({ hasDashboard }) => ( ++const MyMenu = () => { ++ const hasDashboard = useHasDashboard(); + // ... +} + +const MyLayout = ({ children }) => ( + <Layout menu={MyMenu}>{children}</Layout> +); +``` + +## Custom Error Page No Longer Receives Title + +React-admin injects several props to [custom error pages](https://marmelab.com/react-admin/Layout.html#error), including the default app `title`. This prop is no longer injected in v5. If you need it, you'll have to use the `useDefaultTitle` hook instead: + +```diff ++import { useDefaultTitle } from 'react-admin'; + +-const MyError = ({ error, errorInfo, title }) => ( ++const MyError = ({ error, errorInfo }) => { ++ const title = useDefaultTitle(); + // ... +} + +const MyLayout = ({ children }) => ( + <Layout error={MyError}>{children}</Layout> +); +``` + +## Custom Catch All No Longer Receives Title + +React-admin used to inject the default app `title` to [custom catch all pages](https://marmelab.com/react-admin/Admin.html#catchall). This prop is no longer injected in v5. If you need it, you'll have to use the `useDefaultTitle` hook instead: + +```diff ++import { useDefaultTitle } from 'react-admin'; + +-const MyCatchAll = ({ title }) => ( ++const MyCatchAll = () => { ++ const title = useDefaultTitle(); + // ... +} + +const App = () => ( + <Admin catchAll={MyCatchAll} dataProvider={dataProvider}> + ... + </Admin> +); +``` +``` + ## Removed deprecated hooks The following deprecated hooks have been removed diff --git a/docs/Vite.md b/docs/Vite.md index 5e5a79ee343..81e2396ac38 100644 --- a/docs/Vite.md +++ b/docs/Vite.md @@ -110,10 +110,11 @@ To enable it, start by creating a custom layout: ```tsx // in src/admin/MyLayout.tsx -import { CheckForApplicationUpdate, Layout, LayoutProps } from 'react-admin'; +import type { ReactNode } from 'react'; +import { CheckForApplicationUpdate, Layout } from 'react-admin'; -export const MyLayout = ({ children, ...props }: LayoutProps) => ( - <Layout {...props}> +export const MyLayout = ({ children }: { children: ReactNode }) => ( + <Layout> {children} <CheckForApplicationUpdate /> </Layout> diff --git a/docs/canAccess.md b/docs/canAccess.md index b6de7fd5822..0325443165e 100644 --- a/docs/canAccess.md +++ b/docs/canAccess.md @@ -349,7 +349,11 @@ const MyMenu = () => { ); }; -const MyLayout = props => <Layout {...props} menu={MyMenu} />; +const MyLayout = ({ children }) => ( + <Layout menu={MyMenu}> + {children} + </Layout> +); export const App = () => ( <Admin diff --git a/docs/useLogout.md b/docs/useLogout.md index 4957eaf2111..1e97a96641e 100644 --- a/docs/useLogout.md +++ b/docs/useLogout.md @@ -39,8 +39,10 @@ const MyUserMenu = () => ( const MyAppBar = () => <AppBar userMenu={<UserMenu />} />; -const MyLayout = (props) => ( - <Layout {...props} appBar={MyAppBar} /> +const MyLayout = ({ children }) => ( + <Layout appBar={MyAppBar}> + {children} + </Layout> ); export default MyLayout; diff --git a/examples/crm/src/Layout.tsx b/examples/crm/src/Layout.tsx index da32c47b145..440cdbe4102 100644 --- a/examples/crm/src/Layout.tsx +++ b/examples/crm/src/Layout.tsx @@ -1,12 +1,12 @@ -import React, { Suspense, HtmlHTMLAttributes } from 'react'; +import React, { Suspense, ReactNode } from 'react'; import { CssBaseline, Container } from '@mui/material'; -import { CoreLayoutProps, CheckForApplicationUpdate } from 'react-admin'; +import { CheckForApplicationUpdate } from 'react-admin'; import { ErrorBoundary } from 'react-error-boundary'; import { Error, Loading } from 'react-admin'; import Header from './Header'; -const Layout = ({ children }: LayoutProps) => ( +const Layout = ({ children }: { children: ReactNode }) => ( <> <CssBaseline /> <Header /> @@ -22,8 +22,4 @@ const Layout = ({ children }: LayoutProps) => ( </> ); -export interface LayoutProps - extends CoreLayoutProps, - Omit<HtmlHTMLAttributes<HTMLDivElement>, 'title'> {} - export default Layout; diff --git a/examples/demo/src/layout/Layout.tsx b/examples/demo/src/layout/Layout.tsx index 3de84c0c74a..bc6c818fa84 100644 --- a/examples/demo/src/layout/Layout.tsx +++ b/examples/demo/src/layout/Layout.tsx @@ -1,8 +1,10 @@ import * as React from 'react'; -import { Layout, LayoutProps } from 'react-admin'; +import { Layout } from 'react-admin'; import AppBar from './AppBar'; import Menu from './Menu'; -export default (props: LayoutProps) => ( - <Layout {...props} appBar={AppBar} menu={Menu} /> +export default ({ children }: { children: React.ReactNode }) => ( + <Layout appBar={AppBar} menu={Menu}> + {children} + </Layout> ); diff --git a/examples/simple/src/Layout.tsx b/examples/simple/src/Layout.tsx index 9a840ca66b4..f17e8f98f70 100644 --- a/examples/simple/src/Layout.tsx +++ b/examples/simple/src/Layout.tsx @@ -10,9 +10,9 @@ const MyAppBar = () => ( </AppBar> ); -export default props => ( +export default ({ children }) => ( <> - <Layout {...props} appBar={MyAppBar} /> + <Layout appBar={MyAppBar}>{children}</Layout> <ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" diff --git a/packages/create-react-admin/templates/common/src/Layout.tsx b/packages/create-react-admin/templates/common/src/Layout.tsx index b6a3be25da0..ab303b7e8c4 100644 --- a/packages/create-react-admin/templates/common/src/Layout.tsx +++ b/packages/create-react-admin/templates/common/src/Layout.tsx @@ -1,11 +1,11 @@ +import type { ReactNode } from 'react'; import { Layout as RALayout, - LayoutProps, CheckForApplicationUpdate, } from "react-admin"; -export const Layout = ({ children, ...props }: LayoutProps) => ( - <RALayout {...props}> +export const Layout = ({ children }: { children: ReactNode }) => ( + <RALayout> {children} <CheckForApplicationUpdate /> </RALayout> diff --git a/packages/ra-core/src/auth/usePermissions.ts b/packages/ra-core/src/auth/usePermissions.ts index 2661bd86bd7..4f707e44050 100644 --- a/packages/ra-core/src/auth/usePermissions.ts +++ b/packages/ra-core/src/auth/usePermissions.ts @@ -31,12 +31,12 @@ const emptyParams = {}; * @example * import { usePermissions } from 'react-admin'; * - * const PostDetail = props => { + * const PostDetail = () => { * const { isPending, permissions } = usePermissions(); * if (!isPending && permissions == 'editor') { - * return <PostEdit {...props} /> + * return <PostEdit /> * } else { - * return <PostShow {...props} /> + * return <PostShow /> * } * }; */ diff --git a/packages/ra-core/src/controller/create/CreateBase.tsx b/packages/ra-core/src/controller/create/CreateBase.tsx index 54ed9fa90e6..8c66a551c71 100644 --- a/packages/ra-core/src/controller/create/CreateBase.tsx +++ b/packages/ra-core/src/controller/create/CreateBase.tsx @@ -19,8 +19,8 @@ import { ResourceContextProvider } from '../../core'; * * @example // Custom edit layout * - * const PostCreate = props => ( - * <CreateBase {...props}> + * const PostCreate = () => ( + * <CreateBase> * <Grid container> * <Grid item xs={8}> * <SimpleForm> diff --git a/packages/ra-core/src/core/CoreAdmin.tsx b/packages/ra-core/src/core/CoreAdmin.tsx index 85719837906..48e848899fe 100644 --- a/packages/ra-core/src/core/CoreAdmin.tsx +++ b/packages/ra-core/src/core/CoreAdmin.tsx @@ -97,7 +97,6 @@ export const CoreAdmin = (props: CoreAdminProps) => { layout, loading, loginPage, - menu, // deprecated, use a custom layout instead ready, requireAuth, store, @@ -117,7 +116,6 @@ export const CoreAdmin = (props: CoreAdminProps) => { layout={layout} dashboard={dashboard} disableTelemetry={disableTelemetry} - menu={menu} catchAll={catchAll} title={title} loading={loading} diff --git a/packages/ra-core/src/core/CoreAdminRoutes.tsx b/packages/ra-core/src/core/CoreAdminRoutes.tsx index 6c91e4df3b1..c2bd3b923eb 100644 --- a/packages/ra-core/src/core/CoreAdminRoutes.tsx +++ b/packages/ra-core/src/core/CoreAdminRoutes.tsx @@ -7,11 +7,12 @@ import { useScrollToTop, useCreatePath } from '../routing'; import { AdminChildren, CatchAllComponent, + DashboardComponent, LayoutComponent, LoadingComponent, - CoreLayoutProps, } from '../types'; import { useConfigureAdminRouterFromChildren } from './useConfigureAdminRouterFromChildren'; +import { HasDashboardContextProvider } from './HasDashboardContext'; export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { useScrollToTop(); @@ -29,10 +30,8 @@ export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { catchAll: CatchAll, dashboard, loading: LoadingPage, - menu, requireAuth, ready: Ready, - title, } = props; const [canRender, setCanRender] = useState(!requireAuth); @@ -77,8 +76,8 @@ export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { <Route path="/*" element={ - <div> - <Layout dashboard={dashboard} menu={menu} title={title}> + <HasDashboardContextProvider value={!!dashboard}> + <Layout> <Routes> {customRoutesWithLayout} {Children.map(resources, resource => ( @@ -107,21 +106,18 @@ export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { ) : null } /> - <Route - path="*" - element={<CatchAll title={title} />} - /> + <Route path="*" element={<CatchAll />} /> </Routes> </Layout> - </div> + </HasDashboardContextProvider> } /> </Routes> ); }; -export interface CoreAdminRoutesProps - extends Omit<CoreLayoutProps, 'children'> { +export interface CoreAdminRoutesProps { + dashboard?: DashboardComponent; layout: LayoutComponent; catchAll: CatchAllComponent; children?: AdminChildren; diff --git a/packages/ra-core/src/core/CoreAdminUI.tsx b/packages/ra-core/src/core/CoreAdminUI.tsx index 89020f4d3d0..5310d02b391 100644 --- a/packages/ra-core/src/core/CoreAdminUI.tsx +++ b/packages/ra-core/src/core/CoreAdminUI.tsx @@ -4,11 +4,11 @@ import { Routes, Route } from 'react-router-dom'; import { CoreAdminRoutes } from './CoreAdminRoutes'; import { Ready } from '../util'; -import { +import { DefaultTitleContextProvider } from './DefaultTitleContext'; +import type { TitleComponent, LoginComponent, LayoutComponent, - CoreLayoutProps, AdminChildren, CatchAllComponent, DashboardComponent, @@ -17,7 +17,9 @@ import { export type ChildrenFunction = () => ComponentType[]; -const DefaultLayout = ({ children }: CoreLayoutProps) => <>{children}</>; +const DefaultLayout = ({ children }: { children: React.ReactNode }) => ( + <>{children}</> +); export interface CoreAdminUIProps { /** @@ -94,7 +96,11 @@ export interface CoreAdminUIProps { * @example * import { Admin, Layout } from 'react-admin'; * - * const MyLayout = props => <Layout {...props} appBarAlwaysOn />; + * const MyLayout = ({ children }) => ( + * <Layout appBarAlwaysOn> + * {children} + * </Layout> + * ); * * export const App = () => ( * <Admin dataProvider={dataProvider} layout={MyLayout}> @@ -152,12 +158,6 @@ export interface CoreAdminUIProps { */ loginPage?: LoginComponent | boolean; - /** - * @deprecated use a custom layout instead - * @see https://marmelab.com/react-admin/Admin.html#layout - */ - menu?: ComponentType; - /** * Flag to require authentication for all routes. Defaults to false. * @@ -227,9 +227,8 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => { loading = Noop, loginPage: LoginPage = false, authCallbackPage: LoginCallbackPage = false, - menu, // deprecated, use a custom layout instead ready = Ready, - title = 'React Admin', + title, requireAuth = false, } = props; @@ -248,36 +247,39 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => { }, [disableTelemetry]); return ( - <Routes> - {LoginPage !== false && LoginPage !== true ? ( - <Route path="/login" element={createOrGetElement(LoginPage)} /> - ) : null} + <DefaultTitleContextProvider value={title}> + <Routes> + {LoginPage !== false && LoginPage !== true ? ( + <Route + path="/login" + element={createOrGetElement(LoginPage)} + /> + ) : null} + + {LoginCallbackPage !== false && LoginCallbackPage !== true ? ( + <Route + path="/auth-callback" + element={createOrGetElement(LoginCallbackPage)} + /> + ) : null} - {LoginCallbackPage !== false && LoginCallbackPage !== true ? ( <Route - path="/auth-callback" - element={createOrGetElement(LoginCallbackPage)} + path="/*" + element={ + <CoreAdminRoutes + catchAll={catchAll} + dashboard={dashboard} + layout={layout} + loading={loading} + requireAuth={requireAuth} + ready={ready} + > + {children} + </CoreAdminRoutes> + } /> - ) : null} - - <Route - path="/*" - element={ - <CoreAdminRoutes - catchAll={catchAll} - dashboard={dashboard} - layout={layout} - loading={loading} - menu={menu} - requireAuth={requireAuth} - ready={ready} - title={title} - > - {children} - </CoreAdminRoutes> - } - /> - </Routes> + </Routes> + </DefaultTitleContextProvider> ); }; diff --git a/packages/ra-core/src/core/DefaultTitleContext.ts b/packages/ra-core/src/core/DefaultTitleContext.ts new file mode 100644 index 00000000000..40abfc055d3 --- /dev/null +++ b/packages/ra-core/src/core/DefaultTitleContext.ts @@ -0,0 +1,19 @@ +import { createContext, useContext } from 'react'; +import type { TitleComponent } from '../types'; + +export const DefaultTitleContext = createContext<TitleComponent>('React Admin'); + +export const DefaultTitleContextProvider = DefaultTitleContext.Provider; + +/** + * Get the application title defined at the `<Admin>` level + * + * @example + * import { useDefaultTitle } from 'react-admin'; + * + * const AppBar = () => { + * const defaultTitle = useDefaultTitle(); + * return <span>{defaultTitle}</span>; + * } + */ +export const useDefaultTitle = () => useContext(DefaultTitleContext); diff --git a/packages/ra-core/src/core/HasDashboardContext.ts b/packages/ra-core/src/core/HasDashboardContext.ts new file mode 100644 index 00000000000..c47449cfd87 --- /dev/null +++ b/packages/ra-core/src/core/HasDashboardContext.ts @@ -0,0 +1,25 @@ +import { createContext, useContext } from 'react'; + +export const HasDashboardContext = createContext<boolean>(undefined); + +export const HasDashboardContextProvider = HasDashboardContext.Provider; + +/** + * Returns true if the app has a dashboard defined at the <Admin> level. + * + * @private + * @example + * import { useHasDashboard } from 'react-admin'; + * + * const MyMenu = () => { + * const hasDashboard = useHasDashboard(); + * return ( + * <Menu> + * {hasDashboard && <DashboardMenuItem />} + * <MenuItemLink to="/posts" /> + * <MenuItemLink to="/comments" /> + * </Menu> + * ); + * } + */ +export const useHasDashboard = () => useContext(HasDashboardContext); diff --git a/packages/ra-core/src/core/index.ts b/packages/ra-core/src/core/index.ts index 4409feaf86f..5d1221dd656 100644 --- a/packages/ra-core/src/core/index.ts +++ b/packages/ra-core/src/core/index.ts @@ -3,6 +3,8 @@ export * from './CoreAdminContext'; export * from './CoreAdminRoutes'; export * from './CoreAdminUI'; export * from './CustomRoutes'; +export * from './DefaultTitleContext'; +export * from './HasDashboardContext'; export * from './Resource'; export * from './ResourceContext'; export * from './ResourceContextProvider'; diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index f478e41a116..4a2b6126f29 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -11,8 +11,8 @@ import { useWrappedSource } from '../core'; * * @example * - * const PostEdit = (props) => ( - * <Edit {...props}> + * const PostEdit = () => ( + * <Edit> * <SimpleForm<FieldValues>> * <BooleanInput source="hasEmail" /> * <FormDataConsumer> @@ -26,8 +26,8 @@ import { useWrappedSource } from '../core'; * * @example * - * const OrderEdit = (props) => ( - * <Edit {...props}> + * const OrderEdit = () => ( + * <Edit> * <SimpleForm> * <SelectInput source="country" choices={countries} /> * <FormDataConsumer<FieldValues>> diff --git a/packages/ra-core/src/form/FormGroupContextProvider.tsx b/packages/ra-core/src/form/FormGroupContextProvider.tsx index a2454e368d3..1eb1433f9e8 100644 --- a/packages/ra-core/src/form/FormGroupContextProvider.tsx +++ b/packages/ra-core/src/form/FormGroupContextProvider.tsx @@ -12,8 +12,8 @@ import { useFormGroups } from './useFormGroups'; * import { Edit, SimpleForm, TextInput, FormGroupContextProvider, useFormGroup } from 'react-admin'; * import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material'; * - * const PostEdit = (props) => ( - * <Edit {...props}> + * const PostEdit = () => ( + * <Edit> * <SimpleForm> * <TextInput source="title" /> * <FormGroupContextProvider name="options"> diff --git a/packages/ra-core/src/form/useFormGroup.ts b/packages/ra-core/src/form/useFormGroup.ts index afee8d7cdac..d0de0bbcb27 100644 --- a/packages/ra-core/src/form/useFormGroup.ts +++ b/packages/ra-core/src/form/useFormGroup.ts @@ -28,8 +28,8 @@ type FormGroupState = { * import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material'; * import ExpandMoreIcon from '@mui/icons-material/ExpandMoreIcon'; * - * const PostEdit = (props) => ( - * <Edit {...props}> + * const PostEdit = () => ( + * <Edit> * <SimpleForm> * <TextInput source="title" /> * <FormGroupContextProvider name="options"> diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index d06e31f996f..9aa4972364f 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -319,12 +319,7 @@ export type LoginComponent = ComponentType<{}> | ReactElement<any>; export type DashboardComponent = ComponentType<WithPermissionsChildrenParams>; export interface CoreLayoutProps { - children?: ReactNode; - dashboard?: DashboardComponent; - menu?: ComponentType<{ - hasDashboard?: boolean; - }>; - title?: TitleComponent; + children: ReactNode; } export type LayoutComponent = ComponentType<CoreLayoutProps>; diff --git a/packages/ra-no-code/src/ui/Layout.tsx b/packages/ra-no-code/src/ui/Layout.tsx index 6bf29220606..af33c0e3e84 100644 --- a/packages/ra-no-code/src/ui/Layout.tsx +++ b/packages/ra-no-code/src/ui/Layout.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Layout as RaLayout, LayoutProps } from 'react-admin'; +import { Layout as RaLayout } from 'react-admin'; import { useResourcesConfiguration } from '../ResourceConfiguration'; import { Menu } from './Menu'; import { AppBar } from './Appbar'; import { Ready } from './Ready'; -export const Layout = (props: LayoutProps) => { +export const Layout = ({ children }: { children: React.ReactNode }) => { const [resources] = useResourcesConfiguration(); const hasResources = !!resources && Object.keys(resources).length > 0; @@ -13,5 +13,9 @@ export const Layout = (props: LayoutProps) => { return <Ready />; } - return <RaLayout {...props} appBar={AppBar} menu={Menu} />; + return ( + <RaLayout appBar={AppBar} menu={Menu}> + {children} + </RaLayout> + ); }; diff --git a/packages/ra-ui-materialui/src/button/LocalesMenuButton.stories.tsx b/packages/ra-ui-materialui/src/button/LocalesMenuButton.stories.tsx index c03c0ff0227..40f1baecf3b 100644 --- a/packages/ra-ui-materialui/src/button/LocalesMenuButton.stories.tsx +++ b/packages/ra-ui-materialui/src/button/LocalesMenuButton.stories.tsx @@ -167,7 +167,9 @@ const MyAppBar = () => ( /> </AppBar> ); -const MyLayout = props => <Layout {...props} appBar={MyAppBar} />; +const MyLayout = ({ children }) => ( + <Layout appBar={MyAppBar}>{children}</Layout> +); export const FullApp = () => ( <AdminContext diff --git a/packages/ra-ui-materialui/src/button/ToggleThemeButton.stories.tsx b/packages/ra-ui-materialui/src/button/ToggleThemeButton.stories.tsx index 15de1e9a419..ea8c1be3dc1 100644 --- a/packages/ra-ui-materialui/src/button/ToggleThemeButton.stories.tsx +++ b/packages/ra-ui-materialui/src/button/ToggleThemeButton.stories.tsx @@ -108,7 +108,9 @@ const MyAppBar = () => ( <ToggleThemeButton darkTheme={{ palette: { mode: 'dark' } }} /> </AppBar> ); -const MyLayout = props => <Layout {...props} appBar={MyAppBar} />; +const MyLayout = ({ children }) => ( + <Layout appBar={MyAppBar}>{children}</Layout> +); export const Legacy = () => ( <Admin diff --git a/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx b/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx index 61fa0b0d938..f0977468f14 100644 --- a/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx +++ b/packages/ra-ui-materialui/src/button/ToggleThemeButton.tsx @@ -18,7 +18,11 @@ import { useThemesContext, useTheme } from '../theme'; * <AppBar toolbar={<ToggleThemeButton />} /> * ); * - * const MyLayout = props => <Layout {...props} appBar={MyAppBar} />; + * const MyLayout = ({ children }) => ( + * <Layout appBar={MyAppBar}> + * {children} + * </Layout> + * ); */ export const ToggleThemeButton = () => { const translate = useTranslate(); diff --git a/packages/ra-ui-materialui/src/detail/Create.tsx b/packages/ra-ui-materialui/src/detail/Create.tsx index 45d3481cdeb..a5e6618d5d2 100644 --- a/packages/ra-ui-materialui/src/detail/Create.tsx +++ b/packages/ra-ui-materialui/src/detail/Create.tsx @@ -29,8 +29,8 @@ import { CreateBase } from 'ra-core'; * import * as React from "react"; * import { Create, SimpleForm, TextInput } from 'react-admin'; * - * export const PostCreate = (props) => ( - * <Create {...props}> + * export const PostCreate = () => ( + * <Create> * <SimpleForm> * <TextInput source="title" /> * </SimpleForm> diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx index 8164782ff4f..24038cad311 100644 --- a/packages/ra-ui-materialui/src/detail/Edit.tsx +++ b/packages/ra-ui-materialui/src/detail/Edit.tsx @@ -30,8 +30,8 @@ import { EditBase } from 'ra-core'; * import * as React from "react"; * import { Edit, SimpleForm, TextInput } from 'react-admin'; * - * export const PostEdit = (props) => ( - * <Edit {...props}> + * export const PostEdit = () => ( + * <Edit> * <SimpleForm> * <TextInput source="title" /> * </SimpleForm> diff --git a/packages/ra-ui-materialui/src/detail/Tab.tsx b/packages/ra-ui-materialui/src/detail/Tab.tsx index 2a50a5202c6..3fb93b6a63f 100644 --- a/packages/ra-ui-materialui/src/detail/Tab.tsx +++ b/packages/ra-ui-materialui/src/detail/Tab.tsx @@ -32,8 +32,8 @@ import { Labeled } from '../Labeled'; * import PersonPinIcon from '@mui/icons-material/PersonPin'; * import { Show, TabbedShowLayout, TextField } from 'react-admin'; * - * export const PostShow = (props) => ( - * <Show {...props}> + * export const PostShow = () => ( + * <Show> * <TabbedShowLayout> * <TabbedShowLayout.Tab label="Content" icon={<FavoriteIcon />}> * <TextField source="title" /> diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.tsx index 20110acc8c5..bfb30e43a90 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.tsx @@ -18,8 +18,8 @@ import { Toolbar } from './Toolbar'; * import { Create, Edit, SimpleForm, TextInput, DateInput, ReferenceManyField, Datagrid, TextField, DateField, EditButton } from 'react-admin'; * import RichTextInput from 'ra-input-rich-text'; * - * export const PostCreate = (props) => ( - * <Create {...props}> + * export const PostCreate = () => ( + * <Create> * <SimpleForm> * <TextInput source="title" /> * <TextInput source="teaser" options={{ multiline: true }} /> diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.tsx b/packages/ra-ui-materialui/src/form/TabbedForm.tsx index a9cb3d63757..4da9c09b8b1 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedForm.tsx @@ -36,8 +36,8 @@ import { FormTab } from './FormTab'; * EditButton * } from 'react-admin'; * - * export const PostEdit = (props) => ( - * <Edit {...props}> + * export const PostEdit = () => ( + * <Edit> * <TabbedForm> * <TabbedForm.Tab label="summary"> * <TextInput disabled label="Id" source="id" /> diff --git a/packages/ra-ui-materialui/src/input/DateInput.tsx b/packages/ra-ui-materialui/src/input/DateInput.tsx index 6f249875e85..1dd3e738ffa 100644 --- a/packages/ra-ui-materialui/src/input/DateInput.tsx +++ b/packages/ra-ui-materialui/src/input/DateInput.tsx @@ -16,8 +16,8 @@ import { InputHelperText } from './InputHelperText'; * @example * import { Edit, SimpleForm, DateInput } from 'react-admin'; * - * const PostEdit = (props) => ( - * <Edit {...props}> + * const PostEdit = () => ( + * <Edit> * <SimpleForm> * <DateInput source="published_at" /> * </SimpleForm> diff --git a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx index 18450fecd0e..3e41c5276e5 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx @@ -31,8 +31,8 @@ import { AutocompleteArrayInput } from './AutocompleteArrayInput'; * or <CheckboxGroupInput>. * * @example - * export const PostEdit = (props) => ( - * <Edit {...props}> + * export const PostEdit = () => ( + * <Edit> * <SimpleForm> * <ReferenceArrayInput source="tag_ids" reference="tags"> * <SelectArrayInput optionText="name" /> diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx index ac90f900279..d5c6279bdd9 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx @@ -22,8 +22,8 @@ import { AutocompleteInput } from './AutocompleteInput'; * instead of `<AutocompleteInput>`). * * @example // default selector: AutocompleteInput - * export const CommentEdit = (props) => ( - * <Edit {...props}> + * export const CommentEdit = () => ( + * <Edit> * <SimpleForm> * <ReferenceInput label="Post" source="post_id" reference="posts" /> * </SimpleForm> @@ -31,8 +31,8 @@ import { AutocompleteInput } from './AutocompleteInput'; * ); * * @example // using a SelectInput as selector - * export const CommentEdit = (props) => ( - * <Edit {...props}> + * export const CommentEdit = () => ( + * <Edit> * <SimpleForm> * <ReferenceInput label="Post" source="post_id" reference="posts"> * <SelectInput optionText="title" /> diff --git a/packages/ra-ui-materialui/src/input/TimeInput.tsx b/packages/ra-ui-materialui/src/input/TimeInput.tsx index ff2c49cd5b1..d9aa4bbb85f 100644 --- a/packages/ra-ui-materialui/src/input/TimeInput.tsx +++ b/packages/ra-ui-materialui/src/input/TimeInput.tsx @@ -36,8 +36,8 @@ const parseTime = (value: string) => { * @example * import { Edit, SimpleForm, TimeInput } from 'react-admin'; * - * const PostEdit = (props) => ( - * <Edit {...props}> + * const PostEdit = () => ( + * <Edit> * <SimpleForm> * <TimeInput source="published_at" /> * </SimpleForm> diff --git a/packages/ra-ui-materialui/src/layout/AppBar.tsx b/packages/ra-ui-materialui/src/layout/AppBar.tsx index 4e788fc807c..c5a993aafe5 100644 --- a/packages/ra-ui-materialui/src/layout/AppBar.tsx +++ b/packages/ra-ui-materialui/src/layout/AppBar.tsx @@ -49,8 +49,6 @@ export const AppBar: FC<AppBarProps> = memo(props => { children, className, color = 'secondary', - open, - title, toolbar = defaultToolbarElement, userMenu = DefaultUserMenu, container: Container = alwaysOn ? 'div' : HideOnScroll, @@ -119,17 +117,13 @@ AppBar.propTypes = { 'transparent', ]), container: ComponentPropType, - /** - * @deprecated - */ - open: PropTypes.bool, toolbar: PropTypes.element, userMenu: PropTypes.oneOfType([PropTypes.element, PropTypes.bool]), }; const DefaultUserMenu = <UserMenu />; -export interface AppBarProps extends Omit<MuiAppBarProps, 'title'> { +export interface AppBarProps extends MuiAppBarProps { /** * This prop is injected by Layout. You should not use it directly unless * you are using a custom layout. @@ -137,14 +131,6 @@ export interface AppBarProps extends Omit<MuiAppBarProps, 'title'> { */ alwaysOn?: boolean; container?: React.ElementType<any>; - /** - * @deprecated injected by Layout but not used by this AppBar - */ - open?: boolean; - /** - * @deprecated injected by Layout but not used by this AppBar - */ - title?: string | JSX.Element; toolbar?: JSX.Element; userMenu?: JSX.Element | boolean; } diff --git a/packages/ra-ui-materialui/src/layout/CheckForApplicationUpdate.tsx b/packages/ra-ui-materialui/src/layout/CheckForApplicationUpdate.tsx index 0bcf10ad55b..600f209ee5d 100644 --- a/packages/ra-ui-materialui/src/layout/CheckForApplicationUpdate.tsx +++ b/packages/ra-ui-materialui/src/layout/CheckForApplicationUpdate.tsx @@ -20,8 +20,8 @@ import { ApplicationUpdatedNotification } from './ApplicationUpdatedNotification * @example <caption>Basic usage</caption> * import { Admin, Resource, Layout, CheckForApplicationUpdate, ListGuesser } from 'react-admin'; * - * const MyLayout = ({ children, ...props }) => ( - * <Layout {...props}> + * const MyLayout = ({ children }) => ( + * <Layout> * {children} * <CheckForApplicationUpdate /> * </Layout> @@ -55,8 +55,8 @@ import { ApplicationUpdatedNotification } from './ApplicationUpdatedNotification * </Alert> * )); * - * const MyLayout = ({ children, ...props }) => ( - * <Layout {...props}> + * const MyLayout = ({ children }) => ( + * <Layout> * {children} * <CheckForApplicationUpdate notification={<CustomAppUpdatedNotification />} /> * </Layout> diff --git a/packages/ra-ui-materialui/src/layout/Error.tsx b/packages/ra-ui-materialui/src/layout/Error.tsx index 3dd7134a48d..d9a8a489136 100644 --- a/packages/ra-ui-materialui/src/layout/Error.tsx +++ b/packages/ra-ui-materialui/src/layout/Error.tsx @@ -13,7 +13,9 @@ import { import ErrorIcon from '@mui/icons-material/Report'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import History from '@mui/icons-material/History'; -import { TitleComponent, useTranslate } from 'ra-core'; +import { useTranslate, useDefaultTitle } from 'ra-core'; +import type { TitleComponent } from 'ra-core'; + import { Title, TitlePropType } from './Title'; import { useResetErrorBoundaryOnLocationChange } from './useResetErrorBoundaryOnLocationChange'; @@ -28,11 +30,11 @@ export const Error = ( errorInfo, resetErrorBoundary, className, - title, ...rest } = props; const translate = useTranslate(); + const title = useDefaultTitle(); useResetErrorBoundaryOnLocationChange(resetErrorBoundary); if (ErrorComponent) { @@ -129,9 +131,9 @@ Error.propTypes = { interface InternalErrorProps extends Omit<HtmlHTMLAttributes<HTMLDivElement>, 'title'>, - FallbackProps, - ErrorProps { + FallbackProps { className?: string; + errorInfo?: ErrorInfo; } export interface ErrorProps extends Pick<FallbackProps, 'error'> { diff --git a/packages/ra-ui-materialui/src/layout/Layout.stories.tsx b/packages/ra-ui-materialui/src/layout/Layout.stories.tsx index 530349be3ce..fa516b0047f 100644 --- a/packages/ra-ui-materialui/src/layout/Layout.stories.tsx +++ b/packages/ra-ui-materialui/src/layout/Layout.stories.tsx @@ -50,7 +50,7 @@ const Content = () => ( const Wrapper = ({ children = <Content />, theme = createTheme(defaultTheme), - layout: LayoutProp = Layout, + layout: LayoutComponent = Layout, }) => ( <MemoryRouter> <QueryClientProvider client={new QueryClient()}> @@ -58,10 +58,10 @@ const Wrapper = ({ <StoreContextProvider value={memoryStore()}> <PreferencesEditorContextProvider> <AuthContext.Provider value={undefined as any}> - <LayoutProp> + <LayoutComponent> {children} <Title title="React Admin" /> - </LayoutProp> + </LayoutComponent> </AuthContext.Provider> </PreferencesEditorContextProvider> </StoreContextProvider> @@ -93,8 +93,12 @@ const Menu = () => ( </MenuList> ); -const BasicLayout = props => <Layout menu={Menu} {...props} />; +const BasicLayout = ({ children }) => <Layout menu={Menu}>{children}</Layout>; export const Basic = () => <Wrapper layout={BasicLayout} />; -const AppBarAlwaysOnLayout = props => <BasicLayout appBarAlwaysOn {...props} />; +const AppBarAlwaysOnLayout = ({ children }) => ( + <Layout appBarAlwaysOn menu={Menu}> + {children} + </Layout> +); export const AppBarAlwaysOn = () => <Wrapper layout={AppBarAlwaysOnLayout} />; diff --git a/packages/ra-ui-materialui/src/layout/Layout.tsx b/packages/ra-ui-materialui/src/layout/Layout.tsx index b61f78ce019..1477a217d57 100644 --- a/packages/ra-ui-materialui/src/layout/Layout.tsx +++ b/packages/ra-ui-materialui/src/layout/Layout.tsx @@ -1,21 +1,13 @@ -import React, { - ComponentType, - ErrorInfo, - HtmlHTMLAttributes, - Suspense, - useState, -} from 'react'; +import React, { ComponentType, ErrorInfo, Suspense, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import clsx from 'clsx'; import { styled, SxProps } from '@mui/material/styles'; -import { CoreLayoutProps } from 'ra-core'; import { AppBar as DefaultAppBar, AppBarProps } from './AppBar'; import { Sidebar as DefaultSidebar, SidebarProps } from './Sidebar'; import { Menu as DefaultMenu, MenuProps } from './Menu'; import { Error, ErrorProps } from './Error'; import { SkipNavigationButton } from '../button'; -import { useSidebarState } from './useSidebarState'; import { Inspector } from '../preferences'; import { Loading } from './Loading'; @@ -25,15 +17,12 @@ export const Layout = (props: LayoutProps) => { appBarAlwaysOn, children, className, - dashboard, error: errorComponent, menu: Menu = DefaultMenu, sidebar: Sidebar = DefaultSidebar, - title, ...rest } = props; - const [open] = useSidebarState(); const [errorInfo, setErrorInfo] = useState<ErrorInfo>(null); const handleError = (error: Error, info: ErrorInfo) => { @@ -44,10 +33,10 @@ export const Layout = (props: LayoutProps) => { <Core className={clsx('layout', className)} {...rest}> <SkipNavigationButton /> <div className={LayoutClasses.appFrame}> - <AppBar open={open} title={title} alwaysOn={appBarAlwaysOn} /> + <AppBar alwaysOn={appBarAlwaysOn} /> <main className={LayoutClasses.contentWithSidebar}> <Sidebar appBarAlwaysOn={appBarAlwaysOn}> - <Menu hasDashboard={!!dashboard} /> + <Menu /> </Sidebar> <div id="main-content" className={LayoutClasses.content}> <ErrorBoundary @@ -58,7 +47,6 @@ export const Layout = (props: LayoutProps) => { errorComponent={errorComponent} errorInfo={errorInfo} resetErrorBoundary={resetErrorBoundary} - title={title} /> )} > @@ -74,12 +62,11 @@ export const Layout = (props: LayoutProps) => { ); }; -export interface LayoutProps - extends CoreLayoutProps, - Omit<HtmlHTMLAttributes<HTMLDivElement>, 'title'> { +export interface LayoutProps { appBar?: ComponentType<AppBarProps>; appBarAlwaysOn?: boolean; className?: string; + children: React.ReactNode; error?: ComponentType<ErrorProps>; menu?: ComponentType<MenuProps>; sidebar?: ComponentType<SidebarProps>; diff --git a/packages/ra-ui-materialui/src/layout/Menu.stories.tsx b/packages/ra-ui-materialui/src/layout/Menu.stories.tsx index 225e681491a..5210b2b7707 100644 --- a/packages/ra-ui-materialui/src/layout/Menu.stories.tsx +++ b/packages/ra-ui-materialui/src/layout/Menu.stories.tsx @@ -38,7 +38,9 @@ const DemoList = ({ name }) => ( export const Default = () => { const MenuDefault = () => <Menu hasDashboard={true} dense={false} />; - const DefaultLayout = props => <Layout {...props} menu={MenuDefault} />; + const DefaultLayout = ({ children }) => ( + <Layout menu={MenuDefault}>{children}</Layout> + ); return ( <Admin @@ -59,7 +61,9 @@ export const Default = () => { export const Dense = () => { const MenuDense = () => <Menu hasDashboard={true} dense={true} />; - const LayoutDense = props => <Layout {...props} menu={MenuDense} />; + const LayoutDense = ({ children }) => ( + <Layout menu={MenuDense}>{children}</Layout> + ); return ( <Admin @@ -103,7 +107,9 @@ export const Custom = () => { /> </Menu> ); - const CustomLayout = props => <Layout {...props} menu={CustomMenu} />; + const CustomLayout = ({ children }) => ( + <Layout menu={CustomMenu}>{children}</Layout> + ); return ( <MemoryRouter initialEntries={['/']}> @@ -192,7 +198,9 @@ export const MenuItemChild = () => { </Menu> ); }; - const CustomLayout = props => <Layout {...props} menu={CustomMenu} />; + const CustomLayout = ({ children }) => ( + <Layout menu={CustomMenu}>{children}</Layout> + ); return ( <MemoryRouter initialEntries={['/']}> diff --git a/packages/ra-ui-materialui/src/layout/Menu.tsx b/packages/ra-ui-materialui/src/layout/Menu.tsx index ef71c122403..682e4bc0e1c 100644 --- a/packages/ra-ui-materialui/src/layout/Menu.tsx +++ b/packages/ra-ui-materialui/src/layout/Menu.tsx @@ -12,6 +12,7 @@ import { DashboardMenuItem } from './DashboardMenuItem'; import { MenuItemLink } from './MenuItemLink'; import { ResourceMenuItem } from './ResourceMenuItem'; import { ResourceMenuItems } from './ResourceMenuItems'; +import { useHasDashboard } from 'ra-core'; /** * Renders a menu with one menu item per resource by default. You can also set menu items by hand. @@ -36,21 +37,8 @@ import { ResourceMenuItems } from './ResourceMenuItems'; * ); */ export const Menu = (props: MenuProps) => { - const { - hasDashboard, - children = hasDashboard ? ( - [ - <DashboardMenuItem key="default-dashboard-menu-item" />, - <ResourceMenuItems key="default-resource-menu-items" />, - ] - ) : ( - <ResourceMenuItems /> - ), - - className, - ...rest - } = props; - + const { children, className, ...rest } = props; + const hasDashboard = useHasDashboard(); const [open] = useSidebarState(); return ( @@ -64,7 +52,8 @@ export const Menu = (props: MenuProps) => { )} {...rest} > - {children} + {hasDashboard && !children && <DashboardMenuItem />} + {children ?? <ResourceMenuItems />} </Root> ); }; @@ -74,14 +63,12 @@ export interface MenuProps { children?: ReactNode; className?: string; dense?: boolean; - hasDashboard?: boolean; [key: string]: any; } Menu.propTypes = { className: PropTypes.string, dense: PropTypes.bool, - hasDashboard: PropTypes.bool, }; // re-export MenuItem components for convenience diff --git a/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx b/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx index 0898bb6a101..20c4b7f479d 100644 --- a/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx +++ b/packages/ra-ui-materialui/src/layout/MenuItemLink.tsx @@ -53,7 +53,11 @@ import { useTranslate, useBasename } from 'ra-core'; * import { Layout } from 'react-admin'; * import { Menu } from './Menu'; * - * export const Layout = (props) => <Layout {...props} menu={Menu} />; + * export const Layout = ({ children }) => ( + * <Layout menu={Menu}> + * {children} + * </Layout> + * ); * * // then, use this layout in the <Admin layout> prop: * // in src/App.js diff --git a/packages/ra-ui-materialui/src/layout/NotFound.tsx b/packages/ra-ui-materialui/src/layout/NotFound.tsx index 2e777e5aad4..34c6da1e40f 100644 --- a/packages/ra-ui-materialui/src/layout/NotFound.tsx +++ b/packages/ra-ui-materialui/src/layout/NotFound.tsx @@ -5,14 +5,15 @@ import Button from '@mui/material/Button'; import HotTub from '@mui/icons-material/HotTub'; import History from '@mui/icons-material/History'; -import { useAuthenticated, useTranslate } from 'ra-core'; +import { useAuthenticated, useDefaultTitle, useTranslate } from 'ra-core'; import { Title } from './Title'; export const NotFound = props => { - const { className, title, ...rest } = props; + const { className, ...rest } = props; const translate = useTranslate(); useAuthenticated(); + const title = useDefaultTitle(); return ( <Root className={className} {...sanitizeRestProps(rest)}> <Title defaultTitle={title} /> diff --git a/packages/ra-ui-materialui/src/layout/Title.tsx b/packages/ra-ui-materialui/src/layout/Title.tsx index caa4673301a..5366c8042dc 100644 --- a/packages/ra-ui-materialui/src/layout/Title.tsx +++ b/packages/ra-ui-materialui/src/layout/Title.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { ReactElement } from 'react'; import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; -import { RaRecord, warning } from 'ra-core'; +import { RaRecord, TitleComponent, warning } from 'ra-core'; import { PageTitleConfigurable } from './PageTitleConfigurable'; @@ -57,7 +57,7 @@ Title.propTypes = { export interface TitleProps { className?: string; - defaultTitle?: string; + defaultTitle?: TitleComponent; record?: Partial<RaRecord>; title?: string | ReactElement; preferenceKey?: string; diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx index dfe7a41f520..4e90537de4c 100644 --- a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx +++ b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx @@ -338,9 +338,9 @@ export const Title = () => ( </Admin> ); -const LayoutWithFooter = props => ( +const LayoutWithFooter = ({ children }) => ( <> - <Layout {...props} /> + <Layout>{children}</Layout> <div style={{ height: '100px', backgroundColor: 'red' }}>Footer</div> </> ); diff --git a/packages/react-admin/src/Admin.tsx b/packages/react-admin/src/Admin.tsx index a40cddb86f7..49e454978c4 100644 --- a/packages/react-admin/src/Admin.tsx +++ b/packages/react-admin/src/Admin.tsx @@ -108,7 +108,6 @@ export const Admin = (props: AdminProps) => { loading, loginPage, authCallbackPage, - menu, // deprecated, use a custom layout instead notification, queryClient, requireAuth, @@ -145,7 +144,6 @@ export const Admin = (props: AdminProps) => { layout={layout} dashboard={dashboard} disableTelemetry={disableTelemetry} - menu={menu} catchAll={catchAll} title={title} loading={loading}