Skip to content

Commit

Permalink
Composants par défaut sur domain et précisions des types de composants
Browse files Browse the repository at this point in the history
  • Loading branch information
JabX committed Sep 20, 2024
1 parent 4476d79 commit 048093e
Show file tree
Hide file tree
Showing 31 changed files with 543 additions and 388 deletions.
28 changes: 13 additions & 15 deletions packages/forms/src/components/autocomplete-chips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import {observable} from "mobx";
import {useObserver} from "mobx-react";
import {useCallback, useEffect, useState} from "react";

import {DomainFieldType, DomainTypeMultiple, DomainTypeSingle, SingleDomainFieldType} from "@focus4/stores";
import {DomainFieldTypeMultiple, DomainType, SingleDomainFieldType} from "@focus4/stores";
import {CSSProp, useTheme} from "@focus4/styling";
import {AutocompleteCss, Chip, ChipCss, Icon, TextFieldCss} from "@focus4/toolbox";

import {AutocompleteSearch} from "./autocomplete";
import {SelectChipsCss, selectChipsCss} from "./select-chips";
import {toSimpleType} from "./utils";

export interface AutocompleteChipsProps<T extends DomainFieldType, TSource = {key: string; label: string}> {
export interface AutocompleteChipsProps<T extends DomainFieldTypeMultiple, TSource = {key: string; label: string}> {
/** CSS pour les Chips. */
chipTheme?: CSSProp<ChipCss>;
/** Précise dans quel sens les suggestions doivent s'afficher. Par défaut : "auto". */
Expand All @@ -32,7 +32,7 @@ export interface AutocompleteChipsProps<T extends DomainFieldType, TSource = {ke
*/
getLabel?: (item: TSource) => string;
/** Service de résolution de clé. Doit retourner le libellé. */
keyResolver?: (key: DomainTypeSingle<SingleDomainFieldType<T>>) => Promise<string | undefined>;
keyResolver?: (key: DomainType<SingleDomainFieldType<T>>) => Promise<string | undefined>;
/** Placeholder pour le champ texte. */
hint?: string;
/** Préfixe i18n. Par défaut : "focus". */
Expand All @@ -46,7 +46,7 @@ export interface AutocompleteChipsProps<T extends DomainFieldType, TSource = {ke
/** Nom de l'input. */
name?: string;
/** Est appelé à chaque changement de valeur. */
onChange: (value: DomainTypeMultiple<T>) => void;
onChange: (value: DomainType<T>) => void;
/** Service de recherche. */
querySearcher?: (text: string) => Promise<TSource[]>;
/** Active l'appel à la recherche si le champ est vide. */
Expand All @@ -58,9 +58,9 @@ export interface AutocompleteChipsProps<T extends DomainFieldType, TSource = {ke
/** Type du champ (celui du domaine). */
type: T;
/** Empêche la suppression des valeurs correspondants à ce filtre. */
undeletable?: (value: DomainTypeSingle<SingleDomainFieldType<T>>) => boolean;
undeletable?: (value: DomainType<SingleDomainFieldType<T>>) => boolean;
/** Valeur. */
value?: DomainTypeMultiple<T>;
value?: DomainType<T>;
}

const defaultGetKey = (x: any) => x.key;
Expand All @@ -70,7 +70,7 @@ const defaultGetKey = (x: any) => x.key;
*
* S'utilise avec [`autocompleteFor`](/docs/modèle-métier-afficher-des-champs--docs#autocompleteforfield-options) sur un champ liste.
*/
export function AutocompleteChips<T extends DomainFieldType, TSource = {key: string; label: string}>({
export function AutocompleteChips<const T extends DomainFieldTypeMultiple, TSource = {key: string; label: string}>({
chipTheme,
direction,
disabled = false,
Expand All @@ -91,7 +91,7 @@ export function AutocompleteChips<T extends DomainFieldType, TSource = {key: str
theme: pTheme,
type,
undeletable,
value = [] as DomainTypeMultiple<T>
value = [] as DomainType<T>
}: AutocompleteChipsProps<T, TSource>) {
const theme = useTheme<AutocompleteCss & SelectChipsCss & TextFieldCss>("selectChips", selectChipsCss, pTheme);

Expand All @@ -100,32 +100,30 @@ export function AutocompleteChips<T extends DomainFieldType, TSource = {key: str
useEffect(() => {
if (keyResolver) {
for (const item of value.filter(i => !labels.has(i))) {
keyResolver(item as DomainTypeSingle<SingleDomainFieldType<T>>).then(label =>
labels.set(item, label ?? "")
);
keyResolver(item as DomainType<SingleDomainFieldType<T>>).then(label => labels.set(item, label ?? ""));
}
}
}, [keyResolver, value]);

const handleAddValue = useCallback(
function handleAddValue(v?: boolean | number | string) {
if (v && (!maxSelectable || value.length < maxSelectable)) {
onChange?.([...value, v] as DomainTypeMultiple<T>);
onChange?.([...value, v] as DomainType<T>);
}
},
[onChange, maxSelectable, value]
);

const handleRemoveValue = useCallback(
function handleRemoveValue(v: boolean | number | string) {
onChange?.(value.filter(i => i !== v) as DomainTypeMultiple<T>);
onChange?.(value.filter(i => i !== v) as DomainType<T>);
},
[onChange, value]
);

const handleRemoveAll = useCallback(
function handleRemoveAll() {
onChange?.([] as DomainTypeMultiple<T>);
onChange?.([] as DomainType<T>);
},
[onChange]
);
Expand Down Expand Up @@ -180,7 +178,7 @@ export function AutocompleteChips<T extends DomainFieldType, TSource = {key: str
disabled={disabled}
label={labels.get(item) ?? ""}
onDeleteClick={
!undeletable?.(item as DomainTypeSingle<SingleDomainFieldType<T>>)
!undeletable?.(item as DomainType<SingleDomainFieldType<T>>)
? () => handleRemoveValue(item)
: undefined
}
Expand Down
14 changes: 7 additions & 7 deletions packages/forms/src/components/autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import {debounce} from "lodash";
import {ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useState} from "react";

import {DomainFieldType, DomainTypeSingle} from "@focus4/stores";
import {DomainFieldTypeSingle, DomainType} from "@focus4/stores";
import {Autocomplete, AutocompleteProps} from "@focus4/toolbox";

import {stringToDomainType} from "./utils";

export interface AutocompleteSearchProps<T extends DomainFieldType, TSource = {key: string; label: string}>
export interface AutocompleteSearchProps<T extends DomainFieldTypeSingle, TSource = {key: string; label: string}>
extends Omit<
AutocompleteProps<TSource>,
"error" | "loading" | "onChange" | "suggestionMatch" | "value" | "values"
> {
/** Erreur à afficher sous le champ. */
error?: string;
/** Service de résolution de clé. Doit retourner le libellé. */
keyResolver?: (key: DomainTypeSingle<T>) => Promise<string | undefined>;
keyResolver?: (key: DomainType<T>) => Promise<string | undefined>;
/** Au changement. */
onChange?: (key: DomainTypeSingle<T> | undefined, value?: TSource) => void;
onChange: (key: DomainType<T> | undefined, value?: TSource) => void;
/** Service de recherche. */
querySearcher?: (text: string) => Promise<TSource[]>;
/** Active l'appel à la recherche si le champ est vide. */
Expand All @@ -26,7 +26,7 @@ export interface AutocompleteSearchProps<T extends DomainFieldType, TSource = {k
/** Type du champ (celui du domaine). */
type: T;
/** Valeur. */
value?: DomainTypeSingle<T>;
value?: DomainType<T>;
}

const defaultGetKey = (x: any) => x.key;
Expand All @@ -38,7 +38,7 @@ const defaultGetKey = (x: any) => x.key;
* Il s'agit du composant par défaut de tous les domaines pour [`autocompleteFor`](/docs/modèle-métier-afficher-des-champs--docs#autocompleteforfield-options) (`AutocompleteComponent`).
*/
export const AutocompleteSearch = forwardRef(function AutocompleteSearch<
T extends DomainFieldType,
const T extends DomainFieldTypeSingle,
TSource = {key: string; label: string}
>(
{
Expand Down Expand Up @@ -141,6 +141,6 @@ export const AutocompleteSearch = forwardRef(function AutocompleteSearch<
values={values}
/>
);
}) as <T extends DomainFieldType, TSource = {key: string; label: string}>(
}) as <const T extends DomainFieldTypeSingle, TSource = {key: string; label: string}>(
props: AutocompleteSearchProps<T, TSource> & {ref?: React.ForwardedRef<HTMLInputElement | HTMLTextAreaElement>}
) => ReactElement;
4 changes: 3 additions & 1 deletion packages/forms/src/components/boolean-radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ export interface BooleanRadioProps {
/** Libellé pour le "oui". Par défaut: "focus.boolean.yes" */
labelYes?: string;
/** Name for input field. */
name: string;
name?: string;
/** Call with each value change. */
onChange: (value: boolean) => void;
/** Contrôle l'affichage du texte en dessous du champ, quelque soit la valeur de `supportingText` ou `maxLength`. Par défaut : "always". */
showSupportingText?: "always" | "auto" | "never";
/** Type du domain du champ. */
type: "boolean";
/** CSS. */
theme?: CSSProp<BooleanRadioCss & RadioCss>;
/** Value. */
Expand Down
14 changes: 6 additions & 8 deletions packages/forms/src/components/display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {autorun} from "mobx";
import {useObserver} from "mobx-react";
import {useEffect, useState} from "react";

import {DomainFieldType, DomainType, DomainTypeSingle, ReferenceList, SingleDomainFieldType} from "@focus4/stores";
import {DomainFieldType, DomainType, ReferenceList, SingleDomainFieldType} from "@focus4/stores";
import {CSSProp, useTheme} from "@focus4/styling";

import displayCss, {DisplayCss} from "./__style__/display.css";
Expand All @@ -12,9 +12,9 @@ export {displayCss, DisplayCss};
/** Props du composant d'affichage. */
export interface DisplayProps<T extends DomainFieldType> {
/** Formatteur. */
formatter?: (value: DomainTypeSingle<SingleDomainFieldType<T>> | undefined) => string;
formatter?: (value: DomainType<SingleDomainFieldType<T>> | undefined) => string;
/** Service de résolution de code. */
keyResolver?: (key: DomainTypeSingle<SingleDomainFieldType<T>>) => Promise<string>;
keyResolver?: (key: DomainType<SingleDomainFieldType<T>>) => Promise<string | undefined>;
/** Si renseigné pour un affichage multiple en mode `list`, découpe les listes en plusieurs morceaux pour pouvoir les afficher en colonnes par exemple. */
listChunkSize?: number;
/**
Expand Down Expand Up @@ -60,7 +60,7 @@ export function Display<T extends DomainFieldType>({
if (keyResolver) {
if (Array.isArray(value)) {
Promise.all(
value.map((v: DomainTypeSingle<SingleDomainFieldType<T>>) =>
value.map((v: DomainType<SingleDomainFieldType<T>>) =>
// eslint-disable-next-line @typescript-eslint/no-base-to-string
keyResolver(v).then(res => res ?? `${v}`)
)
Expand All @@ -75,14 +75,12 @@ export function Display<T extends DomainFieldType>({
setLabel(
values
?.filter(v =>
value.find(
(v2: DomainTypeSingle<SingleDomainFieldType<T>>) => v[values.$valueKey] === v2
)
value.find((v2: DomainType<SingleDomainFieldType<T>>) => v[values.$valueKey] === v2)
)
.map(v => formatter(v[values.$labelKey] ?? `${v[values.$valueKey]}`))
);
} else {
setLabel(value.map((v: DomainTypeSingle<SingleDomainFieldType<T>>) => formatter(v)));
setLabel(value.map((v: DomainType<SingleDomainFieldType<T>>) => formatter(v)));
}
} else {
setLabel(formatter(values?.getLabel(value) ?? value));
Expand Down
28 changes: 15 additions & 13 deletions packages/forms/src/components/input-date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ export interface InputDateProps {
timezoneCode?: string;
/** CSS. */
theme?: CSSProp<CalendarCss & InputDateCss>;
/** Type du domaine du champ. */
type: "string";
/** Valeur. */
value: string | undefined;
value?: string;
}

/**
Expand Down Expand Up @@ -117,8 +119,8 @@ export function InputDate({
const zone = timezoneCode
? timezoneCode
: ISOStringFormat === "utc-midnight" || ISOStringFormat === "date-only"
? "utc"
: undefined;
? "utc"
: undefined;

/** Convertit le texte en objet Luxon. */
const toLuxon = useCallback(
Expand Down Expand Up @@ -175,8 +177,8 @@ export function InputDate({
let dateTime = targetInputFormat
? DateTime.fromFormat(newDate, targetInputFormat, zone ? {zone} : {})
: zone === "utc"
? DateTime.utc()
: DateTime.now();
? DateTime.utc()
: DateTime.now();

if (ISOStringFormat === "local-utc-midnight") {
dateTime = dateTime.toUTC();
Expand Down Expand Up @@ -282,14 +284,14 @@ export function InputDate({
calendarPosition === "bottom"
? "bottom-auto"
: calendarPosition === "top"
? "top-auto"
: calendarPosition === "left"
? "auto-left"
: calendarPosition === "right"
? "auto-right"
: calendarPosition === "auto"
? "auto-fit"
: calendarPosition
? "top-auto"
: calendarPosition === "left"
? "auto-left"
: calendarPosition === "right"
? "auto-right"
: calendarPosition === "auto"
? "auto-fit"
: calendarPosition
}
>
<Calendar
Expand Down
15 changes: 8 additions & 7 deletions packages/forms/src/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import {range, takeWhile} from "lodash";
import numeral from "numeral";
import {FormEvent, ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useMemo, useState} from "react";

import {DomainFieldType, DomainTypeSingle} from "@focus4/stores";
import {DomainFieldTypeSingle, DomainType} from "@focus4/stores";
import {TextField, TextFieldProps} from "@focus4/toolbox";

import {MaskDefinition, useMask} from "./utils/mask";
import {getInputSelection, setInputSelection} from "./utils/selection";

export interface InputProps<T extends DomainFieldType> extends Omit<TextFieldProps, "error" | "onChange" | "value"> {
export interface InputProps<T extends DomainFieldTypeSingle>
extends Omit<TextFieldProps, "error" | "onChange" | "value"> {
/** Erreur à afficher sous le champ. */
error?: string;
/** Pour un input de type "number", affiche les séparateurs de milliers. */
Expand Down Expand Up @@ -38,11 +39,11 @@ export interface InputProps<T extends DomainFieldType> extends Omit<TextFieldPro
/** Pour un input de type "number", interdit la saisie de nombres négatifs. */
noNegativeNumbers?: boolean;
/** Handler appelé à chaque saisie. Retourne la valeur dans le type de l'input. */
onChange: (value: DomainTypeSingle<T> | undefined) => void;
onChange: (value: DomainType<T> | undefined) => void;
/** Type du champ (celui du domaine). */
type: T;
/** Valeur. */
value?: DomainTypeSingle<T>;
value?: DomainType<T>;
}

/**
Expand All @@ -53,7 +54,7 @@ export interface InputProps<T extends DomainFieldType> extends Omit<TextFieldPro
*
* Il s'agit du composant par défaut de tous les domaines pour [`fieldFor`](/docs/modèle-métier-afficher-des-champs--docs#fieldforfield-options) (`InputComponent`).
*/
export const Input = forwardRef(function Input<T extends DomainFieldType>(
export const Input = forwardRef(function Input<const T extends DomainFieldTypeSingle>(
{
error,
mask,
Expand Down Expand Up @@ -106,7 +107,7 @@ export const Input = forwardRef(function Input<T extends DomainFieldType>(
const handleChange = useCallback(
(v: string, e: FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (type !== "number") {
onChange(v === "" ? undefined : (v as DomainTypeSingle<T>));
onChange(v === "" ? undefined : (v as DomainType<T>));
} else if (v === "") {
setNumberStringValue("");
onChange(undefined);
Expand Down Expand Up @@ -207,6 +208,6 @@ export const Input = forwardRef(function Input<T extends DomainFieldType>(
value={finalValue}
/>
);
}) as <T extends DomainFieldType>(
}) as <const T extends DomainFieldTypeSingle>(
props: InputProps<T> & {ref?: React.ForwardedRef<HTMLInputElement | HTMLTextAreaElement>}
) => ReactElement;
10 changes: 5 additions & 5 deletions packages/forms/src/components/select-autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import i18next from "i18next";
import {useObserver} from "mobx-react";
import {useCallback, useMemo} from "react";

import {DomainFieldType, DomainTypeSingle, ReferenceList} from "@focus4/stores";
import {DomainFieldTypeSingle, DomainType, ReferenceList} from "@focus4/stores";
import {Autocomplete, AutocompleteProps} from "@focus4/toolbox";

import {stringToDomainType} from "./utils";

/** Props du Select. */
export interface SelectAutocompleteProps<T extends DomainFieldType>
export interface SelectAutocompleteProps<T extends DomainFieldTypeSingle>
extends Omit<AutocompleteProps<any>, "error" | "getKey" | "getLabel" | "onChange" | "value" | "values"> {
/** Message d'erreur à afficher. */
error?: string;
Expand All @@ -17,13 +17,13 @@ export interface SelectAutocompleteProps<T extends DomainFieldType>
/** Préfixe i18n. Par défaut : "focus". */
i18nPrefix?: string;
/** Est appelé à chaque changement de valeur. */
onChange: (value: DomainTypeSingle<T> | undefined) => void;
onChange: (value: DomainType<T> | undefined) => void;
/** Type du champ (celui du domaine). */
type: T;
/** Libellé de l'option vide. */
undefinedLabel?: string;
/** Valeur. */
value?: DomainTypeSingle<T>;
value?: DomainType<T>;
/** Liste des valeurs. */
values: ReferenceList;
}
Expand All @@ -35,7 +35,7 @@ const undefinedKey = "$$undefined$$";
*
* S'utilise avec [`selectFor`](/docs/modèle-métier-afficher-des-champs--docs#selectforfield-values-options).
*/
export function SelectAutocomplete<T extends DomainFieldType>({
export function SelectAutocomplete<const T extends DomainFieldTypeSingle>({
error,
hasUndefined = true,
i18nPrefix = "focus",
Expand Down
Loading

0 comments on commit 048093e

Please sign in to comment.