-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
400 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { useDragItem } from "@/hooks/useDrag"; | ||
import { fr } from "@codegouvfr/react-dsfr"; | ||
import Button from "@codegouvfr/react-dsfr/Button"; | ||
import Input from "@codegouvfr/react-dsfr/Input"; | ||
import { tss } from "tss-react"; | ||
import { useFormContext } from "react-hook-form"; | ||
import { InputHTMLAttributes } from "react"; | ||
import { get } from "@/utils"; | ||
import Select, { SelectProps } from "@codegouvfr/react-dsfr/SelectNext"; | ||
|
||
export interface KeyValue { | ||
key?: string; | ||
value: string | null; | ||
} | ||
|
||
export type KeyValues = KeyValue[]; | ||
|
||
interface KeyValueListProps { | ||
index: number; | ||
name: string; | ||
onRemove: (index: number) => void; | ||
valueInputProps?: InputHTMLAttributes<HTMLInputElement>; | ||
valueOptions?: SelectProps.Option[]; | ||
valueType?: "textInput" | "select"; | ||
} | ||
|
||
function KeyValueItem(props: KeyValueListProps) { | ||
const { index, name, onRemove, valueInputProps, valueOptions = [], valueType = "textInput" } = props; | ||
const { dragIndex, ref, startDrag } = useDragItem(); | ||
const { classes, cx } = useStyles({ active: dragIndex === index }); | ||
const { | ||
formState: { errors }, | ||
register, | ||
watch, | ||
} = useFormContext(); | ||
const useKeys = watch(`${name}.useKeys`); | ||
const keyError = get(errors, `${name}.values.${index}.key.message`); | ||
const valueError = get(errors, `${name}.values.${index}.value.message`); | ||
|
||
function handleMove(index: number) { | ||
return (event) => startDrag(event, index); | ||
} | ||
|
||
function handleRemove(index: number) { | ||
return () => onRemove(index); | ||
} | ||
|
||
return ( | ||
<div className={cx(fr.cx("fr-pb-1w"), classes.row, classes.drag)} ref={ref}> | ||
<div> | ||
<Button | ||
title="Ordonner" | ||
priority={"tertiary no outline"} | ||
iconId={"ri-draggable"} | ||
nativeButtonProps={{ onMouseDown: handleMove(index) }} | ||
type="button" | ||
/> | ||
</div> | ||
{useKeys && ( | ||
<div className={classes.cell}> | ||
<Input | ||
label="Clé" | ||
nativeInputProps={register(`${name}.values.${index}.key`)} | ||
state={keyError ? "error" : "default"} | ||
stateRelatedMessage={String(keyError ?? "")} | ||
/> | ||
</div> | ||
)} | ||
<div className={classes.cell}> | ||
{valueType === "textInput" && ( | ||
<Input | ||
label="Valeur" | ||
nativeInputProps={{ | ||
...valueInputProps, | ||
...register(`${name}.values.${index}.value`), | ||
}} | ||
state={valueError ? "error" : "default"} | ||
stateRelatedMessage={String(valueError ?? "")} | ||
/> | ||
)} | ||
{valueType === "select" && ( | ||
<Select | ||
label="Valeur" | ||
placeholder="Select an option" | ||
nativeSelectProps={register(`${name}.values.${index}.value`)} | ||
options={valueOptions} | ||
state={valueError ? "error" : "default"} | ||
stateRelatedMessage={String(valueError ?? "")} | ||
/> | ||
)} | ||
</div> | ||
<div> | ||
<Button title="Supprimer" priority={"tertiary no outline"} iconId={"fr-icon-delete-line"} onClick={handleRemove(index)} /> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default KeyValueItem; | ||
|
||
const useStyles = tss.withParams<{ active: boolean }>().create(({ active }) => ({ | ||
drag: { | ||
position: "relative", | ||
zIndex: active ? 1 : 0, | ||
scale: active ? 1.01 : 1, | ||
}, | ||
row: { | ||
display: "flex", | ||
gap: "1.5rem", | ||
alignItems: "center", | ||
}, | ||
cell: { | ||
display: "flex", | ||
flexDirection: "column", | ||
flex: 1, | ||
height: "104px", | ||
}, | ||
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,124 +1,116 @@ | ||
import * as yup from "yup"; | ||
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; | ||
|
||
import { fr } from "@codegouvfr/react-dsfr"; | ||
import Button from "@codegouvfr/react-dsfr/Button"; | ||
import Input from "@codegouvfr/react-dsfr/Input"; | ||
import { FC, useEffect, useState } from "react"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
|
||
interface KeyValueListProps { | ||
id?: string; | ||
label: string; | ||
hintText: string; | ||
state?: "default" | "error" | "success"; | ||
stateRelatedMessage?: string; | ||
defaultValue?: Record<string, string>; | ||
onChange?: (value: Record<string, string>) => void; | ||
} | ||
import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; | ||
import { useDrag } from "@/hooks/useDrag"; | ||
import { DragProvider } from "@/contexts/drag"; | ||
|
||
type HeaderDatas = Record<string, { name: string; value: string }>; | ||
import KeyValueItem, { KeyValues } from "./KeyValueItem"; | ||
import { InputHTMLAttributes } from "react"; | ||
import { SelectProps } from "@codegouvfr/react-dsfr/SelectNext"; | ||
import { get } from "@/utils"; | ||
|
||
const KeyValueList: FC<KeyValueListProps> = (props: KeyValueListProps) => { | ||
const { label, hintText, state, stateRelatedMessage, defaultValue = {}, onChange } = props; | ||
export interface KeyValuesForm { | ||
values: KeyValues; | ||
useKeys: boolean; | ||
} | ||
|
||
const [datas, setDatas] = useState<HeaderDatas>(() => { | ||
const d: HeaderDatas = {}; | ||
Object.keys(defaultValue).forEach((keyname) => { | ||
const uuid = uuidv4(); | ||
d[uuid] = { name: keyname, value: defaultValue[keyname] }; | ||
}); | ||
return d; | ||
}); | ||
function checkDuplicates({ useKeys, values }: KeyValuesForm) { | ||
const attribute = useKeys ? "key" : "value"; | ||
const items = values.map((item) => item[attribute]); | ||
return new Set(items).size === items.length; | ||
} | ||
|
||
useEffect(() => { | ||
const result = {}; | ||
Object.keys(datas).forEach((uuid) => { | ||
if (datas[uuid].name && datas[uuid].value) { | ||
result[datas[uuid].name] = datas[uuid].value; | ||
} | ||
export function getKeyValueSchema(testConfig?: yup.TestConfig<string | null> | yup.TestConfig<string | null>[]) { | ||
let valueSchema = yup.string().nullable().defined().strict(true); | ||
if (testConfig) { | ||
const tests: yup.TestConfig<string | null>[] = testConfig instanceof Array ? testConfig : [testConfig]; | ||
tests.forEach((test) => { | ||
valueSchema = valueSchema.test(test); | ||
}); | ||
onChange?.(result); | ||
}, [datas, onChange]); | ||
|
||
// Ajout d'une ligne | ||
const handleAdd = () => { | ||
const uuid = uuidv4(); | ||
const d = { ...datas }; | ||
d[uuid] = { name: "", value: "" }; | ||
setDatas(d); | ||
}; | ||
} | ||
return yup | ||
.object({ | ||
useKeys: yup.boolean().required(), | ||
values: yup | ||
.array() | ||
.of( | ||
yup.object({ | ||
key: yup.string(), | ||
value: valueSchema, | ||
}) | ||
) | ||
.required(), | ||
}) | ||
.test("hasDuplicates", "remove or fix duplicates keys / values", checkDuplicates); | ||
} | ||
|
||
// Suppression d'une ligne | ||
const handleRemove = (key: string) => { | ||
const d = { ...datas }; | ||
delete d[key]; | ||
setDatas(d); | ||
}; | ||
interface KeyValueListProps { | ||
label: string; | ||
hintText?: string; | ||
name: string; | ||
valueInputProps?: InputHTMLAttributes<HTMLInputElement>; | ||
valueOptions?: SelectProps.Option[]; | ||
valueType?: "textInput" | "select"; | ||
} | ||
|
||
const handleChangeName = (key: string, name: string) => { | ||
const d = { ...datas }; | ||
d[key].name = name; | ||
setDatas(d); | ||
}; | ||
function KeyValueList(props: KeyValueListProps) { | ||
const { label, hintText, name, valueInputProps, valueOptions, valueType } = props; | ||
const { | ||
control, | ||
formState: { errors }, | ||
} = useFormContext(); | ||
const { fields, append, remove, move } = useFieldArray({ | ||
name: `${name}.values`, | ||
}); | ||
const keyValueError = get(errors, `${name}.root.message`); | ||
const hasError = Boolean(keyValueError); | ||
const drag = useDrag(move); | ||
|
||
const handleChangeValue = (key: string, value: string) => { | ||
const d = { ...datas }; | ||
d[key].value = value; | ||
setDatas(d); | ||
}; | ||
function handleAdd() { | ||
append({ key: "", value: "" }); | ||
} | ||
|
||
return ( | ||
<div> | ||
<div className={fr.cx("fr-input-group", "fr-mb-1v")}> | ||
<label className={fr.cx("fr-label")}>{label}</label> | ||
<span className={fr.cx("fr-hint-text")}>{hintText}</span> | ||
<div className={fr.cx("fr-input-group", { "fr-input-group--error": hasError })}> | ||
<label className={fr.cx("fr-label")}> | ||
{label} | ||
{hintText && <span className={fr.cx("fr-hint-text")}>{hintText}</span>} | ||
</label> | ||
<div className={fr.cx("fr-mt-1w", "fr-mb-1w")}> | ||
<Controller | ||
control={control} | ||
name={`${name}.useKeys`} | ||
render={({ field: { value, onChange } }) => ( | ||
<ToggleSwitch checked={value} label="Définir les clés" inputTitle="Définir les clés" onChange={onChange} showCheckedHint={false} /> | ||
)} | ||
/> | ||
</div> | ||
<DragProvider value={drag}> | ||
{fields.map((field, i) => { | ||
return ( | ||
<KeyValueItem | ||
key={field.id} | ||
index={i} | ||
name={name} | ||
onRemove={remove} | ||
valueInputProps={valueInputProps} | ||
valueOptions={valueOptions} | ||
valueType={valueType} | ||
/> | ||
); | ||
})} | ||
</DragProvider> | ||
<div> | ||
<Button className={fr.cx("fr-mr-1v")} iconId={"fr-icon-add-circle-line"} priority="tertiary" onClick={handleAdd}> | ||
<Button className={fr.cx("fr-mr-1v")} iconId={"fr-icon-add-circle-line"} priority="tertiary" onClick={handleAdd} type="button"> | ||
Ajouter | ||
</Button> | ||
</div> | ||
{Object.keys(datas).map((key) => { | ||
return ( | ||
<div key={key} className={fr.cx("fr-grid-row", "fr-grid-row--gutters", "fr-grid-row--middle")}> | ||
<div className={fr.cx("fr-col-3")}> | ||
<Input | ||
label={null} | ||
nativeInputProps={{ | ||
defaultValue: datas[key].name, | ||
onChange: (e) => handleChangeName(key, e.currentTarget.value), | ||
}} | ||
/> | ||
</div> | ||
<div className={fr.cx("fr-col-3")}> | ||
<Input | ||
label={null} | ||
nativeInputProps={{ | ||
defaultValue: datas[key].value, | ||
onChange: (e) => handleChangeValue(key, e.currentTarget.value), | ||
}} | ||
/> | ||
</div> | ||
<Button title={""} priority={"tertiary no outline"} iconId={"fr-icon-delete-line"} onClick={() => handleRemove(key)} /> | ||
</div> | ||
); | ||
})} | ||
{state !== "default" && ( | ||
<p | ||
className={fr.cx( | ||
(() => { | ||
switch (state) { | ||
case "error": | ||
return "fr-error-text"; | ||
case "success": | ||
return "fr-valid-text"; | ||
} | ||
})() | ||
)} | ||
> | ||
{stateRelatedMessage} | ||
</p> | ||
)} | ||
{hasError && <p className={fr.cx("fr-error-text")}>{String(keyValueError)}</p>} | ||
</div> | ||
); | ||
}; | ||
} | ||
|
||
export default KeyValueList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { createContext, PropsWithChildren, useContext } from "react"; | ||
|
||
export interface IDragContext { | ||
dragIndex: number; | ||
register: (ref: HTMLElement) => void; | ||
startDrag: (event: MouseEvent, index: number) => void; | ||
} | ||
|
||
const dragContext = createContext<IDragContext | null>(null); | ||
|
||
export function DragProvider({ children, value }: PropsWithChildren<{ value: IDragContext }>) { | ||
return <dragContext.Provider value={value}>{children}</dragContext.Provider>; | ||
} | ||
|
||
export function useDragContext() { | ||
const context = useContext(dragContext); | ||
if (!context) { | ||
throw new Error("useDragContext must be used within a DragProvider"); | ||
} | ||
return context; | ||
} |
Oops, something went wrong.