Simplify and better input models

This commit is contained in:
Edouard DUPIN 2025-02-09 01:16:26 +01:00
parent 63272dfb67
commit ddf822e824
17 changed files with 1712 additions and 1885 deletions

2836
front/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,39 @@
/** /**
* Interface of the server (auto-generated code) * Interface of the server (auto-generated code)
*/ */
import { z as zod } from "zod"; import { z as zod } from 'zod';
export const ZodUserCreate = zod.object({ export const ZodUserCreate = zod.object({
login: zod.string().min(3).max(128), login: zod.string().min(3).max(128),
email: zod.string().min(5).max(128), email: zod.string().min(5).max(128),
password: zod.string().min(128).max(128), password: zod.string().min(128).max(128),
}); });
export type UserCreate = zod.infer<typeof ZodUserCreate>; export type UserCreate = zod.infer<typeof ZodUserCreate>;
export function isUserCreate(data: any): data is UserCreate { export function isUserCreate(data: any): data is UserCreate {
try { try {
ZodUserCreate.parse(data); ZodUserCreate.parse(data);
return true; return true;
} catch (e: any) { } catch (e: any) {
console.log(`Fail to parse data type='ZodUserCreate' error=${e}`); console.log(`Fail to parse data type='ZodUserCreate' error=${e}`);
return false; return false;
} }
} }
export const ZodUserCreateWrite = zod.object({ export const ZodUserCreateWrite = zod.object({
login: zod.string().min(3).max(128).optional(), login: zod.string().min(3).max(128).optional(),
email: zod.string().min(5).max(128).optional(), email: zod.string().min(5).max(128).optional(),
password: zod.string().min(128).max(128).optional(), password: zod.string().min(128).max(128).optional(),
}); });
export type UserCreateWrite = zod.infer<typeof ZodUserCreateWrite>; export type UserCreateWrite = zod.infer<typeof ZodUserCreateWrite>;
export function isUserCreateWrite(data: any): data is UserCreateWrite { export function isUserCreateWrite(data: any): data is UserCreateWrite {
try { try {
ZodUserCreateWrite.parse(data); ZodUserCreateWrite.parse(data);
return true; return true;
} catch (e: any) { } catch (e: any) {
console.log(`Fail to parse data type='ZodUserCreateWrite' error=${e}`); console.log(`Fail to parse data type='ZodUserCreateWrite' error=${e}`);
return false; return false;
} }
} }

View File

@ -1,24 +1,11 @@
import { import { DragEventHandler, ReactNode, RefObject } from 'react';
DragEventHandler,
ReactNode,
RefObject,
} from 'react';
import { import { Box, BoxProps, Center, Flex, HStack, Image } from '@chakra-ui/react';
Box, import { MdHighlightOff, MdUploadFile } from 'react-icons/md';
BoxProps,
Center,
Flex,
HStack,
Image,
} from '@chakra-ui/react';
import {
MdHighlightOff,
MdUploadFile,
} from 'react-icons/md';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { DataUrlAccess } from '@/utils/data-url-access'; import { DataUrlAccess } from '@/utils/data-url-access';
import { useFormidableContextElement } from '../formidable'; import { useFormidableContextElement } from '../formidable';
export type DragNdropProps = { export type DragNdropProps = {
@ -29,8 +16,8 @@ export type DragNdropProps = {
}; };
export const DragNdrop = ({ export const DragNdrop = ({
onFilesSelected = () => { }, onFilesSelected = () => {},
onUriSelected = () => { }, onUriSelected = () => {},
width = '100px', width = '100px',
height = '100px', height = '100px',
}: DragNdropProps) => { }: DragNdropProps) => {
@ -111,7 +98,9 @@ export const CenterIcon = ({
top="50%" top="50%"
left="50%" left="50%"
transform="translate(-50%, -50%)" transform="translate(-50%, -50%)"
>{children}</Box> >
{children}
</Box>
</Box> </Box>
); );
}; };
@ -130,20 +119,15 @@ export type FormCoversProps = {
export const FormCovers = ({ export const FormCovers = ({
name, name,
ref, ref,
onFilesSelected = () => { }, onFilesSelected = () => {},
onUriSelected = () => { }, onUriSelected = () => {},
onRemove = () => { }, onRemove = () => {},
...rest ...rest
}: FormCoversProps) => { }: FormCoversProps) => {
const {value, isModify, onRestore} = useFormidableContextElement(name); const { value } = useFormidableContextElement(name);
const urls = const urls = DataUrlAccess.getListThumbnailUrl(value) ?? [];
DataUrlAccess.getListThumbnailUrl(value) ?? [];
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
isModify={isModify}
onRestore={onRestore}
{...rest}
>
<HStack wrap="wrap" width="full"> <HStack wrap="wrap" width="full">
{urls.map((data, index) => ( {urls.map((data, index) => (
<Flex align="flex-start" key={data}> <Flex align="flex-start" key={data}>

View File

@ -1,67 +1,128 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Flex, Text } from '@chakra-ui/react'; import { Box, Flex, Text } from '@chakra-ui/react';
import { MdErrorOutline, MdHelpOutline, MdRefresh } from 'react-icons/md'; import { MdErrorOutline, MdHelpOutline, MdRefresh } from 'react-icons/md';
import { useFormidableContextElement } from '../formidable';
const DisplayLabel = ({
label,
isRequired,
}: {
label?: ReactNode;
isRequired: boolean;
}) => {
if (!label) {
return <></>;
}
return (
<Text marginRight="auto" fontWeight="bold">
{label}{' '}
{isRequired && (
<Text as="span" color="red.600">
*
</Text>
)}
</Text>
);
};
const DisplayHelp = ({ help }: { help?: ReactNode }) => {
if (!help) {
return <></>;
}
return (
<Flex direction="row">
<MdHelpOutline />
<Text alignContent="center">{help}</Text>
</Flex>
);
};
const DisplayError = ({ error }: { error?: ReactNode }) => {
if (!error) {
return <></>;
}
return (
<Flex direction="row" color="red.600">
<MdErrorOutline />
<Text alignContent="center">{error}</Text>
</Flex>
);
};
export type FormGroupProps = { export type FormGroupProps = {
children: ReactNode;
name: string;
error?: ReactNode; error?: ReactNode;
help?: ReactNode; help?: ReactNode;
label?: ReactNode; label?: ReactNode;
isModify?: boolean;
onRestore?: () => void;
isRequired?: boolean; isRequired?: boolean;
children: ReactNode; disableSingleLine?: boolean;
enableModifyNotification?: boolean;
enableReset?: boolean;
}; };
export const FormGroup = ({ export const FormGroup = ({
children, children,
error, name,
help, help,
label, label,
isModify = false,
isRequired = false, isRequired = false,
enableModifyNotification = false, disableSingleLine,
enableReset = false, }: FormGroupProps) => {
onRestore, const { form, error, isModify, onRestore } =
}: FormGroupProps) => ( useFormidableContextElement(name);
<Flex const enableModifyNotification =
borderLeftWidth="3px" form.configuration.enableModifyNotification ?? false;
borderLeftColor={error ? 'red' : enableModifyNotification && isModify ? 'blue' : '#00000000'} const enableReset = form.configuration.enableReset ?? false;
paddingLeft="7px" const singleLine = disableSingleLine
paddingY="4px" ? false
width="full" : form.configuration.singleLineForm;
direction="column" return (
> <Flex
<Flex direction="row" width="full" gap="52px"> borderLeftWidth="3px"
{!!label && ( borderLeftColor={
<Text marginRight="auto" fontWeight="bold"> error
{label}{' '} ? 'red.600'
{isRequired && ( : enableModifyNotification && isModify
<Text as="span" color="red.600"> ? 'blue.600'
* : '#00000000'
</Text> }
)} paddingLeft="7px"
</Text> paddingY="4px"
width="full"
direction="column"
>
{singleLine && (
<>
<Flex direction="row" width="full" gap="52px">
<Box width="10%">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
)}
</Box>
<Flex direction="column" width={'90%'} gap="5px">
{children}
<DisplayHelp help={help} />
<DisplayError error={error} />
</Flex>
</Flex>
</>
)} )}
{!!onRestore && isModify && enableReset && ( {!singleLine && (
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" /> <>
<Flex direction="row" width="full" gap="52px">
<Box width="full">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
)}
</Box>
</Flex>
{children}
<DisplayHelp help={help} />
<DisplayError error={error} />
</>
)} )}
</Flex> </Flex>
{children} );
{!!help && ( };
<Flex direction="row">
<MdHelpOutline />
<Text>{help}</Text>
</Flex>
)}
{!!error && (
<Flex direction="row">
<MdErrorOutline />
<Text>{error}</Text>
</Flex>
)}
</Flex>
);

View File

@ -19,13 +19,10 @@ export const FormInput = ({
placeholder, placeholder,
...rest ...rest
}: FormInputProps) => { }: FormInputProps) => {
const {form, value, isModify, onChange, onRestore} = useFormidableContextElement(name); const {value, onChange} = useFormidableContextElement(name);
return ( return (
<FormGroup <FormGroup
enableModifyNotification={form.configuration.enableModifyNotification} name={name}
enableReset={form.configuration.enableReset}
isModify={isModify}
onRestore={onRestore}
{...rest} {...rest}
> >
<Input <Input

View File

@ -1,8 +1,13 @@
import { RefObject } from 'react'; import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { NumberInputField, NumberInputProps, NumberInputRoot } from '../ui/number-input';
import { useFormidableContextElement } from '../formidable'; import { useFormidableContextElement } from '../formidable';
import {
NumberInputField,
NumberInputProps,
NumberInputRoot,
} from '../ui/number-input';
export type FormNumberProps = Pick< export type FormNumberProps = Pick<
NumberInputProps, NumberInputProps,
@ -25,15 +30,10 @@ export const FormNumber = ({
defaultValue, defaultValue,
...rest ...rest
}: FormNumberProps) => { }: FormNumberProps) => {
const {form, value, isModify, onChange, onRestore} = useFormidableContextElement(name); const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
enableModifyNotification={form.configuration.enableModifyNotification}
enableReset={form.configuration.enableReset}
isModify={isModify}
onRestore={onRestore}
{...rest}
>
<NumberInputRoot <NumberInputRoot
ref={ref} ref={ref}
value={value} value={value}

View File

@ -21,32 +21,29 @@ export const FormPassword = ({
placeholder, placeholder,
...rest ...rest
}: FormInputProps) => { }: FormInputProps) => {
const {form, value, isModify, onChange, onRestore} = useFormidableContextElement(name); const {value, onChange} = useFormidableContextElement(name);
const [showPassword, setShowPassword] = useState<boolean>(false); const [showPassword, setShowPassword] = useState<boolean>(false);
function toggleVisible(): void { function toggleVisible(): void {
setShowPassword((value) => ! value) setShowPassword((value) => ! value)
} }
return ( return (
<FormGroup <FormGroup
enableModifyNotification={form.configuration.enableModifyNotification} name={name}
enableReset={form.configuration.enableReset}
isModify={isModify}
onRestore={onRestore}
{...rest} {...rest}
> >
<chakra.div position="relative"> <chakra.div position="relative" width="full">
<Input <Input
ref={ref} ref={ref}
type={showPassword? "text" : "password"} type={showPassword? "text" : "password"}
name={name} name={name}
autoComplete={name} autoComplete={name}
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
paddingRight="47px" paddingRight="47px"
/> />
<Button variant="ghost" onClick={toggleVisible} position="absolute" top="0" right="0" _hover={{bg:"#0000", shadow:"none", color:"black"}}> <Button variant="ghost" onClick={toggleVisible} position="absolute" top="0" right="0" _hover={{bg:"#0000", shadow:"none", color:"black"}}>
{showPassword? <LuEye/> : <LuEyeOff/>} {showPassword? <LuEye/> : <LuEyeOff/>}
</Button> </Button>
</chakra.div> </chakra.div>
</FormGroup> </FormGroup>
); );

View File

@ -1,6 +1,8 @@
import { RefObject } from 'react'; import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { SelectSingle } from '@/components/select/SelectSingle'; import { SelectSingle } from '@/components/select/SelectSingle';
import { useFormidableContextElement } from '../formidable'; import { useFormidableContextElement } from '../formidable';
export type FormSelectProps = { export type FormSelectProps = {
@ -37,21 +39,18 @@ export const FormSelect = ({
addNewItem, addNewItem,
...rest ...rest
}: FormSelectProps) => { }: FormSelectProps) => {
const {form, value, isModify, onChange, onRestore} = useFormidableContextElement(name); const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
// if set add capability to add the search item // if set add capability to add the search item
const onCreate = !addNewItem const onCreate = !addNewItem
? undefined ? undefined
: (data: string) => { : (data: string) => {
addNewItem(data).then((data: object) => form.setValues({ [name]: data[keyInputKey] })); addNewItem(data).then((data: object) =>
}; form.setValues({ [name]: data[keyInputKey] })
);
};
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
enableModifyNotification={form.configuration.enableModifyNotification}
enableReset={form.configuration.enableReset}
isModify={isModify}
onRestore={onRestore}
{...rest}
>
<SelectSingle <SelectSingle
ref={ref} ref={ref}
value={value} value={value}

View File

@ -2,7 +2,11 @@ import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { SelectMultiple } from '@/components/select/SelectMultiple'; import { SelectMultiple } from '@/components/select/SelectMultiple';
import { useFormidableContext, useFormidableContextElement } from '../formidable';
import {
useFormidableContext,
useFormidableContextElement,
} from '../formidable';
export type FormSelectMultipleProps = { export type FormSelectMultipleProps = {
// Form: Name of the variable // Form: Name of the variable
@ -35,21 +39,20 @@ export const FormSelectMultiple = ({
addNewItem, addNewItem,
...rest ...rest
}: FormSelectMultipleProps) => { }: FormSelectMultipleProps) => {
const {form, value, isModify, onChange, onRestore} = useFormidableContextElement(name); const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
// if set add capability to add the search item // if set add capability to add the search item
const onCreate = !addNewItem const onCreate = !addNewItem
? undefined ? undefined
: (data: string) => { : (data: string) => {
addNewItem(data).then((data: object) => form.setValues({ [name]: [...(form.values[name] ?? []), data[keyInputKey]] })); addNewItem(data).then((data: object) =>
}; form.setValues({
[name]: [...(form.values[name] ?? []), data[keyInputKey]],
})
);
};
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
enableModifyNotification={form.configuration.enableModifyNotification}
enableReset={form.configuration.enableReset}
isModify={isModify}
onRestore={onRestore}
{...rest}
>
<SelectMultiple <SelectMultiple
//ref={ref} //ref={ref}
values={value} values={value}

View File

@ -3,6 +3,7 @@ import { RefObject } from 'react';
import { Textarea } from '@chakra-ui/react'; import { Textarea } from '@chakra-ui/react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { useFormidableContextElement } from '../formidable'; import { useFormidableContextElement } from '../formidable';
export type FormTextareaProps = { export type FormTextareaProps = {
@ -19,15 +20,10 @@ export const FormTextarea = ({
placeholder, placeholder,
...rest ...rest
}: FormTextareaProps) => { }: FormTextareaProps) => {
const {form, value, isModify, onChange, onRestore} = useFormidableContextElement(name); const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
enableModifyNotification={form.configuration.enableModifyNotification}
enableReset={form.configuration.enableReset}
isModify={isModify}
onRestore={onRestore}
{...rest}
>
<Textarea <Textarea
name={name} name={name}
ref={ref} ref={ref}

View File

@ -1,26 +1,39 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { z as zod } from 'zod';
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator'; import { isArray, isNullOrUndefined, isObject } from '@/utils/validator';
import { getDifferences, hasAnyTrue } from './utils'; import { getDifferences, hasAnyTrue } from './utils';
export type FormidableConfig = { export type FormidableConfig = {
enableReset?: boolean; enableReset?: boolean;
enableModifyNotification?: boolean; enableModifyNotification?: boolean;
singleLineForm?: boolean;
}; };
const initialFormConfig: Required<FormidableConfig> = { const initialFormConfig: Required<FormidableConfig> = {
enableReset: true, enableReset: true,
enableModifyNotification: true, enableModifyNotification: true,
singleLineForm: false,
}; };
export const useFormidable = <TYPE extends object = object>({ export const useFormidable = <TYPE extends object = object>({
initialValues = {} as TYPE, initialValues = {} as TYPE,
configuration: inputConfiguration = initialFormConfig, configuration: inputConfiguration = initialFormConfig,
resolver = (_data: TYPE) => {
return {};
},
}: { }: {
initialValues?: TYPE; initialValues?: TYPE;
configuration?: FormidableConfig; configuration?: FormidableConfig;
resolver?: (data: any) => Record<string, string>;
}) => { }) => {
const configuration: Required<FormidableConfig> = { ...initialFormConfig, ...inputConfiguration }; const configuration: Required<FormidableConfig> = {
...initialFormConfig,
...inputConfiguration,
};
const [values, setValues] = useState<TYPE>({ ...initialValues } as TYPE); const [values, setValues] = useState<TYPE>({ ...initialValues } as TYPE);
const [errors, setErrors] = useState<object>({});
const [initialData, setInitialData] = useState<TYPE>(initialValues); const [initialData, setInitialData] = useState<TYPE>(initialValues);
const [isModify, setIsModify] = useState<{ [key: string]: boolean }>({}); const [isModify, setIsModify] = useState<{ [key: string]: boolean }>({});
const [isFormModified, setIsFormModified] = useState<boolean>(false); const [isFormModified, setIsFormModified] = useState<boolean>(false);
@ -57,10 +70,11 @@ export const useFormidable = <TYPE extends object = object>({
const ret = getDifferences(initialData, newValues); const ret = getDifferences(initialData, newValues);
setIsModify(ret); setIsModify(ret);
setIsFormModified(hasAnyTrue(ret)); setIsFormModified(hasAnyTrue(ret));
setErrors(resolver(newValues));
return newValues; return newValues;
}); });
}, },
[setValues, initialData] [setValues, initialData, setErrors, setIsFormModified, setIsModify]
); );
const restoreValue = useCallback( const restoreValue = useCallback(
(data: object) => { (data: object) => {
@ -121,6 +135,7 @@ export const useFormidable = <TYPE extends object = object>({
restoreValue, restoreValue,
setValues: setValuesExternal, setValues: setValuesExternal,
values, values,
errors,
configuration, configuration,
}; };
}; };

View File

@ -1,8 +1,10 @@
import {
ReactNode,
createContext,
useCallback,
import { ReactNode, createContext, useCallback, useContext, useMemo } from 'react'; useContext,
useMemo,
} from 'react';
import { UseFormidableReturn } from './FormidableConfig'; import { UseFormidableReturn } from './FormidableConfig';
@ -12,47 +14,65 @@ export type FromContextProps = {
export const formContext = createContext<FromContextProps>({ export const formContext = createContext<FromContextProps>({
form: { form: {
getDeltaData: ({}:{omit?: string[], only?: string[]}) => {return {};}, getDeltaData: ({}: { omit?: string[]; only?: string[] }) => {
return {};
},
isFormModified: false, isFormModified: false,
isModify: { }, isModify: {},
restoreValues: () => {}, restoreValues: () => {},
restoreValue: (_data: object) => {}, restoreValue: (_data: object) => {},
setValues: (_data: object) => {}, setValues: (_data: object) => {},
values: {}, values: {},
configuration: {enableReset: false, enableModifyNotification: false} errors: {},
} configuration: {
enableReset: false,
enableModifyNotification: false,
singleLineForm: false,
},
},
}); });
export const useFormidableContext = () => { export const useFormidableContext = () => {
const context = useContext(formContext); const context = useContext(formContext);
if (!context) { if (!context) {
throw new Error("useFormContext must be used within a FormProvider"); throw new Error('useFormContext must be used within a FormProvider');
} }
if (!context.form) { if (!context.form) {
throw new Error("useFormContext without defining a From"); throw new Error('useFormContext without defining a From');
} }
return context; return context;
}; };
export const useFormidableContextElement = (name: string) => { export const useFormidableContextElement = (name: string) => {
const {form} = useFormidableContext(); const { form } = useFormidableContext();
if (name === undefined) { if (name === undefined) {
console.error("Can not request useFormidableContextElement with empty 'name'"); console.error(
"Can not request useFormidableContextElement with empty 'name'"
);
} }
const onChange = useCallback((value) => { const onChange = useCallback(
console.log(`new values: ${name}=>${value}`); (value) => {
form.setValues({ [name]: value }); console.log(`new values: ${name}=>${value}`);
}, [name, form, form.setValues]); form.setValues({ [name]: value });
},
[name, form, form.setValues]
);
const onRestore = useCallback(() => { const onRestore = useCallback(() => {
form.restoreValue({ [name]: true }); form.restoreValue({ [name]: true });
}, [name, form, form.restoreValue]); }, [name, form, form.restoreValue]);
return {form, value: form.values[name], isModify: form.isModify[name], onChange, onRestore}; return {
form,
value: form.values[name],
error: form.errors[name],
isModify: form.isModify[name],
onChange,
onRestore,
};
}; };
export type FormidableContextProps= { export type FormidableContextProps = {
form: UseFormidableReturn; form: UseFormidableReturn;
children: ReactNode; children: ReactNode;
} };
export const FormidableContext = ({ export const FormidableContext = ({
form, form,
@ -65,9 +85,6 @@ export const FormidableContext = ({
[form] [form]
); );
return ( return (
<formContext.Provider value={memoContext}> <formContext.Provider value={memoContext}>{children}</formContext.Provider>
{children}
</formContext.Provider>
); );
}; };

View File

@ -1,31 +1,30 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Box } from '@chakra-ui/react';
import { useFormidable } from './FormidableConfig'; import { useFormidable } from './FormidableConfig';
import { FormidableContext } from './FormidableContext'; import { FormidableContext } from './FormidableContext';
import { Box } from '@chakra-ui/react';
export interface FormidableFormProps<TYPE extends object = object>{ export interface FormidableFormProps<TYPE extends object = object> {
form: ReturnType<typeof useFormidable<TYPE>>; form: ReturnType<typeof useFormidable<TYPE>>;
children: ReactNode; children: ReactNode;
onSubmit?:( data: TYPE) => void; onSubmit?: (data: TYPE) => void;
onSubmitDelta?:( data: Partial<TYPE>) => void; onSubmitDelta?: (data: Partial<TYPE>) => void;
} }
export const FormidableForm = <TYPE extends object = object>({ export const FormidableForm = <TYPE extends object = object>({
onSubmit, onSubmit,
onSubmitDelta, onSubmitDelta,
form, form,
children, children,
}: FormidableFormProps<TYPE>) => { }: FormidableFormProps<TYPE>) => {
const handleSubmit = (event: React.FormEvent) => { const handleSubmit = (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
const hasErrors = false;//Object.values(errors).some((err) => err); const hasErrors = false; //Object.values(errors).some((err) => err);
if (!hasErrors) { if (!hasErrors) {
console.log(`request From submit !!! ${JSON.stringify(form.values, null, 2)}`); console.log(
`request From submit !!! ${JSON.stringify(form.values, null, 2)}`
);
if (onSubmit) { if (onSubmit) {
onSubmit(form.values); onSubmit(form.values);
} }
@ -37,9 +36,8 @@ onSubmitDelta,
return ( return (
<FormidableContext form={form}> <FormidableContext form={form}>
<Box as="form" onSubmit={handleSubmit}> <Box as="form" onSubmit={handleSubmit}>
{children} {children}
</Box> </Box>
</FormidableContext> </FormidableContext>
); );
}; };

View File

@ -5,4 +5,7 @@ export {
} from './FormidableContext' } from './FormidableContext'
export { export {
useFormidable useFormidable
} from './FormidableConfig'; } from './FormidableConfig';
export {
zodResolver
} from './utils';

View File

@ -1,56 +1,87 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ZodError } from 'zod';
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator'; import { isArray, isNullOrUndefined, isObject } from '@/utils/validator';
export const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => { export const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => {
for (const key in obj) { for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === true) { if (obj.hasOwnProperty(key) && obj[key] === true) {
return true; return true;
}
} }
return false; }
return false;
}; };
export function getDifferences( export function getDifferences(
obj1: object, obj1: object,
obj2: object obj2: object
): { [key: string]: boolean } { ): { [key: string]: boolean } {
// Create an empty object to store the differences // Create an empty object to store the differences
const result: { [key: string]: boolean } = {}; const result: { [key: string]: boolean } = {};
// Recursive function to compare values // Recursive function to compare values
function compareValues(value1: any, value2: any): boolean { function compareValues(value1: any, value2: any): boolean {
// If both values are objects, compare their properties recursively // If both values are objects, compare their properties recursively
if (isObject(value1) && isObject(value2)) { if (isObject(value1) && isObject(value2)) {
return hasAnyTrue(getDifferences(value1, value2)); return hasAnyTrue(getDifferences(value1, value2));
}
// If both values are arrays, compare their elements
if (isArray(value1) && isArray(value2)) {
//console.log(`Check is array: ${JSON.stringify(value1)} =?= ${JSON.stringify(value2)}`);
if (value1.length !== value2.length) {
return true;
}
for (let i = 0; i < value1.length; i++) {
if (compareValues(value1[i], value2[i])) {
return true;
}
}
return false;
}
// Otherwise, compare the values directly
//console.log(`compare : ${value1} =?= ${value2}`);
return value1 !== value2;
} }
// If both values are arrays, compare their elements
// Get all keys from both objects if (isArray(value1) && isArray(value2)) {
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]); //console.log(`Check is array: ${JSON.stringify(value1)} =?= ${JSON.stringify(value2)}`);
if (value1.length !== value2.length) {
// Iterate over all keys return true;
for (const key of allKeys) { }
if (compareValues(obj1[key], obj2[key])) { for (let i = 0; i < value1.length; i++) {
result[key] = true; if (compareValues(value1[i], value2[i])) {
} else { return true;
result[key] = false;
} }
}
return false;
} }
return result; // Otherwise, compare the values directly
//console.log(`compare : ${value1} =?= ${value2}`);
return value1 !== value2;
}
// Get all keys from both objects
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
// Iterate over all keys
for (const key of allKeys) {
if (compareValues(obj1[key], obj2[key])) {
result[key] = true;
} else {
result[key] = false;
}
}
return result;
} }
export const zodResolver = (zodModel) => {
return (data: any) => {
try {
console.log(`check resolver of: ${JSON.stringify(data, null, 2)}`);
zodModel.parse(data);
return {};
} catch (error) {
if (error instanceof ZodError) {
console.log(
`catch error with resolver: ${JSON.stringify(error, null, 2)}`
);
const formattedErrors = error.issues.reduce(
(acc, issue) => {
if (issue.path.length > 0) {
acc[issue.path[0]] = issue.message;
}
return acc;
},
{} as Record<string, string>
);
console.log(`get errors: ${JSON.stringify(formattedErrors, null, 2)}`);
return formattedErrors;
}
// prevent zod error
throw error;
}
};
};

View File

@ -1,22 +1,22 @@
import { Button, Table, Text } from '@chakra-ui/react'; import { Button, Table, Text } from '@chakra-ui/react';
import { UserCreateWrite, ZodUserCreateWrite } from '@/back-api';
import { PageLayout } from '@/components/Layout/PageLayout'; import { PageLayout } from '@/components/Layout/PageLayout';
import { ParameterLayout } from '@/components/ParameterLayout';
import { TopBar } from '@/components/TopBar/TopBar'; import { TopBar } from '@/components/TopBar/TopBar';
import { UserCreateWrite } from '@/back-api';
import { useFormidable } from '@/components/formidable/FormidableConfig';
import { FormInput } from '@/components/form/FormInput'; import { FormInput } from '@/components/form/FormInput';
import { FormPassword } from '@/components/form/FormPassword'; import { FormPassword } from '@/components/form/FormPassword';
import { ParameterLayout } from '@/components/ParameterLayout'; import { Formidable, zodResolver } from '@/components/formidable';
import { useFormidable } from '@/components/formidable/FormidableConfig';
import { UserService } from '@/service/user.service'; import { UserService } from '@/service/user.service';
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: "numeric", year: 'numeric',
month: "2-digit", month: '2-digit',
day: "2-digit", day: '2-digit',
hour: "2-digit", hour: '2-digit',
minute: "2-digit", minute: '2-digit',
second: "2-digit", second: '2-digit',
//timeZoneName: "short", //timeZoneName: "short",
}; };
export const ManageAccountPage = () => { export const ManageAccountPage = () => {
@ -26,55 +26,66 @@ export const ManageAccountPage = () => {
<TopBar title="Manage accounts" /> <TopBar title="Manage accounts" />
<PageLayout> <PageLayout>
<ParameterLayout.Root> <ParameterLayout.Root>
<ParameterLayout.HeaderBase title="Users" description="List of all users"/> <ParameterLayout.HeaderBase
<ParameterLayout.Content> title="Users"
<Table.Root size="sm" variant="outline" interactive> description="List of all users"
<Table.Header> />
<Table.Row> <ParameterLayout.Content>
<Table.ColumnHeader>id</Table.ColumnHeader> <Table.Root size="sm" variant="outline" interactive>
<Table.ColumnHeader>Login</Table.ColumnHeader> <Table.Header>
<Table.ColumnHeader>e-mail</Table.ColumnHeader> <Table.Row>
<Table.ColumnHeader>blocked</Table.ColumnHeader> <Table.ColumnHeader>id</Table.ColumnHeader>
<Table.ColumnHeader>avatar</Table.ColumnHeader> <Table.ColumnHeader>Login</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">Last connection</Table.ColumnHeader> <Table.ColumnHeader>e-mail</Table.ColumnHeader>
</Table.Row> <Table.ColumnHeader>blocked</Table.ColumnHeader>
</Table.Header> <Table.ColumnHeader>avatar</Table.ColumnHeader>
<Table.Body> <Table.ColumnHeader textAlign="end">
{users?.map((data) => ( Last connection
<Table.Row key={data.id}> </Table.ColumnHeader>
<Table.Cell>{data.id}</Table.Cell> </Table.Row>
<Table.Cell>{data.login}</Table.Cell> </Table.Header>
<Table.Cell>{data.email}</Table.Cell> <Table.Body>
<Table.Cell>{data.blocked? "true" : "false"}</Table.Cell> {users?.map((data) => (
<Table.Cell>{data.avatar? "yes" : "no"}</Table.Cell> <Table.Row key={data.id}>
<Table.Cell textAlign="end"> <Table.Cell>{data.id}</Table.Cell>
{data.lastConnection ? (new Date(data.lastConnection)).toLocaleDateString("fr-FR", options): ""} <Table.Cell>{data.login}</Table.Cell>
</Table.Cell> <Table.Cell>{data.email}</Table.Cell>
</Table.Row> <Table.Cell>{data.blocked ? 'true' : 'false'}</Table.Cell>
))} <Table.Cell>{data.avatar ? 'yes' : 'no'}</Table.Cell>
</Table.Body> <Table.Cell textAlign="end">
</Table.Root> {data.lastConnection
</ParameterLayout.Content> ? new Date(data.lastConnection).toLocaleDateString(
<ParameterLayout.Footer/> 'fr-FR',
options
)
: ''}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</ParameterLayout.Content>
<ParameterLayout.Footer />
</ParameterLayout.Root> </ParameterLayout.Root>
<CreateUserComponent/> <CreateUserComponent />
</PageLayout> </PageLayout>
</> </>
); );
}; };
export const CreateUserComponent = () => { export const CreateUserComponent = () => {
const form = useFormidable<UserCreateWrite>({ const form = useFormidable<UserCreateWrite>({
initialValues: { initialValues: {
login: "", login: '',
email: "", email: '',
password: "", password: '',
}, },
configuration: { configuration: {
enableModifyNotification: false, enableModifyNotification: false,
enableReset: false, enableReset: false,
} singleLineForm: true,
},
resolver: zodResolver(ZodUserCreateWrite),
}); });
//const { connect, lastError, isConnectionLoading } = useLogin(); //const { connect, lastError, isConnectionLoading } = useLogin();
@ -84,36 +95,35 @@ export const CreateUserComponent = () => {
}; };
return ( return (
<ParameterLayout.Root> <Formidable.From form={form}>
<ParameterLayout.HeaderBase title="Create users" /> <ParameterLayout.Root>
<ParameterLayout.Content> <ParameterLayout.HeaderBase
{userCreate.error && <Text colorPalette="@danger">{userCreate.error.message}</Text>} title="Create a users"
<FormInput description="Add a new user on the server"
form={form}
name="login"
isRequired
label="Username"
/> />
<FormPassword <ParameterLayout.Content>
form={form} {userCreate.error && (
name="password" <Text colorPalette="@danger">{userCreate.error.message}</Text>
isRequired )}
label="Password" <FormInput name="login" isRequired label="Username" />
/> <FormInput name="email" isRequired label="E-mail" />
</ParameterLayout.Content> <FormPassword name="password" isRequired label="Password" />
<ParameterLayout.Footer> </ParameterLayout.Content>
<Button <ParameterLayout.Footer>
marginLeft="55%" <Button
marginTop="10px" marginLeft="auto"
variant="solid" marginTop="10px"
background="green" variant="solid"
width="45%" background="green"
disabled={userCreate.isCalling} width="250px"
onClick={onSubmit} maxWidth="45%"
> disabled={userCreate.isCalling}
Login onClick={onSubmit}
</Button> >
</ParameterLayout.Footer> Login
</ParameterLayout.Root>); </Button>
</ParameterLayout.Footer>
} </ParameterLayout.Root>
</Formidable.From>
);
};

View File

@ -7,7 +7,6 @@ import { FormInput } from '@/components/form/FormInput';
import { useLogin } from './useLogin'; import { useLogin } from './useLogin';
import { FormPassword } from '@/components/form/FormPassword'; import { FormPassword } from '@/components/form/FormPassword';
import { FormidableForm } from '@/components/formidable/FormidableForm'; import { FormidableForm } from '@/components/formidable/FormidableForm';
import { on } from 'events';
type LoginFormInputs = { type LoginFormInputs = {
login: string; login: string;