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/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/app_espaceco.ts b/assets/@types/app_espaceco.ts index 0359da8f..f9c963f0 100644 --- a/assets/@types/app_espaceco.ts +++ b/assets/@types/app_espaceco.ts @@ -1,3 +1,5 @@ +import { GridDTO, ReportStatusesDTO, SharedGeorem, SharedThemesDTO, ThemeDTO, UserDTO } from "./espaceco"; + export type GetResponse = { content: T[]; totalPages: number; @@ -7,3 +9,78 @@ export type GetResponse = { export const arrCommunityListFilters = ["public", "iam_member", "affiliation"]; export type CommunityListFilter = (typeof arrCommunityListFilters)[number]; + +export type Address = { + country: string; + city: string; + x: number; + y: number; + zipcode: string; + street: string; + classification: number; + kind: string; + fulltext: string; + metropole: boolean; +}; + +export type Poi = { + country: string; + city: string; + x: number; + y: number; + zipcode: string; + zipcodes: string[]; + poiType: string[]; + street: string; + classification: number; + kind: string; + fulltext: string; + metropole: boolean; +}; + +export type SearchResult = { + status: string; + results: (Address | Poi)[]; +}; + +export type SearchGridFilters = { + searchBy?: ("name" | "title")[]; + fields?: ("name" | "title" | "type" | "extent" | "deleted")[]; + adm?: boolean; +}; + +export type UserType = { + user_id: number; + username: string; + firstname: string | null; + surname: string | null; +}; + +export type Role = "pending" | "member" | "admin"; +export type CommunityMember = UserType & { + grids: GridDTO[]; + role: Role; + active: boolean; + date: string; +}; + +/* FORMULAIRES */ +export type ReportFormType = { + attributes: ThemeDTO[]; + report_statuses: ReportStatusesDTO; + shared_themes?: SharedThemesDTO[]; + shared_georem: SharedGeorem; + all_members_can_valid: boolean; +}; + +export type DescriptionFormType = { + name: string; + description?: string; + keywords?: string[]; +}; + +const isUser = (v: UserDTO | string): v is UserDTO => { + return (v as UserDTO).username !== undefined; +}; + +export { isUser }; diff --git a/assets/@types/espaceco.ts b/assets/@types/espaceco.ts index 019b579f..8f5428ab 100644 --- a/assets/@types/espaceco.ts +++ b/assets/@types/espaceco.ts @@ -1,29 +1,196 @@ +import statuses from "../data/report_statuses.json"; + +export interface ConstraintsDTO { + minLength?: number; + maxLength?: number; + minValue?: string; + maxValue?: string; + pattern?: string; +} + +export const AttributeTypes = ["text", "integer", "double", "checkbox", "list", "date"]; + +export type AttributeType = (typeof AttributeTypes)[number]; +export type AttributeDTO = { + name: string; + type: AttributeType; + default?: string | null; + mandatory?: boolean; + multiple?: boolean; + values?: string[] | null; + help?: string | null; + title?: string; + input_constraints?: ConstraintsDTO | null; + json_schema?: object | null; + required?: boolean; + condition_field?: string; +}; + +export interface ThemeDTO { + theme: string; + database?: string; + table?: string; + attributes: AttributeDTO[]; + help?: string; + global?: boolean; +} + +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: object[]; + attributes: ThemeDTO[]; default_comment: string | null; - position: string; + position: string | null; zoom: number; + zoom_min: number | null; + zoom_max: number | null; + extent: number[] | null; all_members_can_valid: boolean; open_without_affiliation: boolean; open_with_email?: string[]; offline_allowed: boolean; - shared_extractions: boolean; /** @format date-time */ creation: string; - grids: Grids[]; - logo_url: string; + grids: GridDTO[]; + logo_url: string | null; + keywords?: string[]; + documents?: DocumentDTO[]; + report_statuses?: ReportStatusesDTO; + shared_themes?: SharedThemesDTO[]; } -export interface Grids { - name: string; +export interface UserDTO { + id: number; + username: string; + firstname?: string; + surname?: string; +} + +export interface DocumentDTO { + id: number; + short_fileName: string; + mime_type: string; + description?: string; title: string; type: string; + size?: number; + width?: number; + height?: number; + date: string; + geometry?: string; + uri: string; +} +export interface GridDTO { + name: string; + title: string; + type: GridType; deleted: boolean; + extent: number[]; +} + +export interface GridType { + name: string; + title: string; +} +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[]; +} + +export { SharedGeoremOptions }; diff --git a/assets/components/Input/AutocompleteSelect.tsx b/assets/components/Input/AutocompleteSelect.tsx index a88cd8e4..ac1c091b 100644 --- a/assets/components/Input/AutocompleteSelect.tsx +++ b/assets/components/Input/AutocompleteSelect.tsx @@ -1,14 +1,14 @@ import { fr } from "@codegouvfr/react-dsfr"; import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; import { Autocomplete, AutocompleteFreeSoloValueMapping, AutocompleteValue, CreateFilterOptionsConfig, TextField, createFilterOptions } from "@mui/material"; -import { CSSProperties, useId } from "react"; +import { CSSProperties, ReactNode, useId } from "react"; import { ControllerRenderProps } from "react-hook-form"; import { symToStr } from "tsafe/symToStr"; interface AutocompleteSelectProps { id?: string; label: string; - hintText: string; + hintText?: ReactNode; state?: "default" | "error" | "success"; stateRelatedMessage?: string; defaultValue?: T[]; @@ -76,7 +76,6 @@ const AutocompleteSelect = (props: AutocompleteSelectProps) => { {label} {hintText && {hintText}} - void; center?: number[]; @@ -28,7 +32,7 @@ type ZoomRangeProps = { const ZoomRange: FC = (props) => { const { data: capabilities } = useCapabilities(); - const { min, max, values, center = olDefaults.center, onChange } = props; + const { label, hintText, min, max, disableSlider, values, onChange, small = false, center = olDefaults.center } = props; // References sur les deux cartes const leftMapRef = useRef(); @@ -91,18 +95,18 @@ const ZoomRange: FC = (props) => { useEffect(() => { if (leftMapTargetRef.current) { - leftMapRef.current = createMap(leftMapTargetRef.current, olDefaults.zoom_levels.TOP); + leftMapRef.current = createMap(leftMapTargetRef.current, Math.max(min, olDefaults.zoom_levels.TOP)); } if (rightMapTargetRef.current) { - rightMapRef.current = createMap(rightMapTargetRef.current, olDefaults.zoom_levels.BOTTOM); + rightMapRef.current = createMap(rightMapTargetRef.current, Math.min(max, olDefaults.zoom_levels.BOTTOM)); } return () => { leftMapRef.current?.setTarget(undefined); rightMapRef.current?.setTarget(undefined); }; - }, [createMap]); + }, [min, max, createMap]); useEffect(() => { leftMapRef.current?.getView().setZoom(values[0]); @@ -114,11 +118,43 @@ const ZoomRange: FC = (props) => { return (
-
-
-
+ {label && ( + + )} +
+
+
- onChange(newValues)} /> + { + const v = [...values]; + v[0] = Number(e.currentTarget.value); + onChange(v); + }, + }, + { + value: values[1], + onChange: (e) => { + const v = [...values]; + v[1] = Number(e.currentTarget.value); + onChange(v); + }, + }, + ]} + />
); }; diff --git a/assets/config/datastoreNavItems.ts b/assets/config/datastoreNavItems.ts index 20746dd2..2f36eef7 100644 --- a/assets/config/datastoreNavItems.ts +++ b/assets/config/datastoreNavItems.ts @@ -55,6 +55,11 @@ export const datastoreNavItems = (currentDatastore?: Datastore): MainNavigationP }); } + navItems.push({ + text: t("espaceco"), + linkProps: routes.espaceco_community_list().link, + }); + navItems.push(assistanceNavItems(tNavItems)); return navItems; @@ -62,7 +67,7 @@ export const datastoreNavItems = (currentDatastore?: Datastore): MainNavigationP // traductions export const { i18n } = declareComponentKeys< - "dashboard" | "data" | "members" | "manage_storage" | "consumption_monitoring" | "permissions_granted" | "my_account" | "my_access_keys" + "dashboard" | "data" | "members" | "manage_storage" | "consumption_monitoring" | "permissions_granted" | "my_account" | "my_access_keys" | "espaceco" >()("datastoreNavItems"); export const datastoreNavItemsFrTranslations: Translations<"fr">["datastoreNavItems"] = { @@ -74,6 +79,7 @@ export const datastoreNavItemsFrTranslations: Translations<"fr">["datastoreNavIt permissions_granted: "Permissions accordées", my_account: "Mon compte", my_access_keys: "Mes clés d’accès", + espaceco: "Espace Collaboratif", }; export const datastoreNavItemsEnTranslations: Translations<"en">["datastoreNavItems"] = { @@ -85,4 +91,5 @@ export const datastoreNavItemsEnTranslations: Translations<"en">["datastoreNavIt permissions_granted: undefined, my_account: undefined, my_access_keys: undefined, + espaceco: undefined, }; diff --git a/assets/data/doc_thumbnail.json b/assets/data/doc_thumbnail.json new file mode 100644 index 00000000..9471fc20 --- /dev/null +++ b/assets/data/doc_thumbnail.json @@ -0,0 +1,378 @@ +{ + "7z": { + "src": "build/img/vignettes/7z.png", + "title": "Document de type 7Z" + }, + "abw": { + "src": "build/img/vignettes/abw.png", + "title": "Document de type ABW" + }, + "ai": { + "src": "build/img/vignettes/ai.png", + "title": "Document de type AI" + }, + "aiff": { + "src": "build/img/vignettes/aiff.png", + "title": "Document de type AIFF" + }, + "asf": { + "src": "build/img/vignettes/asf.png", + "title": "Document de type ASF" + }, + "avi": { + "src": "build/img/vignettes/avi.png", + "title": "Document de type AVI" + }, + "bin": { + "src": "build/img/vignettes/bin.png", + "title": "Document de type BIN" + }, + "blend": { + "src": "build/img/vignettes/blend.png", + "title": "Document de type BLEND" + }, + "bmp": { + "src": "build/img/vignettes/bmp.png", + "title": "Document de type BMP" + }, + "bz2": { + "src": "build/img/vignettes/bz2.png", + "title": "Document de type BZ2" + }, + "c": { + "src": "build/img/vignettes/c.png", + "title": "Document de type C" + }, + "crq": { + "src": "build/img/vignettes/crq.png", + "title": "Document de type CRQ" + }, + "css": { + "src": "build/img/vignettes/css.png", + "title": "Document de type CSS" + }, + "csv": { + "src": "build/img/vignettes/csv.png", + "title": "Document de type CSV" + }, + "deb": { + "src": "build/img/vignettes/deb.png", + "title": "Document de type DEB" + }, + "defaut": { + "src": "build/img/vignettes/defaut.png", + "title": "Document de type DEFAUT" + }, + "djvu": { + "src": "build/img/vignettes/djvu.png", + "title": "Document de type DJVU" + }, + "doc": { + "src": "build/img/vignettes/doc.png", + "title": "Document de type DOC" + }, + "docx": { + "src": "build/img/vignettes/docx.png", + "title": "Document de type DOCX" + }, + "dvi": { + "src": "build/img/vignettes/dvi.png", + "title": "Document de type DVI" + }, + "dxf": { + "src": "build/img/vignettes/dxf.png", + "title": "Document de type DXF" + }, + "eps": { + "src": "build/img/vignettes/eps.png", + "title": "Document de type EPS" + }, + "extension_thumbnail": { + "src": "build/img/vignettes/extension_thumbnail.png", + "title": "Document de type EXTENSION_THUMBNAIL" + }, + "flv": { + "src": "build/img/vignettes/flv.png", + "title": "Document de type FLV" + }, + "gif": { + "src": "build/img/vignettes/gif.png", + "title": "Document de type GIF" + }, + "gpx": { + "src": "build/img/vignettes/gpx.png", + "title": "Document de type GPX" + }, + "gxt": { + "src": "build/img/vignettes/gxt.png", + "title": "Document de type GXT" + }, + "gz": { + "src": "build/img/vignettes/gz.png", + "title": "Document de type GZ" + }, + "h": { + "src": "build/img/vignettes/h.png", + "title": "Document de type H" + }, + "html": { + "src": "build/img/vignettes/html.png", + "title": "Document de type HTML" + }, + "jpg": { + "src": "build/img/vignettes/jpg.png", + "title": "Document de type JPG" + }, + "kml": { + "src": "build/img/vignettes/kml.png", + "title": "Document de type KML" + }, + "kmz": { + "src": "build/img/vignettes/kmz.png", + "title": "Document de type KMZ" + }, + "mid": { + "src": "build/img/vignettes/mid.png", + "title": "Document de type MID" + }, + "mka": { + "src": "build/img/vignettes/mka.png", + "title": "Document de type MKA" + }, + "mkv": { + "src": "build/img/vignettes/mkv.png", + "title": "Document de type MKV" + }, + "mng": { + "src": "build/img/vignettes/mng.png", + "title": "Document de type MNG" + }, + "mov": { + "src": "build/img/vignettes/mov.png", + "title": "Document de type MOV" + }, + "mp3": { + "src": "build/img/vignettes/mp3.png", + "title": "Document de type MP3" + }, + "mp4": { + "src": "build/img/vignettes/mp4.png", + "title": "Document de type MP4" + }, + "mpg": { + "src": "build/img/vignettes/mpg.png", + "title": "Document de type MPG" + }, + "odb": { + "src": "build/img/vignettes/odb.png", + "title": "Document de type ODB" + }, + "odc": { + "src": "build/img/vignettes/odc.png", + "title": "Document de type ODC" + }, + "odf": { + "src": "build/img/vignettes/odf.png", + "title": "Document de type ODF" + }, + "odg": { + "src": "build/img/vignettes/odg.png", + "title": "Document de type ODG" + }, + "odi": { + "src": "build/img/vignettes/odi.png", + "title": "Document de type ODI" + }, + "odm": { + "src": "build/img/vignettes/odm.png", + "title": "Document de type ODM" + }, + "odp": { + "src": "build/img/vignettes/odp.png", + "title": "Document de type ODP" + }, + "ods": { + "src": "build/img/vignettes/ods.png", + "title": "Document de type ODS" + }, + "odt": { + "src": "build/img/vignettes/odt.png", + "title": "Document de type ODT" + }, + "ogg": { + "src": "build/img/vignettes/ogg.png", + "title": "Document de type OGG" + }, + "otg": { + "src": "build/img/vignettes/otg.png", + "title": "Document de type OTG" + }, + "otp": { + "src": "build/img/vignettes/otp.png", + "title": "Document de type OTP" + }, + "ots": { + "src": "build/img/vignettes/ots.png", + "title": "Document de type OTS" + }, + "ott": { + "src": "build/img/vignettes/ott.png", + "title": "Document de type OTT" + }, + "pas": { + "src": "build/img/vignettes/pas.png", + "title": "Document de type PAS" + }, + "pdf": { + "src": "build/img/vignettes/pdf.png", + "title": "Document de type PDF" + }, + "pgn": { + "src": "build/img/vignettes/pgn.png", + "title": "Document de type PGN" + }, + "png": { + "src": "build/img/vignettes/png.png", + "title": "Document de type PNG" + }, + "pps": { + "src": "build/img/vignettes/pps.png", + "title": "Document de type PPS" + }, + "ppt": { + "src": "build/img/vignettes/ppt.png", + "title": "Document de type PPT" + }, + "pptx": { + "src": "build/img/vignettes/pptx.png", + "title": "Document de type PPTX" + }, + "ps": { + "src": "build/img/vignettes/ps.png", + "title": "Document de type PS" + }, + "psd": { + "src": "build/img/vignettes/psd.png", + "title": "Document de type PSD" + }, + "qt": { + "src": "build/img/vignettes/qt.png", + "title": "Document de type QT" + }, + "ra": { + "src": "build/img/vignettes/ra.png", + "title": "Document de type RA" + }, + "ram": { + "src": "build/img/vignettes/ram.png", + "title": "Document de type RAM" + }, + "rm": { + "src": "build/img/vignettes/rm.png", + "title": "Document de type RM" + }, + "rpm": { + "src": "build/img/vignettes/rpm.png", + "title": "Document de type RPM" + }, + "rtf": { + "src": "build/img/vignettes/rtf.png", + "title": "Document de type RTF" + }, + "sdd": { + "src": "build/img/vignettes/sdd.png", + "title": "Document de type SDD" + }, + "sdw": { + "src": "build/img/vignettes/sdw.png", + "title": "Document de type SDW" + }, + "sit": { + "src": "build/img/vignettes/sit.png", + "title": "Document de type SIT" + }, + "smil": { + "src": "build/img/vignettes/smil.png", + "title": "Document de type SMIL" + }, + "spip": { + "src": "build/img/vignettes/spip.png", + "title": "Document de type SPIP" + }, + "svg": { + "src": "build/img/vignettes/svg.png", + "title": "Document de type SVG" + }, + "swf": { + "src": "build/img/vignettes/swf.png", + "title": "Document de type SWF" + }, + "sxc": { + "src": "build/img/vignettes/sxc.png", + "title": "Document de type SXC" + }, + "sxi": { + "src": "build/img/vignettes/sxi.png", + "title": "Document de type SXI" + }, + "sxw": { + "src": "build/img/vignettes/sxw.png", + "title": "Document de type SXW" + }, + "tex": { + "src": "build/img/vignettes/tex.png", + "title": "Document de type TEX" + }, + "tgz": { + "src": "build/img/vignettes/tgz.png", + "title": "Document de type TGZ" + }, + "tif": { + "src": "build/img/vignettes/tif.png", + "title": "Document de type TIF" + }, + "tiff": { + "src": "build/img/vignettes/tiff.png", + "title": "Document de type TIFF" + }, + "torrent": { + "src": "build/img/vignettes/torrent.png", + "title": "Document de type TORRENT" + }, + "ttf": { + "src": "build/img/vignettes/ttf.png", + "title": "Document de type TTF" + }, + "txt": { + "src": "build/img/vignettes/txt.png", + "title": "Document de type TXT" + }, + "wav": { + "src": "build/img/vignettes/wav.png", + "title": "Document de type WAV" + }, + "wmv": { + "src": "build/img/vignettes/wmv.png", + "title": "Document de type WMV" + }, + "xcf": { + "src": "build/img/vignettes/xcf.png", + "title": "Document de type XCF" + }, + "xls": { + "src": "build/img/vignettes/xls.png", + "title": "Document de type XLS" + }, + "xlsx": { + "src": "build/img/vignettes/xlsx.png", + "title": "Document de type XLSX" + }, + "xml": { + "src": "build/img/vignettes/xml.png", + "title": "Document de type XML" + }, + "zip": { + "src": "build/img/vignettes/zip.png", + "title": "Document de type ZIP" + } +} \ No newline at end of file diff --git a/assets/data/report_statuses.json b/assets/data/report_statuses.json new file mode 100644 index 00000000..73121f2c --- /dev/null +++ b/assets/data/report_statuses.json @@ -0,0 +1,11 @@ +{ + "submit": "Reçu dans nos services", + "pending0": "En demande de qualification", + "pending": "En cours de traitement", + "pending1": "En attente de saisie", + "pending2": "En attente de validation", + "valid": "Pris en compte", + "valid0": "Déjà pris en compte", + "reject": "Rejeté (hors spéc.)", + "reject0": "Rejeté (hors de propos)" +} \ No newline at end of file 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/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/community.ts b/assets/espaceco/api/community.ts index dabd71da..d8604994 100644 --- a/assets/espaceco/api/community.ts +++ b/assets/espaceco/api/community.ts @@ -1,17 +1,23 @@ import SymfonyRouting from "../../modules/Routing"; -import { CommunityListFilter, GetResponse } from "../../@types/app_espaceco"; +import { CommunityListFilter, CommunityMember, GetResponse, Role } from "../../@types/app_espaceco"; import { type CommunityResponseDTO } from "../../@types/espaceco"; 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, }); }; +const getCommunitiesName = () => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_names"); + return jsonFetch(url); +}; + const searchByName = (name: string, filter: CommunityListFilter, signal: AbortSignal) => { const queryParams = { name: `%${name}%`, filter: filter, sort: "name:ASC" }; const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_search", queryParams); @@ -28,6 +34,94 @@ const getAsMember = (queryParams: Record, signal: AbortSignal) }); }; -const community = { get, searchByName, getAsMember }; +const getCommunity = (communityId: number) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_community", { communityId }); + return jsonFetch(url); +}; + +const getCommunityMembers = (communityId: number, page: number, limit: number = 10, signal: AbortSignal) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_members", { + communityId, + page: page, + limit: limit, + roles: ["member", "admin"], + }); + return jsonFetch>(url, { + signal: signal, + }); +}; + +const getCommunityMembershipRequests = (communityId: number, signal: AbortSignal) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_get_members", { communityId, page: 1, limit: 50, roles: ["pending"] }); + return jsonFetch>(url, { + signal: signal, + }); +}; + +const updateMemberRole = (communityId: number, userId: number, role: Role) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_update_member_role", { communityId, userId }); + return jsonFetch( + url, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }, + { role: role } + ); +}; + +const updateMemberGrids = (communityId: number, userId: number, grids: string[]) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_update_member_grids", { communityId, userId }); + return jsonFetch( + url, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }, + { grids: grids } + ); +}; + +const updateLogo = (communityId: number, formData: FormData) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_update_logo", { communityId }); + return jsonFetch( + url, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }, + formData + ); +}; + +const removeMember = (communityId: number, userId: number) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_community_remove_member", { communityId, userId }); + return jsonFetch<{ user_id: number }>(url, { + method: "DELETE", + }); +}; + +const community = { + get, + getCommunitiesName, + getCommunity, + getCommunityMembers, + getCommunityMembershipRequests, + searchByName, + getAsMember, + updateMemberRole, + updateMemberGrids, + removeMember, + updateLogo, +}; export default community; diff --git a/assets/espaceco/api/grid.ts b/assets/espaceco/api/grid.ts new file mode 100644 index 00000000..d92e7cf8 --- /dev/null +++ b/assets/espaceco/api/grid.ts @@ -0,0 +1,32 @@ +import { GetResponse, SearchGridFilters } from "../../@types/app_espaceco"; +import { GridDTO } from "../../@types/espaceco"; +import { jsonFetch } from "../../modules/jsonFetch"; +import SymfonyRouting from "../../modules/Routing"; + +const search = (text: string, filters: SearchGridFilters, otherOptions: RequestInit = {}) => { + const queryParams = { text: `${text}%` }; + ["searchBy", "fields"].forEach((p) => { + if (filters[p] !== undefined) { + queryParams[p] = filters[p].join(","); + } + }); + if (filters.adm !== undefined) { + queryParams["adm"] = new Boolean(filters.adm).toString(); + } + + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_grid_search", queryParams); + return jsonFetch>(url, { + ...otherOptions, + }); +}; + +const fromNames = (names: string[], otherOptions: RequestInit = {}) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_grid_get_by_names", { names: names }); + return jsonFetch>(url, { + ...otherOptions, + }); +}; + +const grid = { search, fromNames }; + +export default grid; diff --git a/assets/espaceco/api/index.ts b/assets/espaceco/api/index.ts index e206c40a..8d8192ee 100644 --- a/assets/espaceco/api/index.ts +++ b/assets/espaceco/api/index.ts @@ -1,7 +1,13 @@ import community from "./community"; +import grid from "./grid"; +import permission from "./permission"; +import user from "./users"; const api = { + user, community, + permission, + grid, }; export default api; 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/api/users.ts b/assets/espaceco/api/users.ts new file mode 100644 index 00000000..5c66a576 --- /dev/null +++ b/assets/espaceco/api/users.ts @@ -0,0 +1,20 @@ +import { UserDTO, 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 search = (search: string, signal: AbortSignal) => { + const url = SymfonyRouting.generate("cartesgouvfr_api_espaceco_user_search", { search }); + return jsonFetch(url, { + method: "GET", + signal: signal, + }); +}; + +const user = { search, getSharedThemes }; + +export default user; diff --git a/assets/espaceco/pages/communities/Communities.tsx b/assets/espaceco/pages/communities/Communities.tsx index 22580128..5eaf6d59 100644 --- a/assets/espaceco/pages/communities/Communities.tsx +++ b/assets/espaceco/pages/communities/Communities.tsx @@ -4,9 +4,12 @@ import { Pagination } from "@codegouvfr/react-dsfr/Pagination"; import RadioButtons from "@codegouvfr/react-dsfr/RadioButtons"; import { useQuery } from "@tanstack/react-query"; import { FC, useMemo, useState } from "react"; + import { CommunityListFilter, GetResponse, arrCommunityListFilters } from "../../../@types/app_espaceco"; import { CommunityResponseDTO } from "../../../@types/espaceco"; +import AppLayout from "../../../components/Layout/AppLayout"; import Skeleton from "../../../components/Utils/Skeleton"; +import { datastoreNavItems } from "../../../config/datastoreNavItems"; import { useTranslation } from "../../../i18n/i18n"; import RQKeys from "../../../modules/espaceco/RQKeys"; import { CartesApiException } from "../../../modules/jsonFetch"; @@ -17,6 +20,8 @@ import SearchCommunity from "./SearchCommunity"; const defaultLimit = 10; +const navItems = datastoreNavItems(); + type QueryParamsType = { page: number; limit: number; @@ -25,7 +30,8 @@ type QueryParamsType = { const Communities: FC = () => { const route = useRoute(); - const { t } = useTranslation("EspaceCoCommunities"); + const { t } = useTranslation("CommunityList"); + const { t: tBreadcrumb } = useTranslation("Breadcrumb"); const filter = useMemo(() => { const f = route.params["filter"]; @@ -45,18 +51,18 @@ const Communities: FC = () => { const [community, setCommunity] = useState(null); const communityQuery = useQuery, CartesApiException>({ - queryKey: RQKeys.community_list(queryParams.page, queryParams.limit), + queryKey: RQKeys.communityList(queryParams.page, queryParams.limit), queryFn: ({ signal }) => api.community.get(queryParams, signal), staleTime: 3600000, - retry: false, + //retry: false, enabled: filter === "public", }); const communitiesAsMember = useQuery, CartesApiException>({ - queryKey: RQKeys.communities_as_member(queryParams.pending ?? false, queryParams.page, queryParams.limit), + queryKey: RQKeys.communitiesAsMember(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", }); @@ -66,7 +72,15 @@ const Communities: FC = () => { }; return ( -
+

{t("title")}

{communityQuery.isError && } @@ -151,7 +165,7 @@ const Communities: FC = () => { )}
-
+ ); }; diff --git a/assets/espaceco/pages/communities/CommunityListItem.tsx b/assets/espaceco/pages/communities/CommunityListItem.tsx index e832e7b6..87535e30 100644 --- a/assets/espaceco/pages/communities/CommunityListItem.tsx +++ b/assets/espaceco/pages/communities/CommunityListItem.tsx @@ -9,21 +9,36 @@ import { useTranslation } from "../../../i18n/i18n"; import placeholder1x1 from "../../../img/placeholder.1x1.png"; import "../../../sass/pages/espaceco/community.scss"; +import { routes } from "../../../router/router"; +import { useApiEspaceCoStore } from "../../../stores/ApiEspaceCoStore"; type CommunityListItemProps = { className?: string; community: CommunityResponseDTO; }; const CommunityListItem: FC = ({ className, community }) => { - const { t } = useTranslation("EspaceCoCommunities"); + const { t } = useTranslation("CommunityList"); + const { t: tCommon } = useTranslation("Common"); + + /* TODO PROVISOIRE A SUPPRIMER ------------- */ + const url = useApiEspaceCoStore((state) => state.api_espaceco_url); + const espacecoUrl = url?.replace("/gcms/api", ""); + const appEnv = document.getElementById("root")?.dataset?.appEnv?.toLowerCase(); + const link = + appEnv === "dev" + ? routes.espaceco_manage_community({ communityId: community.id }).link + : espacecoUrl + ? { href: `${espacecoUrl}/group/${community.id}`, target: "_blank" } + : { href: "#" }; + /* ----------------------------------------- */ const [showDescription, toggleShowDescription] = useToggle(false); return ( <> -
+
-
+
-
-
+
+
+
+
+
{community.detailed_description &&
} diff --git a/assets/espaceco/pages/communities/EspaceCoCommunitiesTr.ts b/assets/espaceco/pages/communities/CommunityListTr.ts similarity index 87% rename from assets/espaceco/pages/communities/EspaceCoCommunitiesTr.ts rename to assets/espaceco/pages/communities/CommunityListTr.ts index d4171b66..391cee39 100644 --- a/assets/espaceco/pages/communities/EspaceCoCommunitiesTr.ts +++ b/assets/espaceco/pages/communities/CommunityListTr.ts @@ -14,9 +14,9 @@ export const { i18n } = declareComponentKeys< | "no_options" | "loading" | "show_details" ->()("EspaceCoCommunities"); +>()("CommunityList"); -export const EspaceCoCommunitiesFrTranslations: Translations<"fr">["EspaceCoCommunities"] = { +export const CommunityListFrTranslations: Translations<"fr">["CommunityList"] = { title: "Liste des guichets", filters: "Filtres", all_public_communities: "Tous les guichets publics", @@ -36,7 +36,7 @@ export const EspaceCoCommunitiesFrTranslations: Translations<"fr">["EspaceCoComm show_details: "Afficher les détails", }; -export const EspaceCoCommunitiesEnTranslations: Translations<"en">["EspaceCoCommunities"] = { +export const CommunityListEnTranslations: Translations<"en">["CommunityList"] = { title: "List of communities", filters: "Filters", all_public_communities: undefined, diff --git a/assets/espaceco/pages/communities/ManageCommunity.tsx b/assets/espaceco/pages/communities/ManageCommunity.tsx new file mode 100644 index 00000000..71f5d5c2 --- /dev/null +++ b/assets/espaceco/pages/communities/ManageCommunity.tsx @@ -0,0 +1,114 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import Button from "@codegouvfr/react-dsfr/Button"; +import Tabs from "@codegouvfr/react-dsfr/Tabs"; +import { useQuery } from "@tanstack/react-query"; +import { FC, useState } from "react"; +import { CommunityResponseDTO } from "../../../@types/espaceco"; +import AppLayout from "../../../components/Layout/AppLayout"; +import LoadingText from "../../../components/Utils/LoadingText"; +import { datastoreNavItems } from "../../../config/datastoreNavItems"; +import { useTranslation } from "../../../i18n/i18n"; +import RQKeys from "../../../modules/espaceco/RQKeys"; +import { CartesApiException } from "../../../modules/jsonFetch"; +import { routes } from "../../../router/router"; +import api from "../../api"; +import Description from "./management/Description"; +import Grid from "./management/Grid"; +import Layer from "./management/Layer"; +import Reports from "./management/Reports"; +import ZoomAndCentering from "./management/ZoomAndCentering"; +import Members from "./management/Members"; + +type ManageCommunityProps = { + communityId: number; +}; + +const navItems = datastoreNavItems(); + +const ManageCommunity: FC = ({ communityId }) => { + const { t } = useTranslation("ManageCommunity"); + const { t: tBreadcrumb } = useTranslation("Breadcrumb"); + + const communityQuery = useQuery({ + queryKey: RQKeys.community(communityId), + queryFn: () => api.community.getCommunity(communityId), + staleTime: 3600000, + }); + + const [selectedTabId, setSelectedTabId] = useState("tab1"); + + return ( + +

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

+ {communityQuery.isError ? ( + +

{communityQuery.error?.message}

+ + + } + /> + ) : communityQuery.isLoading ? ( + + ) : ( + communityQuery.data && ( +
+ + <> + {(() => { + switch (selectedTabId) { + case "tab1": + return ; + case "tab3": + return ; + case "tab4": + return ; + case "tab6": + return ; + case "tab7": + return ; // TODO + case "tab8": + return ; + default: + return

`Content of ${selectedTabId}`

; + } + })()} + +
+
+ ) + )} +
+ ); +}; + +export default ManageCommunity; diff --git a/assets/espaceco/pages/communities/ManageCommunityTr.tsx b/assets/espaceco/pages/communities/ManageCommunityTr.tsx new file mode 100644 index 00000000..fbb713ed --- /dev/null +++ b/assets/espaceco/pages/communities/ManageCommunityTr.tsx @@ -0,0 +1,259 @@ +import { declareComponentKeys } from "i18nifty"; +import { Translations } from "../../../i18n/i18n"; + +export type logoAction = "add" | "modify" | "delete"; + +// traductions +export const { i18n } = declareComponentKeys< + | { K: "title"; P: { name: string | undefined }; R: string } + | "loading" + | "loading_tables" + | "fetch_failed" + | "back_to_list" + | "tab1" + | "tab2" + | "tab3" + | "tab4" + | "tab5" + | "tab6" + | "tab7" + | "tab8" + | "desc.tab.title" + | "desc.name" + | "desc.hint_name" + | "desc.description" + | "desc.hint_description" + | "desc.logo" + | "desc.logo.title" + | { K: "logo_action"; P: { action: logoAction }; R: string } + | "logo_confirm_delete_modal.title" + | "modal.logo.title" + | "modal.logo.file_hint" + | "desc.keywords" + | "desc.documents" + | "desc.documents_hint" + | "desc.no_documents" + | "desc.document.remove" + | "modal.document.title" + | "modal.document.name" + | "modal.document.file_hint" + | "zoom.consistant_error" + | "zoom.tab.title" + | "zoom.position" + | "zoom.position_hint" + | "zoom.zoom_range" + | "zoom.zoom_range_hint" + | "zoom.manage_extent" + | "zoom.extent" + | "zoom.extent_hint" + | "zoom.choice.autocomplete" + | "zoom.choice.manual" + | "zoom.extent_enter_manually" + | "zoom.xmin" + | "zoom.xmax" + | "zoom.ymin" + | "zoom.ymax" + | "layer.tab.title" + | "layer.tabl" + | "layer.tab2" + | "layer.tab3" + | "report.configure_themes" + | "report.configure_themes.explain" + | "report.configure_shared_themes" + | "report.configure_shared_themes.explain" + | "report.configure_statuses" + | "report.configure_statuses.explain" + | "report.manage_permissions" + | "report.manage_permissions.shared_report" + | "report.manage_permissions.shared_report_hint" + | { K: "report.manage_permissions.shared_report.option"; P: { option: string }; R: string } + | "report.manage_permissions.report_answers" + | "report.manage_permissions.authorize" + | "report.manage_permissions.authorize_hint" + | "grid.grids" + | { K: "grid.explain"; R: JSX.Element } +>()("ManageCommunity"); + +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", + tab2: "Bases de données", + tab3: "Zoom, centrage", + tab4: "Couches de la carte", + tab5: "Outils", + tab6: "Signalements", + tab7: "Emprises", + tab8: "Membres", + "desc.tab.title": "Décrire le guichet", + "desc.name": "Nom du guichet", + "desc.hint_name": "Donnez un nom clair et compréhensible", + "desc.description": "Description", + "desc.hint_description": "Bref résumé narratif de l'objectif du guichet", + "desc.logo": "Logo (optionnel)", + "desc.logo.title": "Ajouter, modifier ou supprimer le logo du guichet", + logo_action: ({ action }) => { + switch (action) { + case "add": + return "Ajouter un logo"; + case "modify": + return "Remplacer le logo"; + case "delete": + return "Supprimer le logo"; + } + }, + "logo_confirm_delete_modal.title": "Êtes-vous sûr de vouloir supprimer le logo de ce guichet ?", + "modal.logo.title": "Logo du guichet", + "modal.logo.file_hint": "Taille maximale : 5 Mo. Formats acceptés : jpg, png", + "desc.keywords": "Mots-clés (optionnel)", + "desc.documents": "Documents additionnels (optionnel)", + "desc.documents_hint": + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Dicta suscipit tempora culpa, ea quis illo veniam vero consequuntur soluta nesciunt.", + "desc.no_documents": "Aucun document", + "desc.document.remove": "Supprimer le document", + "modal.document.title": "Ajouter un document", + "modal.document.name": "Titre", + "modal.document.file_hint": "Taille maximale : 5 Mo.", + "zoom.consistant_error": "Emprise et position ne sont pas cohérents", + "zoom.tab.title": "Définir l’état initial de la carte à l’ouverture du guichet", + "zoom.position": "Position", + "zoom.position_hint": "Fixer la position et définissez le niveau de zoom (utilisez votre souris ou la barre de recherche ci-dessous", + "zoom.zoom_range": "Gérer les niveaux de zoom minimum et maximum permis (optionnel)", + "zoom.zoom_range_hint": + "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Libero quisquam hic veritatis, ex ipsum illo labore sint perspiciatis quidem architecto!", + "zoom.manage_extent": "Gérer les bornes de navigation (optionnel)", + "zoom.extent": "Bornes de navigation", + "zoom.extent_hint": + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime vitae maiores suscipit tempore sequi reiciendis nulla optio doloremque! Unde, illo nemo ab accusantium fugiat minus? Natus inventore dolore velit, nostrum dolores molestiae sint laborum, obcaecati, ullam provident repellat consectetur accusamus sunt rerum nobis sequi? Sed maxime fugit dolore! Ipsam, veritatis.", + "zoom.choice.autocomplete": "Recherche d'une emprise administrative", + "zoom.choice.manual": "Saisie manuelle", + "zoom.extent_enter_manually": "Entrer les coordonnées (lon,lat)", + "zoom.xmin": "X min", + "zoom.xmax": "X max", + "zoom.ymin": "Y min", + "zoom.ymax": "Y max", + "layer.tab.title": "Gérer les couches de la carte", + "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.", + "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 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": ( +

+ Ajouter une ou plusieurs emprises. +
+ Tous les membres du guichet y auront accès et pourront donc réaliser des contributions directes sur la base de données. +

+ ), +}; + +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, + tab2: undefined, + tab3: undefined, + tab4: undefined, + tab5: undefined, + tab6: undefined, + tab7: undefined, + tab8: undefined, + "desc.tab.title": undefined, + "desc.name": undefined, + "desc.hint_name": undefined, + "desc.description": undefined, + "desc.hint_description": undefined, + "desc.logo": undefined, + "desc.logo.title": undefined, + logo_action: ({ action }) => { + switch (action) { + case "add": + return "Add logo"; + case "modify": + return "Replace logo"; + case "delete": + return "Delete logo"; + } + }, + "logo_confirm_delete_modal.title": undefined, + "modal.logo.title": undefined, + "modal.logo.file_hint": undefined, + "desc.keywords": undefined, + "desc.documents": undefined, + "desc.documents_hint": undefined, + "desc.no_documents": "No document", + "desc.document.remove": "Remove document", + "modal.document.title": "Add document", + "modal.document.name": "Title", + "modal.document.file_hint": "Maximum file size : 5 Mo.", + "zoom.consistant_error": undefined, + "zoom.tab.title": undefined, + "zoom.position": "Position", + "zoom.position_hint": undefined, + "zoom.zoom_range": undefined, + "zoom.zoom_range_hint": undefined, + "zoom.manage_extent": undefined, + "zoom.extent": undefined, + "zoom.extent_hint": undefined, + "zoom.choice.autocomplete": undefined, + "zoom.choice.manual": undefined, + "zoom.extent_enter_manually": undefined, + "zoom.xmin": "X min", + "zoom.xmax": "X max", + "zoom.ymin": "Y min", + "zoom.ymax": "Y max", + "layer.tab.title": undefined, + "layer.tabl": "My datas", + "layer.tab2": "Geoplateforme datas", + "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/SearchCommunity.tsx b/assets/espaceco/pages/communities/SearchCommunity.tsx index ed34fff9..e35b20d7 100644 --- a/assets/espaceco/pages/communities/SearchCommunity.tsx +++ b/assets/espaceco/pages/communities/SearchCommunity.tsx @@ -17,12 +17,12 @@ type SearchCommunityProps = { }; const SearchCommunity: FC = ({ filter, onChange }) => { - const { t } = useTranslation("EspaceCoCommunities"); + const { t } = useTranslation("CommunityList"); const [search, setSearch] = useDebounceValue("", 500); const searchQuery = useQuery({ - queryKey: RQKeys.search(search, filter), + queryKey: RQKeys.searchCommunities(search, filter), queryFn: ({ signal }) => api.community.searchByName(search, filter, signal), enabled: search.length > 3, }); @@ -39,6 +39,7 @@ const SearchCommunity: FC = ({ filter, onChange }) => { noOptionsText={t("no_options")} getOptionLabel={(option) => option.name} options={searchQuery.data || []} + filterOptions={(x) => x} renderInput={(params) => ( void; + onAdd: (name: string, file: File) => void; +}; + +const AddDocumentDialogModal = createModal({ + id: "add-document-modal", + isOpenedByDefault: false, +}); + +const AddDocumentDialog: FC = ({ onCancel, onAdd }) => { + const { t: tCommon } = useTranslation("Common"); + const { t: tValid } = useTranslation("ManageCommunityValidations"); + const { t } = useTranslation("ManageCommunity"); + + const schema = (t: TranslationFunction<"ManageCommunityValidations", ComponentKey>) => + yup.object().shape({ + name: yup.string().min(7, t("description.modal.document.name.minlength")).required(t("description.modal.document.name.mandatory")), + document: yup + .mixed() + .test({ + name: "exists", + test(files, ctx) { + const file = files?.[0]; + return file === undefined ? ctx.createError({ message: t("description.modal.document.file.mandatory") }) : true; + }, + }) + .test("check-file-size", t("description.modal.document.file.size_error"), (files) => { + const file = files?.[0]; + if (file === undefined) return true; + + const size = file.size / 1024 / 1024; + return size < 5; + }), + }); + + const form = useForm({ + mode: "onChange", + resolver: yupResolver(schema(tValid)), + }); + const { + register, + getValues: getFormValues, + formState: { errors }, + handleSubmit, + resetField, + } = form; + + const clear = () => { + resetField("name"); + resetField("document"); + }; + + const onSubmit = () => { + const values = getFormValues(); + onAdd(values.name, values.document?.[0] as File); + clear(); + }; + + return ( + <> + {createPortal( + { + clear(); + onCancel(); + }, + priority: "secondary", + }, + { + children: tCommon("add"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + priority: "primary", + }, + ]} + > +
+ + +
+
, + document.body + )} + + ); +}; + +export { AddDocumentDialog, AddDocumentDialogModal }; diff --git a/assets/espaceco/pages/communities/management/CommunityLogo.tsx b/assets/espaceco/pages/communities/management/CommunityLogo.tsx new file mode 100644 index 00000000..8e65a053 --- /dev/null +++ b/assets/espaceco/pages/communities/management/CommunityLogo.tsx @@ -0,0 +1,261 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { createModal, ModalProps } from "@codegouvfr/react-dsfr/Modal"; +import { cx } from "@codegouvfr/react-dsfr/tools/cx"; +import { Upload } from "@codegouvfr/react-dsfr/Upload"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { TranslationFunction } from "i18nifty/typeUtils/TranslationFunction"; +import { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import { useHover } from "usehooks-ts"; +import * as yup from "yup"; +import { ConfirmDialog, ConfirmDialogModal } from "../../../../components/Utils/ConfirmDialog"; +import { ComponentKey, useTranslation } from "../../../../i18n/i18n"; +import { getFileExtension, getImageSize, ImageSize } from "../../../../utils"; +import { logoAction } from "../ManageCommunityTr"; + +import placeholder1x1 from "../../../../img/placeholder.1x1.png"; +import "../../../../sass/components/buttons.scss"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { CartesApiException } from "../../../../modules/jsonFetch"; +import { CommunityResponseDTO } from "../../../../@types/espaceco"; +import api from "../../../api"; + +type CommunityLogoProps = { + communityId: number; + logoUrl: string | null; +}; + +const AddLogoModal = createModal({ + id: "add-logo-modal", + isOpenedByDefault: false, +}); + +const schema = (t: TranslationFunction<"ManageCommunityValidations", ComponentKey>) => + yup.object().shape({ + logo: yup + .mixed() + .test("check-file-size", t("description.logo.size_error"), (files) => { + const file = files?.[0] ?? undefined; + + if (file instanceof File) { + const size = file.size / 1024 / 1024; + return size < 5; + } + return true; + }) + .test("check-file-dimensions", t("description.logo.dimensions_error"), async (files) => { + const file = files?.[0] ?? undefined; + if (file) { + const size: ImageSize = await getImageSize(file); + if (size.width > 400 || size.height > 400) { + return false; + } + } + return true; + }) + .test("check-file-type", t("description.logo.format_error"), (files) => { + const file = files?.[0] ?? undefined; + if (file) { + const extension = getFileExtension(file.name); + if (!extension) { + return false; + } + return ["jpg", "jpeg", "png"].includes(extension); + } + return true; + }), + }); + +const CommunityLogo: FC = ({ communityId, logoUrl }) => { + const { t: tCommon } = useTranslation("Common"); + const { t: tValidation } = useTranslation("ManageCommunityValidations"); + const { t } = useTranslation("ManageCommunity"); + + const [isValid, setIsValid] = useState(false); + useEffect(() => { + if (logoUrl) { + fetch(logoUrl).then((res) => { + setIsValid(() => res.status === 200); + }); + } + }, [logoUrl]); + + const action: logoAction = useMemo(() => (isValid ? "modify" : "add"), [isValid]); + + // Boite modale, gestion de l'image + const [modalImageUrl, setModalImageUrl] = useState(""); + const logoDivRef = useRef(null); + const logoIsHovered = useHover(logoDivRef); + + // const queryClient = useQueryClient(); + + // Ajout/modification du logo + const updateLogoMutation = useMutation({ + mutationFn: () => { + const form = new FormData(); + form.append("logo", upload); + return api.community.updateLogo(communityId, form); + }, + onSuccess: (response) => { + AddLogoModal.close(); + + // Mise à jour du contenu de la réponse de datasheetQuery + /*queryClient.setQueryData(RQKeys.datastore_datasheet(datastoreId, datasheetName), (datasheet) => { + if (datasheet) { + datasheet.thumbnail = response; + } + return datasheet; + }); + + // Mise à jour du contenu de la réponse de datasheetListQuery + queryClient.setQueryData(RQKeys.datastore_datasheet_list(datastoreId), (datasheetList = []) => { + return datasheetList.map((datasheet) => { + if (datasheet.name === datasheetName) { + datasheet.thumbnail = response; + } + return datasheet; + }); + });*/ + }, + onSettled: () => { + reset(); + }, + }); + + const { + register, + formState: { errors }, + watch, + resetField, + handleSubmit, + } = useForm({ resolver: yupResolver(schema(tValidation)), mode: "onChange" }); + + const upload: File = watch("logo")?.[0]; + + useEffect(() => { + if (upload !== undefined) { + const reader = new FileReader(); + reader.onload = () => { + setModalImageUrl(reader.result as string); + }; + reader.readAsDataURL(upload); + } + }, [upload]); + + const reset = useCallback(() => { + resetField("logo"); + setModalImageUrl(""); + }, [resetField]); + + const onSubmit = useCallback(async () => { + if (upload) { + // Ajout du logo + updateLogoMutation.mutate(); + } + }, [updateLogoMutation, upload]); + + // Boutons de la boite de dialogue + const AddModalButtons: [ModalProps.ActionAreaButtonProps, ...ModalProps.ActionAreaButtonProps[]] = useMemo(() => { + const btns: [ModalProps.ActionAreaButtonProps, ...ModalProps.ActionAreaButtonProps[]] = [ + { + children: tCommon("cancel"), + onClick: () => { + reset(); + // addThumbnailMutation.reset(); + }, + doClosesModal: true, + priority: "secondary", + }, + { + children: t("logo_action", { action: action }), + onClick: handleSubmit(onSubmit), + doClosesModal: false, + priority: "primary", + }, + ]; + + return btns; + }, [action, /*addThumbnailMutation,*/ handleSubmit, onSubmit, reset, t, tCommon]); + + return ( +
+ +
+ { + currentTarget.onerror = null; // prevents looping + currentTarget.src = placeholder1x1; + }} */ + /> + {logoIsHovered && ( +
+
+ )} +
+ {createPortal( + + {/* {addThumbnailMutation.isError && ( + + )} */} +
+
+ +
+
+ +
+
+ {/* {addThumbnailMutation.isPending && ( +
+ +
{t("thumbnail_modal.action_being", { action: action })}
+
+ )} */} +
, + document.body + )} + { + // deleteThumbnailMutation.mutate(); + }} + /> +
+ ); +}; + +export default memo(CommunityLogo); diff --git a/assets/espaceco/pages/communities/management/Description.tsx b/assets/espaceco/pages/communities/management/Description.tsx new file mode 100644 index 00000000..d275f6d8 --- /dev/null +++ b/assets/espaceco/pages/communities/management/Description.tsx @@ -0,0 +1,213 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import Input from "@codegouvfr/react-dsfr/Input"; +import { cx } from "@codegouvfr/react-dsfr/tools/cx"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { TranslationFunction } from "i18nifty/typeUtils/TranslationFunction"; +import { FC, useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; +import * as yup from "yup"; +import { CommunityResponseDTO, DocumentDTO } from "../../../../@types/espaceco"; +import AutocompleteSelect from "../../../../components/Input/AutocompleteSelect"; +import MarkdownEditor from "../../../../components/Input/MarkdownEditor"; +import thumbnails from "../../../../data/doc_thumbnail.json"; +import categories from "../../../../data/topic_categories.json"; +import { ComponentKey, useTranslation } from "../../../../i18n/i18n"; +import { appRoot } from "../../../../router/router"; +import { getFileExtension } from "../../../../utils"; +import { AddDocumentDialog, AddDocumentDialogModal } from "./AddDocumentDialog"; +import CommunityLogo from "./CommunityLogo"; +import { type CartesApiException } from "../../../../modules/jsonFetch"; +import "../../../../sass/pages/espaceco/community.scss"; +import { useQuery } from "@tanstack/react-query"; +import RQKeys from "../../../../modules/espaceco/RQKeys"; +import api from "../../../api"; + +type DocumentExt = DocumentDTO & { + src: string; + isImage: boolean; +}; + +type DescriptionProps = { + community: CommunityResponseDTO; +}; + +/* const isNewDocument = (d: DocumentDTO | NewDocument): d is NewDocument => "new_id" in d; + +const readFileAsDataURL = async (file: File) => { + const result = await new Promise((resolve) => { + const fileReader = new FileReader(); + fileReader.onload = (e) => resolve(fileReader.result); + fileReader.readAsDataURL(file); + }); + return result; +}; */ + +const Description: FC = ({ community }) => { + // const { tab1 } = useCommunityFormStore(community)(); + + const { t: tCommon } = useTranslation("Common"); + const { t: tValid } = useTranslation("ManageCommunityValidations"); + const { t } = useTranslation("ManageCommunity"); + + const communityNamesQuery = useQuery({ + queryKey: RQKeys.communitiesName(), + queryFn: () => api.community.getCommunitiesName(), + staleTime: 3600000, + }); + + const communityNames = useMemo(() => { + return communityNamesQuery.data?.filter((n) => n !== community.name) ?? []; + }, [community.name, communityNamesQuery]); + + const formattedDocuments = useMemo(() => { + const documents = community.documents ?? []; + return Array.from( + documents.map((d) => { + const result: DocumentExt = { ...d } as DocumentExt; + if (/^image/.test(result.mime_type)) { + result.src = d.uri; + } else { + const extension = getFileExtension(result.short_fileName)?.toLowerCase() ?? "defaut"; + const uri = extension in thumbnails ? thumbnails[extension].src : thumbnails["defaut"].src; + result.src = `${appRoot}/${uri}`; + } + + return result; + }) + ); + }, [community.documents]); + + const schema = (t: TranslationFunction<"ManageCommunityValidations", ComponentKey>) => { + return yup.object({ + name: yup + .string() + .trim(t("trimmed_error")) + .strict(true) + .min(2, t("description.name.minlength")) + .max(80, t("description.name.maxlength")) + .test("is-unique", tValid("description.name.unique"), (name) => { + if (name === undefined) return true; + return !communityNames.includes(name.trim()); + }) + .required(t("description.name.mandatory")), + description: yup.string().max(1024, t("description.desc.maxlength")).required(t("description.desc.mandatory")), + keywords: yup.array().of(yup.string()), + }); + }; + + const { + control, + register, + formState: { errors }, + // setValue: setFormValue, + } = useForm({ + resolver: yupResolver(schema(tValid)), + mode: "onChange", + values: { + name: community.name, + description: community.description ?? "", + // TODO keywords: community.keywords ?? [] + keywords: [], + }, + }); + + return ( + <> +

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

+
+

{tCommon("mandatory_fields")}

+ + ( + { + field.onChange(values); + }} + /> + )} + /> + + ( + field.onChange(value)} + /> + )} + /> + +
+ {formattedDocuments.length ? ( + formattedDocuments.map((d) => ( +
+
{d.title}
+
+ +
+
+ )) + ) : ( +

{t("desc.no_documents")}

+ )} +
+ + AddDocumentDialogModal.close()} + onAdd={(title, file) => { + console.log(title, file.name); // TODO SUPPRIMER + // TODO Mutation : Envoyer le fichier en POST (la route n'existe pas encore) + AddDocumentDialogModal.close(); + }} + /> +
+ + ); +}; + +export default Description; diff --git a/assets/espaceco/pages/communities/management/Grid.tsx b/assets/espaceco/pages/communities/management/Grid.tsx new file mode 100644 index 00000000..2f071a54 --- /dev/null +++ b/assets/espaceco/pages/communities/management/Grid.tsx @@ -0,0 +1,43 @@ +import { FC } from "react"; +import { useForm } from "react-hook-form"; +import { GridDTO } from "../../../../@types/espaceco"; +import { useTranslation } from "../../../../i18n/i18n"; +import GridList from "./GridList"; + +type GridProps = { + grids: GridDTO[]; +}; + +type GridForm = { + grids: string[]; +}; + +const Grid: FC = ({ grids }) => { + const { t } = useTranslation("ManageCommunity"); + + const form = useForm({ + mode: "onSubmit", + values: { + grids: Array.from(grids, (g) => g.name), + }, + }); + const { setValue: setFormValue } = form; + + return ( + <> +

{t("grid.grids")}

+ {t("grid.explain")} + { + setFormValue( + "grids", + Array.from(grids, (g) => g.name) + ); + }} + /> + + ); +}; + +export default Grid; diff --git a/assets/espaceco/pages/communities/management/GridList.tsx b/assets/espaceco/pages/communities/management/GridList.tsx new file mode 100644 index 00000000..b7a2eba0 --- /dev/null +++ b/assets/espaceco/pages/communities/management/GridList.tsx @@ -0,0 +1,75 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import Table from "@codegouvfr/react-dsfr/Table"; +import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from "react"; +import { GridDTO } from "../../../../@types/espaceco"; +import SearchGrids from "./ZoomAndCentering/SearchGrids"; + +type GridListProps = { + grids?: GridDTO[]; + onChange: (grids: GridDTO[]) => void; +}; + +const GridList: FC = ({ grids = [], onChange }) => { + const [grid, setGrid] = useState(null); + const [internal, setInternal] = useState([]); + + useEffect(() => { + setInternal([...grids]); + }, [grids]); + + const handleRemove = useCallback( + (gridName: string) => { + const grids = internal.filter((grid) => grid.name !== gridName); + setInternal(grids); + onChange(grids); + }, + [internal, onChange] + ); + + const handleAdd = () => { + if (grid) { + const grids = Array.from(new Set([...internal, grid])); + setInternal(grids); + onChange(grids); + } + }; + const data: ReactNode[][] = useMemo(() => { + return Array.from(internal, (grid) => [ + grid.name, + grid.title, + grid.type.title, +
+
, + ]); + }, [internal, handleRemove]); + + return ( +
+
+
+ { + if (grid) { + setGrid(grid); + } + }} + /> +
+
+
+
+
+
+ + + ); +}; + +export default GridList; diff --git a/assets/espaceco/pages/communities/management/Layer.tsx b/assets/espaceco/pages/communities/management/Layer.tsx new file mode 100644 index 00000000..d62b9ed1 --- /dev/null +++ b/assets/espaceco/pages/communities/management/Layer.tsx @@ -0,0 +1,71 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Tabs from "@codegouvfr/react-dsfr/Tabs"; +import { Options, optionsFromCapabilities } from "ol/source/WMTS"; +import { FC, useMemo, useState } from "react"; +import useCapabilities from "../../../../hooks/useCapabilities"; +import { useTranslation } from "../../../../i18n/i18n"; + +const baseMaps = ["GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2", "GEOGRAPHICALGRIDSYSTEMS.MAPS.BDUNI.J1", "ORTHOIMAGERY.ORTHOPHOTOS", "CADASTRALPARCELS.PARCELS"]; + +const Layer: FC = () => { + const { t } = useTranslation("ManageCommunity"); + const { data: capabilities } = useCapabilities(); + + /* const cbBaseMapsOptions = useMemo(() => { + if (!capabilities) return []; + + + Array.from(baseMaps, (m) =>
); + }, [capabilities]); */ + + const cbBaseMapsOptions = useMemo(() => { + if (!capabilities) return []; + + const options: Options[] = []; + baseMaps.forEach((m) => { + const wmtsOptions = optionsFromCapabilities(capabilities, { layer: m, style: "Légende générique" }); + if (wmtsOptions) { + options.push(wmtsOptions); + } + }); + return options; + }, [capabilities]); + console.log(cbBaseMapsOptions); + + const [selectedTabId, setSelectedTabId] = useState("tab1"); + + return ( + <> +

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

+
+
+ + <> + {(() => { + switch (selectedTabId) { + case "my_datas": + return

`Content of ${selectedTabId}`

; + case "gp_datas": + return

`Content of ${selectedTabId}`

; + case "base_maps": + return

`Content of ${selectedTabId}`

; + } + })()} + +
+
+
+
+ + ); +}; + +export default Layer; diff --git a/assets/espaceco/pages/communities/management/Members.tsx b/assets/espaceco/pages/communities/management/Members.tsx new file mode 100644 index 00000000..b232bf24 --- /dev/null +++ b/assets/espaceco/pages/communities/management/Members.tsx @@ -0,0 +1,400 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Accordion from "@codegouvfr/react-dsfr/Accordion"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import Button from "@codegouvfr/react-dsfr/Button"; +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import Select from "@codegouvfr/react-dsfr/Select"; +import Table from "@codegouvfr/react-dsfr/Table"; +import Pagination from "@mui/material/Pagination"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { FC, ReactNode, useCallback, useMemo, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { CommunityMember, GetResponse, Role } from "../../../../@types/app_espaceco"; +import { CommunityResponseDTO } from "../../../../@types/espaceco"; +import ConfirmDialog, { ConfirmDialogModal } from "../../../../components/Utils/ConfirmDialog"; +import LoadingIcon from "../../../../components/Utils/LoadingIcon"; +import LoadingText from "../../../../components/Utils/LoadingText"; +import Wait from "../../../../components/Utils/Wait"; +import { declareComponentKeys, Translations, useTranslation } from "../../../../i18n/i18n"; +import RQKeys from "../../../../modules/espaceco/RQKeys"; +import { CartesApiException } from "../../../../modules/jsonFetch"; +import { routes } from "../../../../router/router"; +import api from "../../../api"; +import { AddMembersDialog, AddMembersDialogModal } from "./member/AddMembersDialog"; +import { ManageGridsDialog, ManageGridsDialogModal } from "./member/ManageGridsDialog"; + +export type membersQueryParams = { + page: number; + limit: number; +}; + +type MembersProps = { + community: CommunityResponseDTO; +}; + +const maxFetchedMembers = 10; +const getName = (firstname: string | null, surname: string | null) => `${firstname ? firstname : ""} ${surname ? surname : ""}`; + +const Members: FC = ({ community }) => { + const { t } = useTranslation("EscoCommunityMembers"); + + const queryClient = useQueryClient(); + + const [currentPage, setCurrentPage] = useState(1); + const [errorUpdateRole, setErrorUpdateRole] = useState(undefined); + const [errorRemoveMember, setErrorRemoveMember] = useState(undefined); + + const [action, setAction] = useState<"remove" | "reject" | undefined>(undefined); + const [currentMember, setCurrentMember] = useState(undefined); + + // Les demandes d'affiliation + const membershipRequestsQuery = useQuery, CartesApiException>({ + queryKey: RQKeys.communityMembershipRequests(community.id), + queryFn: ({ signal }) => api.community.getCommunityMembershipRequests(community.id, signal), + staleTime: 60000, + }); + + // Les membres non en demande d'affiliation + const membersQuery = useQuery, CartesApiException>({ + queryKey: RQKeys.communityMembers(community.id, currentPage, maxFetchedMembers), + queryFn: ({ signal }) => api.community.getCommunityMembers(community.id, currentPage, maxFetchedMembers, signal), + staleTime: 60000, + }); + + /* Mise a jour du role de l'utilisateur */ + const updateRoleMutation = useMutation({ + mutationFn: (params) => { + return api.community.updateMemberRole(params.communityId, params.userId, params.role); + }, + onSuccess() { + setErrorUpdateRole(undefined); + + queryClient.refetchQueries({ queryKey: RQKeys.communityMembers(community.id, currentPage, maxFetchedMembers) }); + queryClient.refetchQueries({ queryKey: RQKeys.communityMembershipRequests(community.id) }); + + /* Mise à jour des données de la requête membersQuery */ + /*queryClient.setQueryData>(RQKeys.communityMembers(community.id, currentPage, maxFetchedMembers), (datas) => { + datas?.content.forEach((member) => { + if (member.user_id === response.user_id) { + member.role = response.role; // Mise a jour du role + } + }); + return datas; + }); */ + + /* Mise à jour des données de la requête membershipRequestsQuery */ + /* queryClient.setQueryData(RQKeys.communityMembershipRequests(community.id), (datas) => { + if (datas) { + return datas.filter((member) => member.user_id !== response.user_id); + } + }); */ + }, + onError(e) { + setErrorUpdateRole(e.message); + }, + }); + + /* Suppression d'un membre */ + const removeMemberMutation = useMutation<{ user_id: number }, CartesApiException, { communityId: number; userId: number }>({ + mutationFn: ({ communityId, userId }) => { + return api.community.removeMember(communityId, userId); + }, + onSuccess() { + setAction(undefined); + setErrorRemoveMember(undefined); + + queryClient.refetchQueries({ queryKey: RQKeys.communityMembers(community.id, currentPage, maxFetchedMembers) }); + queryClient.refetchQueries({ queryKey: RQKeys.communityMembershipRequests(community.id) }); + + /* Mise à jour des données de la requête membersQuery */ + /*queryClient.setQueryData>(RQKeys.communityMembers(community.id, currentPage, maxFetchedMembers), (datas) => { + if (datas) { + datas.content = datas.content.filter((member) => member.user_id !== response.user_id); + return datas; + } + });*/ + + /* Mise à jour des données de la requête membershipRequestsQuery */ + /*queryClient.setQueryData(RQKeys.communityMembershipRequests(community.id), (datas) => { + if (datas) { + return datas.filter((member) => member.user_id !== response.user_id); + } + });*/ + }, + onError(e) { + setErrorRemoveMember(e.message); + }, + }); + + const alert = useCallback( + (error: string) => { + return ( + +

{error}

+ + + } + /> + ); + }, + [t] + ); + + const headers = useMemo(() => [t("username_header"), t("name_header"), t("status_header"), t("grids_header"), ""], [t]); + const pendingHeaders = useMemo(() => [t("username_header"), t("name_header"), t("date_header"), ""], [t]); + + const memberData: ReactNode[][] = useMemo(() => { + const datas = membersQuery.data?.content ?? []; + return ( + datas.map((m) => [ + m.username, + getName(m.firstname, m.surname), + , + , +
+
, + ]) ?? [] + ); + }, [community, t, membersQuery.data, updateRoleMutation]); + + const pendingData: ReactNode[][] = useMemo(() => { + const datas = membershipRequestsQuery.data?.content ?? []; + return ( + datas.map((m) => [ + m.username, + getName(m.firstname, m.surname), + m.date, +
+ + +
, + ]) ?? [] + ); + }, [community, membershipRequestsQuery.data, updateRoleMutation, removeMemberMutation, t]); + + return ( +
+ {/* les requêtes ont échoué */} + {membershipRequestsQuery.isError && alert(membershipRequestsQuery.error.message)} + {membersQuery.isError && alert(membersQuery.error.message)} + {errorUpdateRole && alert(errorUpdateRole)} + {errorRemoveMember && alert(errorRemoveMember)} + + {(membershipRequestsQuery.isLoading || membersQuery.isLoading) && } + {membershipRequestsQuery.data?.content && membershipRequestsQuery.data.content.length > 0 && ( + +
+ + )} + {membersQuery.data?.content && membersQuery.data.content.length > 0 && ( +
+
+ +
+
+
+ + setCurrentPage(v)} + /> + +
+ + )} + {updateRoleMutation.isPending && ( + +
+
+
+ +
+
+
{t("update_role")}
+
+
+
+
+ )} + {removeMemberMutation.isPending && ( + +
+
+
+ +
+
+
{t("remove_action", { action: action })}
+
+
+
+
+ )} + { + if (currentMember !== undefined) { + removeMemberMutation.mutate({ communityId: community.id, userId: currentMember.user_id }); + } + }} + /> + + console.log("TODOS")} /> + + ); +}; + +export default Members; + +// traductions +export const { i18n } = declareComponentKeys< + | "fetch_failed" + | "back_to_list" + | "loading_members" + | "loading_membership_requests" + | { K: "membership_requests"; P: { count: number }; R: string } + | "username_header" + | "name_header" + | "status_header" + | "grids_header" + | { K: "role"; P: { role: Role }; R: string } + | "date_header" + | "update_role" + | { K: "remove_action"; P: { action: "remove" | "reject" | undefined }; R: string } + | "confirm_remove" + | "accept" + | "accept_title" + | "reject" + | "reject_title" + | "remove_title" + | "manage_grids" + | "invite" +>()("EscoCommunityMembers"); + +export const EscoCommunityMembersFrTranslations: Translations<"fr">["EscoCommunityMembers"] = { + fetch_failed: "La récupération des membres du guichet a échoué", + back_to_list: "Retour à la liste des guichets", + loading_members: "Chargement des membres du guichet", + loading_membership_requests: "Chargement des demandes d’affiliation", + membership_requests: ({ count }) => `Demandes d’affiliation (${count})`, + username_header: "Nom de l'utilisateur", + name_header: "Nom, prénom", + status_header: "Statut", + grids_header: "Emprises individuelles", + role: ({ role }) => { + switch (role) { + case "admin": + return "Gestionnaire"; + case "member": + return "Membre"; + case "pending": + return "En attente de demande d'affiliation"; + } + }, + date_header: "Date de la demande", + update_role: "Mise à jour du rôle de l'utilisateur en cours ...", + remove_action: ({ action }) => { + switch (action) { + case "remove": + return "Suppression de l'utilisateur en cours ..."; + case "reject": + return "Rejet de la demande d'affiliation en cours ..."; + default: + return ""; + } + }, + confirm_remove: "Êtes-vous sûr de vouloir supprimer cet utilisateur ?", + accept: "Accepter", + accept_title: "Accepter la demande", + reject: "Rejeter", + reject_title: "Rejeter la demande", + remove_title: "Supprimer l'utilisateur", + manage_grids: "Gérer", + invite: "Inviter des membres", +}; + +export const EscoCommunityMembersEnTranslations: Translations<"en">["EscoCommunityMembers"] = { + fetch_failed: undefined, + back_to_list: undefined, + loading_members: undefined, + loading_membership_requests: undefined, + membership_requests: ({ count }) => `Membership requests (${count})`, + username_header: "username", + name_header: undefined, + status_header: "Status", + grids_header: undefined, + role: ({ role }) => `${role}`, + date_header: undefined, + update_role: undefined, + remove_action: ({ action }) => `${action}`, + confirm_remove: undefined, + accept: undefined, + accept_title: undefined, + reject: undefined, + reject_title: undefined, + remove_title: undefined, + manage_grids: "Manage", + invite: 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..eec79c47 --- /dev/null +++ b/assets/espaceco/pages/communities/management/Reports.tsx @@ -0,0 +1,222 @@ +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, useMemo } from "react"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { + CommunityResponseDTO, + 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 { countActiveStatus, getDefaultStatuses, getMinAuthorizedStatus } from "./reports/Utils"; +import Answers from "./reports/Answers"; +import { ReportFormType } from "../../../../@types/app_espaceco"; + +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.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({ + 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), + queryFn: ({ signal }) => api.permission.getThemableTables(community.id, signal), + staleTime: 60000, + }); + + const sharedThemesQuery = useQuery({ + queryKey: RQKeys.userSharedThemes(), + 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), + mode: "onChange", + values: { + attributes: community.attributes ?? [], + 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 && } + {sharedThemesQuery.isError && } + {tablesQuery.isLoading && } + {sharedThemesQuery.isLoading && } + {tablesQuery.data && sharedThemesQuery.data && ( +
+ + + + + +
+ +
+
+ )} +
+ ); +}; + +export default Reports; diff --git a/assets/espaceco/pages/communities/management/SearchTr.tsx b/assets/espaceco/pages/communities/management/SearchTr.tsx new file mode 100644 index 00000000..4595f6d2 --- /dev/null +++ b/assets/espaceco/pages/communities/management/SearchTr.tsx @@ -0,0 +1,15 @@ +import { declareComponentKeys } from "i18nifty"; +import { Translations } from "../../../../i18n/i18n"; + +// traductions +export const { i18n } = declareComponentKeys<"no_results" | "loading">()("Search"); + +export const SearchFrTranslations: Translations<"fr">["Search"] = { + no_results: "Aucun résultat", + loading: "Recherche en cours ...", +}; + +export const SearchEnTranslations: Translations<"en">["Search"] = { + no_results: "No results", + loading: "Searching ...", +}; diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx new file mode 100644 index 00000000..39fae25a --- /dev/null +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering.tsx @@ -0,0 +1,136 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Alert from "@codegouvfr/react-dsfr/Alert"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { containsCoordinate, Extent } from "ol/extent"; +import WKT from "ol/format/WKT"; +import { FC, useCallback, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { CommunityResponseDTO } from "../../../../@types/espaceco"; +import ZoomRange from "../../../../components/Utils/ZoomRange"; +import olDefaults from "../../../../data/ol-defaults.json"; +import { useTranslation } from "../../../../i18n/i18n"; +import { ExtentDialog, ExtentDialogModal } from "./ZoomAndCentering/ExtentDialog"; +import RMap from "./ZoomAndCentering/RMap"; +import Search from "./ZoomAndCentering/Search"; + +type ZoomAndCenteringProps = { + community: CommunityResponseDTO; +}; + +export type ZoomAndCenteringFormType = { + position: number[]; + zoom: number; + zoomMin: number; + zoomMax: number; + extent?: Extent | null; +}; + +const ZoomAndCentering: FC = ({ community }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("ManageCommunity"); + + // Cohérence entre l'extent et la position + const [consistent, setConsistent] = useState(true); + + const getValues = useCallback(() => { + let p; + if (community.position) { + const feature = new WKT().readFeature(community.position, { + dataProjection: "EPSG:4326", + }); + p = feature.getGeometry().getCoordinates(); + } else p = olDefaults.center; + + return { + position: p, + zoom: community.zoom ?? olDefaults.zoom, + zoomMin: community.zoom_min ?? olDefaults.zoom_levels.TOP, + zoomMax: community.zoom_max ?? olDefaults.zoom_levels.BOTTOM, + extent: community.extent, + }; + }, [community]); + + const form = useForm({ + mode: "onSubmit", + values: getValues(), + }); + const { watch, getValues: getFormValues, setValue: setFormValue } = form; + console.log(watch()); + + const position = watch("position"); + const extent = watch("extent"); + + useEffect(() => { + if (position && extent) { + setConsistent(containsCoordinate(extent, position)); + } + return; + }, [position, extent]); + + return ( +
+ {consistent === false && ( + + )} +

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

+
+
+ { + if (newPosition) { + setFormValue("position", newPosition); + } + }} + /> + { + const oldZoom = getFormValues("zoom"); + setFormValue("zoomMin", v[0]); + setFormValue("zoomMax", v[1]); + setFormValue("zoom", oldZoom < v[1] ? oldZoom : v[1]); + }} + /> +
+ +
+
+
+ setFormValue("position", position)} + onZoomChanged={(zoom) => setFormValue("zoom", zoom)} + /> +
+ ExtentDialogModal.close()} + onApply={(e) => { + setFormValue("extent", e); + ExtentDialogModal.close(); + }} + /> +
+
+ ); +}; + +export default ZoomAndCentering; diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx new file mode 100644 index 00000000..c5ae68c2 --- /dev/null +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/ExtentDialog.tsx @@ -0,0 +1,243 @@ +import { fr } from "@codegouvfr/react-dsfr"; +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 { 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 { useTranslation } from "../../../../../i18n/i18n"; +import SearchGrids from "./SearchGrids"; + +type ExtentDialogProps = { + onCancel: () => void; + onApply: (extent: Extent) => void; +}; + +const ExtentDialogModal = createModal({ + id: "extent-modal", + isOpenedByDefault: false, +}); + +type SearchOption = "autocomplete" | "manual"; +type FieldName = "xmin" | "xmax" | "ymin" | "ymax"; + +const filters: SearchGridFilters = { + fields: ["name", "title", "extent"], + adm: true, +}; + +const transform = (value, origin) => (origin === "" ? undefined : value); + +const ExtentDialog: FC = ({ onCancel, onApply }) => { + const { t: tCommon } = useTranslation("Common"); + const { t: tValid } = useTranslation("ManageCommunityValidations"); + const { t } = useTranslation("ManageCommunity"); + + const [choice, setChoice] = useState("manual"); + + const schema = {}; + schema["manual"] = yup.object({ + xmin: yup + .number() + .typeError(tValid("zoom.extent.nan", { field: "${path}" })) + .min(-180, tValid("zoom.greater_than", { field: "${path}", v: -180 })) + .max(180, tValid("zoom.less_than", { field: "${path}", v: 180 })) + .required(tValid("zoom.extent.mandatory", { field: "${path}" })) + .transform(transform) + .test({ + name: "xmin_check", + message: tValid("zoom.f1_less_than_f2", { field1: "xmin", field2: "xmax" }), + test: (value, context) => { + const xmax = context.parent.xmax; + if (value) { + return xmax !== undefined ? value < xmax : true; + } + return true; + }, + }), + ymin: yup + .number() + .typeError(tValid("zoom.extent.nan", { field: "${path}" })) + .min(-90, tValid("zoom.greater_than", { field: "${path}", v: -90 })) + .max(90, tValid("zoom.less_than", { field: "${path}", v: 90 })) + .required(tValid("zoom.extent.mandatory", { field: "${path}" })) + .transform(transform) + .test({ + name: "ymin_check", + message: tValid("zoom.f1_less_than_f2", { field1: "ymin", field2: "ymax" }), + test: (value, context) => { + const ymax = context.parent.ymax; + return ymax !== undefined ? value < ymax : true; + }, + }), + xmax: yup + .number() + .typeError(tValid("zoom.extent.nan", { field: "${path}" })) + .min(-180, tValid("zoom.greater_than", { field: "${path}", v: -180 })) + .max(180, tValid("zoom.less_than", { field: "${path}", v: 180 })) + .required(tValid("zoom.extent.mandatory", { field: "${path}" })) + .transform(transform) + .test({ + name: "xmax_check", + message: tValid("zoom.f1_less_than_f2", { field1: "xmin", field2: "xmax" }), + test: (value, context) => { + const xmin = context.parent.xmin; + return xmin !== undefined ? value > xmin : true; + }, + }), + ymax: yup + .number() + .typeError(tValid("zoom.extent.nan", { field: "${path}" })) + .min(-90, tValid("zoom.greater_than", { field: "${path}", v: -90 })) + .max(90, tValid("zoom.less_than", { field: "${path}", v: 90 })) + .required(tValid("zoom.extent.mandatory", { field: "${path}" })) + .transform(transform) + .test({ + name: "ymax_check", + message: tValid("zoom.f1_less_than_f2", { field1: "ymin", field2: "ymax" }), + test: (value, context) => { + const ymin = context.parent.ymin; + return ymin !== undefined ? value > ymin : true; + }, + }), + }); + schema["autocomplete"] = yup.object({ + extent: yup.array().of(yup.number()).required(tValid("zoom.extent.required")), + }); + + const form = useForm({ + mode: "onChange", + resolver: yupResolver(schema[choice]), + }); + const { + register, + getValues: getFormValues, + setValue: setFormValue, + formState: { errors }, + clearErrors, + handleSubmit, + resetField, + } = form; + + const clear = () => { + ["xmin", "ymin", "xmax", "ymax"].forEach((f) => resetField(f as FieldName, undefined)); + }; + + const onChoiceChanged = (v) => { + clear(); + setChoice(v); + }; + + const onSubmit = () => { + ExtentDialogModal.close(); + + const values = getFormValues(); + onApply([values.xmin, values.ymin, values.xmax, values.ymax]); + setChoice("manual"); + clear(); + }; + + return ( + <> + {createPortal( + { + setChoice("manual"); + clear(); + onCancel(); + }, + priority: "secondary", + }, + { + children: tCommon("apply"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + priority: "primary", + }, + ]} + > + <> + onChoiceChanged("autocomplete"), + }, + }, + { + label: t("zoom.choice.manual"), + nativeInputProps: { + checked: choice === "manual", + onChange: () => onChoiceChanged("manual"), + }, + }, + ]} + /> + {choice === "autocomplete" ? ( +
+ { + setFormValue("extent", grid ? grid : undefined); + clearErrors(); + }} + /> +
+ ) : ( +
+ +
+
+ + +
+
+ + +
+
+
+ )} + +
, + document.body + )} + + ); +}; + +export { ExtentDialog, ExtentDialogModal }; diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/RMap.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/RMap.tsx new file mode 100644 index 00000000..22f44841 --- /dev/null +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/RMap.tsx @@ -0,0 +1,117 @@ +import { Feature } from "ol"; +import { defaults as defaultControls, ScaleLine } from "ol/control"; +import { Coordinate } from "ol/coordinate"; +import Point from "ol/geom/Point"; +import { DragPan, MouseWheelZoom } from "ol/interaction"; +import BaseLayer from "ol/layer/Base"; +import TileLayer from "ol/layer/Tile"; +import VectorLayer from "ol/layer/Vector"; +import Map from "ol/Map"; +import { fromLonLat, toLonLat } from "ol/proj"; +import VectorSource from "ol/source/Vector"; +import WMTS, { optionsFromCapabilities } from "ol/source/WMTS"; +import Icon from "ol/style/Icon"; +import Style from "ol/style/Style"; +import View from "ol/View"; +import { CSSProperties, FC, useEffect, useMemo, useRef } from "react"; +import { UseFormReturn } from "react-hook-form"; +import olDefaults from "../../../../../data/ol-defaults.json"; +import useCapabilities from "../../../../../hooks/useCapabilities"; +import punaise from "../../../../../img/punaise.png"; +import { ZoomAndCenteringFormType } from "../ZoomAndCentering"; + +const mapStyle: CSSProperties = { + height: "400px", +}; + +type RMapProps = { + form: UseFormReturn; + onPositionChanged: (position: Coordinate) => void; + onZoomChanged: (zoom: number) => void; +}; + +const RMap: FC = ({ form, onPositionChanged, onZoomChanged }) => { + const mapTargetRef = useRef(null); + const mapRef = useRef(); + + // Création de la couche openlayers de fond (bg layer) + const { data: capabilities } = useCapabilities(); + + const { watch, getValues: getFormValues } = form; + const position = watch("position"); + + const position3857 = useMemo(() => fromLonLat(position), [position]); + + // Création de la carte une fois bg layer créée + useEffect(() => { + if (!capabilities) return; + + const feature = new Feature(new Point(position3857)); + + // layer punaise + const source = new VectorSource(); + source.addFeatures([feature]); + const layer = new VectorLayer({ + source: source, + style: new Style({ + image: new Icon({ + src: punaise, + // ancrage de la punaise (non centrée) + anchor: [0.5, 0], + anchorOrigin: "bottom-left", + }), + }), + }); + + const layers: BaseLayer[] = []; + + const wmtsOptions = optionsFromCapabilities(capabilities, { + layer: olDefaults.default_background_layer, + }); + if (wmtsOptions) { + const bkgLayer = new TileLayer({ + source: new WMTS(wmtsOptions), + }); + layers.push(bkgLayer); + } + layers.push(layer); + + mapRef.current = new Map({ + target: mapTargetRef.current as HTMLElement, + layers: layers, + controls: defaultControls().extend([new ScaleLine()]), + interactions: [ + new DragPan(), + new MouseWheelZoom({ + useAnchor: false, + }), + ], + view: new View({ + center: position3857, + zoom: getFormValues("zoom"), + minZoom: getFormValues("zoomMin"), + maxZoom: getFormValues("zoomMax"), + }), + }); + + mapRef.current.on("moveend", (e) => { + const map = e.map; + const centerView = map.getView().getCenter() as Coordinate; + const z = map.getView().getZoom() as number; + + if (z !== getFormValues("zoom")) { + onZoomChanged(Math.round(z)); + } + + if (Math.abs(centerView[0] - position3857[0]) > 1 && Math.abs(centerView[1] - position3857[1]) > 1) { + onPositionChanged(toLonLat(centerView)); + } + }); + + return () => mapRef.current?.setTarget(undefined); + }, [capabilities, position3857, getFormValues, onPositionChanged, onZoomChanged]); + + return
; +}; + +export default RMap; diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/Search.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/Search.tsx new file mode 100644 index 00000000..b63f1e28 --- /dev/null +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/Search.tsx @@ -0,0 +1,63 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import Autocomplete from "@mui/material/Autocomplete"; +import TextField from "@mui/material/TextField"; +import { useQuery } from "@tanstack/react-query"; +import { Coordinate } from "ol/coordinate"; +import { FC, ReactNode } from "react"; +import { useDebounceValue } from "usehooks-ts"; +import { SearchResult } from "../../../../../@types/app_espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import RQKeys from "../../../../../modules/espaceco/RQKeys"; +import { jsonFetch } from "../../../../../modules/jsonFetch"; + +type SearchProps = { + label: ReactNode; + hintText?: ReactNode; + filter: Record; + onChange: (value: Coordinate | null) => void; +}; + +const autocompleteUrl = "https://data.geopf.fr/geocodage/completion"; + +const Search: FC = ({ label, hintText, filter, onChange }) => { + const { t } = useTranslation("Search"); + + const [text, setText] = useDebounceValue("", 500); + + const searchQuery = useQuery({ + queryKey: RQKeys.searchAddress(text), + queryFn: async ({ signal }) => { + const qParams = new URLSearchParams({ text: text, ...filter }).toString(); + return jsonFetch(`${autocompleteUrl}?${qParams}`, { signal }); + }, + enabled: text.length >= 3, + }); + + return ( +
+ + + option.fulltext} + options={searchQuery.data?.results ?? []} + filterOptions={(x) => x} + renderInput={(params) => } + isOptionEqualToValue={(option, v) => option.fulltext === v.fulltext} + onInputChange={(_, v) => setText(v)} + onChange={(_, v) => { + if (v) onChange([v.x, v.y]); + }} + /> + +
+ ); +}; + +export default Search; diff --git a/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx b/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx new file mode 100644 index 00000000..18b942c1 --- /dev/null +++ b/assets/espaceco/pages/communities/management/ZoomAndCentering/SearchGrids.tsx @@ -0,0 +1,101 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import Autocomplete, { autocompleteClasses } from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import TextField from "@mui/material/TextField"; +import { useQuery } from "@tanstack/react-query"; +import { FC, ReactNode } from "react"; +import { useDebounceValue } from "usehooks-ts"; +import { GetResponse, SearchGridFilters } from "../../../../../@types/app_espaceco"; +import { GridDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import RQKeys from "../../../../../modules/espaceco/RQKeys"; +import api from "../../../../api"; + +export type SearchGridsProps = { + label: ReactNode; + hintText?: ReactNode; + filters: SearchGridFilters; + state?: "default" | "error" | "success"; + stateRelatedMessage?: string; + onChange: (grid: GridDTO | null) => void; +}; + +const SearchGrids: FC = ({ label, hintText, filters, state, stateRelatedMessage, onChange }) => { + const { t } = useTranslation("Search"); + + const [text, setText] = useDebounceValue("", 500); + + const searchQuery = useQuery>({ + queryKey: RQKeys.searchGrids(text), + queryFn: ({ signal }) => { + return api.grid.search(text, filters, { signal }); + }, + staleTime: 1000 * 60, + enabled: text.length >= 2, + }); + + return ( +
+ + + { + return ( + + {ownerState.getOptionLabel(option)} + + ); + }} + size={"small"} + loading={searchQuery.isLoading} + loadingText={t("loading")} + noOptionsText={t("no_results")} + getOptionLabel={(option) => `${option.name} : ${option.title}`} + options={searchQuery.data?.content ?? []} + filterOptions={(x) => x} + renderInput={(params) => } + isOptionEqualToValue={(option, v) => option.name === v.name} + onInputChange={(_, v) => setText(v)} + onChange={(_, v) => { + onChange(v); + }} + /> + + {state !== "default" && ( +

{ + switch (state) { + case "error": + return "fr-error-text"; + case "success": + return "fr-valid-text"; + } + })(), + "fr-mb-1v" + )} + > + {stateRelatedMessage} +

+ )} +
+ ); +}; + +export default SearchGrids; diff --git a/assets/espaceco/pages/communities/management/member/AddMembersDialog.tsx b/assets/espaceco/pages/communities/management/member/AddMembersDialog.tsx new file mode 100644 index 00000000..9f45222c --- /dev/null +++ b/assets/espaceco/pages/communities/management/member/AddMembersDialog.tsx @@ -0,0 +1,224 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import Table from "@codegouvfr/react-dsfr/Table"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FC, ReactNode, useCallback, useMemo } from "react"; +import { createPortal } from "react-dom"; +import { Controller, useForm } from "react-hook-form"; +import isEmail from "validator/lib/isEmail"; +import * as yup from "yup"; +import { isUser } from "../../../../../@types/app_espaceco"; +import { UserDTO } from "../../../../../@types/espaceco"; +import { declareComponentKeys, Translations, useTranslation } from "../../../../../i18n/i18n"; +import SearchUsers from "./SearchUsers"; + +const AddMembersDialogModal = createModal({ + id: "add-esco-member-modal", + isOpenedByDefault: false, +}); + +const AddMembersDialog: FC = () => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("AddMembersDialog"); + + const schema = yup.object({ + users: yup + .array() + .of(yup.mixed().required()) + .test({ + name: "check-entry", + test: (values, context) => { + if (!values) return true; + for (const v of values) { + if (typeof v === "string") { + if (!isEmail(v)) return context.createError({ message: t("email_not_valid", { value: v }) }); + } + } + return true; + }, + }), + }); + + const { + watch, + control, + getValues: getFormValues, + setValue: setFormValue, + formState: { errors }, + } = useForm({ + mode: "onChange", + resolver: yupResolver(schema), + defaultValues: { + users: [], + }, + }); + + const users = watch("users"); + console.log("USERS : ", users); + + const handleRemove = useCallback( + (user: UserDTO | string) => { + const users = (getFormValues("users") as (UserDTO | string)[]) ?? []; + const filtered = isUser(user) + ? users.filter((u) => { + if (isUser(u)) return u.id !== user.id; + return true; + }) + : users.filter((u) => { + if (!isUser(u)) return u !== user; + return true; + }); + + setFormValue("users", filtered); + }, + [getFormValues, setFormValue] + ); + + const gpUsersData: ReactNode[][] = useMemo(() => { + return ( + users + ?.filter((u) => { + return isUser(u as UserDTO | string); + }) + .map((u) => { + const user = u as UserDTO; + return [ + user.username, +
+
, + ]; + }) ?? [] + ); + }, [users, handleRemove]); + + const newUsersData = + useMemo( + () => + users + ?.filter((u) => typeof u === "string") + .map((u) => { + return [ + u, +
+
, + ]; + }), + [users, handleRemove] + ) ?? []; + + return ( + <> + {createPortal( + { + AddMembersDialogModal.close(); + }, + }, + { + priority: "primary", + children: t("invite"), + doClosesModal: false, + // onClick: handleSubmit(onSubmit), + }, + ]} + > +
+ { + return ( + onChange(users)} + /> + ); + }} + /> +
+ + {gpUsersData.length > 0 ? ( +
+ ) : ( +
{t("none")}
+ )} + +
+ + {newUsersData.length > 0 ? ( +
+ ) : ( +
{t("none")}
+ )} + + + , + document.body + )} + + ); +}; + +export { AddMembersDialog, AddMembersDialogModal }; + +// traductions +export const { i18n } = declareComponentKeys< + | "invite_title" + | "invite" + | "emails" + | { K: "emails_hint"; R: JSX.Element } + | { K: "email_not_valid"; P: { value: string }; R: string } + | "gp_users_to_add" + | "users_to_add" + | "none" +>()("AddMembersDialog"); + +export const AddMembersDialogFrTranslations: Translations<"fr">["AddMembersDialog"] = { + invite_title: "Inviter des membres", + invite: "Inviter", + emails: "Emails", + emails_hint: ( + <> +
+
    +
  • Invitez un utilisateur de la géoplateforme par son login ou son nom d’utilisateur.
  • +
  • + Invitez un utilisateur qui ne fait pas partie de la géoplateforme par son adresse email. Un message lui sera envoyé pour créer un compte + (séparez les adresses email par une virgule). +
  • +
  • Vous pouvez inviter plusieurs membres au groupe en une seule fois
  • +
+
+
+
Une fois les membres invités au groupe, vous pourrez ensuite en désigner certains comme gestionnaires.
+ + ), + email_not_valid: ({ value }) => `La chaîne ${value} n'est pas un email valide`, + gp_users_to_add: "Utilisateurs de la géoplateforme à ajouter", + users_to_add: "Utilisateurs hors géoplateforme à ajouter", + none: "Aucun", +}; + +export const AddMembersDialogEnTranslations: Translations<"en">["AddMembersDialog"] = { + invite_title: "Invite members", + invite: "Invite", + emails: "Emails", + emails_hint: undefined, + email_not_valid: ({ value }) => `${value} is not valid email`, + gp_users_to_add: undefined, + users_to_add: undefined, + none: "None", +}; diff --git a/assets/espaceco/pages/communities/management/member/ManageGridsDialog.tsx b/assets/espaceco/pages/communities/management/member/ManageGridsDialog.tsx new file mode 100644 index 00000000..fea4e773 --- /dev/null +++ b/assets/espaceco/pages/communities/management/member/ManageGridsDialog.tsx @@ -0,0 +1,186 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Accordion from "@codegouvfr/react-dsfr/Accordion"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import Table from "@codegouvfr/react-dsfr/Table"; +import { FC, useCallback, useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { GridDTO } from "../../../../../@types/espaceco"; +import { declareComponentKeys, Translations, useTranslation } from "../../../../../i18n/i18n"; +import GridList from "../GridList"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; + +const ManageGridsDialogModal = createModal({ + id: "manage-grids-modal", + isOpenedByDefault: false, +}); + +type ManageGridsDialogProps = { + communityGrids: GridDTO[]; + userGrids: GridDTO[]; + onApply: (grids: string[]) => void; +}; + +const ManageGridsDialog: FC = ({ communityGrids, userGrids, onApply }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("ManageGridsDialog"); + + const communityGridsData = useMemo(() => { + return communityGrids.map((g) => [g.name, g.title, g.type.title]); + }, [communityGrids]); + + const [grids, setGrids] = useState([]); + useEffect(() => { + setGrids([...userGrids]); + }, [userGrids]); + + const schema = yup.object({ + user_grids: yup.array().of(yup.string().required()), + }); + + const { + setValue: setFormValue, + getValues: getFormValues, + handleSubmit, + } = useForm({ + mode: "onSubmit", + resolver: yupResolver(schema), + values: { + user_grids: Array.from(grids, (g) => g.name), + }, + }); + + const getHeaders = useCallback( + (main: boolean) => { + const headers = [t("num_header"), t("title_header"), t("type_header")]; + if (!main) { + headers.push(""); + } + return headers; + }, + [t] + ); + + const onSubmit = () => { + ManageGridsDialogModal.close(); + + const userGrids = getFormValues("user_grids") ?? []; + onApply(userGrids); + }; + + return ( + <> + {createPortal( + { + ManageGridsDialogModal.close(); + }, + }, + { + priority: "primary", + children: t("add_grids"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + }, + ]} + > + <> + +
+ + {communityGridsData.length ? ( +
+ ) : ( +
+

{t("no_main_grids")}

+
+ )} + + + {/*
+ + {communityGridsData.length ? ( +
+ ) : ( +
+

{t("no_main_grids")}

+
+ )} + */} +
+ + { + const names = Array.from(grids, (g) => g.name); + setFormValue("user_grids", names); + }} + /> +
+ + , + document.body + )} + + ); +}; + +export { ManageGridsDialog, ManageGridsDialogModal }; + +// traductions +export const { i18n } = declareComponentKeys< + | "title" + | { K: "explain"; R: JSX.Element } + | "main_grids" + | "user_grids" + | "main_grids_explain" + | "no_main_grids" + | "add_grids" + | "num_header" + | "title_header" + | "type_header" +>()("ManageGridsDialog"); + +export const ManageGridsDialogFrTranslations: Translations<"fr">["ManageGridsDialog"] = { + title: "Gérer les emprises individuelles du membre", + explain: ( +

+ + Attention: Un utilisateur qui n’a aucune emprise géographique ne pourra faire que des signalements et aucune contribution + directe. + +

+ ), + main_grids: "Emprises générales du guichet", + user_grids: "Emprises individuelles du membre", + main_grids_explain: "Tous les membres du guichet héritent des emprises du guichet. Elles ne peuvent pas être supprimées.", + no_main_grids: "Aucune", + add_grids: "Ajouter les emprises individuelles", + num_header: "Numéro", + title_header: "Nom", + type_header: "Type", +}; + +export const ManageGridsDialogEnTranslations: Translations<"en">["ManageGridsDialog"] = { + title: undefined, + explain: undefined, + main_grids: undefined, + user_grids: undefined, + main_grids_explain: undefined, + no_main_grids: "None", + add_grids: "Add individual grids", + num_header: undefined, + title_header: undefined, + type_header: undefined, +}; diff --git a/assets/espaceco/pages/communities/management/member/SearchUsers.tsx b/assets/espaceco/pages/communities/management/member/SearchUsers.tsx new file mode 100644 index 00000000..78d41f75 --- /dev/null +++ b/assets/espaceco/pages/communities/management/member/SearchUsers.tsx @@ -0,0 +1,111 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import MuiDsfrThemeProvider from "@codegouvfr/react-dsfr/mui"; +import Autocomplete, { autocompleteClasses } from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import TextField from "@mui/material/TextField"; +import { useQuery } from "@tanstack/react-query"; +import { FC, ReactNode, useMemo } from "react"; +import { useDebounceValue } from "usehooks-ts"; +import { isUser } from "../../../../../@types/app_espaceco"; +import { UserDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import RQKeys from "../../../../../modules/espaceco/RQKeys"; +import api from "../../../../api"; + +export type SearchUsersProps = { + label: ReactNode; + hintText?: ReactNode; + value?: (UserDTO | string)[]; + state?: "default" | "error" | "success"; + stateRelatedMessage?: string; + onChange: (users: (UserDTO | string)[]) => void; +}; + +const SearchUsers: FC = ({ label, hintText, value, state, stateRelatedMessage, onChange }) => { + const { t } = useTranslation("Search"); + + const [search, setSearch] = useDebounceValue("", 500); + + const searchQuery = useQuery({ + queryKey: RQKeys.searchUsers(search), + queryFn: ({ signal }) => { + return api.user.search(search, signal); + }, + staleTime: 1000 * 60, + enabled: search.length >= 3, + }); + + const usernames = useMemo(() => { + return value?.map((v) => (isUser(v) ? v.username : v)); + }, [value]); + + return ( +
+ + + { + return ( + + {ownerState.getOptionLabel(option)} + + ); + }} + size={"small"} + loading={searchQuery.isLoading} + loadingText={t("loading")} + noOptionsText={t("no_results")} + getOptionLabel={(option) => (typeof option === "string" ? option : option.username)} + options={searchQuery.data?.filter((u) => !usernames?.includes(u.username)) ?? []} + renderInput={(params) => } + isOptionEqualToValue={(option, v) => { + if (isUser(option) && isUser(v)) return option.username === v.username; + if (typeof option !== typeof v) return false; + return option === v; + }} + onInputChange={(_, v) => setSearch(v)} + onChange={(_, v) => { + onChange(v); + }} + /> + + {state !== "default" && ( +

{ + switch (state) { + case "error": + return "fr-error-text"; + case "success": + return "fr-valid-text"; + } + })(), + "fr-mb-1v" + )} + > + {stateRelatedMessage} +

+ )} +
+ ); +}; + +export default SearchUsers; diff --git a/assets/espaceco/pages/communities/management/reports/AddAttributeDialog.tsx b/assets/espaceco/pages/communities/management/reports/AddAttributeDialog.tsx new file mode 100644 index 00000000..b44d87ab --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/AddAttributeDialog.tsx @@ -0,0 +1,218 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Input from "@codegouvfr/react-dsfr/Input"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import RadioButtons from "@codegouvfr/react-dsfr/RadioButtons"; +import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { FC, useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { AttributeDTO, AttributeType, AttributeTypes } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { AttributeValidations, validateList } from "./AttributeValidations"; +import { AddOrEditAttributeFormType, getInputType, normalizeAttribute } from "./ThemeUtils"; + +type AddAttributeDialogProps = { + modal: ReturnType; + attributes: AttributeDTO[]; + onAdd: (attribute: AttributeDTO) => void; +}; + +const defaultValues: AddOrEditAttributeFormType = { + name: "", + type: "text", + mandatory: false, + default: null, + help: null, + multiple: false, + values: null, +}; + +const AddAttributeDialog: FC = ({ modal, attributes, onAdd }) => { + const { t: tCommon } = useTranslation("Common"); + const { t } = useTranslation("Theme"); + + // Etrange, le register devrait suffire + const [type, setType] = useState("text"); + + const attributeNames: string[] = useMemo(() => { + return Array.from(attributes, (a) => a.name); + }, [attributes]); + + const schema = yup.object({ + name: yup + .string() + .trim(t("trimmed_error")) + .strict(true) + .required(t("dialog.add_attribute.name_mandatory_error")) + .test("is-unique", t("dialog.add_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(), + multiple: yup.boolean(), + values: yup + .string() + .nullable() + .test({ + name: "check-values", + test: (value, context) => { + return validateList(value, context); + }, + }), + }); + + const { + register, + watch, + formState: { errors }, + getValues: getFormValues, + setValue: setFormValue, + reset, + clearErrors, + handleSubmit, + } = useForm({ + mode: "onSubmit", + defaultValues: defaultValues, + resolver: yupResolver(schema), + }); + + const mandatory = watch("mandatory"); + const multiple = watch("multiple"); + + useEffect(() => { + setFormValue("default", ""); + clearErrors(["default", "values"]); + if (type !== "list") { + setFormValue("values", ""); + setFormValue("multiple", false); + } + }, [type, setFormValue, clearErrors]); + + useEffect(() => { + setFormValue("type", type); + }, [setFormValue, type]); + + const onSubmit = () => { + modal.close(); + onAdd(normalizeAttribute(getFormValues())); + reset(defaultValues); + }; + + return ( + <> + {createPortal( + { + reset(defaultValues); + modal.close(); + }, + }, + { + priority: "primary", + children: tCommon("add"), + doClosesModal: false, + onClick: handleSubmit(onSubmit), + }, + ]} + > +
+

{tCommon("mandatory_fields")}

+ + ({ + label: t("dialog.add_attribute.get_type", { type: attrType }), + nativeInputProps: { + checked: attrType === type, + onChange: () => setType(attrType), + }, + }))} + orientation={"horizontal"} + state={errors.type ? "error" : "default"} + stateRelatedMessage={errors?.type?.message} + /> + { + setFormValue("mandatory", checked); + }} + /> + {type === "list" && ( + <> + + { + setFormValue("multiple", checked); + }} + /> + + )} + + +
+
, + document.body + )} + + ); +}; + +export { AddAttributeDialog }; 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..18eae228 --- /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 AddThemeFormType = { + theme: string; + fullname?: string; + help?: string; + global?: boolean; +}; + +const normalize = (theme: AddThemeFormType): 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: AddThemeFormType = { + 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, (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("dialog.edit_theme.name_mandatory_error")) + .test("is-unique", t("dialog.add_theme.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"); + const global = watch("global"); + + 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/Answers.tsx b/assets/espaceco/pages/communities/management/reports/Answers.tsx new file mode 100644 index 00000000..dfeff804 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/Answers.tsx @@ -0,0 +1,40 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import ToggleSwitch from "@codegouvfr/react-dsfr/ToggleSwitch"; +import { FC } from "react"; +import { Controller, UseFormReturn } from "react-hook-form"; +import { ReportFormType } from "../../../../../@types/app_espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; + +type AnswersProps = { + form: UseFormReturn; +}; + +const Answers: FC = ({ form }) => { + const { t } = useTranslation("ManageCommunity"); + + const { control, watch } = form; + + const allMembersCanValid = watch("all_members_can_valid"); + + return ( +
+
{t("report.manage_permissions.report_answers")}
+ ( + + )} + /> +
+ ); +}; + +export default Answers; diff --git a/assets/espaceco/pages/communities/management/reports/AttributeList.tsx b/assets/espaceco/pages/communities/management/reports/AttributeList.tsx new file mode 100644 index 00000000..fcfa4697 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/AttributeList.tsx @@ -0,0 +1,112 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { FC, useCallback, useMemo } from "react"; +import { UseFormReturn } from "react-hook-form"; +import { v4 as uuidv4 } from "uuid"; +import { ReportFormType } from "../../../../../@types/app_espaceco"; +import { ThemeDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { AddAttributeDialog } from "./AddAttributeDialog"; +import EditAttributeDialog from "./EditAttributeDialog"; +import ThemesHelper from "./ThemesHelper"; + +type AttributeListProps = { + form: UseFormReturn; + theme: ThemeDTO; +}; + +const AttributeList: FC = ({ form, theme }) => { + const { t } = useTranslation("Theme"); + + const { watch, setValue: setFormValue } = form; + const themes: ThemeDTO[] = watch("attributes"); + + // Suppression d'un attribut de theme + const handleRemoveAttribute = (attribute: string) => { + const tm = ThemesHelper.removeAttribute(theme.theme, attribute, themes); + setFormValue("attributes", tm); + }; + + const AddAttributeDialogModal: ReturnType = useMemo( + () => + createModal({ + id: `add-attribute-${uuidv4()}`, + isOpenedByDefault: false, + }), + [] + ); + + const getEditModal = useCallback((): ReturnType => { + return createModal({ + id: `edit-attribute-${uuidv4()}`, + isOpenedByDefault: false, + }); + }, []); + + return ( +
+
+
    + {theme?.attributes.map((a) => { + const modal = getEditModal(); + + return ( +
  • +
    +
    {a.name}
    +
    +
    +
    +
    +
    + { + const tm = ThemesHelper.updateAttribute(theme.theme, a.name, newAttribute, themes); + setFormValue("attributes", tm); + }} + /> +
  • + ); + })} +
+ {/* Pas d'ajout d'attributs pour les thèmes liés à des tables */} + {!theme.table && ( + + )} +
+ { + const tm = ThemesHelper.addAttribute(theme.theme, attribute, themes); + setFormValue("attributes", tm); + }} + /> +
+ ); +}; + +export default AttributeList; diff --git a/assets/espaceco/pages/communities/management/reports/AttributeValidations.tsx b/assets/espaceco/pages/communities/management/reports/AttributeValidations.tsx new file mode 100644 index 00000000..7be9416c --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/AttributeValidations.tsx @@ -0,0 +1,100 @@ +import * as yup from "yup"; +import { getTranslation } from "../../../../../i18n/i18n"; +import { isInt, isFloat, isDate } from "validator"; + +const { t } = getTranslation("Theme"); + +class AttributeValidations { + #context: yup.TestContext; + + constructor(context: yup.TestContext) { + this.#context = context; + } + + validateValue = (value?: string | null) => { + if (value === undefined || value === null) return true; + + const { + parent: { type }, + } = this.#context; + + const v = value.trim(); + + switch (type) { + case "text": + return true; + case "integer": + return this.#validateInteger(v); + case "double": + return this.#validateFloat(v); + case "checkbox": { + if (!["0", "1"].includes(v)) { + return this.#context.createError({ message: t("dialog.add_attribute.value.not_a_valid_checkbox") }); + } + return true; + } + case "list": + return this.#validateInList(v); + case "date": + return this.#validateDate(v); + } + }; + + #validateInteger = (value: string): yup.ValidationError | boolean => { + if (!value) return true; + if (!isInt(value, { allow_leading_zeroes: false })) { + return this.#context.createError({ message: t("dialog.add_attribute.value.not_a_valid_integer") }); + } + return true; + }; + + #validateFloat = (value: string): yup.ValidationError | boolean => { + if (!value) return true; + if (!isFloat(value)) { + return this.#context.createError({ message: t("dialog.add_attribute.value.not_a_valid_double") }); + } + return true; + }; + + #validateDate = (value: string): yup.ValidationError | boolean => { + if (!value) return true; + if (!isDate(value)) { + return this.#context.createError({ message: t("dialog.add_attribute.value.not_a_valid_date") }); + } + return true; + }; + + #validateInList = (value: string): yup.ValidationError | boolean => { + const { + parent: { values }, + } = this.#context; + + const list: string[] = values ? values.split("|") : []; + if (!list.includes(value)) { + return this.#context.createError({ message: t("dialog.add_attribute.value_not_in_list_error") }); + } + return true; + }; +} + +const validateList = (value: string | null | undefined, context: yup.TestContext): yup.ValidationError | boolean => { + const { + parent: { type }, + } = context; + + if (type !== "list") { + return true; + } + + const list: string[] = value ? value.split("|") : []; + if (!list.length) { + return context.createError({ message: t("dialog.add_attribute.type_list_not_empty_error") }); + } + const duplicates = list.filter((item, index) => list.indexOf(item) !== index); + if (duplicates.length) { + return context.createError({ message: t("dialog.add_attribute.list_duplicates_error") }); + } + return true; +}; + +export { AttributeValidations, validateList }; 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 new file mode 100644 index 00000000..f25b578c --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/EditThemeDialog.tsx @@ -0,0 +1,134 @@ +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 EditThemeFormType = { + theme: string; + table?: string; + global?: boolean; + help?: string; +}; + +type EditThemeDialogProps = { + modal: ReturnType; + themes: ThemeDTO[]; + currentTheme?: ThemeDTO; + // tables: Partial[]; + onModify: (oldName: string, newTheme: EditThemeFormType) => void; +}; + +const EditThemeDialog: FC = ({ modal, 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("dialog.edit_theme.name_mandatory_error")) + .test("is-unique", t("dialog.edit_theme.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 = () => { + modal.close(); + if (currentTheme) { + const values = getFormValues(); + onModify(currentTheme?.theme, values); + } + }; + + return ( + <> + {createPortal( + +
+

{tCommon("mandatory_fields")}

+ + + {currentTheme?.table ? ( +
+ ) : ( + { + setFormValue("global", checked); + }} + /> + )} +
+ , + document.body + )} + + ); +}; + +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..bd6cd72d --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/Permissions.tsx @@ -0,0 +1,41 @@ +import Checkbox from "@codegouvfr/react-dsfr/Checkbox"; +import { FC } from "react"; +import { UseFormReturn } from "react-hook-form"; +import { ReportFormType } from "../../../../../@types/app_espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; + +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 new file mode 100644 index 00000000..b782e9a7 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ReportStatuses.tsx @@ -0,0 +1,107 @@ +import { fr } from "@codegouvfr/react-dsfr"; +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 } from "../../../../../@types/app_espaceco"; +import { ReportStatusesType } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { EditReportParameterModal, EditReportStatusDialog } from "./EditReportStatusDialog"; +import { statusesAlwaysActive } from "./Utils"; + +type ReportStatusesProps = { + form: UseFormReturn; + state?: "default" | "error" | "success"; +}; + +// const minStatuses = getMinAuthorizedStatus(); + +const ReportStatuses: FC = ({ form, state }) => { + const { t: tStatus } = useTranslation("ReportStatuses"); + const { t } = useTranslation("ManageCommunity"); + + 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")} +
+ { + 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); + } + }} + /> +
+ ); +}; + +export default ReportStatuses; 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..3a9f1e5d --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/SharedThemes.tsx @@ -0,0 +1,135 @@ +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 } from "../../../../../@types/app_espaceco"; +import { declareComponentKeys, Translations, useTranslation } from "../../../../../i18n/i18n"; +import { SetSharedThemesDialogModal, SetSharedThemesDialog, UserSharedThemesType, SharedThemesType } from "./SetSharedThemesDialog"; +import { SharedThemesDTO } from "../../../../../@types/espaceco"; + +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 new file mode 100644 index 00000000..8027ab6d --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ThemeList.tsx @@ -0,0 +1,142 @@ +import { fr } from "@codegouvfr/react-dsfr"; +import Button from "@codegouvfr/react-dsfr/Button"; +import { createModal } from "@codegouvfr/react-dsfr/Modal"; +import { cx } from "@codegouvfr/react-dsfr/tools/cx"; +import { CSSProperties, FC, useCallback } from "react"; +import { UseFormReturn } from "react-hook-form"; +import { v4 as uuidv4 } from "uuid"; +import { ReportFormType } from "../../../../../@types/app_espaceco"; +import { TableResponseDTO, ThemeDTO } from "../../../../../@types/espaceco"; +import { useTranslation } from "../../../../../i18n/i18n"; +import { AddThemeDialog, AddThemeDialogModal } from "./AddThemeDialog"; +import AttributeList from "./AttributeList"; +import EditThemeDialog from "./EditThemeDialog"; +import ThemesHelper from "./ThemesHelper"; + +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[]; + state?: "default" | "error" | "success"; +}; + +const ThemeList: FC = ({ form, tables, state }) => { + const { t } = useTranslation("ManageCommunity"); + const { t: tTheme } = useTranslation("Theme"); + + const { watch, setValue: setFormValue } = form; + const themes: ThemeDTO[] = watch("attributes"); + + // Supression d'un theme + const handleRemoveTheme = (theme: string) => { + const th = ThemesHelper.removeTheme(theme, themes); + setFormValue("attributes", th); + }; + + const getEditModal = useCallback((): ReturnType => { + return createModal({ + id: `edit-theme-${uuidv4()}`, + isOpenedByDefault: false, + }); + }, []); + + return ( +
+

{t("report.configure_themes")}

+ {t("report.configure_themes.explain")} +
+ + {themes.map((t) => { + const modal = getEditModal(); + + return ( +
+
+
+ {t.theme} + {t.table && ( + + + + )} + {t.global !== undefined && t.global === true && ( + + + + )} +
+
+
+
+
+
+ { + const th = ThemesHelper.updateTheme(oldName, newTheme, themes); + setFormValue("attributes", th); + }} + /> + {!t.table && } +
+ ); + })} +
+ {state !== "default" && ( +

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

+ )} + { + const th = ThemesHelper.addTheme(themes, theme); + setFormValue("attributes", th); + }} + /> +
+ ); +}; + +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..45cb519a --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ThemeTr.tsx @@ -0,0 +1,198 @@ +import { declareComponentKeys, Translations } from "../../../../../i18n/i18n"; + +// traductions +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 } + | { K: "delete_attribute"; P: { text: string }; R: string } + | "dialog.add_theme.name" + | "dialog.add_theme.name_mandatory_error" + | "dialog.add_theme.name_unique_error" + | "dialog.add_theme.help" + | "dialog.add_theme.help_hint" + | "dialog.add_theme.link_to_table" + | "dialog.add_theme.link_to_table_hint" + | "dialog.add_theme.not_link" + | "dialog.add_theme.global" + | "dialog.add_theme.global_hint" + | "dialog.edit_theme.name" + | "dialog.edit_theme.name_hint" + | "dialog.edit_theme.name_mandatory_error" + | "dialog.edit_theme.name_unique_error" + | "dialog.edit_theme.help" + | "dialog.edit_theme.global" + | "dialog.edit_theme.global_hint" + | "dialog.add_attribute.name" + | "dialog.add_attribute.mandatory" + | "dialog.add_attribute.name" + | "dialog.add_attribute.name_mandatory_error" + | "dialog.add_attribute.name_unique_error" + | "dialog.add_attribute.mandatory" + | "dialog.add_attribute.type" + | { K: "dialog.add_attribute.get_type"; P: { type: string }; R: string } + | "dialog.add_attribute.list.multiple" + | "dialog.add_attribute.list.values" + | "dialog.add_attribute.value" + | "dialog.add_attribute.value.not_a_valid_integer" + | "dialog.add_attribute.value.not_a_valid_double" + | "dialog.add_attribute.value.not_a_valid_checkbox" + | "dialog.add_attribute.value.not_a_valid_date" + | "dialog.add_attribute.type_list_not_empty_error" + | "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}]`, + delete_attribute: ({ text }) => `Supprimer l'attribut [${text}]`, + "dialog.add_theme.name": "Nom du thème", + "dialog.add_theme.name_mandatory_error": "Le nom du thème est obligatoire", + "dialog.add_theme.name_unique_error": "Le nom doit être unique", + "dialog.add_theme.help": "Texte d'aide (optionnel)", + "dialog.add_theme.help_hint": "Cette description aidera les membres à décrire plus précisément leur signalement", + "dialog.add_theme.link_to_table": "Lier le thème à une base de données et à une table", + "dialog.add_theme.link_to_table_hint": "Un thème lié à une base de données et à une table ne pourra pas être modifié", + "dialog.add_theme.not_link": "Ne pas lier à une table", + "dialog.add_theme.global": "Partage", + "dialog.add_theme.global_hint": "Partager ce thème le rendra visible et sélectionnable pour des signalement non liés à des guichets", + "dialog.edit_theme.name": "Nouveau nom", + "dialog.edit_theme.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.", + "dialog.edit_theme.name_mandatory_error": "Le nom du thème est obligatoire", + "dialog.edit_theme.name_unique_error": "Le nom doit être unique", + "dialog.edit_theme.help": "Nouvelle description (optionnel)", + "dialog.edit_theme.global": "Partage", + "dialog.edit_theme.global_hint": "Partager ce thème le rendra visible et sélectionnable pour des signalement non liés à des guichets", + "dialog.add_attribute.name": "Nom de l'attribut", + "dialog.add_attribute.mandatory": "Attribut obligatoire", + "dialog.add_attribute.name_mandatory_error": "Le nom de l'attribut est obligatoire", + "dialog.add_attribute.name_unique_error": "Le nom doit être unique", + "dialog.add_attribute.type": "Type", + "dialog.add_attribute.get_type": ({ type }) => { + switch (type) { + case "text": + return "Texte"; + case "integer": + return "Entier"; + case "double": + return "Double"; + case "checkbox": + return "Case à cocher"; + case "list": + return "Liste déroulante"; + case "date": + return "Date"; + default: + return ""; + } + }, + "dialog.add_attribute.list.multiple": "Choix multiple", + "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", + "dialog.add_attribute.value.not_a_valid_checkbox": "La valeur doit être 0 ou 1", + "dialog.add_attribute.value.not_a_valid_date": "La valeur n'est pas une date valide", + "dialog.add_attribute.type_list_not_empty_error": "La liste de valeurs ne doit pas être vide", + "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}]`, + delete_attribute: ({ text }) => `Delete attribute [${text}]`, + "dialog.add_theme.name": undefined, + "dialog.add_theme.name_mandatory_error": undefined, + "dialog.add_theme.name_unique_error": undefined, + "dialog.add_theme.help": undefined, + "dialog.add_theme.help_hint": undefined, + "dialog.add_theme.link_to_table": undefined, + "dialog.add_theme.link_to_table_hint": undefined, + "dialog.add_theme.not_link": undefined, + "dialog.add_theme.global": undefined, + "dialog.add_theme.global_hint": undefined, + "dialog.edit_theme.name": undefined, + "dialog.edit_theme.name_hint": undefined, + "dialog.edit_theme.name_mandatory_error": undefined, + "dialog.edit_theme.name_unique_error": undefined, + "dialog.edit_theme.help": undefined, + "dialog.edit_theme.global": undefined, + "dialog.edit_theme.global_hint": undefined, + "dialog.add_attribute.name": undefined, + "dialog.add_attribute.mandatory": undefined, + "dialog.add_attribute.name_mandatory_error": undefined, + "dialog.add_attribute.name_unique_error": undefined, + "dialog.add_attribute.type": undefined, + "dialog.add_attribute.get_type": ({ type }) => { + switch (type) { + case "text": + return "Text"; + case "integer": + return "Integer"; + case "double": + return "Double"; + case "checkbox": + return "Checkbox"; + case "list": + return "List"; + case "date": + return "Date"; + default: + return ""; + } + }, + "dialog.add_attribute.list.multiple": undefined, + "dialog.add_attribute.list.values": undefined, + "dialog.add_attribute.value": undefined, + "dialog.add_attribute.value.not_a_valid_integer": undefined, + "dialog.add_attribute.value.not_a_valid_double": undefined, + "dialog.add_attribute.value.not_a_valid_checkbox": undefined, + "dialog.add_attribute.value.not_a_valid_date": undefined, + "dialog.add_attribute.type_list_not_empty_error": undefined, + "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 new file mode 100644 index 00000000..8ac911e8 --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/ThemeUtils.tsx @@ -0,0 +1,53 @@ +import { AttributeDTO, AttributeType, ThemeDTO } from "../../../../../@types/espaceco"; + +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]) { + delete result[f]; + } + }); + return result; +}; + +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 new file mode 100644 index 00000000..2dcd1e0a --- /dev/null +++ b/assets/espaceco/pages/communities/management/reports/Utils.tsx @@ -0,0 +1,24 @@ +import { ReportStatusesDTO } from "../../../../../@types/espaceco"; +import statuses from "../../../../../data/report_statuses.json"; + +const getDefaultStatuses = (): ReportStatusesDTO => { + const result = {}; + Object.keys(statuses).forEach((s) => { + result[s] = { title: statuses[s], active: true }; + }); + return result as ReportStatusesDTO; +}; + +const getMinAuthorizedStatus = (): number => { + return Object.keys(statuses).length - 2; +}; + +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/espaceco/pages/communities/management/validationTr.tsx b/assets/espaceco/pages/communities/management/validationTr.tsx new file mode 100644 index 00000000..737c907f --- /dev/null +++ b/assets/espaceco/pages/communities/management/validationTr.tsx @@ -0,0 +1,72 @@ +import { declareComponentKeys } from "i18nifty"; +import { Translations } from "../../../../i18n/i18n"; + +// traductions +export const { i18n } = declareComponentKeys< + | "trimmed_error" + | "description.name.mandatory" + | "description.name.unique" + | "description.name.minlength" + | "description.name.maxlength" + | "description.desc.mandatory" + | "description.desc.maxlength" + | "description.logo.size_error" + | "description.logo.dimensions_error" + | "description.logo.format_error" + | { K: "zoom.extent.nan"; P: { field: string }; R: string } + | { K: "zoom.extent.mandatory"; P: { field: string }; R: string } + | { K: "zoom.f1_less_than_f2"; P: { field1: string; field2: string }; R: string } + | { K: "zoom.less_than"; P: { field: string; v: number }; R: string } + | { K: "zoom.greater_than"; P: { field: string; v: number }; R: string } + | "zoom.extent.required" + | "description.modal.document.name.mandatory" + | "description.modal.document.name.minlength" + | "description.modal.document.file.mandatory" + | "description.modal.document.file.size_error" +>()("ManageCommunityValidations"); + +export const ManageCommunityValidationsFrTranslations: Translations<"fr">["ManageCommunityValidations"] = { + trimmed_error: "La chaîne de caractères ne doit contenir aucun espace en début et fin", + "description.name.mandatory": "Le nom est obligatoire", + "description.name.unique": "Ce nom existe déjà", + "description.name.minlength": "Le nom doit faire au moins 2 caractères", + "description.name.maxlength": "Le nom ne doit pas dépasser 80 caractères", + "description.desc.mandatory": "La description est obligatoire", + "description.desc.maxlength": "La description ne doit pas faire plus de 1024 caractères", + "description.logo.size_error": "La taille du fichier ne peut excéder 5 Mo", + "description.logo.dimensions_error": "Les dimensions maximales de l'image sont de 400px x 400px", + "description.logo.format_error": "Le fichier doit être au format jpeg ou png", + "zoom.extent.nan": ({ field }) => `${field} n'est pas un nombre`, + "zoom.extent.mandatory": ({ field }) => `La valeur ${field} est obligatoire`, + "zoom.f1_less_than_f2": ({ field1, field2 }) => `La valeur de ${field1} doit être inférieure à la valeur de ${field2}`, + "zoom.less_than": ({ field, v }) => `La valeur de ${field} doit être inférieure ou égale à ${v}`, + "zoom.greater_than": ({ field, v }) => `La valeur de ${field} doit être supérieure ou égale à ${v}`, + "zoom.extent.required": "La boîte englobante est obligatoire", + "description.modal.document.name.mandatory": "Le nom est obligatoire", + "description.modal.document.name.minlength": "Le nom doit faire au moins 7 caractères", + "description.modal.document.file.mandatory": "Le fichier est obligatoire", + "description.modal.document.file.size_error": "La taille du fichier ne peut excéder 5 Mo", +}; + +export const ManageCommunityValidationsEnTranslations: Translations<"en">["ManageCommunityValidations"] = { + trimmed_error: undefined, + "description.name.mandatory": undefined, + "description.name.unique": "Name already exists", + "description.name.minlength": undefined, + "description.name.maxlength": undefined, + "description.desc.mandatory": undefined, + "description.desc.maxlength": undefined, + "description.logo.size_error": undefined, + "description.logo.dimensions_error": undefined, + "description.logo.format_error": undefined, + "zoom.extent.nan": ({ field }) => `${field} is not a number`, + "zoom.extent.mandatory": ({ field }) => `${field} value is mandatory`, + "zoom.f1_less_than_f2": ({ field1, field2 }) => `${field1} value must be less then ${field2} value`, + "zoom.less_than": ({ field, v }) => `${field} value must be less or equal to ${v}`, + "zoom.greater_than": ({ field, v }) => `${field} value must be greater or equal to ${v}`, + "zoom.extent.required": undefined, + "description.modal.document.name.mandatory": undefined, + "description.modal.document.name.minlength": undefined, + "description.modal.document.file.mandatory": undefined, + "description.modal.document.file.size_error": undefined, +}; 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 7e83649f..18434b4a 100644 --- a/assets/i18n/Common.tsx +++ b/assets/i18n/Common.tsx @@ -6,12 +6,15 @@ export const { i18n } = declareComponentKeys< | "add" | "adding" | "modify" + | "apply" + | "record" | "modifying" | "removing" | "loading" | "continue" | "validate" | "submit" + | "save" | "copy" | "send" | "cancel" @@ -31,6 +34,7 @@ export const { i18n } = declareComponentKeys< | "next_step" | "url_copied" | "copy_to_clipboard" + | "trimmed_error" >()("Common"); export const commonFrTranslations: Translations<"fr">["Common"] = { @@ -39,12 +43,15 @@ export const commonFrTranslations: Translations<"fr">["Common"] = { add: "Ajouter", 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", @@ -64,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"] = { @@ -72,12 +80,15 @@ export const commonEnTranslations: Translations<"en">["Common"] = { add: "Add", 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", @@ -97,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 083281ab..f66df667 100644 --- a/assets/i18n/i18n.ts +++ b/assets/i18n/i18n.ts @@ -50,7 +50,16 @@ export type ComponentKey = | typeof import("../entrepot/pages/service/TableSelection").i18n | typeof import("../entrepot/pages/service/AccessRestrictions").i18n | typeof import("../entrepot/pages/service/wms-vector/UploadStyleFile").i18n - | typeof import("../espaceco/pages/communities/EspaceCoCommunitiesTr").i18n; + | 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/reports/ThemeTr").i18n + | typeof import("../espaceco/pages/communities/management/reports/ReportStatusesTr").i18n + | typeof import("../espaceco/pages/communities/management/reports/SharedThemes").i18n + | typeof import("../espaceco/pages/communities/management/Members").i18n + | typeof import("../espaceco/pages/communities/management/member/AddMembersDialog").i18n + | typeof import("../espaceco/pages/communities/management/member/ManageGridsDialog").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 72a3c27c..f2da7811 100644 --- a/assets/i18n/languages/en.tsx +++ b/assets/i18n/languages/en.tsx @@ -3,9 +3,9 @@ import { navItemsEnTranslations } from "../../config/navItems"; import { AccessesRequestEnTranslations } from "../../entrepot/pages/AccessesRequest"; import { AddMemberEnTranslations } from "../../entrepot/pages/communities/AddMember"; import { CommunityMembersEnTranslations } from "../../entrepot/pages/communities/CommunityMembers"; -import { contactEnTranslations } from "../../pages/assistance/contact/Contact"; import { DashboardProEnTranslations } from "../../entrepot/pages/dashboard/DashboardPro"; import { DatasheetListEnTranslations } from "../../entrepot/pages/datasheet/DatasheetList/DatasheetList"; +import { DatasheetUploadFormEnTranslations } from "../../entrepot/pages/datasheet/DatasheetNew/DatasheetUploadForm"; import { PyramidListItemFrTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/PyramidList/PyramidListItem"; import { VectorDbListItemEnTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasetListTab/VectorDbList/VectorDbListItem"; import { DatasheetViewEnTranslations } from "../../entrepot/pages/datasheet/DatasheetView/DatasheetView"; @@ -24,16 +24,26 @@ import { MyAccessKeysEnTranslations } from "../../entrepot/pages/users/MyAccessK import { UserKeyEnTranslations } from "../../entrepot/pages/users/keys/UserKeyTr"; import { UserKeysListTabEnTranslations } from "../../entrepot/pages/users/keys/UserKeysListTab"; import { PermissionsEnTranslations } from "../../entrepot/pages/users/permissions/PermissionsTr"; -import { EspaceCoCommunitiesEnTranslations } from "../../espaceco/pages/communities/EspaceCoCommunitiesTr"; +import { CommunityListEnTranslations } from "../../espaceco/pages/communities/CommunityListTr"; +import { ManageCommunityEnTranslations } from "../../espaceco/pages/communities/ManageCommunityTr"; +import { ManageCommunityValidationsEnTranslations } from "../../espaceco/pages/communities/management/validationTr"; +import { SearchEnTranslations } from "../../espaceco/pages/communities/management/SearchTr"; import { TMSStyleFilesManagerEnTranslations } from "../../modules/Style/TMSStyleFilesManager"; +import { contactEnTranslations } from "../../pages/assistance/contact/Contact"; import { mapboxStyleValidationEnTranslations } from "../../validations/MapboxStyleValidator"; import { SldStyleValidationErrorsEnTranslations } from "../../validations/SldStyleValidationErrorsTr"; -import { commonEnTranslations } from "../Common"; 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 { ReportStatusesEnTranslations } from "../../espaceco/pages/communities/management/reports/ReportStatusesTr"; +import { SharedThemesEnTranslations } from "../../espaceco/pages/communities/management/reports/SharedThemes"; +import { EscoCommunityMembersEnTranslations } from "../../espaceco/pages/communities/management/Members"; +import { AddMembersDialogEnTranslations } from "../../espaceco/pages/communities/management/member/AddMembersDialog"; +import { ManageGridsDialogEnTranslations } from "../../espaceco/pages/communities/management/member/ManageGridsDialog"; + import type { Translations } from "../i18n"; -import { DatasheetUploadFormEnTranslations } from "../../entrepot/pages/datasheet/DatasheetNew/DatasheetUploadForm"; export const translations: Translations<"en"> = { Common: commonEnTranslations, @@ -67,8 +77,17 @@ export const translations: Translations<"en"> = { TableSelection: TableSelectionEnTranslations, UploadStyleFile: UploadStyleFileEnTranslations, PyramidVectorTmsServiceForm: PyramidVectorTmsServiceFormEnTranslations, - EspaceCoCommunities: EspaceCoCommunitiesEnTranslations, DatasheetUploadForm: DatasheetUploadFormEnTranslations, DatasheetList: DatasheetListEnTranslations, AccessRestrictions: AccessRestrictionsEnTranslations, + CommunityList: CommunityListEnTranslations, + ManageCommunity: ManageCommunityEnTranslations, + ManageCommunityValidations: ManageCommunityValidationsEnTranslations, + Theme: ThemeEnTranslations, + ReportStatuses: ReportStatusesEnTranslations, + SharedThemes: SharedThemesEnTranslations, + Search: SearchEnTranslations, + EscoCommunityMembers: EscoCommunityMembersEnTranslations, + AddMembersDialog: AddMembersDialogEnTranslations, + ManageGridsDialog: ManageGridsDialogEnTranslations, }; diff --git a/assets/i18n/languages/fr.tsx b/assets/i18n/languages/fr.tsx index 3ace06bb..e61b1f2e 100644 --- a/assets/i18n/languages/fr.tsx +++ b/assets/i18n/languages/fr.tsx @@ -24,15 +24,25 @@ import { MyAccessKeysFrTranslations } from "../../entrepot/pages/users/MyAccessK import { UserKeyFrTranslations } from "../../entrepot/pages/users/keys/UserKeyTr"; import { UserKeysListTabFrTranslations } from "../../entrepot/pages/users/keys/UserKeysListTab"; import { PermissionsFrTranslations } from "../../entrepot/pages/users/permissions/PermissionsTr"; -import { EspaceCoCommunitiesFrTranslations } from "../../espaceco/pages/communities/EspaceCoCommunitiesTr"; +import { CommunityListFrTranslations } from "../../espaceco/pages/communities/CommunityListTr"; +import { ManageCommunityFrTranslations } from "../../espaceco/pages/communities/ManageCommunityTr"; +import { ManageCommunityValidationsFrTranslations } from "../../espaceco/pages/communities/management/validationTr"; +import { SearchFrTranslations } from "../../espaceco/pages/communities/management/SearchTr"; import { TMSStyleFilesManagerFrTranslations } from "../../modules/Style/TMSStyleFilesManager"; import { contactFrTranslations } from "../../pages/assistance/contact/Contact"; import { mapboxStyleValidationFrTranslations } from "../../validations/MapboxStyleValidator"; import { SldStyleValidationErrorsFrTranslations } from "../../validations/SldStyleValidationErrorsTr"; -import { commonFrTranslations } from "../Common"; 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 { ReportStatusesFrTranslations } from "../../espaceco/pages/communities/management/reports/ReportStatusesTr"; +import { SharedThemesFrTranslations } from "../../espaceco/pages/communities/management/reports/SharedThemes"; +import { EscoCommunityMembersFrTranslations } from "../../espaceco/pages/communities/management/Members"; +import { AddMembersDialogFrTranslations } from "../../espaceco/pages/communities/management/member/AddMembersDialog"; +import { ManageGridsDialogFrTranslations } from "../../espaceco/pages/communities/management/member/ManageGridsDialog"; + import type { Translations } from "../i18n"; export const translations: Translations<"fr"> = { @@ -67,8 +77,17 @@ export const translations: Translations<"fr"> = { TableSelection: TableSelectionFrTranslations, UploadStyleFile: UploadStyleFileFrTranslations, PyramidVectorTmsServiceForm: PyramidVectorTmsServiceFormFrTranslations, - EspaceCoCommunities: EspaceCoCommunitiesFrTranslations, DatasheetUploadForm: DatasheetUploadFormFrTranslations, DatasheetList: DatasheetListFrTranslations, AccessRestrictions: AccessRestrictionsFrTranslations, + CommunityList: CommunityListFrTranslations, + ManageCommunity: ManageCommunityFrTranslations, + ManageCommunityValidations: ManageCommunityValidationsFrTranslations, + Theme: ThemeFrTranslations, + ReportStatuses: ReportStatusesFrTranslations, + SharedThemes: SharedThemesFrTranslations, + Search: SearchFrTranslations, + EscoCommunityMembers: EscoCommunityMembersFrTranslations, + AddMembersDialog: AddMembersDialogFrTranslations, + ManageGridsDialog: ManageGridsDialogFrTranslations, }; diff --git a/assets/img/punaise.png b/assets/img/punaise.png new file mode 100644 index 00000000..0e0786a4 Binary files /dev/null and b/assets/img/punaise.png differ diff --git a/assets/img/vignettes/7z.png b/assets/img/vignettes/7z.png new file mode 100644 index 00000000..603b701d Binary files /dev/null and b/assets/img/vignettes/7z.png differ diff --git a/assets/img/vignettes/abw.png b/assets/img/vignettes/abw.png new file mode 100644 index 00000000..1d4e3226 Binary files /dev/null and b/assets/img/vignettes/abw.png differ diff --git a/assets/img/vignettes/ai.png b/assets/img/vignettes/ai.png new file mode 100644 index 00000000..b7fa8f0d Binary files /dev/null and b/assets/img/vignettes/ai.png differ diff --git a/assets/img/vignettes/aiff.png b/assets/img/vignettes/aiff.png new file mode 100644 index 00000000..bade6331 Binary files /dev/null and b/assets/img/vignettes/aiff.png differ diff --git a/assets/img/vignettes/asf.png b/assets/img/vignettes/asf.png new file mode 100644 index 00000000..c4d738b2 Binary files /dev/null and b/assets/img/vignettes/asf.png differ diff --git a/assets/img/vignettes/avi.png b/assets/img/vignettes/avi.png new file mode 100644 index 00000000..c4d738b2 Binary files /dev/null and b/assets/img/vignettes/avi.png differ diff --git a/assets/img/vignettes/bin.png b/assets/img/vignettes/bin.png new file mode 100644 index 00000000..5dcd476e Binary files /dev/null and b/assets/img/vignettes/bin.png differ diff --git a/assets/img/vignettes/blend.png b/assets/img/vignettes/blend.png new file mode 100644 index 00000000..d525803e Binary files /dev/null and b/assets/img/vignettes/blend.png differ diff --git a/assets/img/vignettes/bmp.png b/assets/img/vignettes/bmp.png new file mode 100644 index 00000000..41c71675 Binary files /dev/null and b/assets/img/vignettes/bmp.png differ diff --git a/assets/img/vignettes/bz2.png b/assets/img/vignettes/bz2.png new file mode 100644 index 00000000..27776c90 Binary files /dev/null and b/assets/img/vignettes/bz2.png differ diff --git a/assets/img/vignettes/c.png b/assets/img/vignettes/c.png new file mode 100644 index 00000000..0f13b7b9 Binary files /dev/null and b/assets/img/vignettes/c.png differ diff --git a/assets/img/vignettes/crq.png b/assets/img/vignettes/crq.png new file mode 100644 index 00000000..dca327cc Binary files /dev/null and b/assets/img/vignettes/crq.png differ diff --git a/assets/img/vignettes/css.png b/assets/img/vignettes/css.png new file mode 100644 index 00000000..1a94efd4 Binary files /dev/null and b/assets/img/vignettes/css.png differ diff --git a/assets/img/vignettes/csv.png b/assets/img/vignettes/csv.png new file mode 100644 index 00000000..89479cdd Binary files /dev/null and b/assets/img/vignettes/csv.png differ diff --git a/assets/img/vignettes/deb.png b/assets/img/vignettes/deb.png new file mode 100644 index 00000000..5770e190 Binary files /dev/null and b/assets/img/vignettes/deb.png differ diff --git a/assets/img/vignettes/defaut.png b/assets/img/vignettes/defaut.png new file mode 100644 index 00000000..83de589b Binary files /dev/null and b/assets/img/vignettes/defaut.png differ diff --git a/assets/img/vignettes/djvu.png b/assets/img/vignettes/djvu.png new file mode 100644 index 00000000..2352a88d Binary files /dev/null and b/assets/img/vignettes/djvu.png differ diff --git a/assets/img/vignettes/doc.png b/assets/img/vignettes/doc.png new file mode 100644 index 00000000..6b0c91e6 Binary files /dev/null and b/assets/img/vignettes/doc.png differ diff --git a/assets/img/vignettes/docx.png b/assets/img/vignettes/docx.png new file mode 100644 index 00000000..6b0c91e6 Binary files /dev/null and b/assets/img/vignettes/docx.png differ diff --git a/assets/img/vignettes/dvi.png b/assets/img/vignettes/dvi.png new file mode 100644 index 00000000..5fe36239 Binary files /dev/null and b/assets/img/vignettes/dvi.png differ diff --git a/assets/img/vignettes/dxf.png b/assets/img/vignettes/dxf.png new file mode 100644 index 00000000..7aad1f2e Binary files /dev/null and b/assets/img/vignettes/dxf.png differ diff --git a/assets/img/vignettes/eps.png b/assets/img/vignettes/eps.png new file mode 100644 index 00000000..8dd016b5 Binary files /dev/null and b/assets/img/vignettes/eps.png differ diff --git a/assets/img/vignettes/flv.png b/assets/img/vignettes/flv.png new file mode 100644 index 00000000..2f565869 Binary files /dev/null and b/assets/img/vignettes/flv.png differ diff --git a/assets/img/vignettes/gif.png b/assets/img/vignettes/gif.png new file mode 100644 index 00000000..41c71675 Binary files /dev/null and b/assets/img/vignettes/gif.png differ diff --git a/assets/img/vignettes/gpx.png b/assets/img/vignettes/gpx.png new file mode 100644 index 00000000..f0659226 Binary files /dev/null and b/assets/img/vignettes/gpx.png differ diff --git a/assets/img/vignettes/gxt.png b/assets/img/vignettes/gxt.png new file mode 100644 index 00000000..cd8a732f Binary files /dev/null and b/assets/img/vignettes/gxt.png differ diff --git a/assets/img/vignettes/gz.png b/assets/img/vignettes/gz.png new file mode 100644 index 00000000..dd1f8a8c Binary files /dev/null and b/assets/img/vignettes/gz.png differ diff --git a/assets/img/vignettes/h.png b/assets/img/vignettes/h.png new file mode 100644 index 00000000..78941cac Binary files /dev/null and b/assets/img/vignettes/h.png differ diff --git a/assets/img/vignettes/html.png b/assets/img/vignettes/html.png new file mode 100644 index 00000000..e79e80a3 Binary files /dev/null and b/assets/img/vignettes/html.png differ diff --git a/assets/img/vignettes/jpg.png b/assets/img/vignettes/jpg.png new file mode 100644 index 00000000..41c71675 Binary files /dev/null and b/assets/img/vignettes/jpg.png differ diff --git a/assets/img/vignettes/kml.png b/assets/img/vignettes/kml.png new file mode 100644 index 00000000..ce6e436f Binary files /dev/null and b/assets/img/vignettes/kml.png differ diff --git a/assets/img/vignettes/kmz.png b/assets/img/vignettes/kmz.png new file mode 100644 index 00000000..ce6e436f Binary files /dev/null and b/assets/img/vignettes/kmz.png differ diff --git a/assets/img/vignettes/mid.png b/assets/img/vignettes/mid.png new file mode 100644 index 00000000..64a83e17 Binary files /dev/null and b/assets/img/vignettes/mid.png differ diff --git a/assets/img/vignettes/mka.png b/assets/img/vignettes/mka.png new file mode 100644 index 00000000..50665d39 Binary files /dev/null and b/assets/img/vignettes/mka.png differ diff --git a/assets/img/vignettes/mkv.png b/assets/img/vignettes/mkv.png new file mode 100644 index 00000000..c4d738b2 Binary files /dev/null and b/assets/img/vignettes/mkv.png differ diff --git a/assets/img/vignettes/mng.png b/assets/img/vignettes/mng.png new file mode 100644 index 00000000..c4d738b2 Binary files /dev/null and b/assets/img/vignettes/mng.png differ diff --git a/assets/img/vignettes/mov.png b/assets/img/vignettes/mov.png new file mode 100644 index 00000000..4a4df8cc Binary files /dev/null and b/assets/img/vignettes/mov.png differ diff --git a/assets/img/vignettes/mp3.png b/assets/img/vignettes/mp3.png new file mode 100644 index 00000000..50665d39 Binary files /dev/null and b/assets/img/vignettes/mp3.png differ diff --git a/assets/img/vignettes/mp4.png b/assets/img/vignettes/mp4.png new file mode 100644 index 00000000..c4d738b2 Binary files /dev/null and b/assets/img/vignettes/mp4.png differ diff --git a/assets/img/vignettes/mpg.png b/assets/img/vignettes/mpg.png new file mode 100644 index 00000000..c4d738b2 Binary files /dev/null and b/assets/img/vignettes/mpg.png differ diff --git a/assets/img/vignettes/odb.png b/assets/img/vignettes/odb.png new file mode 100644 index 00000000..9eee4c31 Binary files /dev/null and b/assets/img/vignettes/odb.png differ diff --git a/assets/img/vignettes/odc.png b/assets/img/vignettes/odc.png new file mode 100644 index 00000000..ec2c926f Binary files /dev/null and b/assets/img/vignettes/odc.png differ diff --git a/assets/img/vignettes/odf.png b/assets/img/vignettes/odf.png new file mode 100644 index 00000000..51cc1e82 Binary files /dev/null and b/assets/img/vignettes/odf.png differ diff --git a/assets/img/vignettes/odg.png b/assets/img/vignettes/odg.png new file mode 100644 index 00000000..823dc206 Binary files /dev/null and b/assets/img/vignettes/odg.png differ diff --git a/assets/img/vignettes/odi.png b/assets/img/vignettes/odi.png new file mode 100644 index 00000000..56923bb4 Binary files /dev/null and b/assets/img/vignettes/odi.png differ diff --git a/assets/img/vignettes/odm.png b/assets/img/vignettes/odm.png new file mode 100644 index 00000000..4a9986fa Binary files /dev/null and b/assets/img/vignettes/odm.png differ diff --git a/assets/img/vignettes/odp.png b/assets/img/vignettes/odp.png new file mode 100644 index 00000000..87ac0163 Binary files /dev/null and b/assets/img/vignettes/odp.png differ diff --git a/assets/img/vignettes/ods.png b/assets/img/vignettes/ods.png new file mode 100644 index 00000000..6e8b4965 Binary files /dev/null and b/assets/img/vignettes/ods.png differ diff --git a/assets/img/vignettes/odt.png b/assets/img/vignettes/odt.png new file mode 100644 index 00000000..150a637b Binary files /dev/null and b/assets/img/vignettes/odt.png differ diff --git a/assets/img/vignettes/ogg.png b/assets/img/vignettes/ogg.png new file mode 100644 index 00000000..7f02f486 Binary files /dev/null and b/assets/img/vignettes/ogg.png differ diff --git a/assets/img/vignettes/otg.png b/assets/img/vignettes/otg.png new file mode 100644 index 00000000..823dc206 Binary files /dev/null and b/assets/img/vignettes/otg.png differ diff --git a/assets/img/vignettes/otp.png b/assets/img/vignettes/otp.png new file mode 100644 index 00000000..87ac0163 Binary files /dev/null and b/assets/img/vignettes/otp.png differ diff --git a/assets/img/vignettes/ots.png b/assets/img/vignettes/ots.png new file mode 100644 index 00000000..6e8b4965 Binary files /dev/null and b/assets/img/vignettes/ots.png differ diff --git a/assets/img/vignettes/ott.png b/assets/img/vignettes/ott.png new file mode 100644 index 00000000..150a637b Binary files /dev/null and b/assets/img/vignettes/ott.png differ diff --git a/assets/img/vignettes/pas.png b/assets/img/vignettes/pas.png new file mode 100644 index 00000000..509011e9 Binary files /dev/null and b/assets/img/vignettes/pas.png differ diff --git a/assets/img/vignettes/pdf.png b/assets/img/vignettes/pdf.png new file mode 100644 index 00000000..b32187c3 Binary files /dev/null and b/assets/img/vignettes/pdf.png differ diff --git a/assets/img/vignettes/pgn.png b/assets/img/vignettes/pgn.png new file mode 100644 index 00000000..24e650fe Binary files /dev/null and b/assets/img/vignettes/pgn.png differ diff --git a/assets/img/vignettes/png.png b/assets/img/vignettes/png.png new file mode 100644 index 00000000..41c71675 Binary files /dev/null and b/assets/img/vignettes/png.png differ diff --git a/assets/img/vignettes/pps.png b/assets/img/vignettes/pps.png new file mode 100644 index 00000000..abe4b046 Binary files /dev/null and b/assets/img/vignettes/pps.png differ diff --git a/assets/img/vignettes/ppt.png b/assets/img/vignettes/ppt.png new file mode 100644 index 00000000..abe4b046 Binary files /dev/null and b/assets/img/vignettes/ppt.png differ diff --git a/assets/img/vignettes/pptx.png b/assets/img/vignettes/pptx.png new file mode 100644 index 00000000..abe4b046 Binary files /dev/null and b/assets/img/vignettes/pptx.png differ diff --git a/assets/img/vignettes/ps.png b/assets/img/vignettes/ps.png new file mode 100644 index 00000000..8dd016b5 Binary files /dev/null and b/assets/img/vignettes/ps.png differ diff --git a/assets/img/vignettes/psd.png b/assets/img/vignettes/psd.png new file mode 100644 index 00000000..cbe6cbef Binary files /dev/null and b/assets/img/vignettes/psd.png differ diff --git a/assets/img/vignettes/qt.png b/assets/img/vignettes/qt.png new file mode 100644 index 00000000..4a4df8cc Binary files /dev/null and b/assets/img/vignettes/qt.png differ diff --git a/assets/img/vignettes/ra.png b/assets/img/vignettes/ra.png new file mode 100644 index 00000000..b9daeab9 Binary files /dev/null and b/assets/img/vignettes/ra.png differ diff --git a/assets/img/vignettes/ram.png b/assets/img/vignettes/ram.png new file mode 100644 index 00000000..b9daeab9 Binary files /dev/null and b/assets/img/vignettes/ram.png differ diff --git a/assets/img/vignettes/rm.png b/assets/img/vignettes/rm.png new file mode 100644 index 00000000..b9daeab9 Binary files /dev/null and b/assets/img/vignettes/rm.png differ diff --git a/assets/img/vignettes/rpm.png b/assets/img/vignettes/rpm.png new file mode 100644 index 00000000..f3d73a53 Binary files /dev/null and b/assets/img/vignettes/rpm.png differ diff --git a/assets/img/vignettes/rtf.png b/assets/img/vignettes/rtf.png new file mode 100644 index 00000000..1d4e3226 Binary files /dev/null and b/assets/img/vignettes/rtf.png differ diff --git a/assets/img/vignettes/sdd.png b/assets/img/vignettes/sdd.png new file mode 100644 index 00000000..87ac0163 Binary files /dev/null and b/assets/img/vignettes/sdd.png differ diff --git a/assets/img/vignettes/sdw.png b/assets/img/vignettes/sdw.png new file mode 100644 index 00000000..150a637b Binary files /dev/null and b/assets/img/vignettes/sdw.png differ diff --git a/assets/img/vignettes/sit.png b/assets/img/vignettes/sit.png new file mode 100644 index 00000000..55a4f13f Binary files /dev/null and b/assets/img/vignettes/sit.png differ diff --git a/assets/img/vignettes/smil.png b/assets/img/vignettes/smil.png new file mode 100644 index 00000000..ef9bd0e6 Binary files /dev/null and b/assets/img/vignettes/smil.png differ diff --git a/assets/img/vignettes/spip.png b/assets/img/vignettes/spip.png new file mode 100644 index 00000000..837ce5af Binary files /dev/null and b/assets/img/vignettes/spip.png differ diff --git a/assets/img/vignettes/svg.png b/assets/img/vignettes/svg.png new file mode 100644 index 00000000..b155f066 Binary files /dev/null and b/assets/img/vignettes/svg.png differ diff --git a/assets/img/vignettes/swf.png b/assets/img/vignettes/swf.png new file mode 100644 index 00000000..009ee747 Binary files /dev/null and b/assets/img/vignettes/swf.png differ diff --git a/assets/img/vignettes/sxc.png b/assets/img/vignettes/sxc.png new file mode 100644 index 00000000..6e8b4965 Binary files /dev/null and b/assets/img/vignettes/sxc.png differ diff --git a/assets/img/vignettes/sxi.png b/assets/img/vignettes/sxi.png new file mode 100644 index 00000000..87ac0163 Binary files /dev/null and b/assets/img/vignettes/sxi.png differ diff --git a/assets/img/vignettes/sxw.png b/assets/img/vignettes/sxw.png new file mode 100644 index 00000000..150a637b Binary files /dev/null and b/assets/img/vignettes/sxw.png differ diff --git a/assets/img/vignettes/tex.png b/assets/img/vignettes/tex.png new file mode 100644 index 00000000..5fe36239 Binary files /dev/null and b/assets/img/vignettes/tex.png differ diff --git a/assets/img/vignettes/tgz.png b/assets/img/vignettes/tgz.png new file mode 100644 index 00000000..6b6a82e9 Binary files /dev/null and b/assets/img/vignettes/tgz.png differ diff --git a/assets/img/vignettes/tif.png b/assets/img/vignettes/tif.png new file mode 100644 index 00000000..41c71675 Binary files /dev/null and b/assets/img/vignettes/tif.png differ diff --git a/assets/img/vignettes/tiff.png b/assets/img/vignettes/tiff.png new file mode 100644 index 00000000..41c71675 Binary files /dev/null and b/assets/img/vignettes/tiff.png differ diff --git a/assets/img/vignettes/torrent.png b/assets/img/vignettes/torrent.png new file mode 100644 index 00000000..0653af84 Binary files /dev/null and b/assets/img/vignettes/torrent.png differ diff --git a/assets/img/vignettes/ttf.png b/assets/img/vignettes/ttf.png new file mode 100644 index 00000000..8390b808 Binary files /dev/null and b/assets/img/vignettes/ttf.png differ diff --git a/assets/img/vignettes/txt.png b/assets/img/vignettes/txt.png new file mode 100644 index 00000000..6e446db0 Binary files /dev/null and b/assets/img/vignettes/txt.png differ diff --git a/assets/img/vignettes/wav.png b/assets/img/vignettes/wav.png new file mode 100644 index 00000000..3595590f Binary files /dev/null and b/assets/img/vignettes/wav.png differ diff --git a/assets/img/vignettes/wmv.png b/assets/img/vignettes/wmv.png new file mode 100644 index 00000000..c4d738b2 Binary files /dev/null and b/assets/img/vignettes/wmv.png differ diff --git a/assets/img/vignettes/xcf.png b/assets/img/vignettes/xcf.png new file mode 100644 index 00000000..08dac6c3 Binary files /dev/null and b/assets/img/vignettes/xcf.png differ diff --git a/assets/img/vignettes/xls.png b/assets/img/vignettes/xls.png new file mode 100644 index 00000000..548eba35 Binary files /dev/null and b/assets/img/vignettes/xls.png differ diff --git a/assets/img/vignettes/xlsx.png b/assets/img/vignettes/xlsx.png new file mode 100644 index 00000000..548eba35 Binary files /dev/null and b/assets/img/vignettes/xlsx.png differ diff --git a/assets/img/vignettes/xml.png b/assets/img/vignettes/xml.png new file mode 100644 index 00000000..e87d623c Binary files /dev/null and b/assets/img/vignettes/xml.png differ diff --git a/assets/img/vignettes/zip.png b/assets/img/vignettes/zip.png new file mode 100644 index 00000000..603b701d Binary files /dev/null and b/assets/img/vignettes/zip.png differ 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 b2bd8210..ff89321f 100644 --- a/assets/modules/espaceco/RQKeys.ts +++ b/assets/modules/espaceco/RQKeys.ts @@ -1,16 +1,31 @@ import { CommunityListFilter } from "../../@types/app_espaceco"; const RQKeys = { - community_list: (page: number, limit: number): string[] => ["community", page.toString(), limit.toString()], - search: (search: string, filter: CommunityListFilter): string[] => { - return ["search", "community", filter, search]; + communityList: (page: number, limit: number): string[] => ["communities", page.toString(), limit.toString()], + communitiesName: (): string[] => ["communities_names"], + community: (communityId: number): string[] => ["community", communityId.toString()], + communityMembershipRequests: (communityId: number): string[] => ["community", "members", "pending", communityId.toString()], + communityMembers: (communityId: number, page: number, limit: number): string[] => [ + "community", + "members", + communityId.toString(), + page.toString(), + limit.toString(), + ], + searchCommunities: (search: string, filter: CommunityListFilter): string[] => { + return ["searchCommunities", filter, search]; }, - communities_as_member: (pending: boolean, page: number, limit: number): string[] => [ + communitiesAsMember: (pending: boolean, page: number, limit: number): string[] => [ "communities_as_member", new Boolean(pending).toString(), page.toString(), limit.toString(), ], + userSharedThemes: (): string[] => ["user", "shared_themes"], + searchAddress: (search: string): string[] => ["searchAddress", search], + searchGrids: (text: string): string[] => ["searchGrids", text], + searchUsers: (text: string): string[] => ["searchUsers", text], + tables: (communityId: number): string[] => ["feature_types", communityId.toString()], }; export default RQKeys; diff --git a/assets/ol/controls/DisplayCenterControl.ts b/assets/ol/controls/DisplayCenterControl.ts new file mode 100644 index 00000000..44407968 --- /dev/null +++ b/assets/ol/controls/DisplayCenterControl.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2024 P.Prevautel + released under the CeCILL-B license (French BSD license) + (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). +*/ +import * as olColor from "ol/color"; +import Control from "ol/control/Control"; + +import "../../sass/pages/espaceco/drawcenter.scss"; + +type DisplayCenterOptions = { + color?: number[] | string; + width?: number; +}; + +const defaultColor = "#000"; + +/** DisplayCenter draw a target at the center of the map. + * @param + * - color {ol.Color or string} line color + * - width {integer} line width + */ +class DisplayCenterControl extends Control { + constructor(options: DisplayCenterOptions) { + const { color = defaultColor, width = 1 } = options; + + let c: string = defaultColor; + try { + if (Array.isArray(color)) { + c = olColor.asString(color); + } else if (typeof color === "string") { + olColor.fromString(color) as olColor.Color; + c = color; + } + } catch (e) { + c = "#000"; + } + + const div = document.createElement("div"); + div.className = "ol-target ol-unselectable ol-control"; + div.style.setProperty("--drawcenter-background", c); + div.style.setProperty("--drawcenter-width", `${width}px`); + + super({ element: div }); + } +} + +export default DisplayCenterControl; diff --git a/assets/router/RouterRenderer.tsx b/assets/router/RouterRenderer.tsx index 0bf578fa..341a84e2 100644 --- a/assets/router/RouterRenderer.tsx +++ b/assets/router/RouterRenderer.tsx @@ -57,6 +57,7 @@ const PyramidVectorTmsServiceForm = lazy(() => import("../entrepot/pages/service const ServiceView = lazy(() => import("../entrepot/pages/service/view/ServiceView")); const EspaceCoCommunityList = lazy(() => import("../espaceco/pages/communities/Communities")); +const EspaceCoManageCommunity = lazy(() => import("../espaceco/pages/communities/ManageCommunity")); const RouterRenderer: FC = () => { const route = useRoute(); @@ -180,6 +181,8 @@ const RouterRenderer: FC = () => { return ; case "espaceco_community_list": return ; + case "espaceco_manage_community": + return ; default: return ; } diff --git a/assets/router/router.ts b/assets/router/router.ts index b659afff..e6425b8a 100644 --- a/assets/router/router.ts +++ b/assets/router/router.ts @@ -226,7 +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}/espace-collaboratif/${p.communityId}/gerer-le-guichet` ), }; diff --git a/assets/sass/components/zoom-range.scss b/assets/sass/components/zoom-range.scss index 13dbdb14..789f53c4 100644 --- a/assets/sass/components/zoom-range.scss +++ b/assets/sass/components/zoom-range.scss @@ -1,17 +1,17 @@ -.zoom-range-map { - height: 300px; -} - -.ui-map-zoom-levels { +.frx-zoom-range { display: flex; - .ui-top-zoom-level, - .ui-bottom-zoom-level { + .frx-top-zoom, + .frx-bottom-zoom { height: 300px; border: 1px solid lightgray; flex: 0 1 50%; margin: 0 1em; } - .ui-bottom-zoom-level { + .frx-bottom-zoom { margin-left: 0; } + + .frx-zoom-range-sm { + height: 150px; + } } diff --git a/assets/sass/pages/espaceco/community.scss b/assets/sass/pages/espaceco/community.scss index d573fc4e..57d63316 100644 --- a/assets/sass/pages/espaceco/community.scss +++ b/assets/sass/pages/espaceco/community.scss @@ -1,3 +1,9 @@ .frx-community-even { background-color: var(--background-alt-grey); } + +.frx-community-desc-documents { + padding: 0.4rem; + border: 1px solid; + border-color: var(--border-default-grey); +} diff --git a/assets/sass/pages/espaceco/drawcenter.scss b/assets/sass/pages/espaceco/drawcenter.scss new file mode 100644 index 00000000..b1c1291b --- /dev/null +++ b/assets/sass/pages/espaceco/drawcenter.scss @@ -0,0 +1,26 @@ +.ol-target { + inset: 0; + pointer-events: none !important; + background-color: transparent !important ; +} + +.ol-target:before, +.ol-target:after { + content: ""; + position: absolute; + background: var(--drawcenter-background); +} + +.ol-target:before { + width: 100%; + left: 0; + top: 50%; + height: var(--drawcenter-width, 1px); +} + +.ol-target:after { + height: 100%; + left: 50%; + top: 0; + width: var(--drawcenter-width, 1px); +} diff --git a/assets/utils.ts b/assets/utils.ts index dcc79148..5be235ac 100644 --- a/assets/utils.ts +++ b/assets/utils.ts @@ -162,6 +162,31 @@ const getFileExtension = (filename: string) => { return filename.split(".").pop()?.toLowerCase(); }; +export type ImageSize = { + width: number; + height: number; +}; +const getImageSize = async (image: File): Promise => { + return new Promise((resolve, reject) => { + try { + const fileReader = new FileReader(); + fileReader.onload = () => { + const img = new Image(); + img.onload = () => { + resolve({ width: img.width, height: img.height }); + }; + if (typeof fileReader.result === "string") { + img.src = fileReader.result; + } + }; + + fileReader.readAsDataURL(image); + } catch (e) { + reject(e); + } + }); +}; + const formatDateFromISO = (isoDateString: string): string => { const m = isoDateString.match(/([\d\-T:]+)\+\d{2}:\d{2}?/); // "2023-06-02T06:01:46+00:00" if (m) { @@ -233,6 +258,7 @@ export { niceBytes, getProjectionCode, getFileExtension, + getImageSize, formatDateFromISO, formatDateWithoutTimeFromISO, getArrayRange, diff --git a/assets/validations/SldStyleValidationErrorsTr.tsx b/assets/validations/SldStyleValidationErrorsTr.tsx index d9c23540..a4700b30 100644 --- a/assets/validations/SldStyleValidationErrorsTr.tsx +++ b/assets/validations/SldStyleValidationErrorsTr.tsx @@ -1,4 +1,4 @@ -import { declareComponentKeys } from "i18nifty/declareComponentKeys"; +import { declareComponentKeys } from "i18nifty"; import { Translations } from "../i18n/i18n"; export const { i18n } = declareComponentKeys< diff --git a/src/Controller/EspaceCo/CommunityController.php b/src/Controller/EspaceCo/CommunityController.php index 10525978..da3a426b 100644 --- a/src/Controller/EspaceCo/CommunityController.php +++ b/src/Controller/EspaceCo/CommunityController.php @@ -2,15 +2,16 @@ namespace App\Controller\EspaceCo; +use App\Controller\ApiControllerInterface; use App\Exception\ApiException; use App\Exception\CartesApiException; -use App\Controller\ApiControllerInterface; -use Symfony\Component\Routing\Attribute\Route; use App\Services\EspaceCoApi\CommunityApiService; use App\Services\EspaceCoApi\UserApiService; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Routing\Attribute\Route; #[Route( '/api/espaceco/community', @@ -20,7 +21,7 @@ )] class CommunityController extends AbstractController implements ApiControllerInterface { - const SEARCH_LIMIT = 20; + public const SEARCH_LIMIT = 20; public function __construct( private CommunityApiService $communityApiService, @@ -34,44 +35,55 @@ public function get( #[MapQueryParameter] ?int $page = 1, #[MapQueryParameter] ?int $limit = 10, #[MapQueryParameter] ?string $sort = 'name:DESC', - ): JsonResponse - { + ): JsonResponse { try { $response = $this->communityApiService->getCommunities($name, $page, $limit, $sort); + return new JsonResponse($response); } catch (ApiException $ex) { throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); } } + #[Route('/get_names', name: 'get_names', methods: ['GET'])] + public function getCommunitiesName(): JsonResponse + { + try { + $names = $this->communityApiService->getCommunitiesName(); + + return new JsonResponse($names); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + #[Route('/get_as_member', name: 'get_as_member', methods: ['GET'])] public function getMeMember( #[MapQueryParameter] bool $pending, #[MapQueryParameter] ?int $page = 1, #[MapQueryParameter] ?int $limit = 10 - ): JsonResponse - { + ): JsonResponse { try { $me = $this->userApiService->getMe(); $members = array_map(function ($member) { - return ['id'=> $member['community_id'], 'name' => $member['community_name'], 'role' => $member['role']]; - },$me['communities_member']); + return ['id' => $member['community_id'], 'name' => $member['community_name'], 'role' => $member['role']]; + }, $me['communities_member']); - $members = array_filter($members, function($member) use ($pending) { - return $pending ? $member['role'] === 'pending' : $member['role'] !== 'pending'; + $members = array_filter($members, function ($member) use ($pending) { + return $pending ? 'pending' === $member['role'] : 'pending' !== $member['role']; }); $this->_sortMembersByName($members); $members = array_slice(array_values($members), ($page - 1) * $limit, $limit); - + $communities = []; - foreach($members as $member) { + foreach ($members as $member) { $community = $this->communityApiService->getCommunity($member['id']); $communities[] = $community; } $totalPages = floor(count($members) / $limit) + 1; - $previousPage = $page === 1 ? null : $page - 1; + $previousPage = 1 === $page ? null : $page - 1; $nextPage = $page + 1 > $totalPages ? null : $page + 1; return new JsonResponse([ @@ -90,69 +102,147 @@ public function search( #[MapQueryParameter] string $name, #[MapQueryParameter] string $filter, #[MapQueryParameter] string $sort, - ): JsonResponse - { + ): JsonResponse { try { - if (! in_array($filter, ['public','iam_member','affiliation'])) { - throw new ApiException("Le filtre doit être public, iam_member ou affiliation"); + if (!in_array($filter, ['public', 'iam_member', 'affiliation'])) { + throw new ApiException('Le filtre doit être public, iam_member ou affiliation'); } - if ($filter == 'public') { + if ('public' == $filter) { $result = $this->communityApiService->getCommunities($name, 1, self::SEARCH_LIMIT, $sort); $response = $result['content']; } else { - $response = $this->_search($name, $filter !== 'iam_member' ); + $response = $this->_search($name, 'iam_member' !== $filter); } + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/{communityId}', name: 'get_community', methods: ['GET'])] + public function getCommunity(int $communityId): JsonResponse + { + try { + $response = $this->communityApiService->getCommunity($communityId); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + /** + * @param array $roles + */ + #[Route('/{communityId}/members', name: 'get_members', methods: ['GET'])] + public function getMembers( + int $communityId, + #[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^admin|member|pending$/'])] array $roles = [], + #[MapQueryParameter] ?int $page = 1, + #[MapQueryParameter(options: ['min_range' => 1, 'max_range' => 50])] ?int $limit = 10 + ): JsonResponse { + try { + $response = $this->communityApiService->getCommunityMembers($communityId, $roles, $page, $limit); + return new JsonResponse($response); } catch (ApiException $ex) { throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); } } + #[Route('/{communityId}/member/{userId}/update_role', name: 'update_member_role', methods: ['PATCH'])] + public function updateMemberRole(int $communityId, int $userId, Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + $member = $this->communityApiService->updateMember($communityId, $userId, 'role', $data['role']); + + return new JsonResponse($member); + } + + #[Route('/{communityId}/member/{userId}/update_grids', name: 'update_member_grids', methods: ['PATCH'])] + public function updateMemberGrids(int $communityId, int $userId, Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + $member = $this->communityApiService->updateMember($communityId, $userId, 'grids', $data['grids']); + + return new JsonResponse($member); + } + + #[Route('/{communityId}/update_logo', name: 'update_logo', methods: ['PATCH'])] + public function updateLogo(int $communityId, Request $request): JsonResponse + { + try { + $community = $this->communityApiService->getCommunity($communityId); + + $logo = $request->files->get('logo'); + $this->communityApiService->updateLogo($communityId, $logo); + + return new JsonResponse($community); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/{communityId}/member/{userId}/remove', name: 'remove_member', methods: ['DELETE'])] + public function removeMember(int $communityId, int $userId): JsonResponse + { + try { + $this->communityApiService->removeMember($communityId, $userId); + + return new JsonResponse(['user_id' => $userId]); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + /** - * @param string $name - * @param boolean $pending * @return array */ - private function _search(string $name, bool $pending) : array { + private function _search(string $name, bool $pending): array + { $me = $this->userApiService->getMe(); $members = array_map(function ($member) { - return ['id'=> $member['community_id'], 'name' => $member['community_name'], 'role' => $member['role']]; - },$me['communities_member']); + return ['id' => $member['community_id'], 'name' => $member['community_name'], 'role' => $member['role']]; + }, $me['communities_member']); $regex = mb_strtoupper(str_replace('%', '', $name)); - $members = array_filter($members, function($member) use ($regex, $pending) { + $members = array_filter($members, function ($member) use ($regex, $pending) { $communityName = mb_strtoupper($member['name']); $match = preg_match("/$regex/", $communityName); - return $pending ? ($member['role'] === 'pending' && $match) : ($member['role'] !== 'pending' && $match); + + return $pending ? ('pending' === $member['role'] && $match) : ('pending' !== $member['role'] && $match); }); $this->_sortMembersByName($members); $members = array_slice(array_values($members), 0, self::SEARCH_LIMIT); - + $communities = []; - foreach($members as $member) { + foreach ($members as $member) { $community = $this->communityApiService->getCommunity($member['id']); $communities[] = $community; } - return $communities; + return $communities; } - + /** * @param array $members - * @return void */ - private function _sortMembersByName(array &$members) : void + private function _sortMembersByName(array &$members): void { - usort($members, function($m1, $m2) { + usort($members, function ($m1, $m2) { $upM1 = mb_strtoupper($m1['name']); $upM2 = mb_strtoupper($m2['name']); if ($upM1 == $upM2) { return 0; } - return ($upM1 < $upM2) ? -1 : 1; + + return ($upM1 < $upM2) ? -1 : 1; }); } } diff --git a/src/Controller/EspaceCo/GridController.php b/src/Controller/EspaceCo/GridController.php new file mode 100644 index 00000000..670a21fb --- /dev/null +++ b/src/Controller/EspaceCo/GridController.php @@ -0,0 +1,66 @@ + true], + condition: 'request.isXmlHttpRequest()' +)] +class GridController extends AbstractController implements ApiControllerInterface +{ + public const SEARCH_LIMIT = 20; + + public function __construct( + private GridApiService $gridApiService + ) { + } + + /** + * @param array $names + */ + #[Route('/get_by_names', name: 'get_by_names', methods: ['GET'])] + public function getFromArray( + #[MapQueryParameter] array $names + ): JsonResponse { + try { + if (!is_array($names) || 0 == count($names)) { + throw new ApiException('names is not an array or is empty'); + } + + $response = $this->gridApiService->getGridsFromNames($names); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } + + #[Route('/search', name: 'search', methods: ['GET'])] + public function get( + #[MapQueryParameter] string $text, + #[MapQueryParameter] ?string $searchBy, + #[MapQueryParameter] ?string $fields, + #[MapQueryParameter] ?string $adm, + #[MapQueryParameter] ?int $page = 1, + #[MapQueryParameter] ?int $limit = self::SEARCH_LIMIT, + ): JsonResponse { + try { + $response = $this->gridApiService->getGrids($text, $searchBy, $fields, $adm, $page, $limit); + + return new JsonResponse($response); + } catch (ApiException $ex) { + throw new CartesApiException($ex->getMessage(), $ex->getStatusCode(), $ex->getDetails(), $ex); + } + } +} diff --git a/src/Controller/EspaceCo/PermissionController.php b/src/Controller/EspaceCo/PermissionController.php new file mode 100644 index 00000000..e6eb6fe3 --- /dev/null +++ b/src/Controller/EspaceCo/PermissionController.php @@ -0,0 +1,91 @@ + 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; + }); + + $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..502028c4 100644 --- a/src/Controller/EspaceCo/UserController.php +++ b/src/Controller/EspaceCo/UserController.php @@ -4,9 +4,10 @@ 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\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\Routing\Annotation\Route; #[Route( '/api/espaceco/user', @@ -25,6 +26,24 @@ public function __construct( public function getCurrentUser(): JsonResponse { $me = $this->userApiService->getMe(); + + return $this->json($me); + } + + #[Route('/search', name: 'search')] + public function search( + #[MapQueryParameter] string $search + ): JsonResponse { + $users = $this->userApiService->search($search); + + return new JsonResponse($users); + } + + #[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/BaseEspaceCoApiService.php b/src/Services/EspaceCoApi/BaseEspaceCoApiService.php index 99974ade..a2a9e2ed 100644 --- a/src/Services/EspaceCoApi/BaseEspaceCoApiService.php +++ b/src/Services/EspaceCoApi/BaseEspaceCoApiService.php @@ -2,16 +2,15 @@ namespace App\Services\EspaceCoApi; -use Psr\Log\LoggerInterface; use App\Exception\ApiException; use App\Services\AbstractApiService; +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; - +use Symfony\Contracts\HttpClient\ResponseInterface; class BaseEspaceCoApiService extends AbstractApiService { @@ -43,9 +42,8 @@ protected function handleResponse(ResponseInterface $response, bool $expectJson) return $content; } else { - $errorMsg = 'EspaceCo API Error'; $errorResponse = $response->toArray(false); - throw new ApiException($errorMsg, $statusCode, $errorResponse['message']); + throw new ApiException($errorResponse['message'], $statusCode); } } } diff --git a/src/Services/EspaceCoApi/CommunityApiService.php b/src/Services/EspaceCoApi/CommunityApiService.php index ff622e81..6677e3a6 100644 --- a/src/Services/EspaceCoApi/CommunityApiService.php +++ b/src/Services/EspaceCoApi/CommunityApiService.php @@ -2,16 +2,34 @@ namespace App\Services\EspaceCoApi; +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Contracts\HttpClient\HttpClientInterface; + class CommunityApiService extends BaseEspaceCoApiService { - public function getCommunities(string $name, int $page,int $limit, string $sort): array + public function __construct(HttpClientInterface $httpClient, + ParameterBagInterface $parameters, + Filesystem $filesystem, + RequestStack $requestStack, + LoggerInterface $logger, + private UserApiService $userApiService, + private GridApiService $gridApiService + ) { + parent::__construct($httpClient, $parameters, $filesystem, $requestStack, $logger); + } + + public function getCommunities(string $name, int $page, int $limit, string $sort): array { - $response = $this->request('GET', "communities", [], ['name' => $name, 'page' => $page, 'limit' => $limit, 'sort' => $sort], [], false, true, true); - + $response = $this->request('GET', 'communities', [], ['name' => $name, 'page' => $page, 'limit' => $limit, 'sort' => $sort], [], false, true, true); + $contentRange = $response['headers']['content-range'][0]; $totalPages = $this->getResultsPageCount($contentRange, $limit); - $previousPage = $page === 1 ? null : $page - 1; + $previousPage = 1 === $page ? null : $page - 1; $nextPage = $page + 1 > $totalPages ? null : $page + 1; return [ @@ -22,11 +40,82 @@ public function getCommunities(string $name, int $page,int $limit, string $sort) ]; } + public function getCommunitiesName(): array + { + $communities = $this->requestAll('communities', ['fields' => 'name', 'sort' => 'name:ASC']); + + return array_map(fn ($community) => $community['name'], $communities); + } + /** - * @param string $communityId * @return array */ - public function getCommunity(string $communityId) : array { + public function getCommunity(int $communityId): array + { return $this->request('GET', "communities/$communityId"); } -} \ No newline at end of file + + /** + * @param array $roles + * + * @return array + */ + public function getCommunityMembers(int $communityId, array $roles, int $page, int $limit): array + { + $query = ['fields' => 'user_id, grids, role, active, date', 'page' => $page, 'limit' => $limit]; + $query['roles'] = count($roles) ? $roles : ['member', 'admin']; + + $response = $this->request('GET', "communities/$communityId/members", [], $query, [], false, true, true); + + $contentRange = $response['headers']['content-range'][0]; + $totalPages = $this->getResultsPageCount($contentRange, $limit); + $previousPage = 1 === $page ? null : $page - 1; + $nextPage = $page + 1 > $totalPages ? null : $page + 1; + + $members = $response['content']; + foreach ($members as &$member) { + $user = $this->userApiService->getUser($member['user_id'], ['fields' => ['username', 'firstname', 'surname']]); + $member = array_merge($member, $user); + + // Ajout des grids + $grids = []; + foreach ($member['grids'] as $name) { + $grid = $this->gridApiService->getGrid($name); + if (!$grid['deleted']) { + $grids[] = $grid; + } + } + $member['grids'] = $grids; + } + + usort($members, function ($mb1, $mb2) { + if ($mb1['username'] == $mb2['username']) { + return 0; + } + + return (mb_strtolower($mb1['username'], 'UTF-8') < mb_strtolower($mb2['username'], 'UTF-8')) ? -1 : 1; + }); + + return [ + 'content' => $members, + 'totalPages' => $totalPages, + 'previousPage' => $previousPage, + 'nextPage' => $nextPage, + ]; + } + + public function updateMember(int $communityId, int $userId, string $field, mixed $value): array + { + return $this->request('PATCH', "communities/$communityId/members/$userId", [$field => $value]); + } + + public function removeMember(int $communityId, int $userId): array + { + return $this->request('DELETE', "communities/$communityId/members/$userId"); + } + + public function updateLogo(int $communityId, UploadedFile $file): array + { + return $this->request('PATCH', "communities/$communityId", ['logo' => $file], [], [], true); + } +} 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/GridApiService.php b/src/Services/EspaceCoApi/GridApiService.php new file mode 100644 index 00000000..c84c6c94 --- /dev/null +++ b/src/Services/EspaceCoApi/GridApiService.php @@ -0,0 +1,56 @@ + $text, 'page' => $page, 'limit' => $limit]; + if (!is_null($searchBy)) { + $query['searchBy'] = $searchBy; + } + if (!is_null($fields)) { + $query['fields'] = $fields; + } + if (!is_null($adm)) { + $query['adm'] = $adm; + } + + $response = $this->request('GET', 'grids', [], $query, [], false, true, true); + + $contentRange = $response['headers']['content-range'][0]; + $totalPages = $this->getResultsPageCount($contentRange, $limit); + + $previousPage = 1 === $page ? null : $page - 1; + $nextPage = $page + 1 > $totalPages ? null : $page + 1; + + return [ + 'content' => $response['content'], + 'totalPages' => $totalPages, + 'previousPage' => $previousPage, + 'nextPage' => $nextPage, + ]; + } + + public function getGrid(string $gridName): array + { + return $this->request('GET', "grids/$gridName", [], ['fields' => ['name', 'title', 'deleted', 'type']]); + } + + /** + * @param array $names + */ + public function getGridsFromNames(array $names): array + { + $grids = []; + foreach ($names as $gridName) { + $grid = $this->getGrid($gridName); + if (!$grid['deleted']) { + $grids[] = $grid; + } + } + + return $grids; + } +} 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]); + } +} diff --git a/src/Services/EspaceCoApi/UserApiService.php b/src/Services/EspaceCoApi/UserApiService.php index 7d8c6009..2083ba1f 100644 --- a/src/Services/EspaceCoApi/UserApiService.php +++ b/src/Services/EspaceCoApi/UserApiService.php @@ -8,4 +8,27 @@ 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 []; + } + + /** + * @param array $query + */ + public function getUser(int $userId, array $query = []): array + { + return $this->request('GET', "users/$userId", [], $query); + } + + public function search(string $search): array + { + return $this->request('GET', 'users', [], ['search' => $search, 'fields' => ['id', 'username', 'firstname', 'surname']]); + } +}