@geo2france/api-dashboard 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,7 +47,7 @@ export const DSL_Dataset = ({ children, id, provider: provider_input, type: prov
47
47
  : [props.joinKey, props.joinKey];
48
48
  const leftTable = data.length >= 1 ? data : [{ [leftKey]: null }];
49
49
  const rightTable = otherData.length >= 1 ? otherData : [{ [rightKey]: null }];
50
- return from(leftTable).join(from(rightTable), props.joinKey, undefined, aq_join_option).objects();
50
+ return from(leftTable).join(from(rightTable), props.joinKey, undefined, { suffix: ['', '_2'], ...aq_join_option }).objects();
51
51
  };
52
52
  return funct;
53
53
  }
@@ -7,12 +7,34 @@ interface AppContextProps {
7
7
  }
8
8
  export declare const AppContext: import("react").Context<AppContextProps>;
9
9
  export interface DashboardConfig {
10
+ /**
11
+ * Titre principal du tableau de bord (affiché dans le header ou le titre de page).
12
+ */
10
13
  title?: string;
14
+ /**
15
+ * Sous-titre du tableau de bord (optionnel, peut être affiché sous le titre principal).
16
+ */
11
17
  subtitle?: string;
18
+ /**
19
+ * Liste des routes de l'application (chaque route correspond à une page du tableau de bord).
20
+ */
12
21
  routes: RouteConfig[];
22
+ /**
23
+ * Configuration du thème Ant Design (permet de personnaliser les couleurs, la typographie, etc.).
24
+ */
13
25
  theme?: ThemeConfig;
26
+ /**
27
+ * URL ou chemin du logo à afficher dans le tableau de bord.
28
+ */
14
29
  logo: string;
30
+ /**
31
+ * Liste optionnelle de partenaires ou marques à afficher dans le footer ou ailleurs.
32
+ */
15
33
  brands?: Partner[];
34
+ /**
35
+ * Active ou désactive le mode “slider” dans le pied de page (faire défiler les logos de partenaires).
36
+ */
37
+ footerSlider?: boolean;
16
38
  }
17
39
  declare const DashboardApp: React.FC<DashboardConfig>;
18
40
  export default DashboardApp;
@@ -31,7 +31,7 @@ const default_theme = {
31
31
  }
32
32
  };
33
33
  export const AppContext = createContext({});
34
- const DashboardApp = ({ routes, theme, logo, brands, title, subtitle }) => {
34
+ const DashboardApp = ({ routes, theme, logo, brands, footerSlider, title, subtitle }) => {
35
35
  const context_values = { title, subtitle, logo };
36
36
  /* CONTROLS */
37
37
  const [controls, setControles] = useState({});
@@ -41,6 +41,6 @@ const DashboardApp = ({ routes, theme, logo, brands, title, subtitle }) => {
41
41
  ...c
42
42
  }));
43
43
  };
44
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ConfigProvider, { theme: theme || default_theme /* Merger plutôt ?*/, children: _jsx(HelmetProvider, { children: _jsx(AppContext.Provider, { value: context_values, children: _jsx(ControlContext.Provider, { value: { values: controls, pushValue: pushControl }, children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { hasSider: true, style: { minHeight: '100vh' }, children: [_jsx(DashboardSider, { route_config: routes }), _jsxs(Layout, { children: [_jsx(Content, { style: { width: "100%" }, children: _jsx(Outlet, {}) }), _jsx(DasbhoardFooter, { brands: brands })] })] }), children: [generateRoutes(routes), _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }));
44
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ConfigProvider, { theme: theme || default_theme /* Merger plutôt ?*/, children: _jsx(HelmetProvider, { children: _jsx(AppContext.Provider, { value: context_values, children: _jsx(ControlContext.Provider, { value: { values: controls, pushValue: pushControl }, children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { hasSider: true, style: { minHeight: '100vh' }, children: [_jsx(DashboardSider, { route_config: routes }), _jsxs(Layout, { children: [_jsx(Content, { style: { width: "100%" }, children: _jsx(Outlet, {}) }), _jsx(DasbhoardFooter, { brands: brands, slider: footerSlider })] })] }), children: [generateRoutes(routes), _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }));
45
45
  };
46
46
  export default DashboardApp;
@@ -1,6 +1,9 @@
1
1
  import { Partner } from "../../types";
2
+ import "slick-carousel/slick/slick.css";
3
+ import "slick-carousel/slick/slick-theme.css";
2
4
  interface DbFooterProps {
3
5
  brands?: Partner[];
6
+ slider?: boolean;
4
7
  }
5
8
  export declare const DasbhoardFooter: React.FC<DbFooterProps>;
6
9
  export {};
@@ -1,19 +1,33 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Layout, Typography } from "antd";
3
3
  import { useContext, useState } from "react";
4
+ import Slider from "@ant-design/react-slick";
4
5
  import { UpOutlined, DownOutlined } from "@ant-design/icons";
5
6
  import { AppContext } from "./DashboardApp";
7
+ import "slick-carousel/slick/slick.css";
8
+ import "slick-carousel/slick/slick-theme.css";
6
9
  const { Text } = Typography;
7
- export const DasbhoardFooter = ({ brands }) => {
10
+ export const DasbhoardFooter = ({ brands, slider = true }) => {
8
11
  const [isCollapsed, setIsCollapsed] = useState(window.innerWidth < 768 ? true : false);
9
12
  const toggleCollapse = () => {
10
13
  setIsCollapsed(!isCollapsed);
11
14
  };
12
15
  const app_context = useContext(AppContext);
13
- const style_img = {
16
+ // Style avec slider (l'image occupe tout le bloc défilé)
17
+ const style_img = slider ? {
14
18
  maxHeight: "60px",
15
- marginRight: "20px",
16
- };
19
+ maxWidth: "100%",
20
+ margin: "auto"
21
+ }
22
+ // Style sans slider
23
+ : {
24
+ maxHeight: "60px",
25
+ marginRight: "20px",
26
+ };
27
+ const nbBrands = brands?.length || 0;
28
+ // Contenu du footer = logos des partenaires
29
+ // TODO : doit pouvoir être surchargé par l'utilisateur
30
+ const footerContent = brands?.map((p) => (_jsx("a", { href: p.url, children: _jsx("img", { style: style_img, src: p.logo, alt: p.name }) }, p.name)));
17
31
  return (_jsxs(Layout.Footer, { style: {
18
32
  textAlign: "center",
19
33
  color: "#fff",
@@ -23,12 +37,40 @@ export const DasbhoardFooter = ({ brands }) => {
23
37
  right: "0",
24
38
  width: "100%",
25
39
  padding: 2,
26
- height: isCollapsed ? "40px" : "80px",
40
+ height: "auto",
41
+ minHeight: "40px",
27
42
  transition: "height 0.5s ease-in-out",
28
43
  overflow: "hidden",
29
44
  borderTop: "1px solid #ccc",
30
45
  zIndex: 600, // maplibre top zIndex if 500
31
- }, children: [isCollapsed && (_jsxs(Text, { type: "secondary", children: [app_context?.title, " - ", app_context?.subtitle] })), _jsx("div", { style: { display: isCollapsed ? "none" : "block", padding: "10px 0" }, children: brands?.map((p) => (_jsx("a", { href: p.url, children: _jsx("img", { style: style_img, src: p.logo, alt: p.name }) }, p.name))) }), _jsx(Button, { style: {
46
+ }, children: [isCollapsed && (_jsxs(Text, { type: "secondary", children: [app_context?.title, " - ", app_context?.subtitle] })), _jsx("div", { style: { display: isCollapsed ? "none" : "block", padding: "10px 0" }, children: slider
47
+ // Logos avec défilement (choix par défaut)
48
+ ? _jsx(Slider
49
+ // Défilement auto si plus de logos que la lagreur de l'écran ne peut en afficher
50
+ , {
51
+ // Défilement auto si plus de logos que la lagreur de l'écran ne peut en afficher
52
+ autoplay: nbBrands > 4, slidesToShow: Math.min(nbBrands, 4), responsive: [
53
+ {
54
+ breakpoint: 1024,
55
+ settings: {
56
+ autoplay: nbBrands > 3,
57
+ slidesToShow: Math.min(nbBrands, 3)
58
+ }
59
+ },
60
+ {
61
+ breakpoint: 600,
62
+ settings: {
63
+ autoplay: nbBrands > 2,
64
+ slidesToShow: Math.min(nbBrands, 2)
65
+ }
66
+ },
67
+ {
68
+ breakpoint: 480,
69
+ settings: { slidesToShow: 1 }
70
+ }
71
+ ], slidesToScroll: 1, infinite: true, arrows: false, autoplaySpeed: 3000, speed: 1000, children: footerContent })
72
+ // Défilement désactivé
73
+ : footerContent }), _jsx(Button, { style: {
32
74
  position: "absolute",
33
75
  bottom: "5px",
34
76
  right: "10px",
@@ -2,6 +2,7 @@ import type { AnyLayer } from 'react-map-gl/maplibre';
2
2
  import { AnyPaint } from 'mapbox-gl';
3
3
  import React from 'react';
4
4
  import 'maplibre-gl/dist/maplibre-gl.css';
5
+ import { SimpleRecord } from '../../types';
5
6
  type LayerType = AnyLayer["type"];
6
7
  export declare const map_locale: {
7
8
  'CooperativeGesturesHandler.WindowsHelpText': string;
@@ -15,6 +16,8 @@ export declare const map_locale: {
15
16
  interface MapProps extends MapLayerProps {
16
17
  /** Afficher une popup après un click sur la carte */
17
18
  popup?: boolean;
19
+ /** Fonction callback permettant de définir le contenu de la popup */
20
+ popupFormatter?: ((param: SimpleRecord) => React.ReactNode);
18
21
  /** Titre du graphique */
19
22
  title?: string;
20
23
  }
@@ -34,6 +37,8 @@ interface MapLayerProps {
34
37
  xKey?: string;
35
38
  /** Colonne contenant la coordonnées y / latitude */
36
39
  yKey?: string;
40
+ /** Colonne contenant la geométrie au format GeoJSON(4326). Par défaut détection automatique ("geom" ou "geometry") */
41
+ geomKey?: string;
37
42
  }
38
43
  /**
39
44
  * Composant à utiliser comme enfant d'une <Map>
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from "react";
5
5
  import { useDataset } from '../Dataset/hooks';
6
6
  import bbox from '@turf/bbox';
7
7
  import { getType } from '@turf/invariant';
8
- import { featureCollection, point } from '@turf/helpers';
8
+ import { feature, featureCollection, point } from '@turf/helpers';
9
9
  import { usePalette } from '../Palette/Palette';
10
10
  import { from, op } from 'arquero';
11
11
  import 'maplibre-gl/dist/maplibre-gl.css';
@@ -19,20 +19,34 @@ export const map_locale = {
19
19
  };
20
20
  /** Construire un geojson a partir d'un tableau de données*/
21
21
  const build_geojson = (params) => {
22
- const { data, xKey, yKey } = params;
23
- const features_collection = featureCollection(data.map((e) => {
24
- const [x, y] = [parseNumber(e[xKey]), parseNumber(e[yKey])];
25
- return point([x, y], { ...e });
26
- }));
22
+ const { data, xKey, yKey, geomKey } = params;
23
+ let features_collection;
24
+ if (xKey && yKey) { // Construction à partir des champs x et Y
25
+ features_collection = featureCollection(data.map((e) => {
26
+ const [x, y] = [parseNumber(e[xKey]), parseNumber(e[yKey])];
27
+ return point([x, y], { ...e });
28
+ }));
29
+ }
30
+ else if (geomKey) { // Construction à partir de la GeomGeoJSON
31
+ features_collection = featureCollection(data.map((e) => {
32
+ return feature(e[geomKey], { ...e });
33
+ }));
34
+ }
35
+ else {
36
+ features_collection = undefined;
37
+ }
38
+ console.log(geomKey, features_collection);
27
39
  return features_collection;
28
40
  };
29
- export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, title, xKey, yKey }) => {
41
+ export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, popupFormatter: popupFormatterUser, title, xKey, yKey }) => {
30
42
  const mapRef = useRef(null);
31
43
  const [clickedFeature, setClickedFeature] = useState(undefined);
32
44
  useBlockConfig({ title: title });
33
45
  const onClickMap = (evt) => {
34
46
  setClickedFeature({ ...evt.features[0], ...{ lngLat: evt.lngLat } });
35
47
  };
48
+ const current_row = clickedFeature?.properties;
49
+ const popupFormatter = popupFormatterUser || ((row) => categoryKey ? row?.[categoryKey] : undefined);
36
50
  const onMouseMoveMap = (evt) => {
37
51
  if (!mapRef.current) {
38
52
  return;
@@ -45,7 +59,7 @@ export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, t
45
59
  }
46
60
  };
47
61
  return (_jsxs(Maplibre, { cooperativeGestures: true, locale: map_locale, ref: mapRef, interactiveLayerIds: [dataset], onClick: onClickMap, onMouseMove: onMouseMoveMap, style: { width: '100%', height: '500px' }, children: [_jsx(BaseLayer, { layer: "osm" }), _jsx(MapLayer, { dataset: dataset, color: color, type: type, paint: paint, categoryKey: categoryKey, xKey: xKey, yKey: yKey }), clickedFeature?.properties && categoryKey && popup &&
48
- _jsxs(Popup, { longitude: clickedFeature.lngLat.lng, latitude: clickedFeature.lngLat.lat, onClose: () => { setClickedFeature(null); }, children: [_jsxs("div", { children: [" ", clickedFeature.properties[categoryKey], " "] }), " "] })] }));
62
+ _jsx(Popup, { longitude: clickedFeature.lngLat.lng, latitude: clickedFeature.lngLat.lat, onClose: () => { setClickedFeature(null); }, children: _jsx("div", { children: popupFormatter(current_row) || clickedFeature?.properties[categoryKey] }) })] }));
49
63
  };
50
64
  /**
51
65
  * Composant à utiliser comme enfant d'une <Map>
@@ -54,14 +68,17 @@ export const Map = ({ dataset, color, type, paint, categoryKey, popup = false, t
54
68
  * @param { MapLayerProps } props
55
69
  * @returns { ReactElement }
56
70
  */
57
- export const MapLayer = ({ dataset, categoryKey, color = 'red', type = 'circle', paint, xKey, yKey }) => {
71
+ export const MapLayer = ({ dataset, categoryKey, color = 'red', type = 'circle', paint, xKey, yKey, geomKey: geomKey_input }) => {
58
72
  const { current: map } = useMap();
59
73
  const data = useDataset(dataset);
60
74
  // src (lib proj4 pour convertir)
75
+ const keys = data?.data?.[0] ? Object.keys(data?.data?.[0]) : undefined;
76
+ const geomKey = [geomKey_input, "geom", "geometry"].find(c => c && keys?.includes(c));
77
+ console.log(data?.id, data?.data, geomKey);
61
78
  // Si x et y sont definie, on construit le geojson
62
79
  const geojson = xKey && yKey && data?.data ?
63
80
  build_geojson({ data: data.data, xKey: xKey, yKey: yKey })
64
- : data?.geojson; // Sinon on utilise le geojson (fournisseur wfs)
81
+ : data?.data && build_geojson({ data: data?.data, geomKey: geomKey });
65
82
  const geom_type = geojson?.features?.[0] && getType(geojson?.features?.[0]);
66
83
  /** Type de données dans categoryKey (string ou number) */
67
84
  const type_value = categoryKey && typeof (data?.data?.[0]?.[categoryKey]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geo2france/api-dashboard",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "private": false,
5
5
  "description": "Build dashboards with JSX/TSX",
6
6
  "main": "dist/index.js",
@@ -45,6 +45,7 @@
45
45
  "test": "jest --watchAll"
46
46
  },
47
47
  "dependencies": {
48
+ "@ant-design/react-slick": "^1.1.2",
48
49
  "@iconify/react": "^6.0.1",
49
50
  "@turf/bbox": "^7.2.0",
50
51
  "@turf/helpers": "^7.2.0",
@@ -59,6 +60,7 @@
59
60
  "react-error-boundary": "^6.0.0",
60
61
  "react-helmet": "^6.1.0",
61
62
  "react-helmet-async": "^2.0.5",
63
+ "slick-carousel": "^1.8.1",
62
64
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
63
65
  },
64
66
  "devDependencies": {