Merge branch 'wip/form-rework' into dev

This commit is contained in:
Maxime Lalo 2023-05-17 17:57:41 +02:00
commit abcd9c582d
10 changed files with 172 additions and 121 deletions

View File

@ -119,17 +119,18 @@ export default abstract class BaseApiService {
responseJson = await response.json(); responseJson = await response.json();
} catch (err: unknown) { } catch (err: unknown) {
responseJson = null; responseJson = null;
return Promise.reject(err);
} }
if (!response.ok) { if (!response.ok) {
return Promise.reject(response); return Promise.reject(responseJson);
} }
return responseJson as T; return responseJson as T;
} }
protected onError(error: unknown) { protected onError(error: unknown) {
console.error(error); //console.error(error);
} }
} }

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { FormContext, IFormContext } from "."; import { FormContext, IFormContext } from ".";
import { ValidationError } from "class-validator"; import { ValidationError } from "class-validator";
import Typography, { ITypo, ITypoColor } from "../Typography";
export type IProps = { export type IProps = {
value?: string; value?: string;
@ -34,6 +35,10 @@ export default abstract class BaseField<P extends IProps, S extends IState = ISt
constructor(props: P) { constructor(props: P) {
super(props); super(props);
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.hasError = this.hasError.bind(this);
this.renderErrors = this.renderErrors.bind(this);
} }
public override componentDidMount() { public override componentDidMount() {
@ -68,11 +73,39 @@ export default abstract class BaseField<P extends IProps, S extends IState = ISt
}; };
} }
protected onFocus(event: React.FocusEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.context?.onFieldFocusChange(this.props.name, this, true);
}
protected onBlur(event: React.FocusEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.context?.onFieldFocusChange(this.props.name, this, false);
}
protected onChange(event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) { protected onChange(event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) {
this.context?.onFieldChange(this.props.name, this); this.context?.onFieldChange(this.props.name, this);
this.setState({ value: event.currentTarget.value }); this.setState({ value: event.currentTarget.value, validationError: null });
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(event); this.props.onChange(event);
} }
} }
protected hasError(): boolean {
return this.state.validationError !== null;
// if(!this.context) return false;
// if(!this.context.hasOneFocusedInput() && this.state.validationError !== null) return true;
// return this.state.validationError !== null && this.context.isInputFocused(this.props.name);
}
protected renderErrors(): JSX.Element[] | null {
if (!this.state.validationError || !this.state.validationError.constraints) return null;
let errors: JSX.Element[] = [];
Object.entries(this.state.validationError.constraints).forEach(([key, value]) => {
errors.push(
<Typography key={key} typo={ITypo.CAPTION_14} color={ITypoColor.RED_FLASH}>
{value}
</Typography>,
);
});
return errors;
}
} }

View File

@ -15,7 +15,7 @@ type IProps = {
hasBorderRightCollapsed?: boolean; hasBorderRightCollapsed?: boolean;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
name?: string; name: string;
disabled: boolean; disabled: boolean;
errors?: ValidationError; errors?: ValidationError;
}; };
@ -60,53 +60,58 @@ export default class SelectField extends React.Component<IProps, IState> {
public override render(): JSX.Element { public override render(): JSX.Element {
const selectedOption = this.state.selectedOption ?? this.props.selectedOption; const selectedOption = this.state.selectedOption ?? this.props.selectedOption;
return ( return (
<div <div className={classes["container"]}>
className={classNames(classes["root"], this.props.className)} <div
ref={this.rootRef} className={classNames(classes["root"], this.props.className)}
data-disabled={this.props.disabled.toString()} ref={this.rootRef}
data-errored={(this.state.errors !== null).toString()}> data-disabled={this.props.disabled.toString()}
{selectedOption && <input type="text" defaultValue={selectedOption.value as string} name={this.props.name} hidden />} data-errored={(this.state.errors !== null).toString()}>
<label {selectedOption && <input type="text" defaultValue={selectedOption.value as string} name={this.props.name} hidden />}
className={classNames(classes["container-label"])} <label
data-open={this.state.isOpen} className={classNames(classes["container-label"])}
onClick={this.toggle} data-open={this.state.isOpen}
data-border-right-collapsed={this.props.hasBorderRightCollapsed}> onClick={this.toggle}
<div className={classNames(classes["container-input"])}> data-border-right-collapsed={this.props.hasBorderRightCollapsed}>
{selectedOption && ( <div className={classNames(classes["container-input"])}>
<> {selectedOption && (
<span className={classNames(classes["icon"], classes["token-icon"])}>{selectedOption?.icon}</span> <>
<Typography typo={ITypo.P_18}> <span className={classNames(classes["icon"], classes["token-icon"])}>{selectedOption?.icon}</span>
<span className={classes["text"]}>{selectedOption?.label}</span> <Typography typo={ITypo.P_18}>
</Typography> <span className={classes["text"]}>{selectedOption?.label}</span>
</> </Typography>
)} </>
{!selectedOption && ( )}
<div className={classes["placeholder"]} data-open={(selectedOption ? true : false).toString()}> {!selectedOption && (
<Typography typo={ITypo.NAV_INPUT_16}> <div className={classes["placeholder"]} data-open={(selectedOption ? true : false).toString()}>
<span className={classes["text"]}>{this.props.placeholder ?? ""}</span> <Typography typo={ITypo.NAV_INPUT_16}>
</Typography> <span className={classes["text"]}>{this.props.placeholder ?? ""}</span>
</div> </Typography>
)} </div>
</div> )}
<Image className={classes["chevron-icon"]} data-open={this.state.isOpen} src={ChevronIcon} alt="chevron icon" /> </div>
</label> <Image className={classes["chevron-icon"]} data-open={this.state.isOpen} src={ChevronIcon} alt="chevron icon" />
</label>
<ul <ul
className={classes["container-ul"]} className={classes["container-ul"]}
data-open={this.state.isOpen} data-open={this.state.isOpen}
ref={this.contentRef} ref={this.contentRef}
style={{ style={{
height: this.state.listHeight + "px", height: this.state.listHeight + "px",
}}> }}>
{this.props.options.map((option, index) => ( {this.props.options.map((option, index) => (
<li key={`${index}-${option.value}`} className={classes["container-li"]} onClick={(e) => this.onSelect(option, e)}> <li
<div className={classes["token-icon"]}>{option.icon}</div> key={`${index}-${option.value}`}
<Typography typo={ITypo.P_18}>{option.label}</Typography> className={classes["container-li"]}
</li> onClick={(e) => this.onSelect(option, e)}>
))} <div className={classes["token-icon"]}>{option.icon}</div>
</ul> <Typography typo={ITypo.P_18}>{option.label}</Typography>
</li>
))}
</ul>
{this.state.isOpen && <div className={classes["backdrop"]} onClick={this.toggle} />} {this.state.isOpen && <div className={classes["backdrop"]} onClick={this.toggle} />}
</div>
{this.state.errors !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>} {this.state.errors !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div> </div>
); );
@ -172,16 +177,12 @@ export default class SelectField extends React.Component<IProps, IState> {
this.toggle(e); this.toggle(e);
} }
private renderErrors(): JSX.Element[] | null { private renderErrors(): JSX.Element | null {
if (!this.state.errors || !this.state.errors.constraints) return null; if (!this.state.errors) return null;
let errors: JSX.Element[] = []; return (
Object.entries(this.state.errors.constraints).forEach(([key, value]) => { <Typography typo={ITypo.CAPTION_14} color={ITypoColor.RED_FLASH}>
errors.push( {this.props.placeholder} est requis
<Typography key={key} typo={ITypo.CAPTION_14} color={ITypoColor.RED_FLASH}> </Typography>
{value} );
</Typography>,
);
});
return errors;
} }
} }

View File

@ -18,34 +18,23 @@ export default class TextField extends BaseField<IProps> {
const value = this.state.value ?? ""; const value = this.state.value ?? "";
return ( return (
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}> <Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
<div className={classes["root"]} data-is-errored={(this.state.validationError !== null).toString()}> <div className={classes["root"]} data-is-errored={this.hasError().toString()}>
<input <input
onChange={this.onChange} onChange={this.onChange}
data-value={value} data-value={value}
data-has-validation-errors={(this.state.validationError === null).toString()} data-has-validation-errors={(this.state.validationError === null).toString()}
className={classnames(classes["input"], this.props.className)} className={classnames(classes["input"], this.props.className)}
value={value} value={value}
onFocus={this.onFocus}
onBlur={this.onBlur}
name={this.props.name} name={this.props.name}
/> />
<div className={classes["fake-placeholder"]}> <div className={classes["fake-placeholder"]}>
{this.props.placeholder} {!this.props.required && " (Facultatif)"} {this.props.placeholder} {!this.props.required && " (Facultatif)"}
</div> </div>
</div> </div>
{this.state.validationError !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>} {this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</Typography> </Typography>
); );
} }
private renderErrors(): JSX.Element[] | null {
if (!this.state.validationError || !this.state.validationError.constraints) return null;
let errors: JSX.Element[] = [];
Object.entries(this.state.validationError.constraints).forEach(([key, value]) => {
errors.push(
<Typography key={key} typo={ITypo.CAPTION_14} color={ITypoColor.RED_FLASH}>
{value}
</Typography>,
);
});
return errors;
}
} }

View File

@ -18,7 +18,7 @@ export default class TextAreaField extends BaseField<IProps> {
const value = this.state.value ?? ""; const value = this.state.value ?? "";
return ( return (
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}> <Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
<div className={classes["root"]} data-is-errored={(this.state.validationError !== null).toString()}> <div className={classes["root"]} data-is-errored={this.hasError().toString()}>
<textarea <textarea
name={this.props.name} name={this.props.name}
rows={4} rows={4}
@ -27,11 +27,13 @@ export default class TextAreaField extends BaseField<IProps> {
className={classnames(classes["textarea"], this.props.className)} className={classnames(classes["textarea"], this.props.className)}
value={value} value={value}
readOnly={this.props.readonly} readOnly={this.props.readonly}
onFocus={this.onFocus}
onBlur={this.onBlur}
/> />
<div className={classes["fake-placeholder"]}> <div className={classes["fake-placeholder"]}>
{this.props.placeholder} {!this.props.required && " (Facultatif)"} {this.props.placeholder} {!this.props.required && " (Facultatif)"}
</div> </div>
{this.state.validationError !== null && <div className={classes["errors-container"]}>{this.renderErrors()}</div>} {this.hasError() && <div className={classes["errors-container"]}>{this.renderErrors()}</div>}
</div> </div>
</Typography> </Typography>
); );
@ -42,17 +44,4 @@ export default class TextAreaField extends BaseField<IProps> {
value: this.props.defaultValue ?? "", value: this.props.defaultValue ?? "",
}); });
} }
private renderErrors(): JSX.Element[] | null {
if (!this.state.validationError || !this.state.validationError.constraints) return null;
let errors: JSX.Element[] = [];
Object.entries(this.state.validationError.constraints).forEach(([key, value]) => {
errors.push(
<Typography key={key} typo={ITypo.CAPTION_14} color={ITypoColor.RED_FLASH}>
{value}
</Typography>,
);
});
return errors;
}
} }

View File

@ -8,25 +8,39 @@ export type IFormContext = {
setField: (name: string, field: IBaseField) => void; setField: (name: string, field: IBaseField) => void;
unSetField: (name: string) => void; unSetField: (name: string) => void;
onFieldChange: (name: string, field: IBaseField) => void; onFieldChange: (name: string, field: IBaseField) => void;
onFieldFocusChange: (name: string, field: IBaseField, focused: boolean) => void;
isInputFocused: (name: string) => boolean;
hasOneFocusedInput: () => boolean;
}; };
type IFields = { type IFields = {
[key: string]: IBaseField; [key: string]: IBaseField;
}; };
type IState = {}; type IState = {
inputFocused: {
name: string;
field: IBaseField;
focused: boolean;
};
};
export type IProps = { export type IProps = {
onFieldFocusChange?: (name: string, field: IBaseField, focused: boolean) => void;
onFieldChange?: (name: string, field: IBaseField) => void; onFieldChange?: (name: string, field: IBaseField) => void;
onSubmit?: ( onSubmit?: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
e: React.FormEvent<HTMLFormElement> | null,
values: { [key: string]: string },
) => void;
className?: string; className?: string;
children?: ReactNode; children?: ReactNode;
}; };
export const FormContext = React.createContext<IFormContext>({ setField: () => {}, unSetField: () => {}, onFieldChange: () => {} }); export const FormContext = React.createContext<IFormContext>({
setField: () => {},
unSetField: () => {},
onFieldChange: () => {},
onFieldFocusChange: () => {},
isInputFocused: () => false,
hasOneFocusedInput: () => false,
});
export default class Form extends React.Component<IProps, IState> { export default class Form extends React.Component<IProps, IState> {
protected fields: IFields = {}; protected fields: IFields = {};
@ -35,12 +49,22 @@ export default class Form extends React.Component<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
this.state = {}; this.state = {
inputFocused: {
name: "",
field: {} as IBaseField,
focused: false,
},
};
this.setField = this.setField.bind(this); this.setField = this.setField.bind(this);
this.unSetField = this.unSetField.bind(this); this.unSetField = this.unSetField.bind(this);
this.onFieldChange = this.onFieldChange.bind(this); this.onFieldChange = this.onFieldChange.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.formRef = React.createRef(); this.formRef = React.createRef();
this.onFieldFocusChange = this.onFieldFocusChange.bind(this);
this.isInputFocused = this.isInputFocused.bind(this);
this.hasOneFocusedInput = this.hasOneFocusedInput.bind(this);
} }
public override render() { public override render() {
@ -50,6 +74,9 @@ export default class Form extends React.Component<IProps, IState> {
setField: this.setField, setField: this.setField,
unSetField: this.unSetField, unSetField: this.unSetField,
onFieldChange: this.onFieldChange, onFieldChange: this.onFieldChange,
onFieldFocusChange: this.onFieldFocusChange,
isInputFocused: this.isInputFocused,
hasOneFocusedInput: this.hasOneFocusedInput,
}}> }}>
<form className={this.props.className} ref={this.formRef} onSubmit={this.onSubmit}> <form className={this.props.className} ref={this.formRef} onSubmit={this.onSubmit}>
{this.props.children} {this.props.children}
@ -108,7 +135,29 @@ export default class Form extends React.Component<IProps, IState> {
delete this.fields[name]; delete this.fields[name];
} }
protected async onFieldChange(name: string, field: IBaseField) { protected hasOneFocusedInput() {
return this.state.inputFocused.focused;
}
protected isInputFocused(name: string) {
return this.state.inputFocused.name === name && this.state.inputFocused.focused;
}
protected onFieldFocusChange(name: string, field: IBaseField, focused: boolean) {
this.setState({
inputFocused: {
name,
field,
focused,
},
});
if (this.props.onFieldFocusChange) {
this.props.onFieldFocusChange(name, field, focused);
}
}
protected onFieldChange(name: string, field: IBaseField) {
if (this.props.onFieldChange) { if (this.props.onFieldChange) {
this.props.onFieldChange(name, field); this.props.onFieldChange(name, field);
} }

View File

@ -200,6 +200,7 @@ export default class DesignSystem extends BasePage<IProps, IState> {
<div className={classes["sub-section"]}> <div className={classes["sub-section"]}>
<div className={classes["folder-conatainer"]}> <div className={classes["folder-conatainer"]}>
<SelectField <SelectField
name="select"
options={selectOptions} options={selectOptions}
onChange={this.onSelectedOption} onChange={this.onSelectedOption}
placeholder={"Type d'acte"} placeholder={"Type d'acte"}

View File

@ -4,13 +4,16 @@ import Folders from "@Front/Api/LeCoffreApi/SuperAdmin/Folders/Folders";
import Users from "@Front/Api/LeCoffreApi/SuperAdmin/Users/Users"; import Users from "@Front/Api/LeCoffreApi/SuperAdmin/Users/Users";
import Button from "@Front/Components/DesignSystem/Button"; import Button from "@Front/Components/DesignSystem/Button";
import Form from "@Front/Components/DesignSystem/Form"; import Form from "@Front/Components/DesignSystem/Form";
import SelectField, { IOption } from "@Front/Components/DesignSystem/Form/SelectField";
import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import MultiSelect from "@Front/Components/DesignSystem/MultiSelect"; import MultiSelect from "@Front/Components/DesignSystem/MultiSelect";
import RadioBox from "@Front/Components/DesignSystem/RadioBox"; import RadioBox from "@Front/Components/DesignSystem/RadioBox";
import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography"; import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography";
import BackArrow from "@Front/Components/Elements/BackArrow"; import BackArrow from "@Front/Components/Elements/BackArrow";
import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage"; import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage";
import { ValidationError } from "class-validator";
import { Deed, DeedType, Office, OfficeFolder, OfficeFolderHasStakeholder } from "le-coffre-resources/dist/Notary"; import { Deed, DeedType, Office, OfficeFolder, OfficeFolderHasStakeholder } from "le-coffre-resources/dist/Notary";
import User from "le-coffre-resources/dist/Notary"; import User from "le-coffre-resources/dist/Notary";
import { NextRouter, useRouter } from "next/router"; import { NextRouter, useRouter } from "next/router";
import React from "react"; import React from "react";
@ -18,10 +21,6 @@ import { ActionMeta, MultiValue } from "react-select";
import BasePage from "../../Base"; import BasePage from "../../Base";
import classes from "./classes.module.scss"; import classes from "./classes.module.scss";
import { ValidationError } from "class-validator";
import SelectField, { IOption } from "@Front/Components/DesignSystem/Form/SelectField";
import TextField from "@Front/Components/DesignSystem/Form/TextField";
import TextAreaField from "@Front/Components/DesignSystem/Form/TextareaField";
type IFormValues = { type IFormValues = {
folder_number: string; folder_number: string;
@ -103,6 +102,7 @@ class CreateFolderClass extends BasePage<IPropsClass, IState> {
/> />
<SelectField <SelectField
options={this.state.deedTypesOptions} options={this.state.deedTypesOptions}
name="deed"
placeholder={"Type d'acte"} placeholder={"Type d'acte"}
onChange={this.onActTypeChange} onChange={this.onActTypeChange}
errors={this.state.validationError.find((error) => error.property === "deed")} errors={this.state.validationError.find((error) => error.property === "deed")}
@ -112,6 +112,7 @@ class CreateFolderClass extends BasePage<IPropsClass, IState> {
placeholder="Note du dossier" placeholder="Note du dossier"
onChange={this.onPersonalNoteChange} onChange={this.onPersonalNoteChange}
validationError={this.state.validationError.find((error) => error.property === "description")} validationError={this.state.validationError.find((error) => error.property === "description")}
required={false}
/> />
</div> </div>
<div className={classes["access-container"]}> <div className={classes["access-container"]}>
@ -273,7 +274,6 @@ class CreateFolderClass extends BasePage<IPropsClass, IState> {
try { try {
await officeFolderForm.validateOrReject?.({ groups: ["createFolder"], forbidUnknownValues: false }); await officeFolderForm.validateOrReject?.({ groups: ["createFolder"], forbidUnknownValues: false });
} catch (validationErrors) { } catch (validationErrors) {
console.log(validationErrors);
this.setState({ this.setState({
validationError: validationErrors as ValidationError[], validationError: validationErrors as ValidationError[],
}); });
@ -285,22 +285,9 @@ class CreateFolderClass extends BasePage<IPropsClass, IState> {
if (!newOfficeFolder) return; if (!newOfficeFolder) return;
this.props.router.push(`/folders/${newOfficeFolder.uid}`); this.props.router.push(`/folders/${newOfficeFolder.uid}`);
} catch (backError: any) { } catch (backError: any) {
if(backError.target && backError.property){ this.setState({
validationError: backError as ValidationError[],
this.setState({ });
validationError: backError as ValidationError[],
});
}else{
console.error(backError);
this.setState({
validationError: [{
constraints: {
unique: "Le numéro de dossier est déjà utilisé"
},
property: "folder_number",
} as ValidationError],
});
}
} }
} }

View File

@ -58,7 +58,7 @@ class UpdateFolderMetadataClass extends BasePage<IPropsClass, IState> {
placeholder="Numéro de dossier" placeholder="Numéro de dossier"
defaultValue={this.state.selectedFolder?.folder_number} defaultValue={this.state.selectedFolder?.folder_number}
/> />
<Select options={[]} placeholder={"Type d'acte"} selectedOption={deedOption} disabled /> <Select name="deed" options={[]} placeholder={"Type d'acte"} selectedOption={deedOption} disabled />
<TextField placeholder="Ouverture du dossier" defaultValue={openingDate.toLocaleDateString("fr-FR")} disabled /> <TextField placeholder="Ouverture du dossier" defaultValue={openingDate.toLocaleDateString("fr-FR")} disabled />
</div> </div>

View File

@ -50,6 +50,7 @@ class UpdateFolderMetadataClass extends BasePage<IProps, IState> {
<TextField name="input field" placeholder="Intitulé du dossier" /> <TextField name="input field" placeholder="Intitulé du dossier" />
<TextField name="input field" placeholder="Numéro de dossier" /> <TextField name="input field" placeholder="Numéro de dossier" />
<Select <Select
name="deed"
options={selectOptions} options={selectOptions}
onChange={this.onSelectedOption} onChange={this.onSelectedOption}
placeholder={"Type d'acte"} placeholder={"Type d'acte"}