@genesislcap/foundation-react-utils 15.19.0 → 15.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,148 +0,0 @@
1
- import {
2
- controlWrapperTemplate,
3
- type RendererControlProps,
4
- type RendererEntry,
5
- } from '@genesislcap/foundation-forms';
6
- import { html } from '@genesislcap/web-core';
7
- import {
8
- mapStateToControlProps,
9
- type JsonFormsState,
10
- type OwnPropsOfControl,
11
- type RankedTester,
12
- type StatePropsOfControl,
13
- } from '@jsonforms/core';
14
- import r2wc from '@r2wc/react-to-web-component';
15
- import type { ComponentType } from 'react';
16
-
17
- /**
18
- * Converts a React component into a `RendererEntry` for use with `foundation-form`'s
19
- * `additionalRenderers` property.
20
- *
21
- * The component is registered as a custom element via `r2wc` and wrapped in a FAST
22
- * `html` template so it integrates with the JSON Forms dispatch renderer.
23
- *
24
- * **Important — use platform web components for inputs, not native HTML elements.**
25
- * The React component runs inside an `r2wc` custom element inside a FAST template,
26
- * creating a three-layer shadow DOM boundary. React's synthetic event system does not
27
- * cross shadow boundaries, so `onChange` on a plain `<input>` will never fire.
28
- * Use `rapid-number-field`, `rapid-text-field`, etc. and attach listeners via
29
- * `addEventListener` in a `useEffect` instead.
30
- *
31
- * @example
32
- * ```tsx
33
- * import { useEffect, useRef } from 'react';
34
- * import { createReactRenderer, type RendererControlProps } from '@genesislcap/foundation-react-utils';
35
- * import { isNumberControl, rankWith } from '@jsonforms/core';
36
- *
37
- * function PriceRenderer({ data, path, enabled, handleChange }: RendererControlProps) {
38
- * const ref = useRef<HTMLElement>(null);
39
- *
40
- * useEffect(() => {
41
- * const el = ref.current as any;
42
- * if (!el) return;
43
- * const onChange = (e: CustomEvent) => {
44
- * const val = (e.target as any).value;
45
- * handleChange(path, val === '' ? undefined : Number(val));
46
- * };
47
- * el.addEventListener('change', onChange);
48
- * return () => el.removeEventListener('change', onChange);
49
- * }, [path, handleChange]);
50
- *
51
- * return <rapid-number-field ref={ref} value={data ?? ''} disabled={!enabled || undefined} />;
52
- * }
53
- *
54
- * export const priceRendererEntry = createReactRenderer(PriceRenderer, {
55
- * name: 'my-price-renderer',
56
- * tester: rankWith(6, isNumberControl),
57
- * });
58
- *
59
- * // On the form element:
60
- * // form.additionalRenderers = [priceRendererEntry];
61
- * ```
62
- *
63
- * @public
64
- */
65
- export function createReactRenderer(
66
- Component: ComponentType<RendererControlProps>,
67
- options: {
68
- /**
69
- * The custom element tag name to register. Must be unique and contain a hyphen.
70
- * e.g. 'my-price-renderer'
71
- */
72
- name: string;
73
- /**
74
- * RankedTester from @jsonforms/core — determines when this renderer applies.
75
- * Use rankWith(rank, tester) — rank 5+ recommended to override built-ins.
76
- */
77
- tester: RankedTester;
78
- /**
79
- * Custom mapper if you need to transform state differently.
80
- * Defaults to mapStateToControlProps from @jsonforms/core.
81
- */
82
- mapper?: (state: JsonFormsState, ownProps: OwnPropsOfControl) => StatePropsOfControl;
83
- /**
84
- * Whether to wrap the React component in the platform's control-wrapper template,
85
- * which provides consistent label rendering, error display, and accessibility.
86
- * Defaults to true. Set to false if your React component handles its own labels/errors.
87
- *
88
- * Note: if your component has layout conflicts with the wrapper's internal flex styles
89
- * (e.g. a block-level combobox that shrinks to content width), you can target the
90
- * wrapper's inner element directly in light DOM:
91
- *
92
- * .foundation-control-wrapper { display: block; }
93
- */
94
- wrapWithControlWrapper?: boolean;
95
- },
96
- ): RendererEntry {
97
- const WebComponent = r2wc(Component, {
98
- props: {
99
- data: 'json',
100
- path: 'string',
101
- label: 'string',
102
- errors: 'string',
103
- enabled: 'boolean',
104
- required: 'boolean',
105
- handleChange: 'method',
106
- uischema: 'json',
107
- },
108
- });
109
-
110
- if (!customElements.get(options.name)) {
111
- customElements.define(options.name, WebComponent);
112
- }
113
-
114
- const { name } = options;
115
- const useWrapper = options.wrapWithControlWrapper !== false;
116
-
117
- const innerTemplate = (elementName: string) => html`
118
- <${elementName}
119
- :data=${(x: any) => x.control.data}
120
- :path=${(x: any) => x.control.path}
121
- :label=${(x: any) => x.control.label}
122
- :errors=${(x: any) => x.control.errors}
123
- :enabled=${(x: any) => x.control.enabled}
124
- :required=${(x: any) => x.control.required}
125
- :handleChange=${(x: any) => x.control.handleChange}
126
- :uischema=${(x: any) => x.control.uischema}
127
- ></${elementName}>
128
- `;
129
-
130
- const template = useWrapper
131
- ? html`
132
- <template>
133
- ${controlWrapperTemplate({
134
- prefix: 'zero',
135
- innerTemplate: innerTemplate(name) as any,
136
- })}
137
- </template>
138
- `
139
- : html`
140
- <template>${innerTemplate(name)}</template>
141
- `;
142
-
143
- return {
144
- renderer: template,
145
- tester: options.tester,
146
- mapper: options.mapper ?? mapStateToControlProps,
147
- };
148
- }
package/src/index.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * @genesislcap/foundation-react-utils
3
- *
4
- * Utility functions and helpers for building React applications with Genesis Foundation.
5
- *
6
- * Key exports:
7
- * - `createReactRenderer` — wraps a React component as a `RendererEntry` for use with
8
- * `foundation-forms` `additionalRenderers`. Use this instead of writing raw FAST templates
9
- * when your custom form renderer is authored in React/JSX.
10
- * - `createGridProCellRenderer` — wraps a React component as an AG Grid cell renderer
11
- * component class for use with `grid-pro` (register via `gridOptions.components` or the
12
- * grid's `gridComponents` property).
13
- * - `reactFactory` / `reactFactoryWithProvider` — mount React component trees into
14
- * Genesis Foundation layout regions.
15
- */
16
-
17
- export { reactFactory, reactFactoryWithProvider } from './react-layout-factory';
18
- export { createReactRenderer } from './create-react-renderer';
19
- export { createGridProCellRenderer } from './create-grid-pro-cell-renderer';
20
- export { createGridProCellPortals } from './create-grid-pro-cell-portals';
21
- export type { GridProCellPortals } from './create-grid-pro-cell-portals';
22
- export type {
23
- GridProCellRendererOptions,
24
- GridProCellRendererProps,
25
- GridProReactCellRenderer,
26
- } from './create-grid-pro-cell-renderer';
27
- export type { RendererControlProps } from '@genesislcap/foundation-forms';
@@ -1,81 +0,0 @@
1
- import type { ComponentFactory } from '@genesislcap/foundation-layout';
2
- import * as React from 'react';
3
- import { createRoot, Root } from 'react-dom/client';
4
-
5
- /**
6
- * Creates a factory function for rendering React components in layout items.
7
- *
8
- * @param Component - React component to render
9
- * @param props - Optional props to pass to the component
10
- * @returns ComponentFactory compatible with foundation-layout
11
- *
12
- * @example
13
- * ```tsx
14
- * <foundation-layout-item
15
- * registration="my-component"
16
- * title="My Component"
17
- * factory={reactFactory(MyComponent, { someProp: 'value' })}
18
- * />
19
- * ```
20
- */
21
- export function reactFactory<P = {}>(
22
- Component: React.ComponentType<P>,
23
- props?: P,
24
- ): ComponentFactory {
25
- return (container: HTMLElement) => {
26
- const root: Root = createRoot(container);
27
- root.render(<Component {...(props || ({} as P))} />);
28
-
29
- // Return cleanup function
30
- return () => {
31
- root.unmount();
32
- };
33
- };
34
- }
35
-
36
- /**
37
- * Creates a factory function that wraps a React component with a provider (e.g., Redux Provider, Context Provider).
38
- *
39
- * @param Component - React component to render
40
- * @param Wrapper - Wrapper component (e.g., Redux Provider, Context Provider)
41
- * @param wrapperProps - Props for the wrapper component
42
- * @param componentProps - Optional props for the component
43
- * @returns ComponentFactory compatible with foundation-layout
44
- *
45
- * @example
46
- * ```tsx
47
- * import { Provider } from 'react-redux';
48
- * import { store } from './store';
49
- *
50
- * <foundation-layout-item
51
- * registration="my-component"
52
- * title="My Component"
53
- * factory={reactFactoryWithProvider(
54
- * MyComponent,
55
- * Provider,
56
- * { store },
57
- * { someProp: 'value' }
58
- * )}
59
- * />
60
- * ```
61
- */
62
- export function reactFactoryWithProvider<CP = {}, WP = {}>(
63
- Component: React.ComponentType<CP>,
64
- Wrapper: React.ComponentType<WP & { children: React.ReactNode }>,
65
- wrapperProps: WP,
66
- componentProps?: CP,
67
- ): ComponentFactory {
68
- return (container: HTMLElement) => {
69
- const root: Root = createRoot(container);
70
- root.render(
71
- <Wrapper {...wrapperProps}>
72
- <Component {...(componentProps || ({} as CP))} />
73
- </Wrapper>,
74
- );
75
-
76
- // Return cleanup function
77
- return () => {
78
- root.unmount();
79
- };
80
- };
81
- }
@@ -1,141 +0,0 @@
1
- import type { ComponentType, ReactElement, ReactNode } from 'react';
2
- import { Navigate, Route, Routes } from 'react-router-dom';
3
- import type { BoundProtectedRouteProps } from './protected-route';
4
-
5
- /**
6
- * Declarative description of one app route. The `element` is rendered when the
7
- * path matches; guarding, layout nesting, and permission are derived from the
8
- * flags below so the app doesn't hand-write the `<Route>`/`<ProtectedRoute>`
9
- * boilerplate per route.
10
- */
11
- export interface AppRouteConfig {
12
- path: string;
13
- element: ReactNode;
14
- /** Skip the auth guard (e.g. login, not-permitted). Default `false`. */
15
- public?: boolean;
16
- /** Render outside the shared layout. Default `false` (guarded routes sit in the layout). */
17
- noLayout?: boolean;
18
- /** Permission code handed to the guard's optional permission check. */
19
- permissionCode?: string;
20
- /**
21
- * Nested child routes rendered inside this route's `element` (which must
22
- * render an `<Outlet />`). Children use paths relative to this route and
23
- * inherit its guard — they are rendered as plain routes, not re-guarded.
24
- */
25
- children?: AppRouteConfig[];
26
- /** Arbitrary metadata preserved for other consumers (nav items, PBC info, ...). */
27
- data?: Record<string, any>;
28
- }
29
-
30
- /** A simple `from → to` redirect. */
31
- export interface RouteRedirect {
32
- from: string;
33
- to: string;
34
- }
35
-
36
- /**
37
- * Options for {@link renderAppRoutes}.
38
- */
39
- export interface RenderAppRoutesOptions {
40
- /** The full route table (static + any merged PBC routes). */
41
- routes: AppRouteConfig[];
42
- /**
43
- * Guard component (typically from `createProtectedRoute`) wrapping every
44
- * non-public route; receives each route's `permissionCode`.
45
- */
46
- ProtectedRoute: ComponentType<BoundProtectedRouteProps>;
47
- /**
48
- * Layout route element (rendering an `<Outlet />`) that wraps in-layout
49
- * routes. Omit to render every route at the top level.
50
- */
51
- layout?: ReactElement;
52
- /** `from → to` redirects rendered before the routes. */
53
- redirects?: RouteRedirect[];
54
- /** Element for unmatched paths (`*`). */
55
- notFound?: ReactNode;
56
- /**
57
- * When `name` is truthy, short-circuit to single-component mode: render only
58
- * the public routes (so login / session-restore still work) plus a catch-all
59
- * rendering `element` behind the guard. See the single-component deep-link.
60
- */
61
- singleComponent?: { name: string | null | undefined; element: ReactNode };
62
- }
63
-
64
- const resolvePermissionCode = (route: AppRouteConfig): string | undefined =>
65
- route.permissionCode ?? (route.data?.permissionCode as string | undefined);
66
-
67
- /**
68
- * Build the app's `<Routes>` tree from a declarative route table, applying the
69
- * auth guard, layout nesting, redirects, not-found, and the single-component
70
- * short-circuit — so a consuming app configures routes instead of hand-writing
71
- * repetitive `<Route element={<ProtectedRoute>…}>` markup.
72
- */
73
- export function renderAppRoutes({
74
- routes,
75
- ProtectedRoute,
76
- layout,
77
- redirects = [],
78
- notFound,
79
- singleComponent,
80
- }: RenderAppRoutesOptions): ReactElement {
81
- const publicRoutes = routes.filter((r) => r.public);
82
-
83
- // Single-component mode: keep public routes reachable (login / session
84
- // restore) and gate everything else behind the guard rendering the component.
85
- if (singleComponent?.name) {
86
- return (
87
- <Routes>
88
- {publicRoutes.map((r) => (
89
- <Route key={r.path} path={r.path} element={r.element} />
90
- ))}
91
- <Route path="*" element={<ProtectedRoute>{singleComponent.element}</ProtectedRoute>} />
92
- </Routes>
93
- );
94
- }
95
-
96
- const guarded = routes.filter((r) => !r.public);
97
- const inLayout = guarded.filter((r) => !r.noLayout);
98
- const topLevelGuarded = guarded.filter((r) => r.noLayout);
99
-
100
- // Nested children render as plain relative routes inside the parent's
101
- // `<Outlet />`; they inherit the parent's guard, so they are not re-wrapped.
102
- const renderChild = (c: AppRouteConfig): ReactElement =>
103
- c.children?.length ? (
104
- <Route key={c.path} path={c.path} element={c.element}>
105
- {c.children.map(renderChild)}
106
- </Route>
107
- ) : (
108
- <Route key={c.path} path={c.path} element={c.element} />
109
- );
110
-
111
- const renderGuarded = (r: AppRouteConfig) => {
112
- const element = (
113
- <ProtectedRoute permissionCode={resolvePermissionCode(r)}>{r.element}</ProtectedRoute>
114
- );
115
- return r.children?.length ? (
116
- <Route key={r.path} path={r.path} element={element}>
117
- {r.children.map(renderChild)}
118
- </Route>
119
- ) : (
120
- <Route key={r.path} path={r.path} element={element} />
121
- );
122
- };
123
-
124
- return (
125
- <Routes>
126
- {redirects.map((r) => (
127
- <Route key={`redirect:${r.from}`} path={r.from} element={<Navigate to={r.to} replace />} />
128
- ))}
129
- {publicRoutes.map((r) => (
130
- <Route key={r.path} path={r.path} element={r.element} />
131
- ))}
132
- {topLevelGuarded.map(renderGuarded)}
133
- {layout ? (
134
- <Route element={layout}>{inLayout.map(renderGuarded)}</Route>
135
- ) : (
136
- inLayout.map(renderGuarded)
137
- )}
138
- {notFound ? <Route path="*" element={notFound} /> : null}
139
- </Routes>
140
- );
141
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * `@genesislcap/foundation-react-utils/router`
3
- *
4
- * Reusable `react-router-dom` primitives for Genesis Foundation React apps, so
5
- * the app-shell routing logic doesn't have to be reimplemented per app.
6
- *
7
- * Exposed as a subpath (`/router`) rather than from the package root so that
8
- * consumers which don't route (and don't depend on `react-router-dom`) are
9
- * unaffected.
10
- *
11
- * Key exports:
12
- * - `ProtectedRoute` / `createProtectedRoute` — auth + permission gate that
13
- * stashes the full origin location (incl. `search` + `hash`) for post-login
14
- * restore, and redirects to a not-permitted path when authorized-but-blocked.
15
- * - `buildPostLoginRedirect` — rebuild the return URL preserving query + hash.
16
- * - `readInitialParam` / `createComponentRegistry` / `SingleComponentOutlet` —
17
- * render a single registered component full-screen from a `?param=<name>`
18
- * deep-link captured before the shell strips the query string.
19
- * - `renderAppRoutes` — build the app's `<Routes>` from a declarative route
20
- * table (guarding, layout nesting, redirects, not-found, single-component).
21
- * - `mergePbcRoutes` — merge static routes with shell/PBC routes into one table.
22
- */
23
-
24
- export { ProtectedRoute, createProtectedRoute } from './protected-route';
25
- export type {
26
- ProtectedRouteProps,
27
- BoundProtectedRouteProps,
28
- CreateProtectedRouteOptions,
29
- } from './protected-route';
30
- export { buildPostLoginRedirect } from './post-login-redirect';
31
- export type { RedirectableLocationState } from './post-login-redirect';
32
- export {
33
- readInitialParam,
34
- createComponentRegistry,
35
- SingleComponentOutlet,
36
- } from './single-component';
37
- export type { ComponentRegistry, SingleComponentOutletProps } from './single-component';
38
- export { renderAppRoutes } from './app-routes';
39
- export type { AppRouteConfig, RouteRedirect, RenderAppRoutesOptions } from './app-routes';
40
- export { mergePbcRoutes } from './pbc-routes';
41
- export type { PbcRouteInput, MergePbcRoutesOptions } from './pbc-routes';
@@ -1,56 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import type { AppRouteConfig } from './app-routes';
3
-
4
- /**
5
- * Minimal shape of a PBC route as provided by the shell (e.g. `getApp().routes`).
6
- * Kept as an input type — rather than importing `foundation-shell` — so this
7
- * helper stays dependency-free and the `/router` subpath doesn't pull the shell
8
- * into apps that don't use PBCs.
9
- */
10
- export interface PbcRouteInput {
11
- path: string;
12
- title?: string;
13
- /** The PBC element (module/loader) the container will mount. */
14
- element?: unknown;
15
- /** Explicit custom-element tag, if the container shouldn't derive one. */
16
- elementTag?: string;
17
- /** Nav items contributed by this PBC. */
18
- navItems?: unknown;
19
- /** Extra per-route settings (e.g. `permissionCode`), spread into `data`. */
20
- settings?: Record<string, unknown>;
21
- }
22
-
23
- /**
24
- * Options for {@link mergePbcRoutes}.
25
- */
26
- export interface MergePbcRoutesOptions {
27
- /** Render the element for a PBC route (typically `() => <PBCContainer />`). */
28
- renderPbc: (pbc: PbcRouteInput) => ReactNode;
29
- }
30
-
31
- /**
32
- * Merge a static route table with PBC routes (e.g. from `getApp().routes`) into
33
- * one {@link AppRouteConfig} array. PBC routes are guarded, in-layout, and carry
34
- * their element/tag + navItems through `data` so downstream consumers (a PBC
35
- * container, nav-item derivation) can read them — matching the `data` shape the
36
- * hand-rolled version produced.
37
- */
38
- export function mergePbcRoutes(
39
- staticRoutes: AppRouteConfig[],
40
- pbcRoutes: PbcRouteInput[],
41
- { renderPbc }: MergePbcRoutesOptions,
42
- ): AppRouteConfig[] {
43
- const mapped: AppRouteConfig[] = pbcRoutes.map((pbc) => ({
44
- path: `/${pbc.path}`,
45
- element: renderPbc(pbc),
46
- permissionCode: pbc.settings?.permissionCode as string | undefined,
47
- data: {
48
- ...pbc.settings,
49
- pbcElement: pbc.element,
50
- pbcElementTag: pbc.elementTag,
51
- navItems: pbc.navItems,
52
- },
53
- }));
54
-
55
- return [...staticRoutes, ...mapped];
56
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Shape of the router location `state` that a route guard stashes before
3
- * redirecting to login. Structural, so it works with `react-router-dom`'s
4
- * `Location` without importing it here.
5
- */
6
- export interface RedirectableLocationState {
7
- from?: {
8
- pathname: string;
9
- search?: string;
10
- hash?: string;
11
- };
12
- }
13
-
14
- /**
15
- * Build the post-login redirect target from a router location's `state.from`,
16
- * preserving the query string and hash so deep-link params (e.g.
17
- * `?component=<name>`) survive the login bounce. Falls back to `defaultPath`
18
- * when there is no stashed origin.
19
- *
20
- * @param location - The current router location (needs only `state.from`).
21
- * @param defaultPath - Where to land when nothing was stashed. Defaults to `/`.
22
- */
23
- export function buildPostLoginRedirect(
24
- location: { state?: RedirectableLocationState | null },
25
- defaultPath = '/',
26
- ): string {
27
- const from = location.state?.from;
28
- return from ? `${from.pathname}${from.search ?? ''}${from.hash ?? ''}` : defaultPath;
29
- }
@@ -1,121 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import { Navigate, useLocation } from 'react-router-dom';
3
-
4
- /**
5
- * Props for {@link ProtectedRoute}.
6
- */
7
- export interface ProtectedRouteProps {
8
- /** Whether the current user is authenticated. */
9
- isAuthenticated: boolean;
10
- /**
11
- * Whether the current user may view this route. When `false` (and
12
- * authenticated), redirects to `notPermittedPath` instead of rendering.
13
- * Defaults to `true`.
14
- */
15
- hasPermission?: boolean;
16
- /** Path to redirect to when unauthenticated. Defaults to `/login`. */
17
- loginPath?: string;
18
- /** Path to redirect to when authenticated but not permitted. Defaults to `/not-permitted`. */
19
- notPermittedPath?: string;
20
- children: ReactNode;
21
- }
22
-
23
- /**
24
- * Route guard. In order:
25
- * - not authenticated → redirect to `loginPath`, stashing the full current
26
- * location in `state.from` (incl. `search` + `hash`) so the login flow can
27
- * restore the original deep-link via {@link buildPostLoginRedirect};
28
- * - authenticated but `hasPermission === false` → redirect to `notPermittedPath`;
29
- * - otherwise → render `children`.
30
- *
31
- * Deliberately decoupled from any auth package — pass the booleans in (or use
32
- * {@link createProtectedRoute} to bind the checks once).
33
- */
34
- export function ProtectedRoute({
35
- isAuthenticated,
36
- hasPermission = true,
37
- loginPath = '/login',
38
- notPermittedPath = '/not-permitted',
39
- children,
40
- }: ProtectedRouteProps) {
41
- const location = useLocation();
42
-
43
- if (!isAuthenticated) {
44
- return <Navigate to={loginPath} state={{ from: location }} replace />;
45
- }
46
-
47
- if (!hasPermission) {
48
- return <Navigate to={notPermittedPath} replace />;
49
- }
50
-
51
- return <>{children}</>;
52
- }
53
-
54
- /**
55
- * Props of the component returned by {@link createProtectedRoute}. `permissionCode`
56
- * is forwarded to the bound `hasPermission` check (e.g. by {@link renderAppRoutes}).
57
- */
58
- export interface BoundProtectedRouteProps {
59
- children: ReactNode;
60
- /** Route permission code handed to the bound `hasPermission` check. */
61
- permissionCode?: string;
62
- }
63
-
64
- /**
65
- * Options for {@link createProtectedRoute}.
66
- */
67
- export interface CreateProtectedRouteOptions {
68
- /**
69
- * Called at render time to determine auth state. Kept as a function (not a
70
- * boolean) so it is re-evaluated on every render and the util stays free of
71
- * any specific auth dependency.
72
- */
73
- getIsAuthenticated: () => boolean;
74
- /**
75
- * Optional per-route permission check, called at render time with the route's
76
- * `permissionCode`. Absent → every authenticated user is permitted.
77
- */
78
- hasPermission?: (permissionCode: string | undefined) => boolean;
79
- /** Path to redirect to when unauthenticated. Defaults to `/login`. */
80
- loginPath?: string;
81
- /** Path to redirect to when authenticated but not permitted. Defaults to `/not-permitted`. */
82
- notPermittedPath?: string;
83
- }
84
-
85
- /**
86
- * Bind auth (and optionally permission) checks once and get an ergonomic
87
- * `<ProtectedRoute>` that only needs `children` (plus an optional
88
- * `permissionCode`) — ideal when a route table wraps many elements.
89
- *
90
- * **Call this at module scope, never inside a component's render.** It returns a
91
- * new component type each call; invoking it during render recreates that type
92
- * every render, forcing React to unmount and remount the entire protected
93
- * subtree (flicker + lost state).
94
- *
95
- * @example
96
- * ```tsx
97
- * const ProtectedRoute = createProtectedRoute({
98
- * getIsAuthenticated: () => getUser().isAuthenticated,
99
- * hasPermission: (code) => !code || canView(getUser(), code),
100
- * });
101
- * ```
102
- */
103
- export function createProtectedRoute({
104
- getIsAuthenticated,
105
- hasPermission,
106
- loginPath,
107
- notPermittedPath,
108
- }: CreateProtectedRouteOptions) {
109
- return function BoundProtectedRoute({ children, permissionCode }: BoundProtectedRouteProps) {
110
- return (
111
- <ProtectedRoute
112
- isAuthenticated={getIsAuthenticated()}
113
- hasPermission={hasPermission ? hasPermission(permissionCode) : true}
114
- loginPath={loginPath}
115
- notPermittedPath={notPermittedPath}
116
- >
117
- {children}
118
- </ProtectedRoute>
119
- );
120
- };
121
- }