feat: 是开始

main
熊二 3 months ago
parent b4e047a10c
commit bf1d289e8e
  1. 23
      apps/imsfe/src/App.tsx
  2. 14
      apps/simple/package.json
  3. 40
      packages/joy-ui/src/layout/AdminRoot.tsx
  4. 35
      packages/joy-ui/src/layout/Layout.tsx
  5. 18
      packages/joy-ui/src/layout/PageDock.tsx
  6. 9
      packages/joy-ui/src/layout/PageRoot.tsx
  7. 52
      packages/joy-ui/src/layout/PageTitle.tsx
  8. 8
      packages/joy-ui/src/layout/StatusError.tsx
  9. 15
      packages/joy-ui/src/layout/TitlePortal.tsx
  10. 3
      packages/joy-ui/src/layout/index.ts
  11. 7
      packages/rakit/src/accessControl/AccessControlContext.ts
  12. 16
      packages/rakit/src/accessControl/AccessControlProvider.tsx
  13. 96
      packages/rakit/src/accessControl/CanAccess.tsx
  14. 5
      packages/rakit/src/accessControl/index.ts
  15. 52
      packages/rakit/src/accessControl/types.ts
  16. 18
      packages/rakit/src/accessControl/useAccessControl.ts
  17. 70
      packages/rakit/src/accessControl/useCan.ts
  18. 21
      packages/rakit/src/auth/AccessControlContext.ts
  19. 37
      packages/rakit/src/auth/AccessControlProvider.tsx
  20. 40
      packages/rakit/src/auth/CanAccess.tsx
  21. 3
      packages/rakit/src/auth/index.ts
  22. 40
      packages/rakit/src/auth/types.ts
  23. 57
      packages/rakit/src/auth/useCan.ts
  24. 27
      packages/rakit/src/core/AdminRouter.tsx
  25. 0
      packages/rakit/src/core/BasenameContext.ts
  26. 0
      packages/rakit/src/core/BasenameContextProvider.tsx
  27. 134
      packages/rakit/src/core/CoreAdmin.tsx
  28. 176
      packages/rakit/src/core/CoreAdminContext.tsx
  29. 51
      packages/rakit/src/core/CoreAdminRoutes.tsx
  30. 293
      packages/rakit/src/core/CoreAdminUI.tsx
  31. 271
      packages/rakit/src/core/CoreAppContext.tsx
  32. 277
      packages/rakit/src/core/CoreAppRoutes.tsx
  33. 18
      packages/rakit/src/core/DefaultError.tsx
  34. 5
      packages/rakit/src/core/DefaultTitleContext.ts
  35. 0
      packages/rakit/src/core/DefaultTitleContextProvider.tsx
  36. 0
      packages/rakit/src/core/ErrorBoundary.tsx
  37. 7
      packages/rakit/src/core/ErrorContext.ts
  38. 13
      packages/rakit/src/core/ErrorContextProvider.tsx
  39. 3
      packages/rakit/src/core/HasDashboardContext.ts
  40. 21
      packages/rakit/src/core/InAdminContext.tsx
  41. 21
      packages/rakit/src/core/InAppContext.tsx
  42. 18
      packages/rakit/src/core/index.ts
  43. 20
      packages/rakit/src/core/types.ts
  44. 0
      packages/rakit/src/core/useBasename.ts
  45. 82
      packages/rakit/src/core/useConfigureAdminRouterFromChildren.tsx
  46. 0
      packages/rakit/src/core/useDefaultTitle.ts
  47. 3
      packages/rakit/src/core/useErrorContext.ts
  48. 0
      packages/rakit/src/core/useRedirect.ts
  49. 12
      packages/rakit/src/core/useResetErrorBoundaryOnLocationChange.ts
  50. 10
      packages/rakit/src/errorBoundary/ErrorContext.ts
  51. 15
      packages/rakit/src/errorBoundary/ErrorContextProvider.tsx
  52. 4
      packages/rakit/src/errorBoundary/index.ts
  53. 4
      packages/rakit/src/index.ts
  54. 12
      packages/rakit/src/routing/InitialLocationContext.ts
  55. 32
      packages/rakit/src/routing/InitialLocationContextProvider.tsx
  56. 75
      packages/rakit/src/routing/Route.tsx
  57. 9
      packages/rakit/src/routing/index.ts
  58. 17
      packages/rakit/src/routing/useInitialLocation.ts
  59. 20
      packages/rakit/src/routing/useInitialLocationContext.ts
  60. 28
      packages/rakit/src/title/PageTitle.tsx
  61. 37
      packages/rakit/src/title/PageTitleConfigurable.tsx
  62. 41
      packages/rakit/src/title/Title.tsx
  63. 19
      packages/rakit/src/title/TitlePortalProvider.tsx
  64. 4
      packages/rakit/src/title/constants.ts
  65. 8
      packages/rakit/src/title/index.ts
  66. 9
      packages/rakit/src/title/types.ts

@ -1,10 +1,11 @@
import { CoreAdminContext, CoreAppContext, CoreLayoutProps, Route } from '@rakit/core'; import { CoreAdmin, CoreLayoutProps } from '@rakit/core';
import { useFetch } from '@rakit/fetch'; import { FetchContextProvider, useFetch } from '@rakit/fetch';
import { Error, Loading, ThemeProvider } from '@rakit/joy-ui'; import { Error, Loading, ThemeProvider } from '@rakit/joy-ui';
import { createAuthProvider } from './config/createAuthProvider'; import { createAuthProvider } from './config/createAuthProvider';
import { createDataProvider } from './config/createDataProvider'; import { createDataProvider } from './config/createDataProvider';
import { i18nProvider } from './config/i18nProvider'; import { i18nProvider } from './config/i18nProvider';
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { Route } from 'react-router-dom';
const AppLoading = () => <Loading /> const AppLoading = () => <Loading />
@ -22,9 +23,11 @@ const PageError = lazy(() => import("./pages/PageError"));
export default function App() { export default function App() {
return ( return (
<FetchContextProvider>
<ThemeProvider> <ThemeProvider>
<AppContext /> <AppContext />
</ThemeProvider> </ThemeProvider>
</FetchContextProvider>
); );
} }
@ -35,14 +38,14 @@ function AppContext() {
const dataProvider = createDataProvider(fetch); const dataProvider = createDataProvider(fetch);
return ( return (
<CoreAppContext <CoreAdmin
authProvider={authProvider} authProvider={authProvider}
// authCallbackPage={ } // authCallbackPage={ }
// basename={ } // basename={ }
catchAll={<CatchAll />} catchAll={<CatchAll />}
dataProvider={dataProvider} dataProvider={dataProvider}
error={Error} error={Error}
homepage={<CatchAll />} dashboard={<CatchAll />}
initialLocation="/" initialLocation="/"
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
loading={AppLoading} loading={AppLoading}
@ -54,25 +57,17 @@ function AppContext() {
title="进销存系统" title="进销存系统"
> >
<Route <Route
name="pageErrpr"
path="/error/:status" path="/error/:status"
element={<PageError />} element={<PageError />}
/> />
<Route <Route
name="test"
path="/sasda" path="/sasda"
element={<div></div>} element={<div></div>}
/> />
<CoreAdminContext
basepath="/admin"
initialLocation="/admmin"
>
<Route <Route
name="test" path="/sasda2"
path="/sasda"
element={<div>222</div>} element={<div>222</div>}
/> />
</CoreAdminContext> </CoreAdmin>
</CoreAppContext>
) )
} }

@ -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"
}
}

@ -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>
);
}

@ -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 { 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>
);
}

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

@ -112,15 +112,19 @@ const statusDescriptions = {
"500": "There was an error, please try again later.", "500": "There was an error, please try again later.",
}; };
const isStatus = (status: string | undefined): status is keyof typeof statusIcons => {
return !!status && Object.hasOwn(statusIcons, status);
}
export interface StatusErrorProps { export interface StatusErrorProps {
status: "403" | "404" | "500"; status?: string;
icon?: ReactNode; icon?: ReactNode;
title?: ReactNode; title?: ReactNode;
description?: ReactNode; description?: ReactNode;
} }
export function StatusError(props: StatusErrorProps) { export function StatusError(props: StatusErrorProps) {
const status = statusIcons[props.status] ? props.status : "500"; const status = isStatus(props.status) ? props.status : "500";
const siteTitle = useDefaultTitle(); const siteTitle = useDefaultTitle();
const icon = props.icon || statusIcons[status]; const icon = props.icon || statusIcons[status];
const title = props.title || statusTitles[status]; const title = props.title || statusTitles[status];

@ -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,11 @@
export * from "./AdminRoot";
export * from "./AppBar"; export * from "./AppBar";
export * from "./ColorSchemeToggle"; export * from "./ColorSchemeToggle";
export * from "./Error"; export * from "./Error";
export * from "./Layout";
export * from "./Loading"; export * from "./Loading";
export * from "./Notification"; export * from "./Notification";
export * from "./PageActions"; export * from "./PageActions";
export * from "./PageRoot"; export * from "./PageRoot";
export * from "./Sidebar"; export * from "./Sidebar";
export * from "./StatusError"; export * from "./StatusError";
export * from "./TitlePortal";
export * from "./utils"; export * from "./utils";

@ -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 "./AuthContext";
export * from "./Authenticated"; export * from "./Authenticated";
export * from "./CanAccess";
export * from "./LogoutOnMount"; export * from "./LogoutOnMount";
export * from "./types"; export * from "./types";
export * from "./useAuthenticated"; export * from "./useAuthenticated";
export * from "./useAuthProvider"; export * from "./useAuthProvider";
export * from "./useAuthState"; export * from "./useAuthState";
export * from "./useCan";
export * from "./useCheckAuth"; export * from "./useCheckAuth";
export * from "./useGetIdentity"; export * from "./useGetIdentity";
export * from "./useGetPermissions"; export * from "./useGetPermissions";

@ -1,3 +1,4 @@
import { ComponentType } from "react";
import { To } from "react-router-dom"; import { To } from "react-router-dom";
type QueryFunctionContext = { type QueryFunctionContext = {
@ -22,6 +23,41 @@ export interface UserIdentity {
[key: string]: any; [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 AuthProviderCustoms {}
export interface AuthProvider extends AuthProviderCustoms { export interface AuthProvider extends AuthProviderCustoms {
@ -29,7 +65,9 @@ export interface AuthProvider extends AuthProviderCustoms {
logout: (params: any) => Promise<void | false | string>; logout: (params: any) => Promise<void | false | string>;
checkAuth: (params: QueryFunctionContext) => Promise<void>; checkAuth: (params: QueryFunctionContext) => Promise<void>;
checkError: (error: any) => Promise<void>; checkError: (error: any) => Promise<void>;
canAccess?: (options: CanAccessOptions) => CanAccessResult;
accessFallback?: AccessFallbackComponent;
getIdentity?: (params?: QueryFunctionContext) => Promise<UserIdentity>; getIdentity?: (params?: QueryFunctionContext) => Promise<UserIdentity>;
getPermissions: (params: QueryFunctionContext) => Promise<any>; getPermissions: (params: QueryFunctionContext) => Promise<Permission[]>;
handleCallback?: (params?: QueryFunctionContext) => Promise<AuthRedirectResult | void | any>; handleCallback?: (params?: QueryFunctionContext) => Promise<AuthRedirectResult | void | any>;
} }

@ -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]);
}

@ -8,14 +8,14 @@ import { BasenameContextProvider } from './BasenameContextProvider';
export interface AppRouterProps { export interface AppRouterProps {
basename?: string; basename?: string;
children: React.ReactNode; children: ReactNode;
} }
/** /**
* Creates a react-router Router unless the app is already inside existing router. * Creates a react-router Router unless the app is already inside existing router.
* Also creates a BasenameContext with the basename prop * Also creates a BasenameContext with the basename prop
*/ */
export const AppRouter = ({ basename = '', children }: AppRouterProps) => { export function AdminRouter({ basename = '', children }: AppRouterProps) {
const isInRouter = useInRouterContext(); const isInRouter = useInRouterContext();
const Router = isInRouter ? DummyRouter : InternalRouter; const Router = isInRouter ? DummyRouter : InternalRouter;
@ -24,25 +24,16 @@ export const AppRouter = ({ basename = '', children }: AppRouterProps) => {
<Router basename={basename}>{children}</Router> <Router basename={basename}>{children}</Router>
</BasenameContextProvider> </BasenameContextProvider>
); );
}; }
const DummyRouter = ({ function DummyRouter({ children }: AppRouterProps) {
children, return <>{children}</>;
}: { }
children: ReactNode;
basename?: string;
}) => <>{children}</>;
const InternalRouter = ({ function InternalRouter({ basename, children }: AppRouterProps) {
children,
basename,
}: {
children: ReactNode;
basename?: string;
}) => {
const router = createHashRouter( const router = createHashRouter(
[{ path: '*', element: <>{children}</> }], [{ path: '*', element: <>{children}</> }],
{ basename }, { basename }
); );
return <RouterProvider router={router} />; 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 { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Route, Routes } from "react-router-dom"; import { ReactNode, useMemo } from "react";
import { ErrorBoundary } from "../errorBoundary"; import { AuthContext, AuthProvider } from "../auth";
import { DefaultTitleContextProvider } from "../title"; import { DataProvider, DataProviderContext, defaultDataProvider } from "../data";
import { CoreAdminRoutes, CoreAdminRoutesProps } from "./CoreAdminRoutes"; import { I18nContextProvider, I18nProvider } from "../i18n";
import { InAdminContext, useInAdminContext } from "./InAdminContext"; import { NotificationContextProvider } from "../notification";
import { useInAppContext } from "./InAppContext"; 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. * The base path for all URLs generated by react-admin.
* *
@ -24,92 +40,126 @@ export interface CoreAdminContextProps extends CoreAdminRoutesProps {
* </BrowserRouter> * </BrowserRouter>
* ); * );
*/ */
basepath?: string; basename?: string;
children?: ReactNode;
/** /**
* The component displayed when an error is caught in a child component * The data provider used to communicate with the API
* @see https://marmelab.com/react-admin/Admin.html#error *
* @see https://marmelab.com/react-admin/DataProviders.html
* @example * @example
* import { Admin } from 'react-admin'; * 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 = () => ( * const App = () => (
* <Admin error={MyError}> * <Admin dataProvider={dataProvider}>
* ... * ...
* </Admin> * </Admin>
* ); * );
*/ */
error?: ComponentType<any> | ReactElement | null; dataProvider?: DataProvider;
/** /**
* The title of the error page * The internationalization provider for translations
* @see https://marmelab.com/react-admin/Admin.html#title *
* @see https://marmelab.com/react-admin/Translation.html
* @example * @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 { Admin } from 'react-admin';
* import { dataProvider } from './dataProvider'; * import { dataProvider } from './dataProvider';
* import { i18nProvider } from './i18nProvider';
* *
* const App = () => ( * const App = () => (
* <Admin title="My Admin" dataProvider={dataProvider}> * <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}>
* ... * ...
* </Admin> * </Admin>
* ); * );
*/ */
title?: string | ReactElement | null; 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 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;
} }
export function CoreAdminContext(props: CoreAdminContextProps) { export function CoreAdminContext(props: CoreAdminContextProps) {
const { const {
catchAll, authProvider,
basename,
children, children,
dashboard, dataProvider = defaultDataProvider,
error, i18nProvider,
layout, queryClient,
loading, store = defaultStore,
ready,
requireAuth,
title,
} = props; } = props;
const inApp = useInAppContext(); const finalQueryClient = useMemo(
const inAdmin = useInAdminContext(); () => queryClient || new QueryClient(),
[queryClient]
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"
);
}
return ( return (
<DefaultTitleContextProvider value={title}> <AuthContext.Provider value={authProvider}>
<ErrorBoundary error={error}> <DataProviderContext.Provider value={dataProvider}>
<InAdminContext> <StoreContextProvider value={store}>
<Routes> <QueryClientProvider client={finalQueryClient}>
<Route <I18nContextProvider value={i18nProvider}>
path="/*" <NotificationContextProvider>
element={ <AdminRouter basename={basename}>
<CoreAdminRoutes
catchAll={catchAll}
dashboard={dashboard}
layout={layout}
loading={loading}
requireAuth={requireAuth}
ready={ready}
>
{children} {children}
</CoreAdminRoutes> </AdminRouter>
} </NotificationContextProvider>
/> </I18nContextProvider>
</Routes> </QueryClientProvider>
</InAdminContext> </StoreContextProvider>
</ErrorBoundary> </DataProviderContext.Provider>
</DefaultTitleContextProvider> </AuthContext.Provider>
); );
} }

@ -1,12 +1,12 @@
import { ComponentType, ReactElement, ReactNode, useEffect, useState } from 'react'; import { ComponentType, ReactElement, useEffect, useState } from 'react';
import { Navigate, Route, Routes, To } from 'react-router-dom'; import { Navigate, Route, Routes, To } from 'react-router-dom';
import { LogoutOnMount, useCheckAuth } from '../auth'; import { LogoutOnMount, useCheckAuth } from '../auth';
import { CoreLayoutProps } from './types'; import { useScrollToTop } from '../scrollPosition';
import { getReactElement } from '../util'; import { getReactElement } from '../util';
import { DefaultLayout } from './DefaultLayout'; import { DefaultLayout } from './DefaultLayout';
import { HasDashboardContextProvider } from './HasDashboardContextProvider'; import { HasDashboardContextProvider } from './HasDashboardContextProvider';
import { useConfigureRoutesFromChildren } from './useConfigureRoutesFromChildren'; import { AdminChildren, CoreLayoutProps } from './types';
import { InitialLocationContextProvider } from '../routing'; import { useConfigureAdminRouterFromChildren } from './useConfigureAdminRouterFromChildren';
export interface CoreAdminRoutesProps { export interface CoreAdminRoutesProps {
/** /**
@ -41,7 +41,7 @@ export interface CoreAdminRoutesProps {
*/ */
catchAll?: ComponentType<any> | ReactElement | null; catchAll?: ComponentType<any> | ReactElement | null;
children?: ReactNode; children?: AdminChildren;
/** /**
* @example * @example
@ -134,16 +134,18 @@ export interface CoreAdminRoutesProps {
} }
export function CoreAdminRoutes(props: CoreAdminRoutesProps) { export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
const [routes, status] = useConfigureRoutesFromChildren(props.children); useScrollToTop();
const [routes, status] = useConfigureAdminRouterFromChildren(props.children);
const { const {
catchAll: catchAllElement, catchAll: catchAllElement,
dashboard: dashboardElement, dashboard,
initialLocation, initialLocation,
layout: Layout = DefaultLayout, layout: Layout = DefaultLayout,
loading: loadingElement, loading: loadingElement,
requireAuth,
ready: readyElement, ready: readyElement,
requireAuth = false,
} = props; } = props;
const [onlyAnonymousRoutes, setOnlyAnonymousRoutes] = useState(requireAuth); const [onlyAnonymousRoutes, setOnlyAnonymousRoutes] = useState(requireAuth);
@ -168,8 +170,8 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
if (status === 'empty') { if (status === 'empty') {
if (!readyElement) { if (!readyElement) {
throw new Error( throw new Error(
'The app is empty. Please provide an empty component, ' + 'The admin is empty. Please provide an empty component, ' +
'or pass Resource or CustomRoutes as children.' 'or pass Route or CustomRoutes as children.'
); );
} }
return getReactElement(readyElement); return getReactElement(readyElement);
@ -206,34 +208,25 @@ export function CoreAdminRoutes(props: CoreAdminRoutesProps) {
<Route <Route
path="/*" path="/*"
element={ element={
<HasDashboardContextProvider value={!!dashboardElement}> <HasDashboardContextProvider value={!!dashboard}>
<InitialLocationContextProvider
location={initialLocation}
target="admin"
>
<Layout> <Layout>
<Routes> <Routes>
{routes} {routes}
{dashboardElement ? (
<Route {dashboard
index ? (<Route path="/" element={getReactElement(dashboard)} />)
element={ : (initialLocation && initialLocation !== "/")
dashboardElement ? (<Route path="/" element={<Navigate to={initialLocation} />} />)
? (getReactElement(dashboardElement)) : null}
: initialLocation
? (<Navigate to={initialLocation} />)
: null
}
/>
) : null}
{catchAllElement ? ( {catchAllElement ? (
<Route <Route
path="*" path="*"
element={getReactElement(catchAllElement)} /> element={getReactElement(catchAllElement)}
/>
) : null} ) : null}
</Routes> </Routes>
</Layout> </Layout>
</InitialLocationContextProvider>
</HasDashboardContextProvider> </HasDashboardContextProvider>
} }
/> />

@ -0,0 +1,293 @@
import { ComponentType, ReactElement } from "react";
import { Route, Routes, To } from "react-router-dom";
import { DefaultTitleContextProvider } from "../title";
import { getReactElement } from "../util";
import { CoreAdminRoutes } from "./CoreAdminRoutes";
import { DefaultError } from "./DefaultError";
import { DefaultLayout } from "./DefaultLayout";
import { ErrorBoundary } from "./ErrorBoundary";
import { AdminChildren, CoreLayoutProps } from "./types";
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> | ReactElement | 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> | ReactElement | 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 | ReactElement | 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> | ReactElement | 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> | ReactElement | 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> | 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;
/**
* 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,
catchAll,
children,
dashboard,
// disableTelemetry = false,
error = DefaultError,
initialLocation,
layout = DefaultLayout,
loading,
loginPage,
ready = Ready,
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={getReactElement(loginPage)}
/>
) : null}
{authCallbackPage != null ? (
<Route
path="/auth-callback"
element={getReactElement(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>
);
}

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

@ -1,10 +1,20 @@
export * from "./AdminRouter";
export * from "./BasenameContextProvider";
export * from "./CoreAdmin";
export * from "./CoreAdminContext"; export * from "./CoreAdminContext";
export * from "./CoreAdminRoutes"; export * from "./CoreAdminRoutes";
export * from "./CoreAppContext"; export * from "./CoreAdminUI";
export * from "./CoreAppRoutes"; export * from "./DefaultError";
export * from "./DefaultLayout"; export * from "./DefaultLayout";
export * from "./HasDashboardContext"; export * from "./DefaultTitleContextProvider";
export * from "./ErrorBoundary";
export * from "./ErrorContextProvider";
export * from "./HasDashboardContextProvider"; export * from "./HasDashboardContextProvider";
export * from "./types"; export * from "./types";
export * from "./useConfigureRoutesFromChildren"; export * from "./useBasename";
export * from "./useConfigureAdminRouterFromChildren";
export * from "./useDefaultTitle";
export * from "./useErrorContext";
export * from "./useHasDashboard"; 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 { export interface CoreLayoutProps {
children: ReactNode; 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'];
}

@ -8,28 +8,20 @@ import {
useEffect, useEffect,
useState useState
} from "react"; } from "react";
import { Route as ReactRoute } from "react-router-dom"; import { Route } from "react-router-dom";
import { useLogout, usePermissions } from "../auth"; import { useLogout, usePermissions } from "../auth";
import { useSafeSetState } from "../util"; import { useSafeSetState } from "../util";
import { CoreAdminContext, CoreAdminContextProps } from "./CoreAdminContext"; import {
import { Route, RouteProps } from "../routing"; AdminChildren,
AdminRouterStatus,
export type RenderRoutesFunction = (permissions: any) => RenderRoutesFunction
| ReactNode // (permissions) => <><Route /><Route /><Route /></> } from "./types";
| Promise<ReactNode> // (permissions) => fetch().then(() => <><Route /><Route /><Route /></>)
export type AppChildren =
| RenderRoutesFunction
| Iterable<ReactNode | RenderRoutesFunction>
| ReactNode;
export type AppRoutesStatus = 'loading' | 'empty' | 'ready';
function isRenderRoutesFunction(children: AppChildren): children is RenderRoutesFunction { function isRenderRoutesFunction(children: AdminChildren): children is RenderRoutesFunction {
return typeof children === "function"; return typeof children === "function";
} }
function hasIteratorProtocol(children: AppChildren): children is Iterable<ReactNode | RenderRoutesFunction> { function hasIteratorProtocol(children: AdminChildren): children is Iterable<ReactNode | RenderRoutesFunction> {
return ( return (
typeof Symbol !== "undefined" && typeof Symbol !== "undefined" &&
children != null && children != null &&
@ -37,7 +29,7 @@ function hasIteratorProtocol(children: AppChildren): children is Iterable<ReactN
); );
} }
function getRenderRoutesFunctions(children: AppChildren): RenderRoutesFunction[] { function getRenderRoutesFunctions(children: AdminChildren): RenderRoutesFunction[] {
if (isRenderRoutesFunction(children)) { if (isRenderRoutesFunction(children)) {
return [children]; return [children];
} }
@ -55,7 +47,7 @@ function getRenderRoutesFunctions(children: AppChildren): RenderRoutesFunction[]
return []; return [];
} }
export function useConfigureRoutesFromChildren(children: AppChildren): [ReactElement[], AppRoutesStatus] { export function useConfigureAdminRouterFromChildren(children: AdminChildren): [ReactElement[], AdminRouterStatus] {
// Gather custom routes that were declared as direct children of AppRouter // Gather custom routes that were declared as direct children of AppRouter
// e.g. Not returned from the child function (if any) // e.g. Not returned from the child function (if any)
// We need to know right away whether some resources were declared to correctly // We need to know right away whether some resources were declared to correctly
@ -63,7 +55,7 @@ export function useConfigureRoutesFromChildren(children: AppChildren): [ReactEle
const doLogout = useLogout(); const doLogout = useLogout();
const { permissions, isPending } = usePermissions(); const { permissions, isPending } = usePermissions();
const [routes, setRoutes] = useState(getRoutesFromNodes(children)); const [routes, setRoutes] = useState(getRoutesFromNodes(children));
const [status, setStatus] = useSafeSetState<AppRoutesStatus>(() => getStatus(children, routes)); const [status, setStatus] = useSafeSetState<AdminRouterStatus>(() => getStatus(children, routes));
const mergeRoutes = useCallback((newRoutes: ReactElement[]) => { const mergeRoutes = useCallback((newRoutes: ReactElement[]) => {
setRoutes(previous => previous.concat(newRoutes)); setRoutes(previous => previous.concat(newRoutes));
@ -142,21 +134,18 @@ export function useConfigureRoutesFromChildren(children: AppChildren): [ReactEle
return [routes, status]; return [routes, status];
} }
const getStatus = ( function getStatus(children: AdminChildren, routes: ReactNode[]): AdminRouterStatus {
children: AppChildren,
routes: ReactNode[],
): AppRoutesStatus => {
return getRenderRoutesFunctions(children).length > 0 return getRenderRoutesFunctions(children).length > 0
? 'loading' ? 'loading'
: routes.length > 0 : routes.length > 0
? 'ready' ? 'ready'
: 'empty'; : 'empty';
}; }
/** /**
* Inspect the children and return an array of routable elements * Inspect the children and return an array of routable elements
*/ */
function getRoutesFromNodes(children: AppChildren) { function getRoutesFromNodes(children: AdminChildren): ReactElement[] {
const routes: ReactElement[] = []; const routes: ReactElement[] = [];
if (isRenderRoutesFunction(children)) { if (isRenderRoutesFunction(children)) {
@ -178,43 +167,16 @@ function getRoutesFromNodes(children: AppChildren) {
return; return;
} else if (node.type === Fragment) { } else if (node.type === Fragment) {
routes.push(...getRoutesFromNodes(node.props.children)); 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);
} else if (node.type === Route) { } else if (node.type === Route) {
const { routes.push(node);
name, } else if (process.env.NODE_ENV !== "production") {
remembeScrollPosition: _, // TODO 获取 node.type 的 displayName
authorised: __, const name = typeof node.type === "string"
children, ? node.type
...props : ((node.type as any).displayName || node.type.name);
} = (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>
throw new Error( 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 <React.Fragment>`
); );
} }
}); });

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

@ -1,4 +1,4 @@
import * as React from 'react'; import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
/** /**
@ -7,15 +7,15 @@ import { useLocation } from 'react-router-dom';
* the location changes * the location changes
* @param {Function} resetErrorBoundary * @param {Function} resetErrorBoundary
*/ */
export const useResetErrorBoundaryOnLocationChange = ( export function useResetErrorBoundaryOnLocationChange(
resetErrorBoundary: () => void resetErrorBoundary: () => void
) => { ): void {
const { pathname } = useLocation(); const { pathname } = useLocation();
const originalPathname = React.useRef(pathname); const originalPathname = useRef(pathname);
React.useEffect(() => { useEffect(() => {
if (pathname !== originalPathname.current) { if (pathname !== originalPathname.current) {
resetErrorBoundary(); resetErrorBoundary();
} }
}, [pathname, 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 "./auth";
export * from "./core"; export * from "./core";
export * from "./data"; export * from "./data";
export * from "./errorBoundary";
export * from "./i18n"; export * from "./i18n";
export * from "./notification"; export * from "./notification";
export * from "./portal"; export * from "./portal";
export * from "./record"; export * from "./record";
export * from "./routing";
export * from "./scrollPosition"; export * from "./scrollPosition";
export * from "./store"; export * from "./store";
export * from "./title";
export * from "./util"; 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