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 @@
+
+
+
+
+
+
+
+
+ with-config
+
+
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/with-config/package.json b/examples/with-config/package.json
new file mode 100644
index 00000000000..06bf10a48c1
--- /dev/null
+++ b/examples/with-config/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "with-config",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "serve": "vite preview",
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "ra-data-fakerest": "^4.9.2",
+ "react": "^18.2.0",
+ "react-admin": "^4.9.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^18.16.1",
+ "@types/react": "^18.0.22",
+ "@types/react-dom": "^18.0.7",
+ "@vitejs/plugin-react": "^2.2.0",
+ "typescript": "^4.6.4",
+ "vite": "^3.2.0"
+ }
+}
diff --git a/examples/with-config/public/favicon.ico b/examples/with-config/public/favicon.ico
new file mode 100644
index 00000000000..f9222554e6c
Binary files /dev/null and b/examples/with-config/public/favicon.ico differ
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,/9j/4AAQSkZJRgABAQEASABIAAD/4gKgSUNDX1BST0ZJTEUAAQEAAAKQbGNtcwQwAABtbnRyUkdCIFhZWiAH3wAIABMAEgAWADFhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAAADhjcHJ0AAABQAAAAE53dHB0AAABkAAAABRjaGFkAAABpAAAACxyWFlaAAAB0AAAABRiWFlaAAAB5AAAABRnWFlaAAAB+AAAABRyVFJDAAACDAAAACBnVFJDAAACLAAAACBiVFJDAAACTAAAACBjaHJtAAACbAAAACRtbHVjAAAAAAAAAAEAAAAMZW5VUwAAABwAAAAcAHMAUgBHAEIAIABiAHUAaQBsAHQALQBpAG4AAG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAMgAAABwATgBvACAAYwBvAHAAeQByAGkAZwBoAHQALAAgAHUAcwBlACAAZgByAGUAZQBsAHkAAAAAWFlaIAAAAAAAAPbWAAEAAAAA0y1zZjMyAAAAAAABDEoAAAXj///zKgAAB5sAAP2H///7ov///aMAAAPYAADAlFhZWiAAAAAAAABvlAAAOO4AAAOQWFlaIAAAAAAAACSdAAAPgwAAtr5YWVogAAAAAAAAYqUAALeQAAAY3nBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACltwYXJhAAAAAAADAAAAAmZmAADypwAADVkAABPQAAAKW2Nocm0AAAAAAAMAAAAAo9cAAFR7AABMzQAAmZoAACZmAAAPXP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAkLicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAgAMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAABAAUGBwIDBAj/xAA2EAABAwMBBgMHAwQDAQAAAAABAAIDBAUREgYTITFBUSJxgRQyYZGxwdEHQqEkUmLhFRYjkv/EABoBAAIDAQEAAAAAAAAAAAAAAAIDAAEEBQb/xAAkEQACAgIDAAEEAwAAAAAAAAAAAQIRAyEEEjETBSJRcTJBYf/aAAwDAQACEQMRAD8AuIIoIogAooIqygorB72xsc97g1rRkk9FFrlfnVD3Rwu0Qj5uQyko+hRi5eEjlr6aEkOlBI6N4rSbpCG6j4R0zzPoFEIpZpn+Ahv+TuJ9E9Qtipodc0haD15ud5BI+Vsb8aXoq/a6GgJ1UsrgBknTj6rgp/1JtEkgjnbNTk9Xxkt/+m5Cbrvbam9sc2XeU9G0jTFry6Q9M/hddk/T6kpId7UwNmqJCOBOGxjoD3KLvIroiX0VxprhCJaeVr2njlpyutR+k2dlttQZ4KwMZpwIGjwD5lbI785leaSppZWHmJG+JpHfKYppgOLQ9pIAhwBHIpIwBJFJBQhgiEAiFRYQkgmzaCuNDaZHsOJHeBvmVG6VkSt0Me0l93spoqY5Y0+N3Qn8JgjdrcA3xOPU9f8AS485Jy4c8ucep/CzirNBIh4D9zz1WGU7dmyMElRIIXspeBw+fGeJ4N8/wtsEstTUtHFzz1Pb7BNVHvJQA0EAnOo83fH/AGpBR7qniw14yfekz9+qFMNxHiljYwjhqc3kTxPxwnaIZA+6ZoJWDGhjnZ68gnaEvewftHw4BOixTVGc2GsOdI4c3KFXmSuq75Rw0kQ3EWZKieQaQG8g0D4n7KauaGgiNu8eeJJPBN89JLUN8btWg5HDAL++OwUZSMbTWxV9vZNFqxyIcMEELuTdTBtNUbiP3A0D5BOC0QdozzVMWUksoIwTBFBIKEMlFdtJSIKaMHHEuKlKg+3Ly6rgizhugl3wHVLyv7WMxK5Ih8k2+IAJEecNGeLvj5LvoossDsZHl9E2x6ZZNTvDGPp2WFde45P6WlqI4tPAZIGT6rAb0vwSiOox4RgA+84nn6ruiuAhLdA1n+5x5Kt6SsrYagtqqkyuJ8Jxj+FLpIql9qM0b9GW8Ceioaoa2TigrXStDsMb8SOKeYtUgGpxLe5+wVQ2eouftIabs52T7ugHPqrOtNNX7gb+r3zSPE1zNJPljknQaehGSFDo6piax5Em7iZ78h+gXJV1OId7lzIgzIYOePyue4QvxGGs8OsAN6D/ACPfH1QinirgZmODoi4Mb5BFYlqjfSsc58kjxgkN4dua61wWWqFbb2VGCDM4uAPMN6LsJ4p+LwRlM8pZWGUU0UBFYhFUWFV/t0S67xR8muhGT8MqwFDNvKJz4IqxgyWjQ49uyXl3EbidSKxrp31VWaKnJaxoySO6aX7INZG9swlcHuD3OyMkjlxKdLWA2uqJH/3NH8KRVNWz2XJ7LCm1tHSUIyWyL0NvdC6CnBeQHjRqOSB2VwVdnd/1YwRt/wDQx8Pkq4sX9XdoXubpZrGCeo7q7i3eW0OjbqLW8B3RRjdkm+tJHn6usdbV1JY6rnpmBww5jT4QPLn6q09jLdcKBsRgv0lZSFgaaaoYXBuBza4kuB7g5HwCa6y50tRWvi3ZjeHYc1wwQVMdm42NaC3lhXBu6JkikuzHS4QuNuqCPf3biPPCqj9OdonubHa6txLyXkPJ65JH1x6K27tKYrXVPAyRE/A7nBVE7OUEtJtDSs0k6JwHO8zghMemZ6bjZcdqjZT0jo2DAY4tC68rVBHuosY4klx81sWnGqiYcjuRkllAIhGCBFBFUWFc9dSR11FLTyDLXtI8l0BJUXZRt4tNRZ7hPFI3Trw5p6HHBcEk0jgwO93l6q3tsrJ/y1oL4WZqYTqZ8R1H0VRVEDKqikgkaQRkdiCsOWHWR0+Pk7RFRzVNHXQmNwc1pHAHBVq2m+19QIzTgMhaMFsjclx+ap2wW6gn3dNXvqIpA7G+DstcB9CrVtVqsVDa4Zpa2eUmPIDS4knIzgD4FUou9Dvtqpe/oZ9qrfUR1Elw3ZDy7UeGAVKtibgKi3skzwPDj0KiV7ornc6xk8EtdTW+QhraSZ+S49SW8cAeamWzlsFupBG39ztXkotSKl/CmO21N3pbLs7U3GtL/Z4tOsMGScuAwB6qK7JUrbs1t6ex+5kOuESBoceJ4uDeGfJP+1NpO0NLSWyRgNE6cS1Rzza3iGjzOPknKCnipadkEEbY4o2hrWtGAAFojj7O2YJ5eq6oyKCywlhaTIBEJJKEMQigEVRYUViioQJAIIPVVrtzYW0FY2507cQVLtMoH7X9/X6hWVlcl0pKWvtlRTVuPZ3sOs5xpxx1A9COaDJDtGhuKbhKyjW2uT2newSuZq544g+in2ytGYpGzTPMj28WgNAwofRV0cFU6F7w5oOGudw1DoVPbRdKCBrdU0eojg1pyT6BYbr+zsd5dKRIvZN6/ey4yPhyXRTx+PDeXfssaZz6wBzssj6N6nzTiyJrAABgI1vaMrdaZp06SQTniktkvv5WtbIO4o5+RVJgwkikjAAkigVCGsJZWuSaOGMySvaxg5uccAJiqtqqeNxbSxOmP9x8I/KCU4x9DjCUvESIJuvG0Fp2fp2zXWvhpWPzo3h4uxzwBxKi1XtTcJGnS5sDe7Bx+ZVD7WX+p2gvs1TNM+VkZ3cOp2fCD9+amOayPReTG4LZaF2/XVjKmRlotTZIG5DZal5Bce+kch5lcLNqL5erSJrhXPd7SNRiYA2No6AAKocHGOqs21NJs1K3HERgfwg5T6xSQzixTk2zAtEziCn6wwbmoa4DHHoE1tgO9BA9FJLfTua0ODeK5sjpw0WFaagua0Ek+akAcC3KiNnbI0AkKTsfiLJTsb0IyrZue3U3yULuW39stV+fbKlkmmMAPmZ4sPPTCeNoL9HZbPUVbiCWN8I7novPE9TLVVktTM4ukkLnuJ6kldPg4fkk2/DncyfSKr09C2zaizXZzI6SvidM/lE7wv8AkU7rznsdUvG1Nvbk5FWz6r0O2XuPkj5EYYpJJ+isPfIm6NqCQcDySSk0/A2mvStrpepLtWu0kimYcRt+5+K1sZlqa6LmMp13ga1cuUnJ2zrQioqkM21VULfs7VzA4foLWeZ4fdUnjAHmrI/Uiv8A6OmpQffeXkfAD8lVyBlkY75K6XDhWO/yc7ly++vwGMZljGM5I+qvWlpab2eMNiDQGjgAqNb4J2EftwR6cV6Et0Daihp5WjwyRtcPUIebGkhnCabZjTWykmOHsHDkU7U9BHF7ucLXFTljuSc4W8srnUb26OmmeI2Dgt7qzIw44C1BgwmLaq7w2a0S1Dj4sYYM8S7oEyKb0hcmvWRD9S9pI6qaCz0rsiM7ydw79B6c/kq/Mni7cM/RYmd9XUyVEpJe4l7s9StD3nWT105+y9NxsXw4lE8/nyfLkch32Lk07X21zjw3+r5L0HHUa25yvPWxzc7YW9nYk/wVdrKgsAHRcn6lKpx/R0+BG4N/6P8AFL4hxTg2IvZkc1HqSoy8EqUUUgfGFjxt3o0ZUq2f/9k="
+ },
+ {
+ "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.tsx b/packages/ra-core/src/core/CoreAdminRoutes.tsx
index 8624c575c63..d6a0b01059f 100644
--- a/packages/ra-core/src/core/CoreAdminRoutes.tsx
+++ b/packages/ra-core/src/core/CoreAdminRoutes.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import { useState, useEffect, Children, ComponentType } from 'react';
-import { Navigate, Route, Routes } from 'react-router-dom';
+import { Navigate, Route, RouteObject, Routes } from 'react-router-dom';
import { WithPermissions, useCheckAuth } from '../auth';
import { useTimeout } from '../util';
@@ -11,8 +11,12 @@ import {
LayoutComponent,
LoadingComponent,
CoreLayoutProps,
+ ResourceConfig,
+ GetResourcesFunction,
+ GetRoutesFunction,
} from '../types';
import { useConfigureAdminRouterFromChildren } from './useConfigureAdminRouterFromChildren';
+import { useConfigureAdminRouterFromProps } from './useConfigureAdminRouterFromProps';
export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => {
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 ||
+ props.customRoutes ||
+ props.customRoutesWithoutLayout
+ ) {
+ return element;
+ }
+
if (status === 'empty') {
return ;
}
@@ -130,6 +143,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}
diff --git a/packages/ra-core/src/core/Resource.tsx b/packages/ra-core/src/core/Resource.tsx
index 3aae32cb33e..5f2cd3441fd 100644
--- a/packages/ra-core/src/core/Resource.tsx
+++ b/packages/ra-core/src/core/Resource.tsx
@@ -1,42 +1,59 @@
import * as React from 'react';
import { isValidElement } from 'react';
-import { Route, Routes } from 'react-router-dom';
+import { 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 {
+ create: Create,
+ edit: Edit,
+ list: List,
+ name,
+ show: Show,
+ routes,
+ } = props;
+
+ const resourceRoutes = React.useMemo(() => {
+ const allRoutes = [...routes];
+
+ if (Create) {
+ allRoutes.push({
+ path: 'create/*',
+ element: isValidElement(Create) ? Create : ,
+ });
+ }
+
+ 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;
+ }, [routes, Create, Edit, List, Show]);
+
+ const element = useRoutes(resourceRoutes);
return (
-
- {Create && (
- }
- />
- )}
- {Show && (
- }
- />
- )}
- {Edit && (
- }
- />
- )}
- {List && (
- }
- />
- )}
- {props.children}
-
+ {element}
);
};
diff --git a/packages/ra-core/src/core/useConfigureAdminRouterFromProps.tsx b/packages/ra-core/src/core/useConfigureAdminRouterFromProps.tsx
new file mode 100644
index 00000000000..c2e29c5a34b
--- /dev/null
+++ b/packages/ra-core/src/core/useConfigureAdminRouterFromProps.tsx
@@ -0,0 +1,199 @@
+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',
+ 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',
+ 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 ||
+ options.customRoutes ||
+ options.customRoutesWithoutLayout
+ ) {
+ if (isLoading && oneSecondHasPassed) {
+ return [
+ ...customRoutesWithoutLayoutConfig,
+ { path: '/', element: },
+ ];
+ }
+
+ 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 eab3f64bf21..20ca8fc5ddc 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
@@ -297,6 +298,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
) =>
@@ -356,6 +376,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 0d79064caa6..19443ad03b1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6254,6 +6254,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"
@@ -18938,7 +18945,7 @@ __metadata:
languageName: unknown
linkType: soft
-"ra-data-fakerest@^4.10.2, ra-data-fakerest@workspace:packages/ra-data-fakerest":
+"ra-data-fakerest@^4.10.2, 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:
@@ -19355,7 +19362,7 @@ __metadata:
languageName: unknown
linkType: soft
-"react-admin@^4.10.2, react-admin@workspace:packages/react-admin":
+"react-admin@^4.10.2, 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:
@@ -23390,6 +23397,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"