@geo2france/api-dashboard 1.21.0 → 1.22.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.
@@ -1,12 +1,24 @@
1
1
  import { ThemeConfig } from "antd";
2
2
  import { Partner, RouteConfig } from "../../types";
3
+ import { ReactElement, ReactNode } from "react";
4
+ export declare const default_theme: ThemeConfig;
3
5
  interface AppContextProps {
4
6
  title?: string;
5
7
  subtitle?: string;
6
8
  logo?: string;
7
9
  }
10
+ export interface PageProps {
11
+ title?: string;
12
+ /** Icône de la page.
13
+ * Composant ou nom (iconify) de l'icone */
14
+ icon?: ReactElement | string;
15
+ hidden?: boolean;
16
+ children?: ReactNode;
17
+ }
8
18
  export declare const AppContext: import("react").Context<AppContextProps>;
9
19
  export interface DashboardConfig {
20
+ /** Pages de dashboard. La première page sera également l'index (homepage) */
21
+ children?: ReactElement<PageProps> | ReactElement<PageProps>[];
10
22
  /**
11
23
  * Titre principal du tableau de bord (affiché dans le header ou le titre de page).
12
24
  */
@@ -17,16 +29,18 @@ export interface DashboardConfig {
17
29
  subtitle?: string;
18
30
  /**
19
31
  * Liste des routes de l'application (chaque route correspond à une page du tableau de bord).
32
+ * @deprecated since 1.22. Use DashboardApp childrens.
20
33
  */
21
- routes: RouteConfig[];
34
+ routes?: RouteConfig[];
22
35
  /**
23
36
  * Configuration du thème Ant Design (permet de personnaliser les couleurs, la typographie, etc.).
37
+ * Voir : https://ant.design/docs/react/customize-theme#theme
24
38
  */
25
39
  theme?: ThemeConfig;
26
40
  /**
27
41
  * URL ou chemin du logo à afficher dans le tableau de bord.
28
42
  */
29
- logo: string;
43
+ logo?: string;
30
44
  /**
31
45
  * Liste optionnelle de partenaires ou marques à afficher dans le footer ou ailleurs.
32
46
  */
@@ -40,5 +54,12 @@ export interface DashboardConfig {
40
54
  */
41
55
  disablePoweredBy?: boolean;
42
56
  }
57
+ /** Composant principal de l'application.
58
+ *
59
+ * Les enfants de l'application sont les différentes pages de tableau de bord.
60
+ * La configuration globale de l'application (nom, style, etc.) se fait via les propriétés.
61
+ */
43
62
  declare const DashboardApp: React.FC<DashboardConfig>;
44
63
  export default DashboardApp;
64
+ /** Regrouper des pages dans le menu */
65
+ export declare const PagesGroup: React.FC<PageProps>;
@@ -2,19 +2,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
3
  import { ConfigProvider, Layout } from "antd";
4
4
  import { HashRouter, Outlet, Route, Routes } from "react-router-dom";
5
- import { generateRoutes } from "../../utils/route_utils";
5
+ //import { generateRoutes } from "../../utils/route_utils";
6
6
  import DashboardSider from "./Sider";
7
7
  import { Content } from "antd/es/layout/layout";
8
8
  import { ErrorComponent } from "./Error";
9
9
  import { DasbhoardFooter } from "./Footer";
10
- import { createContext } from "react";
10
+ import { Children, createContext, isValidElement } from "react";
11
11
  import { HelmetProvider } from "react-helmet-async";
12
12
  import { createDatasetRegistry } from "../Dataset/hooks";
13
13
  import { DatasetRegistryContext } from "../Dataset/context";
14
14
  import { ControlContext, CreateControlesRegistry } from "../Control/Control";
15
+ import slug from 'slug';
16
+ import { generateRoutes, getFirstValidElement } from "../../utils/route_utils";
17
+ import renderIcon from "../../utils/icon";
15
18
  //import '../../index.css' //TODO a intégrer en jsx
16
19
  const queryClient = new QueryClient();
17
- const default_theme = {
20
+ export const default_theme = {
18
21
  token: {
19
22
  colorPrimary: "#95c11f",
20
23
  linkHoverDecoration: 'underline',
@@ -33,8 +36,47 @@ const default_theme = {
33
36
  }
34
37
  };
35
38
  export const AppContext = createContext({});
36
- const DashboardApp = ({ routes, theme, logo, brands, footerSlider, title, subtitle, disablePoweredBy = false }) => {
39
+ /** Composant principal de l'application.
40
+ *
41
+ * Les enfants de l'application sont les différentes pages de tableau de bord.
42
+ * La configuration globale de l'application (nom, style, etc.) se fait via les propriétés.
43
+ */
44
+ const DashboardApp = ({ children, theme, routes: routes_legacy, logo, brands, footerSlider, title, subtitle, disablePoweredBy = false }) => {
37
45
  const context_values = { title, subtitle, logo };
38
- 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(DatasetRegistryContext.Provider, { value: createDatasetRegistry(), children: _jsx(ControlContext.Provider, { value: CreateControlesRegistry(), children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { hasSider: true, style: { minHeight: '100vh' }, children: [_jsx(DashboardSider, { route_config: routes, poweredBy: !disablePoweredBy }), _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, {}) })] }) }) }) }) }) }) }) }) }));
46
+ const pages = Children.toArray(children)
47
+ .filter(isValidElement);
48
+ const routes = pages.length >= 1 ? pages.map((page, idx) => {
49
+ if (typeof (page.type) != 'string' && page.type.name == PagesGroup.name) { // Groupe
50
+ return ({
51
+ label: page.props.title ?? String(idx),
52
+ path: slug(page.props.title ?? String(idx)),
53
+ element: undefined, // Pas de route pour les groupes
54
+ hidden: page.props.hidden ?? false,
55
+ icon: renderIcon(page.props.icon),
56
+ children: Children.toArray(page.props.children)?.map((c, idx) => ({
57
+ label: c.props.title, // A factoriser avec les pages hors groupes
58
+ path: slug(c.props.title ?? idx),
59
+ element: c,
60
+ hidden: c.props.hidden ?? false,
61
+ icon: renderIcon(c.props.icon)
62
+ }))
63
+ });
64
+ }
65
+ else { //Pages directes (sans groupe)
66
+ return ({
67
+ label: page.props.title ?? String(idx),
68
+ path: slug(page.props.title ?? String(idx)),
69
+ element: page,
70
+ hidden: page.props.hidden ?? false,
71
+ icon: renderIcon(page.props.icon)
72
+ });
73
+ }
74
+ }) : routes_legacy ?? []; // Pour rétro-compatibiltié
75
+ const route_tree = generateRoutes(routes);
76
+ 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(DatasetRegistryContext.Provider, { value: createDatasetRegistry(), children: _jsx(ControlContext.Provider, { value: CreateControlesRegistry(), children: _jsx(HashRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsxs(Layout, { hasSider: true, style: { minHeight: '100vh' }, children: [_jsx(DashboardSider, { route_config: routes, poweredBy: !disablePoweredBy }), _jsxs(Layout, { children: [_jsx(Content, { style: { width: "100%" }, children: _jsx(Outlet, {}) }), _jsx(DasbhoardFooter, { brands: brands, slider: footerSlider })] })] }), children: [_jsx(Route, { index: true, element: getFirstValidElement(route_tree) }), route_tree, _jsx(Route, { path: "*", element: _jsx(ErrorComponent, {}) })] }) }) }) }) }) }) }) }) }));
39
77
  };
40
78
  export default DashboardApp;
79
+ /** Regrouper des pages dans le menu */
80
+ export const PagesGroup = ({ children }) => {
81
+ return children;
82
+ };
package/dist/index.d.ts CHANGED
@@ -16,11 +16,11 @@ import NextPrevSelect from "./components/NextPrevSelect/NextPrevSelect";
16
16
  import Control from "./components/Control/Control";
17
17
  import DashboardChart from "./components/DashboardChart/DashboardChart";
18
18
  import MapLegend from "./components/MapLegend/MapLegend";
19
- import DashboardApp from "./components/Layout/DashboardApp";
19
+ import DashboardApp, { PagesGroup } from "./components/Layout/DashboardApp";
20
20
  import DashboardSider from "./components/Layout/Sider";
21
21
  import DashboardPage from "./components/DashboardPage/Page";
22
22
  import DashboardElement from "./components/DashboardElement/DashboardElement";
23
- export { KeyFigure, DashboardElement, LoadingContainer, FlipCard, Attribution, NextPrevSelect, Control, DashboardChart, DashboardPage, MapLegend, DashboardSider, DashboardApp, };
23
+ export { KeyFigure, DashboardElement, LoadingContainer, FlipCard, Attribution, NextPrevSelect, Control, DashboardChart, DashboardPage, MapLegend, DashboardSider, DashboardApp, PagesGroup, };
24
24
  import { dataProvider as WfsProvider } from "./data_providers/wfs";
25
25
  import { dataProvider as DatafairProvider } from "./data_providers/datafair";
26
26
  import { dataProvider as FileProvider } from "./data_providers/file";
@@ -29,5 +29,6 @@ export type { SimpleRecord, Partner, RouteConfig } from "./types";
29
29
  export type { LegendItem } from "./components/MapLegend/MapLegend";
30
30
  export type { DashboardConfig } from "./components/Layout/DashboardApp";
31
31
  export type { datasetInput } from "./components/Dataset/hooks";
32
+ export type { PageProps } from "./components/Layout/DashboardApp";
32
33
  import * as DSL from './dsl';
33
34
  export { DSL };
package/dist/index.js CHANGED
@@ -20,11 +20,11 @@ import Control from "./components/Control/Control";
20
20
  import DashboardChart from "./components/DashboardChart/DashboardChart";
21
21
  import MapLegend from "./components/MapLegend/MapLegend";
22
22
  // Layout
23
- import DashboardApp from "./components/Layout/DashboardApp";
23
+ import DashboardApp, { PagesGroup } from "./components/Layout/DashboardApp";
24
24
  import DashboardSider from "./components/Layout/Sider";
25
25
  import DashboardPage from "./components/DashboardPage/Page";
26
26
  import DashboardElement from "./components/DashboardElement/DashboardElement";
27
- export { KeyFigure, DashboardElement, LoadingContainer, FlipCard, Attribution, NextPrevSelect, Control, DashboardChart, DashboardPage, MapLegend, DashboardSider, DashboardApp, };
27
+ export { KeyFigure, DashboardElement, LoadingContainer, FlipCard, Attribution, NextPrevSelect, Control, DashboardChart, DashboardPage, MapLegend, DashboardSider, DashboardApp, PagesGroup, };
28
28
  // DataProviders
29
29
  import { dataProvider as WfsProvider } from "./data_providers/wfs";
30
30
  import { dataProvider as DatafairProvider } from "./data_providers/datafair";
@@ -0,0 +1,3 @@
1
+ import { ReactNode } from "react";
2
+ declare const renderIcon: (input: ReactNode | String) => number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
3
+ export default renderIcon;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Icon } from "@iconify/react";
3
+ const renderIcon = (input) => {
4
+ return typeof (input) === 'string' ? _jsx(Icon, { icon: input }) : input;
5
+ };
6
+ export default renderIcon;
@@ -1,6 +1,12 @@
1
1
  import { RouteConfig } from "../types";
2
2
  import type { MenuProps } from 'antd';
3
+ import React from "react";
3
4
  type MenuItem = Required<MenuProps>['items'][number];
4
5
  export declare const generateRoutes: (routes: RouteConfig[]) => import("react/jsx-runtime").JSX.Element[];
5
6
  export declare const generateMenuItems: (routes: RouteConfig[], parentPath?: string) => MenuItem[];
7
+ /** AI Generated,
8
+ * Get first "viewable" route (aka "not a group")
9
+ * Used for index route
10
+ */
11
+ export declare function getFirstValidElement(routes: React.ReactElement[]): React.ReactElement | null;
6
12
  export {};
@@ -1,14 +1,70 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  //Retourne les routes et le menu
3
- import { NavLink, Route } from "react-router-dom";
4
- export const generateRoutes = (routes) => routes.map((route) => (_jsx(Route, { path: route.path, element: !route.children && route.element, children: route.children && generateRoutes(route.children) }, route.path)));
5
- export const generateMenuItems = (routes, parentPath = "") => routes.filter((route) => route.hidden != true).map((route) => {
6
- const fullPath = `${parentPath}/${route.path}`;
7
- const menuItem = {
8
- key: fullPath,
9
- label: route.children ? _jsxs(_Fragment, { children: [" ", route.label || route.path] }) : _jsx(NavLink, { to: fullPath, children: route.label || route.path }),
10
- icon: route.icon,
11
- ...(route.children && { children: generateMenuItems(route.children, fullPath) }), // Ajout conditionnel des enfants
12
- };
13
- return menuItem;
14
- });
3
+ import { NavLink, Outlet, Route } from "react-router-dom";
4
+ import React from "react";
5
+ export const generateRoutes = (routes) => routes.map((route) => (_jsx(Route, { path: route.path, element: route.children ? _jsx(Outlet, {}) : route.element, children: route.children && generateRoutes(route.children) }, route.path)));
6
+ export const generateMenuItems = (routes, parentPath = "") => {
7
+ const out = routes.filter((route) => route.hidden != true).map((route) => {
8
+ const fullPath = `${parentPath}/${route.path}`;
9
+ const menuItem = {
10
+ key: fullPath,
11
+ label: route.children ? _jsxs(_Fragment, { children: [" ", route.label || route.path] }) : _jsx(NavLink, { to: fullPath, children: route.label || route.path }),
12
+ icon: route.icon,
13
+ ...(route.children && { children: generateMenuItems(route.children, fullPath) }), // Ajout conditionnel des enfants. Legacy
14
+ };
15
+ return menuItem;
16
+ });
17
+ return buildMenuTree(out);
18
+ };
19
+ /** AI generated function */
20
+ function buildMenuTree(items) {
21
+ // Trier par profondeur de path
22
+ const sorted = [...items].sort((a, b) =>
23
+ //@ts-ignore
24
+ a.key.split("/").filter(Boolean).length - b.key.split("/").filter(Boolean).length);
25
+ const menuMap = new Map();
26
+ const roots = [];
27
+ for (const item of sorted) {
28
+ //@ts-ignore
29
+ const normalizedKey = item.key.replace(/\/$/, "") || "/";
30
+ const segments = normalizedKey.split("/").filter(Boolean);
31
+ const parentKey = segments.length > 0
32
+ ? "/" + segments.slice(0, -1).join("/")
33
+ : null;
34
+ const parent = parentKey ? menuMap.get(parentKey) : null;
35
+ // On clone l'item pour ne pas muter l'original
36
+ const menuItem = { ...item, key: normalizedKey };
37
+ menuMap.set(normalizedKey, menuItem);
38
+ if (parent) {
39
+ parent.children = parent.children ?? [];
40
+ parent.children.push(menuItem);
41
+ }
42
+ else {
43
+ roots.push(menuItem);
44
+ }
45
+ }
46
+ return roots;
47
+ }
48
+ /** AI Generated,
49
+ * Get first "viewable" route (aka "not a group")
50
+ * Used for index route
51
+ */
52
+ export function getFirstValidElement(routes) {
53
+ function isEmptyOutlet(el) {
54
+ return React.isValidElement(el) && el.type === Outlet;
55
+ }
56
+ for (const route of routes) {
57
+ const el = route.props.element;
58
+ if (el && !isEmptyOutlet(el)) { // Page
59
+ return el;
60
+ }
61
+ //Group
62
+ const children = React.Children.toArray(route.props.children).filter(React.isValidElement);
63
+ if (children.length > 0) {
64
+ const firstChild = getFirstValidElement(children);
65
+ if (firstChild)
66
+ return firstChild;
67
+ }
68
+ }
69
+ return null;
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geo2france/api-dashboard",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "private": false,
5
5
  "description": "Build dashboards with JSX/TSX",
6
6
  "main": "dist/index.js",
@@ -64,17 +64,18 @@
64
64
  "react-helmet": "^6.1.0",
65
65
  "react-helmet-async": "^2.0.5",
66
66
  "slick-carousel": "^1.8.1",
67
+ "slug": "^11.0.1",
67
68
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
68
69
  },
69
70
  "devDependencies": {
70
71
  "@ant-design/icons": "^6.0.2",
71
- "@chromatic-com/storybook": "^5.0.0",
72
+ "@chromatic-com/storybook": "^5.0.1",
72
73
  "@iconify/json": "^2.2.382",
73
- "@storybook/addon-a11y": "^10.2.1",
74
- "@storybook/addon-docs": "^10.2.1",
75
- "@storybook/addon-onboarding": "^10.2.1",
76
- "@storybook/addon-vitest": "^10.2.1",
77
- "@storybook/react-vite": "^10.2.1",
74
+ "@storybook/addon-a11y": "^10.2.13",
75
+ "@storybook/addon-docs": "^10.2.13",
76
+ "@storybook/addon-onboarding": "^10.2.13",
77
+ "@storybook/addon-vitest": "^10.2.13",
78
+ "@storybook/react-vite": "^10.2.13",
78
79
  "@tanstack/react-query": "^5.51.11",
79
80
  "@testing-library/dom": "^10.4.0",
80
81
  "@testing-library/jest-dom": "^6.5.0",
@@ -88,6 +89,7 @@
88
89
  "@types/react-dom": "^18.3.1",
89
90
  "@types/react-helmet-async": "^1.0.1",
90
91
  "@types/react-icons": "^2.2.7",
92
+ "@types/slug": "^5.0.9",
91
93
  "@typescript-eslint/eslint-plugin": "^7.16.1",
92
94
  "@typescript-eslint/parser": "^7.16.1",
93
95
  "@vitejs/plugin-react": "^4.3.1",
@@ -101,7 +103,7 @@
101
103
  "react": "^18.3.1",
102
104
  "react-map-gl": "^7.1.9",
103
105
  "react-router-dom": "^7.13.0",
104
- "storybook": "^10.2.1",
106
+ "storybook": "^10.2.13",
105
107
  "ts-jest": "^29.2.5",
106
108
  "ts-node": "^10.9.2",
107
109
  "tsup": "^8.5.0",