Skip to content

Commit

Permalink
feat: Gestion des documents, emailplanners ...
Browse files Browse the repository at this point in the history
  • Loading branch information
pprev94 committed Nov 19, 2024
1 parent 594310c commit 14fbd05
Show file tree
Hide file tree
Showing 44 changed files with 1,636 additions and 654 deletions.
20 changes: 12 additions & 8 deletions assets/@types/app_espaceco.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EmailPlannerDTO, GridDTO, ReportStatusesDTO, SharedGeorem, SharedThemesDTO, ThemeDTO, UserDTO } from "./espaceco";
import { BasicRecipients, EmailPlannerDTO, GridDTO, ReportStatusesDTO, SharedGeorem, SharedThemesDTO, ThemeDTO, UserDTO } from "./espaceco";

export type GetResponse<T> = {
content: T[];
Expand Down Expand Up @@ -81,18 +81,22 @@ export type DescriptionFormType = {
};

/* email planners */
export const BasicRecipientsArray: string[] = [...BasicRecipients] as string[];

export const EmailPlannerTypes = ["basic", "personal"] as const;
export type EmailPlannerType = (typeof EmailPlannerTypes)[number];

export type EmailPlannerFormType = Omit<EmailPlannerDTO, "id" | "recipients" | "themes" | "condition"> & {
export type EmailPlannerAddType = Omit<EmailPlannerDTO, "id">;

export type EmailPlannerFormType = Omit<EmailPlannerDTO, "id" | "recipients" | "event" | "cancel_event" | "themes" | "condition"> & {
id?: number;
recipients?: string[];
condition?: { status: string[] };
themes: string[];
event: string;
cancel_event: string;
recipients: string[];
statuses?: string[];
themes?: string[];
};

const isUser = (v: UserDTO | string): v is UserDTO => {
export const isUser = (v: UserDTO | string): v is UserDTO => {
return (v as UserDTO).username !== undefined;
};

export { isUser };
25 changes: 13 additions & 12 deletions assets/@types/espaceco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export type TriggerEventType = (typeof TriggerEvents)[number];
export const CancelEvents = ["georem_status_changed", "none"] as const;
export type CancelEventType = (typeof CancelEvents)[number];

export const Recipients = ["Auteur", "Gestionnaire", "Majec"] as const;
export type RecipientType = (typeof Recipients)[number];
export const BasicRecipients = ["Auteur", "Gestionnaire", "Majec"] as const;
export type RecipientType = (typeof BasicRecipients)[number];

export type EmailPlannerDTO = {
id: number;
Expand All @@ -75,8 +75,8 @@ export type EmailPlannerDTO = {
recipients: string[];
event: TriggerEventType;
cancel_event: CancelEventType;
condition: string | null;
themes: string | null;
condition: { status: string[] } | null;
themes: string[];
};

const SharedGeoremOptions = ["all", "restrained", "personal"];
Expand Down Expand Up @@ -121,18 +121,19 @@ export interface UserDTO {

export interface DocumentDTO {
id: number;
title: string;
description: string | null;
short_fileName: string;
mime_type: string;
description?: string;
title: string;
type: string;
size?: number;
width?: number;
height?: number;
size: number | null;
width: number | null;
height: number | null;
date: string;
geometry?: string;
uri: string;
geometry: string | null;
uri: string | null;
download_uri: string;
}

export interface GridDTO {
name: string;
title: string;
Expand Down
179 changes: 105 additions & 74 deletions assets/components/Input/InputCollection.tsx
Original file line number Diff line number Diff line change
@@ -1,108 +1,110 @@
import { fr } from "@codegouvfr/react-dsfr";
import Button from "@codegouvfr/react-dsfr/Button";
import Input from "@codegouvfr/react-dsfr/Input";
import { FC, useCallback, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { useTranslation } from "../../i18n/i18n";
import { FC } from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import isEmail from "validator/lib/isEmail";
import { yupResolver } from "@hookform/resolvers/yup";
import Button from "@codegouvfr/react-dsfr/Button";
import { declareComponentKeys, Translations, useTranslation } from "../../i18n/i18n";

export type ValidatorType = "none" | "email";

interface InputCollectionProps {
label?: string;
hintText?: string;
state?: "default" | "error" | "success";
stateRelatedMessage?: string;
minLength?: number;
validator?: ValidatorType; // On pourrait en mettre plusieurs
value?: string[];
onChange: (value: string[]) => void;
}

const InputCollection: FC<InputCollectionProps> = (props: InputCollectionProps) => {
const { t } = useTranslation("Common");
const { label, hintText, state, stateRelatedMessage, minLength = 0, validator = "none", value = [], onChange } = props;

const { label, hintText, state, stateRelatedMessage, value = [], onChange } = props;
const { t: tCommon } = useTranslation("Common");
const { t } = useTranslation("InputCollection");

/* Calcul de la valeur finale */
const compute = useCallback((values: Record<string, string>) => {
const result: string[] = Object.values(values).filter((v) => v.trim().length);
return [...new Set(result)];
}, []);
const _validator: (value) => boolean = validator === "email" ? isEmail : undefined;

const [internals, setInternals] = useState<Record<string, string>>(() => {
// On met une ligne par defaut
const def = [...value];
if (def.length === 0) {
def.push("");
}

const values: Record<string, string> = {};
def.forEach((value) => {
const uuid = uuidv4();
values[uuid] = value;
});
return values;
const schema = yup.object({
text: yup
.string()
.min(minLength, t("min_length_error", { min: minLength }))
.test({
name: "check",
test(value, ctx) {
if (!value) return true;
if (_validator) {
if (!_validator(value)) {
return ctx.createError({ message: `${value} n'est pas un email valide` });
}
}
return true;
},
}),
});

const num = Object.keys(internals).length;
const {
register,
formState: { errors },
getValues: getFormValues,
resetField,
handleSubmit,
} = useForm<{ text?: string }>({
mode: "onChange",
resolver: yupResolver(schema),
});

/* Ajout d'une ligne */
const handleAdd = () => {
const d = { ...internals };
d[uuidv4()] = "";
setInternals(d);
const onSubmit = () => {
const v = getFormValues("text");
const values = value ? [...value] : [];
if (v) {
values.push(v);
onChange(Array.from(new Set(values)));
resetField("text");
}
};

/* Suppression d'une ligne */
const handleRemove = (key: string) => {
const datas = { ...internals };
const value = datas[key];
delete datas[key];

setInternals(datas);
if (value) {
onChange(compute(datas));
}
const handleRemove = (text: string) => {
const values = value.filter((v) => text !== v);
onChange(values);
};

/* Modification d'une valeur */
const handleChangeValue = useCallback(
(key: string, value: string) => {
const datas = { ...internals };
datas[key] = value;
setInternals(datas);

onChange(compute(datas));
},
[internals, compute, onChange]
);

return (
<div className={fr.cx("fr-input-group", state === "error" && "fr-input-group--error")}>
{label && <label className={fr.cx("fr-label")}>{label}</label>}
{hintText && <span className={fr.cx("fr-hint-text", "fr-mt-2v")}>{hintText}</span>}
<div className={fr.cx("fr-grid-row", "fr-mt-2v")}>
<Button className={fr.cx("fr-mb-1v")} iconId={"fr-icon-add-circle-line"} priority="tertiary" onClick={handleAdd}>
{t("add")}
<div className={fr.cx("fr-grid-row", "fr-grid-row--top")}>
<div className={fr.cx("fr-col")}>
<Input
className={fr.cx("fr-mb-1v")}
label={null}
state={errors.text ? "error" : "default"}
stateRelatedMessage={errors?.text?.message?.toString()}
nativeInputProps={{
...register("text"),
}}
/>
</div>
<Button className={fr.cx("fr-mb-1v")} iconId={"fr-icon-add-circle-line"} priority="tertiary" onClick={handleSubmit(onSubmit)}>
{tCommon("add")}
</Button>
</div>
{Object.keys(internals).map((key) => (
<div key={key} className={fr.cx("fr-grid-row", "fr-grid-row--middle")}>
<div className={fr.cx("fr-col")}>
<Input
className={fr.cx("fr-mb-1v")}
label={null}
nativeInputProps={{
defaultValue: internals[key],
onChange: (e) => handleChangeValue(key, e.currentTarget.value),
}}
/>
</div>
<Button
title={t("delete")}
priority={"tertiary no outline"}
iconId={"fr-icon-delete-line"}
disabled={num === 1}
onClick={() => handleRemove(key)}
/>
{value && (
<div>
{value.map((v) => (
<div key={v} className={fr.cx("fr-grid-row", "fr-grid-row--middle")}>
<div className={fr.cx("fr-col")}>{v}</div>
<Button title={tCommon("delete")} priority={"tertiary no outline"} iconId={"fr-icon-delete-line"} onClick={() => handleRemove(v)} />
</div>
))}
</div>
))}
)}

{state !== "default" && (
<p
className={fr.cx(
Expand All @@ -125,3 +127,32 @@ const InputCollection: FC<InputCollectionProps> = (props: InputCollectionProps)
};

export default InputCollection;

// traductions
export const { i18n } = declareComponentKeys<
{ K: "min_length_error"; P: { min: number | null }; R: string } | { K: "validator_error"; P: { type: ValidatorType; value: string | null }; R: string }
>()("InputCollection");

export const InputCollectionFrTranslations: Translations<"fr">["InputCollection"] = {
min_length_error: ({ min }) => `La chaîne doit faire au mois ${min} caractères `,
validator_error: ({ type, value }) => {
switch (type) {
case "none":
return "";
case "email":
return `${value} n'est pas un email valide`;
}
},
};

export const InputCollectionEnTranslations: Translations<"en">["InputCollection"] = {
min_length_error: ({ min }) => `Minimum string length is ${min}`,
validator_error: ({ type, value }) => {
switch (type) {
case "none":
return "";
case "email":
return `${value} is not a valid email`;
}
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ const AddPermissionForm: FC<AddPermissionFormProps> = ({ datastoreId }) => {
<Controller
control={control}
name="beneficiaries"
render={({ field: { onChange } }) =>
render={({ field: { value, onChange } }) =>
type === "COMMUNITY" ? (
<CommunityListForm
communities={communities}
Expand All @@ -208,6 +208,7 @@ const AddPermissionForm: FC<AddPermissionFormProps> = ({ datastoreId }) => {
<InputCollection
state={errors.beneficiaries ? "error" : "default"}
stateRelatedMessage={errors.beneficiaries?.message?.toString()}
value={value}
onChange={onChange}
/>
)
Expand Down
17 changes: 10 additions & 7 deletions assets/espaceco/api/community.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,21 @@ const updateMemberGrids = (communityId: number, userId: number, grids: string[])

const updateLogo = (communityId: number, formData: FormData) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_update_logo", { communityId });
return jsonFetch<CommunityResponseDTO>(
return jsonFetch<{ logo_url: string }>(
url,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
method: "POST",
},
formData
formData,
true
);
};

const removeLogo = (communityId: number) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_remove_logo", { communityId });
return jsonFetch<null>(url, { method: "DELETE" });
};

const removeMember = (communityId: number, userId: number) => {
const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_remove_member", { communityId, userId });
return jsonFetch<{ user_id: number }>(url, {
Expand All @@ -122,6 +124,7 @@ const community = {
updateMemberGrids,
removeMember,
updateLogo,
removeLogo,
};

export default community;
Loading

0 comments on commit 14fbd05

Please sign in to comment.