diff --git a/package.json b/package.json index de37b27..c3cca4b 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,11 @@ "prestart:app": "wait-on --log --timeout 30000 http-get://localhost:4000", "start:app": "vite", "build": "tsc && vite build", - "preview": "concurrently \"npm run localSettings\" \"npm run vite-preview\"", - "vite-preview": "vite preview", + "prepreview": "cross-env npm run build", + "preview": "concurrently --kill-others --success first \"npm:preview:*\"", + "preview:connectionStrings": "cross-env PORT=4000 node --watch -- public/connectionStrings.cjs", + "prepreview:app": "wait-on --log --timeout 30000 http-get://localhost:4000", + "preview:app": "cross-env PORT=4321 vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "test": "vitest", "test:watch": "vitest --watch", diff --git a/src/components/layout/sideBar/menuItem/types.ts b/src/components/layout/sideBar/menuItem/types.ts index c3d59a5..19c6ce6 100644 --- a/src/components/layout/sideBar/menuItem/types.ts +++ b/src/components/layout/sideBar/menuItem/types.ts @@ -7,6 +7,7 @@ export type MainSectionType = { path?: string; authenticatedOnly?: boolean; component?: ReactNode; + lazy?: () => Promise<{ default: React.ComponentType }>; permissions?: string[]; }; @@ -14,5 +15,4 @@ export type SubsectionMenuItemType = { isInMenu: boolean; icon?: IconType; externalUrl?: string; - isEntryPoint?: boolean; } & MainSectionType; diff --git a/src/features/formWizard/formWizard.tsx b/src/features/formWizard/formWizard.tsx index a732389..de2fbad 100644 --- a/src/features/formWizard/formWizard.tsx +++ b/src/features/formWizard/formWizard.tsx @@ -5,10 +5,8 @@ import * as z from "zod"; import { CheckboxControl, InputControl } from "../../components/formControls"; import { Wizard, WizardPage } from "../../components/wizard/wizard"; +import { delay } from "../../lib/helper"; -const sleep = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - - const _wizardSchema = z.object({ firstName: z.string().min(6), lastname: z.string().min(6), @@ -31,7 +29,7 @@ export default function WizardFormView() { const { t } = useTranslation(); const onSubmit = async (values: Schema) => { - await sleep(300); + await delay(300); window.alert(JSON.stringify(values, null, 2)); }; @@ -39,31 +37,31 @@ export default function WizardFormView() { {t("wizard_form_title")} - + onSubmit={onSubmit} formProps={{ mode: 'onChange', resolver: zodResolver(_wizardSchema) }} > - - - - + + + + - - + + - - + + diff --git a/src/lib/authentication/components/protectedRoute.tsx b/src/lib/authentication/components/protectedRoute.tsx index cb20d2b..82d75bd 100644 --- a/src/lib/authentication/components/protectedRoute.tsx +++ b/src/lib/authentication/components/protectedRoute.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import type { PropsWithChildren } from "react"; import { useAppSelector } from "../../../app/hooks"; import { userSelector } from "../authenticationSlice"; @@ -6,10 +6,9 @@ import Unauthorized from "../unauthorized"; type ProtectedRouteProps = { permissions?: string[]; - children: React.ReactNode; }; -const ProtectedRoute = ({ permissions, children }: ProtectedRouteProps) => { +const ProtectedRoute = ({ permissions, children }: PropsWithChildren) => { const user = useAppSelector(userSelector); const userPermissions = user?.permissions ?? ([] as string[]); const hasAllPermissions = permissions ? permissions.every(permission => userPermissions.includes(permission)) : true; diff --git a/src/lib/router.tsx b/src/lib/router.tsx index d45e409..42b7511 100644 --- a/src/lib/router.tsx +++ b/src/lib/router.tsx @@ -1,8 +1,11 @@ +import type { ReactNode } from "react"; import { Else, If, Then } from "react-if"; -import { Outlet, Route, createBrowserRouter, createRoutesFromChildren } from "react-router-dom"; +import type { RouteObject } from "react-router-dom"; +import { Outlet, createBrowserRouter } from "react-router-dom"; import Layout from "../components/layout/layout"; import type { MainSectionType, SubsectionMenuItemType } from "../components/layout/sideBar/menuItem/types"; +import LazyLoad from "../components/lazyLoad"; import PageNotFound from "../components/pageNotFound"; import SEO from "../components/seo"; import { mainSections } from "../siteMap/mainSections"; @@ -13,9 +16,16 @@ import ProtectedRoute from "./authentication/components/protectedRoute"; import Unauthorized from "./authentication/unauthorized"; import { ErrorFallback } from "./errorFallback"; -const wrapComponent = (x: MainSectionType) => { +const wrapLazy = (x: MainSectionType) => { const checkPermissions = x.permissions && x.permissions.length > 0; + let element: ReactNode = ; + if (x.component) element = x.component; + const lazy = x.lazy; + // key={x.label} is needed to force a rerender when the route changes due to https://github.com/remix-run/react-router/issues/12474 + // assumption: x.label is unique across all routes + if (lazy) element = ; + return ( <> @@ -25,14 +35,22 @@ const wrapComponent = (x: MainSectionType) => { {() => ( - {x.component ?? } + + {element} + )} - {() => {x.component ?? }} + + {() => + + {element} + + } + - {() => <>{x.component ?? }} + {element} ); @@ -41,48 +59,57 @@ const wrapComponent = (x: MainSectionType) => { const renderSections = (s?: SubsectionMenuItemType[]) => { return s ?.filter(x => x.path !== undefined) - .map((x, i) => + .map((x): RouteObject => x.path === "" && !x.subsections ? ( // index route, shall not have children - + { index: true, element: wrapLazy(x) } ) : ( - - {renderSections(x.subsections)} - + { path: x.path, element: wrapLazy(x), children: renderSections(x.subsections) } ), ); }; const routes = mainSections .filter(x => x.path !== undefined) - .map((x, i) => + .map((x): RouteObject => x.path === "" && !x.subsections ? ( // index route, shall not have children - + { index: true, element: wrapLazy(x) } ) : ( - - {renderSections(x.subsections)} - + { path: x.path, element: wrapLazy(x), children: renderSections(x.subsections) } ), ); -export const router = createBrowserRouter( - createRoutesFromChildren( - }> - }> - } /> - } /> - {routes} - } /> - } /> - - , - ), +export const router = createBrowserRouter([ + { + path: "/", + Component: Layout, + children: [ + { + errorElement: , + children: [ + { + path: "auth-callback", + Component: AuthenticationCallback + }, + { + path: "Unauthorized", + Component: Unauthorized + }, + ...routes, + { + path: "null", + element: null + }, + { + path: "*", + Component: PageNotFound + } + ] + } + ] + } +], { future: { - v7_relativeSplatPath: true, - v7_fetcherPersist: true, - v7_normalizeFormMethod: true, - v7_partialHydration: true, - v7_skipActionErrorRevalidation: true } } diff --git a/src/react.d.ts b/src/react.d.ts new file mode 100644 index 0000000..8b062ef --- /dev/null +++ b/src/react.d.ts @@ -0,0 +1,4 @@ +// reason: https://stackoverflow.com/a/71017028 +declare namespace React { + function lazy>(factory: () => Promise<{ default: T }>): T; +} diff --git a/src/siteMap/mainSections.tsx b/src/siteMap/mainSections.tsx index 6655cd6..9ebc3f3 100644 --- a/src/siteMap/mainSections.tsx +++ b/src/siteMap/mainSections.tsx @@ -15,7 +15,6 @@ import { Navigate } from "react-router-dom"; import { Bomb } from "../components/Bomb"; import type { MainSectionType } from "../components/layout/sideBar/menuItem/types"; -import LazyLoad from "../components/lazyLoad"; import DetailsPage from "../features/detailsPage/detailsPage"; import DetailsPageExampleMainView from "../features/detailsPage/detailsPageExampleMainView"; import NoEntryPoint from "../features/staticPageExample/staticPage"; @@ -31,34 +30,30 @@ export const mainSections: MainSectionType[] = [ path: "", label: "index", isInMenu: false, - component: , - isEntryPoint: true, + component: }, { path: "jsonplaceholder", label: "Posts", icon: FaCloudUploadAlt, isInMenu: true, - component: import("../features/fetchApiExample/JsonPlaceHolder")} />, - isEntryPoint: true, + lazy: async () => import("../features/fetchApiExample/JsonPlaceHolder"), }, { path: "playground", label: "PlayGround", icon: FaPlay, isInMenu: true, - component: ( - import("../features/notificationPlayground/notificationPlaygroundView")} /> - ), + lazy: async () => import("../features/notificationPlayground/notificationPlaygroundView") + }, { path: "globalLoadingBar", label: "GlobalLoadingBar", icon: FaPlay, isInMenu: true, - component: ( - import("../features/globalLoadingBar/globalLoadingPage")} /> - ), + lazy: async () => import("../features/globalLoadingBar/globalLoadingPage") + }, { path: "permissionsPlayground", @@ -66,9 +61,8 @@ export const mainSections: MainSectionType[] = [ authenticatedOnly: true, icon: CiLock, isInMenu: true, - component: ( - import("../features/permissionsPlayground/permissionsPlaygroundView")} /> - ), + lazy: async () => import("../features/permissionsPlayground/permissionsPlaygroundView") + }, { path: "protectedRoute", @@ -77,7 +71,8 @@ export const mainSections: MainSectionType[] = [ permissions: ["grant:admin"], icon: CiLock, isInMenu: false, - component: import("../features/permissionsPlayground/protectedRouteView")} />, + lazy: async () => import("../features/permissionsPlayground/protectedRouteView") + , }, { @@ -85,21 +80,24 @@ export const mainSections: MainSectionType[] = [ label: "Config Table", icon: FaTable, isInMenu: true, - component: import("../features/configTable/configTableExample")} />, + lazy: async () => import("../features/configTable/configTableExample") + , }, { path: "moviesTable", label: "Movie Paginated Table", icon: RiMovie2Line, isInMenu: true, - component: import("../features/paginatedTable/moviePage")} />, + lazy: async () => import("../features/paginatedTable/moviePage") + , }, { path: "videoGamesTable", label: "VideoGames Table", icon: FaGamepad, isInMenu: true, - component: import("../features/formExample/videoGamesPage")} />, + lazy: async () => import("../features/formExample/videoGamesPage") + , }, { @@ -107,14 +105,16 @@ export const mainSections: MainSectionType[] = [ label: "Wizard Form", icon: FaTable, isInMenu: true, - component: import("../features/formWizard/formWizard")} />, + lazy: async () => import("../features/formWizard/formWizard") + , }, { path: "translation", label: "Translation Sample", icon: FaFlag, isInMenu: true, - component: import("../features/localization/localizationPage")} />, + lazy: async () => import("../features/localization/localizationPage") + , }, { @@ -123,7 +123,8 @@ export const mainSections: MainSectionType[] = [ icon: FaLock, isInMenu: true, authenticatedOnly: true, - component: import("../features/authentication/authInfoPage")} />, + lazy: async () => import("../features/authentication/authInfoPage") + , }, { path: "bomb", @@ -213,16 +214,3 @@ export const mainSections: MainSectionType[] = [ }, ]; -export function getEntryPointPath(sections: MainSectionType[]): string { - for (const section of sections) { - for (const subsection of section.subsections ?? []) { - if (subsection.path === "/") { - return "/"; - } - if (subsection.isEntryPoint) { - return (section.path ?? "" + subsection.path) || "/"; - } - } - } - return "/"; -}