Compare commits

...

2 Commits

Author SHA1 Message Date
熊二 358b0b8c32 feat: ??? 3 months ago
熊二 bf1d289e8e feat: 是开始 3 months ago
  1. 1
      apps/imsfe/package.json
  2. 54
      apps/imsfe/src/App.tsx
  3. 23
      apps/imsfe/src/config/i18nProvider.ts
  4. 7
      apps/imsfe/src/pages/CatchAll.tsx
  5. 18
      apps/imsfe/src/pages/PageError.tsx
  6. 10
      apps/imsfe/src/pages/SignIn.tsx
  7. 47
      apps/imsfe/src/views/SignInCard.tsx
  8. 14
      apps/simple/package.json
  9. 77
      packages/joy-ui/src/auth/LoginForm.tsx
  10. 36
      packages/joy-ui/src/icons/WechatIcon.tsx
  11. 2
      packages/joy-ui/src/index.ts
  12. 39
      packages/joy-ui/src/language/en_US.ts
  13. 2
      packages/joy-ui/src/language/index.ts
  14. 1
      packages/joy-ui/src/language/zh_CN.ts
  15. 40
      packages/joy-ui/src/layout/AdminRoot.tsx
  16. 4
      packages/joy-ui/src/layout/ColorSchemeToggle.tsx
  17. 11
      packages/joy-ui/src/layout/Forbidden.tsx
  18. 35
      packages/joy-ui/src/layout/Layout.tsx
  19. 18
      packages/joy-ui/src/layout/LoadingIndicator.tsx
  20. 11
      packages/joy-ui/src/layout/NotFound.tsx
  21. 18
      packages/joy-ui/src/layout/PageDock.tsx
  22. 174
      packages/joy-ui/src/layout/PageError.tsx
  23. 9
      packages/joy-ui/src/layout/PageRoot.tsx
  24. 52
      packages/joy-ui/src/layout/PageTitle.tsx
  25. 35
      packages/joy-ui/src/layout/RuntimeError.tsx
  26. 15
      packages/joy-ui/src/layout/TitlePortal.tsx
  27. 12
      packages/joy-ui/src/layout/index.ts
  28. 1
      packages/joy-ui/src/utils/index.ts
  29. 9
      packages/joy-ui/src/utils/useMediaQuery.ts
  30. 7
      packages/rakit/src/accessControl/AccessControlContext.ts
  31. 16
      packages/rakit/src/accessControl/AccessControlProvider.tsx
  32. 96
      packages/rakit/src/accessControl/CanAccess.tsx
  33. 5
      packages/rakit/src/accessControl/index.ts
  34. 52
      packages/rakit/src/accessControl/types.ts
  35. 18
      packages/rakit/src/accessControl/useAccessControl.ts
  36. 70
      packages/rakit/src/accessControl/useCan.ts
  37. 21
      packages/rakit/src/auth/AccessControlContext.ts
  38. 37
      packages/rakit/src/auth/AccessControlProvider.tsx
  39. 40
      packages/rakit/src/auth/CanAccess.tsx
  40. 3
      packages/rakit/src/auth/index.ts
  41. 40
      packages/rakit/src/auth/types.ts
  42. 2
      packages/rakit/src/auth/useAuthState.ts
  43. 57
      packages/rakit/src/auth/useCan.ts
  44. 2
      packages/rakit/src/auth/useCheckAuth.ts
  45. 2
      packages/rakit/src/auth/useHandleAuthCallback.ts
  46. 2
      packages/rakit/src/auth/useLogin.ts
  47. 2
      packages/rakit/src/auth/useLogout.ts
  48. 27
      packages/rakit/src/core/AdminRouter.tsx
  49. 0
      packages/rakit/src/core/BasenameContext.ts
  50. 0
      packages/rakit/src/core/BasenameContextProvider.tsx
  51. 134
      packages/rakit/src/core/CoreAdmin.tsx
  52. 182
      packages/rakit/src/core/CoreAdminContext.tsx
  53. 106
      packages/rakit/src/core/CoreAdminRoutes.tsx
  54. 293
      packages/rakit/src/core/CoreAdminUI.tsx
  55. 271
      packages/rakit/src/core/CoreAppContext.tsx
  56. 277
      packages/rakit/src/core/CoreAppRoutes.tsx
  57. 18
      packages/rakit/src/core/DefaultError.tsx
  58. 111
      packages/rakit/src/core/DefaultReady.tsx
  59. 5
      packages/rakit/src/core/DefaultTitleContext.ts
  60. 0
      packages/rakit/src/core/DefaultTitleContextProvider.tsx
  61. 0
      packages/rakit/src/core/ErrorBoundary.tsx
  62. 7
      packages/rakit/src/core/ErrorContext.ts
  63. 13
      packages/rakit/src/core/ErrorContextProvider.tsx
  64. 3
      packages/rakit/src/core/HasDashboardContext.ts
  65. 21
      packages/rakit/src/core/InAdminContext.tsx
  66. 21
      packages/rakit/src/core/InAppContext.tsx
  67. 5
      packages/rakit/src/core/RoutesWithoutLayout.tsx
  68. 20
      packages/rakit/src/core/index.ts
  69. 20
      packages/rakit/src/core/types.ts
  70. 0
      packages/rakit/src/core/useBasename.ts
  71. 173
      packages/rakit/src/core/useConfigureAdminRouterFromChildren.tsx
  72. 0
      packages/rakit/src/core/useDefaultTitle.ts
  73. 3
      packages/rakit/src/core/useErrorContext.ts
  74. 0
      packages/rakit/src/core/useRedirect.ts
  75. 12
      packages/rakit/src/core/useResetErrorBoundaryOnLocationChange.ts
  76. 10
      packages/rakit/src/errorBoundary/ErrorContext.ts
  77. 15
      packages/rakit/src/errorBoundary/ErrorContextProvider.tsx
  78. 4
      packages/rakit/src/errorBoundary/index.ts
  79. 4
      packages/rakit/src/index.ts
  80. 12
      packages/rakit/src/routing/InitialLocationContext.ts
  81. 32
      packages/rakit/src/routing/InitialLocationContextProvider.tsx
  82. 75
      packages/rakit/src/routing/Route.tsx
  83. 9
      packages/rakit/src/routing/index.ts
  84. 17
      packages/rakit/src/routing/useInitialLocation.ts
  85. 20
      packages/rakit/src/routing/useInitialLocationContext.ts
  86. 28
      packages/rakit/src/title/PageTitle.tsx
  87. 37
      packages/rakit/src/title/PageTitleConfigurable.tsx
  88. 41
      packages/rakit/src/title/Title.tsx
  89. 19
      packages/rakit/src/title/TitlePortalProvider.tsx
  90. 4
      packages/rakit/src/title/constants.ts
  91. 8
      packages/rakit/src/title/index.ts
  92. 9
      packages/rakit/src/title/types.ts

@ -12,6 +12,7 @@
"@emotion/styled": "^11.13.0",
"@mui/icons-material": "^5.16.7",
"@mui/joy": "^5.0.0-beta.48",
"@mui/system": "^6.0.2",
"@mui/utils": "^5.16.6",
"@rakit/core": "workspace:*",
"@rakit/fetch": "workspace:*",

@ -1,10 +1,11 @@
import { CoreAdminContext, CoreAppContext, CoreLayoutProps, Route } from '@rakit/core';
import { useFetch } from '@rakit/fetch';
import { Error, Loading, ThemeProvider } from '@rakit/joy-ui';
import { CoreAdmin, CoreLayoutProps, RoutesWithoutLayout } from '@rakit/core';
import { FetchContextProvider, useFetch } from '@rakit/fetch';
import { RuntimeError, Loading, ThemeProvider, NotFound } from '@rakit/joy-ui';
import { createAuthProvider } from './config/createAuthProvider';
import { createDataProvider } from './config/createDataProvider';
import { i18nProvider } from './config/i18nProvider';
import { lazy, Suspense } from 'react';
import { Route } from 'react-router-dom';
const AppLoading = () => <Loading />
@ -16,15 +17,16 @@ const Layout = (props: CoreLayoutProps) => {
)
}
const CatchAll = lazy(() => import("./pages/CatchAll"));
const SignIn = lazy(() => import("./pages/SignIn"));
const PageError = lazy(() => import("./pages/PageError"));
export default function App() {
return (
<ThemeProvider>
<AppContext />
</ThemeProvider>
<FetchContextProvider>
<ThemeProvider>
<AppContext />
</ThemeProvider>
</FetchContextProvider>
);
}
@ -35,44 +37,28 @@ function AppContext() {
const dataProvider = createDataProvider(fetch);
return (
<CoreAppContext
<CoreAdmin
authProvider={authProvider}
// authCallbackPage={ }
// basename={ }
catchAll={<CatchAll />}
catchAll={NotFound}
dataProvider={dataProvider}
error={Error}
homepage={<CatchAll />}
initialLocation="/"
error={RuntimeError}
// dashboard={<CatchAll />}
// initialLocation="/"
i18nProvider={i18nProvider}
loading={AppLoading}
layout={Layout}
loginPage={SignIn}
// ready={ }
requireAuth={false}
requireAuth={true}
// store={ }
title="进销存系统"
>
<Route
name="pageErrpr"
path="/error/:status"
element={<PageError />}
/>
<Route
name="test"
path="/sasda"
element={<div></div>}
/>
<CoreAdminContext
basepath="/admin"
initialLocation="/admmin"
>
<Route
name="test"
path="/sasda"
element={<div>222</div>}
/>
</CoreAdminContext>
</CoreAppContext>
<Route path="/error/:status" element={<PageError />} />
<RoutesWithoutLayout>
<Route path="/" element={<div>sss</div>} />
</RoutesWithoutLayout>
</CoreAdmin>
)
}

@ -1,25 +1,26 @@
import Polyglot from 'node-polyglot';
import { I18nProvider, Locale } from "@rakit/core";
import merge from 'lodash/merge';
import { enUS, zhCN } from '@rakit/joy-ui';
const locale: Locale = { code: 'zh', name: "简体中文" };
const polyglot = new Polyglot({
locale: locale.code,
phrases: {
phrases: merge({
'': '',
ra: {
page: {
error: 'ra.page.error222',
},
message: {
error: "ra.message.error222",
}
},
},
}, enUS, zhCN),
});
function translate(key: string, options: any = {}) {
if (polyglot.has(key)) {
return polyglot.t(key, options);
}
return key;
}
export const i18nProvider: I18nProvider = {
translate: (key: string, options: any = {}) => polyglot.t(key, options),
translate,
changeLocale: () => Promise.resolve(),
getLocale: () => "zh",
getLocales: () => [locale],

@ -1,7 +0,0 @@
import { Navigate } from "react-router-dom";
export default function CatchAll() {
return (
<Navigate to="/error/404" />
);
}

@ -1,10 +1,20 @@
import { StatusError } from "@rakit/joy-ui";
import {
Forbidden,
NotFound,
PageError as Error
} from "@rakit/joy-ui";
import { useParams } from "react-router-dom";
export default function PageError() {
const { status } = useParams<'status'>();
return (
<StatusError status={status} />
);
switch (status) {
case "403":
return <Forbidden key="error403" />
case "404":
return <NotFound key="error404" />
case "500":
default:
return <Error key="error500" />
}
}

@ -3,10 +3,13 @@ import Box from '@mui/joy/Box';
import IconButton from '@mui/joy/IconButton';
import Typography from '@mui/joy/Typography';
import BadgeRoundedIcon from '@mui/icons-material/BadgeRounded';
import { ColorSchemeToggle } from '@rakit/joy-ui';
import { ColorSchemeToggle, useMediaQuery } from '@rakit/joy-ui';
import { SignInCard } from 'src/views/SignInCard';
import { useDefaultTitle } from '@rakit/core';
export default function SignIn() {
const isSmall = useMediaQuery((theme) => theme.breakpoints.down('md'));
const title = useDefaultTitle();
return (
<Fragment>
<Box
@ -17,6 +20,7 @@ export default function SignIn() {
width: '100%',
px: 2,
justifyContent: "space-between",
background: isSmall ? "var(--palette-background-surface)" : undefined,
}}
>
<Box
@ -31,9 +35,9 @@ export default function SignIn() {
<IconButton variant="soft" color="primary" size="sm">
<BadgeRoundedIcon />
</IconButton>
<Typography level="title-lg">Company logo</Typography>
<Typography level="title-lg">{title}</Typography>
</Box>
<ColorSchemeToggle />
<ColorSchemeToggle variant={isSmall ? 'outlined' : 'plain'} />
</Box>
<SignInCard />
<Box component="footer" sx={{ py: 3 }}>

@ -9,9 +9,21 @@ import Box from '@mui/joy/Box';
import Divider from '@mui/joy/Divider';
import Stack from '@mui/joy/Stack';
import Checkbox from '@mui/joy/Checkbox';
import { GoogleIcon } from '@rakit/joy-ui';
import { useMediaQuery, WechatIcon } from '@rakit/joy-ui';
interface FormElements extends HTMLFormControlsCollection {
email: HTMLInputElement;
password: HTMLInputElement;
persistent: HTMLInputElement;
}
interface SignInFormElement extends HTMLFormElement {
readonly elements: FormElements;
}
export function SignInCard() {
const isSmall = useMediaQuery((theme) => theme.breakpoints.down('md'));
return (
<Sheet
sx={{
@ -24,9 +36,9 @@ export function SignInCard() {
flexDirection: 'column',
gap: 2,
borderRadius: 'sm',
boxShadow: 'md',
boxShadow: isSmall ? undefined : 'md',
}}
variant="outlined"
variant={isSmall ? 'plain' : 'outlined'}
>
<Box
component="main"
@ -54,10 +66,10 @@ export function SignInCard() {
<Stack sx={{ gap: 4, mb: 2 }}>
<Stack sx={{ gap: 1 }}>
<Typography component="h1" level="h3">
Sign in
</Typography>
<Typography level="body-sm">
New to company?{' '}
{' '}
<Link href="#replace-with-a-link" level="title-sm">
Sign up!
</Link>
@ -67,15 +79,16 @@ export function SignInCard() {
variant="soft"
color="neutral"
fullWidth
startDecorator={<GoogleIcon />}
startDecorator={<WechatIcon />}
size="lg"
>
Continue with Google
使
</Button>
</Stack>
<Divider
sx={(theme) => ({
[theme.getColorSchemeSelector('light')]: {
color: { xs: '#FFF', md: 'text.tertiary' },
color: 'text.tertiary',
},
})}
>
@ -96,11 +109,23 @@ export function SignInCard() {
>
<FormControl required>
<FormLabel></FormLabel>
<Input type="text" name="account" placeholder="用户名/手机/邮箱" />
<Input
type="text"
name="account"
placeholder="用户名/手机/邮箱"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl>
<FormControl required>
<FormLabel></FormLabel>
<Input type="password" name="password" placeholder="至少6位" />
<Input
type="password"
name="password"
placeholder="至少6位"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl>
<Stack sx={{ gap: 4, mt: 2 }}>
<Box
@ -115,7 +140,7 @@ export function SignInCard() {
</Link>
</Box>
<Button type="submit" fullWidth>
<Button type="submit" fullWidth size="lg">
</Button>
</Stack>

@ -1,14 +0,0 @@
{
"name": "simple-admin",
"type": "module",
"description": "low level admin & dashboard scaffold",
"dependencies": {
"@rakit/core": "workspace:*",
"@rakit/use-async": "workspace:*",
"@rakit/use-fetch": "workspace:*",
"@rakit/use-invariant": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.1"
}
}

@ -0,0 +1,77 @@
import Sheet from '@mui/joy/Sheet';
import Typography from '@mui/joy/Typography';
import FormControl from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
import Input from '@mui/joy/Input';
import Button from '@mui/joy/Button';
import Link from '@mui/joy/Link';
import Box from '@mui/joy/Box';
import Divider from '@mui/joy/Divider';
import Stack from '@mui/joy/Stack';
import Checkbox from '@mui/joy/Checkbox';
import { FormEvent } from "react";
interface FormElements extends HTMLFormControlsCollection {
email: HTMLInputElement;
password: HTMLInputElement;
persistent: HTMLInputElement;
}
interface SignInFormElement extends HTMLFormElement {
readonly elements: FormElements;
}
export function LoginForm() {
return (
<form
onSubmit={(event: FormEvent<SignInFormElement>) => {
event.preventDefault();
const formElements = event.currentTarget.elements;
const data = {
email: formElements.email.value,
password: formElements.password.value,
persistent: formElements.persistent.checked,
};
alert(JSON.stringify(data, null, 2));
}}
>
<FormControl required>
<FormLabel></FormLabel>
<Input
type="text"
name="account"
placeholder="用户名/手机/邮箱"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl>
<FormControl required>
<FormLabel></FormLabel>
<Input
type="password"
name="password"
placeholder="至少6位"
size="lg"
sx={{ fontSize: "md" }}
/>
</FormControl>
<Stack sx={{ gap: 4, mt: 2 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Checkbox size="sm" label="记住我" name="persistent" />
<Link level="title-sm" href="#replace-with-a-link">
</Link>
</Box>
<Button type="submit" fullWidth size="lg">
</Button>
</Stack>
</form>
)
}

@ -2,25 +2,31 @@ import SvgIcon from '@mui/joy/SvgIcon';
export function WechatIcon() {
return (
<SvgIcon fontSize="xl">
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
<SvgIcon fontSize="xl2">
<svg viewBox="0 0 512 512">
<path
fill="#4285F4"
d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"
d="
M342.248,169.517c8.712,0,17.221,0.724,25.616,1.759C352.687,104.625,282.682,54.21,198.503,54.21
c-95.28,0-172.502,64.541-172.502,144.134c0,45.889,25.819,86.602,65.866,112.945l-22.742,45.605l61.953-26.608
c13.285,4.731,27.09,8.627,41.835,10.44c-2.015-8.796-3.16-17.813-3.16-27.068C169.753,234.178,247.115,169.517,342.248,169.517z
M256.003,119.066c11.905,0,21.56,9.685,21.56,21.623c0,11.942-9.654,21.62-21.56,21.62c-11.912,0-21.563-9.678-21.563-21.62
C234.44,128.75,244.091,119.066,256.003,119.066z M141.001,162.309c-11.907,0-21.562-9.678-21.562-21.62
c0-11.938,9.656-21.623,21.562-21.623s21.563,9.685,21.563,21.623C162.563,152.631,152.906,162.309,141.001,162.309z
"
fill="#51C332"
/>
<path
fill="#34A853"
d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"
d="
M485.999,313.656c0-63.684-64.376-115.312-143.751-115.312
c-79.378,0-143.745,51.628-143.745,115.312c0,63.679,64.367,115.308,143.745,115.308c13.054,0,25.471-1.845,37.519-4.465
l77.483,33.291l-26.798-53.701C464.035,382.983,485.999,350.527,485.999,313.656z M299.125,306.448
c-11.906,0-21.563-9.681-21.563-21.625c0-11.938,9.656-21.616,21.563-21.616c11.91,0,21.561,9.682,21.561,21.616
C320.686,296.768,311.033,306.448,299.125,306.448z M385.373,306.448c-11.912,0-21.561-9.681-21.561-21.625
c0-11.938,9.648-21.616,21.561-21.616c11.911,0,21.563,9.682,21.563,21.616C406.936,296.768,397.284,306.448,385.373,306.448z
"
fill="#51C332"
/>
<path
fill="#FBBC05"
d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"
/>
<path
fill="#EA4335"
d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"
/>
</g>
</svg>
</SvgIcon>
);
}

@ -1,3 +1,5 @@
export * from "./icons";
export * from "./language";
export * from "./layout";
export * from "./theme";
export * from "./utils";

@ -0,0 +1,39 @@
export const enUS = {
ra: {
action: {
back: "back",
},
page: {
create: 'Create %{name}',
dashboard: 'Dashboard',
edit: '%{name} %{recordRepresentation}',
empty: 'No %{name} yet.',
error: 'Something went wrong',
forbidden: "No permission",
invite: 'Do you want to add one?',
list: '%{name}',
loading: 'Loading',
not_found: "Sorry, page not found!",
show: '%{name} %{recordRepresentation}',
},
message: {
about: 'About',
are_you_sure: 'Are you sure?',
auth_error: 'An error occurred while validating the authentication token.',
bulk_delete_content: 'Are you sure you want to delete this %{name}? |||| Are you sure you want to delete these %{smart_count} items?',
bulk_delete_title: 'Delete %{name} |||| Delete %{smart_count} %{name}',
clear_array_input: 'Are you sure you want to clear the whole list?',
delete_content: 'Are you sure you want to delete this item?',
delete_title: 'Delete %{name} #%{id}',
details: 'Details',
error: "A client error occurred and your request couldn't be completed, please try again later.",
forbidden: 'The page you’re trying to access has restricted access. Please refer to your system administrator.',
invalid_form: 'The form is not valid. Please check for errors',
loading: 'Please wait',
no: 'No',
not_found: 'Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling.',
yes: 'Yes',
unsaved_changes: "Some of your changes weren't saved. Are you sure you want to ignore them?",
},
},
};

@ -0,0 +1,2 @@
export * from "./zh_CN";
export * from "./en_US";

@ -0,0 +1 @@
export const zhCN = {}

@ -1,40 +0,0 @@
import { CssVarsProvider } from '@mui/joy/styles';
import CssBaseline from '@mui/joy/CssBaseline';
import Box from '@mui/joy/Box';
import { AppBar } from './AppBar';
import { Sidebar } from './Sidebar';
import { Outlet } from 'react-router-dom';
export function AdminRoot() {
return (
<CssVarsProvider disableTransitionOnChange>
<CssBaseline />
<Box sx={{ display: 'flex', minHeight: '100dvh' }}>
<AppBar />
<Sidebar />
<Box
component="main"
className="MainContent"
sx={{
px: { xs: 2, md: 6 },
pt: {
xs: 'calc(12px + var(--Header-height))',
sm: 'calc(12px + var(--Header-height))',
md: 3,
},
pb: { xs: 2, sm: 2, md: 3 },
flex: 1,
display: 'flex',
flexDirection: 'column',
minWidth: 0,
height: '100dvh',
gap: 1,
}}
>
<Outlet />
</Box>
</Box>
</CssVarsProvider>
);
}

@ -38,10 +38,10 @@ export function ColorSchemeToggle(props: IconButtonProps) {
}}
sx={[
{
"& > *:first-child": {
"& > *:first-of-type": {
display: mode === "dark" ? "none" : "initial",
},
"& > *:last-child": {
"& > *:last-of-type": {
display: mode === "light" ? "none" : "initial",
},
},

@ -0,0 +1,11 @@
import { PageError } from "./PageError";
export function Forbidden() {
return (
<PageError
image="forbidden"
title="ra.page.forbidden"
message="ra.message.forbidden"
/>
);
}

@ -0,0 +1,35 @@
import Box from '@mui/joy/Box';
import { AppBar } from './AppBar';
import { Sidebar } from './Sidebar';
import { Outlet } from 'react-router-dom';
export function Layout() {
return (
<Box sx={{ display: 'flex', minHeight: '100dvh' }}>
<AppBar />
<Sidebar />
<Box
component="main"
className="MainContent"
sx={{
px: { xs: 2, md: 6 },
pt: {
xs: 'calc(12px + var(--Header-height))',
sm: 'calc(12px + var(--Header-height))',
md: 3,
},
pb: { xs: 2, sm: 2, md: 3 },
flex: 1,
display: 'flex',
flexDirection: 'column',
minWidth: 0,
height: '100dvh',
gap: 1,
}}
>
<Outlet />
</Box>
</Box>
);
}

@ -0,0 +1,18 @@
import IconButton, { IconButtonProps } from '@mui/joy/IconButton';
import RefreshIcon from '@mui/icons-material/Refresh';
import { useLoading } from '@rakit/core';
type LoadingIndicatorProps = Omit<IconButtonProps, 'loading'>;
export function LoadingIndicator(props: LoadingIndicatorProps) {
const loading = useLoading();
return (
<IconButton
{...props}
loading={loading}
>
<RefreshIcon />
</IconButton>
);
}

@ -0,0 +1,11 @@
import { PageError } from "./PageError";
export function NotFound() {
return (
<PageError
image="not_found"
title="ra.page.not_found"
message="ra.message.not_found"
/>
);
}

@ -0,0 +1,18 @@
import { Portlet } from "@rakit/core";
import Box, { BoxProps } from "@mui/joy/Box";
export interface PageDockProps extends BoxProps { }
export function PageDock(props: PageDockProps) {
if (props.children == null) {
return null;
}
return (
<Portlet to="page-dock">
<Box
{...props}
/>
</Portlet>
);
}

@ -2,7 +2,8 @@ import AspectRatio from '@mui/joy/AspectRatio';
import Box from '@mui/joy/Box';
import Button from '@mui/joy/Button';
import Typography from '@mui/joy/Typography';
import { useDefaultTitle } from '@rakit/core';
import { useDefaultTitle, useTranslate } from '@rakit/core';
import HistoryIcon from '@mui/icons-material/History';
import { ColorSchemeToggle } from './ColorSchemeToggle';
import { ReactNode } from 'react';
@ -10,22 +11,22 @@ const Icon403 = (
<svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id=":r2:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%">
<stop offset="0%" stop-color="var(--palette-primary-main)" />
<stop offset="100%" stop-color="var(--palette-primary-main)" stop-opacity="0" />
<stop offset="0%" stopColor="var(--palette-primary-main)" />
<stop offset="100%" stopColor="var(--palette-primary-main)" stopOpacity="0" />
</linearGradient>
</defs>
<path fill="url(#:r2:)" fill-rule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<path fill="url(#:r2:)" fillRule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-4.webp" height="300" x="220" y="30" />
<path fill="var(--palette-primary-main)" d="M425.545 119.2c0-5-4.6-9-9.6-8.2-2-3.7-6-6-10.2-5.9 4.3-21.4-30-21.4-25.7 0-8.7-.8-15.1 9.4-10.4 16.8 2.1 3.5 5.9 5.6 10 5.5h38.7v-.1c4.1-.4 7.2-3.9 7.2-8.1zm-321.3 81.8c.1-4.2-4.1-7.8-8.2-7-1.7-3.2-5.1-5.1-8.8-5 3.8-18.4-25.8-18.4-22 0-7.4-.7-12.9 8.1-8.9 14.4 1.8 3 5.1 4.8 8.6 4.7h33.2v-.1c3.4-.4 6.1-3.4 6.1-7z" opacity="0.08" /><path fill="#FFAB00" d="M111.045 142.2c58.7-1 58.6-88.3 0-89.2-58.6 1-58.6 88.3 0 89.2z" opacity="0.12" />
<path fill="#FFD666" d="M111.045 121c30.8-.5 30.8-46.3 0-46.8-30.8.5-30.8 46.3 0 46.8z" />
<path fill="#FBCDBE" d="M278.045 250.1c-4.6-6.5-14 5.1-18.1 7.2-.6-2.1 1.5-41.3-1.4-41.8-2.8-3-8.1-.7-8 3.3.2-4 .5-11.3-5.6-10.2-4.8.6-3.8 6.9-3.8 10.2.1-6.1-9.5-6.1-9.4 0v5.6c.2-4.2-5.7-6.4-8.3-3-2.6-.2-.4 41.8-1.1 43.3-.2 10 8.7 19 18.8 18.7 6.1.4 12.6-1.2 16.8-5.9l19.7-21c1.7-1.6 1.8-4.5.4-6.4z" />
<path fill="#000" fill-opacity="0.24" fill-rule="evenodd" d="M248.745 212.3v32.8h1.9v-31.9c.1-2.9-2.8-5.2-5.6-4.6 2 0 3.7 1.7 3.7 3.7zm-9.4 5.6v27.2h1.9v-26.3c.1-2.8-2.8-5.2-5.5-4.6 1.9 0 3.6 1.8 3.6 3.7zm-9.4 27.2v-21.6c.1-2-1.7-3.7-3.7-3.8 2.8-.6 5.6 1.8 5.5 4.6V245h-1.8v.1z" clip-rule="evenodd" />
<path fill="#000" fillOpacity="0.24" fillRule="evenodd" d="M248.745 212.3v32.8h1.9v-31.9c.1-2.9-2.8-5.2-5.6-4.6 2 0 3.7 1.7 3.7 3.7zm-9.4 5.6v27.2h1.9v-26.3c.1-2.8-2.8-5.2-5.5-4.6 1.9 0 3.6 1.8 3.6 3.7zm-9.4 27.2v-21.6c.1-2-1.7-3.7-3.7-3.8 2.8-.6 5.6 1.8 5.5 4.6V245h-1.8v.1z" clipRule="evenodd" />
<path fill="var(--palette-primary-darker)" d="M244.945 189.8c-67.6 1.3-77 97-11 111.4 81 11.8 92.7-107.3 11-111.4zm-48.5 56.2c-1-40.4 49.8-63.8 79.9-36.9l-68.3 68.3c-7.5-8.7-11.6-19.9-11.6-31.4zm48.5 48.5c-11.5 0-22.7-4.1-31.4-11.6l68.3-68.3c27 30.1 3.5 80.9-36.9 79.9z" />
<path fill="url(#paint0_linear_1_129)" d="M169.245 261h-11.3v-66.6c0-4.5-1.5-5.6-5.6-5.6-5.3.3-13.8-1.4-17.1 4l-55 68.3c-2.7 3.3-1.8 8.8-2 12.8 0 4.1 1.5 5.6 5.6 5.6h54.7v21.7c-.9 7.9 9.1 5.2 13.7 5.6 4.1 0 5.6-1.5 5.6-5.6v-21.7h11.4c4.4 0 5.6-1.5 5.6-5.6-.3-4.8 2-13.8-5.6-12.9zm-30.8 0h-36l36-44.4V261zm263.9 12.1c1.9 44.8-78.7 46-78 1.2h19.3c-.8 15.3 18.3 21.4 30.1 15.5 12.7-6 12.3-29.1-1-34-5.6-2.8-16.6-2-23.1-2.1v-15.1c6.3-.2 17.6.9 22.7-2.3 11.6-5.5 11.9-25.4.9-31.4-10.8-5.9-29 .1-28.2 14.5h-19.4c-.5-28.1 35.4-38.5 57-28.2 23.4 9 24.1 45.5-.2 54.6 12.3 3.9 20.1 14.6 19.9 27.3z" />
<defs>
<linearGradient id="paint0_linear_1_129" x1="78.245" x2="78.245" y1="187.309" y2="307.306" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-light)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" />
<stop stopColor="var(--palette-primary-light)" />
<stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient>
</defs>
</svg>
@ -35,11 +36,11 @@ const Icon404 = (
<svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id=":rm9:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%">
<stop offset="0%" stop-color="var(--palette-primary-main)" />
<stop offset="100%" stop-color="var(--palette-primary-main)" stop-opacity="0" />
<stop offset="0%" stopColor="var(--palette-primary-main)" />
<stop offset="100%" stopColor="var(--palette-primary-main)" stopOpacity="0" />
</linearGradient>
</defs>
<path fill="url(#:rm9:)" fill-rule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<path fill="url(#:rm9:)" fillRule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-6.webp" height="300" x="205" y="30" />
<path fill="#FFAB00" d="M111.1 141.2c58.7-1 58.6-88.3 0-89.2-58.6 1-58.6 88.3 0 89.2z" opacity="0.12" />
<path fill="#FFD666" d="M111.1 120c30.8-.5 30.8-46.3 0-46.8-30.8.5-30.8 46.3 0 46.8z" />
@ -48,8 +49,8 @@ const Icon404 = (
<path fill="var(--palette-primary-main)" d="M425.6 118.2c0-5-4.6-9-9.6-8.2-2-3.7-6-6-10.2-5.9 4.3-21.4-30-21.4-25.7 0-8.7-.8-15.1 9.4-10.4 16.8 2.1 3.5 5.9 5.6 10 5.5h38.7v-.1c4.1-.4 7.2-3.9 7.2-8.1zM104.3 200c.1-4.2-4.1-7.8-8.2-7-1.7-3.2-5.1-5.1-8.8-5 3.8-18.4-25.8-18.4-22 0-7.4-.7-12.9 8.1-8.9 14.4 1.8 3 5.1 4.8 8.6 4.7h33.2v-.1c3.4-.4 6.1-3.4 6.1-7z" opacity="0.08" />
<defs>
<linearGradient id="paint0_linear_1_119" x1="78.3" x2="78.3" y1="187.77" y2="305.935" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-light)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" />
<stop stopColor="var(--palette-primary-light)" />
<stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient>
</defs>
</svg>
@ -59,11 +60,11 @@ const Icon500 = (
<svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id=":r1:" x1="19.496%" x2="77.479%" y1="71.822%" y2="16.69%">
<stop offset="0%" stop-color="var(--palette-primary-main)" />
<stop offset="100%" stop-color="var(--palette-primary-main)" stop-opacity="0" />
<stop offset="0%" stopColor="var(--palette-primary-main)" />
<stop offset="100%" stopColor="var(--palette-primary-main)" stopOpacity="0" />
</linearGradient>
</defs>
<path fill="url(#:r1:)" fill-rule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<path fill="url(#:r1:)" fillRule="nonzero" d="M0 198.78c0 41.458 14.945 79.236 39.539 107.786 28.214 32.765 69.128 53.365 114.734 53.434a148.44 148.44 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.779 75.779 0 0033.957 8.01c5.023 0 9.942-.494 14.7-1.433 13.58-2.67 25.94-8.99 36.09-17.94 6.378-5.627 14.547-8.456 22.897-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.224C474.92 234.58 480 213.388 480 190.958c0-76.93-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.499 0a103.651 103.651 0 00-45.88 10.661c-13.24 6.487-25.011 15.705-34.64 26.939-32.698.544-62.931 11.69-87.676 30.291C25.351 97.155 0 144.882 0 198.781z" opacity="0.2" />
<image href="https://assets.minimals.cc/public/assets/illustrations/characters/character-8.webp" height="300" x="340" y="30" />
<path fill="var(--palette-primary-main)" d="M292.4 266.4h-7.3v-.6h6.7v-59.6h-25.7V118h-23.6v-.6h24.2v88.2h25.7v60.8zM146 164.5h-.6v-21.1h16.5v-19h.6v19.7H146v20.4z" />
<path fill="var(--palette-primary-main)" d="M242.5 112.3c0 3.2-1.3 6.3-3.5 8.5-2.3 2.3-5.3 3.5-8.5 3.5h-82.9c-4.4.1-8.5-2.2-10.7-5.9-2.2-3.8-2.2-8.5 0-12.3 2.2-3.8 6.3-6.1 10.7-5.9h2.8c-2-7.2-.6-14.9 3.9-20.8s11.6-9.4 19-9.4h7c8.9 0 17 4.9 21.1 12.8 2-1 4.2-1.6 6.5-1.6h1.8c3.8 0 7.4 1.5 10.1 4.2 2.7 2.7 4.2 6.3 4.2 10.1v.7c0 1.3-.2 2.7-.6 3.9h6.9c6.8.2 12.2 5.6 12.2 12.2z" opacity="0.08" />
@ -83,48 +84,76 @@ const Icon500 = (
<path fill="var(--palette-primary-darker)" d="M264.4 267.7c.5-1.8-.8-3.7-1.2-5.5-.1-.3-.1-.7 0-1 .2-1.5 1.5-2.6 3-2.6s2.8 1.1 3 2.6c.1.3 0 .7 0 1-.3 1.8-1.6 3.8-1.1 5.6l.4 1.3c.5 1.5-.7 3.1-2.3 3.1-1.6 0-2.7-1.5-2.3-3l.5-1.5zM258 158.8l9.2-4.8 8.8 4.8s-1.6 11.8-8.6 15.2c0 0-8.6-3.3-9.4-15.2z" />
<defs>
<linearGradient id="paint0_linear_1_140" x1="277.574" x2="255.652" y1="143.24" y2="187.057" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-main)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" />
<stop stopColor="var(--palette-primary-main)" />
<stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient>
<linearGradient id="paint1_linear_1_140" x1="138" x2="138" y1="164" y2="287.9" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--palette-primary-light)" />
<stop offset="1" stop-color="var(--palette-primary-dark)" />
<stop stopColor="var(--palette-primary-light)" />
<stop offset="1" stopColor="var(--palette-primary-dark)" />
</linearGradient>
</defs>
</svg>
);
const statusIcons = {
"403": Icon403,
"404": Icon404,
"500": Icon500,
};
function resolveMessageByTitlte(title: ReactNode): string | undefined {
switch (title) {
case "ra.page.error":
return "ra.message.error";
case "ra.page.forbidden":
return "ra.message.forbidden";
case "ra.page.not_found":
return "ra.message.not_found";
default:
return undefined;
}
}
function resolveImageByTitle(title: ReactNode): string | undefined {
switch (title) {
case "ra.page.error":
return "error";
case "forbidden":
return "ra.message.forbidden";
case "ra.page.not_found":
return "not_found";
default:
return undefined;
}
}
const statusTitles = {
"403": "No permission",
"404": "Sorry, page not found!",
"500": "Internal server error",
};
function goBack() {
window.history.go(-1);
}
const statusDescriptions = {
"403": "The page you’re trying to access has restricted access. Please refer to your system administrator.",
"404": "Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling.",
"500": "There was an error, please try again later.",
};
function resolveImage(image: string) {
switch (image) {
case "forbidden":
return Icon403;
case "not_found":
return Icon404;
case "error":
default: // TODO
return Icon500;
}
}
export interface StatusErrorProps {
status: "403" | "404" | "500";
icon?: ReactNode;
export interface PageErrorProps {
image?: ReactNode;
title?: ReactNode;
description?: ReactNode;
message?: ReactNode;
button?: ReactNode;
}
export function StatusError(props: StatusErrorProps) {
const status = statusIcons[props.status] ? props.status : "500";
export function PageError(props: PageErrorProps) {
const { title = "ra.page.error", button } = props;
const image = props.image || resolveImageByTitle(title);
const message = props.message || resolveMessageByTitlte(title);
const translate = useTranslate();
const siteTitle = useDefaultTitle();
const icon = props.icon || statusIcons[status];
const title = props.title || statusTitles[status];
const description = props.description ?? statusDescriptions[status];
const imageNode = typeof image === 'string' ? resolveImage(image) : image;
const titleNode = typeof title === 'string' ? translate(title) : title;
const messageNode = typeof message === 'string' ? translate(message) : message;
return (
<Box
@ -153,29 +182,44 @@ export function StatusError(props: StatusErrorProps) {
<ColorSchemeToggle variant="plain" />
</Box>
<Box sx={{ maxWidth: "448px", p: 2 }}>
<Typography level="h2" textAlign="center">
{title}
</Typography>
<Typography color="neutral" pt={2} textAlign="center">
{description}
</Typography>
<AspectRatio
ratio="1"
sx={(theme) => ({
maxWidth: "320px",
pt: 4, flexShrink: 1,
mx: "auto",
"--palette-primary-light": theme.palette.primary[300],
"--palette-primary-main": theme.palette.primary[500],
"--palette-primary-dark": theme.palette.primary[600],
"--palette-primary-darker": theme.palette.primary[700]
})}
variant="plain"
>
{icon}
</AspectRatio>
{titleNode != null ? (
<Typography level="h2" textAlign="center">
{titleNode}
</Typography>
) : null}
{messageNode != null ? (
<Typography color="neutral" pt={2} textAlign="center">
{messageNode}
</Typography>
) : null}
{imageNode != null ? (
<AspectRatio
ratio="1"
sx={(theme) => ({
maxWidth: "320px",
pt: 4, flexShrink: 1,
mx: "auto",
"--palette-primary-light": theme.palette.primary[300],
"--palette-primary-main": theme.palette.primary[500],
"--palette-primary-dark": theme.palette.primary[600],
"--palette-primary-darker": theme.palette.primary[700]
})}
variant="plain"
>
{imageNode}
</AspectRatio>
) : null}
<Box my={2} textAlign="center">
<Button size="lg" color="neutral">Go to home</Button>
{button != null ? button : (
<Button
size="lg"
color="neutral"
onClick={goBack}
startDecorator={<HistoryIcon />}
>
{translate('ra.action.back')}
</Button>
)}
</Box>
</Box>
</Box>

@ -6,7 +6,7 @@ import Typography from '@mui/joy/Typography';
import HomeRoundedIcon from '@mui/icons-material/HomeRounded';
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
import { ReactNode, useState } from 'react';
import { PortalProvider, TitlePortalProvider } from '@rakit/core';
import { PortalProvider } from '@rakit/core';
export interface PageRootProps {
children: ReactNode;
@ -86,14 +86,17 @@ export function PageRoot(props: PageRootProps) {
/>
<Box ref={setActionsPortal} />
</Box>
<TitlePortalProvider value={titlePortal}>
<PortalProvider
name="page-title"
container={titlePortal}
>
<PortalProvider
name="page-actions"
container={actionsPortal}
>
{props.children}
</PortalProvider>
</TitlePortalProvider>
</PortalProvider>
</Box>
)
}

@ -0,0 +1,52 @@
import {
Portlet,
useDefaultTitle,
useRecordRepresentation,
useTranslate
} from "@rakit/core";
import { ReactElement, JSX } from "react";
export interface PageTitleProps extends JSX.ElementAttributesProperty {
record?: any;
preferenceKey?: string | false;
title?: string | ReactElement;
}
export function PageTitle(props: PageTitleProps) {
const {
record,
preferenceKey,
title,
...rest
} = props;
const translate = useTranslate();
const defaultTitle = useDefaultTitle();
const titleFromPreferences = useRecordRepresentation({
record,
representation: preferenceKey === false ? undefined : preferenceKey,
});
if (!title && !titleFromPreferences && !defaultTitle) {
return null;
}
return (
<Portlet to="page-title">
<span {...rest}>
{
titleFromPreferences
? translate(
titleFromPreferences,
{ ...record, _: titleFromPreferences },
)
: !title
? defaultTitle
: typeof title === "string"
? translate(title, { _: title })
: title
}
</span>
</Portlet>
);
}

@ -3,19 +3,19 @@ import ErrorIcon from '@mui/icons-material/Report';
import Accordion from '@mui/joy/Accordion';
import AccordionDetails from '@mui/joy/AccordionDetails';
import AccordionSummary from '@mui/joy/AccordionSummary';
import Box from '@mui/joy/Box';
import Button from '@mui/joy/Button';
import { styled } from '@mui/joy/styles';
import Typography from '@mui/joy/Typography';
import {
Title,
useDefaultTitle,
useErrorContext,
useResetErrorBoundaryOnLocationChange,
useTranslate
} from '@rakit/core';
import { Fragment } from 'react';
import { ColorSchemeToggle } from './ColorSchemeToggle';
export function Error() {
export function RuntimeError() {
const title = useDefaultTitle();
const { error, errorInfo, resetErrorBoundary } = useErrorContext();
const translate = useTranslate();
@ -23,8 +23,31 @@ export function Error() {
useResetErrorBoundaryOnLocationChange(resetErrorBoundary);
return (
<Fragment>
{title && <Title title={title} />}
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: {
sm: "flex-start",
md: "center",
},
alignItems: "center",
pt: 12,
pb: 8,
minHeight: "100dvh",
}}
>
<Box sx={{
position: "fixed",
top: 0,
insetInline: 0,
display: "flex",
justifyContent: "space-between",
p: 4,
}}>
<div>{title}</div>
<ColorSchemeToggle variant="plain" />
</Box>
<Root>
<h1 className={ErrorClasses.title} role="alert">
<ErrorIcon className={ErrorClasses.icon} />
@ -84,7 +107,7 @@ export function Error() {
</Button>
</div>
</Root>
</Fragment>
</Box>
);
}

@ -1,15 +0,0 @@
import Typography, { TypographyProps } from '@mui/joy/Typography';
export function TitlePortal(props: TypographyProps) {
return (
<Typography
sx={{ color: "inherit" }}
flex="1"
textOverflow="ellipsis"
whiteSpace="nowrap"
overflow="hidden"
level="title-md"
id="react-admin-title"
{...props} />
);
}

@ -1,12 +1,16 @@
export * from "./AdminRoot";
export * from "./AppBar";
export * from "./ColorSchemeToggle";
export * from "./Error";
export * from "./Forbidden";
export * from "./Layout";
export * from "./Loading";
export * from "./LoadingIndicator";
export * from "./NotFound";
export * from "./Notification";
export * from "./PageActions";
export * from "./PageDock";
export * from "./PageError";
export * from "./PageRoot";
export * from "./PageTitle";
export * from "./Sidebar";
export * from "./StatusError";
export * from "./TitlePortal";
export * from "./RuntimeError";
export * from "./utils";

@ -0,0 +1 @@
export * from "./useMediaQuery"

@ -0,0 +1,9 @@
import { Theme } from "@mui/joy/styles";
import {
UseMediaQueryOptions,
useMediaQuery as useMediaQueryForMuiSystem
} from "@mui/system";
export function useMediaQuery(queryInput: string | ((theme: Theme) => string), options?: UseMediaQueryOptions): boolean {
return useMediaQueryForMuiSystem<Theme>(queryInput);
}

@ -1,7 +0,0 @@
import { createContext } from "react";
import { AccessControlContextValue } from "./types";
/**
* @private
*/
export const AccessControlContext = createContext<AccessControlContextValue | undefined>(undefined);

@ -1,16 +0,0 @@
import { ReactNode } from "react";
import { AccessControlContext } from "./AccessControlContext";
import { CanFunction } from "./types";
export type AccessControlProviderProps = {
can?: CanFunction;
children?: ReactNode;
}
export function AccessControlProvider(props: AccessControlProviderProps) {
return (
<AccessControlContext.Provider value={props.value}>
{props.children}
</AccessControlContext.Provider>
)
}

@ -1,96 +0,0 @@
import {
cloneElement,
createElement,
isValidElement,
ReactNode,
useEffect
} from "react";
import {
AccessControlOptions,
AccessFallbackComponent,
CanParams
} from "./types";
import { useCan } from "./useCan";
import { useAccessControl } from "./useAccessControl";
type OnUnauthorizedProps = {
reason?: string;
params: CanParams;
};
export interface CanAccessProps extends CanParams {
children: ReactNode;
/**
* Content to show if access control returns `false`
*/
fallback?: AccessFallbackComponent;
loading?: ReactNode
queryOptions?: AccessControlOptions;
/**
* Callback function to be called if access control returns `can: false`
*/
onUnauthorized?: (props: OnUnauthorizedProps) => void;
}
export function CanAccess(props: CanAccessProps) {
const {
children,
fallback,
loading,
queryOptions,
onUnauthorized,
...params
} = props;
const {
isExecuting: isLoading,
reason,
can,
error,
} = useCan({
...params,
queryOptions,
});
const {
fallback: fallbackElement,
} = useAccessControl({});
useEffect(() => {
if (onUnauthorized && can === false && !isLoading) {
onUnauthorized({ reason, params });
}
}, [can, isLoading]);
if (isLoading) {
return loading;
}
if (can) {
return children;
}
return resolveFallback(
fallback ?? fallbackElement,
reason,
error,
);
}
function resolveFallback(
fallback: AccessFallbackComponent | undefined | null,
reason: string | undefined,
error: unknown,
) {
if (fallback == null) {
return null;
}
if (isValidElement(fallback)) {
return cloneElement(fallback, { reason, error })
}
return createElement(fallback, { reason, error })
}

@ -1,5 +0,0 @@
export * from "./AccessControlProvider";
export * from "./CanAccess";
export * from "./types";
export * from "./useAccessControl";
export * from "./useCan";

@ -1,52 +0,0 @@
import { UseAsyncOptions } from "@rakit/use-async";
import { ComponentType, ReactElement } from "react";
export type CanReturn = {
can: boolean;
reason?: string;
};
export interface AccessParamsCustoms {}
export type AccessParams = Record<string, any> & AccessParamsCustoms;
export interface CanParams {
/**
* Resource name for API data interactions
*/
on?: string;
/**
* Intended action on resource
*/
key: string;
/**
* Parameters associated with the resource
* @type {
* resource?: [IResourceItem](https://refine.dev/docs/api-reference/core/interfaceReferences/#canparams),
* id?: [BaseKey](https://refine.dev/docs/api-reference/core/interfaceReferences/#basekey), [key: string]: any
* }
*/
params?: AccessParams;
}
export type CanFunction = (params: CanParams) => Promise<CanReturn> | CanReturn;
export type AccessControlOptions = Omit<
UseAsyncOptions<CanReturn, CanParams, unknown>,
'executor' | 'variables'
>;
export type AccessFallbackProps = {
reason?: string;
error?: unknown;
}
export type AccessFallbackComponent = ComponentType<AccessFallbackProps> | ReactElement<AccessFallbackProps>;
export interface AccessControlContextCustomValue {}
export interface AccessControlContextValue extends AccessControlContextCustomValue {
can?: CanFunction;
queryOptions?: AccessControlOptions;
fallback?: AccessFallbackComponent;
}

@ -1,18 +0,0 @@
import { useContext } from "react";
import { AccessControlContext } from "./AccessControlContext";
import { AccessControlContextValue } from "./types";
export function useAccessControl(): AccessControlContextValue | undefined;
export function useAccessControl(overrides: AccessControlContextValue): AccessControlContextValue;
export function useAccessControl(overrides?: AccessControlContextValue) {
const fromContext = useContext(AccessControlContext)
if (fromContext != null) {
return {
...fromContext,
...overrides,
}
}
return overrides;
}

@ -1,70 +0,0 @@
import {
useAsync,
UseAsyncResult,
useAutomatic
} from "@rakit/use-async";
import {
AccessControlOptions,
CanParams,
CanReturn
} from "./types";
import { useAccessControl } from "./useAccessControl";
import { useMemo } from "react";
export type UseCanProps = CanParams & {
queryOptions?: AccessControlOptions;
}
export type UseCanResult =
& Omit<
UseAsyncResult<CanReturn, CanParams, unknown>,
'execute' | 'abort' | 'data' | 'isExecuting'
>
& { isLoading: boolean }
& CanReturn;
export function useCan(options: UseCanProps): UseCanResult {
const { queryOptions, ...params } = options;
const context = useAccessControl({});
const can = context?.can
const asyncOptions = {
...context?.queryOptions,
...queryOptions,
}
const {
data,
error,
isExecuting: isLoading,
execute,
} = useAsync<CanReturn, CanParams, unknown>({
...asyncOptions,
variables: params,
executor: (params) => can?.(params) ?? ({ can: true }),
immediate: typeof can !== "undefined",
});
useAutomatic({
execute,
params,
asyncOptions,
});
return useMemo(() => {
if (typeof can === "undefined") {
return {
can: true,
isLoading: false,
error: undefined,
reason: undefined,
}
}
return {
can: data?.can ?? false,
error,
isLoading,
reason: data?.reason,
}
}, [can, data, error, isLoading]);
}

@ -0,0 +1,21 @@
import { createContext } from "react";
import {
AccessFallbackComponent,
CanAccessOptions,
CanAccessResult,
Permission
} from "./types";
/**
* @private
*/
export interface AccessControlContextValue {
canAccess?: (options: CanAccessOptions) => CanAccessResult;
permissions?: Permission[];
accessFallback?: AccessFallbackComponent;
}
/**
* @private
*/
export const AccessControlContext = createContext<AccessControlContextValue>({});

@ -0,0 +1,37 @@
import { ReactNode } from "react";
import { AccessControlContext } from "./AccessControlContext";
import {
AccessFallbackComponent,
CanAccessOptions,
CanAccessResult
} from "./types";
import { useAuthProvider } from "./useAuthProvider";
import { usePermissions } from "./usePermissions";
interface AccessControlProviderProps {
accessFallback?: AccessFallbackComponent;
canAccess?: (options: CanAccessOptions) => CanAccessResult;
children?: ReactNode;
}
export function AccessControlProvider(props: AccessControlProviderProps) {
const authProvider = useAuthProvider();
const {
accessFallback = authProvider?.accessFallback,
canAccess = authProvider?.canAccess,
children
} = props;
const { permissions } = usePermissions();
return (
<AccessControlContext.Provider
value={{
accessFallback,
canAccess,
permissions
}}
>
{children}
</AccessControlContext.Provider>
);
}

@ -0,0 +1,40 @@
import {
createElement,
ReactNode,
useContext
} from "react";
import { AccessControlContext } from "./AccessControlContext";
import {
AccessFallbackComponent,
PermissionIdentifier,
PermissionTarget
} from "./types";
import { useCan } from "./useCan";
export interface CanAccessProps {
target: PermissionTarget;
identifier: PermissionIdentifier;
fallback?: AccessFallbackComponent;
children?: ReactNode;
}
export function CanAccess(props: CanAccessProps) {
const { target, identifier } = props;
const { can, reason } = useCan(target, identifier);
const { accessFallback } = useContext(AccessControlContext);
const { children, fallback = accessFallback } = props;
if (can) {
return children;
}
if (fallback == null) {
return null;
}
return createElement(fallback, {
reason,
target,
identifier
})
}

@ -1,10 +1,13 @@
export * from "./AccessControlProvider";
export * from "./AuthContext";
export * from "./Authenticated";
export * from "./CanAccess";
export * from "./LogoutOnMount";
export * from "./types";
export * from "./useAuthenticated";
export * from "./useAuthProvider";
export * from "./useAuthState";
export * from "./useCan";
export * from "./useCheckAuth";
export * from "./useGetIdentity";
export * from "./useGetPermissions";

@ -1,3 +1,4 @@
import { ComponentType } from "react";
import { To } from "react-router-dom";
type QueryFunctionContext = {
@ -22,6 +23,41 @@ export interface UserIdentity {
[key: string]: any;
}
export type PermissionIdentifier = string | number;
export type PermissionTarget =
| 'route'
| 'menu'
| 'view'
| 'element'
| 'api'
| string;
export interface Permission {
identifier: PermissionIdentifier;
target: PermissionTarget
[key: string]: any;
}
export interface CanAccessOptions {
permissions: Permission[];
target: PermissionTarget;
identifier: string | number;
}
export interface CanAccessResult {
can: boolean;
reason?: string;
}
export type AccessFallbackProps = {
reason?: string;
target: PermissionTarget;
identifier: PermissionIdentifier;
}
export type AccessFallbackComponent = ComponentType<AccessFallbackProps>;
export interface AuthProviderCustoms {}
export interface AuthProvider extends AuthProviderCustoms {
@ -29,7 +65,9 @@ export interface AuthProvider extends AuthProviderCustoms {
logout: (params: any) => Promise<void | false | string>;
checkAuth: (params: QueryFunctionContext) => Promise<void>;
checkError: (error: any) => Promise<void>;
canAccess?: (options: CanAccessOptions) => CanAccessResult;
accessFallback?: AccessFallbackComponent;
getIdentity?: (params?: QueryFunctionContext) => Promise<UserIdentity>;
getPermissions: (params: QueryFunctionContext) => Promise<any>;
getPermissions: (params: QueryFunctionContext) => Promise<Permission[]>;
handleCallback?: (params?: QueryFunctionContext) => Promise<AuthRedirectResult | void | any>;
}

@ -7,7 +7,7 @@ import {
import noop from 'lodash/noop';
import { useEffect, useMemo } from "react";
import { useNotify } from "../notification";
import { useBasename } from "../routing";
import { useBasename } from "../core";
import { getErrorMessage, removeDoubleSlashes } from "../util";
import { defaultAuthParams, useAuthProvider } from "./useAuthProvider";
import { useLogout } from "./useLogout";

@ -0,0 +1,57 @@
import {
useContext,
useEffect,
useMemo,
useState
} from "react";
import {
CanAccessResult,
PermissionIdentifier,
PermissionTarget
} from "./types";
import { AccessControlContext } from "./AccessControlContext";
export function useCan(
target: PermissionTarget,
identifier: PermissionIdentifier,
): CanAccessResult {
const { canAccess, permissions } = useContext(AccessControlContext);
const [can, setCan] = useState(false);
const [reason, setReason] = useState<string>();
useEffect(() => {
let can = false;
let reason: string | undefined = "forbidden";
if (permissions?.length) {
for (const perm of permissions) {
if (perm.identifier !== identifier) {
continue;
}
if (perm.target !== target) {
break;
}
if (canAccess) {
({ can, reason } = canAccess({
target,
permissions,
identifier,
}));
}
}
}
setCan(can)
setReason(reason)
}, [
permissions,
canAccess,
target,
identifier,
]);
return useMemo(() => {
return {
can,
reason,
}
}, [can, reason]);
}

@ -1,7 +1,7 @@
import { useCallback } from "react";
import { To } from "react-router-dom";
import { useNotify } from "../notification";
import { useBasename } from "../routing";
import { useBasename } from "../core";
import { getErrorMessage, removeDoubleSlashes } from "../util";
import { defaultAuthParams, useAuthProvider } from "./useAuthProvider";
import { useLogout } from "./useLogout";

@ -3,7 +3,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import noop from 'lodash/noop';
import { useEffect } from 'react';
import { useLocation } from "react-router-dom";
import { useRedirect } from "../routing";
import { useRedirect } from "../core";
import { AuthRedirectResult } from "./types";
import { useAuthProvider } from "./useAuthProvider";

@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useNotificationContext } from '../notification';
import { defaultAuthParams, useAuthProvider } from './useAuthProvider';
import { useBasename } from '../routing';
import { useBasename } from '../core';
import { removeDoubleSlashes } from '../util';
/**

@ -1,10 +1,10 @@
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from "react";
import { Path, useLocation, useNavigate } from "react-router-dom";
import { useBasename } from "../routing";
import { useResetStore } from "../store";
import { removeDoubleSlashes } from "../util";
import { defaultAuthParams, useAuthProvider } from "./useAuthProvider";
import { useBasename } from '../core';
/**
* Log the current user out by calling the authProvider.logout() method,

@ -8,14 +8,14 @@ import { BasenameContextProvider } from './BasenameContextProvider';
export interface AppRouterProps {
basename?: string;
children: React.ReactNode;
children: ReactNode;
}
/**
* Creates a react-router Router unless the app is already inside existing router.
* Also creates a BasenameContext with the basename prop
*/
export const AppRouter = ({ basename = '', children }: AppRouterProps) => {
export function AdminRouter({ basename = '', children }: AppRouterProps) {
const isInRouter = useInRouterContext();
const Router = isInRouter ? DummyRouter : InternalRouter;
@ -24,25 +24,16 @@ export const AppRouter = ({ basename = '', children }: AppRouterProps) => {
<Router basename={basename}>{children}</Router>
</BasenameContextProvider>
);
};
}
const DummyRouter = ({
children,
}: {
children: ReactNode;
basename?: string;
}) => <>{children}</>;
function DummyRouter({ children }: AppRouterProps) {
return <>{children}</>;
}
const InternalRouter = ({
children,
basename,
}: {
children: ReactNode;
basename?: string;
}) => {
function InternalRouter({ basename, children }: AppRouterProps) {
const router = createHashRouter(
[{ path: '*', element: <>{children}</> }],
{ basename },
{ basename }
);
return <RouterProvider router={router} />;
};
}

@ -0,0 +1,134 @@
import { CoreAdminContext, CoreAdminContextProps } from './CoreAdminContext';
import { CoreAdminUI, CoreAdminUIProps } from './CoreAdminUI';
export type CoreAdminProps = CoreAdminContextProps & CoreAdminUIProps;
/**
* Main admin component, entry point to the application.
*
* Initializes the various contexts (auth, data, i18n, router)
* and defines the main routes.
*
* Expects a list of resources as children, or a function returning a list of
* resources based on the permissions.
*
* @example
*
* // static list of resources
*
* import {
* CoreAdmin,
* Resource,
* ListGuesser,
* useDataProvider,
* } from 'ra-core';
*
* const App = () => (
* <CoreAdmin dataProvider={myDataProvider}>
* <Resource name="posts" list={ListGuesser} />
* </CoreAdmin>
* );
*
* // dynamic list of resources based on permissions
*
* import {
* CoreAdmin,
* Resource,
* ListGuesser,
* useDataProvider,
* } from 'ra-core';
*
* const App = () => (
* <CoreAdmin dataProvider={myDataProvider}>
* {permissions => [
* <Resource name="posts" key="posts" list={ListGuesser} />,
* ]}
* </CoreAdmin>
* );
*
* // If you have to build a dynamic list of resources using a side effect,
* // you can't use <CoreAdmin>. But as it delegates to sub components,
* // it's relatively straightforward to replace it:
*
* import * as React from 'react';
* import { useEffect, useState } from 'react';
* import {
* CoreAdminContext,
* CoreAdminUI,
* Resource,
* ListGuesser,
* useDataProvider,
* } from 'ra-core';
*
* const App = () => (
* <CoreAdminContext dataProvider={myDataProvider}>
* <UI />
* </CoreAdminContext>
* );
*
* const UI = () => {
* const [resources, setResources] = useState([]);
* const dataProvider = useDataProvider();
* useEffect(() => {
* dataProvider.introspect().then(r => setResources(r));
* }, []);
*
* return (
* <CoreAdminUI>
* {resources.map(resource => (
* <Resource name={resource.name} key={resource.key} list={ListGuesser} />
* ))}
* </CoreAdminUI>
* );
* };
*/
export const CoreAdmin = (props: CoreAdminProps) => {
const {
authCallbackPage,
authProvider,
basename,
catchAll,
children,
dashboard,
dataProvider,
disableTelemetry,
error,
i18nProvider,
initialLocation,
queryClient,
layout,
loading,
loginPage,
ready,
requireAuth,
store,
title = 'React Admin',
} = props;
return (
<CoreAdminContext
authProvider={authProvider}
basename={basename}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
queryClient={queryClient}
store={store}
>
<CoreAdminUI
authCallbackPage={authCallbackPage}
catchAll={catchAll}
dashboard={dashboard}
disableTelemetry={disableTelemetry}
error={error}
initialLocation={initialLocation}
layout={layout}
loading={loading}
loginPage={loginPage}
ready={ready}
requireAuth={requireAuth}
title={title}
>
{children}
</CoreAdminUI>
</CoreAdminContext>
);
};

@ -1,12 +1,28 @@
import { ComponentType, ReactElement } from "react";
import { Route, Routes } from "react-router-dom";
import { ErrorBoundary } from "../errorBoundary";
import { DefaultTitleContextProvider } from "../title";
import { CoreAdminRoutes, CoreAdminRoutesProps } from "./CoreAdminRoutes";
import { InAdminContext, useInAdminContext } from "./InAdminContext";
import { useInAppContext } from "./InAppContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode, useMemo } from "react";
import { AuthContext, AuthProvider } from "../auth";
import { DataProvider, DataProviderContext, defaultDataProvider } from "../data";
import { I18nContextProvider, I18nProvider } from "../i18n";
import { NotificationContextProvider } from "../notification";
import { defaultStore, Store, StoreContextProvider } from "../store";
import { AdminRouter } from "./AdminRouter";
export interface CoreAdminContextProps {
/**
* The authentication provider for security and permissions
*
* @see https://marmelab.com/react-admin/Authentication.html
* @example
* import authProvider from './authProvider';
*
* const App = () => (
* <Admin authProvider={authProvider}>
* ...
* </Admin>
* );
*/
authProvider?: AuthProvider;
export interface CoreAdminContextProps extends CoreAdminRoutesProps {
/**
* The base path for all URLs generated by react-admin.
*
@ -24,92 +40,126 @@ export interface CoreAdminContextProps extends CoreAdminRoutesProps {
* </BrowserRouter>
* );
*/
basepath?: string;
basename?: string;
children?: ReactNode;
/**
* The component displayed when an error is caught in a child component
* @see https://marmelab.com/react-admin/Admin.html#error
* The data provider used to communicate with the API
*
* @see https://marmelab.com/react-admin/DataProviders.html
* @example
* import { Admin } from 'react-admin';
* import { MyError } from './error';
* import simpleRestProvider from 'ra-data-simple-rest';
* const dataProvider = simpleRestProvider('http://path.to.my.api/');
*
* const App = () => (
* <Admin error={MyError}>
* <Admin dataProvider={dataProvider}>
* ...
* </Admin>
* );
*/
error?: ComponentType<any> | ReactElement | null;
dataProvider?: DataProvider;
/**
* The title of the error page
* @see https://marmelab.com/react-admin/Admin.html#title
* The internationalization provider for translations
*
* @see https://marmelab.com/react-admin/Translation.html
* @example
* // in src/i18nProvider.js
* import polyglotI18nProvider from 'ra-i18n-polyglot';
* import fr from 'ra-language-french';
*
* export const i18nProvider = polyglotI18nProvider(() => fr, 'fr');
*
* // in src/App.js
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { i18nProvider } from './i18nProvider';
*
* const App = () => (
* <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}>
* ...
* </Admin>
* );
*/
i18nProvider?: I18nProvider;
/**
* The react-query client
*
* @see https://marmelab.com/react-admin/Admin.html#queryclient
* @example
* import { Admin } from 'react-admin';
* import { QueryClient } from '@tanstack/react-query';
*
* const queryClient = new QueryClient({
* defaultOptions: {
* queries: {
* retry: false,
* structuralSharing: false,
* },
* mutations: {
* retryDelay: 10000,
* },
* },
* });
*
* const App = () => (
* <Admin title="My Admin" dataProvider={dataProvider}>
* ...
* </Admin>
* <Admin queryClient={queryClient} dataProvider={...}>
* ...
* </Admin>
* );
*/
title?: string | ReactElement | null;
queryClient?: QueryClient;
/**
* The adapter for storing user preferences
*
* @see https://marmelab.com/react-admin/Admin.html#store
* @example
* import { Admin, memoryStore } from 'react-admin';
*
* const App = () => (
* <Admin dataProvider={dataProvider} store={memoryStore()}>
* ...
* </Admin>
* );
*/
store?: Store;
}
export function CoreAdminContext(props: CoreAdminContextProps) {
const {
catchAll,
authProvider,
basename,
children,
dashboard,
error,
layout,
loading,
ready,
requireAuth,
title,
dataProvider = defaultDataProvider,
i18nProvider,
queryClient,
store = defaultStore,
} = props;
const inApp = useInAppContext();
const inAdmin = useInAdminContext();
if (!inApp) {
throw new Error(
"You've tried to access admin context outside <AppContext />.\n" +
"Please wrap your code with it first.\n"
);
}
if (inAdmin) {
throw new Error(
"You've tried to access admin context inside another <AdminContext />.\n" +
"Please unwrap your code with it first.\n"
);
}
const finalQueryClient = useMemo(
() => queryClient || new QueryClient(),
[queryClient]
);
return (
<DefaultTitleContextProvider value={title}>
<ErrorBoundary error={error}>
<InAdminContext>
<Routes>
<Route
path="/*"
element={
<CoreAdminRoutes
catchAll={catchAll}
dashboard={dashboard}
layout={layout}
loading={loading}
requireAuth={requireAuth}
ready={ready}
>
<AuthContext.Provider value={authProvider}>
<DataProviderContext.Provider value={dataProvider}>
<StoreContextProvider value={store}>
<QueryClientProvider client={finalQueryClient}>
<I18nContextProvider value={i18nProvider}>
<NotificationContextProvider>
<AdminRouter basename={basename}>
{children}
</CoreAdminRoutes>
}
/>
</Routes>
</InAdminContext>
</ErrorBoundary>
</DefaultTitleContextProvider>
</AdminRouter>
</NotificationContextProvider>
</I18nContextProvider>
</QueryClientProvider>
</StoreContextProvider>
</DataProviderContext.Provider>
</AuthContext.Provider>
);
}

@ -1,12 +1,27 @@
import { ComponentType, ReactElement, ReactNode, useEffect, useState } from 'react';
import { Navigate, Route, Routes, To } from 'react-router-dom';
import { LogoutOnMount, useCheckAuth } from '../auth';
import { CoreLayoutProps } from './types';
import { getReactElement } from '../util';
import {
ComponentType,
useEffect,
useState
} from 'react';
import {
Navigate,
Route,
Routes,
To
} from 'react-router-dom';
import {
AccessControlProvider,
LogoutOnMount,
useCheckAuth
} from '../auth';
import { useScrollToTop } from '../scrollPosition';
import { DefaultLayout } from './DefaultLayout';
import { HasDashboardContextProvider } from './HasDashboardContextProvider';
import { useConfigureRoutesFromChildren } from './useConfigureRoutesFromChildren';
import { InitialLocationContextProvider } from '../routing';
import {
AdminChildren,
CoreLayoutProps
} from './types';
import { useConfigureAdminRouterFromChildren } from './useConfigureAdminRouterFromChildren';
export interface CoreAdminRoutesProps {
/**
@ -39,9 +54,9 @@ export interface CoreAdminRoutesProps {
* </Admin>
* );
*/
catchAll?: ComponentType<any> | ReactElement | null;
catchAll?: ComponentType<any> | null;
children?: ReactNode;
children?: AdminChildren;
/**
* @example
@ -59,7 +74,7 @@ export interface CoreAdminRoutesProps {
* />
* )
*/
dashboard?: ComponentType | ReactElement | null;
dashboard?: ComponentType<any> | null;
initialLocation?: To;
@ -87,7 +102,7 @@ export interface CoreAdminRoutesProps {
/**
* The component displayed while fetching the auth provider if the admin child is an async function
*/
loading?: ComponentType<any> | ReactElement | null;
loading?: ComponentType<any> | null;
/**
* The page to display when the admin has no Resource children
@ -109,7 +124,7 @@ export interface CoreAdminRoutesProps {
* </Admin>
* );
*/
ready?: ComponentType<any> | ReactElement | null;
ready?: ComponentType<any> | null;
/**
* Flag to require authentication for all routes. Defaults to false.
@ -134,16 +149,22 @@ export interface CoreAdminRoutesProps {
}
export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
const [routes, status] = useConfigureRoutesFromChildren(props.children);
useScrollToTop();
const {
catchAll: catchAllElement,
dashboard: dashboardElement,
routesWithLayout,
routesWithoutLayout,
status,
} = useConfigureAdminRouterFromChildren(props.children);
const {
catchAll: CatchAll,
dashboard: Dashboard,
initialLocation,
layout: Layout = DefaultLayout,
loading: loadingElement,
ready: readyElement,
requireAuth = false,
loading: Loading,
requireAuth,
ready: Ready,
} = props;
const [onlyAnonymousRoutes, setOnlyAnonymousRoutes] = useState(requireAuth);
@ -166,23 +187,25 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
}, [checkAuth, requireAuth]);
if (status === 'empty') {
if (!readyElement) {
if (!Ready) {
throw new Error(
'The app is empty. Please provide an empty component, ' +
'or pass Resource or CustomRoutes as children.'
'The admin is empty. Please provide an empty component, ' +
'or pass Route or CustomRoutes as children.'
);
}
return getReactElement(readyElement);
return <Ready />;
}
if (status === 'loading' || checkAuthLoading) {
return (
<Routes>
{/* Render the routes that were outside the child function. */}
{routesWithoutLayout}
<Route
path="*"
element={
<div style={{ height: '100vh' }}>
{loadingElement ? getReactElement(loadingElement) : 'loading...'}
{Loading ? <Loading /> : null}
</div>
}
/>
@ -193,6 +216,7 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
if (onlyAnonymousRoutes) {
return (
<Routes>
{routesWithoutLayout}
<Route
path="*"
element={<LogoutOnMount />}
@ -203,37 +227,31 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
return (
<Routes>
{routesWithoutLayout}
<Route
path="/*"
element={
<HasDashboardContextProvider value={!!dashboardElement}>
<InitialLocationContextProvider
location={initialLocation}
target="admin"
>
<HasDashboardContextProvider value={!!Dashboard}>
<AccessControlProvider>
<Layout>
<Routes>
{routes}
{dashboardElement ? (
<Route
index
element={
dashboardElement
? (getReactElement(dashboardElement))
: initialLocation
? (<Navigate to={initialLocation} />)
: null
}
/>
) : null}
{catchAllElement ? (
{routesWithLayout}
{Dashboard
? (<Route path="/" element={<Dashboard />} />)
: (initialLocation && initialLocation !== "/")
? (<Route path="/" element={<Navigate to={initialLocation} />} />)
: null}
{CatchAll ? (
<Route
path="*"
element={getReactElement(catchAllElement)} />
element={<CatchAll />}
/>
) : null}
</Routes>
</Layout>
</InitialLocationContextProvider>
</AccessControlProvider>
</HasDashboardContextProvider>
}
/>

@ -0,0 +1,293 @@
import { ComponentType, ReactElement } from "react";
import { Route, Routes, To } from "react-router-dom";
import { CoreAdminRoutes } from "./CoreAdminRoutes";
import { DefaultError } from "./DefaultError";
import { DefaultLayout } from "./DefaultLayout";
import { DefaultTitleContextProvider } from "./DefaultTitleContextProvider";
import { ErrorBoundary } from "./ErrorBoundary";
import { AdminChildren, CoreLayoutProps } from "./types";
import { DefaultReady } from "./DefaultReady";
export interface CoreAdminUIProps {
/**
* The content displayed when the user visits the /auth-callback page, used for redirection by third-party authentication providers
*
* @see https://marmelab.com/react-admin/Admin.html#authcallbackpage
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { authProvider } from './authProvider';
* import MyAuthCallbackPage from './MyAuthCallbackPage';
*
* const App = () => (
* <Admin
* authCallbackPage={MyAuthCallbackPage}
* authProvider={authProvider}
* dataProvider={dataProvider}
* >
* ...
* </Admin>
* );
*/
authCallbackPage?: ComponentType<any> | null;
/**
* A catch-all react component to display when the URL does not match any
*
* @see https://marmelab.com/react-admin/Admin.html#catchall
* @example
* // in src/NotFound.js
* import Card from '@mui/material/Card';
* import CardContent from '@mui/material/CardContent';
* import { Title } from 'react-admin';
*
* export const NotFound = () => (
* <Card>
* <Title title="Not Found" />
* <CardContent>
* <h1>404: Page not found</h1>
* </CardContent>
* </Card>
* );
*
* // in src/App.js
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { NotFound } from './NotFound';
*
* const App = () => (
* <Admin catchAll={NotFound} dataProvider={dataProvider}>
* ...
* </Admin>
* );
*/
catchAll?: ComponentType<any> | null;
children?: AdminChildren;
/**
* The component to use for the dashboard page (displayed on the `/` route).
*
* @see https://marmelab.com/react-admin/Admin.html#dashboard
* @example
* import { Admin } from 'react-admin';
* import Dashboard from './Dashboard';
* import { dataProvider } from './dataProvider';
*
* const App = () => (
* <Admin dashboard={Dashboard} dataProvider={dataProvider}>
* ...
* </Admin>
* );
*/
dashboard?: ComponentType | null;
/**
* Set to true to disable anonymous telemetry collection
*
* @see https://marmelab.com/react-admin/Admin.html#disabletelemetry
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
*
* const App = () => (
* <Admin disableTelemetry dataProvider={dataProvider}>
* ...
* </Admin>
* );
*/
disableTelemetry?: boolean;
/**
* The component displayed when an error is caught in a child component
* @see https://marmelab.com/react-admin/Admin.html#error
* @example
* import { Admin } from 'react-admin';
* import { MyError } from './error';
*
* const App = () => (
* <Admin error={MyError}>
* ...
* </Admin>
* );
*/
error?: ComponentType<any> | null;
initialLocation?: To;
/**
* The main app layout component
*
* @see https://marmelab.com/react-admin/Admin.html#layout
* @example
* import { Admin, Layout } from 'react-admin';
*
* const MyLayout = ({ children }) => (
* <Layout appBarAlwaysOn>
* {children}
* </Layout>
* );
*
* export const App = () => (
* <Admin dataProvider={dataProvider} layout={MyLayout}>
* ...
* </Admin>
* );
*/
layout?: ComponentType<CoreLayoutProps>;
/**
* The component displayed while fetching the auth provider if the admin child is an async function
*/
loading?: ComponentType<any> | null;
/**
* The component displayed when the user visits the /login page
* @see https://marmelab.com/react-admin/Admin.html#loginpage
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { authProvider } from './authProvider';
* import MyLoginPage from './MyLoginPage';
*
* const App = () => (
* <Admin
* loginPage={MyLoginPage}
* authProvider={authProvider}
* dataProvider={dataProvider}
* >
* ...
* </Admin>
* );
*/
loginPage?: ComponentType<any> | null;
/**
* The page to display when the admin has no Resource children
*
* @see https://marmelab.com/react-admin/Admin.html#ready
* @example
* import { Admin } from 'react-admin';
*
* const Ready = () => (
* <div>
* <h1>Admin ready</h1>
* <p>You can now add resources</p>
* </div>
* )
*
* const App = () => (
* <Admin ready={Ready}>
* ...
* </Admin>
* );
*/
ready?: ComponentType<any> | null;
/**
* Flag to require authentication for all routes. Defaults to false.
*
* @see https://marmelab.com/react-admin/Admin.html#requireauth
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { authProvider } from './authProvider';
*
* const App = () => (
* <Admin
* requireAuth
* authProvider={authProvider}
* dataProvider={dataProvider}
* >
* ...
* </Admin>
* );
*/
requireAuth?: boolean;
/**
* The title of the error page
* @see https://marmelab.com/react-admin/Admin.html#title
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
*
* const App = () => (
* <Admin title="My Admin" dataProvider={dataProvider}>
* ...
* </Admin>
* );
*/
title?: string | ReactElement | null;
}
export const CoreAdminUI = (props: CoreAdminUIProps) => {
const {
authCallbackPage: AuthCallbackPage,
catchAll,
children,
dashboard,
// disableTelemetry = false,
error = DefaultError,
initialLocation,
layout = DefaultLayout,
loading,
loginPage: LoginPage,
ready = DefaultReady,
requireAuth = false,
title = 'React Admin',
} = props;
// useEffect(() => {
// if (
// disableTelemetry ||
// process.env.NODE_ENV !== 'production' ||
// typeof window === 'undefined' ||
// typeof window.location === 'undefined' ||
// typeof Image === 'undefined'
// ) {
// return;
// }
// const img = new Image();
// img.src = `https://react-admin-telemetry.marmelab.com/react-admin-telemetry?domain=${window.location.hostname}`;
// }, [disableTelemetry]);
return (
<DefaultTitleContextProvider value={title}>
<ErrorBoundary error={error}>
<Routes>
{LoginPage != null ? (
<Route
path="/login"
element={<LoginPage />}
/>
) : null}
{AuthCallbackPage != null ? (
<Route
path="/auth-callback"
element={<AuthCallbackPage />}
/>
) : null}
<Route
path="/*"
element={
<CoreAdminRoutes
catchAll={catchAll}
dashboard={dashboard}
initialLocation={initialLocation}
layout={layout}
loading={loading}
ready={ready}
requireAuth={requireAuth}
>
{children}
</CoreAdminRoutes>
}
/>
</Routes>
</ErrorBoundary>
</DefaultTitleContextProvider>
);
};

@ -1,271 +0,0 @@
import { ComponentType, ReactElement, useMemo } from "react";
import { Route as ReactRoute, Routes } from "react-router-dom";
import { CoreAppRoutes, CoreAppRoutesProps } from "./CoreAppRoutes";
import { defaultStore, Store, StoreContextProvider } from "../store";
import { AuthContext, AuthProvider } from "../auth";
import { I18nContextProvider, I18nProvider } from "../i18n";
import { NotificationContextProvider } from "../notification";
import { AppRouter } from "../routing";
import { DefaultTitleContextProvider } from "../title";
import { ErrorBoundary } from "../errorBoundary";
import { DataProvider, DataProviderContext } from "../data";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export interface CoreAppContextProps extends CoreAppRoutesProps {
/**
* The authentication provider for security and permissions
*
* @see https://marmelab.com/react-admin/Authentication.html
* @example
* import authProvider from './authProvider';
*
* const App = () => (
* <Admin authProvider={authProvider}>
* ...
* </Admin>
* );
*/
authProvider?: AuthProvider;
/**
* The content displayed when the user visits the /auth-callback page, used for redirection by third-party authentication providers
*
* @see https://marmelab.com/react-admin/Admin.html#authcallbackpage
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { authProvider } from './authProvider';
* import MyAuthCallbackPage from './MyAuthCallbackPage';
*
* const App = () => (
* <Admin
* authCallbackPage={MyAuthCallbackPage}
* authProvider={authProvider}
* dataProvider={dataProvider}
* >
* ...
* </Admin>
* );
*/
authCallbackPage?: ComponentType<any> | null;
/**
* The base path for all URLs generated by react-admin.
*
* @see https://marmelab.com/react-admin/Admin.html#using-react-admin-in-a-sub-path
* @example
* import { Admin } from 'react-admin';
* import { BrowserRouter } from 'react-router-dom';
* import { dataProvider } from './dataProvider';
*
* const App = () => (
* <BrowserRouter>
* <Admin basename="/admin" dataProvider={dataProvider}>
* ...
* </Admin>
* </BrowserRouter>
* );
*/
basename?: string;
dataProvider?: DataProvider;
/**
* The component displayed when an error is caught in a child component
* @see https://marmelab.com/react-admin/Admin.html#error
* @example
* import { Admin } from 'react-admin';
* import { MyError } from './error';
*
* const App = () => (
* <Admin error={MyError}>
* ...
* </Admin>
* );
*/
error?: ComponentType<any> | ReactElement | null;
/**
* The internationalization provider for translations
*
* @see https://marmelab.com/react-admin/Translation.html
* @example
* // in src/i18nProvider.js
* import polyglotI18nProvider from 'ra-i18n-polyglot';
* import fr from 'ra-language-french';
*
* export const i18nProvider = polyglotI18nProvider(() => fr, 'fr');
*
* // in src/App.js
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { i18nProvider } from './i18nProvider';
*
* const App = () => (
* <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}>
* ...
* </Admin>
* );
*/
i18nProvider?: I18nProvider;
/**
* The component displayed when the user visits the /login page
* @see https://marmelab.com/react-admin/Admin.html#loginpage
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { authProvider } from './authProvider';
* import MyLoginPage from './MyLoginPage';
*
* const App = () => (
* <Admin
* loginPage={MyLoginPage}
* authProvider={authProvider}
* dataProvider={dataProvider}
* >
* ...
* </Admin>
* );
*/
loginPage?: ComponentType<any> | null;
/**
* The react-query client
*
* @see https://marmelab.com/react-admin/Admin.html#queryclient
* @example
* import { Admin } from 'react-admin';
* import { QueryClient } from '@tanstack/react-query';
*
* const queryClient = new QueryClient({
* defaultOptions: {
* queries: {
* retry: false,
* structuralSharing: false,
* },
* mutations: {
* retryDelay: 10000,
* },
* },
* });
*
* const App = () => (
* <Admin queryClient={queryClient} dataProvider={...}>
* ...
* </Admin>
* );
*/
queryClient?: QueryClient;
/**
* The adapter for storing user preferences
*
* @see https://marmelab.com/react-admin/Admin.html#store
* @example
* import { Admin, memoryStore } from 'react-admin';
*
* const App = () => (
* <Admin dataProvider={dataProvider} store={memoryStore()}>
* ...
* </Admin>
* );
*/
store?: Store;
/**
* The title of the error page
* @see https://marmelab.com/react-admin/Admin.html#title
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
*
* const App = () => (
* <Admin title="My Admin" dataProvider={dataProvider}>
* ...
* </Admin>
* );
*/
title?: string | ReactElement | null;
}
export function CoreAppContext(props: CoreAppContextProps) {
const {
authProvider,
authCallbackPage: LoginCallbackPage,
basename,
catchAll,
children,
dashboard,
dataProvider,
error,
i18nProvider,
initialLocation,
layout,
loading,
loginPage: LoginPage,
queryClient,
ready,
requireAuth,
store = defaultStore,
title,
} = props;
const finalQueryClient = useMemo(
() => queryClient || new QueryClient(),
[queryClient]
);
return (
<AuthContext.Provider value={authProvider}>
<DataProviderContext.Provider value={dataProvider}>
<I18nContextProvider value={i18nProvider}>
<NotificationContextProvider>
<StoreContextProvider value={store}>
<QueryClientProvider client={finalQueryClient}>
<AppRouter basename={basename}>
<DefaultTitleContextProvider value={title}>
<ErrorBoundary error={error}>
<Routes>
{LoginPage != null ? (
<ReactRoute
path="/login"
element={<LoginPage />}
/>
) : null}
{LoginCallbackPage != null ? (
<ReactRoute
path="/auth-callback"
element={<LoginCallbackPage />}
/>
) : null}
<ReactRoute
path="/*"
element={
<CoreAppRoutes
catchAll={catchAll}
dashboard={dashboard}
initialLocation={initialLocation}
layout={layout}
loading={loading}
requireAuth={requireAuth}
ready={ready}
>
{children}
</CoreAppRoutes>
}
/>
</Routes>
</ErrorBoundary>
</DefaultTitleContextProvider>
</AppRouter>
</QueryClientProvider>
</StoreContextProvider>
</NotificationContextProvider>
</I18nContextProvider>
</DataProviderContext.Provider>
</AuthContext.Provider>
);
}

@ -1,277 +0,0 @@
import {
ComponentType,
ReactElement,
ReactNode,
useEffect,
useState
} from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { LogoutOnMount, useCheckAuth } from '../auth';
import { InitialLocationContextProvider } from '../routing';
import { getReactElement } from '../util';
import { DefaultLayout } from './DefaultLayout';
import { CoreLayoutProps } from './types';
import { useConfigureRoutesFromChildren } from './useConfigureRoutesFromChildren';
import { useScrollToTop } from '../scrollPosition';
import { HasDashboardContextProvider } from './HasDashboardContextProvider';
export interface CoreAppRoutesProps {
/**
* A catch-all react component to display when the URL does not match any
*
* @see https://marmelab.com/react-admin/Admin.html#catchall
* @example
* // in src/NotFound.js
* import Card from '@mui/material/Card';
* import CardContent from '@mui/material/CardContent';
* import { Title } from 'react-admin';
*
* export const NotFound = () => (
* <Card>
* <Title title="Not Found" />
* <CardContent>
* <h1>404: Page not found</h1>
* </CardContent>
* </Card>
* );
*
* // in src/App.js
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { NotFound } from './NotFound';
*
* const App = () => (
* <Admin catchAll={NotFound} dataProvider={dataProvider}>
* ...
* </Admin>
* );
*/
catchAll?: ComponentType<any> | ReactElement | null;
children?: ReactNode;
/**
* @example
* import { WithPermissions } from "rwas";
* import HomepageView from "./views/Homepage";
*
* const authParams = {
* params: { route: 'homepage' },
* };
*
* const firstpage = (
* <WithPermissions
* authParams={authParams}
* component={HomepageView}
* />
* )
*/
dashboard?: ComponentType | ReactElement | null;
/**
* @example
* import { WithPermissions } from "rwas";
* import HomepageView from "./views/Homepage";
*
* const authParams = {
* params: { route: 'homepage' },
* };
*
* const firstpage = (
* <WithPermissions
* authParams={authParams}
* component={HomepageView}
* />
* )
*/
homepage?: ComponentType<any> | ReactElement | null;
initialLocation?: string;
/**
* The main app layout component
*
* @see https://marmelab.com/react-admin/Admin.html#layout
* @example
* import { Admin, Layout } from 'react-admin';
*
* const MyLayout = ({ children }) => (
* <Layout appBarAlwaysOn>
* {children}
* </Layout>
* );
*
* export const App = () => (
* <Admin dataProvider={dataProvider} layout={MyLayout}>
* ...
* </Admin>
* );
*/
layout?: ComponentType<CoreLayoutProps>;
/**
* The component displayed while fetching the auth provider if the admin child is an async function
*/
loading?: ComponentType<any> | ReactElement | null;
/**
* The page to display when the admin has no Resource children
*
* @see https://marmelab.com/react-admin/Admin.html#ready
* @example
* import { Admin } from 'react-admin';
*
* const Ready = () => (
* <div>
* <h1>Admin ready</h1>
* <p>You can now add resources</p>
* </div>
* )
*
* const App = () => (
* <Admin ready={Ready}>
* ...
* </Admin>
* );
*/
ready?: ComponentType<any> | ReactElement | null;
/**
* Flag to require authentication for all routes. Defaults to false.
*
* @see https://marmelab.com/react-admin/Admin.html#requireauth
* @example
* import { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider';
* import { authProvider } from './authProvider';
*
* const App = () => (
* <Admin
* requireAuth
* authProvider={authProvider}
* dataProvider={dataProvider}
* >
* ...
* </Admin>
* );
*/
requireAuth?: boolean;
}
export function CoreAppRoutes(props: CoreAppRoutesProps) {
useScrollToTop();
const [routes, status] = useConfigureRoutesFromChildren(props.children);
const {
catchAll: catchAllElement,
dashboard,
homepage: homepageElement,
initialLocation,
layout: Layout = DefaultLayout,
loading: loadingElement,
requireAuth,
ready: readyElement,
} = props;
const [onlyAnonymousRoutes, setOnlyAnonymousRoutes] = useState(requireAuth);
const [checkAuthLoading, setCheckAuthLoading] = useState(requireAuth);
const checkAuth = useCheckAuth();
useEffect(() => {
if (requireAuth) {
// do not log the user out on failure to allow access to custom routes with no layout
// for other routes, the LogoutOnMount component will log the user out
checkAuth(undefined, false)
.then(() => {
setOnlyAnonymousRoutes(false);
})
.catch(() => { })
.finally(() => {
setCheckAuthLoading(false);
});
}
}, [checkAuth, requireAuth]);
if (status === 'empty') {
if (!readyElement) {
throw new Error(
'The admin is empty. Please provide an empty component, ' +
'or pass Route or CustomRoutes as children.'
);
}
return getReactElement(readyElement);
}
if (status === 'loading' || checkAuthLoading) {
return (
<Routes>
<Route
path="*"
element={
<div style={{ height: '100vh' }}>
{loadingElement ? getReactElement(loadingElement) : 'loading...'}
</div>
}
/>
</Routes>
);
}
if (onlyAnonymousRoutes) {
return (
<Routes>
<Route
path="*"
element={<LogoutOnMount />}
/>
</Routes>
);
}
return (
<InitialLocationContextProvider
location={initialLocation}
target="app"
>
<Routes>
<Route
path="/*"
element={
<HasDashboardContextProvider value={!!dashboard}>
<Layout>
<Routes>
{routes}
<Route
path="/"
element={
homepageElement
? (getReactElement(homepageElement))
: initialLocation
? (<Navigate to={initialLocation} />)
: null
}
/>
{dashboard
? (<Route path="/" element={getReactElement(dashboard)} />)
: (initialLocation && initialLocation !== "/")
? (<Route path="/" element={<Navigate to={initialLocation} />} />)
: null}
{catchAllElement ? (
<Route
path="*"
element={getReactElement(catchAllElement)}
/>
) : null}
</Routes>
</Layout>
</HasDashboardContextProvider>
}
/>
</Routes>
</InitialLocationContextProvider>
);
}

@ -0,0 +1,18 @@
import { useErrorContext } from "./useErrorContext";
import { useResetErrorBoundaryOnLocationChange } from "./useResetErrorBoundaryOnLocationChange";
export function DefaultError() {
const { error, errorInfo, resetErrorBoundary } = useErrorContext();
useResetErrorBoundaryOnLocationChange(resetErrorBoundary);
return (
<div>
<h1>Error</h1>
<pre>
{error.message}
{errorInfo?.componentStack}
</pre>
</div>
);
}

@ -0,0 +1,111 @@
import { useState } from 'react';
const styles = {
root: {
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column' as 'column',
fontFamily: '"Roboto", sans-serif',
},
main: {
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center' as 'center',
flexDirection: 'column' as 'column',
background: 'linear-gradient(135deg, #00023b 0%, #00023b 50%, #313264 100%)',
color: 'white',
fontSize: '1.5em',
fontWeight: 'bold' as 'bold',
},
secondary: {
height: '20vh',
background: '#e8e8e8',
color: 'black',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-evenly',
},
link: {
textAlign: 'center' as 'center',
width: 150,
display: 'block',
textDecoration: 'none',
color: 'black',
opacity: 0.7,
},
linkHovered: {
opacity: 1,
},
image: {
width: 50,
},
logo: {
height: 100,
},
};
const Button = ({ img, label }: {
img: string;
label: string;
href: string
}) => {
const [hovered, setHovered] = useState(false);
return (
<div>
<a
href="#"
style={
hovered
? { ...styles.link, ...styles.linkHovered }
: styles.link
}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<img src={img} alt={label} style={styles.image} />
<br />
{label}
</a>
</div>
);
};
export function DefaultReady() {
if (process.env.NODE_ENV === 'production') {
return <span />
}
return (
<div style={styles.root}>
<div style={styles.main}>
<img
style={styles.logo}
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhbHF1ZV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTMxIDEzMSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTMxIDEzMTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMyMjI0NTg7fQoJLnN0MXtmaWxsOiM1MTUzN0Q7fQoJLnN0MntmaWxsOiNBM0E0Qjk7fQoJLnN0M3tmaWxsOiMwMDAyM0I7fQoJLnN0NHtmaWxsOiNGRkZGRkY7fQoJLnN0NXtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO30KPC9zdHlsZT4KPHRpdGxlPkxvZ29fc29tYnJlX2FpPC90aXRsZT4KPGcgaWQ9IlJlY3RhbmdsZV81NiI+Cgk8Zz4KCQk8cmVjdCB4PSIxOS4xIiB5PSIxOSIgdHJhbnNmb3JtPSJtYXRyaXgoMC41IC0wLjg2NiAwLjg2NiAwLjUgLTIzLjkyMjYgODkuNTQ2KSIgY2xhc3M9InN0MCIgd2lkdGg9IjkyLjkiIGhlaWdodD0iOTIuOSIvPgoJCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik04MywxMzAuM0wwLjgsODIuOUw0OC4yLDAuN2w4Mi4yLDQ3LjVMODMsMTMwLjN6IE0zLjUsODIuMWw3OC43LDQ1LjVsNDUuNS03OC43TDQ5LDMuNEwzLjUsODIuMXoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iUmVjdGFuZ2xlXzU2LTIiPgoJPGc+CgkJPHJlY3QgeD0iMTkiIHk9IjE5LjEiIHRyYW5zZm9ybT0ibWF0cml4KDAuODY2IC0wLjUgMC41IDAuODY2IC0yMy45Nzc3IDQxLjUyNykiIGNsYXNzPSJzdDAiIHdpZHRoPSI5Mi45IiBoZWlnaHQ9IjkyLjkiLz4KCQk8cGF0aCBjbGFzcz0ic3QyIiBkPSJNNDcuOSwxMzFMMCw0OEw4My4xLDBsNDgsODMuMUw0Ny45LDEzMXogTTQuMSw0OS4xbDQ1LDc3LjlsNzcuOS00NUw4Miw0LjFMNC4xLDQ5LjF6Ii8+Cgk8L2c+CjwvZz4KPGcgaWQ9IlJlY3RhbmdsZV81Ni0zIj4KCTxnPgoJCTxyZWN0IHg9IjE5LjEiIHk9IjE5IiBjbGFzcz0ic3QzIiB3aWR0aD0iOTIuOSIgaGVpZ2h0PSI5Mi45Ii8+CgkJPHBhdGggY2xhc3M9InN0NCIgZD0iTTExNC41LDExNC41SDE2LjZWMTYuNWg5Ny45VjExNC41eiBNMjEuNiwxMDkuNWg4Ny45VjIxLjVIMjEuNlYxMDkuNXoiLz4KCTwvZz4KPC9nPgo8ZyBpZD0iUmEiPgoJPGcgY2xhc3M9InN0NSI+CgkJPHBhdGggY2xhc3M9InN0NCIgZD0iTTU5LDg2LjdsLTYuNy0xOS4yaC0xLjJIMzguOXYxOS4yaC01LjZWMzguNWgxOC41YzMuNiwwLDYuMywwLjYsOC4xLDEuOGMxLjgsMS4yLDMsMi44LDMuNSw0LjgKCQkJYzAuNSwyLDAuOCw0LjYsMC44LDcuOGMwLDMuNS0wLjQsNi40LTEuMyw4LjdjLTAuOCwyLjMtMi42LDMuOS01LjMsNC44TDY1LDg2LjdINTl6IE01NS43LDYxLjZjMS4yLTAuNywyLTEuNywyLjQtMwoJCQljMC40LTEuMywwLjYtMy4yLDAuNi01LjZjMC0yLjUtMC4yLTQuMy0wLjUtNS42Yy0wLjMtMS4zLTEuMS0yLjItMi4zLTIuOWMtMS4yLTAuNy0zLTEtNS41LTFIMzguOXYxOS4xSDUwCgkJCUM1Mi41LDYyLjYsNTQuNCw2Mi4zLDU1LjcsNjEuNnoiLz4KCQk8cGF0aCBjbGFzcz0ic3Q0IiBkPSJNNzQuMyw4NWMtMS42LTEuNS0yLjUtNC4yLTIuNS04LjJjMC0yLjcsMC4zLTQuOCwwLjktNi4zYzAuNi0xLjUsMS42LTIuNiwzLTMuM2MxLjQtMC43LDMuNC0xLDYtMQoJCQljMS4zLDAsNS4xLDAuMSwxMS40LDAuM3YtMi40YzAtMi45LTAuMi01LTAuNy02LjJjLTAuNS0xLjItMS4zLTItMi42LTIuNGMtMS4yLTAuMy0zLjMtMC41LTYuMy0wLjVjLTEuMywwLTMsMC4xLTQuOSwwLjIKCQkJYy0yLDAuMS0zLjYsMC4zLTQuOCwwLjV2LTQuM2MzLjMtMC43LDcuMS0xLDExLjQtMWMzLjcsMCw2LjUsMC40LDguNCwxLjJjMS44LDAuOCwzLjEsMi4yLDMuOCw0LjFjMC43LDEuOSwxLDQuNywxLDguNHYyMi41aC00LjgKCQkJbC0wLjMtNWgtMC4zYy0wLjgsMi4yLTIuMiwzLjctNC4xLDQuNGMtMS45LDAuNy00LjEsMS4xLTYuNiwxLjFDNzguNiw4Ny4yLDc2LDg2LjUsNzQuMyw4NXogTTg5LjEsODJjMS4yLTAuNCwyLjItMS4yLDIuOC0yLjQKCQkJYzAuOS0xLjgsMS4zLTQuMywxLjMtNy4zdi0yaC0xMGMtMS43LDAtMywwLjItMy44LDAuNWMtMC44LDAuMy0xLjQsMC45LTEuNywxLjhjLTAuMywwLjktMC41LDIuMi0wLjUsNGMwLDEuOCwwLjIsMy4xLDAuNiwzLjkKCQkJYzAuNCwwLjgsMS4xLDEuNCwyLDEuOGMxLDAuMywyLjUsMC41LDQuNSwwLjVDODYuMiw4Mi42LDg3LjgsODIuNCw4OS4xLDgyeiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo="
alt="react-admin logo" />
<h1>Welcome to Rakit</h1>
<div>
Your application is properly configured.
<br />
Now you can add a &lt;Route&gt; as child of
&lt;Admin&gt;.
</div>
</div>
<div style={styles.secondary}>
<Button
href="https://marmelab.com/react-admin/documentation.html"
img="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IgoJIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNCAyNCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnIGlkPSJCb3VuZGluZ19Cb3giPgoJPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+CjwvZz4KPGcgaWQ9IkZsYXQiPgoJPGcgaWQ9InVpX3g1Rl9zcGVjX3g1Rl9oZWFkZXJfY29weV8yIj4KCTwvZz4KCTxnPgoJCTxwYXRoIGQ9Ik0yMSw1Yy0xLjExLTAuMzUtMi4zMy0wLjUtMy41LTAuNWMtMS45NSwwLTQuMDUsMC40LTUuNSwxLjVjLTEuNDUtMS4xLTMuNTUtMS41LTUuNS0xLjVTMi40NSw0LjksMSw2djE0LjY1CgkJCWMwLDAuMjUsMC4yNSwwLjUsMC41LDAuNWMwLjEsMCwwLjE1LTAuMDUsMC4yNS0wLjA1QzMuMSwyMC40NSw1LjA1LDIwLDYuNSwyMGMxLjk1LDAsNC4wNSwwLjQsNS41LDEuNWMxLjM1LTAuODUsMy44LTEuNSw1LjUtMS41CgkJCWMxLjY1LDAsMy4zNSwwLjMsNC43NSwxLjA1YzAuMSwwLjA1LDAuMTUsMC4wNSwwLjI1LDAuMDVjMC4yNSwwLDAuNS0wLjI1LDAuNS0wLjVWNkMyMi40LDUuNTUsMjEuNzUsNS4yNSwyMSw1eiBNMywxOC41VjcKCQkJYzEuMS0wLjM1LDIuMy0wLjUsMy41LTAuNWMxLjM0LDAsMy4xMywwLjQxLDQuNSwwLjk5djExLjVDOS42MywxOC40MSw3Ljg0LDE4LDYuNSwxOEM1LjMsMTgsNC4xLDE4LjE1LDMsMTguNXogTTIxLDE4LjUKCQkJYy0xLjEtMC4zNS0yLjMtMC41LTMuNS0wLjVjLTEuMzQsMC0zLjEzLDAuNDEtNC41LDAuOTlWNy40OWMxLjM3LTAuNTksMy4xNi0wLjk5LDQuNS0wLjk5YzEuMiwwLDIuNCwwLjE1LDMuNSwwLjVWMTguNXoiLz4KCQk8cGF0aCBvcGFjaXR5PSIwLjMiIGQ9Ik0xMSw3LjQ5QzkuNjMsNi45MSw3Ljg0LDYuNSw2LjUsNi41QzUuMyw2LjUsNC4xLDYuNjUsMyw3djExLjVDNC4xLDE4LjE1LDUuMywxOCw2LjUsMTgKCQkJYzEuMzQsMCwzLjEzLDAuNDEsNC41LDAuOTlWNy40OXoiLz4KCTwvZz4KCTxnPgoJCTxwYXRoIGQ9Ik0xNy41LDEwLjVjMC44OCwwLDEuNzMsMC4wOSwyLjUsMC4yNlY5LjI0QzE5LjIxLDkuMDksMTguMzYsOSwxNy41LDljLTEuMjgsMC0yLjQ2LDAuMTYtMy41LDAuNDd2MS41NwoJCQlDMTQuOTksMTAuNjksMTYuMTgsMTAuNSwxNy41LDEwLjV6Ii8+CgkJPHBhdGggZD0iTTE3LjUsMTMuMTZjMC44OCwwLDEuNzMsMC4wOSwyLjUsMC4yNlYxMS45Yy0wLjc5LTAuMTUtMS42NC0wLjI0LTIuNS0wLjI0Yy0xLjI4LDAtMi40NiwwLjE2LTMuNSwwLjQ3djEuNTcKCQkJQzE0Ljk5LDEzLjM2LDE2LjE4LDEzLjE2LDE3LjUsMTMuMTZ6Ii8+CgkJPHBhdGggZD0iTTE3LjUsMTUuODNjMC44OCwwLDEuNzMsMC4wOSwyLjUsMC4yNnYtMS41MmMtMC43OS0wLjE1LTEuNjQtMC4yNC0yLjUtMC4yNGMtMS4yOCwwLTIuNDYsMC4xNi0zLjUsMC40N3YxLjU3CgkJCUMxNC45OSwxNi4wMiwxNi4xOCwxNS44MywxNy41LDE1LjgzeiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo="
label="Documentation" />
<Button
href="https://github.com/marmelab/react-admin/tree/master/examples"
img="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJub25lIiBkPSJNMCAwaDI0djI0SDBWMHoiLz48cGF0aCBkPSJNOS40IDE2LjZMNC44IDEybDQuNi00LjZMOCA2bC02IDYgNiA2IDEuNC0xLjR6bTUuMiAwbDQuNi00LjYtNC42LTQuNkwxNiA2bDYgNi02IDYtMS40LTEuNHoiLz48L3N2Zz4="
label="Examples" />
<Button
href="https://stackoverflow.com/questions/tagged/react-admin"
img="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IgoJIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyNCAyNCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnIGlkPSJCb3VuZGluZ19Cb3giPgoJPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+CjwvZz4KPGcgaWQ9IkZsYXQiPgoJPGcgaWQ9InVpX3g1Rl9zcGVjX3g1Rl9oZWFkZXJfY29weV8yIj4KCTwvZz4KCTxnPgoJCTxjaXJjbGUgb3BhY2l0eT0iMC4zIiBjeD0iOSIgY3k9IjgiIHI9IjIiLz4KCQk8cGF0aCBvcGFjaXR5PSIwLjMiIGQ9Ik05LDE1Yy0yLjcsMC01LjgsMS4yOS02LDIuMDFMMywxOGgxMnYtMUMxNC44LDE2LjI5LDExLjcsMTUsOSwxNXoiLz4KCQk8cGF0aCBkPSJNMTYuNjcsMTMuMTNDMTguMDQsMTQuMDYsMTksMTUuMzIsMTksMTd2M2g0di0zQzIzLDE0LjgyLDE5LjQzLDEzLjUzLDE2LjY3LDEzLjEzeiIvPgoJCTxwYXRoIGQ9Ik0xNSwxMmMyLjIxLDAsNC0xLjc5LDQtNGMwLTIuMjEtMS43OS00LTQtNGMtMC40NywwLTAuOTEsMC4xLTEuMzMsMC4yNEMxNC41LDUuMjcsMTUsNi41OCwxNSw4cy0wLjUsMi43My0xLjMzLDMuNzYKCQkJQzE0LjA5LDExLjksMTQuNTMsMTIsMTUsMTJ6Ii8+CgkJPHBhdGggZD0iTTksMTJjMi4yMSwwLDQtMS43OSw0LTRjMC0yLjIxLTEuNzktNC00LTRTNSw1Ljc5LDUsOEM1LDEwLjIxLDYuNzksMTIsOSwxMnogTTksNmMxLjEsMCwyLDAuOSwyLDJjMCwxLjEtMC45LDItMiwyCgkJCVM3LDkuMSw3LDhDNyw2LjksNy45LDYsOSw2eiIvPgoJCTxwYXRoIGQ9Ik05LDEzYy0yLjY3LDAtOCwxLjM0LTgsNHYzaDE2di0zQzE3LDE0LjM0LDExLjY3LDEzLDksMTN6IE0xNSwxOEgzbDAtMC45OUMzLjIsMTYuMjksNi4zLDE1LDksMTVzNS44LDEuMjksNiwyVjE4eiIvPgoJPC9nPgo8L2c+Cjwvc3ZnPgo="
label="Community" />
</div>
</div>
);
};

@ -1,3 +1,6 @@
import { createContext, ReactElement } from "react";
export const DefaultTitleContext = createContext<string | ReactElement>('React WebApp Scaffold');
/**
* @private
*/
export const DefaultTitleContext = createContext<string | ReactElement>('Rakit');

@ -0,0 +1,7 @@
import { createContext } from "react";
import { ErrorContextValue } from "./types";
/**
* @private
*/
export const ErrorContext = createContext<ErrorContextValue | null>(null)

@ -0,0 +1,13 @@
import { ProviderProps, ReactElement } from "react";
import { ErrorContext } from "./ErrorContext";
import { ErrorContextValue } from "./types";
export function ErrorContextProvider(
props: ProviderProps<ErrorContextValue>
): ReactElement {
return (
<ErrorContext.Provider value={props.value}>
{props.children}
</ErrorContext.Provider>
);
}

@ -1,3 +1,6 @@
import { createContext } from "react";
/**
* @private
*/
export const HasDashboardContext = createContext<boolean>(false);

@ -1,21 +0,0 @@
import { createContext, PropsWithChildren, useContext } from "react";
const InAdmin = createContext(false);
/**
* @private
*/
export function InAdminContext(props: PropsWithChildren) {
return (
<InAdmin.Provider value={true}>
{props.children}
</InAdmin.Provider>
)
}
/**
* @private
*/
export function useInAdminContext() {
return useContext(InAdmin);
}

@ -1,21 +0,0 @@
import { createContext, PropsWithChildren, useContext } from "react";
const InApp = createContext(false);
/**
* @private
*/
export function InAppContext(props: PropsWithChildren) {
return (
<InApp.Provider value={true}>
{props.children}
</InApp.Provider>
)
}
/**
* @private
*/
export function useInAppContext() {
return useContext(InApp);
}

@ -0,0 +1,5 @@
import { ReactNode } from "react";
export function RoutesWithoutLayout(_props: { children?: ReactNode }) {
return null;
}

@ -1,10 +1,22 @@
export * from "./AdminRouter";
export * from "./BasenameContextProvider";
export * from "./CoreAdmin";
export * from "./CoreAdminContext";
export * from "./CoreAdminRoutes";
export * from "./CoreAppContext";
export * from "./CoreAppRoutes";
export * from "./CoreAdminUI";
export * from "./DefaultError";
export * from "./DefaultLayout";
export * from "./HasDashboardContext";
export * from "./DefaultReady";
export * from "./DefaultTitleContextProvider";
export * from "./ErrorBoundary";
export * from "./ErrorContextProvider";
export * from "./HasDashboardContextProvider";
export * from "./RoutesWithoutLayout";
export * from "./types";
export * from "./useConfigureRoutesFromChildren";
export * from "./useBasename";
export * from "./useConfigureAdminRouterFromChildren";
export * from "./useDefaultTitle";
export * from "./useErrorContext";
export * from "./useHasDashboard";
export * from "./useRedirect";
export * from "./useResetErrorBoundaryOnLocationChange";

@ -1,5 +1,23 @@
import { ReactNode } from "react";
import { ErrorInfo, ReactNode } from "react";
import { FallbackProps } from "react-error-boundary";
export interface CoreLayoutProps {
children: ReactNode;
}
export type RenderRoutesFunction = (permissions: any) =>
| ReactNode // (permissions) => <><Route /><Route /><Route /></>
| Promise<ReactNode> // (permissions) => fetch().then(() => <><Route /><Route /><Route /></>)
export type AdminChildren =
| RenderRoutesFunction
| Iterable<ReactNode | RenderRoutesFunction>
| ReactNode;
export type AdminRouterStatus = 'loading' | 'empty' | 'ready';
export interface ErrorContextValue {
errorInfo?: ErrorInfo;
error: Error;
resetErrorBoundary: FallbackProps['resetErrorBoundary'];
}

@ -1,35 +1,31 @@
import {
Children,
Dispatch,
Fragment,
isValidElement,
PropsWithChildren,
ReactElement,
ReactNode,
SetStateAction,
useCallback,
useEffect,
useState
} from "react";
import { Route as ReactRoute } from "react-router-dom";
import { Route } from "react-router-dom";
import { useLogout, usePermissions } from "../auth";
import { useSafeSetState } from "../util";
import { CoreAdminContext, CoreAdminContextProps } from "./CoreAdminContext";
import { Route, RouteProps } from "../routing";
export type RenderRoutesFunction = (permissions: any) =>
| ReactNode // (permissions) => <><Route /><Route /><Route /></>
| Promise<ReactNode> // (permissions) => fetch().then(() => <><Route /><Route /><Route /></>)
export type AppChildren =
| RenderRoutesFunction
| Iterable<ReactNode | RenderRoutesFunction>
| ReactNode;
export type AppRoutesStatus = 'loading' | 'empty' | 'ready';
import { RoutesWithoutLayout } from "./RoutesWithoutLayout";
import {
AdminChildren,
AdminRouterStatus,
RenderRoutesFunction
} from "./types";
function isRenderRoutesFunction(children: AppChildren): children is RenderRoutesFunction {
function isRenderRoutesFunction(children: AdminChildren): children is RenderRoutesFunction {
return typeof children === "function";
}
function hasIteratorProtocol(children: AppChildren): children is Iterable<ReactNode | RenderRoutesFunction> {
function hasIteratorProtocol(children: AdminChildren): children is Iterable<ReactNode | RenderRoutesFunction> {
return (
typeof Symbol !== "undefined" &&
children != null &&
@ -37,7 +33,7 @@ function hasIteratorProtocol(children: AppChildren): children is Iterable<ReactN
);
}
function getRenderRoutesFunctions(children: AppChildren): RenderRoutesFunction[] {
function getRenderRoutesFunctions(children: AdminChildren): RenderRoutesFunction[] {
if (isRenderRoutesFunction(children)) {
return [children];
}
@ -55,19 +51,15 @@ function getRenderRoutesFunctions(children: AppChildren): RenderRoutesFunction[]
return [];
}
export function useConfigureRoutesFromChildren(children: AppChildren): [ReactElement[], AppRoutesStatus] {
export function useConfigureAdminRouterFromChildren(children: AdminChildren) {
// Gather custom routes that were declared as direct children of AppRouter
// e.g. Not returned from the child function (if any)
// We need to know right away whether some resources were declared to correctly
// initialize the status at the next stop
const doLogout = useLogout();
const { permissions, isPending } = usePermissions();
const [routes, setRoutes] = useState(getRoutesFromNodes(children));
const [status, setStatus] = useSafeSetState<AppRoutesStatus>(() => getStatus(children, routes));
const mergeRoutes = useCallback((newRoutes: ReactElement[]) => {
setRoutes(previous => previous.concat(newRoutes));
}, [setRoutes]);
const [routes, setRoutes, mergeRoutes] = useRoutesState(getRoutesFromNodes(children));
const [status, setStatus] = useSafeSetState<AdminRouterStatus>(() => getStatus(children, routes));
if (!status) {
throw new Error('Status should be defined');
@ -78,9 +70,15 @@ export function useConfigureRoutesFromChildren(children: AppChildren): [ReactEle
const onResolve = () => {
funcCounts -= 1;
if (funcCounts <= 0) {
setStatus('ready');
}
setTimeout(() => {
if (funcCounts <= 0) {
setStatus(
routes.withLayout.length > 0 || routes.withoutLayout.length > 0
? 'ready'
: 'empty'
);
}
})
}
const resolveChildFunction = async (childFunc: RenderRoutesFunction) => {
@ -107,11 +105,11 @@ export function useConfigureRoutesFromChildren(children: AppChildren): [ReactEle
const updateFromChildren = async () => {
const functionChild = getRenderRoutesFunctions(children);
const newRoutes = getRoutesFromNodes(children);
mergeRoutes(newRoutes);
setRoutes(newRoutes);
setStatus(
functionChild.length > 0
? 'loading'
: newRoutes.length > 0
: newRoutes.withLayout.length > 0 || newRoutes.withoutLayout.length > 0
? 'ready'
: 'empty'
);
@ -139,28 +137,68 @@ export function useConfigureRoutesFromChildren(children: AppChildren): [ReactEle
setStatus,
]);
return [routes, status];
return {
routesWithLayout: routes.withLayout,
routesWithoutLayout: routes.withoutLayout,
status,
};
}
const getStatus = (
children: AppChildren,
routes: ReactNode[],
): AppRoutesStatus => {
type Routes = {
withLayout: ReactNode[],
withoutLayout: ReactNode[],
}
/*
* A hook that store the routes and resources just like setState but also provides an additional function
* to merge new routes and resources with the existing ones.
*/
const useRoutesState = (initialState: Routes): [
Routes,
Dispatch<SetStateAction<Routes>>,
(newRoutes: Routes) => void,
] => {
const [routes, setRoutes] = useState(initialState);
const mergeRoutes = useCallback(
(newRouteGroups: Routes) => {
setRoutes(previous => ({
withLayout: previous.withLayout.concat(
newRouteGroups.withLayout
),
withoutLayout:
previous.withoutLayout.concat(
newRouteGroups.withoutLayout
),
}));
},
[]
);
return [routes, setRoutes, mergeRoutes];
};
function getStatus(children: AdminChildren, routes: Routes): AdminRouterStatus {
return getRenderRoutesFunctions(children).length > 0
? 'loading'
: routes.length > 0
: routes.withLayout.length > 0 || routes.withoutLayout.length
? 'ready'
: 'empty';
};
}
/**
* Inspect the children and return an array of routable elements
*/
function getRoutesFromNodes(children: AppChildren) {
const routes: ReactElement[] = [];
function getRoutesFromNodes(children: AdminChildren): Routes {
const withLayout: ReactNode[] = [];
const withoutLayout: ReactNode[] = [];
if (isRenderRoutesFunction(children)) {
return routes;
return {
withLayout,
withoutLayout,
}
}
if (
@ -177,47 +215,30 @@ function getRoutesFromNodes(children: AppChildren) {
// conditionals in their route config.
return;
} else if (node.type === Fragment) {
routes.push(...getRoutesFromNodes(node.props.children));
} else if (node.type === CoreAdminContext) {
// TODO ...
const { basepath } = (node as ReactElement<CoreAdminContextProps>).props;
routes.push(
<ReactRoute
key="adminContext"
path={basepath}
element={node}
/>
)
} else if (node.type === ReactRoute) {
routes.push(node);
const customRoutesFromFragment = getRoutesFromNodes(node.props.children);
withLayout.push(...customRoutesFromFragment.withLayout);
withoutLayout.push(...customRoutesFromFragment.withoutLayout);
} else if (node.type === Route) {
const {
name,
remembeScrollPosition: _,
authorised: __,
children,
...props
} = (node as ReactElement<RouteProps>).props;
routes.push(
// @ts-ignore
<ReactRoute
key={name}
{...props}
element={node}
Component={null}
>
{getRoutesFromNodes(children)}
</ReactRoute>
);
} else {
// todo 与 react-router 报错保持一致
// throw new Error("");
// [div] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
withLayout.push(node);
} else if (node.type === RoutesWithoutLayout) {
const customRoutesElement = node as ReactElement<PropsWithChildren>;
if (customRoutesElement.props.children != null) {
withoutLayout.push(customRoutesElement.props.children);
}
} else if (process.env.NODE_ENV !== "production") {
// TODO 获取 node.type 的 displayName
const name = typeof node.type === "string"
? node.type
: ((node.type as any).displayName || node.type.name);
throw new Error(
"[" + node.type + "] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>"
`[${name}] is not a <Route> component. ` +
`All component children of <Routes> must be a <Route> or <RoutesWithoutLayout> or <React.Fragment>`
);
}
});
return routes;
return {
withLayout,
withoutLayout,
};
}

@ -1,5 +1,6 @@
import { useContext } from "react";
import { ErrorContext, ErrorContextValue } from "./ErrorContext";
import { ErrorContext } from "./ErrorContext";
import { ErrorContextValue } from "./types";
export function useErrorContext(): ErrorContextValue {
const errorContext = useContext(ErrorContext);

@ -1,4 +1,4 @@
import * as React from 'react';
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
/**
@ -7,15 +7,15 @@ import { useLocation } from 'react-router-dom';
* the location changes
* @param {Function} resetErrorBoundary
*/
export const useResetErrorBoundaryOnLocationChange = (
export function useResetErrorBoundaryOnLocationChange(
resetErrorBoundary: () => void
) => {
): void {
const { pathname } = useLocation();
const originalPathname = React.useRef(pathname);
const originalPathname = useRef(pathname);
React.useEffect(() => {
useEffect(() => {
if (pathname !== originalPathname.current) {
resetErrorBoundary();
}
}, [pathname, resetErrorBoundary]);
};
}

@ -1,10 +0,0 @@
import { createContext, ErrorInfo } from "react";
import { FallbackProps } from "react-error-boundary";
export interface ErrorContextValue {
errorInfo?: ErrorInfo;
error: Error;
resetErrorBoundary: FallbackProps['resetErrorBoundary'];
}
export const ErrorContext = createContext<ErrorContextValue | null>(null)

@ -1,15 +0,0 @@
import * as React from "react";
import {
ErrorContext,
ErrorContextValue,
} from "./ErrorContext";
export const ErrorContextProvider = (
props: React.ProviderProps<ErrorContextValue>
): React.ReactElement => {
return (
<ErrorContext.Provider value={props.value}>
{props.children}
</ErrorContext.Provider>
)
}

@ -1,4 +0,0 @@
export * from "./ErrorBoundary";
export * from "./ErrorContext";
export * from "./ErrorContextProvider";
export * from "./useErrorContext";

@ -1,14 +1,10 @@
export * from "./accessControl";
export * from "./auth";
export * from "./core";
export * from "./data";
export * from "./errorBoundary";
export * from "./i18n";
export * from "./notification";
export * from "./portal";
export * from "./record";
export * from "./routing";
export * from "./scrollPosition";
export * from "./store";
export * from "./title";
export * from "./util";

@ -1,12 +0,0 @@
import { createContext } from "react";
import { To } from "react-router-dom";
export interface InitialLocationContextValue {
app?: To;
admin?: To;
}
/**
* @private
*/
export const InitialLocationContext = createContext<InitialLocationContextValue | undefined>(undefined);

@ -1,32 +0,0 @@
import { ReactNode } from "react";
import { To } from "react-router-dom";
import {
InitialLocationContext,
InitialLocationContextValue
} from "./InitialLocationContext";
import { useInitialLocationContext } from "./useInitialLocationContext";
export interface InitialLocationContextProviderProps {
location?: To;
target: keyof InitialLocationContextValue
children?: ReactNode;
}
export function InitialLocationContextProvider(props: InitialLocationContextProviderProps) {
const { children, target, location } = props;
const fromContext = useInitialLocationContext({});
const value = {
...fromContext,
[target]: location ?? fromContext[target]
}
if (Object.values(value).some(v => v == null)) {
return children;
}
return (
<InitialLocationContext.Provider value={value}>
{children}
</InitialLocationContext.Provider>
)
}

@ -1,75 +0,0 @@
import {
IndexRouteProps as IndexRoutePropsRaw,
PathRouteProps as PathRoutePropsRaw,
} from "react-router-dom";
import { Authenticated } from "../auth";
import { RestoreScrollPosition } from "../scrollPosition";
import { ComponentType, ReactNode } from "react";
import { AccessParams, CanAccess } from "../accessControl";
export type RoutePropsBase = {
accessParams?: AccessParams;
authorised?: boolean;
name: string;
remembeScrollPosition?: boolean;
}
export type IndexRouteProps = IndexRoutePropsRaw & RoutePropsBase;
export type PathRouteProps = PathRoutePropsRaw & RoutePropsBase;
export type RouteProps = IndexRouteProps | PathRouteProps;
export function Route(props: RouteProps) {
const {
accessParams,
authorised,
Component,
element,
name,
remembeScrollPosition,
} = props;
let children = getElement(element, Component);
if (authorised) {
children = (
<CanAccess
on="route"
key={name}
params={accessParams}
>
{getElement(element, Component)}
</CanAccess>
);
}
if (remembeScrollPosition) {
children = (
<RestoreScrollPosition storeKey={name}>
{children}
</RestoreScrollPosition>
);
}
if (authorised) {
children = (
<Authenticated>
{children}
</Authenticated>
);
}
return children;
}
function getElement(
element: ReactNode | undefined | null,
Component: ComponentType | undefined | null
) {
if (element != null) {
return element;
}
if (Component != null) {
return (<Component />)
}
return null;
}

@ -1,9 +0,0 @@
export * from "./AppRouter";
export * from "./BasenameContextProvider";
export * from "./InitialLocationContextProvider";
export * from "./Route";
export * from "./useBasename";
export * from "./useInitialLocation";
export * from "./useInitialLocationContext";
export * from "./useRedirect";
export * from "./useResetErrorBoundaryOnLocationChange";

@ -1,17 +0,0 @@
import { To } from "react-router-dom";
import { useInAdminContext } from "../core/InAdminContext";
import { useInAppContext } from "../core/InAppContext";
import { useInitialLocationContext } from "./useInitialLocationContext";
export function useInitialLocation(fallback?: To): To | undefined {
const inAdmin = useInAdminContext();
const inApp = useInAppContext();
const { app, admin } = useInitialLocationContext({});
if (inAdmin) {
return admin;
}
if (inApp) {
return app;
}
return fallback;
}

@ -1,20 +0,0 @@
import { useContext } from "react";
import {
InitialLocationContext,
InitialLocationContextValue
} from "./InitialLocationContext";
export function useInitialLocationContext(): InitialLocationContextValue;
export function useInitialLocationContext(overrides: InitialLocationContextValue): InitialLocationContextValue;
export function useInitialLocationContext(overrides?: InitialLocationContextValue) {
const fromContext = useContext(InitialLocationContext);
if (fromContext != null) {
return {
...fromContext,
...overrides,
}
}
return overrides;
}

@ -1,28 +0,0 @@
import { useTranslate } from "../i18n";
import { TitleProps } from "./types";
export function PageTitle(props: TitleProps) {
const {
title,
defaultTitle,
className,
...rest
} = props;
const translate = useTranslate();
if (!title && !defaultTitle) {
return null;
}
return (
<span className={className}>
{!title ? (
<span {...rest}>{defaultTitle}</span>
) : typeof title === 'string' ? (
<span {...rest}>{translate(title, { _: title })}</span>
) : (
title
)}
</span>
);
}

@ -1,37 +0,0 @@
import { PageTitle } from './PageTitle';
import { useRecordRepresentation } from '../record';
import { useTranslate } from '../i18n';
import { TitleProps } from './types';
export const PageTitleConfigurable = ({
preferenceKey,
title,
defaultTitle,
record,
...props
}: TitleProps) => {
const translate = useTranslate();
const titleFromPreferences = useRecordRepresentation({
record,
representation: preferenceKey === false ? undefined : preferenceKey,
});
if (titleFromPreferences) {
return (
<span className={props.className} {...props}>
{translate(titleFromPreferences, {
...record,
_: titleFromPreferences,
})}
</span>
)
}
return (
<PageTitle
title={title}
defaultTitle={defaultTitle}
{...props}
/>
);
};

@ -1,41 +0,0 @@
import { Portlet } from '../portal';
import { titlePortalName } from './constants';
import { PageTitle } from './PageTitle';
import { PageTitleConfigurable } from './PageTitleConfigurable';
import { TitleProps } from './types';
export const Title = (props: TitleProps) => {
const {
defaultTitle,
title,
preferenceKey,
...rest
} = props;
return (
<Portlet to={titlePortalName}>
{() => {
if (!defaultTitle && !title) {
console.warn('Missing title prop in <Title> element');
}
if (preferenceKey === false) {
return (
<PageTitle
title={title}
defaultTitle={defaultTitle}
{...rest}
/>
);
}
return (
<PageTitleConfigurable
title={title}
defaultTitle={defaultTitle}
preferenceKey={preferenceKey}
{...rest}
/>
);
}}
</Portlet>
);
};

@ -1,19 +0,0 @@
import { ReactNode } from "react";
import { PortalProvider } from "../portal";
import { titlePortalName } from "./constants";
export interface TitleContainerProviderProps {
children?: ReactNode;
value: string | Element | null;
}
export function TitlePortalProvider(props: TitleContainerProviderProps) {
return (
<PortalProvider
name={titlePortalName}
container={props.value}
>
{props.children}
</PortalProvider>
);
}

@ -1,4 +0,0 @@
/**
* @private
*/
export const titlePortalName = "@@rakit-title-portal";

@ -1,8 +0,0 @@
export * from "./DefaultTitleContext";
export * from "./DefaultTitleContextProvider";
export * from "./PageTitle";
export * from "./PageTitleConfigurable";
export * from "./Title";
export * from "./TitlePortalProvider";
export * from "./types";
export * from "./useDefaultTitle";

@ -1,9 +0,0 @@
import { ReactElement } from "react";
export interface TitleProps {
className?: string;
defaultTitle?: ReactElement;
record?: any;
title?: string | ReactElement;
preferenceKey?: string | false;
}
Loading…
Cancel
Save