diff --git a/Topic_category_cartesgouv.xlsx b/Topic_category_cartesgouv.xlsx new file mode 100644 index 00000000..79dae27c Binary files /dev/null and b/Topic_category_cartesgouv.xlsx differ diff --git a/assets/@types/espaceco.ts b/assets/@types/espaceco.ts index 76510cee..b9c17eef 100644 --- a/assets/@types/espaceco.ts +++ b/assets/@types/espaceco.ts @@ -9,20 +9,22 @@ export interface ConstraintsDTO { } export const AttributeTypes = ["text", "integer", "double", "checkbox", "list", "date"]; + export type AttributeType = (typeof AttributeTypes)[number]; -export interface AttributeDTO { +export type AttributeDTO = { name: string; type: AttributeType; - default?: string; + default?: string | null; mandatory?: boolean; - values?: string[]; - help?: string; + multiple?: boolean; + values?: string[] | null; + help?: string | null; title?: string; - input_constraints?: ConstraintsDTO; - json_schema?: object; + input_constraints?: ConstraintsDTO | null; + json_schema?: object | null; required?: boolean; condition_field?: string; -} +}; export interface ThemeDTO { theme: string; @@ -33,28 +35,38 @@ export interface ThemeDTO { global?: boolean; } -export type ReportStatuses = keyof typeof statuses; -export type ReportStatusesDTO = Record< - ReportStatuses, - { - wording: string; - help?: string; - } ->; - -export type ReportStatusesDTO2 = { - status: ReportStatuses; - wording: string; - help?: string; -}[]; +export type UserSharedThemesDTO = { + community_id: number; + community_name: string; + themes: ThemeDTO[]; +}; + +export type SharedThemesDTO = { + community_id: number; + community_name: string; + themes: string[]; +}; + +export type ReportStatusesType = keyof typeof statuses; +export type ReportStatusParams = { + title: string; + description?: string; + active: boolean; +}; +export type ReportStatusesDTO = Record; + +const SharedGeoremOptions = ["all", "restrained", "personal"]; +export type SharedGeorem = (typeof SharedGeoremOptions)[number]; export interface CommunityResponseDTO { id: number; description: string | null; detailed_description?: string | null; name: string; active: boolean; + listed: boolean; shared_georem: "all" | "restrained" | "personal"; + shared_extractions: boolean; email: string | null; attributes: ThemeDTO[]; default_comment: string | null; @@ -67,14 +79,14 @@ export interface CommunityResponseDTO { open_without_affiliation: boolean; open_with_email?: string[]; offline_allowed: boolean; - shared_extractions: boolean; /** @format date-time */ creation: string; grids: Grid[]; logo_url: string | null; keywords?: string[]; documents?: DocumentDTO[]; - report_statuses?: ReportStatusesDTO2; + report_statuses?: ReportStatusesDTO; + shared_themes?: SharedThemesDTO[]; } export interface DocumentDTO { @@ -176,5 +188,10 @@ export interface TableResponseDTO { export type ReportFormType = { attributes: ThemeDTO[]; - report_statuses?: ReportStatusesDTO2; + report_statuses: ReportStatusesDTO; + shared_themes?: SharedThemesDTO[]; + shared_georem: SharedGeorem; + all_members_can_valid: boolean; }; + +export { SharedGeoremOptions }; diff --git a/assets/data/report_statuses.json b/assets/data/report_statuses.json index f636add4..73121f2c 100644 --- a/assets/data/report_statuses.json +++ b/assets/data/report_statuses.json @@ -7,6 +7,5 @@ "valid": "Pris en compte", "valid0": "Déjà pris en compte", "reject": "Rejeté (hors spéc.)", - "reject0": "Rejeté (hors de propos)", - "test": "En mode test" + "reject0": "Rejeté (hors de propos)" } \ No newline at end of file diff --git a/assets/entrepot/pages/dashboard/DashboardPro.tsx b/assets/entrepot/pages/dashboard/DashboardPro.tsx index 7c8cf94c..4a77bf86 100644 --- a/assets/entrepot/pages/dashboard/DashboardPro.tsx +++ b/assets/entrepot/pages/dashboard/DashboardPro.tsx @@ -1,5 +1,4 @@ import { fr } from "@codegouvfr/react-dsfr"; -import Button from "@codegouvfr/react-dsfr/Button"; import { Tile } from "@codegouvfr/react-dsfr/Tile"; import { useMutation, useQuery } from "@tanstack/react-query"; import { declareComponentKeys } from "i18nifty"; @@ -19,6 +18,7 @@ import { useAuthStore } from "../../../stores/AuthStore"; import api from "../../api"; import avatarSvgUrl from "@codegouvfr/react-dsfr/dsfr/artwork/pictograms/digital/avatar.svg"; +import internetSvgUrl from "@codegouvfr/react-dsfr/dsfr/artwork/pictograms/digital/internet.svg"; import mailSendSvgUrl from "@codegouvfr/react-dsfr/dsfr/artwork/pictograms/digital/mail-send.svg"; import humanCoopSvgUrl from "@codegouvfr/react-dsfr/dsfr/artwork/pictograms/environment/human-cooperation.svg"; import padlockSvgUrl from "@codegouvfr/react-dsfr/dsfr/artwork/pictograms/system/padlock.svg"; @@ -175,8 +175,16 @@ const DashboardPro = () => { {isApiEspaceCoDefined() && ( -
- +
+
+ +
)} diff --git a/assets/espaceco/api/index.ts b/assets/espaceco/api/index.ts index 590cda61..8d8192ee 100644 --- a/assets/espaceco/api/index.ts +++ b/assets/espaceco/api/index.ts @@ -1,8 +1,10 @@ import community from "./community"; import grid from "./grid"; import permission from "./permission"; +import user from "./users"; const api = { + user, community, permission, grid, diff --git a/assets/espaceco/api/users.ts b/assets/espaceco/api/users.ts new file mode 100644 index 00000000..2e950b7c --- /dev/null +++ b/assets/espaceco/api/users.ts @@ -0,0 +1,12 @@ +import { UserSharedThemesDTO } from "../../@types/espaceco"; +import { jsonFetch } from "../../modules/jsonFetch"; +import SymfonyRouting from "../../modules/Routing"; + +const getSharedThemes = () => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_user_shared_themes"); + return jsonFetch(url); +}; + +const user = { getSharedThemes }; + +export default user; diff --git a/assets/espaceco/pages/communities/Communities.tsx b/assets/espaceco/pages/communities/Communities.tsx index 60205a31..ed83d0a3 100644 --- a/assets/espaceco/pages/communities/Communities.tsx +++ b/assets/espaceco/pages/communities/Communities.tsx @@ -31,6 +31,7 @@ type QueryParamsType = { const Communities: FC = () => { const route = useRoute(); const { t } = useTranslation("CommunityList"); + const { t: tBreadcrumb } = useTranslation("Breadcrumb"); const filter = useMemo(() => { const f = route.params["filter"]; @@ -71,7 +72,15 @@ const Communities: FC = () => { }; return ( - +

{t("title")}

{communityQuery.isError && } diff --git a/assets/espaceco/pages/communities/ManageCommunity.tsx b/assets/espaceco/pages/communities/ManageCommunity.tsx index 451d41bd..b31a3685 100644 --- a/assets/espaceco/pages/communities/ManageCommunity.tsx +++ b/assets/espaceco/pages/communities/ManageCommunity.tsx @@ -15,9 +15,9 @@ import { routes } from "../../../router/router"; import api from "../../api"; import Description from "./management/Description"; import Grid from "./management/Grid"; -import ZoomAndCentering from "./management/ZoomAndCentering"; import Layer from "./management/Layer"; import Reports from "./management/Reports"; +import ZoomAndCentering from "./management/ZoomAndCentering"; type ManageCommunityProps = { communityId: number; @@ -27,6 +27,7 @@ const navItems = datastoreNavItems(); const ManageCommunity: FC = ({ communityId }) => { const { t } = useTranslation("ManageCommunity"); + const { t: tBreadcrumb } = useTranslation("Breadcrumb"); const communityQuery = useQuery({ queryKey: RQKeys.community(communityId), @@ -37,7 +38,18 @@ const ManageCommunity: FC = ({ communityId }) => { const [selectedTabId, setSelectedTabId] = useState("tab1"); return ( - +

{t("title", { name: communityQuery.data?.name })}

{communityQuery.isError ? ( = ({ communityId }) => { ) : communityQuery.isLoading ? ( ) : ( - communityQuery.data !== undefined && ( + communityQuery.data && (
()("ManageCommunity"); @@ -66,6 +76,7 @@ export const { i18n } = declareComponentKeys< 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 ...", + loading_tables: "Recherche des tables pour la configuration des thèmes en cours ...", fetch_failed: "La récupération des informations sur le guichet a échoué", back_to_list: "Retour à la liste des guichets", tab1: "Description", @@ -129,9 +140,31 @@ export const ManageCommunityFrTranslations: Translations<"fr">["ManageCommunity" "report.configure_themes": "Configurer les thèmes et attributs des signalements (optionnel)", "report.configure_themes.explain": "Afin de permettre aux membres de votre groupe de soumettre des signalements sur d'autres thématiques que celles IGN (Adresse, Bâti, Points d'intérêts...), vous pouvez ajouter vos propres thèmes et personnaliser le formulaire de saisie d'un nouveau signalement pour l'adapter à vos besoins métier. Les membres de votre groupe verront ces thèmes, en plus ou à la place des thèmes IGN, sur l'interface de saisie d'un nouveau signalement sur l'espace collaboratif, les plugins SIG et l'application mobile.", + "report.configure_shared_themes": "Afficher des thèmes partagés (optionnel)", + "report.configure_shared_themes.explain": "Vous pouvez également choisir des thèmes partagés qui apparaitront sur ce guichet.", "report.configure_statuses": "Paramétrer les status des signalements (optionnel)", "report.configure_statuses.explain": - "Vous pouvez supprimer un maximum de 2 status en les décochant, changer le nom des status et ajouter une explication des status pour améliorer la compréhension de vos utilisateurs.", + "Vous pouvez supprimer un maximum de 2 status en les décochant, changer leur nom et ajouter une explication pour améliorer la compréhension de vos utilisateurs.", + "report.manage_permissions": "Gérer les permissions (optionnel)", + "report.manage_permissions.shared_report": "Partage des signalements", + "report.manage_permissions.shared_report_hint": + "Vous pouvez déterminer quels utilisateurs ont accès aux signalements du groupe. Choisissez si les signalements du groupe sont :", + "report.manage_permissions.shared_report.option": ({ option }) => { + switch (option) { + case "all": + return "Visibles de tout le monde"; + case "restrained": + return "Visibles uniquement des membres du guichet"; + case "personal": + return "Visibles uniquement de leur auteur et des gestionnaires du guichet"; + default: + return ""; + } + }, + "report.manage_permissions.report_answers": "Réponses aux signalements", + "report.manage_permissions.authorize": "Autoriser", + "report.manage_permissions.authorize_hint": + "Tous les membres d'un groupe peuvent répondre aux signalements le concernant mais seuls les gestionnaires peuvent valider ces réponses et donc clore les signalements. En cochant la case suivante vous autorisez tous les membres de ce groupe à apporter des réponses sans validation.", "grid.grids": "Emprises du guichet (optionnel)", "grid.explain": (

@@ -145,6 +178,7 @@ export const ManageCommunityFrTranslations: Translations<"fr">["ManageCommunity" export const ManageCommunityEnTranslations: Translations<"en">["ManageCommunity"] = { title: ({ name }) => (name === undefined ? "Manage front office" : `Manage front office - ${name}`), loading: undefined, + loading_tables: undefined, fetch_failed: undefined, back_to_list: undefined, tab1: undefined, @@ -204,8 +238,19 @@ export const ManageCommunityEnTranslations: Translations<"en">["ManageCommunity" "layer.tab3": "Base maps", "report.configure_themes": undefined, "report.configure_themes.explain": undefined, + "report.configure_shared_themes": undefined, + "report.configure_shared_themes.explain": undefined, "report.configure_statuses": undefined, "report.configure_statuses.explain": undefined, + "report.manage_permissions": undefined, + "report.manage_permissions.shared_report": undefined, + "report.manage_permissions.shared_report_hint": undefined, + "report.manage_permissions.shared_report.option": ({ option }) => { + return `${option}`; + }, + "report.manage_permissions.report_answers": undefined, + "report.manage_permissions.authorize": undefined, + "report.manage_permissions.authorize_hint": undefined, "grid.grids": undefined, "grid.explain": undefined, }; diff --git a/assets/espaceco/pages/communities/management/GridList.tsx b/assets/espaceco/pages/communities/management/GridList.tsx index e5ea74a4..09a82504 100644 --- a/assets/espaceco/pages/communities/management/GridList.tsx +++ b/assets/espaceco/pages/communities/management/GridList.tsx @@ -12,6 +12,7 @@ type GridListProps = { const GridList: FC = ({ grids = [], onChange }) => { const [grid, setGrid] = useState(null); + console.log(grid); const [internal, setInternal] = useState([...grids]); @@ -40,13 +41,13 @@ const GridList: FC = ({ grids = [], onChange }) => { }, [internal, handleRemove]); return ( - <> +

setGrid(grid)} /> @@ -57,8 +58,8 @@ const GridList: FC = ({ grids = [], onChange }) => {
- - +
+ ); }; diff --git a/assets/espaceco/pages/communities/management/Reports.tsx b/assets/espaceco/pages/communities/management/Reports.tsx index d13ed983..28e62f9b 100644 --- a/assets/espaceco/pages/communities/management/Reports.tsx +++ b/assets/espaceco/pages/communities/management/Reports.tsx @@ -1,38 +1,124 @@ +import { fr } from "@codegouvfr/react-dsfr"; import Alert from "@codegouvfr/react-dsfr/Alert"; +import Button from "@codegouvfr/react-dsfr/Button"; import { yupResolver } from "@hookform/resolvers/yup"; import { useQuery } from "@tanstack/react-query"; -import { FC } from "react"; +import { FC, useMemo } from "react"; import { useForm } from "react-hook-form"; import * as yup from "yup"; -import { CommunityResponseDTO, ReportFormType, TableResponseDTO } from "../../../../@types/espaceco"; +import { + CommunityResponseDTO, + ReportFormType, + ReportStatusesType, + SharedGeoremOptions, + SharedThemesDTO, + TableResponseDTO, + UserSharedThemesDTO, +} from "../../../../@types/espaceco"; +import LoadingText from "../../../../components/Utils/LoadingText"; +import statuses from "../../../../data/report_statuses.json"; +import { useTranslation } from "../../../../i18n/i18n"; import RQKeys from "../../../../modules/espaceco/RQKeys"; import { CartesApiException } from "../../../../modules/jsonFetch"; import api from "../../../api"; +import Permissions from "./reports/Permissions"; import ReportStatuses from "./reports/ReportStatuses"; +import type { UserSharedThemesType } from "./reports/SetSharedThemesDialog"; +import SharedThemes from "./reports/SharedThemes"; import ThemeList from "./reports/ThemeList"; -import getDefaultStatuses from "./reports/Utils"; -import Wait from "../../../../components/Utils/Wait"; -import { fr } from "@codegouvfr/react-dsfr"; -import LoadingText from "../../../../components/Utils/LoadingText"; -import { useTranslation } from "../../../../i18n/i18n"; +import { countActiveStatus, getDefaultStatuses, getMinAuthorizedStatus } from "./reports/Utils"; +import Answers from "./reports/Answers"; type ReportsProps = { community: CommunityResponseDTO; }; +const minStatuses = getMinAuthorizedStatus(); + const Reports: FC = ({ community }) => { const { t: tCommon } = useTranslation("Common"); + const { t: tStatus } = useTranslation("ReportStatuses"); + const { t } = useTranslation("ManageCommunity"); - /*const schema = yup.object({ - attributes: yup.array().of(yup.object()).required(), - report_statuses: yup.array().of( + const schema: yup.ObjectSchema = yup.object({ + attributes: yup + .array() + .of( + yup.object({ + theme: yup.string().required(), + database: yup.string(), + table: yup.string(), + attributes: yup + .array() + .of( + yup.object({ + name: yup.string().required(), + type: yup.string().required(), + default: yup.string().nullable(), + mandatory: yup.boolean(), + multiple: yup.boolean(), + values: yup + .array() + .test({ + name: "check-values", + test: (list) => { + if (!list) return true; + for (const element of list) { + if (element !== null && typeof element !== "string") return false; + } + return true; + }, + }) + .nullable(), + help: yup.string().nullable(), + title: yup.string(), + input_constraints: yup + .object({ + minLength: yup.number(), + minValue: yup.string(), + maxValue: yup.string(), + pattern: yup.string(), + }) + .nullable(), + json_schema: yup.object().nullable(), + required: yup.boolean(), + condition_field: yup.string(), + }) + ) + .required(), + }) + ) + .required(), + report_statuses: yup.lazy(() => { + const rs = {}; + Object.keys(statuses).forEach((status) => { + const s = status as ReportStatusesType; + rs[s] = yup.object({ + title: yup.string().required(), + description: yup.string().nullable(), + active: yup.boolean().required(), + }); + }); + return yup + .object() + .shape(rs) + .test("minStatuses", tStatus("min_statuses"), (statuses) => { + if (!statuses) return false; + const c = countActiveStatus(statuses); + return c >= minStatuses; + }) + .required(); + }), + shared_themes: yup.array().of( yup.object({ - status: yup.string().required(), - wording: yup.string().required(), - help: yup.string(), + community_id: yup.number().required(), + community_name: yup.string().required(), + themes: yup.array().of(yup.string().required()).required(), }) - ) - });*/ + ), + shared_georem: yup.string().oneOf(SharedGeoremOptions).required(), + all_members_can_valid: yup.boolean().required(), + }); const tablesQuery = useQuery[], CartesApiException>({ queryKey: RQKeys.tables(community.id), @@ -40,29 +126,93 @@ const Reports: FC = ({ community }) => { staleTime: 60000, }); + const sharedThemesQuery = useQuery({ + queryKey: RQKeys.user_shared_themes(), + queryFn: () => api.user.getSharedThemes(), + staleTime: 3600000, + }); + + // Filtrage des themes partages qui sont déjà dans la communauté + const userSharedThemes = useMemo(() => { + if (sharedThemesQuery.data) { + const communities = sharedThemesQuery.data.filter((sht) => { + return sht.community_id !== community.id; + }); + const ret: UserSharedThemesType = {}; + communities.forEach((comm) => { + const themes = Array.from(comm.themes, (t) => t.theme); + ret[comm.community_id] = { communityName: comm.community_name, themes: themes }; + }); + return ret; + } + return {}; + }, [community, sharedThemesQuery.data]); + + /** + * On regarde la conformité entre les thèmes partagés de l'utilisateur et les thèmes + * partagés de la communauté + */ + const sharedThemes = useMemo(() => { + const shared = community.shared_themes ?? []; + + const ret: SharedThemesDTO[] = []; + if (userSharedThemes) { + shared + .filter((s) => s.community_id in userSharedThemes) + .forEach((s) => { + const themes = s.themes.filter((theme) => userSharedThemes[s.community_id].themes.indexOf(theme) >= 0); + if (themes.length) { + ret.push({ ...s, themes: themes }); + } + }); + } + return ret; + }, [community, userSharedThemes]); + const form = useForm({ - // resolver: yupResolver(schema), + resolver: yupResolver(schema), mode: "onChange", values: { attributes: community.attributes ?? [], - report_statuses: getDefaultStatuses(), + report_statuses: community.report_statuses ?? getDefaultStatuses(), + shared_themes: sharedThemes, + shared_georem: community.shared_georem, + all_members_can_valid: community.all_members_can_valid, }, }); + const { + handleSubmit, + getValues: getFormValues, + formState: { errors }, + } = form; + + const onSubmit = () => { + console.log(getFormValues()); + }; + return (
- {tablesQuery.isError ? ( - - ) : tablesQuery.isLoading ? ( - -
- -
-
- ) : ( + {tablesQuery.isError && } + {sharedThemesQuery.isError && } + {tablesQuery.isLoading && } + {sharedThemesQuery.isLoading && } + {tablesQuery.data && sharedThemesQuery.data && (
- - + + + + + +
+ +
)}
diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx index 8b5bdb5b..ef1e0167 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx @@ -23,7 +23,7 @@ type formType = { zoom: number; zoomMin: number; zoomMax: number; - extent?: Extent | null; + extent: Extent | null; }; const ZoomAndCentering: FC = ({ community }) => { diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx index edb8254e..811eb68b 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx @@ -3,16 +3,14 @@ import Input from "@codegouvfr/react-dsfr/Input"; 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 { Extent } from "ol/extent"; import { FC, useState } from "react"; import { createPortal } from "react-dom"; import { useForm } from "react-hook-form"; import * as yup from "yup"; -// import { SearchGridFilters } from "../../../../../@types/app_espaceco"; -import Skeleton from "../../../../../components/Utils/Skeleton"; +import { SearchGridFilters } from "../../../../../@types/app_espaceco"; import { useTranslation } from "../../../../../i18n/i18n"; -import { Extent } from "ol/extent"; -// import SearchGrids from "./SearchGrids"; +import SearchGrids from "./SearchGrids"; type ExtentDialogProps = { onCancel: () => void; @@ -27,11 +25,10 @@ const ExtentDialogModal = createModal({ type SearchOption = "autocomplete" | "manual"; type FieldName = "xmin" | "xmax" | "ymin" | "ymax"; -/* const filters: SearchGridFilters = { - searchBy: ["name", "title"], +const filters: SearchGridFilters = { fields: ["name", "title", "extent"], adm: true, -}; */ +}; const transform = (value, origin) => (origin === "" ? undefined : value); @@ -55,7 +52,10 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { message: tValid("zoom.f1_less_than_f2", { field1: "xmin", field2: "xmax" }), test: (value, context) => { const xmax = context.parent.xmax; - return xmax !== undefined ? value < xmax : true; + if (value) { + return xmax !== undefined ? value < xmax : true; + } + return true; }, }), ymin: yup @@ -112,7 +112,9 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { const { register, getValues: getFormValues, + setValue: setFormValue, formState: { errors }, + clearErrors, handleSubmit, resetField, } = form; @@ -127,6 +129,8 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { }; const onSubmit = () => { + ExtentDialogModal.close(); + const values = getFormValues(); onApply([values.xmin, values.ymin, values.xmax, values.ymax]); setChoice("manual"); @@ -141,7 +145,7 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { buttons={[ { children: tCommon("cancel"), - doClosesModal: false, + doClosesModal: true, onClick: () => { setChoice("manual"); clear(); @@ -176,51 +180,57 @@ const ExtentDialog: FC = ({ onCancel, onApply }) => { }, ]} /> - {/* TODO DECOMMENTER ET METTRE A LA PLACE DE Skeleton CI-DESSOUS + {choice === "autocomplete" ? ( { - console.log(extent); + onChange={(grid) => { + if (grid) { + setFormValue("xmin", grid.extent?.[0]); + setFormValue("ymin", grid.extent?.[1]); + setFormValue("xmax", grid.extent?.[2]); + setFormValue("ymax", grid.extent?.[3]); + } else clear(); + clearErrors(); }} - /> */} - {choice === "autocomplete" ? ( - + /> ) : ( -
- -
-
- - -
-
- - -
-
-
+ )} +
+
+ + +
+
+ + +
+
, document.body diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx index aef50597..035c820d 100644 --- a/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx @@ -1,6 +1,6 @@ import { fr } from "@codegouvfr/react-dsfr"; import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; -import Autocomplete from "@mui/material/Autocomplete"; +import Autocomplete, { autocompleteClasses } from "@mui/material/Autocomplete"; import { useQuery } from "@tanstack/react-query"; import { FC, ReactNode } from "react"; import { useDebounceValue } from "usehooks-ts"; @@ -10,6 +10,7 @@ import { useTranslation } from "../../../../../i18n/i18n"; import RQKeys from "../../../../../modules/espaceco/RQKeys"; import api from "../../../../api"; import TextField from "@mui/material/TextField"; +import Box from "@mui/material/Box"; export type SearchGridsProps = { label: ReactNode; @@ -34,12 +35,33 @@ const SearchGrids: FC = ({ label, hintText, filters, onChange return (
-
); }; diff --git a/assets/espaceco/pages/communities/management/reports/AttributeValidations.tsx b/assets/espaceco/pages/communities/management/reports/AttributeValidations.tsx index df155883..7be9416c 100644 --- a/assets/espaceco/pages/communities/management/reports/AttributeValidations.tsx +++ b/assets/espaceco/pages/communities/management/reports/AttributeValidations.tsx @@ -11,8 +11,8 @@ class AttributeValidations { this.#context = context; } - validateValue = (value: string | undefined) => { - if (value === undefined) return true; + validateValue = (value?: string | null) => { + if (value === undefined || value === null) return true; const { parent: { type }, @@ -77,7 +77,7 @@ class AttributeValidations { }; } -const validateList = (value: string | undefined, context: yup.TestContext): yup.ValidationError | boolean => { +const validateList = (value: string | null | undefined, context: yup.TestContext): yup.ValidationError | boolean => { const { parent: { type }, } = context; diff --git a/assets/espaceco/pages/communities/management/reports/EditAttributeDialog.tsx b/assets/espaceco/pages/communities/management/reports/EditAttributeDialog.tsx new file mode 100644 index 00000000..43796fba --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/EditAttributeDialog.tsx @@ -0,0 +1,188 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Input from "@codegouvfr/react-dsfr/Input"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FC, useMemo } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { AttributeDTO, ThemeDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { AttributeValidations, validateList } from "./AttributeValidations"; +import { AddOrEditAttributeFormType, getInputType, normalizeAttribute } from "./ThemeUtils"; + +type EditAttributeDialogProps = { + modal: ReturnType; + theme: ThemeDTO; + attribute: AttributeDTO; + onModify: (newAttribute: AttributeDTO) => void; +}; + +const EditAttributeDialog: FC = ({ modal, theme, attribute, onModify }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("Theme"); + + const attributeNames: string[] = useMemo(() => { + return Array.from( + theme.attributes.filter((a) => a.name !== attribute.name), + (a) => a.name + ); + }, [theme, attribute]); + + const schema = yup.lazy(() => { + const s = { + name: yup + .string() + .trim(t("trimmed_error")) + .strict(true) + .required(t("dialog.edit_attribute.name_mandatory_error")) + .test("is-unique", t("dialog.edit_attribute.name_unique_error"), (value) => { + const v = value.trim(); + return !attributeNames.includes(v); + }), + type: yup.string().required(), + mandatory: yup.boolean(), + default: yup + .string() + .nullable() + .test({ + name: "check-value", + test: (value, context) => { + const validator = new AttributeValidations(context); + return validator.validateValue(value); + }, + }), + help: yup.string().nullable(), + }; + if (attribute.type === "list") { + s["values"] = yup.string().test({ + name: "check-values", + test: (value, context) => { + return validateList(value, context); + }, + }); + s["multiple"] = yup.boolean(); + } + return yup.object().shape(s); + }); + + const { + register, + watch, + formState: { errors }, + getValues: getFormValues, + setValue: setFormValue, + handleSubmit, + } = useForm({ + mode: "onSubmit", + values: { + name: attribute.name, + type: attribute.type, + mandatory: attribute.mandatory, + values: attribute.values ? attribute.values.join("|") : null, + default: attribute.default, + multiple: attribute.multiple, + help: attribute.help, + }, + resolver: yupResolver(schema), + }); + + const onSubmit = () => { + modal.close(); + onModify(normalizeAttribute(getFormValues())); + }; + + const mandatory = watch("mandatory"); + const multiple = watch("multiple"); + + return ( + <> + {createPortal( + { + modal.close(); + }, + }, + { + priority: "primary", + children: tCommon("modify"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + }, + ]} + > +
+

{tCommon("mandatory_fields")}

+ + { + setFormValue("mandatory", checked); + }} + /> + {attribute.type === "list" && ( + <> + + { + setFormValue("multiple", checked); + }} + /> + + )} + + +
+
, + document.body + )} + + ); +}; + +export default EditAttributeDialog; diff --git a/assets/espaceco/pages/communities/management/reports/EditReportStatusDialog.tsx b/assets/espaceco/pages/communities/management/reports/EditReportStatusDialog.tsx new file mode 100644 index 00000000..6c6f2d74 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/EditReportStatusDialog.tsx @@ -0,0 +1,107 @@ +import Button from "@codegouvfr/react-dsfr/Button"; +import Input from "@codegouvfr/react-dsfr/Input"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FC } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { ReportStatusesType, ReportStatusParams } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { getDefaultStatuses } from "./Utils"; + +const EditReportParameterModal = createModal({ + id: "status-modal", + isOpenedByDefault: false, +}); + +const defaultStatuses = getDefaultStatuses(); + +type EditReportStatusDialogProps = { + status?: ReportStatusesType; + statusParams?: ReportStatusParams; + onModify: (values: Omit) => void; +}; + +const EditReportStatusDialog: FC = ({ status, statusParams, onModify }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("ReportStatuses"); + + const schema = yup.object({ + title: yup.string().trim(tCommon("trimmed_error")).strict(true).required(), + description: yup.string(), + }); + + const { + register, + setValue: setFormValue, + getValues: getFormValues, + formState: { errors }, + handleSubmit, + } = useForm<{ title: string; description?: string }>({ + mode: "onSubmit", + resolver: yupResolver(schema), + values: { + title: statusParams?.title ?? "", + description: statusParams?.description ?? "", + }, + }); + + const onSubmit = () => { + EditReportParameterModal.close(); + onModify(getFormValues()); + }; + + return createPortal( + +
+ { + const title = getFormValues("title"); + if (status && title !== defaultStatuses[status].title) { + setFormValue("title", defaultStatuses[status].title); + } + }} + /> + } + state={errors?.[`report_statuses.${status}.title`] ? "error" : "default"} + stateRelatedMessage={errors?.[`report_statuses.${status}.title`]?.message} + nativeInputProps={{ + ...register("title"), + }} + /> + +
+
, + document.body + ); +}; + +export { EditReportParameterModal, EditReportStatusDialog }; diff --git a/assets/espaceco/pages/communities/management/reports/EditReportStatusDialog_save.tsx b/assets/espaceco/pages/communities/management/reports/EditReportStatusDialog_save.tsx new file mode 100644 index 00000000..333821f5 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/EditReportStatusDialog_save.tsx @@ -0,0 +1,105 @@ +import Button from "@codegouvfr/react-dsfr/Button"; +import Input from "@codegouvfr/react-dsfr/Input"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FC } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { ReportStatusesType, ReportStatusParams } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { getDefaultStatuses } from "./Utils"; + +const EditReportParameterModal = createModal({ + id: "status-modal", + isOpenedByDefault: false, +}); + +const defaultStatuses = getDefaultStatuses(); + +type EditReportStatusDialogProps = { + status: ReportStatusesType; + statusParams: ReportStatusParams; + onModify: (values: Omit) => void; +}; + +const EditReportStatusDialog: FC = ({ status, statusParams, onModify }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("ReportStatuses"); + + const schema = yup.object({ + title: yup.string().trim(tCommon("trimmed_error")).strict(true).required(), + description: yup.string(), + }); + + const { + register, + setValue: setFormValue, + getValues: getFormValues, + formState: { errors }, + handleSubmit, + } = useForm<{ title: string; description?: string }>({ + mode: "onSubmit", + resolver: yupResolver(schema), + values: { + title: statusParams?.title ?? "", + description: statusParams?.description ?? "", + }, + }); + + const onSubmit = () => { + EditReportParameterModal.close(); + onModify(getFormValues()); + }; + + return createPortal( + +
+ { + setFormValue("title", defaultStatuses[status].title); + }} + /> + } + state={errors.title ? "error" : "default"} + stateRelatedMessage={errors?.title?.message} + nativeInputProps={{ + ...register("title"), + }} + /> + +
+
, + document.body + ); +}; + +export { EditReportParameterModal, EditReportStatusDialog }; diff --git a/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx b/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx index 13670fb6..f25b578c 100644 --- a/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx +++ b/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx @@ -17,19 +17,15 @@ export type EditThemeFormType = { help?: string; }; -const EditThemeDialogModal = createModal({ - id: "edit-theme", - isOpenedByDefault: false, -}); - type EditThemeDialogProps = { + modal: ReturnType; themes: ThemeDTO[]; currentTheme?: ThemeDTO; // tables: Partial[]; onModify: (oldName: string, newTheme: EditThemeFormType) => void; }; -const EditThemeDialog: FC = ({ themes, currentTheme, onModify }) => { +const EditThemeDialog: FC = ({ modal, themes, currentTheme, onModify }) => { const { t: tCommon } = useTranslation("Common"); const { t } = useTranslation("Theme"); @@ -68,7 +64,7 @@ const EditThemeDialog: FC = ({ themes, currentTheme, onMod }); const onSubmit = () => { - EditThemeDialogModal.close(); + modal.close(); if (currentTheme) { const values = getFormValues(); onModify(currentTheme?.theme, values); @@ -78,7 +74,7 @@ const EditThemeDialog: FC = ({ themes, currentTheme, onMod return ( <> {createPortal( - = ({ themes, currentTheme, onMod /> )} - , + , document.body )} ); }; -export { EditThemeDialog, EditThemeDialogModal }; +export default EditThemeDialog; diff --git a/assets/espaceco/pages/communities/management/reports/Permissions.tsx b/assets/espaceco/pages/communities/management/reports/Permissions.tsx new file mode 100644 index 00000000..a749192b --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/Permissions.tsx @@ -0,0 +1,41 @@ +import { FC } from "react"; +import { useTranslation } from "../../../../../i18n/i18n"; +import Checkbox from "@codegouvfr/react-dsfr/Checkbox"; +import { UseFormReturn } from "react-hook-form"; +import { ReportFormType } from "../../../../../@types/espaceco"; + +type PermissionsProps = { + form: UseFormReturn; +}; + +const Permissions: FC = ({ form }) => { + const { t } = useTranslation("ManageCommunity"); + + const { + register, + formState: { errors }, + } = form; + + return ( +
+

{t("report.manage_permissions")}

+ { + return { + label: t("report.manage_permissions.shared_report.option", { option: option }), + nativeInputProps: { + ...register("shared_georem"), + value: option, + }, + }; + })} + state={errors.shared_themes ? "error" : "default"} + stateRelatedMessage={errors?.shared_themes?.message} + /> +
+ ); +}; + +export default Permissions; diff --git a/assets/espaceco/pages/communities/management/reports/ReportStatuses.tsx b/assets/espaceco/pages/communities/management/reports/ReportStatuses.tsx index 2ab42d11..e3a22dec 100644 --- a/assets/espaceco/pages/communities/management/reports/ReportStatuses.tsx +++ b/assets/espaceco/pages/communities/management/reports/ReportStatuses.tsx @@ -3,54 +3,102 @@ import Button from "@codegouvfr/react-dsfr/Button"; import Checkbox from "@codegouvfr/react-dsfr/Checkbox"; import { FC, useState } from "react"; import { UseFormReturn } from "react-hook-form"; -import { ReportFormType, ReportStatusesDTO2 } from "../../../../../@types/espaceco"; +import { ReportFormType, ReportStatusesType } from "../../../../../@types/espaceco"; import { useTranslation } from "../../../../../i18n/i18n"; -import getDefaultStatuses from "./Utils"; +import { EditReportParameterModal, EditReportStatusDialog } from "./EditReportStatusDialog"; +import { statusesAlwaysActive } from "./Utils"; type ReportStatusesProps = { form: UseFormReturn; - statuses?: ReportStatusesDTO2; + state?: "default" | "error" | "success"; }; -const ReportStatuses: FC = ({ form, statuses }) => { + +// const minStatuses = getMinAuthorizedStatus(); + +const ReportStatuses: FC = ({ form, state }) => { + const { t: tStatus } = useTranslation("ReportStatuses"); const { t } = useTranslation("ManageCommunity"); - const [newStatus, setNewStatus] = useState(() => { - return statuses ? { ...statuses } : getDefaultStatuses(); - }); + const { + watch, + register, + setValue: setFormValue, + formState: { errors }, + } = form; + const statuses = watch("report_statuses"); + + const [currentStatus, setCurrentStatus] = useState(); + + // Changement d'etat d'un checkbox + /*const handleOnChange = (status: string, checked: boolean) => { + const v = { ...statuses }; + const num = countActiveStatus(v); + if ((!checked && num > minStatuses) || checked) { + v[status].active = checked; + } + setFormValue("report_statuses", v); + }; */ return ( -
+

{t("report.configure_statuses")}

{t("report.configure_statuses.explain")} -
-
-
    - {newStatus.map((s) => ( -
  • -
    -
    - -
    -
    -
    -
    -
    -
    -
  • - ))} -
-
+
+ { + const label = ( +
+ {statuses[s].title} +
+ ); + return { + label: label, + nativeInputProps: { + ...register(`report_statuses.${s}.active`), + disabled: statusesAlwaysActive.includes(s), + }, + }; + })} + />
+ {state !== "default" && ( +

{ + switch (state) { + case "error": + return "fr-error-text"; + case "success": + return "fr-valid-text"; + } + })() + )} + > + {errors.report_statuses?.root?.message} +

+ )} + { + if (currentStatus) { + const v = { ...statuses }; + v[currentStatus] = { ...v[currentStatus], ...values }; + setFormValue("report_statuses", v); + } + }} + />
); }; diff --git a/assets/espaceco/pages/communities/management/reports/ReportStatusesTr.tsx b/assets/espaceco/pages/communities/management/reports/ReportStatusesTr.tsx new file mode 100644 index 00000000..4d759c3a --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ReportStatusesTr.tsx @@ -0,0 +1,23 @@ +import { declareComponentKeys, Translations } from "../../../../../i18n/i18n"; + +export const { i18n } = declareComponentKeys<"parameter" | "title" | "description" | "description_placeholder" | "back_to_default" | "min_statuses">()( + "ReportStatuses" +); + +export const ReportStatusesFrTranslations: Translations<"fr">["ReportStatuses"] = { + parameter: "Paramétrer", + title: "Titre", + description: "Description", + description_placeholder: "Entrer le texte d'aide pour vos utilisateurs.", + back_to_default: "Revenir à la valeur par défault", + min_statuses: "Vous pouvez supprimer un maximum de 2 statuts", +}; + +export const ReportStatusesEnTranslations: Translations<"en">["ReportStatuses"] = { + parameter: "Parameter", + title: "Title", + description: "Description", + description_placeholder: "Enter help text for your users.", + back_to_default: "Go back to default value", + min_statuses: "You can delete a maximum of 2 statuses", +}; diff --git a/assets/espaceco/pages/communities/management/reports/SetSharedThemesDialog.tsx b/assets/espaceco/pages/communities/management/reports/SetSharedThemesDialog.tsx new file mode 100644 index 00000000..30be1b91 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/SetSharedThemesDialog.tsx @@ -0,0 +1,128 @@ +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { FC, useMemo } from "react"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { createPortal } from "react-dom"; +import * as yup from "yup"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import Checkbox from "@codegouvfr/react-dsfr/Checkbox"; +import { fr } from "@codegouvfr/react-dsfr"; +import { SharedThemesDTO } from "../../../../../@types/espaceco"; + +export type UserSharedThemesType = Record; +export type SharedThemesType = Record; + +const SetSharedThemesDialogModal = createModal({ + id: "set-shared-themes", + isOpenedByDefault: false, +}); + +type SetSharedThemesDialogProps = { + userSharedThemes: UserSharedThemesType; + sharedThemes: SharedThemesType; + onApply: (values: SharedThemesDTO[]) => void; +}; + +type formType = { + shared_themes: Record; +}; + +const SetSharedThemesDialog: FC = ({ userSharedThemes, sharedThemes, onApply }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("SharedThemes"); + + const schema = yup + .object({ + shared_themes: yup.lazy(() => { + const ret = {}; + Object.keys(userSharedThemes).forEach((community) => { + ret[community] = yup.array().of(yup.string()).required(); + }); + return yup.object().shape(ret).required(); + }), + }) + .required(); + + const defaultValues = useMemo(() => { + const def: formType["shared_themes"] = {}; + + Object.keys(userSharedThemes).forEach((community) => { + const themes: string[] = userSharedThemes[community].themes.filter((th) => { + return community in sharedThemes && sharedThemes[community].includes(th); + }); + def[community] = themes; + }); + return { shared_themes: def }; + }, [userSharedThemes, sharedThemes]); + + const { + watch, + register, + getValues: getFormValues, + handleSubmit, + } = useForm({ + mode: "onSubmit", + resolver: yupResolver(schema), + values: defaultValues, + }); + + const shared = watch("shared_themes"); + + const onSubmit = () => { + SetSharedThemesDialogModal.close(); + const values = getFormValues("shared_themes"); + + const sharedThemes: SharedThemesDTO[] = []; + Object.keys(values).forEach((community) => { + if (values[community].length) { + const communityName = userSharedThemes[community].communityName; + sharedThemes.push({ community_id: Number(community), community_name: communityName, themes: values[community] }); + } + }); + onApply(sharedThemes); + }; + + return createPortal( + +
+ {Object.keys(userSharedThemes).map((community) => { + const options = userSharedThemes[community].themes.map((theme) => ({ + label: theme, + nativeInputProps: { + value: theme, + checked: shared[community].includes(theme), + // @ts-expect-error ??? + ...register(`shared_themes.${community}`), + }, + })); + return ( + + ); + })} +
+
, + document.body + ); +}; + +export { SetSharedThemesDialogModal, SetSharedThemesDialog }; diff --git a/assets/espaceco/pages/communities/management/reports/SharedThemes.tsx b/assets/espaceco/pages/communities/management/reports/SharedThemes.tsx new file mode 100644 index 00000000..a54cbcaa --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/SharedThemes.tsx @@ -0,0 +1,134 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { CSSProperties, FC } from "react"; +import { UseFormReturn } from "react-hook-form"; +import { ReportFormType, SharedThemesDTO } from "../../../../../@types/espaceco"; +import { declareComponentKeys, Translations, useTranslation } from "../../../../../i18n/i18n"; +import { SetSharedThemesDialogModal, SetSharedThemesDialog, UserSharedThemesType, SharedThemesType } from "./SetSharedThemesDialog"; + +type SharedThemesProps = { + form: UseFormReturn; + userSharedThemes: UserSharedThemesType; +}; + +const style: CSSProperties = { + backgroundColor: fr.colors.decisions.background.contrast.grey.default, +}; + +const SharedThemes: FC = ({ form, userSharedThemes }) => { + const { t: tmc } = useTranslation("ManageCommunity"); + const { t } = useTranslation("SharedThemes"); + + const { watch, setValue: setFormValue } = form; + const sharedThemes = watch("shared_themes") ?? []; + + const workingSharedThemes: SharedThemesType = {}; + sharedThemes.forEach((st) => (workingSharedThemes[st.community_id] = st.themes)); + + const handleRemoveCommunity = (communityId: number) => { + const v = sharedThemes.filter((st) => st.community_id !== communityId); + setFormValue("shared_themes", v); + }; + + const handleRemoveTheme = (communityId: number, theme: string) => { + const result: SharedThemesDTO[] = []; + sharedThemes.forEach((st) => { + if (st.community_id === communityId) { + const shTheme = { ...st, themes: st.themes.filter((th) => th !== theme) }; + if (shTheme.themes.length) { + result.push(shTheme); + } + } else result.push(st); + }); + setFormValue("shared_themes", result); + }; + + return ( +
+

{tmc("report.configure_shared_themes")}

+ {tmc("report.configure_shared_themes.explain")} +
+ + {sharedThemes?.map((st) => ( +
+
+
{st.community_name}
+
+
+
+
+
+
+
+
    + {st.themes.map((theme) => ( +
  • +
    +
    {theme}
    +
    +
    +
    +
    +
    +
  • + ))} +
+
+
+
+ ))} +
+ {workingSharedThemes && ( + setFormValue("shared_themes", sharedThemes)} + /> + )} +
+ ); +}; + +export default SharedThemes; + +export const { i18n } = declareComponentKeys< + { K: "delete_community"; P: { text: string }; R: string } | { K: "delete_theme"; P: { text: string }; R: string } | "manage" | "dialog.title" +>()("SharedThemes"); + +export const SharedThemesFrTranslations: Translations<"fr">["SharedThemes"] = { + delete_community: ({ text }) => `Supprimer tous les thèmes de la communauté [${text}]`, + delete_theme: ({ text }) => `Remove theme [${text}]`, + manage: "Gérer", + "dialog.title": "Sélectionner les thèmes partagés à afficher", +}; + +export const SharedThemesEnTranslations: Translations<"en">["SharedThemes"] = { + delete_community: ({ text }) => `Remove all themes of the community [${text}]`, + delete_theme: ({ text }) => `Supprimer le thème [${text}]`, + manage: "Manage", + "dialog.title": undefined, +}; diff --git a/assets/espaceco/pages/communities/management/reports/ThemeList.tsx b/assets/espaceco/pages/communities/management/reports/ThemeList.tsx index 3c5c49f3..60a805ff 100644 --- a/assets/espaceco/pages/communities/management/reports/ThemeList.tsx +++ b/assets/espaceco/pages/communities/management/reports/ThemeList.tsx @@ -1,14 +1,16 @@ 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, useState } from "react"; +import { CSSProperties, FC, useCallback } from "react"; import { UseFormReturn } from "react-hook-form"; +import { v4 as uuidv4 } from "uuid"; import { ReportFormType, TableResponseDTO, ThemeDTO } from "../../../../../@types/espaceco"; import { useTranslation } from "../../../../../i18n/i18n"; import { AddThemeDialog, AddThemeDialogModal } from "./AddThemeDialog"; import AttributeList from "./AttributeList"; -import { EditThemeDialog, EditThemeDialogModal } from "./EditThemeDialog"; -import normalizeTheme from "./ThemeUtils"; +import EditThemeDialog from "./EditThemeDialog"; +import ThemesHelper from "./ThemesHelper"; const customStyle: CSSProperties = { border: "solid 1.5px", @@ -22,92 +24,114 @@ const themeStyle: CSSProperties = { type ThemeListProps = { form: UseFormReturn; tables: Partial[]; + state?: "default" | "error" | "success"; }; -const ThemeList: FC = ({ form, tables }) => { +const ThemeList: FC = ({ form, tables, state }) => { const { t } = useTranslation("ManageCommunity"); const { t: tTheme } = useTranslation("Theme"); - const { watch, setValue: setFormValue, getValues: getFormValues } = form; + const { watch, setValue: setFormValue } = form; const themes: ThemeDTO[] = watch("attributes"); - const [currentTheme, setCurrentTheme] = useState(); - // Supression d'un theme const handleRemoveTheme = (theme: string) => { - const a = themes.filter((a) => a.theme !== theme); - setFormValue("attributes", a); + 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) => ( -
-
-
- {t.theme} - {t.table && ( - - - - )} - {t.global !== undefined && t.global === true && ( - - - - )} -
-
-
-
+ { + const th = ThemesHelper.updateTheme(oldName, newTheme, themes); + setFormValue("attributes", th); + }} + /> + {!t.table && }
- {t.table === undefined && } -
- ))} + ); + })}
+ {state !== "default" && ( +

{ + switch (state) { + case "error": + return "fr-error-text"; + case "success": + return "fr-valid-text"; + } + })() + )} + > + {tTheme("attributes_not_conform")} +

+ )} { - const attributes = getFormValues("attributes"); - if (attributes) { - attributes.push(theme); - } - setFormValue("attributes", attributes); - }} - /> - { - const a = getFormValues("attributes").map((t) => (oldName === t.theme ? normalizeTheme({ ...t, ...newTheme }) : t)); - setFormValue("attributes", a); + const th = ThemesHelper.addTheme(themes, theme); + setFormValue("attributes", th); }} />
diff --git a/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx b/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx index 9a48e0d3..45cb519a 100644 --- a/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx +++ b/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx @@ -5,6 +5,7 @@ export const { i18n } = declareComponentKeys< | "add_theme" | "add_attribute" | "trimmed_error" + | "attributes_not_conform" | { K: "modify_theme"; P: { text: string }; R: string } | { K: "delete_theme"; P: { text: string }; R: string } | { K: "modify_attribute"; P: { text: string }; R: string } @@ -45,12 +46,21 @@ export const { i18n } = declareComponentKeys< | "dialog.add_attribute.list_duplicates_error" | "dialog.add_attribute.value_not_in_list_error" | "dialog.add_attribute.description" + | "dialog.edit_attribute.name" + | "dialog.edit_attribute.name_mandatory_error" + | "dialog.edit_attribute.name_unique_error" + | "dialog.edit_attribute.mandatory" + | "dialog.edit_attribute.list.multiple" + | "dialog.edit_attribute.list.values" + | "dialog.edit_attribute.value" + | "dialog.edit_attribute.description" >()("Theme"); export const ThemeFrTranslations: Translations<"fr">["Theme"] = { add_theme: "Ajouter un thème", add_attribute: "Ajouter un attribut", trimmed_error: "La chaîne de caractères ne doit contenir aucun espace en début et fin", + attributes_not_conform: "Les attributs ne sont pas conformes", modify_theme: ({ text }) => `Modifier le thème [${text}]`, delete_theme: ({ text }) => `Supprimer le thème [${text}]`, modify_attribute: ({ text }) => `Modifier l'attribut [${text}]`, @@ -97,7 +107,7 @@ export const ThemeFrTranslations: Translations<"fr">["Theme"] = { } }, "dialog.add_attribute.list.multiple": "Choix multiple", - "dialog.add_attribute.list.values": "Valeurs", + "dialog.add_attribute.list.values": "Valeurs (à séparer par des '|')", "dialog.add_attribute.value": "Valeur par défaut (optionnel sauf pour le type Liste)", "dialog.add_attribute.value.not_a_valid_integer": "La valeur n'est pas un entier valide", "dialog.add_attribute.value.not_a_valid_double": "La valeur n'est pas un double valide", @@ -107,12 +117,21 @@ export const ThemeFrTranslations: Translations<"fr">["Theme"] = { "dialog.add_attribute.list_duplicates_error": "Il y a des valeurs en double dans la liste", "dialog.add_attribute.value_not_in_list_error": "La valeur doit être dans la liste", "dialog.add_attribute.description": "Description (optionnel)", + "dialog.edit_attribute.name": "Nouveau nom", + "dialog.edit_attribute.name_mandatory_error": "Le nom de l'attribut est obligatoire", + "dialog.edit_attribute.name_unique_error": "Le nom doit être unique", + "dialog.edit_attribute.mandatory": "Attribut obligatoire", + "dialog.edit_attribute.list.multiple": "Choix multiple", + "dialog.edit_attribute.list.values": "Valeurs (à séparer par des '|')", + "dialog.edit_attribute.value": "Valeur par défaut (optionnel sauf pour le type Liste)", + "dialog.edit_attribute.description": "Nouvelle description", }; export const ThemeEnTranslations: Translations<"en">["Theme"] = { add_theme: "Add Theme", add_attribute: undefined, trimmed_error: undefined, + attributes_not_conform: undefined, modify_theme: ({ text }) => `Modify theme [${text}]`, delete_theme: ({ text }) => `Delete theme [${text}]`, modify_attribute: ({ text }) => `Modify attribute [${text}]`, @@ -168,4 +187,12 @@ export const ThemeEnTranslations: Translations<"en">["Theme"] = { "dialog.add_attribute.list_duplicates_error": undefined, "dialog.add_attribute.value_not_in_list_error": undefined, "dialog.add_attribute.description": undefined, + "dialog.edit_attribute.name": undefined, + "dialog.edit_attribute.name_mandatory_error": undefined, + "dialog.edit_attribute.name_unique_error": undefined, + "dialog.edit_attribute.mandatory": undefined, + "dialog.edit_attribute.list.multiple": undefined, + "dialog.edit_attribute.list.values": undefined, + "dialog.edit_attribute.value": undefined, + "dialog.edit_attribute.description": undefined, }; diff --git a/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx b/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx index 8813aebb..8ac911e8 100644 --- a/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx +++ b/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx @@ -1,6 +1,16 @@ -import { ThemeDTO } from "../../../../../@types/espaceco"; +import { AttributeDTO, AttributeType, ThemeDTO } from "../../../../../@types/espaceco"; -const normalizeTheme = (theme: ThemeDTO) => { +export type AddOrEditAttributeFormType = { + name: string; + type: string; + mandatory?: boolean; + default?: string | null; + help?: string | null; + multiple?: boolean; + values?: string | null; +}; + +const normalizeTheme = (theme: ThemeDTO): ThemeDTO => { const result = { ...theme }; ["global", "help"].forEach((f) => { if (f in result && !result[f]) { @@ -10,4 +20,34 @@ const normalizeTheme = (theme: ThemeDTO) => { return result; }; -export default normalizeTheme; +const normalizeAttribute = (attribute: AddOrEditAttributeFormType): AttributeDTO => { + const result: AttributeDTO = { + name: attribute.name, + type: attribute.type, + }; + + if (attribute.type === "list") { + result.values = attribute.values?.split("|") ?? []; + } else if (attribute.default !== "") { + result.default = attribute.default === "" ? null : attribute.default; + } + + if (attribute.help) { + result.help = attribute.help; + } + + ["mandatory", "multiple"].forEach((f) => { + if (attribute[f]) { + result[f] = attribute[f]; + } + }); + + return result; +}; + +/* Recuperation de input type à partir de type */ +const getInputType = (type: AttributeType) => { + return type === "date" ? "date" : "text"; +}; + +export { normalizeTheme, normalizeAttribute, getInputType }; diff --git a/assets/espaceco/pages/communities/management/reports/ThemesHelper.tsx b/assets/espaceco/pages/communities/management/reports/ThemesHelper.tsx new file mode 100644 index 00000000..14cfffcd --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ThemesHelper.tsx @@ -0,0 +1,53 @@ +import { AttributeDTO, ThemeDTO } from "../../../../../@types/espaceco"; +import { normalizeTheme } from "./ThemeUtils"; + +export default class ThemesHelper { + static addTheme(themes: ThemeDTO[], theme: ThemeDTO): ThemeDTO[] { + return [...themes, ...[theme]]; + } + + static updateTheme(name: string, newTheme: Partial, themes: ThemeDTO[]): ThemeDTO[] { + const tm = [...themes]; + return tm.map((t) => (name === t.theme ? normalizeTheme({ ...t, ...newTheme }) : t)); + } + + static removeTheme(name: string, themes: ThemeDTO[]): ThemeDTO[] { + return themes.filter((a) => a.theme !== name); + } + + static addAttribute(theme: string, attribute: AttributeDTO, themes: ThemeDTO[]): ThemeDTO[] { + return Array.from(themes, (t) => { + if (t.theme === theme) { + const attr = [...t.attributes]; + attr.push(attribute); + return { ...t, attributes: attr }; + } + return t; + }); + } + + static updateAttribute(theme: string, attribute: string, newAttribute: AttributeDTO, themes: ThemeDTO[]): ThemeDTO[] { + return Array.from(themes, (t) => { + if (t.theme === theme) { + const newAttributes = Array.from(t.attributes, (a) => { + if (a.name === attribute) { + return newAttribute; + } + return a; + }); + t.attributes = newAttributes; + } + return t; + }); + } + + static removeAttribute(theme: string, attribute: string, themes: ThemeDTO[]): ThemeDTO[] { + return Array.from(themes, (t) => { + if (t.theme === theme) { + const attr = t.attributes.filter((a) => a.name !== attribute); + return { ...t, attributes: attr }; + } + return t; + }); + } +} diff --git a/assets/espaceco/pages/communities/management/reports/Utils.tsx b/assets/espaceco/pages/communities/management/reports/Utils.tsx index 010607b1..2dcd1e0a 100644 --- a/assets/espaceco/pages/communities/management/reports/Utils.tsx +++ b/assets/espaceco/pages/communities/management/reports/Utils.tsx @@ -1,20 +1,24 @@ -import { ReportStatuses, ReportStatusesDTO2 } from "../../../../../@types/espaceco"; +import { ReportStatusesDTO } from "../../../../../@types/espaceco"; import statuses from "../../../../../data/report_statuses.json"; -/*const getDefaultStatuses = (): ReportStatusesDTO => { +const getDefaultStatuses = (): ReportStatusesDTO => { const result = {}; Object.keys(statuses).forEach((s) => { - result[s] = { wording: statuses[s].wording, help: null }; + result[s] = { title: statuses[s], active: true }; }); return result as ReportStatusesDTO; -};*/ +}; -const getDefaultStatuses = (): ReportStatusesDTO2 => { - const result: ReportStatusesDTO2 = []; - Object.keys(statuses).forEach((s) => { - result.push({ status: s as ReportStatuses, wording: statuses[s], help: undefined }); - }); - return result; +const getMinAuthorizedStatus = (): number => { + return Object.keys(statuses).length - 2; }; -export default getDefaultStatuses; +const countActiveStatus = (statuses: ReportStatusesDTO) => { + let c = 0; + Object.keys(statuses).forEach((s) => (c += statuses[s].active ? 1 : 0)); + return c; +}; + +const statusesAlwaysActive = ["submit", "valid"]; + +export { getDefaultStatuses, getMinAuthorizedStatus, countActiveStatus, statusesAlwaysActive }; diff --git a/assets/i18n/Breadcrumb.tsx b/assets/i18n/Breadcrumb.tsx index e9f133a4..cb2d3731 100644 --- a/assets/i18n/Breadcrumb.tsx +++ b/assets/i18n/Breadcrumb.tsx @@ -40,6 +40,8 @@ export const { i18n } = declareComponentKeys< | "datastore_pyramid_vector_tms_service_new" | "datastore_pyramid_vector_tms_service_edit" | "datastore_service_view" + | "espaceco_community_list" + | { K: "espaceco_manage_community"; P: { communityName?: string }; R: string } >()("Breadcrumb"); export const BreadcrumbFrTranslations: Translations<"fr">["Breadcrumb"] = { @@ -82,6 +84,8 @@ export const BreadcrumbFrTranslations: Translations<"fr">["Breadcrumb"] = { datastore_pyramid_vector_tms_service_new: "Création d'un service TMS", datastore_pyramid_vector_tms_service_edit: "Modification d'un service TMS", datastore_service_view: "Prévisualisation d'un service", + espaceco_community_list: "Espace collaboratif", + espaceco_manage_community: ({ communityName }) => `Gérer le guichet ${communityName ?? ""}`, }; export const BreadcrumbEnTranslations: Translations<"en">["Breadcrumb"] = { @@ -124,4 +128,6 @@ export const BreadcrumbEnTranslations: Translations<"en">["Breadcrumb"] = { datastore_pyramid_vector_tms_service_new: "Create a TMS service", datastore_pyramid_vector_tms_service_edit: "Modify a TMS service", datastore_service_view: "Preview a service", + espaceco_community_list: "Collaborative space", + espaceco_manage_community: ({ communityName }) => `Manage community ${communityName ?? ""}`, }; diff --git a/assets/i18n/Common.tsx b/assets/i18n/Common.tsx index 58b47284..18434b4a 100644 --- a/assets/i18n/Common.tsx +++ b/assets/i18n/Common.tsx @@ -7,12 +7,14 @@ export const { i18n } = declareComponentKeys< | "adding" | "modify" | "apply" + | "record" | "modifying" | "removing" | "loading" | "continue" | "validate" | "submit" + | "save" | "copy" | "send" | "cancel" @@ -32,6 +34,7 @@ export const { i18n } = declareComponentKeys< | "next_step" | "url_copied" | "copy_to_clipboard" + | "trimmed_error" >()("Common"); export const commonFrTranslations: Translations<"fr">["Common"] = { @@ -41,12 +44,14 @@ export const commonFrTranslations: Translations<"fr">["Common"] = { adding: "Ajout en cours ...", modify: "Modifier", apply: "Appliquer", + record: "Enregistrer", modifying: "Modification en cours ...", removing: "Suppression en cours ...", loading: "Chargement ...", continue: "Continuer", validate: "Valider", submit: "Soumettre", + save: "Sauvegarder", copy: "Copier", send: "Envoyer", cancel: "Annuler", @@ -66,6 +71,7 @@ export const commonFrTranslations: Translations<"fr">["Common"] = { next_step: "Étape suivante", url_copied: "URL copiée", copy_to_clipboard: "Copier dans le presse-papier", + trimmed_error: "La chaîne de caractères ne doit contenir aucun espace en début et fin", }; export const commonEnTranslations: Translations<"en">["Common"] = { @@ -75,12 +81,14 @@ export const commonEnTranslations: Translations<"en">["Common"] = { adding: "Adding ...", modify: "Modify", apply: "Apply", + record: "Record", modifying: "modifying ...", removing: "Removing ...", loading: "Loading ...", continue: "Continue", validate: "Validate", submit: "Submit", + save: "Save", copy: "Copy", send: "Send", cancel: "Cancel", @@ -100,4 +108,5 @@ export const commonEnTranslations: Translations<"en">["Common"] = { next_step: "Next step", url_copied: "URL copied", copy_to_clipboard: "Copier dans le presse-papier", + 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 78a3ca23..1aa7b02b 100644 --- a/assets/i18n/i18n.ts +++ b/assets/i18n/i18n.ts @@ -54,7 +54,9 @@ 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/reports/ThemeTr").i18n; + | typeof import("../espaceco/pages/communities/management/reports/ThemeTr").i18n + | typeof import("../espaceco/pages/communities/management/reports/ReportStatusesTr").i18n + | typeof import("../espaceco/pages/communities/management/reports/SharedThemes").i18n; export type Translations = GenericTranslations; export type LocalizedString = Parameters[0]; diff --git a/assets/i18n/languages/en.tsx b/assets/i18n/languages/en.tsx index b849735d..588fa096 100644 --- a/assets/i18n/languages/en.tsx +++ b/assets/i18n/languages/en.tsx @@ -37,6 +37,8 @@ import { commonEnTranslations } from "../Common"; import { RightsEnTranslations } from "../Rights"; import { StyleEnTranslations } from "../Style"; import { ThemeEnTranslations } from "../../espaceco/pages/communities/management/reports/ThemeTr"; +import { ReportStatusesEnTranslations } from "../../espaceco/pages/communities/management/reports/ReportStatusesTr"; +import { SharedThemesEnTranslations } from "../../espaceco/pages/communities/management/reports/SharedThemes"; import type { Translations } from "../i18n"; @@ -79,5 +81,7 @@ export const translations: Translations<"en"> = { ManageCommunity: ManageCommunityEnTranslations, ManageCommunityValidations: ManageCommunityValidationsEnTranslations, Theme: ThemeEnTranslations, + ReportStatuses: ReportStatusesEnTranslations, + SharedThemes: SharedThemesEnTranslations, Search: SearchEnTranslations, }; diff --git a/assets/i18n/languages/fr.tsx b/assets/i18n/languages/fr.tsx index dda493ff..ad4301a1 100644 --- a/assets/i18n/languages/fr.tsx +++ b/assets/i18n/languages/fr.tsx @@ -37,6 +37,8 @@ import { commonFrTranslations } from "../Common"; import { RightsFrTranslations } from "../Rights"; import { StyleFrTranslations } from "../Style"; import { ThemeFrTranslations } from "../../espaceco/pages/communities/management/reports/ThemeTr"; +import { ReportStatusesFrTranslations } from "../../espaceco/pages/communities/management/reports/ReportStatusesTr"; +import { SharedThemesFrTranslations } from "../../espaceco/pages/communities/management/reports/SharedThemes"; import type { Translations } from "../i18n"; @@ -79,5 +81,7 @@ export const translations: Translations<"fr"> = { ManageCommunity: ManageCommunityFrTranslations, ManageCommunityValidations: ManageCommunityValidationsFrTranslations, Theme: ThemeFrTranslations, + ReportStatuses: ReportStatusesFrTranslations, + SharedThemes: SharedThemesFrTranslations, Search: SearchFrTranslations, }; diff --git a/assets/modules/entrepot/breadcrumbs.ts b/assets/modules/entrepot/breadcrumbs.ts index 951fb1c7..840d2a95 100644 --- a/assets/modules/entrepot/breadcrumbs.ts +++ b/assets/modules/entrepot/breadcrumbs.ts @@ -1,9 +1,9 @@ import { BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb"; import { Route } from "type-route"; +import { Datastore } from "../../@types/app"; import { getTranslation } from "../../i18n/i18n"; import { routes } from "../../router/router"; -import { Datastore } from "../../@types/app"; const { t } = getTranslation("Breadcrumb"); @@ -181,7 +181,6 @@ const getBreadcrumb = (route: Route, datastore?: Datastore): Brea ]; return { ...defaultProps, currentPageLabel: t(route.name) }; - case "espaceco_community_list": case "home": default: return undefined; diff --git a/assets/modules/espaceco/RQKeys.ts b/assets/modules/espaceco/RQKeys.ts index 38804c8f..d722a8dc 100644 --- a/assets/modules/espaceco/RQKeys.ts +++ b/assets/modules/espaceco/RQKeys.ts @@ -1,6 +1,7 @@ import { CommunityListFilter } from "../../@types/app_espaceco"; const RQKeys = { + user_shared_themes: (): string[] => ["user", "shared_themes"], community: (communityId: number): string[] => ["community", communityId.toString()], community_list: (page: number, limit: number): string[] => ["communities", page.toString(), limit.toString()], searchCommunities: (search: string, filter: CommunityListFilter): string[] => { diff --git a/assets/router/router.ts b/assets/router/router.ts index 3cf7ddea..e6425b8a 100644 --- a/assets/router/router.ts +++ b/assets/router/router.ts @@ -226,14 +226,14 @@ const routeDefs = { page: param.query.optional.number.default(1), filter: param.query.optional.string.default("public"), }, - () => `${appRoot}/espaceco/community` + () => `${appRoot}/espace-collaboratif` ), espaceco_manage_community: defineRoute( { communityId: param.path.number, }, - (p) => `${appRoot}/espaceco/community/${p.communityId}/gestion` + (p) => `${appRoot}/espace-collaboratif/${p.communityId}/gerer-le-guichet` ), }; diff --git a/src/Controller/EspaceCo/PermissionController.php b/src/Controller/EspaceCo/PermissionController.php index b5950f62..e6eb6fe3 100644 --- a/src/Controller/EspaceCo/PermissionController.php +++ b/src/Controller/EspaceCo/PermissionController.php @@ -81,7 +81,9 @@ public function getThemableTables(int $communityId): JsonResponse return ($fna < $fnb) ? -1 : 1; }); - return new JsonResponse(array_unique($response, SORT_REGULAR)); + $t = array_values(array_unique($response, SORT_REGULAR)); + + return new JsonResponse($t); } catch (ApiException $ex) { throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); } diff --git a/src/Controller/EspaceCo/UserController.php b/src/Controller/EspaceCo/UserController.php index 60867501..5b1e28ba 100644 --- a/src/Controller/EspaceCo/UserController.php +++ b/src/Controller/EspaceCo/UserController.php @@ -4,9 +4,9 @@ use App\Controller\ApiControllerInterface; use App\Services\EspaceCoApi\UserApiService; -use Symfony\Component\Routing\Annotation\Route; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Routing\Annotation\Route; #[Route( '/api/espaceco/user', @@ -25,6 +25,15 @@ public function __construct( public function getCurrentUser(): JsonResponse { $me = $this->userApiService->getMe(); + + return $this->json($me); + } + + #[Route('/me/shared_themes', name: 'shared_themes')] + public function getSharedThemes(): JsonResponse + { + $me = $this->userApiService->getSharedThemes(); + return $this->json($me); } -} \ No newline at end of file +} diff --git a/src/Services/EspaceCoApi/UserApiService.php b/src/Services/EspaceCoApi/UserApiService.php index 7d8c6009..5b985d20 100644 --- a/src/Services/EspaceCoApi/UserApiService.php +++ b/src/Services/EspaceCoApi/UserApiService.php @@ -8,4 +8,14 @@ public function getMe(): array { return $this->request('GET', 'users/me'); } -} \ No newline at end of file + + public function getSharedThemes(): array + { + $result = $this->request('GET', 'users/me', [], ['fields' => 'shared_themes']); + if (is_array($result) && array_key_exists('shared_themes', $result)) { + return $result['shared_themes']; + } + + return []; + } +}