Bläddra i källkod

Merge branch 'develop' into 1429-the-number-visual-element-number-should-have-property-type-step

namnguyen 11 månader sedan
förälder
incheckning
8d3df327e7

+ 15 - 3
frontend/taipy-gui/base/src/app.ts

@@ -4,7 +4,8 @@ import { uploadFile } from "../../src/workers/fileupload";
 
 import { Socket, io } from "socket.io-client";
 import { DataManager, ModuleData } from "./dataManager";
-import { initSocket } from "./utils";
+import { initSocket } from "./socket";
+import { TaipyWsAdapter, WsAdapter } from "./wsAdapter";
 
 export type OnInitHandler = (taipyApp: TaipyApp) => void;
 export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
@@ -23,8 +24,10 @@ export class TaipyApp {
     appId: string;
     clientId: string;
     context: string;
+    metadata: Record<string, unknown>;
     path: string | undefined;
     routes: Route[] | undefined;
+    wsAdapters: WsAdapter[];
 
     constructor(
         onInit: OnInitHandler | undefined = undefined,
@@ -39,10 +42,12 @@ export class TaipyApp {
         this.functionData = undefined;
         this.clientId = "";
         this.context = "";
+        this.metadata = {};
         this.appId = "";
         this.routes = undefined;
         this.path = path;
         this.socket = socket;
+        this.wsAdapters = [new TaipyWsAdapter()];
         // Init socket io connection
         initSocket(socket, this);
     }
@@ -51,6 +56,7 @@ export class TaipyApp {
     get onInit() {
         return this._onInit;
     }
+
     set onInit(handler: OnInitHandler | undefined) {
         if (handler !== undefined && handler.length !== 1) {
             throw new Error("onInit() requires one parameter");
@@ -61,6 +67,7 @@ export class TaipyApp {
     get onChange() {
         return this._onChange;
     }
+
     set onChange(handler: OnChangeHandler | undefined) {
         if (handler !== undefined && handler.length !== 3) {
             throw new Error("onChange() requires three parameters");
@@ -71,6 +78,7 @@ export class TaipyApp {
     get onNotify() {
         return this._onNotify;
     }
+
     set onNotify(handler: OnNotifyHandler | undefined) {
         if (handler !== undefined && handler.length !== 3) {
             throw new Error("onNotify() requires three parameters");
@@ -105,6 +113,10 @@ export class TaipyApp {
     }
 
     // Public methods
+    registerWsAdapter(wsAdapter: WsAdapter) {
+        this.wsAdapters.unshift(wsAdapter);
+    }
+
     getEncodedName(varName: string, module: string) {
         return this.variableData?.getEncodedName(varName, module);
     }
@@ -152,7 +164,7 @@ export class TaipyApp {
         if (!path || path === "") {
             path = window.location.pathname.slice(1);
         }
-        sendWsMessage(this.socket, "GMC", "get_module_context", { path: path }, this.clientId);
+        sendWsMessage(this.socket, "GMC", "get_module_context", { path: path || "/" }, this.clientId);
     }
 
     trigger(actionName: string, triggerId: string, payload: Record<string, unknown> = {}) {
@@ -165,7 +177,7 @@ export class TaipyApp {
     }
 
     getPageMetadata() {
-        return JSON.parse(localStorage.getItem("tp_cp_meta") || "{}");
+        return this.metadata;
     }
 }
 

+ 1 - 1
frontend/taipy-gui/base/src/dataManager.ts

@@ -2,7 +2,7 @@ export type ModuleData = Record<string, VarName>;
 
 export type VarName = Record<string, VarData>;
 
-interface VarData {
+export interface VarData {
     type: string;
     value: unknown;
     encoded_name: string;

+ 9 - 0
frontend/taipy-gui/base/src/exports.ts

@@ -0,0 +1,9 @@
+import { WsAdapter } from "./wsAdapter";
+import { sendWsMessage } from "../../src/context/wsUtils";
+// import { TaipyApp } from "./app";
+
+export {
+    WsAdapter,
+    sendWsMessage,
+    // TaipyApp,
+};

+ 0 - 1
frontend/taipy-gui/base/src/index.ts

@@ -7,5 +7,4 @@ export type { OnChangeHandler, OnInitHandler, ModuleData };
 
 window.addEventListener("beforeunload", () => {
     document.cookie = "tprh=;path=/;Max-Age=-99999999;";
-    localStorage.removeItem("tp_cp_meta");
 });

+ 7 - 0
frontend/taipy-gui/base/src/packaging/package.json

@@ -0,0 +1,7 @@
+{
+  "name": "taipy-gui-base",
+  "version": "3.2.0",
+  "private": true,
+  "main": "./taipy-gui-base.js",
+  "types": "./taipy-gui-base.d.ts"
+}

+ 113 - 0
frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts

@@ -0,0 +1,113 @@
+import { Socket } from "socket.io-client";
+
+export type ModuleData = Record<string, VarName>;
+export type VarName = Record<string, VarData>;
+export interface VarData {
+    type: string;
+    value: unknown;
+    encoded_name: string;
+}
+declare class DataManager {
+    _data: Record<string, unknown>;
+    _init_data: ModuleData;
+    constructor(variableModuleData: ModuleData);
+    init(variableModuleData: ModuleData): ModuleData;
+    getEncodedName(varName: string, module: string): string | undefined;
+    getName(encodedName: string): [string, string] | undefined;
+    get(encodedName: string): unknown;
+    getInfo(encodedName: string): VarData | undefined;
+    getDataTree(): ModuleData;
+    getAllData(): Record<string, unknown>;
+    update(encodedName: string, value: unknown): void;
+}
+export type OnInitHandler = (taipyApp: TaipyApp) => void;
+export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
+export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void;
+export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void;
+export type Route = [string, string];
+export declare class TaipyApp {
+    socket: Socket;
+    _onInit: OnInitHandler | undefined;
+    _onChange: OnChangeHandler | undefined;
+    _onNotify: OnNotifyHandler | undefined;
+    _onReload: OnReloadHandler | undefined;
+    variableData: DataManager | undefined;
+    functionData: DataManager | undefined;
+    appId: string;
+    clientId: string;
+    context: string;
+    path: string | undefined;
+    routes: Route[] | undefined;
+    wsAdapters: WsAdapter[];
+    constructor(
+        onInit?: OnInitHandler | undefined,
+        onChange?: OnChangeHandler | undefined,
+        path?: string | undefined,
+        socket?: Socket | undefined
+    );
+    get onInit(): OnInitHandler | undefined;
+    set onInit(handler: OnInitHandler | undefined);
+    get onChange(): OnChangeHandler | undefined;
+    set onChange(handler: OnChangeHandler | undefined);
+    get onNotify(): OnNotifyHandler | undefined;
+    set onNotify(handler: OnNotifyHandler | undefined);
+    get onReload(): OnReloadHandler | undefined;
+    set onReload(handler: OnReloadHandler | undefined);
+    init(): void;
+    registerWsAdapter(wsAdapter: WsAdapter): void;
+    getEncodedName(varName: string, module: string): string | undefined;
+    getName(encodedName: string): [string, string] | undefined;
+    get(encodedName: string): unknown;
+    getInfo(encodedName: string): VarData | undefined;
+    getDataTree(): ModuleData | undefined;
+    getAllData(): Record<string, unknown> | undefined;
+    getFunctionList(): string[];
+    getRoutes(): Route[] | undefined;
+    update(encodedName: string, value: unknown): void;
+    getContext(): string;
+    updateContext(path?: string | undefined): void;
+    trigger(actionName: string, triggerId: string, payload?: Record<string, unknown>): void;
+    upload(encodedName: string, files: FileList, progressCallback: (val: number) => void): Promise<string>;
+    getPageMetadata(): any;
+}
+export type WsMessageType =
+    | "A"
+    | "U"
+    | "DU"
+    | "MU"
+    | "RU"
+    | "AL"
+    | "BL"
+    | "NA"
+    | "ID"
+    | "MS"
+    | "DF"
+    | "PR"
+    | "ACK"
+    | "GMC"
+    | "GDT"
+    | "AID"
+    | "GR";
+export interface WsMessage {
+    type: WsMessageType | str;
+    name: string;
+    payload: Record<string, unknown> | unknown;
+    propagate: boolean;
+    client_id: string;
+    module_context: string;
+    ack_id?: string;
+}
+export declare const sendWsMessage: (
+    socket: Socket | undefined,
+    type: WsMessageType | str,
+    name: string,
+    payload: Record<string, unknown> | unknown,
+    id: string,
+    moduleContext?: string,
+    propagate?: boolean,
+    serverAck?: (val: unknown) => void
+) => string;
+export declare abstract class WsAdapter {
+    abstract supportedMessageTypes: string[];
+    abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean;
+}

+ 46 - 0
frontend/taipy-gui/base/src/socket.ts

@@ -0,0 +1,46 @@
+import { Socket } from "socket.io-client";
+import { WsMessage, sendWsMessage } from "../../src/context/wsUtils";
+import { TaipyApp } from "./app";
+
+export const initSocket = (socket: Socket, taipyApp: TaipyApp) => {
+    socket.on("connect", () => {
+        if (taipyApp.clientId === "" || taipyApp.appId === "") {
+            taipyApp.init();
+        }
+    });
+    // Send a request to get App ID to verify that the app has not been reloaded
+    socket.io.on("reconnect", () => {
+        console.log("WebSocket reconnected");
+        sendWsMessage(socket, "AID", "reconnect", taipyApp.appId, taipyApp.clientId, taipyApp.context);
+    });
+    // try to reconnect on connect_error
+    socket.on("connect_error", (err) => {
+        console.log("Error connecting WebSocket: ", err);
+        setTimeout(() => {
+            socket && socket.connect();
+        }, 500);
+    });
+    // try to reconnect on server disconnection
+    socket.on("disconnect", (reason, details) => {
+        console.log("WebSocket disconnected due to: ", reason, details);
+        if (reason === "io server disconnect") {
+            socket && socket.connect();
+        }
+    });
+    // handle message data from backend
+    socket.on("message", (message: WsMessage) => {
+        // handle messages with registered websocket adapters
+        for (const adapter of taipyApp.wsAdapters) {
+            if (adapter.supportedMessageTypes.includes(message.type)) {
+                const messageResolved = adapter.handleWsMessage(message, taipyApp);
+                if (messageResolved) {
+                    return;
+                }
+            }
+        }
+    });
+    // only now does the socket tries to open/connect
+    if (!socket.connected) {
+        socket.connect();
+    }
+};

+ 0 - 117
frontend/taipy-gui/base/src/utils.ts

@@ -1,117 +0,0 @@
-import merge from "lodash/merge";
-import { Socket } from "socket.io-client";
-import { IdMessage, storeClientId } from "../../src/context/utils";
-import { WsMessage, sendWsMessage } from "../../src/context/wsUtils";
-import { TaipyApp } from "./app";
-import { DataManager, ModuleData } from "./dataManager";
-
-interface MultipleUpdatePayload {
-    name: string;
-    payload: { value: unknown };
-}
-
-interface AlertMessage extends WsMessage {
-    atype: string;
-    message: string;
-}
-
-const initWsMessageTypes = ["ID", "AID", "GMC"];
-
-export const initSocket = (socket: Socket, taipyApp: TaipyApp) => {
-    socket.on("connect", () => {
-        if (taipyApp.clientId === "" || taipyApp.appId === "") {
-            taipyApp.init();
-        }
-    });
-    // Send a request to get App ID to verify that the app has not been reloaded
-    socket.io.on("reconnect", () => {
-        console.log("WebSocket reconnected")
-        sendWsMessage(socket, "AID", "reconnect", taipyApp.appId, taipyApp.clientId, taipyApp.context);
-    });
-    // try to reconnect on connect_error
-    socket.on("connect_error", (err) => {
-        console.log("Error connecting WebSocket: ", err);
-        setTimeout(() => {
-            socket && socket.connect();
-        }, 500);
-    });
-    // try to reconnect on server disconnection
-    socket.on("disconnect", (reason, details) => {
-        console.log("WebSocket disconnected due to: ", reason, details);
-        if (reason === "io server disconnect") {
-            socket && socket.connect();
-        }
-    });
-    // handle message data from backend
-    socket.on("message", (message: WsMessage) => {
-        processWsMessage(message, taipyApp);
-    });
-    // only now does the socket tries to open/connect
-    if (!socket.connected) {
-        socket.connect();
-    }
-};
-
-const processWsMessage = (message: WsMessage, taipyApp: TaipyApp) => {
-    if (message.type) {
-        if (message.type === "MU" && Array.isArray(message.payload)) {
-            for (const muPayload of message.payload as [MultipleUpdatePayload]) {
-                const encodedName = muPayload.name;
-                const { value } = muPayload.payload;
-                taipyApp.variableData?.update(encodedName, value);
-                taipyApp.onChange && taipyApp.onChange(taipyApp, encodedName, value);
-            }
-        } else if (message.type === "ID") {
-            const { id } = message as unknown as IdMessage;
-            storeClientId(id);
-            taipyApp.clientId = id;
-            taipyApp.updateContext(taipyApp.path);
-        } else if (message.type === "GMC") {
-            const mc = (message.payload as Record<string, unknown>).data as string;
-            window.localStorage.setItem("ModuleContext", mc);
-            taipyApp.context = mc;
-        } else if (message.type === "GDT") {
-            const payload = message.payload as Record<string, ModuleData>;
-            const variableData = payload.variable;
-            const functionData = payload.function;
-            if (taipyApp.variableData && taipyApp.functionData) {
-                const varChanges = taipyApp.variableData.init(variableData);
-                const functionChanges = taipyApp.functionData.init(functionData);
-                const changes = merge(varChanges, functionChanges);
-                if (varChanges || functionChanges) {
-                    taipyApp.onReload && taipyApp.onReload(taipyApp, changes);
-                }
-            } else {
-                taipyApp.variableData = new DataManager(variableData);
-                taipyApp.functionData = new DataManager(functionData);
-                taipyApp.onInit && taipyApp.onInit(taipyApp);
-            }
-        } else if (message.type === "AID") {
-            const payload = message.payload as Record<string, unknown>;
-            if (payload.name === "reconnect") {
-                return taipyApp.init();
-            }
-            taipyApp.appId = payload.id as string;
-        } else if (message.type === "GR") {
-            const payload = message.payload as [string, string][];
-            taipyApp.routes = payload;
-        } else if (message.type === "AL" && taipyApp.onNotify) {
-            const payload = message as AlertMessage;
-            taipyApp.onNotify(taipyApp, payload.atype, payload.message);
-        }
-        postWsMessageProcessing(message, taipyApp);
-    }
-};
-
-const postWsMessageProcessing = (message: WsMessage, taipyApp: TaipyApp) => {
-    // perform data population only when all necessary metadata is ready
-    if (
-        initWsMessageTypes.includes(message.type) &&
-        taipyApp.clientId !== "" &&
-        taipyApp.appId !== "" &&
-        taipyApp.context !== "" &&
-        taipyApp.routes !== undefined
-    ) {
-        sendWsMessage(taipyApp.socket, "GDT", "get_data_tree", {}, taipyApp.clientId, taipyApp.context);
-    }
-};

+ 102 - 0
frontend/taipy-gui/base/src/wsAdapter.ts

@@ -0,0 +1,102 @@
+import merge from "lodash/merge";
+import { TaipyApp } from "./app";
+import { IdMessage, storeClientId } from "../../src/context/utils";
+import { WsMessage, sendWsMessage } from "../../src/context/wsUtils";
+import { DataManager, ModuleData } from "./dataManager";
+
+export abstract class WsAdapter {
+    abstract supportedMessageTypes: string[];
+
+    abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean;
+}
+
+interface MultipleUpdatePayload {
+    name: string;
+    payload: { value: unknown };
+}
+
+interface AlertMessage extends WsMessage {
+    atype: string;
+    message: string;
+}
+
+export class TaipyWsAdapter extends WsAdapter {
+    supportedMessageTypes: string[];
+    initWsMessageTypes: string[];
+    constructor() {
+        super();
+        this.supportedMessageTypes = ["MU", "ID", "GMC", "GDT", "AID", "GR", "AL"];
+        this.initWsMessageTypes = ["ID", "AID", "GMC"];
+    }
+    handleWsMessage(message: WsMessage, taipyApp: TaipyApp): boolean {
+        if (message.type) {
+            if (message.type === "MU" && Array.isArray(message.payload)) {
+                for (const muPayload of message.payload as [MultipleUpdatePayload]) {
+                    const encodedName = muPayload.name;
+                    const { value } = muPayload.payload;
+                    taipyApp.variableData?.update(encodedName, value);
+                    taipyApp.onChange && taipyApp.onChange(taipyApp, encodedName, value);
+                }
+            } else if (message.type === "ID") {
+                const { id } = message as unknown as IdMessage;
+                storeClientId(id);
+                taipyApp.clientId = id;
+                taipyApp.updateContext(taipyApp.path);
+            } else if (message.type === "GMC") {
+                const payload = message.payload as Record<string, unknown>;
+                taipyApp.context = payload.context as string;
+                if (payload?.metadata) {
+                    try {
+                        taipyApp.metadata = JSON.parse((payload.metadata as string) || "{}");
+                    } catch (e) {
+                        console.error("Error parsing metadata from Taipy Designer", e);
+                    }
+                }
+            } else if (message.type === "GDT") {
+                const payload = message.payload as Record<string, ModuleData>;
+                const variableData = payload.variable;
+                const functionData = payload.function;
+                if (taipyApp.variableData && taipyApp.functionData) {
+                    const varChanges = taipyApp.variableData.init(variableData);
+                    const functionChanges = taipyApp.functionData.init(functionData);
+                    const changes = merge(varChanges, functionChanges);
+                    if (varChanges || functionChanges) {
+                        taipyApp.onReload && taipyApp.onReload(taipyApp, changes);
+                    }
+                } else {
+                    taipyApp.variableData = new DataManager(variableData);
+                    taipyApp.functionData = new DataManager(functionData);
+                    taipyApp.onInit && taipyApp.onInit(taipyApp);
+                }
+            } else if (message.type === "AID") {
+                const payload = message.payload as Record<string, unknown>;
+                if (payload.name === "reconnect") {
+                    taipyApp.init();
+                    return true;
+                }
+                taipyApp.appId = payload.id as string;
+            } else if (message.type === "GR") {
+                const payload = message.payload as [string, string][];
+                taipyApp.routes = payload;
+            } else if (message.type === "AL" && taipyApp.onNotify) {
+                const payload = message as AlertMessage;
+                taipyApp.onNotify(taipyApp, payload.atype, payload.message);
+            }
+            this.postWsMessageProcessing(message, taipyApp);
+            return true;
+        }
+        return false;
+    }
+    postWsMessageProcessing(message: WsMessage, taipyApp: TaipyApp) {
+        // perform data population only when all necessary metadata is ready
+        if (
+            this.initWsMessageTypes.includes(message.type) &&
+            taipyApp.clientId !== "" &&
+            taipyApp.appId !== "" &&
+            taipyApp.context !== "" &&
+            taipyApp.routes !== undefined
+        ) {
+            sendWsMessage(taipyApp.socket, "GDT", "get_data_tree", {}, taipyApp.clientId, taipyApp.context);
+        }
+    }
+}

+ 76 - 43
frontend/taipy-gui/base/webpack.config.js

@@ -1,56 +1,89 @@
 const path = require("path");
 const webpack = require("webpack");
-const resolveApp = relativePath => path.resolve(__dirname, relativePath);
+const CopyWebpackPlugin = require("copy-webpack-plugin");
+const resolveApp = (relativePath) => path.resolve(__dirname, relativePath);
 
 const moduleName = "TaipyGuiBase";
 const basePath = "../../../taipy/gui/webapp";
 const webAppPath = resolveApp(basePath);
+const taipyGuiBaseExportPath = resolveApp(basePath + "/taipy-gui-base-export");
 
-module.exports = {
-    target: "web",
-    entry: {
-        "default": "./base/src/index.ts",
-        "preview": "./base/src/index-preview.ts",
-    },
-    output: {
-        filename: (arg) => {
-            if (arg.chunk.name === "default") {
-                return "taipy-gui-base.js";
-            }
-            return "[name].taipy-gui-base.js";
-        },
-        chunkFilename: "[name].taipy-gui-base.js",
-        path: webAppPath,
-        globalObject: "this",
-        library: {
-            name: moduleName,
-            type: "umd",
+module.exports = [
+    {
+        target: "web",
+        entry: {
+            default: "./base/src/index.ts",
+            preview: "./base/src/index-preview.ts",
         },
-    },
-    optimization: {
-        splitChunks: {
-            chunks: 'all',
-            name: "shared",
+        output: {
+            filename: (arg) => {
+                if (arg.chunk.name === "default") {
+                    return "taipy-gui-base.js";
+                }
+                return "[name].taipy-gui-base.js";
+            },
+            chunkFilename: "[name].taipy-gui-base.js",
+            path: webAppPath,
+            globalObject: "this",
+            library: {
+                name: moduleName,
+                type: "umd",
+            },
+        },
+        optimization: {
+            splitChunks: {
+                chunks: "all",
+                name: "shared",
+            },
         },
+        module: {
+            rules: [
+                {
+                    test: /\.tsx?$/,
+                    use: "ts-loader",
+                    exclude: /node_modules/,
+                },
+            ],
+        },
+        resolve: {
+            extensions: [".tsx", ".ts", ".js", ".tsx"],
+        },
+        // externals: {
+        //     "socket.io-client": {
+        //         commonjs: "socket.io-client",
+        //         commonjs2: "socket.io-client",
+        //         amd: "socket.io-client",
+        //         root: "_",
+        //     },
+        // },
     },
-    module: {
-        rules: [
-            {
-                test: /\.tsx?$/,
-                use: "ts-loader",
-                exclude: /node_modules/,
+    {
+        entry: "./base/src/exports.ts",
+        output: {
+            filename: "taipy-gui-base.js",
+            path: taipyGuiBaseExportPath,
+            library: {
+                name: moduleName,
+                type: "umd",
             },
+            publicPath: "",
+        },
+        module: {
+            rules: [
+                {
+                    test: /\.tsx?$/,
+                    use: "ts-loader",
+                    exclude: /node_modules/,
+                },
+            ],
+        },
+        resolve: {
+            extensions: [".tsx", ".ts", ".js", ".tsx"],
+        },
+        plugins: [
+            new CopyWebpackPlugin({
+                patterns: [{ from: "./base/src/packaging", to: taipyGuiBaseExportPath }],
+            }),
         ],
     },
-    resolve: {
-        extensions: [".tsx", ".ts", ".js", ".tsx"],
-    },
-    // externals: {
-    //     "socket.io-client": {
-    //         commonjs: "socket.io-client",
-    //         commonjs2: "socket.io-client",
-    //         amd: "socket.io-client",
-    //         root: "_",
-    //     },
-    // },
-};
+];

+ 5 - 5
frontend/taipy-gui/package-lock.json

@@ -61,7 +61,7 @@
         "css-loader": "^7.1.0",
         "css-mediaquery": "^0.1.2",
         "dotenv-webpack": "^8.0.0",
-        "dts-bundle-generator": "^9.2.1",
+        "dts-bundle-generator": "^7.2.0",
         "eslint": "^8.57.0",
         "eslint-plugin-react": "^7.26.1",
         "eslint-plugin-react-hooks": "^4.2.0",
@@ -5918,12 +5918,12 @@
       }
     },
     "node_modules/dts-bundle-generator": {
-      "version": "9.5.1",
-      "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz",
-      "integrity": "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-7.2.0.tgz",
+      "integrity": "sha512-pHjRo52hvvLDRijzIYRTS9eJR7vAOs3gd/7jx+7YVnLU8ay3yPUWGtHXPtuMBSlJYk/s4nq1SvXObDCZVguYMg==",
       "dev": true,
       "dependencies": {
-        "typescript": ">=5.0.2",
+        "typescript": ">=4.5.2",
         "yargs": "^17.6.0"
       },
       "bin": {

+ 2 - 1
frontend/taipy-gui/package.json

@@ -46,6 +46,7 @@
     "lint:fix": "npm run lint -- --fix",
     "coverage": "npm test -- --coverage",
     "types": "dts-bundle-generator -o packaging/taipy-gui.gen.d.ts src/extensions/exports.ts",
+    "types-base": "dts-bundle-generator -o base/src/packaging/taipy-gui-base.gen.d.ts base/src/exports.ts",
     "doc": "typedoc --plugin typedoc-plugin-markdown --excludeNotDocumented --disableSources src/extensions/exports.ts",
     "doc.json": "typedoc --json docs/taipy-gui.json src/extensions/exports.ts",
     "mkdocs": "typedoc --options typedoc-mkdocs.json"
@@ -97,7 +98,7 @@
     "css-loader": "^7.1.0",
     "css-mediaquery": "^0.1.2",
     "dotenv-webpack": "^8.0.0",
-    "dts-bundle-generator": "^9.2.1",
+    "dts-bundle-generator": "^7.2.0",
     "eslint": "^8.57.0",
     "eslint-plugin-react": "^7.26.1",
     "eslint-plugin-react-hooks": "^4.2.0",

+ 1 - 5
frontend/taipy-gui/src/components/Taipy/Navigate.tsx

@@ -27,7 +27,7 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
     const { dispatch, state } = useContext(TaipyContext);
     const navigate = useNavigate();
     const location = useLocation();
-    const SPECIAL_PARAMS = ["tp_reload_all", "tp_reload_same_route_only", "tprh", "tp_cp_meta"];
+    const SPECIAL_PARAMS = ["tp_reload_all", "tp_reload_same_route_only", "tprh"];
 
     useEffect(() => {
         if (to) {
@@ -65,10 +65,6 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
                     if (tprh !== undefined) {
                         // Add a session cookie for the resource handler id
                         document.cookie = `tprh=${tprh};path=/;`;
-                        const meta = params?.tp_cp_meta;
-                        if (meta !== undefined) {
-                            localStorage.setItem("tp_cp_meta", meta);
-                        }
                         navigate(0);
                     }
                 }

+ 32 - 0
frontend/taipy-gui/webpack.config.js

@@ -34,6 +34,7 @@ const webAppPath = resolveApp(basePath);
 const reactManifestPath = resolveApp(basePath + "/" + reactBundle + "-manifest.json");
 const reactDllPath = resolveApp(basePath + "/" + reactBundle + ".dll.js")
 const taipyDllPath = resolveApp(basePath + "/" + taipyBundle + ".js")
+const taipyGuiBaseExportPath = resolveApp(basePath + "/taipy-gui-base-export");
 
 module.exports = (env, options) => {
     const envVariables = {
@@ -217,5 +218,36 @@ module.exports = (env, options) => {
         //         root: "_",
         //     },
         // },
+    },
+    {
+        entry: "./base/src/exports.ts",
+        output: {
+            filename: "taipy-gui-base.js",
+            path: taipyGuiBaseExportPath,
+            library: {
+                name: taipyGuiBaseBundleName,
+                type: "umd",
+            },
+            publicPath: "",
+        },
+        module: {
+            rules: [
+                {
+                    test: /\.tsx?$/,
+                    use: "ts-loader",
+                    exclude: /node_modules/,
+                },
+            ],
+        },
+        resolve: {
+            extensions: [".tsx", ".ts", ".js", ".tsx"],
+        },
+        plugins: [
+            new CopyWebpackPlugin({
+                patterns: [
+                    { from: "./base/src/packaging", to: taipyGuiBaseExportPath },
+                ],
+            }),
+        ],
     }];
 };

+ 3 - 0
taipy/__init__.py

@@ -30,5 +30,8 @@ if find_spec("taipy"):
     if find_spec("taipy.enterprise"):
         from taipy.enterprise._init import *
 
+    if find_spec("taipy.designer"):
+        from taipy.designer._init import *
+
     if find_spec("taipy._run"):
         from taipy._run import _run as run

+ 2 - 2
taipy/core/data/csv.py

@@ -188,7 +188,7 @@ class CSVDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
             )
         else:
             self._convert_data_to_dataframe(exposed_type, data).to_csv(
-                self._path, index=False, encoding=self.properties[self.__ENCODING_KEY], header=None
+                self._path, index=False, encoding=self.properties[self.__ENCODING_KEY], header=False
             )
 
     def write_with_column_names(self, data: Any, columns: Optional[List[str]] = None, job_id: Optional[JobId] = None):
@@ -201,6 +201,6 @@ class CSVDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
         """
         df = self._convert_data_to_dataframe(self.properties[self._EXPOSED_TYPE_PROPERTY], data)
         if columns and isinstance(df, pd.DataFrame):
-            df.columns = columns
+            df.columns = pd.Index(columns, dtype="object")
         df.to_csv(self._path, index=False, encoding=self.properties[self.__ENCODING_KEY])
         self.track_edit(timestamp=datetime.now(), job_id=job_id)

+ 1 - 1
taipy/core/data/excel.py

@@ -301,7 +301,7 @@ class ExcelDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
                 if columns:
                     data[key].columns = columns
 
-                df.to_excel(writer, key, index=False, header=self.properties[self._HAS_HEADER_PROPERTY] or None)
+                df.to_excel(writer, key, index=False, header=self.properties[self._HAS_HEADER_PROPERTY] or False)
 
     def _write(self, data: Any):
         if isinstance(data, Dict):

+ 4 - 4
taipy/core/data/parquet.py

@@ -214,10 +214,10 @@ class ParquetDataNode(DataNode, _FileDataNodeMixin, _TabularDataNodeMixin):
         }
         kwargs.update(self.properties[self.__WRITE_KWARGS_PROPERTY])
         kwargs.update(write_kwargs)
-        if isinstance(data, pd.Series):
-            df = pd.DataFrame(data)
-        else:
-            df = self._convert_data_to_dataframe(self.properties[self._EXPOSED_TYPE_PROPERTY], data)
+
+        df = self._convert_data_to_dataframe(self.properties[self._EXPOSED_TYPE_PROPERTY], data)
+        if isinstance(df, pd.Series):
+            df = pd.DataFrame(df)
 
         # Ensure that the columns are strings, otherwise writing will fail with pandas 1.3.5
         df.columns = df.columns.astype(str)

+ 9 - 3
taipy/core/data/sql_table.py

@@ -10,7 +10,7 @@
 # specific language governing permissions and limitations under the License.
 
 from datetime import datetime, timedelta
-from typing import Any, Dict, List, Optional, Set
+from typing import Any, Dict, List, Optional, Set, Union
 
 import pandas as pd
 from sqlalchemy import MetaData, Table
@@ -146,8 +146,14 @@ class SQLTableDataNode(_AbstractSQLDataNode):
         connection.execute(table.insert(), data)
 
     @classmethod
-    def __insert_dataframe(cls, df: pd.DataFrame, table: Any, connection: Any, delete_table: bool) -> None:
-        cls.__insert_dicts(df.to_dict(orient="records"), table, connection, delete_table)
+    def __insert_dataframe(
+        cls, df: Union[pd.DataFrame, pd.Series], table: Any, connection: Any, delete_table: bool
+    ) -> None:
+        if isinstance(df, pd.Series):
+            data = [df.to_dict()]
+        elif isinstance(df, pd.DataFrame):
+            data = df.to_dict(orient="records")
+        cls.__insert_dicts(data, table, connection, delete_table)
 
     @classmethod
     def __delete_all_rows(cls, table: Any, connection: Any, delete_table: bool) -> None:

+ 2 - 2
taipy/gui/custom/_page.py

@@ -46,13 +46,13 @@ class ResourceHandler(ABC):
     User can implement this class to provide custom resources for the custom pages
     """
 
-    id: str = ""
+    rh_id: str = ""
 
     def __init__(self) -> None:
         _ExternalResourceHandlerManager().register(self)
 
     def get_id(self) -> str:
-        return self.id if id != "" else str(id(self))
+        return self.rh_id if self.rh_id != "" else str(id(self))
 
     @abstractmethod
     def get_resources(self, path: str, taipy_resource_path: str) -> t.Any:

+ 27 - 4
taipy/gui/gui.py

@@ -598,6 +598,12 @@ class Gui:
             setattr(g, "update_count", update_count)  # noqa: B010
             return None
 
+    def _handle_connect(self):
+        pass
+
+    def _handle_disconnect(self):
+        pass
+
     def _manage_message(self, msg_type: _WsType, message: dict) -> None:
         try:
             client_id = None
@@ -632,6 +638,8 @@ class Gui:
                         self.__handle_ws_app_id(message)
                     elif msg_type == _WsType.GET_ROUTES.value:
                         self.__handle_ws_get_routes()
+                    else:
+                        self._manage_external_message(msg_type, message)
                 self.__send_ack(message.get("ack_id"))
         except Exception as e:  # pragma: no cover
             if isinstance(e, AttributeError) and (name := message.get("name")):
@@ -652,6 +660,11 @@ class Gui:
             else:
                 _warn(f"Decoding Message has failed: {message}", e)
 
+    # To be expanded by inheriting classes
+    # this will be used to handle ws messages that is not handled by the base Gui class
+    def _manage_external_message(self, msg_type: _WsType, message: dict) -> None:
+        pass
+
     def __front_end_update(
         self,
         var_name: str,
@@ -1098,15 +1111,24 @@ class Gui:
 
     def __handle_ws_get_module_context(self, payload: t.Any):
         if isinstance(payload, dict):
+            page_path = str(payload.get("path"))
+            if page_path in {"/", ""}:
+                page_path = Gui.__root_page_name
             # Get Module Context
-            if mc := self._get_page_context(str(payload.get("path"))):
+            if mc := self._get_page_context(page_path):
+                page_renderer = self._get_page(page_path)._renderer
                 self._bind_custom_page_variables(
-                    self._get_page(str(payload.get("path")))._renderer, self._get_client_id()
+                    page_renderer, self._get_client_id()
                 )
+                # get metadata if there is one
+                metadata: t.Dict[str, t.Any] = {}
+                if hasattr(page_renderer, "_metadata"):
+                    metadata = getattr(page_renderer, "_metadata", {})
+                meta_return = json.dumps(metadata, cls=_TaipyJsonEncoder) if metadata else None
                 self.__send_ws(
                     {
                         "type": _WsType.GET_MODULE_CONTEXT.value,
-                        "payload": {"data": mc},
+                        "payload": {"context": mc, "metadata": meta_return},
                     }
                 )
 
@@ -2136,6 +2158,8 @@ class Gui:
 
     def _bind_custom_page_variables(self, page: CustomPage, client_id: t.Optional[str]):
         """Handle the bindings of custom page variables"""
+        if not isinstance(page, CustomPage):
+            return
         with self.get_flask_app().app_context() if has_app_context() else contextlib.nullcontext():  # type: ignore[attr-defined]
             self.__set_client_id_in_context(client_id)
             with self._set_locals_context(page._get_module_name()):
@@ -2165,7 +2189,6 @@ class Gui:
                 to=page_name,
                 params={
                     _Server._RESOURCE_HANDLER_ARG: pr._resource_handler.get_id(),
-                    _Server._CUSTOM_PAGE_META_ARG: json.dumps(pr._metadata, cls=_TaipyJsonEncoder),
                 },
             ):
                 # Proactively handle the bindings of custom page variables

+ 8 - 1
taipy/gui/server.py

@@ -48,7 +48,6 @@ class _Server:
     __OPENING_CURLY = r"\1&#x7B;"
     __CLOSING_CURLY = r"&#x7D;\2"
     _RESOURCE_HANDLER_ARG = "tprh"
-    _CUSTOM_PAGE_META_ARG = "tp_cp_meta"
 
     def __init__(
         self,
@@ -111,6 +110,14 @@ class _Server:
             elif "type" in message:
                 gui._manage_message(message["type"], message)
 
+        @self._ws.on("connect")
+        def handle_connect():
+            gui._handle_connect()
+
+        @self._ws.on("disconnect")
+        def handle_disconnect():
+            gui._handle_disconnect()
+
     def __is_ignored(self, file_path: str) -> bool:
         if not hasattr(self, "_ignore_matches"):
             __IGNORE_FILE = ".taipyignore"