From f5b15aa4aefd1b749d86fc940ba2554bc0a796fb Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 24 May 2023 17:49:50 +0200 Subject: [PATCH] Add support for admin configuration through props instead of elements --- examples/with-config/README.md | 50 +++++ examples/with-config/index.html | 125 +++++++++++ examples/with-config/package.json | 24 +++ examples/with-config/public/favicon.ico | Bin 0 -> 15086 bytes examples/with-config/public/manifest.json | 15 ++ examples/with-config/src/App.tsx | 58 +++++ examples/with-config/src/README.md | 8 + examples/with-config/src/authProvider.ts | 41 ++++ examples/with-config/src/data.json | 121 +++++++++++ examples/with-config/src/dataProvider.ts | 4 + examples/with-config/src/help.txt | 3 + examples/with-config/src/index.tsx | 9 + examples/with-config/src/users.json | 18 ++ examples/with-config/src/vite-env.d.ts | 1 + examples/with-config/tsconfig.json | 26 +++ examples/with-config/vite.config.ts | 42 ++++ packages/ra-core/src/core/CoreAdmin.tsx | 6 + .../ra-core/src/core/CoreAdminRoutes.spec.tsx | 5 - packages/ra-core/src/core/CoreAdminRoutes.tsx | 22 +- packages/ra-core/src/core/CoreAdminUI.tsx | 14 +- packages/ra-core/src/core/Resource.tsx | 121 ++++++++--- .../core/useConfigureAdminRouterFromProps.tsx | 200 ++++++++++++++++++ packages/ra-core/src/types.ts | 21 ++ packages/react-admin/src/Admin.tsx | 6 + yarn.lock | 28 ++- 25 files changed, 925 insertions(+), 43 deletions(-) create mode 100644 examples/with-config/README.md create mode 100644 examples/with-config/index.html create mode 100644 examples/with-config/package.json create mode 100644 examples/with-config/public/favicon.ico create mode 100644 examples/with-config/public/manifest.json create mode 100644 examples/with-config/src/App.tsx create mode 100644 examples/with-config/src/README.md create mode 100644 examples/with-config/src/authProvider.ts create mode 100644 examples/with-config/src/data.json create mode 100644 examples/with-config/src/dataProvider.ts create mode 100644 examples/with-config/src/help.txt create mode 100644 examples/with-config/src/index.tsx create mode 100644 examples/with-config/src/users.json create mode 100644 examples/with-config/src/vite-env.d.ts create mode 100644 examples/with-config/tsconfig.json create mode 100644 examples/with-config/vite.config.ts create mode 100644 packages/ra-core/src/core/useConfigureAdminRouterFromProps.tsx diff --git a/examples/with-config/README.md b/examples/with-config/README.md new file mode 100644 index 00000000000..6231eacdcfa --- /dev/null +++ b/examples/with-config/README.md @@ -0,0 +1,50 @@ +# with-config + +## Installation + +Install the application dependencies by running: + +```sh +npm install +# or +yarn install +``` + +## Development + +Start the application in development mode by running: + +```sh +npm run dev +# or +yarn dev +``` + +## Production + +Build the application in production mode by running: + +```sh +npm run build +# or +yarn build +``` + +## DataProvider + +The included data provider use [FakeREST](https://github.com/marmelab/fakerest) to simulate a backend. +You'll find a `data.json` file in the `src` directory that includes some fake data for testing purposes. + +It includes two resources, posts and comments. +Posts have the following properties: `id`, `title` and `content`. +Comments have the following properties: `id`, `post_id` and `content`. + +## Authentication + +The included auth provider should only be used for development and test purposes. +You'll find a `users.json` file in the `src` directory that includes the users you can use. + +You can sign in to the application with the following usernames and password: +- janedoe / password +- johndoe / password + diff --git a/examples/with-config/index.html b/examples/with-config/index.html new file mode 100644 index 00000000000..26018ebc157 --- /dev/null +++ b/examples/with-config/index.html @@ -0,0 +1,125 @@ + + +
+ + + + + +aw`p#1NSF%0Ax%$@CD|GwnRkHx!0r3LZ zuP&c`YNR*PO0Qu*OXx@6R%{Qu*Z7>ee^EUd_?%_Gio}N;S;v@+Tk%+iw4~+PQu?6_ zlwL^bo}AuD3$3R5SW3UCG2Z06zc2m9`u|Px*&Ob_FaOzoW#Rm3R?lB&_4)yQ`=5n= zqkbbx*U#wJ0=@g4`5ZuO&RE{5>?F)?&x#|{eJ zvV~*W84riIZ>QKhw;8p}IsC-#-E#g#P7$$`j0E6lT%<6828I9F?>iG>vwVhxubI){ znkTpL*$6-5Hs@cY*L%zj{KY!I!=E63+WPD9KczE=k`cpok@tymq ^1py{j?GGy;M{C!YRQ0mA7@8`y#`MQc{GqUR`P+Eq|n26{Ki<_xwhvq$KR2o zQ{PjIA9?_BUC5FGJo|R*CVPD*=l6gE_)%8?9Vc-vv}gQyH)TM7Su@qvlKGp`Kfs4t zBE)T>hcF)GeP9E3Ui =eV`vKgNc #$S$_|^Ort+*?^>Ygh6Ja1gL zu3Vw>$B$D4=UkC@jXZUd?%ld2a}V9J*(e9Ba3pt62|su>tXWe+4cP9Wb+l*wdWN7x zST8wf)WV_e$JG3i!;Nn#;YS;+W4)ZV#=!f_iSx)xj#I*A>U(PNOFyigDPa0cjhotc zsmA}Bc+An0EPh^le9vewJb%3D^Vb_bf4 ^(vvUrw;6oX;Q4Hrh8Je zN1&&0)(5@lj~qMR>F2>Pj(Wx&_5>j9_zpT%fBZ&W+K?uV#h$_^K|K_Fa>W0U+nmN{ z57e!}A87Yc1!}}|Q2kM-J+Ps>)WX6y0UkeZZ}B5ASLl1x31B?bGmPV$FzQGQ`)}bo zPkU=?a<;XVnyf({4cQhI7aP&GLV59fR ? e0CzV!eJGUvJ8Y<&6Om6xZ3FbR{`zBWAR9^6A7|^5#o7PtD(j@y3O)+j8tQ!G f{zYZYai-D-WlzywAN1w-b@$Kf?q5TFVCnN;r_I_P literal 0 HcmV?d00001 diff --git a/examples/with-config/public/manifest.json b/examples/with-config/public/manifest.json new file mode 100644 index 00000000000..0b29eca21ce --- /dev/null +++ b/examples/with-config/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "with-config", + "name": "{{name}}", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/with-config/src/App.tsx b/examples/with-config/src/App.tsx new file mode 100644 index 00000000000..7eb96a1c0a4 --- /dev/null +++ b/examples/with-config/src/App.tsx @@ -0,0 +1,58 @@ +import { + Admin, + ListGuesser, + EditGuesser, + ShowGuesser, + CardContentInner, + Button, +} from 'react-admin'; +import { Link } from 'react-router-dom'; +import { dataProvider } from './dataProvider'; +import { authProvider } from './authProvider'; + +export const App = () => ( + ({ + posts: { + edit: EditGuesser, + list: ListGuesser, + routes: [ + { + path: 'all/*', + element: , + }, + { + path: '*', + element: ( + + Posts Dashboard + + ), + }, + ], + }, + comments: { + list: ListGuesser, + edit: EditGuesser, + show: ShowGuesser, + }, + })} + customRoutes={(permissions: any) => [ + { path: 'custom', element:
+ +Custom route}, + ]} + customRoutesWithoutLayout={[ + { + path: 'custom-no-layout', + element:Custom route no layout, + }, + ]} + /> +); diff --git a/examples/with-config/src/README.md b/examples/with-config/src/README.md new file mode 100644 index 00000000000..4002368cc6c --- /dev/null +++ b/examples/with-config/src/README.md @@ -0,0 +1,8 @@ +## Authentication + +The included auth provider should only be used for development and test purposes. +You'll find a `users.json` file in the `src` directory that includes the users you can use. + +You can sign in to the application with the following usernames and password: +- janedoe / password +- johndoe / password diff --git a/examples/with-config/src/authProvider.ts b/examples/with-config/src/authProvider.ts new file mode 100644 index 00000000000..9a6bea9a813 --- /dev/null +++ b/examples/with-config/src/authProvider.ts @@ -0,0 +1,41 @@ +import { AuthProvider, HttpError } from 'react-admin'; +import data from './users.json'; + +/** + * This authProvider is only for test purposes. Don't use it in production. + */ +export const authProvider: AuthProvider = { + login: ({ username, password }) => { + const user = data.users.find( + u => u.username === username && u.password === password + ); + + if (user) { + let { password, ...userToPersist } = user; + localStorage.setItem('user', JSON.stringify(userToPersist)); + return Promise.resolve(); + } + + return Promise.reject( + new HttpError('Unauthorized', 401, { + message: 'Invalid username or password', + }) + ); + }, + logout: () => { + localStorage.removeItem('user'); + return Promise.resolve(); + }, + checkError: () => Promise.resolve(), + checkAuth: () => + localStorage.getItem('user') ? Promise.resolve() : Promise.reject(), + getPermissions: () => Promise.resolve(['admin']), + getIdentity: () => { + const persistedUser = localStorage.getItem('user'); + const user = persistedUser ? JSON.parse(persistedUser) : null; + + return Promise.resolve(user); + }, +}; + +export default authProvider; diff --git a/examples/with-config/src/data.json b/examples/with-config/src/data.json new file mode 100644 index 00000000000..bfe9e63794e --- /dev/null +++ b/examples/with-config/src/data.json @@ -0,0 +1,121 @@ +{ + "posts": [ + { + "id": 0, + "title": "Post 1", + "content": "Content 1" + }, + { + "id": 1, + "title": "Post 2", + "content": "Content 2" + }, + { + "id": 2, + "title": "Post 3", + "content": "Content 3" + }, + { + "id": 3, + "title": "Post 4", + "content": "Content 4" + }, + { + "id": 4, + "title": "Post 5", + "content": "Content 5" + }, + { + "id": 5, + "title": "Post 6", + "content": "Content 6" + }, + { + "id": 6, + "title": "Post 7", + "content": "Content 7" + }, + { + "id": 7, + "title": "Post 8", + "content": "Content 8" + }, + { + "id": 8, + "title": "Post 9", + "content": "Content 9" + }, + { + "id": 9, + "title": "Post 10", + "content": "Content 10" + }, + { + "id": 10, + "title": "Post 11", + "content": "Content 11" + }, + { + "id": 11, + "title": "Post 12", + "content": "Content 12" + } + ], + "comments": [ + { + "id": 0, + "postId": 0, + "content": "Comment 1" + }, + { + "id": 1, + "postId": 0, + "content": "Comment 2" + }, + { + "id": 2, + "postId": 1, + "content": "Comment 3" + }, + { + "id": 3, + "postId": 1, + "content": "Comment 4" + }, + { + "id": 4, + "postId": 2, + "content": "Comment 5" + }, + { + "id": 5, + "postId": 2, + "content": "Comment 6" + }, + { + "id": 6, + "postId": 3, + "content": "Comment 7" + }, + { + "id": 7, + "postId": 3, + "content": "Comment 8" + }, + { + "id": 8, + "postId": 3, + "content": "Comment 9" + }, + { + "id": 9, + "postId": 4, + "content": "Comment 10" + }, + { + "id": 10, + "postId": 4, + "content": "Comment 11" + } + ] +} diff --git a/examples/with-config/src/dataProvider.ts b/examples/with-config/src/dataProvider.ts new file mode 100644 index 00000000000..cf9b9301e3f --- /dev/null +++ b/examples/with-config/src/dataProvider.ts @@ -0,0 +1,4 @@ +import fakeRestDataProvider from 'ra-data-fakerest'; +import data from './data.json'; + +export const dataProvider = fakeRestDataProvider(data, true); diff --git a/examples/with-config/src/help.txt b/examples/with-config/src/help.txt new file mode 100644 index 00000000000..19af2e8a2c1 --- /dev/null +++ b/examples/with-config/src/help.txt @@ -0,0 +1,3 @@ +You can sign in to the application with the following usernames and password: +- janedoe / password +- johndoe / password \ No newline at end of file diff --git a/examples/with-config/src/index.tsx b/examples/with-config/src/index.tsx new file mode 100644 index 00000000000..6e3c16c5696 --- /dev/null +++ b/examples/with-config/src/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ++ +); diff --git a/examples/with-config/src/users.json b/examples/with-config/src/users.json new file mode 100644 index 00000000000..779908e298e --- /dev/null +++ b/examples/with-config/src/users.json @@ -0,0 +1,18 @@ +{ + "users": [ + { + "id": 1, + "username": "janedoe", + "password": "password", + "fullName": "Jane Doe", + "avatar": "data:image/jpeg;base64,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" + }, + { + "id": 2, + "username": "johndoe", + "password": "password", + "fullName": "John Doe", + "avatar": "data:image/jpeg;base64,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" + } + ] +} diff --git a/examples/with-config/src/vite-env.d.ts b/examples/with-config/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/examples/with-config/src/vite-env.d.ts @@ -0,0 +1 @@ +///+ diff --git a/examples/with-config/tsconfig.json b/examples/with-config/tsconfig.json new file mode 100644 index 00000000000..a273b0cfc0e --- /dev/null +++ b/examples/with-config/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/examples/with-config/vite.config.ts b/examples/with-config/vite.config.ts new file mode 100644 index 00000000000..97809db145c --- /dev/null +++ b/examples/with-config/vite.config.ts @@ -0,0 +1,42 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import fs from 'fs'; + +let aliases: any[] = []; +try { + const packages = fs.readdirSync(path.resolve(__dirname, '../../packages')); + aliases = packages.map(dirName => { + const packageJson = require(path.resolve( + __dirname, + '../../packages', + dirName, + 'package.json' + )); + return { + find: new RegExp(`^${packageJson.name}$`), + replacement: path.resolve( + __dirname, + `../../packages/${packageJson.name}/src` + ), + }; + }, {}); +} catch {} + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + define: { + 'process.env': process.env, + }, + resolve: { + alias: [ + ...aliases, + { + find: /^@mui\/icons-material\/(.*)/, + replacement: '@mui/icons-material/esm/$1', + }, + ], + }, + base: './', +}); diff --git a/packages/ra-core/src/core/CoreAdmin.tsx b/packages/ra-core/src/core/CoreAdmin.tsx index 85719837906..04ea52c13c4 100644 --- a/packages/ra-core/src/core/CoreAdmin.tsx +++ b/packages/ra-core/src/core/CoreAdmin.tsx @@ -102,6 +102,9 @@ export const CoreAdmin = (props: CoreAdminProps) => { requireAuth, store, title = 'React Admin', + resources, + customRoutes, + customRoutesWithoutLayout, } = props; return ( { loginPage={loginPage} requireAuth={requireAuth} ready={ready} + resources={resources} + customRoutes={customRoutes} + customRoutesWithoutLayout={customRoutesWithoutLayout} > {children} diff --git a/packages/ra-core/src/core/CoreAdminRoutes.spec.tsx b/packages/ra-core/src/core/CoreAdminRoutes.spec.tsx index 6ecfd2bbd05..fa2e98903bc 100644 --- a/packages/ra-core/src/core/CoreAdminRoutes.spec.tsx +++ b/packages/ra-core/src/core/CoreAdminRoutes.spec.tsx @@ -16,10 +16,6 @@ const CatchAll = () => ; const Loading = () => <>Loading>; describe(' ', () => { - const defaultProps = { - customRoutes: [], - }; - describe('With resources as regular children', () => { it('should render resources and custom routes with and without layout', () => { const history = createMemoryHistory(); @@ -224,7 +220,6 @@ describe(' ', () => { history={history} > { const oneSecondHasPassed = useTimeout(1000); @@ -25,6 +29,7 @@ export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { status, resources, } = useConfigureAdminRouterFromChildren(props.children); + const element = useConfigureAdminRouterFromProps(props); const { layout: Layout, @@ -50,6 +55,14 @@ export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { } }, [checkAuth, requireAuth]); + if ( + props.resources != null || + props.customRoutes != null || + props.customRoutesWithoutLayout != null + ) { + return element; + } + if (status === 'empty') { return diff --git a/packages/ra-core/src/core/Resource.tsx b/packages/ra-core/src/core/Resource.tsx index 3aae32cb33e..45e2063f6b0 100644 --- a/packages/ra-core/src/core/Resource.tsx +++ b/packages/ra-core/src/core/Resource.tsx @@ -1,42 +1,67 @@ import * as React from 'react'; -import { isValidElement } from 'react'; -import { Route, Routes } from 'react-router-dom'; +import { Children, isValidElement } from 'react'; +import { RouteObject, useRoutes } from 'react-router-dom'; import { ResourceProps } from '../types'; import { ResourceContextProvider } from './ResourceContextProvider'; export const Resource = (props: ResourceProps) => { - const { create: Create, edit: Edit, list: List, name, show: Show } = props; + const { + children, + create: Create, + edit: Edit, + list: List, + name, + show: Show, + routes, + } = props; + + const resourceRoutes = React.useMemo(() => { + let allRoutes = []; + + if (routes != null) { + allRoutes = [...routes]; + } + + if (children != null) { + allRoutes.push(...createRoutesFromChildren(children)); + } + + if (Create) { + allRoutes.push({ + path: 'create/*', + element: isValidElement(Create) ? Create :; } @@ -119,10 +132,6 @@ export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { ); }; -CoreAdminRoutes.defaultProps = { - customRoutes: [], -}; - export interface CoreAdminRoutesProps extends CoreLayoutProps { layout: LayoutComponent; catchAll: CatchAllComponent; @@ -130,6 +139,9 @@ export interface CoreAdminRoutesProps extends CoreLayoutProps { loading: LoadingComponent; requireAuth?: boolean; ready?: ComponentType; + resources?: Record | GetResourcesFunction; + customRoutes?: RouteObject[] | GetRoutesFunction; + customRoutesWithoutLayout?: RouteObject[] | GetRoutesFunction; } const defaultAuthParams = { params: { route: 'dashboard' } }; diff --git a/packages/ra-core/src/core/CoreAdminUI.tsx b/packages/ra-core/src/core/CoreAdminUI.tsx index 8462cf9162f..1bf9cf9bc43 100644 --- a/packages/ra-core/src/core/CoreAdminUI.tsx +++ b/packages/ra-core/src/core/CoreAdminUI.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ComponentType, useEffect, isValidElement, createElement } from 'react'; -import { Routes, Route } from 'react-router-dom'; +import { Routes, Route, RouteObject } from 'react-router-dom'; import { CoreAdminRoutes } from './CoreAdminRoutes'; import { Ready } from '../util'; @@ -13,6 +13,9 @@ import { CatchAllComponent, DashboardComponent, LoadingComponent, + ResourceConfig, + GetResourcesFunction, + GetRoutesFunction, } from '../types'; export type ChildrenFunction = () => ComponentType[]; @@ -35,6 +38,9 @@ export interface CoreAdminUIProps { requireAuth?: boolean; ready?: ComponentType; title?: TitleComponent; + resources?: Record | GetResourcesFunction; + customRoutes?: RouteObject[] | GetRoutesFunction; + customRoutesWithoutLayout?: RouteObject[] | GetRoutesFunction; } export const CoreAdminUI = (props: CoreAdminUIProps) => { @@ -51,6 +57,9 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => { ready = Ready, title = 'React Admin', requireAuth = false, + resources, + customRoutes, + customRoutesWithoutLayout, } = props; useEffect(() => { @@ -92,6 +101,9 @@ export const CoreAdminUI = (props: CoreAdminUIProps) => { requireAuth={requireAuth} ready={ready} title={title} + resources={resources} + customRoutes={customRoutes} + customRoutesWithoutLayout={customRoutesWithoutLayout} > {children} , + }); + } + + if (Show) { + allRoutes.push({ + path: ':id/show/*', + element: isValidElement(Show) ? Show : , + }); + } + + if (Edit) { + allRoutes.push({ + path: ':id/*', + element: isValidElement(Edit) ? Edit : , + }); + } + + if (List) { + allRoutes.push({ + path: '/*', + element: isValidElement(List) ? List : , + }); + } + + return allRoutes; + }, [children, routes, Create, Edit, List, Show]); + const element = useRoutes(resourceRoutes); return (
- ); }; @@ -65,3 +90,39 @@ Resource.registerResource = ({ icon, recordRepresentation, }); + +function createRoutesFromChildren(children: React.ReactNode): RouteObject[] { + let routes: RouteObject[] = []; + + React.Children.forEach(children, element => { + if (!React.isValidElement(element)) { + // Ignore non-elements. This allows people to more easily inline + // conditionals in their route config. + return; + } + + if (element.type === React.Fragment) { + // Transparently support React.Fragment and its children. + routes.push.apply( + routes, + createRoutesFromChildren(element.props.children) + ); + return; + } + + let route: RouteObject = { + caseSensitive: element.props.caseSensitive, + element: element.props.element, + index: element.props.index, + path: element.props.path, + }; + + if (element.props.children) { + route.children = createRoutesFromChildren(element.props.children); + } + + routes.push(route); + }); + + return routes; +} diff --git a/packages/ra-core/src/core/useConfigureAdminRouterFromProps.tsx b/packages/ra-core/src/core/useConfigureAdminRouterFromProps.tsx new file mode 100644 index 00000000000..038a16608d0 --- /dev/null +++ b/packages/ra-core/src/core/useConfigureAdminRouterFromProps.tsx @@ -0,0 +1,200 @@ +import * as React from 'react'; +import { Navigate, Outlet, useRoutes } from 'react-router'; +import { useQuery } from 'react-query'; +import { WithPermissions, usePermissions } from '../auth'; +import { useTimeout } from '../util'; +import { CoreAdminRoutesProps } from './CoreAdminRoutes'; +import { Resource } from './Resource'; +import { defaultAuthParams } from '../auth/useAuthProvider'; +import { useCreatePath } from '../routing'; +import { useResourceDefinitionContext } from './useResourceDefinitionContext'; + +export const useConfigureAdminRouterFromProps = ( + options: CoreAdminRoutesProps +) => { + const { permissions, isLoading: isLoadingPermissions } = usePermissions(); + const { register, unregister } = useResourceDefinitionContext(); + const createPath = useCreatePath(); + const oneSecondHasPassed = useTimeout(1000); + const { + layout: Layout, + catchAll: CatchAll, + dashboard, + loading: LoadingPage, + menu, + ready: Ready, + title, + } = options; + + const { data: resourcesConfig, isLoading: isLoadingResources } = useQuery({ + queryKey: ['resourcesConfig', options.resources], + queryFn: () => { + if (typeof options.resources === 'function') { + return options.resources(permissions); + } + + return options.resources ?? []; + }, + initialData: [], + }); + + const { + data: customRoutesConfig, + isLoading: isLoadingCustomRoutes, + } = useQuery({ + queryKey: [ + 'customRoutes', + options.customRoutes != null + ? typeof options.customRoutes === 'function' + ? options.customRoutes + : options.customRoutes.map(route => route.path) + : '', + ], + queryFn: () => { + if (typeof options.customRoutes === 'function') { + return options.customRoutes(permissions); + } + + return options.customRoutes ?? []; + }, + initialData: [], + }); + + const { + data: customRoutesWithoutLayoutConfig, + isLoading: isLoadingCustomRoutesWithoutLayout, + } = useQuery({ + queryKey: [ + 'customRoutesWithoutLayout', + options.customRoutesWithoutLayout != null + ? typeof options.customRoutesWithoutLayout === 'function' + ? options.customRoutesWithoutLayout + : options.customRoutesWithoutLayout.map(route => route.path) + : '', + ], + queryFn: () => { + if (typeof options.customRoutesWithoutLayout === 'function') { + return options.customRoutesWithoutLayout(permissions); + } + + return options.customRoutesWithoutLayout ?? []; + }, + initialData: [], + }); + + React.useEffect(() => { + Object.keys(resourcesConfig).forEach(resource => { + register( + Resource.registerResource({ + name: resource, + ...resourcesConfig[resource], + }) + ); + }); + return () => { + Object.keys(resourcesConfig).forEach(resource => { + unregister({ + name: resource, + ...resourcesConfig[resource], + }); + }); + }; + }, [unregister, resourcesConfig, register]); + + const isLoading = + isLoadingPermissions || + isLoadingResources || + isLoadingCustomRoutes || + isLoadingCustomRoutesWithoutLayout; + + let routes = React.useMemo(() => { + if ( + options.resources == null && + options.customRoutes == null && + options.customRoutesWithoutLayout == null + ) { + return []; + } + if (isLoading && oneSecondHasPassed) { + return [ + ...customRoutesWithoutLayoutConfig, + { path: '/', element:- {Create && ( - + {element}} - /> - )} - {Show && ( - } - /> - )} - {Edit && ( - } - /> - )} - {List && ( - } - /> - )} - {props.children} - }, + ]; + } + + if (!isLoading) { + if ( + customRoutesConfig.length === 0 && + resourcesConfig.length === 0 + ) { + return [{ path: '/', element: }]; + } + return [ + ...customRoutesWithoutLayoutConfig, + { + path: '/*', + element: ( + + + ), + children: [ + ...customRoutesConfig, + ...Object.keys(resourcesConfig).map(resource => ({ + path: `${resource}/*`, + element: ( ++ + ), + })), + { + path: '', + element: dashboard ? ( + + ) : Object.keys(resourcesConfig).length > 0 ? ( + + ) : null, + }, + { + path: '*', + element: , + }, + ], + }, + ]; + } + + return []; + }, [ + CatchAll, + LoadingPage, + Ready, + dashboard, + isLoading, + Layout, + menu, + oneSecondHasPassed, + options.customRoutes, + options.customRoutesWithoutLayout, + options.resources, + resourcesConfig, + title, + createPath, + customRoutesConfig, + customRoutesWithoutLayoutConfig, + ]); + + return useRoutes(routes); +}; diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index 7566a2f7d50..76193a5bfc5 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -1,6 +1,7 @@ import { ReactNode, ReactElement, ComponentType } from 'react'; import { WithPermissionsChildrenParams } from './auth/WithPermissions'; import { AuthActionType } from './auth/types'; +import { RouteObject } from 'react-router'; /** * data types @@ -300,6 +301,25 @@ export type Dispatch = T extends (...args: infer A) => any : never; export type ResourceElement = ReactElement ; +export type ResourceConfig = Pick< + ResourceProps, + | 'create' + | 'edit' + | 'icon' + | 'list' + | 'options' + | 'recordRepresentation' + | 'show' + | 'routes' +>; + +export type GetResourcesFunction = ( + permissions: any +) => Record | Promise >; +export type GetRoutesFunction = ( + permissions: any +) => RouteObject[] | Promise ; + export type RenderResourcesFunction = ( permissions: any ) => @@ -359,6 +379,7 @@ export interface ResourceProps { recordRepresentation?: ReactElement | RecordToStringFunction | string; options?: ResourceOptions; children?: ReactNode; + routes?: RouteObject[]; } export type Exporter = ( diff --git a/packages/react-admin/src/Admin.tsx b/packages/react-admin/src/Admin.tsx index 32e36c59979..bc0efaeceba 100644 --- a/packages/react-admin/src/Admin.tsx +++ b/packages/react-admin/src/Admin.tsx @@ -114,6 +114,9 @@ export const Admin = (props: AdminProps) => { darkTheme, defaultTheme, title = 'React Admin', + resources, + customRoutes, + customRoutesWithoutLayout, } = props; if (loginPage === true && process.env.NODE_ENV !== 'production') { @@ -149,6 +152,9 @@ export const Admin = (props: AdminProps) => { notification={notification} requireAuth={requireAuth} ready={ready} + resources={resources} + customRoutes={customRoutes} + customRoutesWithoutLayout={customRoutesWithoutLayout} > {children} diff --git a/yarn.lock b/yarn.lock index 43152881a11..8d9cca41fc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6334,6 +6334,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.16.1": + version: 18.16.14 + resolution: "@types/node@npm:18.16.14" + checksum: 28b00ede91212971c8724c73a922d7dec70d4a3b95d576b28946b12ebe82f2ba7f3d07b2c2e1584e1149cc11ca1c93d4da1f5c48d9b541a9c0311b88d03ea43b + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -18894,7 +18901,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-fakerest@^4.10.6, ra-data-fakerest@workspace:packages/ra-data-fakerest": +"ra-data-fakerest@^4.10.6, ra-data-fakerest@^4.9.2, ra-data-fakerest@workspace:packages/ra-data-fakerest": version: 0.0.0-use.local resolution: "ra-data-fakerest@workspace:packages/ra-data-fakerest" dependencies: @@ -19311,7 +19318,7 @@ __metadata: languageName: unknown linkType: soft -"react-admin@^4.10.6, react-admin@workspace:packages/react-admin": +"react-admin@^4.10.6, react-admin@^4.9.0, react-admin@workspace:packages/react-admin": version: 0.0.0-use.local resolution: "react-admin@workspace:packages/react-admin" dependencies: @@ -23248,6 +23255,23 @@ __metadata: languageName: node linkType: hard +"with-config@workspace:examples/with-config": + version: 0.0.0-use.local + resolution: "with-config@workspace:examples/with-config" + dependencies: + "@types/node": ^18.16.1 + "@types/react": ^18.0.22 + "@types/react-dom": ^18.0.7 + "@vitejs/plugin-react": ^2.2.0 + ra-data-fakerest: ^4.9.2 + react: ^18.2.0 + react-admin: ^4.9.0 + react-dom: ^18.2.0 + typescript: ^4.6.4 + vite: ^3.2.0 + languageName: unknown + linkType: soft + "word-wrap@npm:^1.2.3, word-wrap@npm:~1.2.3": version: 1.2.3 resolution: "word-wrap@npm:1.2.3"