From 163eb3c3f319d24b4dca88c1091993e03ee28249 Mon Sep 17 00:00:00 2001 From: Bruno Tot Date: Sat, 16 Nov 2024 14:32:36 +0100 Subject: [PATCH] chore: add frontend visual fixes --- .../controllers/UserController.ts | 8 ++ .../app-vite-react/package.json | 1 + .../BoundarySuspenseGroup.tsx | 20 +++ .../components/BoundarySuspenseGroup/index.ts | 1 + .../app/components/Datatable/Datatable.tsx | 13 ++ .../DatatableContainer/DatatableContainer.tsx | 2 + .../impl/ClientDatatable/ClientDatatable.tsx | 33 ++--- .../Datatable/impl/ClientDatatable/types.ts | 14 +-- .../impl/ServerDatatable/ServerDatatable.tsx | 24 ++-- .../Datatable/impl/ServerDatatable/types.ts | 16 +-- .../src/app/components/Datatable/types.ts | 12 +- .../components/Header/ComputedBreadcrumbs.tsx | 47 +++---- .../LoaderSuspense/LoaderSuspense.tsx | 16 +++ .../app/components/LoaderSuspense/index.ts | 1 + .../src/app/components/Protect/Protect.tsx | 53 ++------ .../QueryErrorBoundary/QueryErrorBoundary.tsx | 43 +++++++ .../components/QueryErrorBoundary/index.ts | 1 + .../components/ClientResponsiveTable.tsx | 18 +-- .../components/TableRowMobile.tsx | 27 ++++ .../admin-settings/manage-users/index.tsx | 115 +++++++++--------- .../manage-users/pages/create-user/index.tsx | 20 +-- .../manage-users/pages/edit-user/index.tsx | 16 ++- .../src/app/provider/ConfirmProvider.tsx | 8 +- .../src/app/provider/SnackbarProvider.tsx | 8 +- .../app-vite-react/src/app/routes.tsx | 35 ++---- .../src/lib/@tanstack/QueryClientProvider.tsx | 8 +- .../src/lib/keycloak-js/KeycloakProvider.tsx | 2 +- .../src/lib/keycloak-js/KeycloakRoute.tsx | 4 +- .../app-vite-react/src/main.tsx | 15 +-- .../app-vite-react/src/server/ReactApp.tsx | 80 ++++++------ .../src/server/route-typings.ts | 19 ++- .../src/app/models/form/UserForm.ts | 13 +- pnpm-lock.yaml | 12 ++ 33 files changed, 392 insertions(+), 313 deletions(-) create mode 100644 packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/BoundarySuspenseGroup.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/index.ts create mode 100644 packages/mern-sample-app/app-vite-react/src/app/components/Datatable/Datatable.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/LoaderSuspense.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/index.ts create mode 100644 packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/QueryErrorBoundary.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/index.ts create mode 100644 packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/TableRowMobile.tsx diff --git a/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts b/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts index ac8d4388..f919ce91 100644 --- a/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts +++ b/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts @@ -16,6 +16,14 @@ export class UserController { @contract(contracts.User.findAll, withRouteSecured(Role.Enum["avr-admin"])) async findAll(): RouteOutput { + /*return { + status: 500, + body: { + message: "Not implemented", + status: 500, + timestamp: new Date().toISOString(), + }, + };*/ return { status: 200, body: await this.userService.findAll(), diff --git a/packages/mern-sample-app/app-vite-react/package.json b/packages/mern-sample-app/app-vite-react/package.json index c3f5f1d5..d1478a54 100644 --- a/packages/mern-sample-app/app-vite-react/package.json +++ b/packages/mern-sample-app/app-vite-react/package.json @@ -40,6 +40,7 @@ "material-ui-popup-state": "^5.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.1.2", "react-hook-form": "^7.53.1", "react-i18next": "^14.1.0", "react-router-dom": "^6.22.3" diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/BoundarySuspenseGroup.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/BoundarySuspenseGroup.tsx new file mode 100644 index 00000000..7401c215 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/BoundarySuspenseGroup.tsx @@ -0,0 +1,20 @@ +import type { NavigationRouteProtectParam } from "@org/app-vite-react/server/route-typings"; + +import { LoaderSuspense } from "../LoaderSuspense"; +import { Protect } from "../Protect"; +import { QueryErrorBoundary } from "../QueryErrorBoundary"; + +export type BoundarySuspenseGroupProps = { + protect?: NavigationRouteProtectParam; + children: React.ReactNode; +}; + +export function BoundarySuspenseGroup({ protect, children }: BoundarySuspenseGroupProps) { + return ( + + + {children} + + + ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/index.ts b/packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/index.ts new file mode 100644 index 00000000..5346d38a --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/components/BoundarySuspenseGroup/index.ts @@ -0,0 +1 @@ +export * from "./BoundarySuspenseGroup"; diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/Datatable.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/Datatable.tsx new file mode 100644 index 00000000..865fda9c --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/Datatable.tsx @@ -0,0 +1,13 @@ +import { + type ServerDatatableProps, + type ClientDatatableProps, + ClientDatatable, + ServerDatatable, +} from "./impl"; + +export type DatatableProps = ServerDatatableProps | ClientDatatableProps; + +export function Datatable(props: DatatableProps) { + if (props.sync) return ; + return ; +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/components/DatatableContainer/DatatableContainer.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/components/DatatableContainer/DatatableContainer.tsx index 86ac033f..9298ec2b 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/components/DatatableContainer/DatatableContainer.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/components/DatatableContainer/DatatableContainer.tsx @@ -4,6 +4,8 @@ import type { PropsWithChildren } from "react"; import { Card } from "@mui/material"; export function DatatableContainer({ children }: PropsWithChildren) { + // return <>{children}; + //return {children}; return {children}; } diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx index 5a7b3403..5c8a0255 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx @@ -13,27 +13,33 @@ import { TablePagination, } from "@mui/material"; import { DtSortableCell } from "@org/app-vite-react/app/components/Datatable/components/DtSortableCell/DtSortableCell"; -import { DEFAULT_PAGINATION_OPTIONS } from "@org/app-vite-react/app/components/Datatable/types"; import { ClientResponsiveTable } from "@org/app-vite-react/app/pages/admin-settings/manage-users/components"; import { Fragment, useMemo, useState } from "react"; export function ClientDatatable(props: ClientDatatableProps) { - const { data, columns, disablePagination = false, renderMobileRow } = props; + const { + data, + columns, + pagination, + onPaginationChange, + disablePagination = false, + renderMobileRow, + keyMapper, + } = props; const matchesMobile = mui.useMediaQuery("(max-width:678px)"); const [sortData, setSortData] = useState([]); - const [paginationOptions, setPaginationOptions] = useState(DEFAULT_PAGINATION_OPTIONS); const onPageChange = (newPage: number) => { - setPaginationOptions({ ...paginationOptions, page: newPage }); + onPaginationChange({ ...pagination, page: newPage }); }; const onRowsPerPageChange = (newRowsPerPage: number) => { - setPaginationOptions({ ...paginationOptions, rowsPerPage: newRowsPerPage }); + onPaginationChange({ ...pagination, rowsPerPage: newRowsPerPage }); }; const filteredData = useMemo(() => { if (disablePagination) return data; - const { page, rowsPerPage } = paginationOptions; + const { page, rowsPerPage } = pagination; let localData = data; if (sortData.length > 0) { localData = [...data].sort((a, b) => { @@ -49,7 +55,7 @@ export function ClientDatatable(props: ClientDatatableProps) { } return localData.slice(page * rowsPerPage, (page + 1) * rowsPerPage); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, paginationOptions, disablePagination, sortData]); + }, [data, pagination, disablePagination, sortData]); // eslint-disable-next-line @typescript-eslint/no-unused-vars const onSortColumnClick = (id: string, _event: MouseEvent) => { @@ -81,8 +87,8 @@ export function ClientDatatable(props: ClientDatatableProps) { labelDisplayedRows={({ from, to, count }) => `${from}-${to} to ${count}`} rowsPerPageOptions={[5, 10, 25, 50, 100]} count={data.length} - page={paginationOptions.page} - rowsPerPage={paginationOptions.rowsPerPage} + page={pagination.page} + rowsPerPage={pagination.rowsPerPage} showFirstButton showLastButton onPageChange={(_, newPage) => onPageChange(newPage)} @@ -134,13 +140,8 @@ export function ClientDatatable(props: ClientDatatableProps) { - {filteredData.map((item, i) => ( - + {filteredData.map(item => ( + {columns.map(({ id, align, renderBody }) => ( {renderBody(item, { cleanup: () => {} })} diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts index 464f3f71..9883c461 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts @@ -1,14 +1,6 @@ -import type { DtBaseColumn } from "@org/app-vite-react/app/components/Datatable/types"; +import type { DtBaseProps } from "@org/app-vite-react/app/components/Datatable/types"; -export type DtClientColumnSort = (o1: T, o2: T) => number; - -export type DtClientColumn = DtBaseColumn & { - sort?: DtClientColumnSort; -}; - -export type ClientDatatableProps = { - data: T[]; - columns: DtClientColumn[]; +export type ClientDatatableProps = DtBaseProps number> & { + sync: true; disablePagination?: boolean; - renderMobileRow: (item: T) => React.ReactNode; }; diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/ServerDatatable.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/ServerDatatable.tsx index b4f833bb..8867afcd 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/ServerDatatable.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/ServerDatatable.tsx @@ -19,22 +19,22 @@ export function ServerDatatable({ data, columns, keyMapper, - paginationOptions, - onPaginationOptionsChange, + pagination, + onPaginationChange, count, }: ServerDatatableProps) { const sortData = - paginationOptions?.order.map((order: TODO) => { + pagination?.order.map((order: TODO) => { const [id, direction] = order.split(" "); return { id, direction } as DtBaseSortItem; }) ?? []; const onPageChange = (newPage: number) => { - onPaginationOptionsChange({ ...paginationOptions, page: newPage }); + onPaginationChange({ ...pagination, page: newPage }); }; const onRowsPerPageChange = (newRowsPerPage: number) => { - onPaginationOptionsChange({ ...paginationOptions, rowsPerPage: newRowsPerPage }); + onPaginationChange({ ...pagination, rowsPerPage: newRowsPerPage }); }; const onSortColumnClick = useCallback( @@ -43,19 +43,19 @@ export function ServerDatatable({ //console.log(_event); const sortIndex = sortData.findIndex((v: TODO) => v.id === id); if (sortIndex < 0) { - onPaginationOptionsChange({ ...paginationOptions, order: [`${id} asc`] }); + onPaginationChange({ ...pagination, order: [`${id} asc`] }); return; } const sortProps = sortData[sortIndex]; const oldDirection = sortProps.direction; if (oldDirection === "desc") { - onPaginationOptionsChange({ ...paginationOptions, order: [] }); + onPaginationChange({ ...pagination, order: [] }); return; } - onPaginationOptionsChange({ ...paginationOptions, order: [`${id} desc`] }); + onPaginationChange({ ...pagination, order: [`${id} desc`] }); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [paginationOptions, sortData], + [pagination, sortData], ); return ( @@ -96,7 +96,7 @@ export function ServerDatatable({ {columns.map(({ id, align, renderBody }) => ( - {renderBody(item)} + {renderBody(item, { cleanup: () => {} })} ))} @@ -111,8 +111,8 @@ export function ServerDatatable({ labelDisplayedRows={({ from, to, count }) => `${from}-${to} to ${count}`} rowsPerPageOptions={[10, 25, 50, 100]} count={count} - page={paginationOptions?.page ?? 0} - rowsPerPage={paginationOptions?.rowsPerPage ?? 0} + page={pagination?.page ?? 0} + rowsPerPage={pagination?.rowsPerPage ?? 0} showFirstButton showLastButton onPageChange={(_, newPage) => onPageChange(newPage)} diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/types.ts b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/types.ts index fd40b186..2067e125 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/types.ts +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ServerDatatable/types.ts @@ -1,16 +1,6 @@ -import type { DtBaseColumn } from "@org/app-vite-react/app/components/Datatable/types"; +import type { DtBaseProps } from "@org/app-vite-react/app/components/Datatable/types"; -import { type PaginationOptions } from "@org/lib-api-client"; - -export type DtServerColumn = DtBaseColumn & { - sort?: string; -}; - -export type ServerDatatableProps = { - data: T[]; - columns: DtServerColumn[]; - keyMapper: (value: T) => string; +export type ServerDatatableProps = DtBaseProps & { + sync?: false; count: number; - paginationOptions: PaginationOptions; - onPaginationOptionsChange: (paginationOptions: PaginationOptions) => void; }; diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts index c88bf6aa..9cd7c717 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts @@ -9,11 +9,21 @@ export type DtBaseColumnRenderHeader = () => ReactNode; export type DtBaseColumnRenderBody = (value: T, config: DtBaseColumnConfig) => ReactNode; export type DtBaseOrder = DtBaseSortItem[]; export type DtBaseSortItem = { id: string; direction: "asc" | "desc" }; -export type DtBaseColumn = { +export type DtBaseColumn = { id: string; align?: DtBaseColumnAlign; renderHeader: DtBaseColumnRenderHeader; renderBody: DtBaseColumnRenderBody; + sort?: SORT; +}; + +export type DtBaseProps = { + data: T[]; + columns: DtBaseColumn[]; + renderMobileRow: (item: T) => React.ReactNode; + keyMapper: (value: T) => string; + pagination: PaginationOptions; + onPaginationChange: (paginationOptions: PaginationOptions) => void; }; export const DEFAULT_PAGINATION_OPTIONS: PaginationOptions = { diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Header/ComputedBreadcrumbs.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Header/ComputedBreadcrumbs.tsx index e6721e57..e9b317ed 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Header/ComputedBreadcrumbs.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Header/ComputedBreadcrumbs.tsx @@ -1,47 +1,35 @@ +import type { NavigationRouteHandle } from "@org/app-vite-react/server/route-typings"; import type { TODO } from "@org/lib-commons"; import * as icons from "@mui/icons-material"; import * as mui from "@mui/material"; import { sigDirection } from "@org/app-vite-react/app/signals/sigDirection"; -import { useState } from "react"; +import { useTranslation } from "@org/app-vite-react/lib/i18next"; +import { useMemo, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; import { type UIMatch, useMatches } from "react-router-dom"; -type Crumb = { +export type NavigationCrumb = { match: UIMatch; label: string; }; -function convertToCrumbs(matches: UIMatch[]): /*Crumb[]*/ TODO[] { - const crumbs: Crumb[] = []; +function useNavigationCrumbs(): NavigationCrumb[] { + const matches: UIMatch[] = useMatches(); + const t = useTranslation(); - //console.log(matches); - - for (const match of matches) { - const handle: TODO = match.handle; - //console.log(match); - - if (handle?.crumb) { - crumbs.push({ + const crumbs: NavigationCrumb[] = useMemo(() => { + const c: NavigationCrumb[] = []; + for (const match of matches) { + const handle = match.handle as NavigationRouteHandle; + if (!handle?.crumb) continue; + c.push({ match: match, - label: handle?.crumb?.(match.params) || undefined, + label: handle.crumb(t, match.params), }); } - - /*if ( - "handle" in match && - match.handle && - typeof match.handle === "object" && - "crumb" in match.handle && - match.handle.crumb && - typeof match.handle.crumb === "function" - ) { - crumbs.push({ - match: match, - label: handle?.crumb?.(match.params) || undefined, - }); - }*/ - } + return c; + }, [t, matches]); return crumbs; } @@ -92,9 +80,8 @@ export function LocalBreadcrumbs({ } export function ComputedBreadcrumbs() { + const crumbs = useNavigationCrumbs(); const matchesDesktop = mui.useMediaQuery("(min-width:678px)"); - const matches = useMatches(); - const crumbs = convertToCrumbs(matches); const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/LoaderSuspense.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/LoaderSuspense.tsx new file mode 100644 index 00000000..051d9cf1 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/LoaderSuspense.tsx @@ -0,0 +1,16 @@ +import * as mui from "@mui/material"; +import { Suspense } from "react"; + +export function LoaderSuspense({ children }: React.PropsWithChildren) { + return ( + + + + } + > + {children} + + ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/index.ts b/packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/index.ts new file mode 100644 index 00000000..3d44eb0f --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/components/LoaderSuspense/index.ts @@ -0,0 +1 @@ +export * from "./LoaderSuspense"; diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Protect/Protect.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Protect/Protect.tsx index eee915ff..3fdcdc42 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Protect/Protect.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Protect/Protect.tsx @@ -1,4 +1,5 @@ -import type { Role } from "@org/lib-api-client"; +import type { KeycloakUser } from "@org/app-vite-react/lib/keycloak-js"; +import type { NavigationRouteProtectParam } from "@org/app-vite-react/server/route-typings"; import type { ReactNode } from "react"; import * as mui from "@mui/material"; @@ -6,52 +7,23 @@ import { sigUser } from "@org/app-vite-react/app/signals/sigUser"; export type ProtectProps = { children: ReactNode; - roles: Role[]; + protect?: NavigationRouteProtectParam; }; -export function Protect({ children, roles }: ProtectProps) { - const user = sigUser.value; +function isAuthorized(user: KeycloakUser, protect?: NavigationRouteProtectParam): boolean { + if (!protect) return true; - if (!user) { - // Not authenticated - return ( - - - - 🔒 Restricted Access - - - You need to sign in to view this content. - - (window.location.href = "/login")} // Replace with your login URL - > - Sign In - - - - ); + if (Array.isArray(protect)) { + return protect.length === 0 || protect.every(p => p(user)); } - if (user.roles.length === 0) { - return <>{children}; - } + return protect(user); +} - const hasRole = user.roles.some(r => roles.includes(r)); +export function Protect({ children, protect }: ProtectProps) { + const user = sigUser.value!; - if (!hasRole) { - // Authenticated but lacks permissions + if (!isAuthorized(user, protect)) { return ( {children}; } diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/QueryErrorBoundary.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/QueryErrorBoundary.tsx new file mode 100644 index 00000000..3425a1ff --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/QueryErrorBoundary.tsx @@ -0,0 +1,43 @@ +import * as mui from "@mui/material"; +import { QueryErrorResetBoundary } from "@tanstack/react-query"; +import { ErrorBoundary } from "react-error-boundary"; + +export function QueryErrorBoundary({ children }: React.PropsWithChildren) { + return ( + + {({ reset }) => ( + ( + + + + ⚠️ Error + +
{JSON.stringify(error, null, 2)}
+ resetErrorBoundary()} + > + Retry + +
+
+ )} + > + {children} +
+ )} +
+ ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/index.ts b/packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/index.ts new file mode 100644 index 00000000..f6df2a50 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/components/QueryErrorBoundary/index.ts @@ -0,0 +1 @@ +export * from "./QueryErrorBoundary"; diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx index 7fafbaca..d772a52b 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx @@ -1,16 +1,16 @@ import type { ClientDatatableProps } from "@org/app-vite-react/app/components/Datatable"; + import * as icons from "@mui/icons-material"; import * as mui from "@mui/material"; import React, { useState } from "react"; -export type ClientResponsiveTableProps = ClientDatatableProps & { +export type ClientResponsiveTableProps = Omit, "disablePagination"> & { renderRow: (item: T) => React.ReactNode; }; export function ClientResponsiveTable({ columns, data, - disablePagination, renderRow, }: ClientResponsiveTableProps) { const [selectedItem, setSelectedItem] = useState(null); @@ -39,13 +39,7 @@ export function ClientResponsiveTable({ onClick={() => handleItemClick(item)} sx={{ cursor: "pointer", - //backgroundColor: "red", borderRadius: 1, - //marginBottom: 1, - //padding: 2, - "&:hover": { - //backgroundColor: "rgba(255, 255, 255, 0.1)", - }, }} > {renderRow(item)} @@ -81,7 +75,7 @@ export function ClientResponsiveTable({ justifyContent: "space-between", marginBottom: 1.5, paddingY: 1, - borderBottom: "1px solid", + borderBottom: idx < columns.length - 1 ? "1px solid" : undefined, borderColor: "divider", }} > @@ -101,12 +95,6 @@ export function ClientResponsiveTable({
)} - - - - Close - - ); diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/TableRowMobile.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/TableRowMobile.tsx new file mode 100644 index 00000000..9c51c1f7 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/TableRowMobile.tsx @@ -0,0 +1,27 @@ +import * as mui from "@mui/material"; + +export type TableRowMobileProps = { + title: string; + subtitle: string; +}; + +export function TableRowMobile({ title, subtitle }: TableRowMobileProps) { + return ( + + + {title} + + + {subtitle} + + + ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx index 9ac7bf6f..d937135b 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx @@ -1,21 +1,27 @@ -import type { UserDto } from "@org/lib-api-client"; import type { zod } from "@org/lib-commons"; import * as icons from "@mui/icons-material"; import * as mui from "@mui/material"; -import { DatatableContainer, ClientDatatable } from "@org/app-vite-react/app/components/Datatable"; +import { + DatatableContainer, + DEFAULT_PAGINATION_OPTIONS, +} from "@org/app-vite-react/app/components/Datatable"; import { DatatableFilterButton } from "@org/app-vite-react/app/components/Datatable/components/DatatableFilterButton"; +import { Datatable } from "@org/app-vite-react/app/components/Datatable/Datatable"; import { InputText } from "@org/app-vite-react/app/forms"; -import { useConfirmContext } from "@org/app-vite-react/app/provider/ConfirmProvider"; -import { useSnackbarContext } from "@org/app-vite-react/app/provider/SnackbarProvider"; +import { useConfirm } from "@org/app-vite-react/app/provider/ConfirmProvider"; +import { useSnackbar } from "@org/app-vite-react/app/provider/SnackbarProvider"; import { sigDirection } from "@org/app-vite-react/app/signals/sigDirection"; import { sigUser } from "@org/app-vite-react/app/signals/sigUser"; import { tsrClient, tsrQuery } from "@org/app-vite-react/lib/@ts-rest"; import { useZodForm } from "@org/app-vite-react/lib/react-hook-form"; +import { type PaginationOptions, type UserDto } from "@org/lib-api-client"; import { z } from "@org/lib-commons"; import React from "react"; import { useNavigate } from "react-router-dom"; +import { TableRowMobile } from "./components/TableRowMobile"; + /*function buildPaginationQueryParams(paginationOptions: PaginationOptions): { paginationOptions: string; } { @@ -36,6 +42,10 @@ export const DEFAULT_USER_FILTERS: UserFilters = { }; export default function ManageUsersPage() { + const navigate = useNavigate(); + const confirm = useConfirm(); + const snack = useSnackbar(); + const alignLeft = sigDirection.value === "rtl" ? "right" : "left"; const [filters, setFilters] = React.useState>({}); const { control, @@ -46,11 +56,17 @@ export default function ManageUsersPage() { defaultValue: DEFAULT_USER_FILTERS, }); - const { data, isPending, refetch } = tsrQuery.User.findAll.useQuery({ + const { data, refetch } = tsrQuery.User.findAll.useSuspenseQuery({ queryKey: ["User.findAll"], staleTime: 1000, }); + /*const [open, setOpen] = React.useState(false); + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + };*/ + const filteredData = React.useMemo( () => data?.body.filter(user => { @@ -65,36 +81,16 @@ export default function ManageUsersPage() { [data?.body, filters], ); - const navigate = useNavigate(); - - const confirmAction = useConfirmContext(); - const snack = useSnackbarContext(); - - const alignLeft = sigDirection.value === "rtl" ? "right" : "left"; - - const onSearch = (model: UserFilters) => { - setFilters(model); - }; - - /*const [paginationOptions, setPaginationOptions] = useState({ + const [pagination, setPagination] = React.useState({ ...DEFAULT_PAGINATION_OPTIONS, order: ["username asc"], - });*/ - - /*const fetchUsers = useCallback(async () => { - const query = buildPaginationQueryParams(paginationOptions); - const users = await tsrClient.User.findAllPaginated({ query }); - if (users.status !== 200) throw new Error("Failed to fetch users."); - setUserResponse(users.body); - }, [paginationOptions]);*/ - - if (isPending) { - return <>; - } + }); - if (data?.status !== 200) { - return
Error
; - } + /*const { data: userData, isPending: isPendingUsers, refetch: refetchUsers } = tsrQuery.User.findAllPaginated.useQuery({ + queryKey: ['User.findAllPaginated', paginationOptions], + queryData: {query: buildPaginationQueryParams(paginationOptions)}, + staleTime: 1000 + });*/ return ( <> @@ -104,7 +100,7 @@ export default function ManageUsersPage() { form.reset()} - onSearch={handleSearch(onSearch)} + onSearch={handleSearch(setFilters)} filters={[ { label: "Username", @@ -127,32 +123,14 @@ export default function ManageUsersPage() { Add User - + + sync={true} + pagination={pagination} + onPaginationChange={setPagination} data={filteredData} - //count={data.body.length} - //keyMapper={user => user.username} - //paginationOptions={paginationOptions} - //onPaginationOptionsChange={paginationOptions => setPaginationOptions(paginationOptions)} + keyMapper={({ username }) => username} renderMobileRow={user => ( - - - {user.username} - - - {user.email || "--"} - - + )} columns={[ { @@ -219,7 +197,7 @@ export default function ManageUsersPage() { color="error" disabled={user.username === sigUser.value?.username} onClick={() => { - confirmAction({ + confirm({ title: "Warning", message: `Are you sure You want to delete user "${user.username}"?`, onConfirm: async () => { @@ -243,7 +221,10 @@ export default function ManageUsersPage() { navigate(`/admin/users/${user.username}/edit`)} + onClick={() => { + navigate(`/admin/users/${user.username}/edit`); + //setOpen(true); + }} > Edit @@ -252,6 +233,22 @@ export default function ManageUsersPage() { }, ]} /> + {/* + {}} + defaultValue={DEFAULT_USER_FORM_STATE} + /> + */} ); diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx index fc07b49f..9faaf930 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx @@ -1,27 +1,13 @@ -import type { UserForm as UserFormModel } from "@org/lib-api-client"; - -import { useSnackbarContext } from "@org/app-vite-react/app/provider/SnackbarProvider"; +import { useSnackbar } from "@org/app-vite-react/app/provider/SnackbarProvider"; import { tsrClient } from "@org/app-vite-react/lib/@ts-rest"; +import { DEFAULT_USER_FORM_STATE, type UserForm as UserFormModel } from "@org/lib-api-client"; import { useNavigate } from "react-router-dom"; import { UserForm } from "../../components"; -// TODO: Move this to shared package. -// eslint-disable-next-line react-refresh/only-export-components -export const DEFAULT_USER_FORM_STATE: UserFormModel = { - id: "", - username: "", - password: "", - roles: ["avr-user"], - email: "", - firstName: "", - lastName: "", - enabled: true, -}; - export default function CreateUserPage() { const navigate = useNavigate(); - const snack = useSnackbarContext(); + const snack = useSnackbar(); const handleSubmit = async (model: UserFormModel) => { await tsrClient.User.createUser({ diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx index 197b8bf4..72ecb25b 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx @@ -1,6 +1,7 @@ import type { UserForm as UserFormModel } from "@org/lib-api-client"; -import { useSnackbarContext } from "@org/app-vite-react/app/provider/SnackbarProvider"; +import { Card } from "@mui/material"; +import { useSnackbar } from "@org/app-vite-react/app/provider/SnackbarProvider"; import { tsrClient } from "@org/app-vite-react/lib/@ts-rest"; import React from "react"; import { useNavigate, useParams } from "react-router-dom"; @@ -9,7 +10,7 @@ import { UserForm } from "../../components"; export default function EditUserPage() { const navigate = useNavigate(); - const snack = useSnackbarContext(); + const snack = useSnackbar(); const { username: selectedUsername } = useParams<{ username: string }>(); const [selectedUserForm, setSelectedUserForm] = React.useState( @@ -50,4 +51,15 @@ export default function EditUserPage() { groups={["update"]} /> ); + + return ( + + + + ); } diff --git a/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx b/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx index 571e8f5a..915da6b3 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx @@ -44,7 +44,7 @@ export type ConfirmVisualProps = { }; // eslint-disable-next-line react-refresh/only-export-components -export function useConfirm() { +function useConfirmLocal() { const [open, setOpen] = React.useState(false); const [confirmDialogVisualProps, setConfirmDialogVisualProps] = @@ -77,15 +77,15 @@ export const ConfirmContext = React.createContext< >(undefined); // eslint-disable-next-line react-refresh/only-export-components -export function useConfirmContext() { +export function useConfirm() { const confirmAction = React.useContext(ConfirmContext); if (!confirmAction) { - throw new Error("useConfirmContext must be used within a ConfirmProvider"); + throw new Error("useConfirm must be used within a ConfirmProvider"); } return confirmAction; } export function ConfirmProvider({ children }: React.PropsWithChildren) { - const { confirmAction, ...confirmDialogProps } = useConfirm(); + const { confirmAction, ...confirmDialogProps } = useConfirmLocal(); return ( diff --git a/packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx b/packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx index 1f6e60f3..48531de3 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx @@ -35,7 +35,7 @@ function Snackbar({ export type PartialSnackbarProps = Omit; // eslint-disable-next-line react-refresh/only-export-components -export function useSnackbar() { +function useSnackbarLocal() { const [open, setOpen] = React.useState(false); const onClose = () => { @@ -65,15 +65,15 @@ export const SnackbarContext = React.createContext< >(undefined); // eslint-disable-next-line react-refresh/only-export-components -export function useSnackbarContext() { +export function useSnackbar() { const snack = React.useContext(SnackbarContext); if (!snack) { - throw new Error("useSnackbarContext must be used within a SnackbarProvider"); + throw new Error("useSnackbar must be used within a SnackbarProvider"); } return snack; } export function SnackbarProvider({ children }: React.PropsWithChildren) { - const { snackbarProps, snack } = useSnackbar(); + const { snackbarProps, snack } = useSnackbarLocal(); return ( diff --git a/packages/mern-sample-app/app-vite-react/src/app/routes.tsx b/packages/mern-sample-app/app-vite-react/src/app/routes.tsx index 517ae69e..4862aec2 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/routes.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/routes.tsx @@ -1,8 +1,5 @@ import type { NavigationRoute } from "@org/app-vite-react/server/route-typings"; -import type { TODO } from "@org/lib-commons"; -//import * as icons from "@mui/icons-material"; -import { Protect } from "@org/app-vite-react/app/components/Protect"; import ManageUsersPage from "@org/app-vite-react/app/pages/admin-settings/manage-users"; import HomePage from "@org/app-vite-react/app/pages/home"; import VisualPreferencesPage from "@org/app-vite-react/app/pages/visual-preferences"; @@ -13,18 +10,16 @@ import EditUserPage from "./pages/admin-settings/manage-users/pages/edit-user"; export const routes: NavigationRoute[] = [ { variant: "single", - label: () => "Home", - //icon: , + label: t => t("dashboard"), path: "/", - Component: () => , + Component: HomePage, handle: { - crumb: () => "Home", + crumb: t => t("dashboard"), }, }, { variant: "single", label: () => "Visual preferences", - //icon: , path: "visual-preferences", Component: VisualPreferencesPage, handle: { @@ -35,9 +30,7 @@ export const routes: NavigationRoute[] = [ label: () => "Admin settings", variant: "group", path: "admin", - secure: user => { - return !!user?.roles.some(r => ["avr-admin"].includes(r)); - }, + secure: user => !!user?.roles.some(r => ["avr-admin"].includes(r)), handle: { crumb: () => "Admin settings", disableLink: true, @@ -53,39 +46,27 @@ export const routes: NavigationRoute[] = [ children: [ { variant: "single", + Component: ManageUsersPage, label: () => "Manage users", path: "", - Component: () => ( - - - - ), }, { variant: "single", + Component: CreateUserPage, hidden: true, path: "add", handle: { crumb: () => "Create user", }, - Component: () => ( - - - - ), }, { variant: "single", + Component: EditUserPage, hidden: true, path: ":username/edit", handle: { - crumb: ({ username }: TODO) => "Edit user " + username, + crumb: (_, params) => "Edit user " + params?.username, }, - Component: () => ( - - - - ), }, ], }, diff --git a/packages/mern-sample-app/app-vite-react/src/lib/@tanstack/QueryClientProvider.tsx b/packages/mern-sample-app/app-vite-react/src/lib/@tanstack/QueryClientProvider.tsx index 096806f8..9585bceb 100644 --- a/packages/mern-sample-app/app-vite-react/src/lib/@tanstack/QueryClientProvider.tsx +++ b/packages/mern-sample-app/app-vite-react/src/lib/@tanstack/QueryClientProvider.tsx @@ -8,7 +8,13 @@ import { import { tsrQuery } from "../@ts-rest/tsRestApiClient"; // eslint-disable-next-line react-refresh/only-export-components -export const queryClient = new QueryClient(); +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 3, + }, + }, +}); export function QueryClientProvider({ children }: PropsWithChildren) { return ( diff --git a/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakProvider.tsx b/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakProvider.tsx index 5c143540..3e966095 100644 --- a/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakProvider.tsx +++ b/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakProvider.tsx @@ -7,7 +7,7 @@ import { ReactKeycloakProvider, useKeycloak } from "@react-keycloak/web"; import { jwtDecode } from "jwt-decode"; import { type KeycloakTokenParsed } from "keycloak-js"; import React from "react"; -import { StrictMode, type PropsWithChildren } from "react"; +import { /*StrictMode,*/ type PropsWithChildren } from "react"; const KeycloakImpl = ({ children }: React.PropsWithChildren) => { const { keycloak, initialized } = useKeycloak(); diff --git a/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakRoute.tsx b/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakRoute.tsx index ad16217f..2f581dab 100644 --- a/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakRoute.tsx +++ b/packages/mern-sample-app/app-vite-react/src/lib/keycloak-js/KeycloakRoute.tsx @@ -1,3 +1,5 @@ +import type { NavigationRouteProtect } from "@org/app-vite-react/server/route-typings"; + import { type KeycloakUser } from "@org/app-vite-react/lib/keycloak-js/KeycloakUser"; import { Navigate, useLocation, type RouteObject } from "react-router-dom"; @@ -6,7 +8,7 @@ export function KeycloakRoute({ user, Component, }: { - secure: (user: KeycloakUser | null) => boolean; + secure: NavigationRouteProtect; user: KeycloakUser | null; Component: NonNullable; }) { diff --git a/packages/mern-sample-app/app-vite-react/src/main.tsx b/packages/mern-sample-app/app-vite-react/src/main.tsx index 193493f5..b16b6258 100644 --- a/packages/mern-sample-app/app-vite-react/src/main.tsx +++ b/packages/mern-sample-app/app-vite-react/src/main.tsx @@ -3,15 +3,13 @@ import "@org/app-vite-react/lib/i18next/i18n"; import { Layout as layoutElement } from "@org/app-vite-react/app/layout"; import { providers } from "@org/app-vite-react/app/providers"; import { routes } from "@org/app-vite-react/app/routes"; -import { sigUser } from "@org/app-vite-react/app/signals/sigUser"; import { MuiCssBaseline as cssBaseline } from "@org/app-vite-react/lib/@mui"; -import { KeycloakRoute, type KeycloakUser } from "@org/app-vite-react/lib/keycloak-js"; import { reactServer } from "@org/app-vite-react/server/server"; import { useRouteError } from "react-router-dom"; -import { type RouteObject } from "react-router-dom"; import "@org/app-vite-react/main.css"; +// eslint-disable-next-line react-refresh/only-export-components function RootErrorPage() { const error = useRouteError() as Error; return ( @@ -23,16 +21,6 @@ function RootErrorPage() { ); } -function ProtectedRoute({ - secure, - Component, -}: { - secure: (user: KeycloakUser | null) => boolean; - Component: NonNullable; -}) { - return ; -} - reactServer.run({ rootId: "root", routes, @@ -40,5 +28,4 @@ reactServer.run({ errorElement: RootErrorPage, layoutElement, cssBaseline, - protectedRoute: ProtectedRoute, }); diff --git a/packages/mern-sample-app/app-vite-react/src/server/ReactApp.tsx b/packages/mern-sample-app/app-vite-react/src/server/ReactApp.tsx index 4670ae27..6b8a3c3e 100644 --- a/packages/mern-sample-app/app-vite-react/src/server/ReactApp.tsx +++ b/packages/mern-sample-app/app-vite-react/src/server/ReactApp.tsx @@ -1,13 +1,16 @@ -import { sigDirection } from "@org/app-vite-react/app/signals/sigDirection"; -import { type KeycloakUser } from "@org/app-vite-react/lib/keycloak-js"; -import { - type NavigationRouteItem, - type NavigationRoute, +import type { + NavigationRouteProtect, + NavigationRoute, + NavigationRouteItem, } from "@org/app-vite-react/server/route-typings"; + +import { sigDirection } from "@org/app-vite-react/app/signals/sigDirection"; import React from "react"; import ReactDOM from "react-dom/client"; import * as RouterDOM from "react-router-dom"; +import { BoundarySuspenseGroup } from "../app/components/BoundarySuspenseGroup"; + export type Provider = React.FC<{ children: React.ReactNode }>; // Function to nest children within a component @@ -32,24 +35,30 @@ type ReactAppConfig = { routes: NavigationRoute[]; cssBaseline: React.FC; rootId?: string; - protectedRoute: React.FC<{ - secure: (user: KeycloakUser | null) => boolean; - Component: NonNullable; - }>; }; +function getProtectFns( + oldProtectFns: NavigationRouteProtect[], + newProtectFn?: NavigationRouteProtect, +) { + const protectFns = [...oldProtectFns]; + if (newProtectFn) { + protectFns.push(newProtectFn); + } + return protectFns; +} + export class ReactApp { routes!: NavigationRoute[]; + layoutElement!: Provider; providers!: Provider[]; + errorElement!: React.FC; + cssBaseline!: React.FC; rootId?: string; #domRoutes!: RouterDOM.RouteObject[]; - #protectedRoute!: React.FC<{ - secure: (user: KeycloakUser | null) => boolean; - Component: NonNullable; - }>; constructor() { // NOOP @@ -65,7 +74,6 @@ export class ReactApp { #loadConfig(config: ReactAppConfig) { this.routes = [...config.routes]; - this.#protectedRoute = config.protectedRoute; this.cssBaseline = config.cssBaseline; this.rootId = config.rootId; this.errorElement = config.errorElement; @@ -96,43 +104,39 @@ export class ReactApp { ]); } - #convertNavigationToRoutes(data: NavigationRoute[]): RouterDOM.RouteObject[] { + #convertNavigationToRoutes( + data: NavigationRoute[], + nestedProtectFns: NavigationRouteProtect[] = [], + ): RouterDOM.RouteObject[] { const routes: RouterDOM.RouteObject[] = []; - const protectComponentIfNeeded = (item: NavigationRouteItem) => { + const wrapComponentInBoundarySuspenseGroup = ( + item: NavigationRouteItem, + protectFns: NavigationRouteProtect[], + ) => { const Component = item.Component; - const secure = item.secure; - if (!secure) return; - - const ProtectedRoute = this.#protectedRoute; - - // prettier-ignore - const ProtectedComponent = () => ; - - item.Component = ProtectedComponent; + item.Component = () => ( + + + + ); }; for (const item of data) { if (item.variant === "single") { - protectComponentIfNeeded(item); - routes.push(item); + wrapComponentInBoundarySuspenseGroup(item, getProtectFns(nestedProtectFns, item.secure)); + routes.push(item as RouterDOM.RouteObject); continue; } - // Register route for displaying breadcrumbs on menus and groups if `handle` is provided. - if ("handle" in item && item.handle) { - //routes.push(item); - } - //routes.push(...this.#convertNavigationToRoutes(item.children)); - const localItem = { ...item }; - localItem.children = this.#convertNavigationToRoutes(item.children) as NavigationRoute[]; + localItem.children = this.#convertNavigationToRoutes( + item.children, + getProtectFns(nestedProtectFns, item.secure), + ) as NavigationRoute[]; - routes.push(localItem); + routes.push(localItem as RouterDOM.RouteObject); } return routes; diff --git a/packages/mern-sample-app/app-vite-react/src/server/route-typings.ts b/packages/mern-sample-app/app-vite-react/src/server/route-typings.ts index 120b41a5..704d47b8 100644 --- a/packages/mern-sample-app/app-vite-react/src/server/route-typings.ts +++ b/packages/mern-sample-app/app-vite-react/src/server/route-typings.ts @@ -1,7 +1,13 @@ import { type I18nTranslateFn } from "@org/app-vite-react/lib/i18next"; import { type KeycloakUser } from "@org/app-vite-react/lib/keycloak-js"; import { type ReactNode } from "react"; -import { type RouteObject } from "react-router-dom"; +import { type RouteObject as RouteObjectDom } from "react-router-dom"; + +type RouteObject = Omit; + +export type NavigationRouteProtect = (user: KeycloakUser | null) => boolean; + +export type NavigationRouteProtectParam = NavigationRouteProtect | NavigationRouteProtect[]; type NavigationRouteUiHidden = { hidden: true; @@ -13,9 +19,17 @@ type NavigationRouteUiVisible = { icon?: ReactNode; }; +export type NavigationRouteHandle = + | { + disableLink?: boolean; + crumb?: (translateFn: I18nTranslateFn, params: Record) => string; + } + | undefined; + export type NavigationRouteUi = { path: string; - secure?: (user: KeycloakUser | null) => boolean; + secure?: NavigationRouteProtect; + handle?: NavigationRouteHandle; } & (NavigationRouteUiVisible | NavigationRouteUiHidden); // prettier-ignore @@ -28,7 +42,6 @@ export type NavigationRouteItem = RouteObject & NavigationRouteUi & { export type NavigationRouteItems = NavigationRouteUi & { variant: "menu" | "group"; children: NavigationRoute[]; - handle?: NonNullable; }; export type NavigationRoute = NavigationRouteItem | NavigationRouteItems; diff --git a/packages/mern-sample-app/lib-api-client/src/app/models/form/UserForm.ts b/packages/mern-sample-app/lib-api-client/src/app/models/form/UserForm.ts index 794ef4ef..4457ec5a 100644 --- a/packages/mern-sample-app/lib-api-client/src/app/models/form/UserForm.ts +++ b/packages/mern-sample-app/lib-api-client/src/app/models/form/UserForm.ts @@ -24,8 +24,15 @@ export const UserForm = z description: "User Form", }); -/*UserForm.merge({ - username: UserForm.shape.username -})*/ +export const DEFAULT_USER_FORM_STATE: UserForm = { + id: "", + username: "", + password: "", + roles: ["avr-user"], + email: "", + firstName: "", + lastName: "", + enabled: true, +}; export type UserForm = zod.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b43f4e4f..3f43123e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-error-boundary: + specifier: ^4.1.2 + version: 4.1.2(react@18.3.1) react-hook-form: specifier: ^7.53.1 version: 7.53.1(react@18.3.1) @@ -7374,6 +7377,15 @@ packages: scheduler: 0.23.2 dev: false + /react-error-boundary@4.1.2(react@18.3.1): + resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.25.7 + react: 18.3.1 + dev: false + /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false