Skip to content

Commit

Permalink
feat: testing page
Browse files Browse the repository at this point in the history
  • Loading branch information
tonai committed Feb 12, 2025
1 parent f319bf5 commit 3499a2f
Show file tree
Hide file tree
Showing 9 changed files with 639 additions and 83 deletions.
99 changes: 99 additions & 0 deletions assets/components/Input/KeyValueItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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";

export interface KeyValue {
key?: string;
value: string | null;
}

export type KeyValues = KeyValue[];

interface KeyValueListProps {
index: number;
keyValue?: string;
onChange: (index: number, key: string, value: string) => void;
onRemove: (index: number) => void;
useKeys: boolean;
value: string | null;
}

function KeyValueItem(props: KeyValueListProps) {
const { index, keyValue, onChange, onRemove, useKeys, value } = props;
const { dragIndex, ref, startDrag } = useDragItem();
const { classes, cx } = useStyles({ active: dragIndex === index });

function handleMove(index: number) {
return (event) => startDrag(event, index);
}

function handleChange(index: number, field: string) {
return (event) => onChange(index, field, event.target.value);
}

function handleRemove(index: number) {
return () => onRemove(index);
}

return (
<div className={cx(fr.cx("fr-pb-2w"), classes.row, classes.drag)} ref={ref}>
<div className={classes.buttons}>
<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={{
value: keyValue,
onChange: handleChange(index, "key"),
}}
/>
</div>
)}
<div className={classes.cell}>
<Input
label="Valeur"
nativeInputProps={{
value: value ?? "",
onChange: handleChange(index, "value"),
}}
/>
</div>
<div className={classes.buttons}>
<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,
// ...(!active && { transition: "translate 200ms ease-out" }),
// transition: "scale 200ms ease-out",
// scale: active? 1.01 : 1,
},
row: {
display: "flex",
gap: "1.5rem",
alignItems: "center",
},
cell: {
flex: 1,
},
buttons: {
alignSelf: "flex-end",
},
}));
129 changes: 46 additions & 83 deletions assets/components/Input/KeyValueList.tsx
Original file line number Diff line number Diff line change
@@ -1,106 +1,69 @@
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";
import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch";
import { Dispatch, SetStateAction } from "react";
import { useDrag } from "@/hooks/useDrag";
import { DragProvider } from "@/contexts/drag";

import KeyValueItem, { KeyValues } from "./KeyValueItem";

interface KeyValueListProps {
id?: string;
label: string;
hintText: string;
hintText?: string;
// onChange: Dispatch<SetStateAction<KeyValues>>;
onChange: (value: KeyValues) => void;
setUseKeys: Dispatch<SetStateAction<boolean>>;
state?: "default" | "error" | "success";
stateRelatedMessage?: string;
defaultValue?: Record<string, string>;
onChange?: (value: Record<string, string>) => void;
useKeys: boolean;
value: KeyValues;
}

type HeaderDatas = Record<string, { name: string; value: string }>;

const KeyValueList: FC<KeyValueListProps> = (props: KeyValueListProps) => {
const { label, hintText, state, stateRelatedMessage, defaultValue = {}, onChange } = props;

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;
});

useEffect(() => {
const result = {};
Object.keys(datas).forEach((uuid) => {
if (datas[uuid].name && datas[uuid].value) {
result[datas[uuid].name] = datas[uuid].value;
}
});
onChange?.(result);
}, [datas, onChange]);
function KeyValueList(props: KeyValueListProps) {
const { label, hintText, onChange, setUseKeys, state, stateRelatedMessage, useKeys, value } = props;
const drag = useDrag((callback) => onChange(callback(value)));

// Ajout d'une ligne
const handleAdd = () => {
const uuid = uuidv4();
const d = { ...datas };
d[uuid] = { name: "", value: "" };
setDatas(d);
};
function handleAdd() {
onChange([...value, { key: "", value: "" }]);
}

// Suppression d'une ligne
const handleRemove = (key: string) => {
const d = { ...datas };
delete d[key];
setDatas(d);
};
function handleChange(index: number, key: string, val: string) {
onChange(
value.map((item, i) => {
if (index === i) {
return { ...item, [key]: val };
}
return item;
})
);
}

const handleChangeName = (key: string, name: string) => {
const d = { ...datas };
d[key].name = name;
setDatas(d);
};

const handleChangeValue = (key: string, value: string) => {
const d = { ...datas };
d[key].value = value;
setDatas(d);
};
function handleRemove(index: number) {
onChange(value.filter((_, i) => i !== index));
}

return (
<div>
<div className={fr.cx("fr-pb-2w")}>
<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>
<label className={fr.cx("fr-label")}>
{label}
{hintText && <span className={fr.cx("fr-hint-text")}>{hintText}</span>}
</label>
</div>
<div className={fr.cx("fr-mt-1w", "fr-mb-1w")}>
<ToggleSwitch label="Définir les clés" inputTitle="Définir les clés" checked={useKeys} onChange={setUseKeys} showCheckedHint={false} />
</div>
<DragProvider value={drag}>
{value.map(({ key, value }, i) => {
return <KeyValueItem key={i} index={i} keyValue={key} onChange={handleChange} onRemove={handleRemove} useKeys={useKeys} value={value} />;
})}
</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(
Expand All @@ -119,6 +82,6 @@ const KeyValueList: FC<KeyValueListProps> = (props: KeyValueListProps) => {
)}
</div>
);
};
}

export default KeyValueList;
115 changes: 115 additions & 0 deletions assets/components/Input/KeyValueList2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { fr } from "@codegouvfr/react-dsfr";
import Button from "@codegouvfr/react-dsfr/Button";
import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch";
import { Dispatch, SetStateAction } from "react";
import { useDrag } from "@/hooks/useDrag";
import { DragProvider } from "@/contexts/drag";

import KeyValueItem, { KeyValues } from "./KeyValueItem";

// export type KeyValues = (string | null)[] | Record<string, string | null>;

type KeyValueListProps = KeyValueListProps.Common & (KeyValueListProps.Array | KeyValueListProps.Object);

export namespace KeyValueListProps {
export interface Common {
id?: string;
label: string;
hintText?: string;
setUseKeys: Dispatch<SetStateAction<boolean>>;
state?: "default" | "error" | "success";
stateRelatedMessage?: string;
useKeys: boolean;
}
export interface Array<T = (string | null)[]> {
useKeys: false;
onChange: (value: T) => void;
value: T;
}
export interface Object<T = Record<string, string | null>> {
useKeys: true;
onChange: (value: T) => void;
value: T;
}
}

function KeyValueList2(props: KeyValueListProps) {
const { label, hintText, onChange, setUseKeys, state, stateRelatedMessage, useKeys, value } = props;
const arrayValue: KeyValues =
value instanceof Array ? value.map((item) => ({ value: item ?? "" })) : Object.entries(value).map(([key, value]) => ({ key, value: value ?? "" }));
const drag = useDrag((callback) => setData(callback(arrayValue)));

function setData(newArray: KeyValues) {
if (useKeys) {
onChange(Object.fromEntries(newArray.map(({ key, value }) => [key, value || null])));
} else {
onChange(newArray.map(({ value }) => value || null));
}
}

function handleAdd() {
if (useKeys) {
onChange({ ...value, "": "" });
} else {
onChange([...value, ""]);
}
}

function handleChange(index: number, key: string, val: string) {
setData(
arrayValue.map((item, i) => {
if (index === i) {
return { ...item, [key]: val };
}
return item;
})
);
}

function handleRemove(index: number) {
setData(arrayValue.filter((_, i) => i !== index));
}

return (
<div className={fr.cx("fr-pb-2w")}>
<div className={fr.cx("fr-input-group", "fr-mb-1v")}>
<label className={fr.cx("fr-label")}>
{label}
{hintText && <span className={fr.cx("fr-hint-text")}>{hintText}</span>}
</label>
</div>
<div className={fr.cx("fr-mt-1w", "fr-mb-1w")}>
<ToggleSwitch label="Définir les clés" inputTitle="Définir les clés" checked={useKeys} onChange={setUseKeys} showCheckedHint={false} />
</div>
<DragProvider value={drag}>
{arrayValue.map(({ key, value }, i) => {
return <KeyValueItem key={i} index={i} keyValue={key} onChange={handleChange} onRemove={handleRemove} useKeys={useKeys} value={value} />;
})}
</DragProvider>
<div>
<Button className={fr.cx("fr-mr-1v")} iconId={"fr-icon-add-circle-line"} priority="tertiary" onClick={handleAdd} type="button">
Ajouter
</Button>
</div>
{state !== "default" && (
<p
className={fr.cx(
(() => {
switch (state) {
case "error":
return "fr-error-text";
case "success":
return "fr-valid-text";
}
})()
)}
>
{stateRelatedMessage}
</p>
)}
</div>
);
}

export default KeyValueList2;
21 changes: 21 additions & 0 deletions assets/contexts/drag.tsx
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;
}
Loading

0 comments on commit 3499a2f

Please sign in to comment.