From 509288ead7027c8d4996798f3932601b28b7d825 Mon Sep 17 00:00:00 2001 From: Bruno Tot Date: Wed, 13 Nov 2024 16:47:07 +0100 Subject: [PATCH] chore: refactor more on frontend and add confirm dialog context --- .../controllers/UserController.ts | 13 +- .../middleware/withValidatedBody.ts | 48 ++++ .../validators/UserValidator.ts | 16 +- .../lib/@ts-rest/TsRestExpressRouteTypes.ts | 6 + .../public/locales/ar/translation.json | 31 +++ .../components/Header/ComputedBreadcrumbs.tsx | 133 ++++++----- .../src/app/components/Header/Header.tsx | 7 +- .../src/app/components/Sidebar/Sidebar.tsx | 3 +- .../src/app/forms/input/Input/Input.tsx | 100 +++++++++ .../src/app/forms/input/Input/index.ts | 1 + .../forms/input/InputSelect/InputSelect.tsx | 144 ++++++------ .../app/forms/input/InputText/InputText.tsx | 50 ++--- .../forms/input/InputToggle/InputToggle.tsx | 71 ++++++ .../src/app/forms/input/InputToggle/index.ts | 1 + .../src/app/forms/input/index.ts | 2 + .../InputLocaleSelect/InputLocaleSelect.tsx | 4 +- .../app-vite-react/src/app/layout/Layout.tsx | 5 +- .../src/app/models/ConstraintViolation.ts | 2 +- .../app-vite-react/src/app/models/Locale.ts | 2 +- .../src/app/models/SidebarPosition.ts | 5 - .../app-vite-react/src/app/models/index.ts | 1 - .../admin-settings/manage-users/index.tsx | 56 ++--- .../manage-users/pages/edit-user/index.tsx | 9 +- .../app/pages/visual-preferences/index.tsx | 208 ++++-------------- .../src/app/provider/ConfirmProvider.tsx | 94 ++++++++ .../app-vite-react/src/app/providers.tsx | 6 +- .../src/app/signals/sigDirection.ts | 7 + .../src/app/signals/sigLayoutWidth.ts | 2 + .../src/app/signals/sigLocale.ts | 13 +- .../src/app/signals/sigSidebarPosition.ts | 13 -- .../src/app/signals/sigTheme.ts | 6 +- .../app-vite-react/src/index.html | 6 + .../app-vite-react/src/lib/i18next/i18n.ts | 11 +- .../material-ui-confirm/ConfirmProvider.tsx | 5 - .../src/lib/material-ui-confirm/index.ts | 1 - .../src/lib/react-hook-form/useZodForm.ts | 5 - .../app-vite-react/src/server/LocalStorage.ts | 7 +- .../app-vite-react/src/server/ReactApp.tsx | 2 + .../src/app/utils/common-models/Validator.ts | 2 +- 39 files changed, 658 insertions(+), 440 deletions(-) create mode 100644 packages/mern-sample-app/app-node-express/src/app/infrastructure/middleware/withValidatedBody.ts create mode 100644 packages/mern-sample-app/app-vite-react/public/locales/ar/translation.json create mode 100644 packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/Input.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/index.ts create mode 100644 packages/mern-sample-app/app-vite-react/src/app/forms/input/InputToggle/InputToggle.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/forms/input/InputToggle/index.ts delete mode 100644 packages/mern-sample-app/app-vite-react/src/app/models/SidebarPosition.ts create mode 100644 packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/signals/sigDirection.ts delete mode 100644 packages/mern-sample-app/app-vite-react/src/app/signals/sigSidebarPosition.ts delete mode 100644 packages/mern-sample-app/app-vite-react/src/lib/material-ui-confirm/ConfirmProvider.tsx delete mode 100644 packages/mern-sample-app/app-vite-react/src/lib/material-ui-confirm/index.ts 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 598219cf..a3bbdab2 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 @@ -4,9 +4,12 @@ import type { TODO } from "@org/lib-commons"; import { contract } from "@org/app-node-express/app/infrastructure/decorators"; import { withRouteSecured } from "@org/app-node-express/app/infrastructure/middleware/withRouteSecured"; +import { withValidatedBody } from "@org/app-node-express/app/infrastructure/middleware/withValidatedBody"; import { autowired, inject } from "@org/app-node-express/lib/ioc"; import { contracts, Role } from "@org/lib-api-client"; +import { VALIDATORS } from "../validators"; + @inject("UserController") export class UserController { @autowired() private userService: UserService; @@ -53,9 +56,9 @@ export class UserController { @contract( contracts.User.createUser, withRouteSecured(Role.Enum["avr-admin"]), - // withValidator(UserValidator, { groups: ["create"] }), + // TODO + withValidatedBody(/*UserForm, */ VALIDATORS.User, { groups: ["create"] }), ) - // @validators("User", { groups: ["create"] }) async createUser( payload: RouteInput, ): RouteOutput { @@ -66,7 +69,11 @@ export class UserController { }; } - @contract(contracts.User.updateUser, withRouteSecured(Role.Enum["avr-admin"])) + @contract( + contracts.User.updateUser, + withRouteSecured(Role.Enum["avr-admin"]), + withValidatedBody(VALIDATORS.User, { groups: ["update"] }), + ) async updateUser( payload: RouteInput, ): RouteOutput { diff --git a/packages/mern-sample-app/app-node-express/src/app/infrastructure/middleware/withValidatedBody.ts b/packages/mern-sample-app/app-node-express/src/app/infrastructure/middleware/withValidatedBody.ts new file mode 100644 index 00000000..377ab81f --- /dev/null +++ b/packages/mern-sample-app/app-node-express/src/app/infrastructure/middleware/withValidatedBody.ts @@ -0,0 +1,48 @@ +/** + * @packageDocumentation + */ + +import type { VALIDATORS } from "../validators"; +import type { RouteMiddlewareFactory } from "@org/app-node-express/lib/@ts-rest"; +import type { RequestHandler } from "express"; + +import { IocRegistry, inject } from "@org/app-node-express/lib/ioc"; +import { RestError, type ValidatorOptions, type Validators } from "@org/lib-api-client"; + +const IOC_KEY = "withValidatedBody"; + +export type ValidatorSchema = keyof typeof VALIDATORS; + +export interface ValidatedBodyMiddleware { + middleware(validators: Validators, options: ValidatorOptions): RequestHandler[]; +} + +@inject(IOC_KEY) +export class WithValidatedBody implements ValidatedBodyMiddleware { + middleware(validators: Validators, options: ValidatorOptions): RequestHandler[] { + return [ + async (req, _res, next) => { + const body = req.body || undefined; + if (!body) return next(); + const validatorEntries = Object.entries(validators); + const validationResults = await Promise.all( + validatorEntries.map(([, validate]) => validate(body, options)), + ); + const hasErrors = validationResults.some(res => !res); + if (hasErrors) return next(new RestError(400, "Body validation failed")); + next(); + }, + ]; + } +} + +export function withValidatedBody( + validators: Validators, + options: ValidatorOptions = { groups: [] }, +): RouteMiddlewareFactory { + return () => { + return IocRegistry.getInstance() + .inject(IOC_KEY) + .middleware(validators, options); + }; +} diff --git a/packages/mern-sample-app/app-node-express/src/app/infrastructure/validators/UserValidator.ts b/packages/mern-sample-app/app-node-express/src/app/infrastructure/validators/UserValidator.ts index 95b2f2ad..4b0c9614 100644 --- a/packages/mern-sample-app/app-node-express/src/app/infrastructure/validators/UserValidator.ts +++ b/packages/mern-sample-app/app-node-express/src/app/infrastructure/validators/UserValidator.ts @@ -2,15 +2,21 @@ import type { UserService } from "../service/UserService"; import type { UserForm, Validator, Validators } from "@org/lib-api-client"; import { IocRegistry } from "@org/app-node-express/lib"; +import { RestError } from "@org/lib-api-client"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const usernameShouldBeUnique: Validator = async (model, _options) => { +// Returns false if validation success + +export const usernameShouldBeUnique: Validator = async (model, options) => { + if (options.groups && !options.groups.includes("create")) return true; const userService = IocRegistry.getInstance().inject("UserService"); try { - return (await userService.findOneByUsername(model.username)) !== null; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { + await userService.findOneByUsername(model.username); return false; + } catch (error: unknown) { + if (error instanceof RestError && error.content.status === 404) { + return true; + } + throw error; } }; diff --git a/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/TsRestExpressRouteTypes.ts b/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/TsRestExpressRouteTypes.ts index a6f397bd..4018e329 100644 --- a/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/TsRestExpressRouteTypes.ts +++ b/packages/mern-sample-app/app-node-express/src/lib/@ts-rest/TsRestExpressRouteTypes.ts @@ -7,6 +7,7 @@ */ import type { AppRoute, ServerInferResponses } from "@org/lib-api-client"; +import type { TODO } from "@org/lib-commons"; import type { AppRouteImplementation } from "@ts-rest/express"; import type { RequestHandler } from "express"; @@ -28,6 +29,11 @@ export type RouteInput = Parameters = (data: RouteInput) => RouteOutput; +/** + * Represents a handler function for any route, taking any data and returning any output. + */ +export type UntypedRouteHandler = (data: TODO) => TODO; + /** * Represents a factory for generating an array of Express middleware request handlers. * @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/express/index.d.ts#L112 RequestHandler} diff --git a/packages/mern-sample-app/app-vite-react/public/locales/ar/translation.json b/packages/mern-sample-app/app-vite-react/public/locales/ar/translation.json new file mode 100644 index 00000000..cf019c49 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/public/locales/ar/translation.json @@ -0,0 +1,31 @@ +{ + "accountSettings": "إعدادات الحساب", + "appTheme": "الثيم", + "apps": "التطبيقات", + "borderRadius": "نصف قطر الحافة", + "calendar": "التقويم", + "cards": "البطاقات", + "dashboard": "لوحة التحكم", + "doSearch": "بحث", + "error": "خطأ", + "forms": "النماذج", + "icons": "الرموز", + "invoice": "فاتورة", + "list": "قائمة", + "login": "تسجيل الدخول", + "pages": "الصفحات", + "register": "التسجيل", + "rolesAndPermissions": "الأدوار والصلاحيات", + "setDarkMode": "وضع داكن", + "setLightMode": "وضع مضيء", + "systemLanguage": "اللغة", + "systemLayout": "التخطيط", + "systemLayoutHorizontal": "تصميم أفقي", + "systemLayoutSidebar": "تصميم بشريط جانبي", + "tables": "الجداول", + "test": "اختبار", + "typography": "الخطوط", + "user": "المستخدم", + "userInterface": "واجهة المستخدم", + "view": "عرض" +} 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 d6b5eac4..e6721e57 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 @@ -2,6 +2,7 @@ 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 { Link as RouterLink } from "react-router-dom"; import { type UIMatch, useMatches } from "react-router-dom"; @@ -45,81 +46,35 @@ function convertToCrumbs(matches: UIMatch[]): /*Crumb[]*/ TODO return crumbs; } -export function ComputedBreadcrumbs() { - const matchesDesktop = mui.useMediaQuery("(min-width:678px)"); - const matches = useMatches(); - const crumbs = convertToCrumbs(matches); - //.filter(match => "handle" in match && match.handle && typeof match.handle === "object" && "crumb" in match.handle && match.handle.crumb && typeof match.handle.crumb === "function") - //.map(match => match.handle.crumb(match.data)); - - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - if (!matchesDesktop) { - return ( - <> - - - {crumbs[crumbs.length - 1].label} - - - - - } - aria-label="breadcrumb" - sx={{ - "& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:first-child": { - width: "100%", - }, - "& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:nth-child(odd):not(:first-child)": { - flex: 1, - }, - }} - > - {crumbs.map((crumb, index) => ( - - {crumb.label} - - ))} - - - - - ); - } +export function LocalBreadcrumbs({ + crumbs, + mobileLayout, +}: { + crumbs: TODO[]; + mobileLayout: boolean; +}) { + const sx: mui.SxProps | undefined = !mobileLayout + ? undefined + : { + "& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:first-child": { + width: "100%", + }, + "& .MuiBreadcrumbs-ol .MuiBreadcrumbs-li:nth-child(odd):not(:first-child)": { + flex: 1, + }, + }; return ( } + separator={ + sigDirection.value === "ltr" ? ( + + ) : ( + + ) + } > {crumbs.map((crumb, index) => ( ); } + +export function ComputedBreadcrumbs() { + const matchesDesktop = mui.useMediaQuery("(min-width:678px)"); + const matches = useMatches(); + const crumbs = convertToCrumbs(matches); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + if (matchesDesktop) return ; + + return ( + <> + setAnchorEl(e.currentTarget)} + sx={{ borderRadius: 0.25 }} + > + + {crumbs[crumbs.length - 1].label} + + + setAnchorEl(null)}> + + + + + + ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Header/Header.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Header/Header.tsx index f91365fa..ed93f8ef 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Header/Header.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Header/Header.tsx @@ -7,7 +7,7 @@ import { InputLayoutToggle } from "@org/app-vite-react/app/inputs/InputLayoutTog import { InputLocaleSelect } from "@org/app-vite-react/app/inputs/InputLocaleSelect"; import { sigLayoutVariant } from "@org/app-vite-react/app/signals/sigLayoutVariant"; import { sigLayoutWidth } from "@org/app-vite-react/app/signals/sigLayoutWidth"; -import { sigLocale } from "@org/app-vite-react/app/signals/sigLocale"; +import { getFontFamily, sigLocale } from "@org/app-vite-react/app/signals/sigLocale"; import { sigSidebarOpen } from "@org/app-vite-react/app/signals/sigSidebarOpen"; import { sigThemeOpts } from "@org/app-vite-react/app/signals/sigTheme"; @@ -94,7 +94,10 @@ export function Header({ /> (sigLocale.value = locale)} + onChange={locale => { + sigLocale.value = locale; + sigThemeOpts.value = { ...sigThemeOpts.value, fontFamily: getFontFamily(locale) }; + }} /> diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Sidebar/Sidebar.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Sidebar/Sidebar.tsx index 2a73a078..8e075de0 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Sidebar/Sidebar.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Sidebar/Sidebar.tsx @@ -2,7 +2,6 @@ import type { ReactNode } from "react"; import { SwipeableDrawer, useMediaQuery } from "@mui/material"; import { sigLayoutVariant } from "@org/app-vite-react/app/signals/sigLayoutVariant"; -import { sigSidebarPosition } from "@org/app-vite-react/app/signals/sigSidebarPosition"; export type SidebarProps = { width?: number; @@ -22,7 +21,7 @@ export function Sidebar({ children, }: SidebarProps) { const matchesDesktop = useMediaQuery("(min-width:678px)"); - const computedAnchor = matchesDesktop ? sigSidebarPosition.value : "bottom"; + const computedAnchor = matchesDesktop ? "left" : "bottom"; const paperWidth = matchesDesktop ? width : "100%"; const drawerWidth = hidden ? 0 : paperWidth; diff --git a/packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/Input.tsx b/packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/Input.tsx new file mode 100644 index 00000000..386b44b2 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/Input.tsx @@ -0,0 +1,100 @@ +import type { TODO } from "@org/lib-commons"; + +import * as rhf from "react-hook-form"; + +// React specific + +export type CombinedInputProps< + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +> = InputVisualProps & TypedInputProps; + +export type DeepItemType = T extends Array ? DeepItemType : T; + +export type ValueType< + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +> = DeepItemType : TInput>; + +// + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type InputRender<_TInput> = (props: { + value: TODO; + onChange: (value: TODO) => void; +}) => JSX.Element; + +export type RhfControllerProps< + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +> = Omit, "render" | "control" | "name"> & { + control: rhf.Control; + name: TName; +}; + +export type InputVisualProps = Partial<{ + label: string; + error: string; + required: boolean; + disabled: boolean; + placeholder: string; +}>; + +export type UncontrolledInputProps< + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +> = RhfControllerProps & { + controlled?: false; + control: rhf.Control; + name: TName; +}; + +export type ControlledInputProps = { + controlled: true; + value: TInput; + onChange: (value: TInput) => void; +}; + +export type TypedInputProps< + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +> = ControlledInputProps | UncontrolledInputProps; + +function ControlledInput({ + onChange, + render, + value, +}: Omit, "controlled"> & BaseInputProps) { + return render({ value, onChange }); +} + +function UncontrolledInput< + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +>({ + render, + ...controllerProps +}: UncontrolledInputProps & BaseInputProps>) { + return ( + + {...controllerProps} + render={({ field: { value, onChange } }) => render({ value: value as TODO, onChange })} + /> + ); +} + +export type BaseInputProps = { + render: InputRender; +}; + +export function Input< + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +>(props: BaseInputProps> & TypedInputProps) { + if (props.controlled) return ; + return ; +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/index.ts b/packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/index.ts new file mode 100644 index 00000000..be66d766 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/forms/input/Input/index.ts @@ -0,0 +1 @@ +export * from "./Input"; diff --git a/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputSelect/InputSelect.tsx b/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputSelect/InputSelect.tsx index 9ce45677..6db4ce62 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputSelect/InputSelect.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputSelect/InputSelect.tsx @@ -1,27 +1,34 @@ -import type { InputProps } from "../InputText"; +import type { + CombinedInputProps, + ValueType, +} from "@org/app-vite-react/app/forms/input/Input/Input"; +import type * as rhf from "react-hook-form"; import * as mui from "@mui/material"; -import { Controller, type FieldPath, type FieldValues } from "react-hook-form"; +import { Input } from "@org/app-vite-react/app/forms/input/Input/Input"; export type InputSelectProps< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, - TItem = string, -> = InputProps & { - options: TItem[]; + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +> = CombinedInputProps & { + options: ValueType[]; multiple?: boolean; -} & (TItem extends object + startAdornment?: React.ReactNode; +} & (ValueType extends object ? Required<{ - getOptionLabel: (option: TItem) => string; + getOptionLabel: (option: ValueType) => string; + renderOption: (option: ValueType) => React.ReactNode; }> : { - getOptionLabel?: (option: TItem) => string; + getOptionLabel?: (option: ValueType) => string; + renderOption?: (option: ValueType) => React.ReactNode; }); export function InputSelect< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, - TItem = string, + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, >({ label, error = "", @@ -30,67 +37,64 @@ export function InputSelect< options, placeholder = "", getOptionLabel, + renderOption, multiple = false, - ...controllerProps -}: InputSelectProps) { + startAdornment, + ...inputProps +}: InputSelectProps) { const computedGetOptionLabel = getOptionLabel ? getOptionLabel - : (option: TItem) => option as unknown as string; + : (option: ValueType) => String(option); + const computedRenderOption = renderOption + ? renderOption + : (option: ValueType) => <>{String(option)}; + return ( - ( - field.onChange(value)} - filterSelectedOptions={multiple} - renderOption={(props, option) => { - const textual = computedGetOptionLabel(option); - return ( - - {textual} - - ); - }} - renderInput={params => ( - - )} - /> - )} + + {...inputProps} + render={({ value, onChange }) => { + return ( + , typeof multiple> + multiple={multiple} + disabled={disabled} + options={options} + getOptionLabel={o => { + return computedGetOptionLabel(o); + }} + disableCloseOnSelect={multiple} + value={value} + onChange={(_event, value) => onChange(value)} + filterSelectedOptions={multiple} + renderOption={(props, option) => { + const optionBody = computedRenderOption(option); + const optionLabel = computedGetOptionLabel(option); + return ( + + {optionBody} + + ); + }} + renderInput={params => ( + + )} + /> + ); + }} /> ); - - /*return ( - ( - - )} - /> - );*/ } diff --git a/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputText/InputText.tsx b/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputText/InputText.tsx index 89a5e404..82686d4a 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputText/InputText.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputText/InputText.tsx @@ -1,37 +1,20 @@ -import * as mui from "@mui/material"; -import { - Controller, - type FieldPath, - type FieldValues, - type ControllerProps, -} from "react-hook-form"; - -export type InputControllerProps< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, -> = Omit, "render">; +import type * as rhf from "react-hook-form"; -export type InputProps< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, -> = InputControllerProps & { - label: string; - error?: string; - required?: boolean; - disabled?: boolean; - placeholder?: string; -}; +import * as mui from "@mui/material"; +import { Input, type CombinedInputProps } from "@org/app-vite-react/app/forms/input/Input/Input"; export type InputTextProps< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, -> = InputProps & { + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +> = CombinedInputProps & { type?: React.InputHTMLAttributes["type"]; }; export function InputText< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, >({ label, error = "", @@ -39,14 +22,15 @@ export function InputText< disabled = false, type = "text", placeholder = "", - ...controllerProps -}: InputTextProps) { + ...inputProps +}: InputTextProps) { return ( - ( + + {...inputProps} + render={({ value, onChange }) => ( = rhf.FieldPath, +> = CombinedInputProps & { + options: ValueType[]; +} & (ValueType extends object + ? Required<{ + renderOption: (option: ValueType) => React.ReactNode; + }> + : { + renderOption?: (option: ValueType) => React.ReactNode; + }); + +export function InputToggle< + TInput, + TForm extends rhf.FieldValues = rhf.FieldValues, + TName extends rhf.FieldPath = rhf.FieldPath, +>({ + //label, + //error = "", + //required = false, + //disabled = false, + options, + renderOption, + ...inputProps +}: InputToggleProps) { + const computedRenderOption = renderOption + ? renderOption + : (option: ValueType) => <>{String(option)}; + + return ( + + {...inputProps} + render={({ value, onChange }) => ( + onChange(value)} + aria-label="Theme" + sx={{ + width: "100%", + }} + > + {options.map(option => ( + + {computedRenderOption(option)} + + ))} + + )} + /> + ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputToggle/index.ts b/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputToggle/index.ts new file mode 100644 index 00000000..04a0e8df --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/forms/input/InputToggle/index.ts @@ -0,0 +1 @@ +export * from "./InputToggle"; diff --git a/packages/mern-sample-app/app-vite-react/src/app/forms/input/index.ts b/packages/mern-sample-app/app-vite-react/src/app/forms/input/index.ts index 20695dc4..f4fa9862 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/forms/input/index.ts +++ b/packages/mern-sample-app/app-vite-react/src/app/forms/input/index.ts @@ -1,2 +1,4 @@ +export * from "./Input"; export * from "./InputText"; export * from "./InputSelect"; +export * from "./InputToggle"; diff --git a/packages/mern-sample-app/app-vite-react/src/app/inputs/InputLocaleSelect/InputLocaleSelect.tsx b/packages/mern-sample-app/app-vite-react/src/app/inputs/InputLocaleSelect/InputLocaleSelect.tsx index 288d320e..6f903ab4 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/inputs/InputLocaleSelect/InputLocaleSelect.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/inputs/InputLocaleSelect/InputLocaleSelect.tsx @@ -7,11 +7,13 @@ import { useMemo, useState } from "react"; import { Flag } from "../../components/Flag/Flag"; +// eslint-disable-next-line react-refresh/only-export-components export function getLocaleNativeName(locale: I18nLocale) { const name: string = new Intl.DisplayNames([locale], { type: "language", }).of(locale)!; - return name.charAt(0).toUpperCase() + name.slice(1); + const res = name.charAt(0).toUpperCase() + name.slice(1); + return res; } export type InputLocaleSelectProps = { diff --git a/packages/mern-sample-app/app-vite-react/src/app/layout/Layout.tsx b/packages/mern-sample-app/app-vite-react/src/app/layout/Layout.tsx index 96cf2b8c..a6d42d7d 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/layout/Layout.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/layout/Layout.tsx @@ -8,7 +8,6 @@ import { Sidebar } from "@org/app-vite-react/app/components/Sidebar"; import { sigLayoutVariant } from "@org/app-vite-react/app/signals/sigLayoutVariant"; import { sigLayoutWidth } from "@org/app-vite-react/app/signals/sigLayoutWidth"; import { sigSidebarOpen } from "@org/app-vite-react/app/signals/sigSidebarOpen"; -import { sigSidebarPosition } from "@org/app-vite-react/app/signals/sigSidebarPosition"; import { type NavigationRoute } from "@org/app-vite-react/server/route-typings"; import { HorizontalLayout } from "./HorizontalLayout"; @@ -64,7 +63,7 @@ export function Layout({ children }: PropsWithChildren) { paddingInline: "0 !important", }} > - {sigSidebarPosition.value === "left" && SidebarComponent} + {SidebarComponent}