diff --git a/.vscode/launch.json b/.vscode/launch.json index 0c0046ce..1033fd1c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,6 +6,12 @@ "request": "launch", "port": 9003 }, + { + "name": "Listen for Xdebug 2 (Legacy)", + "type": "php", + "request": "launch", + "port": 9000 + }, { "name": "Listen for Xdebug (in docker)", "type": "php", diff --git a/assets/@types/espaceco.ts b/assets/@types/espaceco.ts index 32288088..3c9a2840 100644 --- a/assets/@types/espaceco.ts +++ b/assets/@types/espaceco.ts @@ -1,3 +1,34 @@ +export interface ConstraintsDTO { + minLength?: number; + maxLength?: number; + minValue?: string; + maxValue?: string; + pattern?: string; +} + +export interface AttributeDTO { + name: string; + type: "text" | "integer" | "double" | "checkbox" | "list" | "date"; + default?: string; + mandatory?: boolean; + values?: string[]; + help?: string; + title?: string; + input_constraints?: ConstraintsDTO; + json_schema?: object; + required?: boolean; + condition_field?: string; +} + +export interface ThemeDTO { + theme: string; + database?: string; + table?: string; + attributes: AttributeDTO[]; + help?: string; + global?: boolean; +} + export interface CommunityResponseDTO { id: number; description: string | null; @@ -6,7 +37,7 @@ export interface CommunityResponseDTO { active: boolean; shared_georem: "all" | "restrained" | "personal"; email: string | null; - attributes: object[]; + attributes: ThemeDTO[]; default_comment: string | null; position: string | null; zoom: number; @@ -55,3 +86,70 @@ export interface GridType { export interface CommunityPatchDTO extends Partial> { logo: File | null; } + +export interface PermissionResponseDTO { + id: number; + database: number; + table: number | null; + column: number | null; + level: "NONE" | "VIEW" | "EXPORT" | "EDIT" | "ADMIN"; +} + +export interface ColumnDTO { + table_id: number; + crs: string | null; + enum: object | string[] | null; + default_value: string | null; + read_only: boolean; + id: number; + type: string; + target_table: string | null; + target_entity: string | null; + name: string; + short_name: string; + title: string; + description: string | null; + min_length: number | null; + max_length: number | null; + nullable: boolean; + unique: boolean; + srid: number | null; + position: number; + min_value: string | null; + max_value: string | null; + pattern: string | null; + is3d: boolean; + constraint: object | null; + condition_field: string | null; + computed: boolean; + automatic: boolean; + custom_id: boolean; + formula: string | null; + json_schema: object | null; + jeux_attributs: object | null; + queryable: boolean; + required: boolean; + mime_types: string | null; +} + +export interface TableResponseDTO { + database_id: number; + database: string; + database_versioning: boolean; + full_name: string; + id_name: string; + geometry_name: string; + min_zoom_level: number | null; + max_zoom_level: number | null; + tile_zoom_level: number | null; + read_only: boolean; + id: number; + name: string; + title: string; + description: string | null; + thematic_ids: string[] | null; + position: number; + wfs: string; + wfs_transactions: string; + columns: ColumnDTO[]; +} diff --git a/assets/doc_thumbnail.js b/assets/doc_thumbnail.js new file mode 100644 index 00000000..f41e94b5 --- /dev/null +++ b/assets/doc_thumbnail.js @@ -0,0 +1,376 @@ +const docThumbnails = { + "7z": { + src: "img/vignettes/7z.png", + title: "Document de type 7Z", + }, + abw: { + src: "img/vignettes/abw.png", + title: "Document de type ABW", + }, + ai: { + src: "img/vignettes/ai.png", + title: "Document de type AI", + }, + aiff: { + src: "img/vignettes/aiff.png", + title: "Document de type AIFF", + }, + asf: { + src: "img/vignettes/asf.png", + title: "Document de type ASF", + }, + avi: { + src: "img/vignettes/avi.png", + title: "Document de type AVI", + }, + bin: { + src: "img/vignettes/bin.png", + title: "Document de type BIN", + }, + blend: { + src: "img/vignettes/blend.png", + title: "Document de type BLEND", + }, + bmp: { + src: "img/vignettes/bmp.png", + title: "Document de type BMP", + }, + bz2: { + src: "img/vignettes/bz2.png", + title: "Document de type BZ2", + }, + c: { + src: "img/vignettes/c.png", + title: "Document de type C", + }, + crq: { + src: "img/vignettes/crq.png", + title: "Document de type CRQ", + }, + css: { + src: "img/vignettes/css.png", + title: "Document de type CSS", + }, + csv: { + src: "img/vignettes/csv.png", + title: "Document de type CSV", + }, + deb: { + src: "img/vignettes/deb.png", + title: "Document de type DEB", + }, + defaut: { + src: "img/vignettes/defaut.png", + title: "Document de type DEFAUT", + }, + djvu: { + src: "img/vignettes/djvu.png", + title: "Document de type DJVU", + }, + doc: { + src: "img/vignettes/doc.png", + title: "Document de type DOC", + }, + docx: { + src: "img/vignettes/docx.png", + title: "Document de type DOCX", + }, + dvi: { + src: "img/vignettes/dvi.png", + title: "Document de type DVI", + }, + dxf: { + src: "img/vignettes/dxf.png", + title: "Document de type DXF", + }, + eps: { + src: "img/vignettes/eps.png", + title: "Document de type EPS", + }, + flv: { + src: "img/vignettes/flv.png", + title: "Document de type FLV", + }, + gif: { + src: "img/vignettes/gif.png", + title: "Document de type GIF", + }, + gpx: { + src: "img/vignettes/gpx.png", + title: "Document de type GPX", + }, + gxt: { + src: "img/vignettes/gxt.png", + title: "Document de type GXT", + }, + gz: { + src: "img/vignettes/gz.png", + title: "Document de type GZ", + }, + h: { + src: "img/vignettes/h.png", + title: "Document de type H", + }, + html: { + src: "img/vignettes/html.png", + title: "Document de type HTML", + }, + jpg: { + src: "img/vignettes/jpg.png", + title: "Document de type JPG", + }, + kml: { + src: "img/vignettes/kml.png", + title: "Document de type KML", + }, + kmz: { + src: "img/vignettes/kmz.png", + title: "Document de type KMZ", + }, + mid: { + src: "img/vignettes/mid.png", + title: "Document de type MID", + }, + mka: { + src: "img/vignettes/mka.png", + title: "Document de type MKA", + }, + mkv: { + src: "img/vignettes/mkv.png", + title: "Document de type MKV", + }, + mng: { + src: "img/vignettes/mng.png", + title: "Document de type MNG", + }, + mov: { + src: "img/vignettes/mov.png", + title: "Document de type MOV", + }, + mp3: { + src: "img/vignettes/mp3.png", + title: "Document de type MP3", + }, + mp4: { + src: "img/vignettes/mp4.png", + title: "Document de type MP4", + }, + mpg: { + src: "img/vignettes/mpg.png", + title: "Document de type MPG", + }, + odb: { + src: "img/vignettes/odb.png", + title: "Document de type ODB", + }, + odc: { + src: "img/vignettes/odc.png", + title: "Document de type ODC", + }, + odf: { + src: "img/vignettes/odf.png", + title: "Document de type ODF", + }, + odg: { + src: "img/vignettes/odg.png", + title: "Document de type ODG", + }, + odi: { + src: "img/vignettes/odi.png", + title: "Document de type ODI", + }, + odm: { + src: "img/vignettes/odm.png", + title: "Document de type ODM", + }, + odp: { + src: "img/vignettes/odp.png", + title: "Document de type ODP", + }, + ods: { + src: "img/vignettes/ods.png", + title: "Document de type ODS", + }, + odt: { + src: "img/vignettes/odt.png", + title: "Document de type ODT", + }, + ogg: { + src: "img/vignettes/ogg.png", + title: "Document de type OGG", + }, + otg: { + src: "img/vignettes/otg.png", + title: "Document de type OTG", + }, + otp: { + src: "img/vignettes/otp.png", + title: "Document de type OTP", + }, + ots: { + src: "img/vignettes/ots.png", + title: "Document de type OTS", + }, + ott: { + src: "img/vignettes/ott.png", + title: "Document de type OTT", + }, + pas: { + src: "img/vignettes/pas.png", + title: "Document de type PAS", + }, + pdf: { + src: "img/vignettes/pdf.png", + title: "Document de type PDF", + }, + pgn: { + src: "img/vignettes/pgn.png", + title: "Document de type PGN", + }, + png: { + src: "img/vignettes/png.png", + title: "Document de type PNG", + }, + pps: { + src: "img/vignettes/pps.png", + title: "Document de type PPS", + }, + ppt: { + src: "img/vignettes/ppt.png", + title: "Document de type PPT", + }, + pptx: { + src: "img/vignettes/pptx.png", + title: "Document de type PPTX", + }, + ps: { + src: "img/vignettes/ps.png", + title: "Document de type PS", + }, + psd: { + src: "img/vignettes/psd.png", + title: "Document de type PSD", + }, + qt: { + src: "img/vignettes/qt.png", + title: "Document de type QT", + }, + ra: { + src: "img/vignettes/ra.png", + title: "Document de type RA", + }, + ram: { + src: "img/vignettes/ram.png", + title: "Document de type RAM", + }, + rm: { + src: "img/vignettes/rm.png", + title: "Document de type RM", + }, + rpm: { + src: "img/vignettes/rpm.png", + title: "Document de type RPM", + }, + rtf: { + src: "img/vignettes/rtf.png", + title: "Document de type RTF", + }, + sdd: { + src: "img/vignettes/sdd.png", + title: "Document de type SDD", + }, + sdw: { + src: "img/vignettes/sdw.png", + title: "Document de type SDW", + }, + sit: { + src: "img/vignettes/sit.png", + title: "Document de type SIT", + }, + smil: { + src: "img/vignettes/smil.png", + title: "Document de type SMIL", + }, + spip: { + src: "img/vignettes/spip.png", + title: "Document de type SPIP", + }, + svg: { + src: "img/vignettes/svg.png", + title: "Document de type SVG", + }, + swf: { + src: "img/vignettes/swf.png", + title: "Document de type SWF", + }, + sxc: { + src: "img/vignettes/sxc.png", + title: "Document de type SXC", + }, + sxi: { + src: "img/vignettes/sxi.png", + title: "Document de type SXI", + }, + sxw: { + src: "img/vignettes/sxw.png", + title: "Document de type SXW", + }, + tex: { + src: "img/vignettes/tex.png", + title: "Document de type TEX", + }, + tgz: { + src: "img/vignettes/tgz.png", + title: "Document de type TGZ", + }, + tif: { + src: "img/vignettes/tif.png", + title: "Document de type TIF", + }, + tiff: { + src: "img/vignettes/tiff.png", + title: "Document de type TIFF", + }, + torrent: { + src: "img/vignettes/torrent.png", + title: "Document de type TORRENT", + }, + ttf: { + src: "img/vignettes/ttf.png", + title: "Document de type TTF", + }, + txt: { + src: "img/vignettes/txt.png", + title: "Document de type TXT", + }, + wav: { + src: "img/vignettes/wav.png", + title: "Document de type WAV", + }, + wmv: { + src: "img/vignettes/wmv.png", + title: "Document de type WMV", + }, + xcf: { + src: "img/vignettes/xcf.png", + title: "Document de type XCF", + }, + xls: { + src: "img/vignettes/xls.png", + title: "Document de type XLS", + }, + xlsx: { + src: "img/vignettes/xlsx.png", + title: "Document de type XLSX", + }, + xml: { + src: "img/vignettes/xml.png", + title: "Document de type XML", + }, + zip: { + src: "img/vignettes/zip.png", + title: "Document de type ZIP", + }, +}; + +export default docThumbnails; diff --git a/assets/espaceco/api/community.ts b/assets/espaceco/api/community.ts index 9249cffe..724b56d2 100644 --- a/assets/espaceco/api/community.ts +++ b/assets/espaceco/api/community.ts @@ -6,6 +6,7 @@ import { jsonFetch } from "../../modules/jsonFetch"; const get = (queryParams: { page: number; limit: number }, signal: AbortSignal) => { const params = { ...queryParams, sort: "name:ASC" }; + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get", params); return jsonFetch>(url, { signal: signal, diff --git a/assets/espaceco/api/index.ts b/assets/espaceco/api/index.ts index eab915ee..590cda61 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"; const api = { community, + permission, grid, }; diff --git a/assets/espaceco/api/permission.ts b/assets/espaceco/api/permission.ts new file mode 100644 index 00000000..a3fa19d3 --- /dev/null +++ b/assets/espaceco/api/permission.ts @@ -0,0 +1,14 @@ +import { TableResponseDTO } from "../../@types/espaceco"; +import { jsonFetch } from "../../modules/jsonFetch"; +import SymfonyRouting from "../../modules/Routing"; + +const getThemableTables = (communityId: number, signal: AbortSignal) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_permission_get_themable_tables_by_community", { communityId: communityId }); + return jsonFetch[]>(url, { + signal: signal, + }); +}; + +const permission = { getThemableTables }; + +export default permission; diff --git a/assets/espaceco/pages/communities/Communities.tsx b/assets/espaceco/pages/communities/Communities.tsx index af88cc3f..60205a31 100644 --- a/assets/espaceco/pages/communities/Communities.tsx +++ b/assets/espaceco/pages/communities/Communities.tsx @@ -53,7 +53,7 @@ const Communities: FC = () => { queryKey: RQKeys.community_list(queryParams.page, queryParams.limit), queryFn: ({ signal }) => api.community.get(queryParams, signal), staleTime: 3600000, - retry: false, + //retry: false, enabled: filter === "public", }); @@ -61,7 +61,7 @@ const Communities: FC = () => { queryKey: RQKeys.communities_as_member(queryParams.pending ?? false, queryParams.page, queryParams.limit), queryFn: ({ signal }) => api.community.getAsMember(queryParams, signal), staleTime: 3600000, - retry: false, + //retry: false, enabled: filter === "iam_member" || filter === "affiliation", }); diff --git a/assets/espaceco/pages/communities/ManageCommunity.tsx b/assets/espaceco/pages/communities/ManageCommunity.tsx index f5755b2d..8929dbe7 100644 --- a/assets/espaceco/pages/communities/ManageCommunity.tsx +++ b/assets/espaceco/pages/communities/ManageCommunity.tsx @@ -17,6 +17,7 @@ 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"; type ManageCommunityProps = { communityId: number; @@ -77,6 +78,8 @@ const ManageCommunity: FC = ({ communityId }) => { return ; case "tab4": return ; + case "tab6": + return ; case "tab7": return ; // TODO default: diff --git a/assets/espaceco/pages/communities/ManageCommunityTr.tsx b/assets/espaceco/pages/communities/ManageCommunityTr.tsx index ed597ee1..7d43dddb 100644 --- a/assets/espaceco/pages/communities/ManageCommunityTr.tsx +++ b/assets/espaceco/pages/communities/ManageCommunityTr.tsx @@ -55,6 +55,8 @@ export const { i18n } = declareComponentKeys< | "layer.tabl" | "layer.tab2" | "layer.tab3" + | "report.configure_themes" + | "report.configure_themes.explain" | "grid.grids" | { K: "grid.explain"; R: JSX.Element } >()("ManageCommunity"); @@ -122,6 +124,9 @@ export const ManageCommunityFrTranslations: Translations<"fr">["ManageCommunity" "layer.tabl": "Mes données", "layer.tab2": "Données de la géoplateforme", "layer.tab3": "Fonds de carte", + "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.", "grid.grids": "Emprises du guichet (optionnel)", "grid.explain": (

@@ -192,6 +197,8 @@ export const ManageCommunityEnTranslations: Translations<"en">["ManageCommunity" "layer.tabl": "My datas", "layer.tab2": "Geoplateforme datas", "layer.tab3": "Base maps", + "report.configure_themes": undefined, + "report.configure_themes.explain": undefined, "grid.grids": undefined, "grid.explain": undefined, }; diff --git a/assets/espaceco/pages/communities/management/Reports.tsx b/assets/espaceco/pages/communities/management/Reports.tsx new file mode 100644 index 00000000..660cf38b --- /dev/null +++ b/assets/espaceco/pages/communities/management/Reports.tsx @@ -0,0 +1,62 @@ +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 { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { CommunityResponseDTO, TableResponseDTO } from "../../../../@types/espaceco"; +import { useTranslation } from "../../../../i18n/i18n"; +import RQKeys from "../../../../modules/espaceco/RQKeys"; +import { CartesApiException } from "../../../../modules/jsonFetch"; +import api from "../../../api"; +import { AddThemeDialog, AddThemeDialogModal } from "./reports/AddThemeDialog"; +import ThemeList from "./reports/ThemeList"; + +type ReportsProps = { + community: CommunityResponseDTO; +}; + +const Reports: FC = ({ community }) => { + const { t } = useTranslation("Theme"); + + const schema = yup.object({ + attributes: yup.array().of(yup.object()), + }); + + const tablesQuery = useQuery[], CartesApiException>({ + queryKey: RQKeys.tables(community.id), + queryFn: ({ signal }) => api.permission.getThemableTables(community.id, signal), + staleTime: 60000, + }); + + const form = useForm({ resolver: yupResolver(schema), mode: "onChange", values: { attributes: community.attributes ?? [] } }); + const { getValues: getFormValues, setValue: setFormValue } = form; + + return ( +

+ {tablesQuery.isError && } + {tablesQuery.isError ? ( + + ) : ( + <> + + {/* + { + const attributes = getFormValues("attributes"); + if (attributes) { + attributes.push(theme); + } + setFormValue("attributes", attributes); + }} + /> */} + + )} +
+ ); +}; + +export default Reports; diff --git a/assets/espaceco/pages/communities/management/reports/AddThemeDialog.tsx b/assets/espaceco/pages/communities/management/reports/AddThemeDialog.tsx new file mode 100644 index 00000000..29da9c29 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/AddThemeDialog.tsx @@ -0,0 +1,190 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Input from "@codegouvfr/react-dsfr/Input"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import Select from "@codegouvfr/react-dsfr/Select"; +import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FC, useEffect, useMemo } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { TableResponseDTO, ThemeDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import normalizeTheme from "./ThemeUtils"; + +const AddThemeDialogModal = createModal({ + id: "add-theme", + isOpenedByDefault: false, +}); + +type AddThemeDialogProps = { + themes: ThemeDTO[]; + tables: Partial[]; + onAdd: (theme: ThemeDTO) => void; +}; + +type AddTheme = { + theme: string; + fullname?: string; + help?: string; + global?: boolean; +}; + +const normalize = (theme: AddTheme): ThemeDTO => { + const result = { ...theme }; + + result["attributes"] = []; + if (theme.fullname) { + const words = theme.fullname.split(":"); + result["database"] = words[0]; + result["table"] = words[1]; + delete result.fullname; + } + return normalizeTheme(result as ThemeDTO); +}; + +const defaultValues: AddTheme = { + theme: "", + fullname: "", + help: "", + global: false, +}; + +const AddThemeDialog: FC = ({ themes, tables, onAdd }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("Theme"); + + const themeNames: string[] = useMemo(() => { + return Array.from(themes as ThemeDTO[], (a) => a.theme); + }, [themes]); + + const tableOptions = useMemo(() => { + const a = Array.from(tables, (t) => ( + + )); + a.unshift( + + ); + return a; + }, [t, tables]); + + const schema = yup.object({ + theme: yup + .string() + .trim(t("trimmed_error")) + .strict(true) + .required(t("name_mandatory_error")) + .test("is-unique", t("name_unique_error"), (value) => { + const v = value.trim(); + return !themeNames.includes(v); + }), + fullname: yup.string(), + help: yup.string(), + global: yup.boolean(), + }); + + const { + register, + watch, + formState: { errors }, + setValue: setFormValue, + getValues: getFormValues, + reset, + handleSubmit, + } = useForm({ + mode: "onSubmit", + defaultValues: defaultValues, + resolver: yupResolver(schema), + }); + + const tableFullName = watch("fullname"); + + useEffect(() => { + if (tableFullName) { + setFormValue("global", false); + } + }, [setFormValue, tableFullName]); + + const onSubmit = () => { + AddThemeDialogModal.close(); + onAdd(normalize(getFormValues())); + reset(defaultValues); + }; + + return ( + <> + {createPortal( + { + reset(defaultValues); + AddThemeDialogModal.close(); + }, + }, + { + priority: "primary", + children: tCommon("add"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + }, + ]} + > +
+

{tCommon("mandatory_fields")}

+ + + + + { + setFormValue("global", checked); + }} + /> +
+
, + document.body + )} + + ); +}; + +export { AddThemeDialog, AddThemeDialogModal }; diff --git a/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx b/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx new file mode 100644 index 00000000..e66cb3a3 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx @@ -0,0 +1,138 @@ +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 { ThemeDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; + +export type EditTheme = { + theme: string; + table?: string; + global?: boolean; + help?: string; +}; + +const EditThemeDialogModal = createModal({ + id: "edit-theme", + isOpenedByDefault: false, +}); + +type EditThemeDialogProps = { + themes: ThemeDTO[]; + currentTheme?: ThemeDTO; + // tables: Partial[]; + onModify: (oldName: string, newTheme: EditTheme) => void; +}; + +const EditThemeDialog: FC = ({ themes, currentTheme, onModify }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("Theme"); + + const themeNames: string[] = useMemo(() => { + return Array.from( + themes.filter((t) => t.theme !== currentTheme?.theme), + (t) => t.theme + ); + }, [themes, currentTheme]); + + const schema = yup.object({ + theme: yup + .string() + .required(t("name_mandatory_error")) + .test("is-unique", t("name_unique_error"), (value) => !themeNames.includes(value)), + table: yup.string(), + help: yup.string(), + global: yup.boolean(), + }); + + const { + register, + formState: { errors }, + setValue: setFormValue, + getValues: getFormValues, + handleSubmit, + } = useForm({ + mode: "onSubmit", + values: { + theme: currentTheme?.theme ?? "", + table: currentTheme?.database ? `${currentTheme?.database}:${currentTheme?.table}` : "", + global: currentTheme?.global === undefined ? false : currentTheme?.global, + help: currentTheme?.help, + }, + resolver: yupResolver(schema), + }); + + const onSubmit = () => { + EditThemeDialogModal.close(); + if (currentTheme) { + const values = getFormValues(); + onModify(currentTheme?.theme, values); + } + }; + + return ( + <> + {createPortal( + +
+

{tCommon("mandatory_fields")}

+ + {currentTheme?.table ? ( +
+ ) : ( + { + setFormValue("global", checked); + }} + /> + )} + +
+ , + document.body + )} + + ); +}; + +export { EditThemeDialog, EditThemeDialogModal }; diff --git a/assets/espaceco/pages/communities/management/reports/ThemeList.tsx b/assets/espaceco/pages/communities/management/reports/ThemeList.tsx new file mode 100644 index 00000000..772813a9 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ThemeList.tsx @@ -0,0 +1,167 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { CSSProperties, FC, useState } from "react"; +import { UseFormReturn } from "react-hook-form"; +import { TableResponseDTO, ThemeDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { AddThemeDialog, AddThemeDialogModal } from "./AddThemeDialog"; +import { EditThemeDialog, EditThemeDialogModal } from "./EditThemeDialog"; +import normalizeTheme from "./ThemeUtils"; +import { cx } from "@codegouvfr/react-dsfr/tools/cx"; + +const customStyle: CSSProperties = { + border: "solid 1.5px", + borderColor: fr.colors.decisions.border.actionHigh.blueFrance.default, +}; + +const themeStyle: CSSProperties = { + backgroundColor: fr.colors.decisions.background.contrast.grey.default, +}; + +type ThemeListProps = { + form: UseFormReturn; + tables: Partial[]; +}; + +const ThemeList: FC = ({ form, tables }) => { + const { t } = useTranslation("ManageCommunity"); + const { t: tTheme } = useTranslation("Theme"); + + const { watch, setValue: setFormValue, getValues: getFormValues } = form; + const attributes: ThemeDTO[] = watch("attributes"); + + const [currentTheme, setCurrentTheme] = useState(); + + // Supression d'un theme + const handleRemoveTheme = (theme: string) => { + const a = attributes.filter((a) => a.theme !== theme); + setFormValue("attributes", a); + }; + + // Suppression d'un attribut de theme + const handleRemoveAttribute = (theme: string, attribute: string) => { + const a = Array.from(attributes, (t) => { + if (t.theme === theme) { + const attr = t.attributes.filter((a) => a.name !== attribute); + return { ...t, attributes: attr }; + } + return t; + }); + setFormValue("attributes", a); + }; + + return ( +
+

{t("report.configure_themes")}

+ {t("report.configure_themes.explain")} +
+ + {attributes.map((t) => ( +
+
+
+ {t.theme} + {t.table && ( + + + + )} + {t.global !== undefined && t.global === true && ( + + + + )} +
+
+
+
+
+
+ {t.table === undefined && ( +
+
+
    + {t.attributes.map((a) => ( +
  • +
    +
    {a.name}
    +
    +
    +
    +
    +
    +
  • + ))} +
+ +
+
+ )} +
+ ))} +
+ { + 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); + }} + /> +
+ ); +}; + +export default ThemeList; diff --git a/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx b/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx new file mode 100644 index 00000000..814ffc01 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx @@ -0,0 +1,71 @@ +import { declareComponentKeys, Translations } from "../../../../../i18n/i18n"; + +// traductions +export const { i18n } = declareComponentKeys< + | "add_theme" + | { K: "modify_theme"; P: { text: string }; R: string } + | { K: "delete_theme"; P: { text: string }; R: string } + | "name" + | "name_hint" + | "name_mandatory_error" + | "name_unique_error" + | "trimmed_error" + | "global" + | "global_hint" + | "description" + | "add_attribute" + | { K: "modify_attribute"; P: { text: string }; R: string } + | { K: "delete_attribute"; P: { text: string }; R: string } + | "dialog.add.name" + | "dialog.add.link_to_table" + | "dialog.add.link_to_table_hint" + | "dialog.add.not_link" + | "dialog.add.help" + | "dialog.add.help_hint" +>()("Theme"); + +export const ThemeFrTranslations: Translations<"fr">["Theme"] = { + add_theme: "Ajouter un thème", + modify_theme: ({ text }) => `Modifier le thème [${text}]`, + delete_theme: ({ text }) => `Supprimer le thème [${text}]`, + name: "Nouveau nom", + name_hint: "Attention, si vous modifiez le nom du thème, les utilisateurs devront recocher le thème dans leur profil pour y avoir accès.", + name_mandatory_error: "Le nom du thème est obligatoire", + name_unique_error: "Le nom doit être unique", + trimmed_error: "La chaîne de caractères ne doit contenir aucun espace en début et fin", + global: "Partage", + global_hint: "Partager ce thème le rendra visible et sélectionnable pour des signalement non liés à des guichets", + description: "Nouvelle description (optionnel)", + add_attribute: "Ajouter un attribut", + modify_attribute: ({ text }) => `Modifier l'attribut [${text}]`, + delete_attribute: ({ text }) => `Supprimer l'attribut [${text}]`, + "dialog.add.name": "Nom du thème", + "dialog.add.link_to_table": "Lier le thème à une base de données et à une table", + "dialog.add.link_to_table_hint": "Un thème lié à une base de données et à une table ne pourra pas être modifié", + "dialog.add.not_link": "Ne pas lier à une table", + "dialog.add.help": "Texte d'aide (optionnel)", + "dialog.add.help_hint": "Cette description aidera les membres à décrire plus précisément leur signalement", +}; + +export const ThemeEnTranslations: Translations<"en">["Theme"] = { + add_theme: "Add Theme", + modify_theme: ({ text }) => `Modify theme [${text}]`, + delete_theme: ({ text }) => `Delete theme [${text}]`, + name: undefined, + name_hint: undefined, + name_mandatory_error: undefined, + name_unique_error: undefined, + trimmed_error: undefined, + global: undefined, + global_hint: undefined, + description: undefined, + add_attribute: undefined, + modify_attribute: ({ text }) => `Modify attribute [${text}]`, + delete_attribute: ({ text }) => `Delete attribute [${text}]`, + "dialog.add.name": undefined, + "dialog.add.link_to_table": undefined, + "dialog.add.link_to_table_hint": undefined, + "dialog.add.not_link": undefined, + "dialog.add.help": undefined, + "dialog.add.help_hint": undefined, +}; diff --git a/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx b/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx new file mode 100644 index 00000000..8813aebb --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx @@ -0,0 +1,13 @@ +import { ThemeDTO } from "../../../../../@types/espaceco"; + +const normalizeTheme = (theme: ThemeDTO) => { + const result = { ...theme }; + ["global", "help"].forEach((f) => { + if (f in result && !result[f]) { + delete result[f]; + } + }); + return result; +}; + +export default normalizeTheme; diff --git a/assets/i18n/i18n.ts b/assets/i18n/i18n.ts index 0fbfd752..78a3ca23 100644 --- a/assets/i18n/i18n.ts +++ b/assets/i18n/i18n.ts @@ -53,7 +53,8 @@ export type ComponentKey = | typeof import("../espaceco/pages/communities/CommunityListTr").i18n | 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/SearchTr").i18n + | typeof import("../espaceco/pages/communities/management/reports/ThemeTr").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 e553be2e..b849735d 100644 --- a/assets/i18n/languages/en.tsx +++ b/assets/i18n/languages/en.tsx @@ -36,6 +36,7 @@ import { BreadcrumbEnTranslations } from "../Breadcrumb"; import { commonEnTranslations } from "../Common"; import { RightsEnTranslations } from "../Rights"; import { StyleEnTranslations } from "../Style"; +import { ThemeEnTranslations } from "../../espaceco/pages/communities/management/reports/ThemeTr"; import type { Translations } from "../i18n"; @@ -77,5 +78,6 @@ export const translations: Translations<"en"> = { CommunityList: CommunityListEnTranslations, ManageCommunity: ManageCommunityEnTranslations, ManageCommunityValidations: ManageCommunityValidationsEnTranslations, + Theme: ThemeEnTranslations, Search: SearchEnTranslations, }; diff --git a/assets/i18n/languages/fr.tsx b/assets/i18n/languages/fr.tsx index 0cb5afdf..dda493ff 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 { ThemeFrTranslations } from "../../espaceco/pages/communities/management/reports/ThemeTr"; import type { Translations } from "../i18n"; @@ -77,5 +78,6 @@ export const translations: Translations<"fr"> = { CommunityList: CommunityListFrTranslations, ManageCommunity: ManageCommunityFrTranslations, ManageCommunityValidations: ManageCommunityValidationsFrTranslations, + Theme: ThemeFrTranslations, Search: SearchFrTranslations, }; diff --git a/assets/modules/espaceco/RQKeys.ts b/assets/modules/espaceco/RQKeys.ts index 18028e33..38804c8f 100644 --- a/assets/modules/espaceco/RQKeys.ts +++ b/assets/modules/espaceco/RQKeys.ts @@ -14,6 +14,7 @@ const RQKeys = { ], searchAddress: (search: string): string[] => ["searchAddress", search], searchGrids: (text: string): string[] => ["searchGrids", text], + tables: (communityId: number): string[] => ["feature_types", communityId.toString()], }; export default RQKeys; diff --git a/src/Controller/EspaceCo/PermissionController.php b/src/Controller/EspaceCo/PermissionController.php new file mode 100644 index 00000000..b5950f62 --- /dev/null +++ b/src/Controller/EspaceCo/PermissionController.php @@ -0,0 +1,89 @@ + true], + condition: 'request.isXmlHttpRequest()' +)] +class PermissionController extends AbstractController implements ApiControllerInterface +{ + public const LEVELS = [ + 'NONE' => 0, + 'VIEW' => 1, + 'EXPORT' => 2, + 'EDIT' => 3, + 'ADMIN' => 4, + ]; + + public function __construct( + private PermissionApiService $permissionApiService, + private DatabaseApiService $databaseApiService + ) { + } + + /** + * Recupere les tables pouvant être utilisées pour theme d'une communauté. + */ + #[Route('/get_themable_tables/{communityId}', name: 'get_themable_tables_by_community', methods: ['GET'])] + public function getThemableTables(int $communityId): JsonResponse + { + try { + $tablesToremove = []; // Les tables a supprimer (celles qui ont une permission NONE ou ADMIN) + + $response = []; + + $permissions = $this->permissionApiService->getAllByCommunity($communityId); + foreach ($permissions as $permission) { + $tableId = $permission['table']; + if ('NONE' === $permission['level'] || 'ADMIN' === $permission['level']) { + if (!is_null($tableId)) { + // Table a supprimer + $fullName = $this->databaseApiService->getTableFullName($permission['database'], $tableId); + if (!in_array($fullName, $tablesToremove)) { + $tablesToremove[] = $fullName; + } + } + continue; + } + + if (is_null($permission['table'])) { // Ajout de toutes les tables + // TODO Ajouter columns + $tables = $this->databaseApiService->getAllTables($permission['database'], ['id', 'database_id', 'full_name'/* , 'columns' */]); + foreach ($tables as $table) { + $response[] = $table; + } + } + } + + $response = array_filter($response, function ($table, $name) use ($tablesToremove) { + return !in_array($name, $tablesToremove); + }, ARRAY_FILTER_USE_BOTH); + + usort($response, function ($a, $b) { + $fna = $a['full_name']; + $fnb = $b['full_name']; + if ($fna === $fnb) { + return 0; + } + + return ($fna < $fnb) ? -1 : 1; + }); + + return new JsonResponse(array_unique($response, SORT_REGULAR)); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } +} diff --git a/src/Services/EspaceCoApi/DatabaseApiService.php b/src/Services/EspaceCoApi/DatabaseApiService.php new file mode 100644 index 00000000..4c41424a --- /dev/null +++ b/src/Services/EspaceCoApi/DatabaseApiService.php @@ -0,0 +1,29 @@ +request('GET', "databases/$databaseId/tables/$tableId"); + } + + public function getTableFullName(int $databaseId, int $tableId): string + { + return $this->request('GET', "databases/$databaseId/tables/$tableId", [], ['fields' => 'full_name']); + } + + /** + * @param array $fields + */ + public function getAllTables(int $databaseId, array $fields = []): array + { + return $this->requestAll("databases/$databaseId/tables", ['fields' => $fields]); + } + + public function getColumns(int $databaseId, int $tableId): array + { + return $this->request('GET', "databases/$databaseId/tables/$tableId", [], ['fields' => 'columns']); + } +} diff --git a/src/Services/EspaceCoApi/PermissionApiService.php b/src/Services/EspaceCoApi/PermissionApiService.php new file mode 100644 index 00000000..f0054478 --- /dev/null +++ b/src/Services/EspaceCoApi/PermissionApiService.php @@ -0,0 +1,11 @@ +requestAll('permissions', ['group' => $communityId]); + } +}