parent
b4e047a10c
commit
bf1d289e8e
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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 |
||||||
|
}) |
||||||
|
} |
@ -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]); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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']; |
||||||
|
} |
||||||
|
@ -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,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…
Reference in new issue