From 14fbd05f4d90f428cc857a0617992e47bcc123bc Mon Sep 17 00:00:00 2001 From: pprevautel Date: Tue, 19 Nov 2024 09:22:36 +0100 Subject: [PATCH] feat: Gestion des documents, emailplanners ... --- assets/@types/app_espaceco.ts | 20 +- assets/@types/espaceco.ts | 25 +- assets/components/Input/InputCollection.tsx | 179 +++++++------ .../ManagePermissions/AddPermissionForm.tsx | 3 +- assets/espaceco/api/community.ts | 17 +- assets/espaceco/api/communityDocuments.ts | 56 ++++ assets/espaceco/api/emailplanner.ts | 16 ++ assets/espaceco/api/index.ts | 2 + .../pages/communities/ManageCommunityTr.tsx | 49 +++- .../management/AddDocumentDialog.tsx | 121 --------- .../communities/management/CommunityLogo.tsx | 92 ++++--- .../communities/management/Description.tsx | 123 +++------ .../pages/communities/management/Reports.tsx | 6 +- .../ZoomAndCentering/ExtentDialog.tsx | 26 +- .../description/AddDocumentDialog.tsx | 176 +++++++++++++ .../management/description/DocumentList.tsx | 244 ++++++++++++++++++ .../description/EditDocumentDialog.tsx | 136 ++++++++++ .../management/member/AddMembersDialog.tsx | 1 - .../reports/AddOrEditEmailPlannerDialog.tsx | 86 ------ .../management/reports/EditThemeDialog.tsx | 17 +- .../management/reports/EmailPlanners.tsx | 92 +++++-- .../management/reports/ThemeList.tsx | 39 ++- .../management/reports/ThemeUtils.tsx | 2 +- .../emailplanners/AddEmailPlannerDialog.tsx | 152 +++++------ .../emailplanners/AddOrEditEmailPlannerTr.tsx | 3 + .../reports/emailplanners/Defaults.tsx | 19 +- .../emailplanners/EditEmailPlannerDialog.tsx | 74 ++++++ .../emailplanners/PersonalEmailPlanner.tsx | 85 ++++-- .../emailplanners/RecipientsManager.tsx | 40 ++- .../reports/emailplanners/schemas.tsx | 92 +++++++ .../communities/management/validationTr.tsx | 2 +- assets/i18n/Common.tsx | 3 + assets/i18n/i18n.ts | 2 + assets/i18n/languages/en.tsx | 4 + assets/i18n/languages/fr.tsx | 4 + assets/modules/espaceco/RQKeys.ts | 3 +- assets/sass/components/autocomplete.scss | 2 +- assets/sass/pages/espaceco/community.scss | 37 ++- .../EspaceCo/CommunityController.php | 45 +++- .../EspaceCo/CommunityDocumentController.php | 98 +++++++ .../EspaceCo/EmailPlannerController.php | 14 + .../EspaceCoApi/CommunityApiService.php | 16 +- .../CommunityDocumentApiService.php | 59 +++++ .../EspaceCoApi/EmailPlannerApiService.php | 8 + 44 files changed, 1636 insertions(+), 654 deletions(-) create mode 100644 assets/espaceco/api/communityDocuments.ts delete mode 100644 assets/espaceco/pages/communities/management/AddDocumentDialog.tsx create mode 100644 assets/espaceco/pages/communities/management/description/AddDocumentDialog.tsx create mode 100644 assets/espaceco/pages/communities/management/description/DocumentList.tsx create mode 100644 assets/espaceco/pages/communities/management/description/EditDocumentDialog.tsx delete mode 100644 assets/espaceco/pages/communities/management/reports/AddOrEditEmailPlannerDialog.tsx create mode 100644 assets/espaceco/pages/communities/management/reports/emailplanners/EditEmailPlannerDialog.tsx create mode 100644 assets/espaceco/pages/communities/management/reports/emailplanners/schemas.tsx create mode 100644 src/Controller/EspaceCo/CommunityDocumentController.php create mode 100644 src/Services/EspaceCoApi/CommunityDocumentApiService.php diff --git a/assets/@types/app_espaceco.ts b/assets/@types/app_espaceco.ts index ee5e511d..944a6fdc 100644 --- a/assets/@types/app_espaceco.ts +++ b/assets/@types/app_espaceco.ts @@ -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 = { content: T[]; @@ -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 & { +export type EmailPlannerAddType = Omit; + +export type EmailPlannerFormType = Omit & { 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 }; diff --git a/assets/@types/espaceco.ts b/assets/@types/espaceco.ts index 1d5dff7a..4ac9ebf3 100644 --- a/assets/@types/espaceco.ts +++ b/assets/@types/espaceco.ts @@ -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; @@ -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"]; @@ -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; diff --git a/assets/components/Input/InputCollection.tsx b/assets/components/Input/InputCollection.tsx index bd3fba73..5062daa6 100644 --- a/assets/components/Input/InputCollection.tsx +++ b/assets/components/Input/InputCollection.tsx @@ -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 = (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) => { - 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>(() => { - // On met une ligne par defaut - const def = [...value]; - if (def.length === 0) { - def.push(""); - } - - const values: Record = {}; - 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 (
{label && } {hintText && {hintText}} -
-
- {Object.keys(internals).map((key) => ( -
-
- handleChangeValue(key, e.currentTarget.value), - }} - /> -
-
+ ))}
- ))} + )} + {state !== "default" && (

= (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`; + } + }, +}; diff --git a/assets/entrepot/pages/datastore/ManagePermissions/AddPermissionForm.tsx b/assets/entrepot/pages/datastore/ManagePermissions/AddPermissionForm.tsx index 5309015c..f211feec 100644 --- a/assets/entrepot/pages/datastore/ManagePermissions/AddPermissionForm.tsx +++ b/assets/entrepot/pages/datastore/ManagePermissions/AddPermissionForm.tsx @@ -196,7 +196,7 @@ const AddPermissionForm: FC = ({ datastoreId }) => { + render={({ field: { value, onChange } }) => type === "COMMUNITY" ? ( = ({ datastoreId }) => { ) diff --git a/assets/espaceco/api/community.ts b/assets/espaceco/api/community.ts index d8604994..57eccd86 100644 --- a/assets/espaceco/api/community.ts +++ b/assets/espaceco/api/community.ts @@ -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( + 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(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, { @@ -122,6 +124,7 @@ const community = { updateMemberGrids, removeMember, updateLogo, + removeLogo, }; export default community; diff --git a/assets/espaceco/api/communityDocuments.ts b/assets/espaceco/api/communityDocuments.ts new file mode 100644 index 00000000..cdeb600e --- /dev/null +++ b/assets/espaceco/api/communityDocuments.ts @@ -0,0 +1,56 @@ +import { DocumentDTO } from "../../@types/espaceco"; +import { jsonFetch } from "../../modules/jsonFetch"; +import SymfonyRouting from "../../modules/Routing"; + +const getAll = (communityId: number, fields: string[], signal: AbortSignal) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_document_get_all", { + communityId, + fields: fields, + }); + return jsonFetch(url, { + signal: signal, + }); +}; + +const add = (communityId: number, data: FormData) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_document_add", { communityId }); + return jsonFetch( + url, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }, + data, + true + ); +}; + +const update = (communityId: number, documentId: number, data: object) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_document_update", { communityId, documentId }); + return jsonFetch( + url, + { + method: "PATCH", + }, + data + ); +}; + +const remove = (communityId: number, documentId: number) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_document_delete", { communityId, documentId }); + return jsonFetch(url, { + method: "DELETE", + }); +}; + +const communityDocuments = { + getAll, + add, + update, + remove, +}; + +export default communityDocuments; diff --git a/assets/espaceco/api/emailplanner.ts b/assets/espaceco/api/emailplanner.ts index af9191b5..e7fdfd39 100644 --- a/assets/espaceco/api/emailplanner.ts +++ b/assets/espaceco/api/emailplanner.ts @@ -7,6 +7,21 @@ const getAll = (communityId: number) => { return jsonFetch(url); }; +const add = (communityId: number, data: object) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_emailplanner_add", { communityId }); + return jsonFetch( + url, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }, + data + ); +}; + const remove = (communityId: number, emailplannerId: number) => { const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_emailplanner_remove", { communityId, emailplannerId }); return jsonFetch<{ emailplanner_id: number }>(url, { @@ -16,6 +31,7 @@ const remove = (communityId: number, emailplannerId: number) => { const emailplanner = { getAll, + add, remove, }; diff --git a/assets/espaceco/api/index.ts b/assets/espaceco/api/index.ts index b888a480..0e623a2b 100644 --- a/assets/espaceco/api/index.ts +++ b/assets/espaceco/api/index.ts @@ -1,4 +1,5 @@ import community from "./community"; +import communityDocuments from "./communityDocuments"; import emailplanner from "./emailplanner"; import grid from "./grid"; import permission from "./permission"; @@ -7,6 +8,7 @@ import user from "./users"; const api = { user, community, + communityDocuments, emailplanner, permission, grid, diff --git a/assets/espaceco/pages/communities/ManageCommunityTr.tsx b/assets/espaceco/pages/communities/ManageCommunityTr.tsx index 8a5b3aef..04d48567 100644 --- a/assets/espaceco/pages/communities/ManageCommunityTr.tsx +++ b/assets/espaceco/pages/communities/ManageCommunityTr.tsx @@ -25,6 +25,7 @@ export const { i18n } = declareComponentKeys< | "desc.logo" | "desc.logo.title" | { K: "logo_action"; P: { action: logoAction }; R: string } + | { K: "running_action"; P: { action: logoAction }; R: string } | "logo_confirm_delete_modal.title" | "modal.logo.title" | "modal.logo.file_hint" @@ -32,9 +33,13 @@ export const { i18n } = declareComponentKeys< | "desc.documents" | "desc.documents_hint" | "desc.no_documents" - | "desc.document.remove" + | "desc.adding_document" + | "desc.updating_document" + | "desc.confirm_remove_document" + | "desc.removing_document" | "modal.document.title" - | "modal.document.name" + | "modal.document.title_field" + | "modal.document.description" | "modal.document.file_hint" | "zoom.consistant_error" | "zoom.tab.title" @@ -75,6 +80,19 @@ export const { i18n } = declareComponentKeys< | { K: "grid.explain"; R: JSX.Element } >()("ManageCommunity"); +/** + * "thumbnail_modal.action_being": ({ action }) => { + switch (action) { + case "add": + return "Ajout de la vignette en cours ..."; + case "modify": + return "Remplacement de la vignette en cours ..."; + case "delete": + return "Suppression de la vignette en cours ..."; + } + }, + */ + export const ManageCommunityFrTranslations: Translations<"fr">["ManageCommunity"] = { title: ({ name }) => (name === undefined ? "Gérer le guichet" : `Gérer le guichet - ${name}`), loading: "Recherche du guichet en cours ...", @@ -105,6 +123,16 @@ export const ManageCommunityFrTranslations: Translations<"fr">["ManageCommunity" return "Supprimer le logo"; } }, + running_action: ({ action }) => { + switch (action) { + case "add": + return "Ajout du logo en cours ..."; + case "modify": + return "Mise à jour du logo en cours ..."; + case "delete": + return "Suppression du logo en cours ..."; + } + }, "logo_confirm_delete_modal.title": "Êtes-vous sûr de vouloir supprimer le logo de ce guichet ?", "modal.logo.title": "Logo du guichet", "modal.logo.file_hint": "Taille maximale : 5 Mo. Formats acceptés : jpg, png", @@ -113,9 +141,13 @@ export const ManageCommunityFrTranslations: Translations<"fr">["ManageCommunity" "desc.documents_hint": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Dicta suscipit tempora culpa, ea quis illo veniam vero consequuntur soluta nesciunt.", "desc.no_documents": "Aucun document", - "desc.document.remove": "Supprimer le document", + "desc.adding_document": "Ajout d'un document en cours ...", + "desc.updating_document": "Modification d'un document en cours ...", + "desc.confirm_remove_document": "Êtes-vous sûr de vouloir supprimer ce document ?", + "desc.removing_document": "Suppression du document en cours ...", "modal.document.title": "Ajouter un document", - "modal.document.name": "Titre", + "modal.document.title_field": "Titre", + "modal.document.description": "Description (optionnel)", "modal.document.file_hint": "Taille maximale : 5 Mo.", "zoom.consistant_error": "Emprise et position ne sont pas cohérents", "zoom.tab.title": "Définir l’état initial de la carte à l’ouverture du guichet", @@ -211,6 +243,7 @@ export const ManageCommunityEnTranslations: Translations<"en">["ManageCommunity" return "Delete logo"; } }, + running_action: ({ action }) => `${action} running`, "logo_confirm_delete_modal.title": undefined, "modal.logo.title": undefined, "modal.logo.file_hint": undefined, @@ -218,9 +251,13 @@ export const ManageCommunityEnTranslations: Translations<"en">["ManageCommunity" "desc.documents": undefined, "desc.documents_hint": undefined, "desc.no_documents": "No document", - "desc.document.remove": "Remove document", + "desc.adding_document": undefined, + "desc.updating_document": undefined, + "desc.confirm_remove_document": undefined, + "desc.removing_document": undefined, "modal.document.title": "Add document", - "modal.document.name": "Title", + "modal.document.title_field": "Title", + "modal.document.description": "Description (optional)", "modal.document.file_hint": "Maximum file size : 5 Mo.", "zoom.consistant_error": undefined, "zoom.tab.title": undefined, diff --git a/assets/espaceco/pages/communities/management/AddDocumentDialog.tsx b/assets/espaceco/pages/communities/management/AddDocumentDialog.tsx deleted file mode 100644 index 2533f988..00000000 --- a/assets/espaceco/pages/communities/management/AddDocumentDialog.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { fr } from "@codegouvfr/react-dsfr"; -import Input from "@codegouvfr/react-dsfr/Input"; -import { createModal } from "@codegouvfr/react-dsfr/Modal"; -import { Upload } from "@codegouvfr/react-dsfr/Upload"; -import { yupResolver } from "@hookform/resolvers/yup"; -import { TranslationFunction } from "i18nifty/typeUtils/TranslationFunction"; -import { FC } from "react"; -import { createPortal } from "react-dom"; -import { useForm } from "react-hook-form"; -import * as yup from "yup"; -import { ComponentKey, useTranslation } from "../../../../i18n/i18n"; - -type AddDocumentDialogProps = { - onCancel: () => void; - onAdd: (name: string, file: File) => void; -}; - -const AddDocumentDialogModal = createModal({ - id: "add-document-modal", - isOpenedByDefault: false, -}); - -const AddDocumentDialog: FC = ({ onCancel, onAdd }) => { - const { t: tCommon } = useTranslation("Common"); - const { t: tValid } = useTranslation("ManageCommunityValidations"); - const { t } = useTranslation("ManageCommunity"); - - const schema = (t: TranslationFunction<"ManageCommunityValidations", ComponentKey>) => - yup.object().shape({ - name: yup.string().min(7, t("description.modal.document.name.minlength")).required(t("description.modal.document.name.mandatory")), - document: yup - .mixed() - .test({ - name: "exists", - test(files, ctx) { - const file = files?.[0]; - return file === undefined ? ctx.createError({ message: t("description.modal.document.file.mandatory") }) : true; - }, - }) - .test("check-file-size", t("description.modal.document.file.size_error"), (files) => { - const file = files?.[0]; - if (file === undefined) return true; - - const size = file.size / 1024 / 1024; - return size < 5; - }), - }); - - const form = useForm({ - mode: "onChange", - resolver: yupResolver(schema(tValid)), - }); - const { - register, - getValues: getFormValues, - formState: { errors }, - handleSubmit, - resetField, - } = form; - - const clear = () => { - resetField("name"); - resetField("document"); - }; - - const onSubmit = () => { - const values = getFormValues(); - onAdd(values.name, values.document?.[0] as File); - clear(); - }; - - return ( - <> - {createPortal( - { - clear(); - onCancel(); - }, - priority: "secondary", - }, - { - children: tCommon("add"), - doClosesModal: false, - onClick: handleSubmit(onSubmit), - priority: "primary", - }, - ]} - > -

- - -
- , - document.body - )} - - ); -}; - -export { AddDocumentDialog, AddDocumentDialogModal }; diff --git a/assets/espaceco/pages/communities/management/CommunityLogo.tsx b/assets/espaceco/pages/communities/management/CommunityLogo.tsx index 8e65a053..978a0637 100644 --- a/assets/espaceco/pages/communities/management/CommunityLogo.tsx +++ b/assets/espaceco/pages/communities/management/CommunityLogo.tsx @@ -15,11 +15,13 @@ import { ComponentKey, useTranslation } from "../../../../i18n/i18n"; import { getFileExtension, getImageSize, ImageSize } from "../../../../utils"; import { logoAction } from "../ManageCommunityTr"; -import placeholder1x1 from "../../../../img/placeholder.1x1.png"; -import "../../../../sass/components/buttons.scss"; +import Alert from "@codegouvfr/react-dsfr/Alert"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { CartesApiException } from "../../../../modules/jsonFetch"; import { CommunityResponseDTO } from "../../../../@types/espaceco"; +import placeholder1x1 from "../../../../img/placeholder.1x1.png"; +import RQKeys from "../../../../modules/espaceco/RQKeys"; +import { CartesApiException } from "../../../../modules/jsonFetch"; +import "../../../../sass/components/buttons.scss"; import api from "../../../api"; type CommunityLogoProps = { @@ -73,8 +75,10 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => { const { t: tValidation } = useTranslation("ManageCommunityValidations"); const { t } = useTranslation("ManageCommunity"); - const [isValid, setIsValid] = useState(false); + const [isValid, setIsValid] = useState(false); useEffect(() => { + setIsValid(false); + if (logoUrl) { fetch(logoUrl).then((res) => { setIsValid(() => res.status === 200); @@ -82,6 +86,9 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => { } }, [logoUrl]); + console.log("LOGO_URL : ", logoUrl); + console.log("ISVALID : ", isValid); + const action: logoAction = useMemo(() => (isValid ? "modify" : "add"), [isValid]); // Boite modale, gestion de l'image @@ -89,10 +96,10 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => { const logoDivRef = useRef(null); const logoIsHovered = useHover(logoDivRef); - // const queryClient = useQueryClient(); + const queryClient = useQueryClient(); // Ajout/modification du logo - const updateLogoMutation = useMutation({ + const updateLogoMutation = useMutation<{ logo_url: string }, CartesApiException>({ mutationFn: () => { const form = new FormData(); form.append("logo", upload); @@ -101,23 +108,27 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => { onSuccess: (response) => { AddLogoModal.close(); - // Mise à jour du contenu de la réponse de datasheetQuery - /*queryClient.setQueryData(RQKeys.datastore_datasheet(datastoreId, datasheetName), (datasheet) => { - if (datasheet) { - datasheet.thumbnail = response; - } - return datasheet; - }); + queryClient.setQueryData(RQKeys.community(communityId), (community) => + community ? { ...community, logo_url: response.logo_url } : community + ); + }, + onSettled: () => { + reset(); + }, + }); - // Mise à jour du contenu de la réponse de datasheetListQuery - queryClient.setQueryData(RQKeys.datastore_datasheet_list(datastoreId), (datasheetList = []) => { - return datasheetList.map((datasheet) => { - if (datasheet.name === datasheetName) { - datasheet.thumbnail = response; - } - return datasheet; - }); - });*/ + // Suppression de la vignette + const removeLogoMutation = useMutation({ + mutationFn: () => { + if (communityId) { + return api.community.removeLogo(communityId); + } + return Promise.resolve(null); + }, + onSuccess: () => { + queryClient.setQueryData(RQKeys.community(communityId), (community) => + community ? { ...community, logo_url: null } : community + ); }, onSettled: () => { reset(); @@ -163,7 +174,7 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => { children: tCommon("cancel"), onClick: () => { reset(); - // addThumbnailMutation.reset(); + updateLogoMutation.reset(); }, doClosesModal: true, priority: "secondary", @@ -177,7 +188,7 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => { ]; return btns; - }, [action, /*addThumbnailMutation,*/ handleSubmit, onSubmit, reset, t, tCommon]); + }, [action, updateLogoMutation, handleSubmit, onSubmit, reset, t, tCommon]); return (
@@ -187,10 +198,10 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => { className={logoIsHovered ? "frx-btn--transparent fr-img--transparent-transition" : ""} loading="lazy" src={isValid ? logoUrl : placeholder1x1} - /* onError={({ currentTarget }) => { + onError={({ currentTarget }) => { currentTarget.onerror = null; // prevents looping currentTarget.src = placeholder1x1; - }} */ + }} /> {logoIsHovered && (
@@ -211,17 +222,20 @@ const CommunityLogo: FC = ({ communityId, logoUrl }) => {
)}
+ {updateLogoMutation.isError && ( + + )} + {removeLogoMutation.isError && ( + + )} + {removeLogoMutation.isPending && ( +
+ +
{t("running_action", { action: "delete" })}
+
+ )} {createPortal( - {/* {addThumbnailMutation.isError && ( - - )} */}
= ({ communityId, logoUrl }) => {
- {/* {addThumbnailMutation.isPending && ( + {updateLogoMutation.isPending && (
-
{t("thumbnail_modal.action_being", { action: action })}
+
{t("running_action", { action: action })}
- )} */} + )}
, document.body )} { - // deleteThumbnailMutation.mutate(); + removeLogoMutation.mutate(); }} /> diff --git a/assets/espaceco/pages/communities/management/Description.tsx b/assets/espaceco/pages/communities/management/Description.tsx index d275f6d8..319aba60 100644 --- a/assets/espaceco/pages/communities/management/Description.tsx +++ b/assets/espaceco/pages/communities/management/Description.tsx @@ -1,8 +1,7 @@ -import { fr } from "@codegouvfr/react-dsfr"; -import Button from "@codegouvfr/react-dsfr/Button"; +import Alert from "@codegouvfr/react-dsfr/Alert"; import Input from "@codegouvfr/react-dsfr/Input"; -import { cx } from "@codegouvfr/react-dsfr/tools/cx"; import { yupResolver } from "@hookform/resolvers/yup"; +import { useQuery } from "@tanstack/react-query"; import { TranslationFunction } from "i18nifty/typeUtils/TranslationFunction"; import { FC, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; @@ -10,23 +9,15 @@ import * as yup from "yup"; import { CommunityResponseDTO, DocumentDTO } from "../../../../@types/espaceco"; import AutocompleteSelect from "../../../../components/Input/AutocompleteSelect"; import MarkdownEditor from "../../../../components/Input/MarkdownEditor"; -import thumbnails from "../../../../data/doc_thumbnail.json"; +import LoadingText from "../../../../components/Utils/LoadingText"; import categories from "../../../../data/topic_categories.json"; -import { ComponentKey, useTranslation } from "../../../../i18n/i18n"; -import { appRoot } from "../../../../router/router"; -import { getFileExtension } from "../../../../utils"; -import { AddDocumentDialog, AddDocumentDialogModal } from "./AddDocumentDialog"; -import CommunityLogo from "./CommunityLogo"; +import { ComponentKey, declareComponentKeys, Translations, useTranslation } from "../../../../i18n/i18n"; +import RQKeys from "../../../../modules/espaceco/RQKeys"; import { type CartesApiException } from "../../../../modules/jsonFetch"; import "../../../../sass/pages/espaceco/community.scss"; -import { useQuery } from "@tanstack/react-query"; -import RQKeys from "../../../../modules/espaceco/RQKeys"; import api from "../../../api"; - -type DocumentExt = DocumentDTO & { - src: string; - isImage: boolean; -}; +import CommunityLogo from "./CommunityLogo"; +import DocumentList from "./description/DocumentList"; type DescriptionProps = { community: CommunityResponseDTO; @@ -48,7 +39,8 @@ const Description: FC = ({ community }) => { const { t: tCommon } = useTranslation("Common"); const { t: tValid } = useTranslation("ManageCommunityValidations"); - const { t } = useTranslation("ManageCommunity"); + const { t: tmc } = useTranslation("ManageCommunity"); + const { t } = useTranslation("Description"); const communityNamesQuery = useQuery({ queryKey: RQKeys.communitiesName(), @@ -56,28 +48,17 @@ const Description: FC = ({ community }) => { staleTime: 3600000, }); + // TODO DECOMMENTER + const communityDocumentsQuery = useQuery({ + queryKey: RQKeys.communityDocuments(community.id), + queryFn: ({ signal }) => api.communityDocuments.getAll(community.id, [], signal), + staleTime: 3600000, + }); + const communityNames = useMemo(() => { return communityNamesQuery.data?.filter((n) => n !== community.name) ?? []; }, [community.name, communityNamesQuery]); - const formattedDocuments = useMemo(() => { - const documents = community.documents ?? []; - return Array.from( - documents.map((d) => { - const result: DocumentExt = { ...d } as DocumentExt; - if (/^image/.test(result.mime_type)) { - result.src = d.uri; - } else { - const extension = getFileExtension(result.short_fileName)?.toLowerCase() ?? "defaut"; - const uri = extension in thumbnails ? thumbnails[extension].src : thumbnails["defaut"].src; - result.src = `${appRoot}/${uri}`; - } - - return result; - }) - ); - }, [community.documents]); - const schema = (t: TranslationFunction<"ManageCommunityValidations", ComponentKey>) => { return yup.object({ name: yup @@ -114,12 +95,14 @@ const Description: FC = ({ community }) => { return ( <> -

{t("desc.tab.title")}

+ {communityDocumentsQuery.isError && } + {communityDocumentsQuery.isLoading && } +

{tmc("desc.tab.title")}

{tCommon("mandatory_fields")}

= ({ community }) => { name="description" render={({ field }) => ( = ({ community }) => { name="keywords" render={({ field }) => ( = ({ community }) => { /> )} /> - -
- {formattedDocuments.length ? ( - formattedDocuments.map((d) => ( -
-
{d.title}
-
- -
-
- )) - ) : ( -

{t("desc.no_documents")}

- )} -
- - AddDocumentDialogModal.close()} - onAdd={(title, file) => { - console.log(title, file.name); // TODO SUPPRIMER - // TODO Mutation : Envoyer le fichier en POST (la route n'existe pas encore) - AddDocumentDialogModal.close(); - }} - /> + {communityDocumentsQuery.data && }
); }; export default Description; + +// traductions +export const { i18n } = declareComponentKeys<"loading_documents">()("Description"); + +export const DescriptionFrTranslations: Translations<"fr">["Description"] = { + loading_documents: "Recherche des tables pour la configuration des thèmes ...", +}; + +export const DescriptionEnTranslations: Translations<"en">["Description"] = { + loading_documents: undefined, +}; diff --git a/assets/espaceco/pages/communities/management/Reports.tsx b/assets/espaceco/pages/communities/management/Reports.tsx index 42e04d4b..b4c72016 100644 --- a/assets/espaceco/pages/communities/management/Reports.tsx +++ b/assets/espaceco/pages/communities/management/Reports.tsx @@ -122,8 +122,10 @@ const Reports: FC = ({ community }) => { recipients: yup.array().of(yup.string().required()).required(), event: yup.string().oneOf(["georem_created", "georem_status_changed"]).required(), cancel_event: yup.string().oneOf(["none", "georem_status_changed"]).required(), - condition: yup.string().transform(setToNull).required(), - themes: yup.string().transform(setToNull).required(), + condition: yup.object({ + status: yup.array().of(yup.string().required()).required(), + }), + themes: yup.array().of(yup.string().required()).required(), }) ), shared_themes: yup.array().of( diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx index c5ae68c2..e4703a69 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx @@ -4,7 +4,7 @@ import { createModal } from "@codegouvfr/react-dsfr/Modal"; import RadioButtons from "@codegouvfr/react-dsfr/RadioButtons"; import { yupResolver } from "@hookform/resolvers/yup"; import { Extent } from "ol/extent"; -import { FC, useState } from "react"; +import { FC, useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { useForm } from "react-hook-form"; import * as yup from "yup"; @@ -123,22 +123,28 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { resetField, } = form; - const clear = () => { - ["xmin", "ymin", "xmax", "ymax"].forEach((f) => resetField(f as FieldName, undefined)); - }; - const onChoiceChanged = (v) => { - clear(); setChoice(v); }; + const clear = useCallback(() => { + ["xmin", "ymin", "xmax", "ymax"].forEach((f) => resetField(f as FieldName, undefined)); + }, [resetField]); + + useEffect(() => { + choice === "autocomplete" ? clear() : resetField("extent", undefined); + }, [choice, clear, resetField]); + const onSubmit = () => { ExtentDialogModal.close(); const values = getFormValues(); - onApply([values.xmin, values.ymin, values.xmax, values.ymax]); - setChoice("manual"); - clear(); + const extent = choice === "autocomplete" ? values.extent : [values.xmin, values.ymin, values.xmax, values.ymax]; + onApply(extent); + + if (choice !== "manual") { + setChoice("manual"); + } else clear(); }; return ( @@ -192,7 +198,7 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { state={errors.extent ? "error" : "default"} stateRelatedMessage={errors.extent?.message?.toString()} onChange={(grid) => { - setFormValue("extent", grid ? grid : undefined); + setFormValue("extent", grid ? grid.extent : undefined); clearErrors(); }} /> diff --git a/assets/espaceco/pages/communities/management/description/AddDocumentDialog.tsx b/assets/espaceco/pages/communities/management/description/AddDocumentDialog.tsx new file mode 100644 index 00000000..6f127d00 --- /dev/null +++ b/assets/espaceco/pages/communities/management/description/AddDocumentDialog.tsx @@ -0,0 +1,176 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Input from "@codegouvfr/react-dsfr/Input"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { cx } from "@codegouvfr/react-dsfr/tools/cx"; +import { Upload } from "@codegouvfr/react-dsfr/Upload"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { TranslationFunction } from "i18nifty/typeUtils/TranslationFunction"; +import { FC, useCallback, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { ComponentKey, useTranslation } from "../../../../../i18n/i18n"; +import placeholder1x1 from "../../../../../img/placeholder.1x1.png"; + +import "../../../../../sass/pages/espaceco/community.scss"; + +const AddDocumentDialogModal = createModal({ + id: "add-document-modal", + isOpenedByDefault: false, +}); + +type AddDocumentDialogProps = { + onAdd: (data: FormData) => void; +}; + +const AddDocumentDialog: FC = ({ onAdd }) => { + const { t: tCommon } = useTranslation("Common"); + const { t: tValid } = useTranslation("ManageCommunityValidations"); + const { t } = useTranslation("ManageCommunity"); + + const getSchema = useCallback( + (t: TranslationFunction<"ManageCommunityValidations", ComponentKey>) => + yup.object().shape({ + title: yup.string().min(10, t("description.modal.document.name.minlength")).required(t("description.modal.document.name.mandatory")), + description: yup.string(), + document: yup + .mixed() + .test({ + name: "exists", + test(files, ctx) { + const file = files?.[0]; + return file === undefined ? ctx.createError({ message: t("description.modal.document.file.mandatory") }) : true; + }, + }) + .test("check-file-size", t("description.modal.document.file.size_error"), (files) => { + const file = files?.[0]; + if (file === undefined) return true; + + const size = file.size / 1024 / 1024; + return size < 5; + }), + }), + [] + ); + + const schema = getSchema(tValid); + type FormType = yup.InferType; + + const form = useForm({ + mode: "onSubmit", + resolver: yupResolver(schema), + }); + + const { + register, + watch, + getValues: getFormValues, + formState: { errors }, + handleSubmit, + resetField, + } = form; + + const clear = () => { + resetField("title"); + resetField("description"); + resetField("document"); + }; + + const onSubmit = () => { + const values = getFormValues(); + + const data = new FormData(); + data.append("title", values.title); + data.append("description", values.description ?? ""); + data.append("document", values.document?.[0] as File); + onAdd(data); + clear(); + }; + + const documentFile: File = watch("document")?.[0]; + const [modalImageUrl, setModalImageUrl] = useState(""); + + useEffect(() => { + setModalImageUrl(""); + + if (!documentFile) { + return; + } + if (/image/.test(documentFile.type)) { + const reader = new FileReader(); + reader.onload = () => { + setModalImageUrl(reader.result as string); + }; + reader.readAsDataURL(documentFile); + } + }, [documentFile]); + + return ( + <> + {createPortal( + { + clear(); + AddDocumentDialogModal.close(); + }, + priority: "secondary", + }, + { + children: tCommon("add"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + priority: "primary", + }, + ]} + > +
+ + +
+
+ +
+
+
+ +
+
+
+
+
, + document.body + )} + + ); +}; + +export { AddDocumentDialog, AddDocumentDialogModal }; diff --git a/assets/espaceco/pages/communities/management/description/DocumentList.tsx b/assets/espaceco/pages/communities/management/description/DocumentList.tsx new file mode 100644 index 00000000..9acef085 --- /dev/null +++ b/assets/espaceco/pages/communities/management/description/DocumentList.tsx @@ -0,0 +1,244 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import Table from "@codegouvfr/react-dsfr/Table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { FC, ReactNode, useCallback, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { v4 as uuidv4 } from "uuid"; +import { DocumentDTO } from "../../../../../@types/espaceco"; +import LoadingText from "../../../../../components/Utils/LoadingText"; +import Wait from "../../../../../components/Utils/Wait"; +import thumbnails from "../../../../../data/doc_thumbnail.json"; +import { useTranslation } from "../../../../../i18n/i18n"; +import RQKeys from "../../../../../modules/espaceco/RQKeys"; +import { CartesApiException } from "../../../../../modules/jsonFetch"; +import { appRoot } from "../../../../../router/router"; +import { useSnackbarStore } from "../../../../../stores/SnackbarStore"; +import { getFileExtension } from "../../../../../utils"; +import api from "../../../../api"; +import { AddDocumentDialog, AddDocumentDialogModal } from "./AddDocumentDialog"; +import { EditDocumentDialog, EditDocumentDialogModal } from "./EditDocumentDialog"; + +type DocumentListProps = { + communityId: number; + documents: DocumentDTO[]; +}; + +const ConfirmRemoveDocumentDialogModal = createModal({ + id: `confirm-delete-document-${uuidv4()}`, + isOpenedByDefault: false, +}); + +const DocumentList: FC = ({ communityId, documents }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("ManageCommunity"); + + const setMessage = useSnackbarStore((state) => state.setMessage); + const [currentDocument, setCurrentDocument] = useState(); + + const copyToClipboard = useCallback( + (uri) => { + navigator.clipboard.writeText(uri); + setMessage(tCommon("url_copied")); + }, + [tCommon, setMessage] + ); + + const getThumbnail = useCallback((fileName: string) => { + const extension = getFileExtension(fileName); + + let src = thumbnails.defaut; + if (extension) { + src = thumbnails[extension].src; + } + return `${appRoot}/${src}`; + }, []); + + const queryClient = useQueryClient(); + + const addDocumentMutation = useMutation({ + mutationFn: (data) => api.communityDocuments.add(communityId, data), + onSuccess: (document) => { + queryClient.setQueryData(RQKeys.communityDocuments(communityId), (oldDocuments) => { + const documents = oldDocuments ? [...oldDocuments] : []; + documents.push(document); + return documents; + }); + }, + }); + + const updateDocumentMutation = useMutation({ + mutationFn: (data) => { + if (currentDocument) { + return api.communityDocuments.update(communityId, currentDocument.id, data); + } + return Promise.reject(); + }, + onSuccess: (document) => { + queryClient.setQueryData(RQKeys.communityDocuments(communityId), (oldDocuments) => { + const documents = oldDocuments ? [...oldDocuments] : []; + + const index = documents.findIndex((d) => d.id === document.id); + if (index >= 0) { + documents[index] = { ...document }; + } + return documents; + }); + }, + onSettled: () => setCurrentDocument(undefined), + }); + + const removeDocumentMutation = useMutation({ + mutationFn: () => { + if (currentDocument) { + return api.communityDocuments.remove(communityId, currentDocument.id); + } + return Promise.reject(); + }, + onSuccess: () => { + queryClient.setQueryData(RQKeys.communityDocuments(communityId), (oldDocuments) => { + const documents = oldDocuments ? [...oldDocuments] : []; + return documents.filter((d) => d.id !== currentDocument?.id); + }); + }, + onSettled: () => setCurrentDocument(undefined), + }); + + const datas: ReactNode[][] = useMemo(() => { + return documents.map((d) => { + const element = d.uri ? ( + + ) : ( + + ); + return [ +
+ {element} + {d.title} +
, +
+ {d.description} +
, +
+ {d.uri && ( +
, + ]; + }); + }, [tCommon, documents, getThumbnail, copyToClipboard]); + + return ( +
+ {addDocumentMutation.isError && } + {updateDocumentMutation.isError && } + {removeDocumentMutation.isError && } + {addDocumentMutation.isPending && ( + +
+ +
+
+ )} + {updateDocumentMutation.isPending && ( + +
+ +
+
+ )} + {removeDocumentMutation.isPending && ( + +
+ +
+
+ )} + + + { + addDocumentMutation.mutate(data); + AddDocumentDialogModal.close(); + }} + /> + { + updateDocumentMutation.mutate(data); + EditDocumentDialogModal.close(); + }} + /> + {createPortal( + { + if (currentDocument) { + removeDocumentMutation.mutate(); + } + }, + }, + ]} + > +
+ , + document.body + )} +
+ ); +}; + +export default DocumentList; diff --git a/assets/espaceco/pages/communities/management/description/EditDocumentDialog.tsx b/assets/espaceco/pages/communities/management/description/EditDocumentDialog.tsx new file mode 100644 index 00000000..8710df19 --- /dev/null +++ b/assets/espaceco/pages/communities/management/description/EditDocumentDialog.tsx @@ -0,0 +1,136 @@ +import Input from "@codegouvfr/react-dsfr/Input"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { TranslationFunction } from "i18nifty/typeUtils/TranslationFunction"; +import { FC, useCallback, useMemo } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { ComponentKey, useTranslation } from "../../../../../i18n/i18n"; + +import { DocumentDTO } from "../../../../../@types/espaceco"; +import "../../../../../sass/pages/espaceco/community.scss"; + +const EditDocumentDialogModal = createModal({ + id: "edit-document-modal", + isOpenedByDefault: false, +}); + +type EditDocumentDialogProps = { + editDocument?: DocumentDTO; + onModify: (data: object) => void; +}; + +const EditDocumentDialog: FC = ({ editDocument, onModify }) => { + const { t: tCommon } = useTranslation("Common"); + const { t: tValid } = useTranslation("ManageCommunityValidations"); + const { t } = useTranslation("ManageCommunity"); + + const getSchema = useCallback( + (t: TranslationFunction<"ManageCommunityValidations", ComponentKey>) => + yup.object().shape({ + title: yup.string().min(10, t("description.modal.document.name.minlength")).required(t("description.modal.document.name.mandatory")), + description: yup.string(), + }), + [] + ); + + const schema = getSchema(tValid); + type FormType = yup.InferType; + + const originalValues = useMemo(() => { + return editDocument + ? { + title: editDocument.title, + description: editDocument.description ?? "", + } + : { title: "", description: "" }; + }, [editDocument]); + + const form = useForm({ + mode: "onChange", + resolver: yupResolver(schema), + values: originalValues, + }); + + const { + register, + getValues: getFormValues, + formState: { errors }, + handleSubmit, + resetField, + } = form; + + const clear = () => { + resetField("title"); + resetField("description"); + }; + + const onSubmit = () => { + const formValues = getFormValues(); + + const body = {}; + if (formValues.title !== originalValues.title) { + body["title"] = formValues.title; + } + if (formValues.description !== originalValues.description) { + body["description"] = originalValues.description; + } + + if (Object.keys(body).length) { + onModify(body); + } + clear(); + }; + + return ( + <> + {createPortal( + { + clear(); + EditDocumentDialogModal.close(); + }, + priority: "secondary", + }, + { + children: tCommon("modify"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + priority: "primary", + }, + ]} + > +
+ + +
+
, + document.body + )} + + ); +}; + +export { EditDocumentDialog, EditDocumentDialogModal }; diff --git a/assets/espaceco/pages/communities/management/member/AddMembersDialog.tsx b/assets/espaceco/pages/communities/management/member/AddMembersDialog.tsx index 9f45222c..15983caf 100644 --- a/assets/espaceco/pages/communities/management/member/AddMembersDialog.tsx +++ b/assets/espaceco/pages/communities/management/member/AddMembersDialog.tsx @@ -55,7 +55,6 @@ const AddMembersDialog: FC = () => { }); const users = watch("users"); - console.log("USERS : ", users); const handleRemove = useCallback( (user: UserDTO | string) => { diff --git a/assets/espaceco/pages/communities/management/reports/AddOrEditEmailPlannerDialog.tsx b/assets/espaceco/pages/communities/management/reports/AddOrEditEmailPlannerDialog.tsx deleted file mode 100644 index c394830c..00000000 --- a/assets/espaceco/pages/communities/management/reports/AddOrEditEmailPlannerDialog.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { createModal } from "@codegouvfr/react-dsfr/Modal"; -import { EmailPlannerDTO } from "../../../../../@types/espaceco"; -import { createPortal } from "react-dom"; -import { FC, useEffect, useMemo, useState } from "react"; -import { declareComponentKeys, Translations, useTranslation } from "../../../../../i18n/i18n"; -import Checkbox from "@codegouvfr/react-dsfr/Checkbox"; -import * as yup from "yup"; -import { useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import RadioButtons from "@codegouvfr/react-dsfr/RadioButtons"; -import { fr } from "@codegouvfr/react-dsfr"; - -const AddOrEditEmailPlannerDialogModal = createModal({ - id: "addoredit-emailplanner", - isOpenedByDefault: false, -}); - -type AddOrEditEmailPlannerDialogProps = { - emailPlanner?: EmailPlannerDTO; -}; - -const EmailPlannerTypes = ["basic", "personnal"]; -type EmailPlannerType = (typeof EmailPlannerTypes)[number]; - -type EmailPlannerForm = { - email_type: EmailPlannerType; -}; - -const AddOrEditEmailPlannerDialog: FC = ({ emailPlanner }) => { - /* const { t: tCommon } = useTranslation("Common"); - const { t } = useTranslation("AddOrEditEmailPlannerDialog"); - - const schema: yup.ObjectSchema = yup.object({ - email_type: yup.string().required().oneOf(EmailPlannerTypes), - }); - - const { register, watch } = useForm({ - mode: "onChange", - resolver: yupResolver(schema), - values: { - email_type: emailPlanner ? "personnal" : "basic", - }, - }); - - return ( - <> - {createPortal( - {}, - }, - { - children: emailPlanner ? tCommon("modify") : tCommon("add"), - priority: "primary", - doClosesModal: false, - onClick: () => {}, - }, - ]} - > -
- ({ - label: t("email_planner_type", { type: ept }), - nativeInputProps: { - ...register("email_type"), - value: ept, - }, - }))} - /> -

{tCommon("mandatory_fields")}

-
-
, - document.body - )} - - ); */ - return <>; -}; - -export { AddOrEditEmailPlannerDialogModal, AddOrEditEmailPlannerDialog }; diff --git a/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx b/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx index f25b578c..3589b89e 100644 --- a/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx +++ b/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx @@ -18,14 +18,17 @@ export type EditThemeFormType = { }; type EditThemeDialogProps = { - modal: ReturnType; themes: ThemeDTO[]; currentTheme?: ThemeDTO; - // tables: Partial[]; onModify: (oldName: string, newTheme: EditThemeFormType) => void; }; -const EditThemeDialog: FC = ({ modal, themes, currentTheme, onModify }) => { +const EditThemeDialogModal = createModal({ + id: "edit-theme", + isOpenedByDefault: false, +}); + +const EditThemeDialog: FC = ({ themes, currentTheme, onModify }) => { const { t: tCommon } = useTranslation("Common"); const { t } = useTranslation("Theme"); @@ -64,7 +67,7 @@ const EditThemeDialog: FC = ({ modal, themes, currentTheme }); const onSubmit = () => { - modal.close(); + EditThemeDialogModal.close(); if (currentTheme) { const values = getFormValues(); onModify(currentTheme?.theme, values); @@ -74,7 +77,7 @@ const EditThemeDialog: FC = ({ modal, themes, currentTheme return ( <> {createPortal( - = ({ modal, themes, currentTheme /> )} - , + , document.body )} ); }; -export default EditThemeDialog; +export { EditThemeDialogModal, EditThemeDialog }; diff --git a/assets/espaceco/pages/communities/management/reports/EmailPlanners.tsx b/assets/espaceco/pages/communities/management/reports/EmailPlanners.tsx index 8b5b0a62..68ecabc3 100644 --- a/assets/espaceco/pages/communities/management/reports/EmailPlanners.tsx +++ b/assets/espaceco/pages/communities/management/reports/EmailPlanners.tsx @@ -1,19 +1,21 @@ import { fr } from "@codegouvfr/react-dsfr"; +import Alert from "@codegouvfr/react-dsfr/Alert"; import Button from "@codegouvfr/react-dsfr/Button"; import Table from "@codegouvfr/react-dsfr/Table"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { FC, ReactNode, useMemo, useState } from "react"; import { UseFormReturn, useWatch } from "react-hook-form"; -import { ReportFormType } from "../../../../../@types/app_espaceco"; +import { EmailPlannerAddType, ReportFormType } from "../../../../../@types/app_espaceco"; import { EmailPlannerDTO, ReportStatusesDTO, ThemeDTO } from "../../../../../@types/espaceco"; import { ConfirmDialog, ConfirmDialogModal } from "../../../../../components/Utils/ConfirmDialog"; +import LoadingText from "../../../../../components/Utils/LoadingText"; import Wait from "../../../../../components/Utils/Wait"; import { declareComponentKeys, Translations, useTranslation } from "../../../../../i18n/i18n"; import RQKeys from "../../../../../modules/espaceco/RQKeys"; import { CartesApiException } from "../../../../../modules/jsonFetch"; import api from "../../../../api"; -import { AddOrEditEmailPlannerDialogModal, AddOrEditEmailPlannerDialog } from "./AddOrEditEmailPlannerDialog"; import { AddEmailPlannerDialog, AddEmailPlannerDialogModal } from "./emailplanners/AddEmailPlannerDialog"; +import { EditEmailPlannerDialog, EditEmailPlannerDialogModal } from "./emailplanners/EditEmailPlannerDialog"; type EmailPlannersProps = { communityId: number; @@ -21,22 +23,23 @@ type EmailPlannersProps = { emailPlanners: EmailPlannerDTO[]; }; -const checkStatus = (ep: EmailPlannerDTO, statuses: string[]) => { - const condition = ep.condition ? JSON.parse(ep.condition) : {}; - +const checkStatus = (ep: EmailPlannerDTO, statuses: ReportStatusesDTO) => { + const condition = ep.condition ?? { status: [] }; + if (!("status" in condition)) { + return true; + } // Verification du status - if ("status" in condition) { - for (const s of condition["status"]) { - if (!statuses.includes(s)) { - return false; - } + for (const s of condition["status"]) { + if (!(s in statuses)) { + return false; } } + return true; }; const checkThemes = (ep: EmailPlannerDTO, themeNames: string[]) => { - const epThemes = ep.themes ? JSON.parse(ep.themes) : []; + const epThemes = ep.themes ?? []; for (const theme of epThemes) { if (!themeNames.includes(theme)) { return false; @@ -46,7 +49,6 @@ const checkThemes = (ep: EmailPlannerDTO, themeNames: string[]) => { }; const EmailPlanners: FC = ({ communityId, form, emailPlanners }) => { - const { t: tCommon } = useTranslation("Common"); const { t: tmc } = useTranslation("ManageCommunity"); const { t } = useTranslation("EmailPlanners"); @@ -55,8 +57,11 @@ const EmailPlanners: FC = ({ communityId, form, emailPlanner const reportStatuses: ReportStatusesDTO = useWatch({ control, name: "report_statuses" }); // Les status actifs - const activeStatuses = useMemo(() => { - return Object.keys(reportStatuses).filter((s) => reportStatuses[s].active); + const activeStatuses = useMemo(() => { + return Object.keys(reportStatuses).reduce((accumulator, status) => { + if (reportStatuses[status].active) accumulator[status] = reportStatuses[status]; + return accumulator; + }, {}); }, [reportStatuses]); // Les thèmes du groupe @@ -66,8 +71,23 @@ const EmailPlanners: FC = ({ communityId, form, emailPlanner const queryClient = useQueryClient(); + // Ajout d'un email de suivi + const addPlannerMutation = useMutation({ + mutationFn: (data) => { + return api.emailplanner.add(communityId, data); + }, + onSuccess: (planner) => { + setCurrentEmailPlanner(undefined); + queryClient.setQueryData(RQKeys.emailPlanners(communityId), (oldPlanners: EmailPlannerDTO[]) => { + const emailPlanners = [...oldPlanners]; + emailPlanners.push(planner); + return emailPlanners; + }); + }, + }); + // Suppression d'un email de suivi - const { isPending: isRemovePending, mutate: mutateRemove } = useMutation<{ emailplanner_id: number }, CartesApiException, number>({ + const removePlannerMutation = useMutation<{ emailplanner_id: number }, CartesApiException, number>({ mutationFn: (emailplannerId) => { return api.emailplanner.remove(communityId, emailplannerId!); }, @@ -77,6 +97,7 @@ const EmailPlanners: FC = ({ communityId, form, emailPlanner return emailPlanners?.filter((ep) => ep.id !== data.emailplanner_id); }); }, + onSettled: () => setCurrentEmailPlanner(undefined), }); const datas = useMemo(() => { @@ -94,7 +115,7 @@ const EmailPlanners: FC = ({ communityId, form, emailPlanner size="small" onClick={() => { setCurrentEmailPlanner(ep); - AddOrEditEmailPlannerDialogModal.open(); + EditEmailPlannerDialogModal.open(); }} />
= ({ communityId, form, emailPlanner ]} data={datas} /> - - + addPlannerMutation.mutate(values)} /> + console.log(values)} + /> { if (currentEmailPlanner) { - mutateRemove(currentEmailPlanner.id); + removePlannerMutation.mutate(currentEmailPlanner.id); } }} /> @@ -178,8 +214,10 @@ export const { i18n } = declareComponentKeys< | "cancel_event_header" | "repeat_header" | "add" + | "adding" | "modify" | "remove" + | "removing" | "confirm_remove_title" >()("EmailPlanners"); @@ -192,8 +230,10 @@ export const EmailPlannersFrTranslations: Translations<"fr">["EmailPlanners"] = cancel_event_header: "Evénement annulateur", repeat_header: "Répétition", add: "Ajouter un email de suivi", + adding: "Ajout de l'email de suivi en cours ...", modify: "Modifier l'email de suivi", remove: "Supprimer l'email de suivi", + removing: "Suppression de l'email de suivi en cours ...", confirm_remove_title: "Êtes-vous sûr de vouloir supprimer cet email de suivi ?", }; @@ -206,7 +246,9 @@ export const EmailPlannersEnTranslations: Translations<"en">["EmailPlanners"] = cancel_event_header: undefined, repeat_header: undefined, add: undefined, + adding: undefined, modify: undefined, remove: undefined, + removing: undefined, confirm_remove_title: undefined, }; diff --git a/assets/espaceco/pages/communities/management/reports/ThemeList.tsx b/assets/espaceco/pages/communities/management/reports/ThemeList.tsx index 8027ab6d..b26e2616 100644 --- a/assets/espaceco/pages/communities/management/reports/ThemeList.tsx +++ b/assets/espaceco/pages/communities/management/reports/ThemeList.tsx @@ -1,16 +1,14 @@ import { fr } from "@codegouvfr/react-dsfr"; import Button from "@codegouvfr/react-dsfr/Button"; -import { createModal } from "@codegouvfr/react-dsfr/Modal"; import { cx } from "@codegouvfr/react-dsfr/tools/cx"; -import { CSSProperties, FC, useCallback } from "react"; +import { CSSProperties, FC, useState } from "react"; import { UseFormReturn } from "react-hook-form"; -import { v4 as uuidv4 } from "uuid"; import { ReportFormType } from "../../../../../@types/app_espaceco"; import { TableResponseDTO, ThemeDTO } from "../../../../../@types/espaceco"; import { useTranslation } from "../../../../../i18n/i18n"; import { AddThemeDialog, AddThemeDialogModal } from "./AddThemeDialog"; import AttributeList from "./AttributeList"; -import EditThemeDialog from "./EditThemeDialog"; +import { EditThemeDialog, EditThemeDialogModal } from "./EditThemeDialog"; import ThemesHelper from "./ThemesHelper"; const customStyle: CSSProperties = { @@ -35,30 +33,23 @@ const ThemeList: FC = ({ form, tables, state }) => { const { watch, setValue: setFormValue } = form; const themes: ThemeDTO[] = watch("attributes"); + const [currentTheme, setCurrentTheme] = useState(); + // Supression d'un theme const handleRemoveTheme = (theme: string) => { const th = ThemesHelper.removeTheme(theme, themes); setFormValue("attributes", th); }; - const getEditModal = useCallback((): ReturnType => { - return createModal({ - id: `edit-theme-${uuidv4()}`, - isOpenedByDefault: false, - }); - }, []); - return (

{t("report.configure_themes")}

{t("report.configure_themes.explain")}
- {themes.map((t) => { - const modal = getEditModal(); - return (
@@ -83,7 +74,8 @@ const ThemeList: FC = ({ form, tables, state }) => { iconId="fr-icon-edit-line" size="small" onClick={() => { - modal.open(); + setCurrentTheme(t); + EditThemeDialogModal.open(); }} />
- { - const th = ThemesHelper.updateTheme(oldName, newTheme, themes); - setFormValue("attributes", th); - }} - /> {!t.table && }
); @@ -127,6 +110,14 @@ const ThemeList: FC = ({ form, tables, state }) => { {tTheme("attributes_not_conform")}

)} + { + const th = ThemesHelper.updateTheme(oldName, newTheme, themes); + setFormValue("attributes", th); + }} + /> { +const getInputType = (type?: AttributeType) => { return type === "date" ? "date" : "text"; }; diff --git a/assets/espaceco/pages/communities/management/reports/emailplanners/AddEmailPlannerDialog.tsx b/assets/espaceco/pages/communities/management/reports/emailplanners/AddEmailPlannerDialog.tsx index da38bd86..b05af519 100644 --- a/assets/espaceco/pages/communities/management/reports/emailplanners/AddEmailPlannerDialog.tsx +++ b/assets/espaceco/pages/communities/management/reports/emailplanners/AddEmailPlannerDialog.tsx @@ -4,17 +4,15 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { FC, useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { Controller, useForm } from "react-hook-form"; +import isEmail from "validator/lib/isEmail"; import * as yup from "yup"; -import { EmailPlannerFormType, EmailPlannerType, EmailPlannerTypes } from "../../../../../../@types/app_espaceco"; -import { CancelEvents, TriggerEvents } from "../../../../../../@types/espaceco"; +import { BasicRecipientsArray, EmailPlannerAddType, EmailPlannerFormType, EmailPlannerType, EmailPlannerTypes } from "../../../../../../@types/app_espaceco"; +import { CancelEventType, EmailPlannerDTO, ReportStatusesDTO, TriggerEventType } from "../../../../../../@types/espaceco"; +import AutocompleteSelect from "../../../../../../components/Input/AutocompleteSelect"; import { useTranslation } from "../../../../../../i18n/i18n"; -import { setToNull } from "../../../../../../utils"; import { getAddDefaultValues } from "./Defaults"; import PersonalEmailPlanner from "./PersonalEmailPlanner"; -import RecipientsManager from "./RecipientsManager"; - -const cloneEvents = [...TriggerEvents] as string[]; -const cloneCancelEvents = [...CancelEvents] as string[]; +import { getBasicSchema, getPersonalSchema } from "./schemas"; const AddEmailPlannerDialogModal = createModal({ id: "add-emailplanner", @@ -23,104 +21,66 @@ const AddEmailPlannerDialogModal = createModal({ type AddEmailPlannerDialogProps = { themes: string[]; - statuses: string[]; + statuses: ReportStatusesDTO; + onAdd: (values: EmailPlannerAddType) => void; }; -const AddEmailPlannerDialog: FC = ({ themes, statuses }) => { +const AddEmailPlannerDialog: FC = ({ themes, statuses, onAdd }) => { const { t: tCommon } = useTranslation("Common"); const { t } = useTranslation("AddOrEditEmailPlanner"); - const [type, setType] = useState("basic"); - - const baseSchema = yup.object({ - type: yup.string().oneOf([...EmailPlannerTypes]), - recipients: yup.array().of(yup.string().required()).min(1, t("validation.error.email.min")).required(), - }); - - const schema = {}; - - schema["basic"] = baseSchema; - schema["personal"] = baseSchema.shape({ - subject: yup.string().required(t("validation.subject.mandatory")), - body: yup.string().required(t("validation.body.mandatory")), - delay: yup.number().min(1, t("validation.delay.positive")).required(t("validation.delay.mandatory")), - repeat: yup.boolean().required(), - event: yup.string().required().oneOf(cloneEvents), - cancel_event: yup - .string() - .required() - .oneOf([...cloneCancelEvents]), - condition: yup.object({ - status: yup - .array() - .of(yup.string().oneOf(statuses).required()) - .test({ - name: "validate-condition", - test: (value, context) => { - if (!value || !context.from) return true; - - const [, parent] = context.from; - const { event } = parent.value; - if (event === "georem_status_changed") { - if (value && value.length === 0) { - return context.createError({ message: t("validation.condition.mandatory") }); - } - } + const [type, setType] = useState<"basic" | "personal">("basic"); - return true; - }, - }), - }), - /*.test({ - name: "validate-condition", - test: (value, context) => { - const { - parent: { event }, - } = context; - if (event === "georem_status_changed") { - if (!("status" in value) || value["status"]?.length === 0) { - return context.createError({ message: t("validation.condition.mandatory") }); - } - } - return true; - }, - })*/ themes: yup.string().nullable().transform(setToNull), - }); + const schemas = {}; + schemas["basic"] = getBasicSchema(); + schemas["personal"] = getPersonalSchema(themes, statuses); const form = useForm({ mode: "onChange", - resolver: yupResolver(schema[type]), + resolver: yupResolver(schemas[type]), defaultValues: getAddDefaultValues(type), }); const { control, - watch, formState: { errors }, getValues: getFormValues, reset, handleSubmit, } = form; - /* TODO SUPPRIMER */ - const values = watch(); - useEffect(() => { - console.log("VALUES : ", values); - }, [values]); - useEffect(() => { reset(getAddDefaultValues(type)); }, [reset, type]); const resetForm = useCallback(() => { + /* si type est déjà basic, setType ne déclenchera pas le useEffect précédent + et ne fera donc pas un reset du formulaire avec les valeurs par défaut */ type === "basic" ? reset(getAddDefaultValues(type)) : setType("basic"); }, [type, reset]); - // TODO const onSubmit = () => { const values = getFormValues(); - console.log(values); - resetForm(); + let form: EmailPlannerAddType = { + subject: values.subject, + event: values.event as TriggerEventType, + cancel_event: values.cancel_event as CancelEventType, + body: values.body, + recipients: values.recipients, + themes: values.themes ?? [], + condition: null, + delay: values.delay, + repeat: values.repeat, + }; + + if (values.event === "georem_status_changed") { + const statuses = values.statuses ?? []; + form = { ...form, condition: { status: statuses } }; + } + + onAdd(form); + + resetForm(); AddEmailPlannerDialogModal.close(); }; @@ -162,18 +122,36 @@ const AddEmailPlannerDialog: FC = ({ themes, statuse

{tCommon("mandatory_fields")}

{type === "basic" ? ( - ( - - )} - /> + <> +
{t("dialog.recipients")}
+ ( + { + return option === value; + }} + searchFilter={{ limit: 10 }} + onChange={(_, value) => { + if (value && Array.isArray(value)) { + value = value.filter((v) => { + if (BasicRecipientsArray.includes(v)) return true; + return isEmail(v); + }); + onChange(value); + } + }} + value={value} + /> + )} + /> + ) : ( )} diff --git a/assets/espaceco/pages/communities/management/reports/emailplanners/AddOrEditEmailPlannerTr.tsx b/assets/espaceco/pages/communities/management/reports/emailplanners/AddOrEditEmailPlannerTr.tsx index 6f58f7a4..fc83f329 100644 --- a/assets/espaceco/pages/communities/management/reports/emailplanners/AddOrEditEmailPlannerTr.tsx +++ b/assets/espaceco/pages/communities/management/reports/emailplanners/AddOrEditEmailPlannerTr.tsx @@ -33,6 +33,7 @@ export const { i18n } = declareComponentKeys< | "validation.body.mandatory" | "validation.delay.mandatory" | "validation.delay.positive" + | "validation.themes.mandatory" | "validation.condition.mandatory" | { K: "validation.error.email_not_valid"; P: { value: string }; R: string } | "validation.error.email.min" @@ -100,6 +101,7 @@ export const AddOrEditEmailPlannerFrTranslations: Translations<"fr">["AddOrEditE "validation.body.mandatory": "Le corps de l'email est obligatoire", "validation.delay.mandatory": "Le délai est obligatoire", "validation.delay.positive": "Le délai doit être supérieur à 0", + "validation.themes.mandatory": "Les thèmes sont obligatoires", "validation.condition.mandatory": "Vous devez sélectionner au moins un statut si l'évènement déclencheur est [Modification du statut]", "validation.error.email_not_valid": ({ value }) => `La chaîne ${value} n'est pas un email valide`, "validation.error.email.min": "Il doit y avoir au moins un email destinataire", @@ -135,6 +137,7 @@ export const AddOrEditEmailPlannerEnTranslations: Translations<"en">["AddOrEditE "validation.body.mandatory": "Email body is mandatory", "validation.delay.mandatory": undefined, "validation.delay.positive": undefined, + "validation.themes.mandatory": undefined, "validation.condition.mandatory": undefined, "validation.error.email_not_valid": ({ value }) => `String ${value} is not a valid email`, "validation.error.email.min": undefined, diff --git a/assets/espaceco/pages/communities/management/reports/emailplanners/Defaults.tsx b/assets/espaceco/pages/communities/management/reports/emailplanners/Defaults.tsx index c346d26d..73d1bff1 100644 --- a/assets/espaceco/pages/communities/management/reports/emailplanners/Defaults.tsx +++ b/assets/espaceco/pages/communities/management/reports/emailplanners/Defaults.tsx @@ -7,15 +7,26 @@ const getAddDefaultValues = (type: EmailPlannerType): EmailPlannerFormType => { delay: 1, cancel_event: "georem_status_changed", repeat: false, - // recipients: [], subject: type === "personal" ? "" : "Nouveau signalement", body: type === "personal" ? "" : "Le signalement n° _id_ a été envoyé le _openingDate_ par _author_", - condition: { status: [] }, + recipients: [], themes: [], }; }; const getEditDefaultValues = (emailPlaner: EmailPlannerDTO): EmailPlannerFormType => { + let statuses: string[] = []; + if (emailPlaner.condition) { + try { + const condition = emailPlaner.condition ?? { status: [] }; + if ("status" in condition) { + statuses = condition["status"]; + } + } catch (e) { + /* empty */ + } + } + return { id: emailPlaner.id, event: emailPlaner.event, @@ -25,8 +36,8 @@ const getEditDefaultValues = (emailPlaner: EmailPlannerDTO): EmailPlannerFormTyp recipients: emailPlaner.recipients, subject: emailPlaner.subject, body: emailPlaner.body, - condition: emailPlaner.condition ? JSON.parse(emailPlaner.condition) : undefined, - themes: emailPlaner.themes ? JSON.parse(emailPlaner.themes) : [], + statuses: statuses, + themes: emailPlaner.themes, }; }; diff --git a/assets/espaceco/pages/communities/management/reports/emailplanners/EditEmailPlannerDialog.tsx b/assets/espaceco/pages/communities/management/reports/emailplanners/EditEmailPlannerDialog.tsx new file mode 100644 index 00000000..72f46111 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/emailplanners/EditEmailPlannerDialog.tsx @@ -0,0 +1,74 @@ +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FC, useEffect } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import { EmailPlannerFormType } from "../../../../../../@types/app_espaceco"; +import { EmailPlannerDTO, ReportStatusesDTO } from "../../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../../i18n/i18n"; +import { getAddDefaultValues, getEditDefaultValues } from "./Defaults"; +import PersonalEmailPlanner from "./PersonalEmailPlanner"; +import { getPersonalSchema } from "./schemas"; + +const EditEmailPlannerDialogModal = createModal({ + id: "edit-emailplanner", + isOpenedByDefault: false, +}); + +type EditEmailPlannerDialogProps = { + emailPlanner?: EmailPlannerDTO; + themes: string[]; + statuses: ReportStatusesDTO; + onModify: (values: EmailPlannerDTO) => void; +}; + +const EditEmailPlannerDialog: FC = ({ emailPlanner, themes, statuses, onModify }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("AddOrEditEmailPlanner"); + + const schema = getPersonalSchema(themes, statuses); + + const form = useForm({ + mode: "onSubmit", + values: emailPlanner ? getEditDefaultValues(emailPlanner) : getAddDefaultValues("personal"), + resolver: yupResolver(schema), + }); + + const { watch } = form; + + /* TODO SUPPRIMER */ + const values = watch(); + useEffect(() => { + console.log("VALUES : ", values); + }, [values]); + + return ( + <> + {createPortal( + +
+ +
+
, + document.body + )} + + ); +}; +export { EditEmailPlannerDialog, EditEmailPlannerDialogModal }; diff --git a/assets/espaceco/pages/communities/management/reports/emailplanners/PersonalEmailPlanner.tsx b/assets/espaceco/pages/communities/management/reports/emailplanners/PersonalEmailPlanner.tsx index d1da5fa6..c7df13a8 100644 --- a/assets/espaceco/pages/communities/management/reports/emailplanners/PersonalEmailPlanner.tsx +++ b/assets/espaceco/pages/communities/management/reports/emailplanners/PersonalEmailPlanner.tsx @@ -2,22 +2,27 @@ import { fr } from "@codegouvfr/react-dsfr"; import Input from "@codegouvfr/react-dsfr/Input"; import Select from "@codegouvfr/react-dsfr/Select"; import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; -import { FC, useEffect } from "react"; +import { FC, useCallback, useEffect, useMemo } from "react"; import { Controller, UseFormReturn, useWatch } from "react-hook-form"; -import { EmailPlannerFormType } from "../../../../../../@types/app_espaceco"; -import { CancelEvents, TriggerEvents } from "../../../../../../@types/espaceco"; +import isEmail from "validator/lib/isEmail"; +import { BasicRecipientsArray, EmailPlannerFormType } from "../../../../../../@types/app_espaceco"; +import { CancelEvents, ReportStatusesDTO, ReportStatusesType, TriggerEvents } from "../../../../../../@types/espaceco"; import AutocompleteSelect from "../../../../../../components/Input/AutocompleteSelect"; import MarkdownEditor from "../../../../../../components/Input/MarkdownEditor"; import { useTranslation } from "../../../../../../i18n/i18n"; import getKeywordsExtraCommands from "./EmailPlannerKeywords"; -import RecipientsManager from "./RecipientsManager"; import "../../../../../../sass/components/react-md-editor.scss"; type PersonalEmailPlannerProps = { form: UseFormReturn; themes: string[]; - statuses: string[]; + statuses: ReportStatusesDTO; +}; + +type StatusAutocompleteOption = { + status: ReportStatusesType; + title: string; }; const PersonalEmailPlanner: FC = ({ form, themes, statuses }) => { @@ -40,10 +45,33 @@ const PersonalEmailPlanner: FC = ({ form, themes, sta useEffect(() => { if (event === "georem_created") { - resetField("condition", undefined); + resetField("statuses", { defaultValue: [] }); } }, [resetField, event]); + const options = useMemo(() => { + const options = Object.keys(statuses).reduce((accumulator, status) => { + accumulator[status] = { title: statuses[status].title, status: status }; + return accumulator; + }, {}); + return Object.values(options); + }, [statuses]); + + const getValue = useCallback( + (value?: string[]) => { + if (!value) return []; + + const options = Object.keys(statuses).reduce((accumulator, status) => { + if (value.includes(status)) { + accumulator[status] = { title: statuses[status].title, status: status }; + } + return accumulator; + }, {}); + return Object.values(options); + }, + [statuses] + ); + return (
= ({ form, themes, sta return ( field.onChange(value)} @@ -88,17 +118,26 @@ const PersonalEmailPlanner: FC = ({ form, themes, sta {event === "georem_status_changed" && ( { return ( (option as StatusAutocompleteOption).title} + isOptionEqualToValue={(option, value) => { + const optionStatus = (option as StatusAutocompleteOption).status; + const valueStatus = (value as StatusAutocompleteOption).status; + return optionStatus === valueStatus; + }} searchFilter={{ limit: 10 }} - onChange={(_, value) => field.onChange(value)} - value={field.value ?? []} + onChange={(_, value) => { + const selected = value as StatusAutocompleteOption[]; + field.onChange(selected.map((s) => s.status)); + }} + value={getValue(field.value)} /> ); }} @@ -139,15 +178,31 @@ const PersonalEmailPlanner: FC = ({ form, themes, sta showCheckedHint={false} onChange={(checked) => setFormValue("repeat", checked)} /> +
{t("dialog.recipients")}
( - { + return option === value; + }} + searchFilter={{ limit: 10 }} + onChange={(_, value) => { + if (value && Array.isArray(value)) { + value = value.filter((v) => { + if (BasicRecipientsArray.includes(v)) return true; + return isEmail(v); + }); + onChange(value); + } + }} + value={value} /> )} /> diff --git a/assets/espaceco/pages/communities/management/reports/emailplanners/RecipientsManager.tsx b/assets/espaceco/pages/communities/management/reports/emailplanners/RecipientsManager.tsx index 92b47fbc..663ff927 100644 --- a/assets/espaceco/pages/communities/management/reports/emailplanners/RecipientsManager.tsx +++ b/assets/espaceco/pages/communities/management/reports/emailplanners/RecipientsManager.tsx @@ -1,15 +1,17 @@ import { fr } from "@codegouvfr/react-dsfr"; import Checkbox from "@codegouvfr/react-dsfr/Checkbox"; import { yupResolver } from "@hookform/resolvers/yup"; -import { FC, useEffect, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { FC, useEffect } from "react"; +import { Controller, useForm, useWatch } from "react-hook-form"; import isEmail from "validator/lib/isEmail"; import * as yup from "yup"; -import { Recipients } from "../../../../../../@types/espaceco"; +import { BasicRecipientsArray } from "../../../../../../@types/app_espaceco"; +import { BasicRecipients } from "../../../../../../@types/espaceco"; import InputCollection from "../../../../../../components/Input/InputCollection"; import { useTranslation } from "../../../../../../i18n/i18n"; type RecipientFormType = { + basicRecipients?: string[]; extraRecipients?: string[]; }; @@ -22,6 +24,7 @@ type RecipientsManagerProps = { const defaultValues = (recipients) => { return { + basicRecipients: recipients.filter((recipient) => !isEmail(recipient)), extraRecipients: recipients.filter((recipient) => isEmail(recipient)), }; }; @@ -29,9 +32,8 @@ const defaultValues = (recipients) => { const RecipientsManager: FC = ({ value, state, stateRelatedMessage, onChange }) => { const { t } = useTranslation("AddOrEditEmailPlanner"); - const [basicRecipients, setBasicRecipients] = useState(() => value.filter((recipient) => !isEmail(recipient))); - const schema = yup.object({ + basicRecipients: yup.array().of(yup.string().oneOf(BasicRecipientsArray).required()), extraRecipients: yup .array() .of(yup.string().required()) @@ -52,7 +54,7 @@ const RecipientsManager: FC = ({ value, state, stateRela const { control, - watch, + register, formState: { errors }, } = useForm({ mode: "onChange", @@ -60,34 +62,29 @@ const RecipientsManager: FC = ({ value, state, stateRela values: defaultValues(value), }); - const extraRecipients = watch("extraRecipients"); + const basicRecipients = useWatch({ + control: control, + name: "basicRecipients", + }); + const extraRecipients = useWatch({ + control: control, + name: "extraRecipients", + }); useEffect(() => { const recipients = [...(basicRecipients ?? []), ...(extraRecipients ?? [])]; onChange(recipients); }, [basicRecipients, extraRecipients, onChange]); - const handleChange = (event: React.ChangeEvent) => { - let basics = [...basicRecipients]; - - const checked = event.currentTarget.checked; - if (checked) { - basics.push(event.currentTarget.value); - } else basics = basics.filter((v) => v !== event.currentTarget.value); - - setBasicRecipients([...new Set(basics)]); - }; - return (
{t("dialog.recipients")}
({ + options={BasicRecipients.map((r) => ({ label: t("recipient", { name: r }), nativeInputProps: { - onChange: (e) => handleChange(e), + ...register("basicRecipients"), value: r, - checked: basicRecipients.includes(r), }, }))} /> @@ -96,6 +93,7 @@ const RecipientsManager: FC = ({ value, state, stateRela name="extraRecipients" render={({ field: { value, onChange } }) => ( { + if (value === undefined) return true; + if (!value.length) { + return ctx.createError({ message: t("validation.error.email.min") }); + } + for (const v of value) { + if (BasicRecipientsArray.includes(v)) continue; + if (!isEmail(v)) { + return ctx.createError({ message: t("validation.error.email_not_valid", { value: v }) }); + } + } + return true; + }, + }) + .required(), +}); + +const getBasicSchema = () => { + return recipientsSchema; +}; + +const getPersonalSchema = (themes: string[], statuses: ReportStatusesDTO) => { + return recipientsSchema.concat( + yup.object({ + subject: yup.string().required(t("validation.subject.mandatory")), + body: yup.string().required(t("validation.body.mandatory")), + delay: yup.number().min(1, t("validation.delay.positive")).required(t("validation.delay.mandatory")), + repeat: yup.boolean().required(), + event: yup.string().required().oneOf(cloneEvents), + cancel_event: yup + .string() + .required() + .oneOf([...cloneCancelEvents]), + statuses: yup + .array() + .of(yup.string().oneOf(Object.keys(statuses)).required()) + .test({ + name: "validate-status", + test: (value, context) => { + if (!value) return true; + const { + parent: { event }, + } = context; + + if (event === "georem_status_changed") { + if (value && value.length === 0) { + return context.createError({ message: t("validation.condition.mandatory") }); + } + } + + return true; + }, + }), + themes: yup + .array() + .of(yup.string().oneOf(themes).required()) + .test({ + name: "validate-status", + test: (value, context) => { + const v = value ?? []; + const { + parent: { event }, + } = context; + + if (event === "georem_created" && v.length === 0) { + return context.createError({ message: t("validation.themes.mandatory") }); + } + return true; + }, + }), + }) + ); +}; + +export { getBasicSchema, getPersonalSchema }; diff --git a/assets/espaceco/pages/communities/management/validationTr.tsx b/assets/espaceco/pages/communities/management/validationTr.tsx index 737c907f..98561c76 100644 --- a/assets/espaceco/pages/communities/management/validationTr.tsx +++ b/assets/espaceco/pages/communities/management/validationTr.tsx @@ -43,7 +43,7 @@ export const ManageCommunityValidationsFrTranslations: Translations<"fr">["Manag "zoom.greater_than": ({ field, v }) => `La valeur de ${field} doit être supérieure ou égale à ${v}`, "zoom.extent.required": "La boîte englobante est obligatoire", "description.modal.document.name.mandatory": "Le nom est obligatoire", - "description.modal.document.name.minlength": "Le nom doit faire au moins 7 caractères", + "description.modal.document.name.minlength": "Le nom doit faire au moins 10 caractères", "description.modal.document.file.mandatory": "Le fichier est obligatoire", "description.modal.document.file.size_error": "La taille du fichier ne peut excéder 5 Mo", }; diff --git a/assets/i18n/Common.tsx b/assets/i18n/Common.tsx index 18434b4a..1082a5cf 100644 --- a/assets/i18n/Common.tsx +++ b/assets/i18n/Common.tsx @@ -34,6 +34,7 @@ export const { i18n } = declareComponentKeys< | "next_step" | "url_copied" | "copy_to_clipboard" + | "download" | "trimmed_error" >()("Common"); @@ -71,6 +72,7 @@ export const commonFrTranslations: Translations<"fr">["Common"] = { next_step: "Étape suivante", url_copied: "URL copiée", copy_to_clipboard: "Copier dans le presse-papier", + download: "Télécharger", trimmed_error: "La chaîne de caractères ne doit contenir aucun espace en début et fin", }; @@ -108,5 +110,6 @@ export const commonEnTranslations: Translations<"en">["Common"] = { next_step: "Next step", url_copied: "URL copied", copy_to_clipboard: "Copier dans le presse-papier", + download: "Download", trimmed_error: "The character string must not contain any spaces at the beginning and end", }; diff --git a/assets/i18n/i18n.ts b/assets/i18n/i18n.ts index 7f435a0e..83fa2db2 100644 --- a/assets/i18n/i18n.ts +++ b/assets/i18n/i18n.ts @@ -20,6 +20,7 @@ export type ComponentKey = | typeof import("./Breadcrumb").i18n | typeof import("./Rights").i18n | typeof import("./Style").i18n + | typeof import("../components/Input/InputCollection").i18n | typeof import("../entrepot/pages/users/Me").i18n | typeof import("../entrepot/pages/communities/AddMember").i18n | typeof import("../entrepot/pages/communities/CommunityMembers").i18n @@ -54,6 +55,7 @@ export type ComponentKey = | typeof import("../espaceco/pages/communities/ManageCommunityTr").i18n | typeof import("../espaceco/pages/communities/management/validationTr").i18n | typeof import("../espaceco/pages/communities/management/SearchTr").i18n + | typeof import("../espaceco/pages/communities/management/Description").i18n | typeof import("../espaceco/pages/communities/management/Reports").i18n | typeof import("../espaceco/pages/communities/management/reports/ThemeTr").i18n | typeof import("../espaceco/pages/communities/management/reports/ReportStatusesTr").i18n diff --git a/assets/i18n/languages/en.tsx b/assets/i18n/languages/en.tsx index 74a68342..485cc3ea 100644 --- a/assets/i18n/languages/en.tsx +++ b/assets/i18n/languages/en.tsx @@ -42,10 +42,12 @@ import { SharedThemesEnTranslations } from "../../espaceco/pages/communities/man import { EscoCommunityMembersEnTranslations } from "../../espaceco/pages/communities/management/Members"; import { AddMembersDialogEnTranslations } from "../../espaceco/pages/communities/management/member/AddMembersDialog"; import { ManageGridsDialogEnTranslations } from "../../espaceco/pages/communities/management/member/ManageGridsDialog"; +import { DescriptionEnTranslations } from "../../espaceco/pages/communities/management/Description"; import { ReportsEnTranslations } from "../../espaceco/pages/communities/management/Reports"; import { EmailPlannersEnTranslations } from "../../espaceco/pages/communities/management/reports/EmailPlanners"; import { AddOrEditEmailPlannerEnTranslations } from "../../espaceco/pages/communities/management/reports/emailplanners/AddOrEditEmailPlannerTr"; import { EmailPlannerKeywordsEnTranslations } from "../../espaceco/pages/communities/management/reports/emailplanners/EmailPlannerKeywords"; +import { InputCollectionEnTranslations } from "../../components/Input/InputCollection"; import type { Translations } from "../i18n"; @@ -54,6 +56,7 @@ export const translations: Translations<"en"> = { Breadcrumb: BreadcrumbEnTranslations, Rights: RightsEnTranslations, Style: StyleEnTranslations, + InputCollection: InputCollectionEnTranslations, Me: MeEnTranslations, AddMember: AddMemberEnTranslations, CommunityMembers: CommunityMembersEnTranslations, @@ -88,6 +91,7 @@ export const translations: Translations<"en"> = { ManageCommunity: ManageCommunityEnTranslations, ManageCommunityValidations: ManageCommunityValidationsEnTranslations, Theme: ThemeEnTranslations, + Description: DescriptionEnTranslations, Reports: ReportsEnTranslations, EmailPlanners: EmailPlannersEnTranslations, AddOrEditEmailPlanner: AddOrEditEmailPlannerEnTranslations, diff --git a/assets/i18n/languages/fr.tsx b/assets/i18n/languages/fr.tsx index 48d45c68..ec642df0 100644 --- a/assets/i18n/languages/fr.tsx +++ b/assets/i18n/languages/fr.tsx @@ -36,6 +36,7 @@ import { BreadcrumbFrTranslations } from "../Breadcrumb"; import { commonFrTranslations } from "../Common"; import { RightsFrTranslations } from "../Rights"; import { StyleFrTranslations } from "../Style"; +import { DescriptionFrTranslations } from "../../espaceco/pages/communities/management/Description"; import { ReportsFrTranslations } from "../../espaceco/pages/communities/management/Reports"; import { EmailPlannersFrTranslations } from "../../espaceco/pages/communities/management/reports/EmailPlanners"; import { AddOrEditEmailPlannerFrTranslations } from "../../espaceco/pages/communities/management/reports/emailplanners/AddOrEditEmailPlannerTr"; @@ -46,6 +47,7 @@ import { SharedThemesFrTranslations } from "../../espaceco/pages/communities/man import { EscoCommunityMembersFrTranslations } from "../../espaceco/pages/communities/management/Members"; import { AddMembersDialogFrTranslations } from "../../espaceco/pages/communities/management/member/AddMembersDialog"; import { ManageGridsDialogFrTranslations } from "../../espaceco/pages/communities/management/member/ManageGridsDialog"; +import { InputCollectionFrTranslations } from "../../components/Input/InputCollection"; import type { Translations } from "../i18n"; @@ -54,6 +56,7 @@ export const translations: Translations<"fr"> = { Breadcrumb: BreadcrumbFrTranslations, Rights: RightsFrTranslations, Style: StyleFrTranslations, + InputCollection: InputCollectionFrTranslations, Me: MeFrTranslations, AddMember: AddMemberFrTranslations, CommunityMembers: CommunityMembersFrTranslations, @@ -88,6 +91,7 @@ export const translations: Translations<"fr"> = { ManageCommunity: ManageCommunityFrTranslations, ManageCommunityValidations: ManageCommunityValidationsFrTranslations, Reports: ReportsFrTranslations, + Description: DescriptionFrTranslations, EmailPlanners: EmailPlannersFrTranslations, AddOrEditEmailPlanner: AddOrEditEmailPlannerFrTranslations, EmailPlannerKeywords: EmailPlannerKeywordsFrTranslations, diff --git a/assets/modules/espaceco/RQKeys.ts b/assets/modules/espaceco/RQKeys.ts index 374abb56..5613ccce 100644 --- a/assets/modules/espaceco/RQKeys.ts +++ b/assets/modules/espaceco/RQKeys.ts @@ -4,7 +4,6 @@ const RQKeys = { communityList: (page: number, limit: number): string[] => ["communities", page.toString(), limit.toString()], communitiesName: (): string[] => ["communities_names"], community: (communityId: number): string[] => ["community", communityId.toString()], - emailPlanners: (communityId: number): string[] => ["emailplanners", communityId.toString()], communityMembershipRequests: (communityId: number): string[] => ["community", "members", "pending", communityId.toString()], communityMembers: (communityId: number, page: number, limit: number): string[] => [ "community", @@ -22,11 +21,13 @@ const RQKeys = { page.toString(), limit.toString(), ], + communityDocuments: (communityId: number): string[] => ["community", "documents", communityId.toString()], userSharedThemes: (): string[] => ["user", "shared_themes"], searchAddress: (search: string): string[] => ["searchAddress", search], searchGrids: (text: string): string[] => ["searchGrids", text], searchUsers: (text: string): string[] => ["searchUsers", text], tables: (communityId: number): string[] => ["feature_types", communityId.toString()], + emailPlanners: (communityId: number): string[] => ["emailplanners", communityId.toString()], }; export default RQKeys; diff --git a/assets/sass/components/autocomplete.scss b/assets/sass/components/autocomplete.scss index ba164391..69a31d1b 100644 --- a/assets/sass/components/autocomplete.scss +++ b/assets/sass/components/autocomplete.scss @@ -1,4 +1,4 @@ .MuiAutocomplete-option { - padding: 0; + padding: 0.25rem; height: 1.25rem; } diff --git a/assets/sass/pages/espaceco/community.scss b/assets/sass/pages/espaceco/community.scss index 57d63316..ce42c311 100644 --- a/assets/sass/pages/espaceco/community.scss +++ b/assets/sass/pages/espaceco/community.scss @@ -2,8 +2,43 @@ background-color: var(--background-alt-grey); } -.frx-community-desc-documents { +/*.frx-community-desc-documents { padding: 0.4rem; border: 1px solid; border-color: var(--border-default-grey); +}*/ + +.frx-document-tile { + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid; + border-color: var(--border-default-grey); + background-color: var(--background-contrast-grey); + padding: 1rem; + margin: 0.5rem; + gap: 0.4rem; + justify-content: space-between; + img { + width: 48px; + height: 48px; + max-width: 48px; + max-height: 48px; + } +} + +.frx-document-image { + width: 48px; + height: 48px; + max-width: 48px; + max-height: 48px; +} + +.frx-image-modal { + height: 64px; + width: 64px; + img { + max-height: 64px; + max-width: 64px; + } } diff --git a/src/Controller/EspaceCo/CommunityController.php b/src/Controller/EspaceCo/CommunityController.php index da3a426b..f82b7f18 100644 --- a/src/Controller/EspaceCo/CommunityController.php +++ b/src/Controller/EspaceCo/CommunityController.php @@ -8,10 +8,13 @@ use App\Services\EspaceCoApi\CommunityApiService; use App\Services\EspaceCoApi\UserApiService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Uid\Uuid; #[Route( '/api/espaceco/community', @@ -23,10 +26,15 @@ class CommunityController extends AbstractController implements ApiControllerInt { public const SEARCH_LIMIT = 20; + private string $varDataPath; + public function __construct( + ParameterBagInterface $parameters, + private Filesystem $fs, private CommunityApiService $communityApiService, private UserApiService $userApiService ) { + $this->varDataPath = $parameters->get('upload_path'); } #[Route('/get', name: 'get', methods: ['GET'])] @@ -121,11 +129,14 @@ public function search( } } + /** + * @param array $fields + */ #[Route('/{communityId}', name: 'get_community', methods: ['GET'])] - public function getCommunity(int $communityId): JsonResponse + public function getCommunity(int $communityId, #[MapQueryParameter] ?array $fields = []): JsonResponse { try { - $response = $this->communityApiService->getCommunity($communityId); + $response = $this->communityApiService->getCommunity($communityId, $fields); return new JsonResponse($response); } catch (ApiException $ex) { @@ -172,16 +183,36 @@ public function updateMemberGrids(int $communityId, int $userId, Request $reques return new JsonResponse($member); } - #[Route('/{communityId}/update_logo', name: 'update_logo', methods: ['PATCH'])] + #[Route('/{communityId}/update_logo', name: 'update_logo', methods: ['POST'])] public function updateLogo(int $communityId, Request $request): JsonResponse { try { - $community = $this->communityApiService->getCommunity($communityId); - $logo = $request->files->get('logo'); - $this->communityApiService->updateLogo($communityId, $logo); - return new JsonResponse($community); + $uuid = Uuid::v4(); + $tempFileDir = join(DIRECTORY_SEPARATOR, [$this->varDataPath, $uuid]); + $tempFilePath = join(DIRECTORY_SEPARATOR, [$tempFileDir, $logo->getClientOriginalName()]); + + $logo->move($tempFileDir, $logo->getClientOriginalName()); + + $this->communityApiService->updateLogo($communityId, $tempFilePath); + $this->fs->remove($tempFileDir); + + $community = $this->communityApiService->getCommunity($communityId, ['fields' => ['logo_url']]); + + return new JsonResponse(['logo_url' => $community['logo_url']]); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/{communityId}/remove_logo', name: 'remove_logo', methods: ['DELETE'])] + public function removeLogo(int $communityId): JsonResponse + { + try { + $this->communityApiService->removeLogo($communityId); + + return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT); } catch (ApiException $ex) { throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); } diff --git a/src/Controller/EspaceCo/CommunityDocumentController.php b/src/Controller/EspaceCo/CommunityDocumentController.php new file mode 100644 index 00000000..2d545e63 --- /dev/null +++ b/src/Controller/EspaceCo/CommunityDocumentController.php @@ -0,0 +1,98 @@ + true], + condition: 'request.isXmlHttpRequest()' +)] +class CommunityDocumentController extends AbstractController implements ApiControllerInterface +{ + private string $varDataPath; + + public function __construct( + ParameterBagInterface $parameters, + private Filesystem $fs, + private CommunityDocumentApiService $communityDocumentApiService, + ) { + $this->varDataPath = $parameters->get('upload_path'); + } + + /** + * @param array $fields + */ + #[Route('/get_all', name: 'get_all', methods: ['GET'])] + public function getAll( + int $communityId, + #[MapQueryParameter] ?array $fields = [], + ): JsonResponse { + try { + $response = $this->communityDocumentApiService->getDocuments($communityId, $fields); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/add', name: 'add', methods: ['POST'])] + public function addDocument(int $communityId, Request $request): JsonResponse + { + try { + $title = $request->request->get('title'); + $description = $request->request->get('description'); + + $document = $request->files->get('document'); + + $uuid = Uuid::v4(); + $tempFileDir = join(DIRECTORY_SEPARATOR, [$this->varDataPath, $uuid]); + $tempFilePath = join(DIRECTORY_SEPARATOR, [$tempFileDir, $document->getClientOriginalName()]); + + $document->move($tempFileDir, $document->getClientOriginalName()); + + $response = $this->communityDocumentApiService->addDocument($communityId, $title, $description, $tempFilePath); + $this->fs->remove($tempFileDir); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/update/{documentId}', name: 'update', methods: ['PATCH'])] + public function updateDocument(int $communityId, int $documentId, Request $request): JsonResponse + { + try { + $data = json_decode($request->getContent(), true); + + $response = $this->communityDocumentApiService->updateDocument($communityId, $documentId, $data); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/delete/{documentId}', name: 'delete', methods: ['DELETE'])] + public function deleteDocument(int $communityId, int $documentId): JsonResponse + { + $this->communityDocumentApiService->deleteDocument($communityId, $documentId); + + return new JsonResponse(null, JsonResponse::HTTP_NO_CONTENT); + } +} diff --git a/src/Controller/EspaceCo/EmailPlannerController.php b/src/Controller/EspaceCo/EmailPlannerController.php index d32c8704..f6d56b6e 100644 --- a/src/Controller/EspaceCo/EmailPlannerController.php +++ b/src/Controller/EspaceCo/EmailPlannerController.php @@ -8,6 +8,7 @@ use App\Services\EspaceCoApi\EmailPlannerApiService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; #[Route( @@ -37,6 +38,19 @@ public function get(int $communityId): JsonResponse } } + #[Route('/{communityId}', name: 'add', methods: ['POST'])] + public function add(int $communityId, Request $request): JsonResponse + { + try { + $data = json_decode($request->getContent(), true); + $response = $this->emailPlannerApiService->add($communityId, $data); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + #[Route('/{communityId}/remove/{emailPlannerId}', name: 'remove', methods: ['DELETE'])] public function removeEmailPlanners(int $communityId, int $emailPlannerId): JsonResponse { diff --git a/src/Services/EspaceCoApi/CommunityApiService.php b/src/Services/EspaceCoApi/CommunityApiService.php index 6677e3a6..152e2b0b 100644 --- a/src/Services/EspaceCoApi/CommunityApiService.php +++ b/src/Services/EspaceCoApi/CommunityApiService.php @@ -5,7 +5,6 @@ use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -48,11 +47,13 @@ public function getCommunitiesName(): array } /** + * @param array $fields + * * @return array */ - public function getCommunity(int $communityId): array + public function getCommunity(int $communityId, array $fields = []): array { - return $this->request('GET', "communities/$communityId"); + return $this->request('GET', "communities/$communityId", [], ['fields' => $fields]); } /** @@ -114,8 +115,13 @@ public function removeMember(int $communityId, int $userId): array return $this->request('DELETE', "communities/$communityId/members/$userId"); } - public function updateLogo(int $communityId, UploadedFile $file): array + public function updateLogo(int $communityId, string $filePath): array + { + return $this->sendFile('POST', "communities/$communityId/logo", $filePath, [], [], 'logo'); + } + + public function removeLogo(int $communityId): array { - return $this->request('PATCH', "communities/$communityId", ['logo' => $file], [], [], true); + return $this->request('DELETE', "communities/$communityId/logo"); } } diff --git a/src/Services/EspaceCoApi/CommunityDocumentApiService.php b/src/Services/EspaceCoApi/CommunityDocumentApiService.php new file mode 100644 index 00000000..5b7cc41d --- /dev/null +++ b/src/Services/EspaceCoApi/CommunityDocumentApiService.php @@ -0,0 +1,59 @@ + $fields + */ + public function getDocuments(int $communityId, array $fields = []): array + { + return $this->request('GET', "communities/$communityId/documents", ['fields' => $fields]); + } + + public function addDocument(int $communityId, string $title, string $description, string $tempFilePath): array + { + $formFields = [ + 'title' => $title, + 'description' => $description, + ]; + $formFields['document'] = DataPart::fromPath($tempFilePath); + + $formData = new FormDataPart($formFields); + $body = $formData->bodyToIterable(); + $headers = $formData->getPreparedHeaders()->toArray(); + + return $this->request('POST', "communities/$communityId/documents", $body, [], $headers, true); + } + + /** + * @param array $data + */ + public function updateDocument(int $communityId, int $documentId, array $data): array + { + return $this->request('PATCH', "communities/$communityId/documents/$documentId", $data); + } + + public function deleteDocument(int $communityId, int $documentId): array + { + return $this->request('DELETE', "communities/$communityId/documents/$documentId"); + } +} diff --git a/src/Services/EspaceCoApi/EmailPlannerApiService.php b/src/Services/EspaceCoApi/EmailPlannerApiService.php index d49958a3..e864e194 100644 --- a/src/Services/EspaceCoApi/EmailPlannerApiService.php +++ b/src/Services/EspaceCoApi/EmailPlannerApiService.php @@ -27,6 +27,14 @@ public function getAll(int $communityId): array return $this->requestAll("communities/$communityId/emailplanners"); } + /** + * @param array $data + */ + public function add(int $communityId, array $data): array + { + return $this->request('POST', "communities/$communityId/emailplanners", $data); + } + public function remove(int $communityId, int $emailPlannerId): array { return $this->request('DELETE', "communities/$communityId/emailplanners/$emailPlannerId");