From 2c5c915fced9b48539f831135ec4c99254c9dd80 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Mon, 20 Jan 2025 15:55:06 +0700 Subject: [PATCH 01/24] TaipyRendered with updated webpack --- .../src/components/Taipy/TaipyRendered.tsx | 86 ++++++++++++++++++ frontend/taipy-gui/base/src/exports.ts | 3 + .../base/src/packaging/taipy-gui-base.d.ts | 88 +++++++++++++++++++ frontend/taipy-gui/base/webpack.config.js | 14 +++ frontend/taipy-gui/webpack.config.js | 14 +++ 5 files changed, 205 insertions(+) create mode 100644 frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx new file mode 100644 index 0000000000..5c4cacc08b --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2021-2025 Avaiga Private Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +import React, { ComponentType, useEffect, useReducer } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import JsxParser from "react-jsx-parser"; + +import { ThemeProvider } from "@mui/system"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3"; + +import { PageContext, TaipyContext } from "../../../../src/context/taipyContext"; +import { emptyArray } from "../../../../src/utils"; +import ErrorFallback from "../../../../src/utils/ErrorBoundary"; +import { getRegisteredComponents } from "../../../../src/components/Taipy"; +import { renderError, unregisteredRender } from "../../../../src/components/Taipy/Unregistered"; +import { + INITIAL_STATE, + initializeWebSocket, + taipyInitialize, + taipyReducer, +} from "../../../../src/context/taipyReducers"; + +interface PageState { + jsx?: string; + module?: string; +} + +interface TaipyRenderedProps { + pageState: PageState; +} + +const TaipyRendered = (props: TaipyRenderedProps) => { + const { pageState } = props; + const [state, dispatch] = useReducer(taipyReducer, INITIAL_STATE, taipyInitialize); + const themeClass = "taipy-" + state.theme.palette.mode; + + useEffect(() => { + initializeWebSocket(state.socket, dispatch); + }, [state.socket]); + + useEffect(() => { + const classes = [themeClass]; + document.body.classList.forEach((cls) => { + if (!cls.startsWith("taipy-")) { + classes.push(cls); + } + }); + document.body.className = classes.join(" "); + }, [themeClass]); + + return ( + + + + + + } + jsx={pageState.jsx} + renderUnrecognized={unregisteredRender} + allowUnknownElements={false} + renderError={renderError} + blacklistedAttrs={emptyArray} + /> + + + + + + ); +}; + +export default TaipyRendered; diff --git a/frontend/taipy-gui/base/src/exports.ts b/frontend/taipy-gui/base/src/exports.ts index ff1f70f524..8e65058951 100644 --- a/frontend/taipy-gui/base/src/exports.ts +++ b/frontend/taipy-gui/base/src/exports.ts @@ -1,7 +1,10 @@ import { TaipyApp, createApp, OnChangeHandler, OnInitHandler } from "./app"; import { WsAdapter } from "./wsAdapter"; import { ModuleData } from "./dataManager"; +import TaipyRendered from "./components/Taipy/TaipyRendered"; +import Slider from "../../src/components/Taipy/Slider"; export default TaipyApp; export { TaipyApp, createApp, WsAdapter }; +export { TaipyRendered, Slider }; export type { OnChangeHandler, OnInitHandler, ModuleData }; diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index be8ce809a3..77ef5604db 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -1,3 +1,4 @@ +import { ReactNode } from "react"; import { Socket } from "socket.io-client"; export type ModuleData = Record; @@ -186,5 +187,92 @@ export declare const createApp: ( socket?: Socket, handleCookie?: boolean, ) => TaipyApp; +export interface PageState { + jsx?: string; + module?: string; +} +export interface TaipyRenderedProps { + pageState: PageState; +} +export declare const TaipyRendered: (props: TaipyRenderedProps) => import("react/jsx-runtime").JSX.Element; +export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps { + defaultActive?: boolean; + active?: boolean; +} +export interface TaipyHoverProps { + hoverText?: string; + defaultHoverText?: string; +} +export interface TaipyDynamicProps extends TaipyBaseProps { + updateVarName?: string; + propagate?: boolean; + updateVars?: string; +} +export interface TaipyBaseProps { + id?: string; + libClassName?: string; + className?: string; + dynamicClassName?: string; + privateClassName?: string; + children?: ReactNode; +} +export interface TaipyChangeProps { + onChange?: string; +} +/** + * An Icon representation. + */ +export interface Icon { + /** The URL to the image. */ + path: string; + /** The text. */ + text: string; + /** light theme path */ + lightPath?: string; + /** dark theme path */ + darkPath?: string; +} +/** + * A string or an icon. + */ +export type stringIcon = string | Icon; +export interface LovProps extends TaipyActiveProps, TaipyChangeProps { + defaultLov?: string; + lov?: LoV; + value?: T; + defaultValue?: U; + height?: string | number; + valueById?: boolean; +} +/** + * A LoV (list of value) element. + * + * Each `LoVElt` holds: + * + * - Its identifier as a string; + * - Its label (or icon) as a `stringIcon`; + * - Potential child elements as an array of `LoVElt`s. + */ +export type LoVElt = [string, stringIcon, LoVElt[]?]; +/** + * A series of LoV elements. + */ +export type LoV = LoVElt[]; +export interface SliderProps + extends LovProps { + width?: string; + height?: string; + min?: number; + max?: number; + step?: number; + textAnchor?: string; + continuous?: boolean; + labels?: string | boolean; + orientation?: string; + changeDelay?: number; +} +export declare const Slider: (props: SliderProps) => import("react/jsx-runtime").JSX.Element; export { TaipyApp as default }; + +export {}; diff --git a/frontend/taipy-gui/base/webpack.config.js b/frontend/taipy-gui/base/webpack.config.js index c6ab51695c..ea376c752f 100644 --- a/frontend/taipy-gui/base/webpack.config.js +++ b/frontend/taipy-gui/base/webpack.config.js @@ -84,5 +84,19 @@ module.exports = [ patterns: [{ from: "./base/src/packaging", to: taipyGuiBaseExportPath }], }), ], + externals: { + "react": { + commonjs: "react", + commonjs2: "react", + amd: "react", + root: "_", + }, + "react-dom": { + commonjs: "react-dom", + commonjs2: "react-dom", + amd: "react-dom", + root: "_", + }, + }, }, ]; diff --git a/frontend/taipy-gui/webpack.config.js b/frontend/taipy-gui/webpack.config.js index cb5739799b..07f1b71bf9 100644 --- a/frontend/taipy-gui/webpack.config.js +++ b/frontend/taipy-gui/webpack.config.js @@ -248,5 +248,19 @@ module.exports = (env, options) => { ], }), ], + externals: { + "react": { + commonjs: "react", + commonjs2: "react", + amd: "react", + root: "_", + }, + "react-dom": { + commonjs: "react-dom", + commonjs2: "react-dom", + amd: "react-dom", + root: "_", + }, + }, }]; }; From ab6c09c5fe97c2fc157f8b9c9077ab7ad135500c Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Tue, 21 Jan 2025 14:38:15 +0700 Subject: [PATCH 02/24] add npm pack for gui base export --- frontend/taipy-gui/base/src/packaging/package.json | 6 +++++- frontend/taipy-gui/package.json | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/taipy-gui/base/src/packaging/package.json b/frontend/taipy-gui/base/src/packaging/package.json index a86270292e..d7aa1e031f 100644 --- a/frontend/taipy-gui/base/src/packaging/package.json +++ b/frontend/taipy-gui/base/src/packaging/package.json @@ -3,5 +3,9 @@ "version": "4.1.0", "private": true, "main": "./taipy-gui-base.js", - "types": "./taipy-gui-base.d.ts" + "types": "./taipy-gui-base.d.ts", + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } } diff --git a/frontend/taipy-gui/package.json b/frontend/taipy-gui/package.json index 9afc6ba29b..4536017fdf 100644 --- a/frontend/taipy-gui/package.json +++ b/frontend/taipy-gui/package.json @@ -40,8 +40,8 @@ "inst": "npm i", "start": "echo no start see python", "build:dev": "webpack --mode development", - "build": "webpack --mode production", - "build-base": "webpack --mode production --config ./base/webpack.config.js", + "build": "webpack --mode production && npm run pack-base", + "build-base": "webpack --mode production --config ./base/webpack.config.js && npm run pack-base", "test": "cross-env TZ=UTC jest", "lint": "eslint --ext .ts,.tsx", "lint:fix": "npm run lint -- --fix", @@ -50,7 +50,8 @@ "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" + "mkdocs": "typedoc --options typedoc-mkdocs.json", + "pack-base": "cd ../../taipy/gui/webapp/taipy-gui-base-export && npm pack && mv taipy-gui-base-4.1.0.tgz ../taipy-gui-base.tgz && cd ../ && rm -rf taipy-gui-base-export" }, "husky": { "hooks": { From 449931ce3f19eadbabe11cf93825c3232ee328b4 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Tue, 21 Jan 2025 21:07:49 +0700 Subject: [PATCH 03/24] add elementmanager --- frontend/taipy-gui/base/src/app.ts | 16 +++ .../src/components/Taipy/TaipyRendered.tsx | 20 ++- frontend/taipy-gui/base/src/exports.ts | 3 - .../base/src/packaging/taipy-gui-base.d.ts | 120 +++++------------- .../taipy-gui/base/src/renderer/canvas.ts | 27 ++++ .../base/src/renderer/elementManager.ts | 36 ++++++ .../base/src/renderer/elementRenderer.ts | 30 +++++ frontend/taipy-gui/base/src/store.ts | 17 +++ frontend/taipy-gui/package-lock.json | 31 ++++- frontend/taipy-gui/package.json | 3 +- 10 files changed, 199 insertions(+), 104 deletions(-) create mode 100644 frontend/taipy-gui/base/src/renderer/canvas.ts create mode 100644 frontend/taipy-gui/base/src/renderer/elementManager.ts create mode 100644 frontend/taipy-gui/base/src/renderer/elementRenderer.ts create mode 100644 frontend/taipy-gui/base/src/store.ts diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index 7053e1f9b6..eda5d18bfd 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -10,6 +10,7 @@ import { TaipyWsAdapter, WsAdapter } from "./wsAdapter"; import { WsMessageType } from "../../src/context/wsUtils"; import { getBase } from "./utils"; import { CookieHandler } from "./cookieHandler"; +import { Element, ElementManager } from "./renderer/elementManager"; export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; @@ -47,6 +48,7 @@ export class TaipyApp { path: string | undefined; routes: Route[] | undefined; wsAdapters: WsAdapter[]; + elementManager: ElementManager; constructor( onInit: OnInitHandler | undefined = undefined, @@ -68,6 +70,7 @@ export class TaipyApp { this.path = path; this.socket = socket; this.wsAdapters = [new TaipyWsAdapter()]; + this.elementManager = new ElementManager(this); this._ackList = []; this._rdc = {}; this._cookieHandler = handleCookie ? new CookieHandler() : undefined; @@ -295,6 +298,19 @@ export class TaipyApp { getBaseUrl() { return getBase(); } + + createCanvas(domElement: HTMLElement) { + this.elementManager.init(domElement); + } + + addElement2Canvas( + type: string, + bindingEncodedVarName: string | undefined = undefined, + wrapperHtml: [string, string] | undefined = undefined, + id: string | undefined = undefined, + ) { + this.elementManager.addElement({ id, type, bindingEncodedVarName, wrapperHtml }); + } } export const createApp = ( diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx index 5c4cacc08b..446af73692 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx @@ -11,7 +11,7 @@ * specific language governing permissions and limitations under the License. */ -import React, { ComponentType, useEffect, useReducer } from "react"; +import React, { ComponentType, useEffect, useMemo, useReducer } from "react"; import { ErrorBoundary } from "react-error-boundary"; import JsxParser from "react-jsx-parser"; @@ -30,18 +30,14 @@ import { taipyInitialize, taipyReducer, } from "../../../../src/context/taipyReducers"; +import useStore from "../../store"; -interface PageState { - jsx?: string; - module?: string; -} - -interface TaipyRenderedProps { - pageState: PageState; -} - -const TaipyRendered = (props: TaipyRenderedProps) => { - const { pageState } = props; +const TaipyRendered = () => { + const jsx = useStore((state) => state.jsx); + const module = useStore((state) => state.module); + const pageState = useMemo(() => { + return { jsx, module }; + }, [jsx, module]); const [state, dispatch] = useReducer(taipyReducer, INITIAL_STATE, taipyInitialize); const themeClass = "taipy-" + state.theme.palette.mode; diff --git a/frontend/taipy-gui/base/src/exports.ts b/frontend/taipy-gui/base/src/exports.ts index 8e65058951..ff1f70f524 100644 --- a/frontend/taipy-gui/base/src/exports.ts +++ b/frontend/taipy-gui/base/src/exports.ts @@ -1,10 +1,7 @@ import { TaipyApp, createApp, OnChangeHandler, OnInitHandler } from "./app"; import { WsAdapter } from "./wsAdapter"; import { ModuleData } from "./dataManager"; -import TaipyRendered from "./components/Taipy/TaipyRendered"; -import Slider from "../../src/components/Taipy/Slider"; export default TaipyApp; export { TaipyApp, createApp, WsAdapter }; -export { TaipyRendered, Slider }; export type { OnChangeHandler, OnInitHandler, ModuleData }; diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index 77ef5604db..61c865bbf0 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -1,4 +1,3 @@ -import { ReactNode } from "react"; import { Socket } from "socket.io-client"; export type ModuleData = Record; @@ -99,6 +98,30 @@ declare class CookieHandler { addBeforeUnloadListener(): void; deleteCookie(): Promise; } +declare class TaipyCanvas { + taipyApp: TaipyApp; + constructor(taipyApp: TaipyApp); + init(domElement: HTMLElement): void; + updateContent(jsx: string): void; +} +export interface TaipyRenderer { + render(elements: Element[]): string; +} +export interface Element { + id: string | undefined; + type: string; + bindingEncodedVarName: string | undefined; + wrapperHtml: [string, string] | undefined; +} +declare class ElementManager { + _elements: Element[]; + _renderer: TaipyRenderer; + _canvas: TaipyCanvas; + taipyApp: TaipyApp; + constructor(taipyApp: TaipyApp); + init(domElement: HTMLElement): void; + addElement(element: Element): void; +} export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void; @@ -132,6 +155,7 @@ export declare class TaipyApp { path: string | undefined; routes: Route[] | undefined; wsAdapters: WsAdapter[]; + elementManager: ElementManager; constructor( onInit?: OnInitHandler | undefined, onChange?: OnChangeHandler | undefined, @@ -159,7 +183,7 @@ export declare class TaipyApp { onWsStatusUpdateEvent(messageQueue: string[]): void; init(): void; initApp(): void; - sendWsMessage(type: WsMessageType | string, id: string, payload: unknown, context?: string | undefined): void; + sendWsMessage(type: WsMessageType | str, id: string, payload: unknown, context?: string | undefined): void; registerWsAdapter(wsAdapter: WsAdapter): void; getEncodedName(varName: string, module: string): string | undefined; getName(encodedName: string): [string, string] | undefined; @@ -179,6 +203,13 @@ export declare class TaipyApp { getPageMetadata(): Record; getWsStatus(): string[]; getBaseUrl(): string; + createCanvas(domElement: HTMLElement): void; + addElement2Canvas( + type: string, + bindingEncodedVarName?: string | undefined, + wrapperHtml?: [string, string] | undefined, + id?: string | undefined, + ): void; } export declare const createApp: ( onInit?: OnInitHandler, @@ -187,91 +218,6 @@ export declare const createApp: ( socket?: Socket, handleCookie?: boolean, ) => TaipyApp; -export interface PageState { - jsx?: string; - module?: string; -} -export interface TaipyRenderedProps { - pageState: PageState; -} -export declare const TaipyRendered: (props: TaipyRenderedProps) => import("react/jsx-runtime").JSX.Element; -export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps { - defaultActive?: boolean; - active?: boolean; -} -export interface TaipyHoverProps { - hoverText?: string; - defaultHoverText?: string; -} -export interface TaipyDynamicProps extends TaipyBaseProps { - updateVarName?: string; - propagate?: boolean; - updateVars?: string; -} -export interface TaipyBaseProps { - id?: string; - libClassName?: string; - className?: string; - dynamicClassName?: string; - privateClassName?: string; - children?: ReactNode; -} -export interface TaipyChangeProps { - onChange?: string; -} -/** - * An Icon representation. - */ -export interface Icon { - /** The URL to the image. */ - path: string; - /** The text. */ - text: string; - /** light theme path */ - lightPath?: string; - /** dark theme path */ - darkPath?: string; -} -/** - * A string or an icon. - */ -export type stringIcon = string | Icon; -export interface LovProps extends TaipyActiveProps, TaipyChangeProps { - defaultLov?: string; - lov?: LoV; - value?: T; - defaultValue?: U; - height?: string | number; - valueById?: boolean; -} -/** - * A LoV (list of value) element. - * - * Each `LoVElt` holds: - * - * - Its identifier as a string; - * - Its label (or icon) as a `stringIcon`; - * - Potential child elements as an array of `LoVElt`s. - */ -export type LoVElt = [string, stringIcon, LoVElt[]?]; -/** - * A series of LoV elements. - */ -export type LoV = LoVElt[]; -export interface SliderProps - extends LovProps { - width?: string; - height?: string; - min?: number; - max?: number; - step?: number; - textAnchor?: string; - continuous?: boolean; - labels?: string | boolean; - orientation?: string; - changeDelay?: number; -} -export declare const Slider: (props: SliderProps) => import("react/jsx-runtime").JSX.Element; export { TaipyApp as default }; diff --git a/frontend/taipy-gui/base/src/renderer/canvas.ts b/frontend/taipy-gui/base/src/renderer/canvas.ts new file mode 100644 index 0000000000..5e387985ad --- /dev/null +++ b/frontend/taipy-gui/base/src/renderer/canvas.ts @@ -0,0 +1,27 @@ +import { TaipyApp } from "../app"; +import { createRoot } from "react-dom/client"; +import useStore from "../store"; +import { createElement } from "react"; +import TaipyRendered from "../components/Taipy/TaipyRendered"; + +export class TaipyCanvas { + taipyApp: TaipyApp; + + constructor(taipyApp: TaipyApp) { + this.taipyApp = taipyApp; + } + + init(domElement: HTMLElement) { + if (domElement) { + const root = createRoot(domElement); + root.render(createElement(TaipyRendered)); + useStore.getState().setModule(this.taipyApp.getContext()); + } else { + console.error("Root element not found!"); + } + } + + updateContent(jsx: string) { + useStore.getState().setJsx(jsx); + } +} diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/renderer/elementManager.ts new file mode 100644 index 0000000000..688de33b57 --- /dev/null +++ b/frontend/taipy-gui/base/src/renderer/elementManager.ts @@ -0,0 +1,36 @@ +import { TaipyApp } from "../app"; +import { TaipyCanvas } from "./canvas"; +import { FrontendRenderer, TaipyRenderer } from "./elementRenderer"; + +export interface Element { + id: string | undefined; + type: string; + bindingEncodedVarName: string | undefined; + wrapperHtml: [string, string] | undefined; +} + +export class ElementManager { + _elements: Element[]; + _renderer: TaipyRenderer; + _canvas: TaipyCanvas; + taipyApp: TaipyApp; + + constructor(taipyApp: TaipyApp) { + this.taipyApp = taipyApp; + this._elements = []; + this._renderer = new FrontendRenderer(taipyApp); + this._canvas = new TaipyCanvas(taipyApp); + } + + init(domElement: HTMLElement) { + this._canvas.init(domElement); + } + + addElement(element: Element) { + if (element.id === undefined) { + element.id = Math.random().toString(36).substring(10); + } + this._elements.push(element); + this._canvas.updateContent(this._renderer.render(this._elements)); + } +} diff --git a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts new file mode 100644 index 0000000000..3c576fcf9e --- /dev/null +++ b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts @@ -0,0 +1,30 @@ +import { TaipyApp } from "../app"; +import { Element } from "./elementManager"; + +export interface TaipyRenderer { + render(elements: Element[]): string; +} + +export class FrontendRenderer implements TaipyRenderer { + taipyApp: TaipyApp; + + constructor(taipyApp: TaipyApp) { + this.taipyApp = taipyApp; + } + + render(elements: Element[]): string { + const elList: string[] = []; + for (const e of elements) { + const defaultValue = e.bindingEncodedVarName + ? this.taipyApp?.variableData?.get(e.bindingEncodedVarName) + : undefined; + const bindingVarStr = defaultValue + ? ` defaultValue={"${defaultValue}"} updateVarName="${e.bindingEncodedVarName}" value={${e.bindingEncodedVarName}}` + : ""; + elList.push( + `${e.wrapperHtml && e.wrapperHtml[0]}<${e.type} key="${e.id}" ${bindingVarStr} />${e.wrapperHtml && e.wrapperHtml[1]}`, + ); + } + return elList.join("\n"); + } +} diff --git a/frontend/taipy-gui/base/src/store.ts b/frontend/taipy-gui/base/src/store.ts new file mode 100644 index 0000000000..2710d351db --- /dev/null +++ b/frontend/taipy-gui/base/src/store.ts @@ -0,0 +1,17 @@ +import { create } from "zustand"; + +interface TaipyGuiBaseState { + jsx: string; + module: string; + setJsx: (newJsx: string) => void; + setModule: (newModule: string) => void; +} + +const useStore = create()((set) => ({ + jsx: "", + module: "", + setJsx: (newJsx: string) => set(() => ({ jsx: newJsx })), + setModule: (newModule: string) => set(() => ({ module: newModule })), +})); + +export default useStore; diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json index 54e7c0c8a3..e703ffbc9f 100644 --- a/frontend/taipy-gui/package-lock.json +++ b/frontend/taipy-gui/package-lock.json @@ -35,7 +35,8 @@ "react-window": "^1.8.6", "react-window-infinite-loader": "^1.0.7", "socket.io-client": "^4.3.2", - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.2", + "zustand": "^5.0.3" }, "devDependencies": { "@testing-library/jest-dom": "^6.1.3", @@ -14676,6 +14677,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/taipy-gui/package.json b/frontend/taipy-gui/package.json index 4536017fdf..19856fea75 100644 --- a/frontend/taipy-gui/package.json +++ b/frontend/taipy-gui/package.json @@ -30,7 +30,8 @@ "react-window": "^1.8.6", "react-window-infinite-loader": "^1.0.7", "socket.io-client": "^4.3.2", - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.2", + "zustand": "^5.0.3" }, "overrides": { "react": "$react", From 81c0ab09218e57d55db5f36d0f4bb0e67025af28 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Tue, 21 Jan 2025 21:44:42 +0700 Subject: [PATCH 04/24] update string type --- frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index 61c865bbf0..037b719822 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -183,7 +183,7 @@ export declare class TaipyApp { onWsStatusUpdateEvent(messageQueue: string[]): void; init(): void; initApp(): void; - sendWsMessage(type: WsMessageType | str, id: string, payload: unknown, context?: string | undefined): void; + sendWsMessage(type: WsMessageType | string, id: string, payload: unknown, context?: string | undefined): void; registerWsAdapter(wsAdapter: WsAdapter): void; getEncodedName(varName: string, module: string): string | undefined; getName(encodedName: string): [string, string] | undefined; From 789640fcb46bacaf6bd340e128da56acbb81d83c Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Wed, 22 Jan 2025 16:12:49 +0700 Subject: [PATCH 05/24] per Fred --- .../base/src/renderer/elementManager.ts | 3 ++- .../base/src/renderer/elementRenderer.ts | 24 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/renderer/elementManager.ts index 688de33b57..55fa57aee8 100644 --- a/frontend/taipy-gui/base/src/renderer/elementManager.ts +++ b/frontend/taipy-gui/base/src/renderer/elementManager.ts @@ -1,3 +1,4 @@ +import { nanoid } from "nanoid"; import { TaipyApp } from "../app"; import { TaipyCanvas } from "./canvas"; import { FrontendRenderer, TaipyRenderer } from "./elementRenderer"; @@ -28,7 +29,7 @@ export class ElementManager { addElement(element: Element) { if (element.id === undefined) { - element.id = Math.random().toString(36).substring(10); + element.id = nanoid(10); } this._elements.push(element); this._canvas.updateContent(this._renderer.render(this._elements)); diff --git a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts index 3c576fcf9e..e56f25e524 100644 --- a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts +++ b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts @@ -13,18 +13,16 @@ export class FrontendRenderer implements TaipyRenderer { } render(elements: Element[]): string { - const elList: string[] = []; - for (const e of elements) { - const defaultValue = e.bindingEncodedVarName - ? this.taipyApp?.variableData?.get(e.bindingEncodedVarName) - : undefined; - const bindingVarStr = defaultValue - ? ` defaultValue={"${defaultValue}"} updateVarName="${e.bindingEncodedVarName}" value={${e.bindingEncodedVarName}}` - : ""; - elList.push( - `${e.wrapperHtml && e.wrapperHtml[0]}<${e.type} key="${e.id}" ${bindingVarStr} />${e.wrapperHtml && e.wrapperHtml[1]}`, - ); - } - return elList.join("\n"); + return elements + .map((e) => { + const defaultValue = e.bindingEncodedVarName + ? this.taipyApp?.variableData?.get(e.bindingEncodedVarName) + : undefined; + const bindingVarStr = defaultValue + ? ` defaultValue={"${defaultValue}"} updateVarName="${e.bindingEncodedVarName}" value={${e.bindingEncodedVarName}}` + : ""; + return `${e.wrapperHtml && e.wrapperHtml[0]}<${e.type} key="${e.id}" ${bindingVarStr} />${e.wrapperHtml && e.wrapperHtml[1]}`; + }) + .join("/n"); } } From ff7985f27104e8a275c2116de286b99932b6c6db Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Fri, 24 Jan 2025 16:09:00 +0700 Subject: [PATCH 06/24] render from backend --- frontend/taipy-gui/base/src/app.ts | 6 +-- .../base/src/renderer/elementManager.ts | 15 ++++--- .../base/src/renderer/elementRenderer.ts | 45 ++++++++++++------- taipy/gui/gui.py | 19 +++++++- taipy/gui/server.py | 11 +++-- 5 files changed, 65 insertions(+), 31 deletions(-) diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index eda5d18bfd..3b694d0d57 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -10,7 +10,7 @@ import { TaipyWsAdapter, WsAdapter } from "./wsAdapter"; import { WsMessageType } from "../../src/context/wsUtils"; import { getBase } from "./utils"; import { CookieHandler } from "./cookieHandler"; -import { Element, ElementManager } from "./renderer/elementManager"; +import { ElementManager } from "./renderer/elementManager"; export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; @@ -305,11 +305,11 @@ export class TaipyApp { addElement2Canvas( type: string, - bindingEncodedVarName: string | undefined = undefined, + properties: Record | undefined = undefined, wrapperHtml: [string, string] | undefined = undefined, id: string | undefined = undefined, ) { - this.elementManager.addElement({ id, type, bindingEncodedVarName, wrapperHtml }); + this.elementManager.addElement({ id, type, properties, wrapperHtml }); } } diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/renderer/elementManager.ts index 55fa57aee8..21de167455 100644 --- a/frontend/taipy-gui/base/src/renderer/elementManager.ts +++ b/frontend/taipy-gui/base/src/renderer/elementManager.ts @@ -1,25 +1,26 @@ import { nanoid } from "nanoid"; import { TaipyApp } from "../app"; import { TaipyCanvas } from "./canvas"; -import { FrontendRenderer, TaipyRenderer } from "./elementRenderer"; +import { ElementRenderer } from "./elementRenderer"; export interface Element { - id: string | undefined; type: string; - bindingEncodedVarName: string | undefined; - wrapperHtml: [string, string] | undefined; + id?: string; + properties?: Record; + wrapperHtml?: [string, string]; + jsx?: string; } export class ElementManager { _elements: Element[]; - _renderer: TaipyRenderer; + _renderer: ElementRenderer; _canvas: TaipyCanvas; taipyApp: TaipyApp; constructor(taipyApp: TaipyApp) { this.taipyApp = taipyApp; this._elements = []; - this._renderer = new FrontendRenderer(taipyApp); + this._renderer = new ElementRenderer(taipyApp); this._canvas = new TaipyCanvas(taipyApp); } @@ -32,6 +33,6 @@ export class ElementManager { element.id = nanoid(10); } this._elements.push(element); - this._canvas.updateContent(this._renderer.render(this._elements)); + this._renderer.render(this._elements).then((jsx) => this._canvas.updateContent(jsx)); } } diff --git a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts index e56f25e524..e57a8acd7f 100644 --- a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts +++ b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts @@ -1,28 +1,39 @@ import { TaipyApp } from "../app"; import { Element } from "./elementManager"; +import axios from "axios"; -export interface TaipyRenderer { - render(elements: Element[]): string; -} - -export class FrontendRenderer implements TaipyRenderer { +export class ElementRenderer { taipyApp: TaipyApp; constructor(taipyApp: TaipyApp) { this.taipyApp = taipyApp; } - render(elements: Element[]): string { - return elements - .map((e) => { - const defaultValue = e.bindingEncodedVarName - ? this.taipyApp?.variableData?.get(e.bindingEncodedVarName) - : undefined; - const bindingVarStr = defaultValue - ? ` defaultValue={"${defaultValue}"} updateVarName="${e.bindingEncodedVarName}" value={${e.bindingEncodedVarName}}` - : ""; - return `${e.wrapperHtml && e.wrapperHtml[0]}<${e.type} key="${e.id}" ${bindingVarStr} />${e.wrapperHtml && e.wrapperHtml[1]}`; - }) - .join("/n"); + async render(elements: Element[], force: boolean = false): Promise { + const renderedElements = await Promise.all( + elements.map(async (element) => { + if (force || !element.jsx) { + element.jsx = await this.renderSingle(element); + } + return `${element.wrapperHtml?.[0] || ""}${element.jsx}${element.wrapperHtml?.[1] || ""}`; + }), + ); + return renderedElements.join("\n"); + } + + async renderSingle(element: Element): Promise { + try { + const result = await axios.post<{ jsx: string }>( + `${this.taipyApp.getBaseUrl()}taipy-element-jsx?client_id=${this.taipyApp.clientId}`, + { + type: element.type, + properties: { ...element.properties, id: element.id }, + context: this.taipyApp.getContext(), + }, + ); + return result.data.jsx; + } catch (error) { + throw new Error(`Failed to render element '${element.type} - ${element.id}': ${error}`); + } } } diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index 9d11e1ee08..409cd558bc 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -2579,7 +2579,11 @@ def __render_page(self, page_name: str) -> t.Any: with self._set_locals_context(context): self._call_on_page_load(nav_page) return self._server._render( - page._rendered_jsx, page._script_paths if page._script_paths is not None else [], page._style if page._style is not None else "", page._head, context # noqa: E501 + page._rendered_jsx, + page._script_paths if page._script_paths is not None else [], + page._style if page._style is not None else "", + page._head, + context, # noqa: E501 ) else: return ("No page template", 404) @@ -2594,6 +2598,17 @@ def _render_route(self) -> t.Any: } ) + def __render_element(self) -> t.Any: + self.__set_client_id_in_context() + data = request.get_json() + if not data: + return jsonify({"error": "No element data provided"}), 400 + with self._set_locals_context(data.get("context")): + el = _Factory.call_builder(self, data.get("type"), data.get("properties"), True) + if el is None: + return jsonify({"error": f"Failed to generate element of type '{data.type}'"}), 400 + return {"jsx": _Server._render_jsx_fragment(f"{el[0]}")} + def get_flask_app(self) -> Flask: """Get the internal Flask application. @@ -2810,6 +2825,8 @@ def __register_blueprint(self): # server URL Rule for flask rendered react-router pages_bp.add_url_rule(f"/{Gui.__INIT_URL}", view_func=self.__init_route) + pages_bp.add_url_rule("/taipy-element-jsx", view_func=self.__render_element, methods=["POST"]) + _Hooks()._add_external_blueprint(self, __name__) # Register Flask Blueprint if available diff --git a/taipy/gui/server.py b/taipy/gui/server.py index df13e34048..65237830ac 100644 --- a/taipy/gui/server.py +++ b/taipy/gui/server.py @@ -239,12 +239,17 @@ def my_index(path): return taipy_bp - # Update to render as JSX - def _render(self, html_fragment, script_paths, style, head, context): + @staticmethod + def _render_jsx_fragment(html_fragment): template_str = _Server.__RE_OPENING_CURLY.sub(_Server.__OPENING_CURLY, html_fragment) template_str = _Server.__RE_CLOSING_CURLY.sub(_Server.__CLOSING_CURLY, template_str) template_str = template_str.replace('"{!', "{") template_str = template_str.replace('!}"', "}") + return template_str + + # Update to render as JSX + def _render(self, html_fragment, script_paths, style, head, context): + template_str = _Server._render_jsx_fragment(html_fragment) style = get_style(style) return self._direct_render_json( { @@ -252,7 +257,7 @@ def _render(self, html_fragment, script_paths, style, head, context): "style": (style + os.linesep) if style else "", "head": head or [], "context": context or self._gui._get_default_module_name(), - "scriptPaths": script_paths + "scriptPaths": script_paths, } ) From d669466f6e1951399f00ac40f16b77480e349c69 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Wed, 5 Feb 2025 14:40:51 +0700 Subject: [PATCH 07/24] add callback on re render --- frontend/taipy-gui/base/src/app.ts | 17 ++++++++++++++ .../src/components/Taipy/TaipyRendered.tsx | 6 +++++ .../base/src/packaging/taipy-gui-base.d.ts | 23 +++++++++++++------ .../taipy-gui/base/src/renderer/canvas.ts | 1 + frontend/taipy-gui/base/src/store.ts | 5 ++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index 3b694d0d57..c0cb9ac03c 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -18,6 +18,7 @@ export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void; export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void; export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void; +export type OnCanvasReRender = (taipyApp: TaipyApp) => void; export type OnEvent = | OnInitHandler | OnChangeHandler @@ -36,6 +37,7 @@ export class TaipyApp { _onReload: OnReloadHandler | undefined; _onWsMessage: OnWsMessage | undefined; _onWsStatusUpdate: OnWsStatusUpdate | undefined; + _onCanvasReRender: OnCanvasReRender | undefined; _ackList: string[]; _rdc: Record>; _cookieHandler: CookieHandler | undefined; @@ -167,6 +169,21 @@ export class TaipyApp { this.onWsStatusUpdate && this.onWsStatusUpdate(this, messageQueue); } + get onCanvasReRender() { + return this._onCanvasReRender; + } + + set onCanvasReRender(handler: OnCanvasReRender | undefined) { + if (handler !== undefined && handler?.length !== 1) { + throw new Error("onCanvasReRender() requires one parameter"); + } + this._onCanvasReRender = handler; + } + + onCanvasReRenderEvent() { + this.onCanvasReRender && this.onCanvasReRender(this); + } + // Utility methods init() { this.clientId = ""; diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx index 446af73692..9d58f7ba07 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx @@ -35,6 +35,7 @@ import useStore from "../../store"; const TaipyRendered = () => { const jsx = useStore((state) => state.jsx); const module = useStore((state) => state.module); + const app = useStore((state) => state.app); const pageState = useMemo(() => { return { jsx, module }; }, [jsx, module]); @@ -55,6 +56,10 @@ const TaipyRendered = () => { document.body.className = classes.join(" "); }, [themeClass]); + useEffect(() => { + app && app.onCanvasReRenderEvent(); + }, [jsx, app]); + return ( @@ -70,6 +75,7 @@ const TaipyRendered = () => { allowUnknownElements={false} renderError={renderError} blacklistedAttrs={emptyArray} + renderInWrapper={false} /> diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index 037b719822..0eabd4d54f 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -104,18 +104,22 @@ declare class TaipyCanvas { init(domElement: HTMLElement): void; updateContent(jsx: string): void; } -export interface TaipyRenderer { - render(elements: Element[]): string; +declare class ElementRenderer { + taipyApp: TaipyApp; + constructor(taipyApp: TaipyApp); + render(elements: Element[], force?: boolean): Promise; + renderSingle(element: Element): Promise; } export interface Element { - id: string | undefined; type: string; - bindingEncodedVarName: string | undefined; - wrapperHtml: [string, string] | undefined; + id?: string; + properties?: Record; + wrapperHtml?: [string, string]; + jsx?: string; } declare class ElementManager { _elements: Element[]; - _renderer: TaipyRenderer; + _renderer: ElementRenderer; _canvas: TaipyCanvas; taipyApp: TaipyApp; constructor(taipyApp: TaipyApp); @@ -128,6 +132,7 @@ export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void; export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void; export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void; +export type OnCanvasReRender = (taipyApp: TaipyApp) => void; export type Route = [string, string]; export type RequestDataCallback = ( taipyApp: TaipyApp, @@ -143,6 +148,7 @@ export declare class TaipyApp { _onReload: OnReloadHandler | undefined; _onWsMessage: OnWsMessage | undefined; _onWsStatusUpdate: OnWsStatusUpdate | undefined; + _onCanvasReRender: OnCanvasReRender | undefined; _ackList: string[]; _rdc: Record>; _cookieHandler: CookieHandler | undefined; @@ -181,6 +187,9 @@ export declare class TaipyApp { get onWsStatusUpdate(): OnWsStatusUpdate | undefined; set onWsStatusUpdate(handler: OnWsStatusUpdate | undefined); onWsStatusUpdateEvent(messageQueue: string[]): void; + get onCanvasReRender(): OnCanvasReRender | undefined; + set onCanvasReRender(handler: OnCanvasReRender | undefined); + onCanvasReRenderEvent(): void; init(): void; initApp(): void; sendWsMessage(type: WsMessageType | string, id: string, payload: unknown, context?: string | undefined): void; @@ -206,7 +215,7 @@ export declare class TaipyApp { createCanvas(domElement: HTMLElement): void; addElement2Canvas( type: string, - bindingEncodedVarName?: string | undefined, + properties?: Record | undefined, wrapperHtml?: [string, string] | undefined, id?: string | undefined, ): void; diff --git a/frontend/taipy-gui/base/src/renderer/canvas.ts b/frontend/taipy-gui/base/src/renderer/canvas.ts index 5e387985ad..055f4b49e1 100644 --- a/frontend/taipy-gui/base/src/renderer/canvas.ts +++ b/frontend/taipy-gui/base/src/renderer/canvas.ts @@ -15,6 +15,7 @@ export class TaipyCanvas { if (domElement) { const root = createRoot(domElement); root.render(createElement(TaipyRendered)); + useStore.getState().setApp(this.taipyApp); useStore.getState().setModule(this.taipyApp.getContext()); } else { console.error("Root element not found!"); diff --git a/frontend/taipy-gui/base/src/store.ts b/frontend/taipy-gui/base/src/store.ts index 2710d351db..b85bbaec64 100644 --- a/frontend/taipy-gui/base/src/store.ts +++ b/frontend/taipy-gui/base/src/store.ts @@ -1,17 +1,22 @@ import { create } from "zustand"; +import { TaipyApp } from "./app"; interface TaipyGuiBaseState { jsx: string; module: string; + app?: TaipyApp; setJsx: (newJsx: string) => void; setModule: (newModule: string) => void; + setApp: (newApp: TaipyApp) => void; } const useStore = create()((set) => ({ jsx: "", module: "", + app: undefined, setJsx: (newJsx: string) => set(() => ({ jsx: newJsx })), setModule: (newModule: string) => set(() => ({ module: newModule })), + setApp: (newApp: TaipyApp) => set(() => ({ app: newApp })), })); export default useStore; From ec8b99f09b6611d14e07688a6cee7634a592e2f0 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Tue, 18 Feb 2025 15:21:05 +0700 Subject: [PATCH 08/24] both edit + view mode working (still some issues on resizing + mode switch rendering on edit mode) --- frontend/taipy-gui/base/src/app.ts | 28 ++++++--- .../src/components/Taipy/TaipyRendered.tsx | 9 ++- .../taipy-gui/base/src/renderer/canvas.ts | 62 +++++++++++++++++-- .../base/src/renderer/elementManager.ts | 51 ++++++++++++++- .../base/src/renderer/elementRenderer.ts | 15 +++-- frontend/taipy-gui/base/src/store.ts | 8 +++ 6 files changed, 149 insertions(+), 24 deletions(-) diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index c0cb9ac03c..62ea4f7139 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -11,6 +11,7 @@ import { WsMessageType } from "../../src/context/wsUtils"; import { getBase } from "./utils"; import { CookieHandler } from "./cookieHandler"; import { ElementManager } from "./renderer/elementManager"; +import useStore from "./store"; export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; @@ -18,7 +19,7 @@ export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void; export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void; export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void; -export type OnCanvasReRender = (taipyApp: TaipyApp) => void; +export type OnCanvasReRender = (taipyApp: TaipyApp, isEditMode: boolean) => void; export type OnEvent = | OnInitHandler | OnChangeHandler @@ -174,14 +175,14 @@ export class TaipyApp { } set onCanvasReRender(handler: OnCanvasReRender | undefined) { - if (handler !== undefined && handler?.length !== 1) { - throw new Error("onCanvasReRender() requires one parameter"); + if (handler !== undefined && handler?.length !== 2) { + throw new Error("onCanvasReRender() requires two parameter"); } this._onCanvasReRender = handler; } onCanvasReRenderEvent() { - this.onCanvasReRender && this.onCanvasReRender(this); + this.onCanvasReRender && this.onCanvasReRender(this, useStore.getState().editMode); } // Utility methods @@ -316,17 +317,30 @@ export class TaipyApp { return getBase(); } - createCanvas(domElement: HTMLElement) { - this.elementManager.init(domElement); + createCanvas(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement) { + this.elementManager.init(canvasDomElement, canvasEditModeCanvas); } addElement2Canvas( type: string, properties: Record | undefined = undefined, wrapperHtml: [string, string] | undefined = undefined, + wrapperHtmlEditMode: [string, string] | undefined = undefined, id: string | undefined = undefined, ) { - this.elementManager.addElement({ id, type, properties, wrapperHtml }); + this.elementManager.addElement({ id, type, properties, wrapperHtml, wrapperHtmlEditMode }); + } + + setCanvasEditMode(bool: boolean) { + this.elementManager.setEditMode(bool); + } + + modifyElement(id: string, elemenetProperties: Record) { + this.elementManager.modifyElement(id, elemenetProperties); + } + + deleteElement(id: string) { + this.elementManager.deleteElement(id); } } diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx index 9d58f7ba07..9771cf7939 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx @@ -32,8 +32,13 @@ import { } from "../../../../src/context/taipyReducers"; import useStore from "../../store"; -const TaipyRendered = () => { - const jsx = useStore((state) => state.jsx); +interface TaipyRenderedProps { + editMode?: boolean; +} + +const TaipyRendered = (props: TaipyRenderedProps) => { + const { editMode } = props; + const jsx = useStore((state) => (editMode ? state.editModeJsx : state.jsx)); const module = useStore((state) => state.module); const app = useStore((state) => state.app); const pageState = useMemo(() => { diff --git a/frontend/taipy-gui/base/src/renderer/canvas.ts b/frontend/taipy-gui/base/src/renderer/canvas.ts index 055f4b49e1..e1b3a6b842 100644 --- a/frontend/taipy-gui/base/src/renderer/canvas.ts +++ b/frontend/taipy-gui/base/src/renderer/canvas.ts @@ -1,28 +1,78 @@ import { TaipyApp } from "../app"; -import { createRoot } from "react-dom/client"; +import { createRoot, Root } from "react-dom/client"; import useStore from "../store"; import { createElement } from "react"; import TaipyRendered from "../components/Taipy/TaipyRendered"; export class TaipyCanvas { taipyApp: TaipyApp; + #root?: Root; + #editModeRoot?: Root; + #canvasElement?: HTMLElement; + #canvasEditModeElement?: HTMLElement; constructor(taipyApp: TaipyApp) { this.taipyApp = taipyApp; } - init(domElement: HTMLElement) { - if (domElement) { - const root = createRoot(domElement); - root.render(createElement(TaipyRendered)); + init(canvasElement: HTMLElement, canvasEditModeElement?: HTMLElement) { + if (canvasElement) { + this.initCanvas(canvasElement, false); useStore.getState().setApp(this.taipyApp); useStore.getState().setModule(this.taipyApp.getContext()); + if (canvasEditModeElement) { + this.initCanvas(canvasEditModeElement, true); + useStore.getState().setEditMode(true); + } } else { console.error("Root element not found!"); } } - updateContent(jsx: string) { + initCanvas(canvasElement: HTMLElement, editMode: boolean) { + if (editMode) { + this.#canvasEditModeElement = canvasElement; + this.#editModeRoot = createRoot(this.#canvasEditModeElement); + this.#editModeRoot.render(createElement(TaipyRendered, { editMode })); + return; + } + this.#canvasElement = canvasElement; + this.#root = createRoot(this.#canvasElement); + this.#root.render(createElement(TaipyRendered, { editMode })); + } + + resetCanvas(editMode: boolean) { + if (editMode && this.#editModeRoot) { + this.#editModeRoot.unmount(); + // remove all elements from canvas + while (this.#canvasEditModeElement?.firstChild) { + this.#canvasEditModeElement.removeChild(this.#canvasEditModeElement.firstChild); + } + useStore.getState().setEditModeJsx(""); + this.#editModeRoot.render(createElement(TaipyRendered, { editMode })); + return; + } + if (!editMode && this.#root && this.#canvasElement) { + // remove all elements from canvas + try { + this.#root.unmount(); + } catch (error) { + console.error("Error while unmounting root canvas", error); + } + while (this.#canvasElement?.firstChild) { + this.#canvasElement.removeChild(this.#canvasElement.firstChild); + } + useStore.getState().setJsx(""); + this.#root = createRoot(this.#canvasElement); + this.#root.render(createElement(TaipyRendered, { editMode })); + } + } + + updateContent(jsx: string, editMode: boolean) { + if (editMode) { + useStore.getState().setEditModeJsx(jsx); + return; + } useStore.getState().setJsx(jsx); } } diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/renderer/elementManager.ts index 21de167455..8a0a2353ae 100644 --- a/frontend/taipy-gui/base/src/renderer/elementManager.ts +++ b/frontend/taipy-gui/base/src/renderer/elementManager.ts @@ -2,13 +2,16 @@ import { nanoid } from "nanoid"; import { TaipyApp } from "../app"; import { TaipyCanvas } from "./canvas"; import { ElementRenderer } from "./elementRenderer"; +import useStore from "../store"; export interface Element { type: string; id?: string; properties?: Record; wrapperHtml?: [string, string]; + wrapperHtmlEditMode?: [string, string]; jsx?: string; + editModeJsx?: string; } export class ElementManager { @@ -24,15 +27,57 @@ export class ElementManager { this._canvas = new TaipyCanvas(taipyApp); } - init(domElement: HTMLElement) { - this._canvas.init(domElement); + init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement) { + this._canvas.init(canvasDomElement, canvasEditModeCanvas); + } + + setEditMode(editMode: boolean) { + console.log("Setting edit mode to", editMode); + const currentEditMode = useStore.getState().editMode; + if (currentEditMode === editMode) { + return; + } + useStore.getState().setEditMode(editMode); + // Reset view mode canvas if switched back to edit mode + if (editMode) { + this._canvas.resetCanvas(false); + } + // if in view mode -> render it because chalkit will not be rendering it + if (!editMode) { + this.render(true); + } + } + + render(force: boolean = false) { + const { editMode } = useStore.getState(); + this._renderer.render(this._elements, editMode, force).then((jsx) => this._canvas.updateContent(jsx, editMode)); } addElement(element: Element) { if (element.id === undefined) { element.id = nanoid(10); } + // check if element already exists based on id + if (this._elements.find((el) => el.id === element.id)) { + return; + } this._elements.push(element); - this._renderer.render(this._elements).then((jsx) => this._canvas.updateContent(jsx)); + this.render(); + } + + modifyElement(id: string, elementProperties: Record) { + this._elements = this._elements.map((el) => { + if (el.id !== id) { + return el; + } + const properties = { ...el.properties, ...elementProperties }; + return { ...el, properties, jsx: "", editModeJsx: "" }; + }); + this.render(); + } + + deleteElement(id: string) { + this._elements = this._elements.filter((el) => el.id !== id); + this.render(); } } diff --git a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts index e57a8acd7f..27e1ff0ccb 100644 --- a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts +++ b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts @@ -9,25 +9,28 @@ export class ElementRenderer { this.taipyApp = taipyApp; } - async render(elements: Element[], force: boolean = false): Promise { + async render(elements: Element[], editMode: boolean, force: boolean = false): Promise { const renderedElements = await Promise.all( elements.map(async (element) => { - if (force || !element.jsx) { - element.jsx = await this.renderSingle(element); + const jsxMode = editMode ? "editModeJsx" : "jsx"; + const wrapperHtml = editMode ? element.wrapperHtmlEditMode : element.wrapperHtml; + if (force || !element[jsxMode]) { + element[jsxMode] = await this.renderSingle(element, editMode); } - return `${element.wrapperHtml?.[0] || ""}${element.jsx}${element.wrapperHtml?.[1] || ""}`; + return `${wrapperHtml?.[0] || ""}${element[jsxMode]}${wrapperHtml?.[1] || ""}`; }), ); return renderedElements.join("\n"); } - async renderSingle(element: Element): Promise { + async renderSingle(element: Element, editMode: boolean): Promise { try { + const id = element.id + "-el" + (editMode ? "" : "-active"); const result = await axios.post<{ jsx: string }>( `${this.taipyApp.getBaseUrl()}taipy-element-jsx?client_id=${this.taipyApp.clientId}`, { type: element.type, - properties: { ...element.properties, id: element.id }, + properties: { ...element.properties, id, active: !editMode }, context: this.taipyApp.getContext(), }, ); diff --git a/frontend/taipy-gui/base/src/store.ts b/frontend/taipy-gui/base/src/store.ts index b85bbaec64..ef9504da2c 100644 --- a/frontend/taipy-gui/base/src/store.ts +++ b/frontend/taipy-gui/base/src/store.ts @@ -2,19 +2,27 @@ import { create } from "zustand"; import { TaipyApp } from "./app"; interface TaipyGuiBaseState { + editMode: boolean; jsx: string; + editModeJsx: string; module: string; app?: TaipyApp; + setEditMode: (newEditMode: boolean) => void; setJsx: (newJsx: string) => void; + setEditModeJsx: (newJsx: string) => void; setModule: (newModule: string) => void; setApp: (newApp: TaipyApp) => void; } const useStore = create()((set) => ({ + editMode: false, jsx: "", + editModeJsx: "", module: "", app: undefined, + setEditMode: (newEditMode: boolean) => set(() => ({ editMode: newEditMode })), setJsx: (newJsx: string) => set(() => ({ jsx: newJsx })), + setEditModeJsx: (newJsx: string) => set(() => ({ editModeJsx: newJsx })), setModule: (newModule: string) => set(() => ({ module: newModule })), setApp: (newApp: TaipyApp) => set(() => ({ app: newApp })), })); From 3f6da5c48c0a33895821f61bda1b0b594cd50575 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Wed, 19 Feb 2025 17:17:45 +0700 Subject: [PATCH 09/24] add elementaction callback --- frontend/taipy-gui/base/src/app.ts | 11 +++++----- .../base/src/renderer/elementManager.ts | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index 62ea4f7139..8ffaf12213 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -10,7 +10,7 @@ import { TaipyWsAdapter, WsAdapter } from "./wsAdapter"; import { WsMessageType } from "../../src/context/wsUtils"; import { getBase } from "./utils"; import { CookieHandler } from "./cookieHandler"; -import { ElementManager } from "./renderer/elementManager"; +import { ElementAction, ElementManager } from "./renderer/elementManager"; import useStore from "./store"; export type OnInitHandler = (taipyApp: TaipyApp) => void; @@ -19,7 +19,7 @@ export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void; export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void; export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void; -export type OnCanvasReRender = (taipyApp: TaipyApp, isEditMode: boolean) => void; +export type OnCanvasReRender = (taipyApp: TaipyApp, isEditMode: boolean, elementActions?: ElementAction) => void; export type OnEvent = | OnInitHandler | OnChangeHandler @@ -175,14 +175,15 @@ export class TaipyApp { } set onCanvasReRender(handler: OnCanvasReRender | undefined) { - if (handler !== undefined && handler?.length !== 2) { - throw new Error("onCanvasReRender() requires two parameter"); + if (handler !== undefined && handler?.length !== 3) { + throw new Error("onCanvasReRender() requires three parameter"); } this._onCanvasReRender = handler; } onCanvasReRenderEvent() { - this.onCanvasReRender && this.onCanvasReRender(this, useStore.getState().editMode); + this.onCanvasReRender && + this.onCanvasReRender(this, useStore.getState().editMode, this.elementManager.getElementActionFromQueue()); } // Utility methods diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/renderer/elementManager.ts index 8a0a2353ae..dbfce0adf5 100644 --- a/frontend/taipy-gui/base/src/renderer/elementManager.ts +++ b/frontend/taipy-gui/base/src/renderer/elementManager.ts @@ -14,10 +14,23 @@ export interface Element { editModeJsx?: string; } +export enum ElementActionEnum { + Add = "add", + Modify = "modify", + Delete = "delete", +} + +export interface ElementAction { + action: ElementActionEnum; + id: string; + payload?: Record; +} + export class ElementManager { _elements: Element[]; _renderer: ElementRenderer; _canvas: TaipyCanvas; + _elementActions: ElementAction[]; taipyApp: TaipyApp; constructor(taipyApp: TaipyApp) { @@ -25,6 +38,7 @@ export class ElementManager { this._elements = []; this._renderer = new ElementRenderer(taipyApp); this._canvas = new TaipyCanvas(taipyApp); + this._elementActions = []; } init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement) { @@ -62,6 +76,7 @@ export class ElementManager { return; } this._elements.push(element); + this._elementActions.push({ action: ElementActionEnum.Add, id: element.id }); this.render(); } @@ -73,11 +88,17 @@ export class ElementManager { const properties = { ...el.properties, ...elementProperties }; return { ...el, properties, jsx: "", editModeJsx: "" }; }); + this._elementActions.push({ action: ElementActionEnum.Modify, id, payload: elementProperties }); this.render(); } deleteElement(id: string) { this._elements = this._elements.filter((el) => el.id !== id); + this._elementActions.push({ action: ElementActionEnum.Delete, id }); this.render(); } + + getElementActionFromQueue() { + return this._elementActions.shift(); + } } From f627f2403ab8a3b0601e1f96b504834882fc3cd2 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Thu, 20 Feb 2025 15:32:20 +0700 Subject: [PATCH 10/24] each element is rendered as a separate jsx --- frontend/taipy-gui/base/src/app.ts | 85 +++++++------- .../src/components/Taipy/TaipyElement.tsx | 75 ++++++++++++ .../src/components/Taipy/TaipyRendered.tsx | 46 ++------ .../base/src/components/Taipy/utils.ts | 20 ++++ frontend/taipy-gui/base/src/dataManager.ts | 98 ++++++++-------- .../base/src/packaging/taipy-gui-base.d.ts | 93 ++++++++------- .../taipy-gui/base/src/renderer/canvas.ts | 63 ++++------ .../base/src/renderer/elementManager.ts | 109 ++++++++---------- .../base/src/renderer/elementRenderer.ts | 42 ------- frontend/taipy-gui/base/src/store.ts | 50 +++++--- frontend/taipy-gui/base/src/wsAdapter.ts | 13 ++- 11 files changed, 362 insertions(+), 332 deletions(-) create mode 100644 frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx create mode 100644 frontend/taipy-gui/base/src/components/Taipy/utils.ts delete mode 100644 frontend/taipy-gui/base/src/renderer/elementRenderer.ts diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index 8ffaf12213..e17b98f182 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -10,8 +10,7 @@ import { TaipyWsAdapter, WsAdapter } from "./wsAdapter"; import { WsMessageType } from "../../src/context/wsUtils"; import { getBase } from "./utils"; import { CookieHandler } from "./cookieHandler"; -import { ElementAction, ElementManager } from "./renderer/elementManager"; -import useStore from "./store"; +import { CanvasRenderConfig, Element, ElementAction, ElementManager } from "./renderer/elementManager"; export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; @@ -19,7 +18,7 @@ export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void; export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void; export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void; -export type OnCanvasReRender = (taipyApp: TaipyApp, isEditMode: boolean, elementActions?: ElementAction) => void; +export type OnCanvasReRender = (taipyApp: TaipyApp, isEditMode: boolean, elementAction?: ElementAction) => void; export type OnEvent = | OnInitHandler | OnChangeHandler @@ -32,16 +31,15 @@ type RequestDataCallback = (taipyApp: TaipyApp, encodedName: string, dataEventKe export class TaipyApp { socket: Socket; - _onInit: OnInitHandler | undefined; - _onChange: OnChangeHandler | undefined; - _onNotify: OnNotifyHandler | undefined; - _onReload: OnReloadHandler | undefined; - _onWsMessage: OnWsMessage | undefined; - _onWsStatusUpdate: OnWsStatusUpdate | undefined; - _onCanvasReRender: OnCanvasReRender | undefined; - _ackList: string[]; - _rdc: Record>; - _cookieHandler: CookieHandler | undefined; + #onInit: OnInitHandler | undefined; + #onChange: OnChangeHandler | undefined; + #onNotify: OnNotifyHandler | undefined; + #onReload: OnReloadHandler | undefined; + #onWsMessage: OnWsMessage | undefined; + #onWsStatusUpdate: OnWsStatusUpdate | undefined; + #onCanvasReRender: OnCanvasReRender | undefined; + ackList: string[]; + rdc: Record>; variableData: DataManager | undefined; functionData: DataManager | undefined; appId: string; @@ -74,24 +72,23 @@ export class TaipyApp { this.socket = socket; this.wsAdapters = [new TaipyWsAdapter()]; this.elementManager = new ElementManager(this); - this._ackList = []; - this._rdc = {}; - this._cookieHandler = handleCookie ? new CookieHandler() : undefined; + this.ackList = []; + this.rdc = {}; // Init socket io connection only when cookie is not handled // Socket will be initialized by cookie handler when it is used - this._cookieHandler ? this._cookieHandler?.init(socket, this) : initSocket(socket, this); + handleCookie ? new CookieHandler().init(socket, this) : initSocket(socket, this); } // Getter and setter get onInit() { - return this._onInit; + return this.#onInit; } set onInit(handler: OnInitHandler | undefined) { if (handler !== undefined && handler.length !== 1) { throw new Error("onInit() requires one parameter"); } - this._onInit = handler; + this.#onInit = handler; } onInitEvent() { @@ -99,14 +96,14 @@ export class TaipyApp { } get onChange() { - return this._onChange; + return this.#onChange; } set onChange(handler: OnChangeHandler | undefined) { if (handler !== undefined && handler.length !== 3 && handler.length !== 4) { throw new Error("onChange() requires three or four parameters"); } - this._onChange = handler; + this.#onChange = handler; } onChangeEvent(encodedName: string, value: unknown, dataEventKey?: string) { @@ -114,14 +111,14 @@ export class TaipyApp { } get onNotify() { - return this._onNotify; + return this.#onNotify; } set onNotify(handler: OnNotifyHandler | undefined) { if (handler !== undefined && handler.length !== 3) { throw new Error("onNotify() requires three parameters"); } - this._onNotify = handler; + this.#onNotify = handler; } onNotifyEvent(type: string, message: string) { @@ -129,13 +126,13 @@ export class TaipyApp { } get onReload() { - return this._onReload; + return this.#onReload; } set onReload(handler: OnReloadHandler | undefined) { if (handler !== undefined && handler?.length !== 2) { throw new Error("onReload() requires two parameters"); } - this._onReload = handler; + this.#onReload = handler; } onReloadEvent(removedChanges: ModuleData) { @@ -143,13 +140,13 @@ export class TaipyApp { } get onWsMessage() { - return this._onWsMessage; + return this.#onWsMessage; } set onWsMessage(handler: OnWsMessage | undefined) { if (handler !== undefined && handler?.length !== 3) { throw new Error("onWsMessage() requires three parameters"); } - this._onWsMessage = handler; + this.#onWsMessage = handler; } onWsMessageEvent(event: string, payload: unknown) { @@ -157,13 +154,13 @@ export class TaipyApp { } get onWsStatusUpdate() { - return this._onWsStatusUpdate; + return this.#onWsStatusUpdate; } set onWsStatusUpdate(handler: OnWsStatusUpdate | undefined) { if (handler !== undefined && handler?.length !== 2) { throw new Error("onWsStatusUpdate() requires two parameters"); } - this._onWsStatusUpdate = handler; + this.#onWsStatusUpdate = handler; } onWsStatusUpdateEvent(messageQueue: string[]) { @@ -171,19 +168,18 @@ export class TaipyApp { } get onCanvasReRender() { - return this._onCanvasReRender; + return this.#onCanvasReRender; } set onCanvasReRender(handler: OnCanvasReRender | undefined) { if (handler !== undefined && handler?.length !== 3) { throw new Error("onCanvasReRender() requires three parameter"); } - this._onCanvasReRender = handler; + this.#onCanvasReRender = handler; } - onCanvasReRenderEvent() { - this.onCanvasReRender && - this.onCanvasReRender(this, useStore.getState().editMode, this.elementManager.getElementActionFromQueue()); + onCanvasReRenderEvent(canvasIsEditMode: boolean, elementAction?: ElementAction) { + this.onCanvasReRender && this.onCanvasReRender(this, canvasIsEditMode, elementAction); } // Utility methods @@ -212,8 +208,8 @@ export class TaipyApp { } const ackId = sendWsMessage(this.socket, type, id, payload, this.clientId, context); if (ackId) { - this._ackList.push(ackId); - this.onWsStatusUpdateEvent(this._ackList); + this.ackList.push(ackId); + this.onWsStatusUpdateEvent(this.ackList); } } @@ -281,7 +277,7 @@ export class TaipyApp { // preserve options for this data key so it can be called during refresh this.variableData?.addRequestDataOptions(encodedName, dataKey, options); // preserve callback so it can be called later - this._rdc[encodedName] = { ...this._rdc[encodedName], [dataKey]: cb }; + this.rdc[encodedName] = { ...this.rdc[encodedName], [dataKey]: cb }; // call the ws to request data this.sendWsMessage("DU", encodedName, options); } @@ -311,32 +307,33 @@ export class TaipyApp { } getWsStatus() { - return this._ackList; + return this.ackList; } getBaseUrl() { return getBase(); } + // ElementManager API createCanvas(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement) { this.elementManager.init(canvasDomElement, canvasEditModeCanvas); } addElement2Canvas( type: string, - properties: Record | undefined = undefined, - wrapperHtml: [string, string] | undefined = undefined, - wrapperHtmlEditMode: [string, string] | undefined = undefined, - id: string | undefined = undefined, + id: string, + rootId: string, + wrapper: CanvasRenderConfig["wrapper"], + properties: Element["properties"] | undefined = undefined, ) { - this.elementManager.addElement({ id, type, properties, wrapperHtml, wrapperHtmlEditMode }); + this.elementManager.addElement(type, id, rootId, wrapper, properties); } setCanvasEditMode(bool: boolean) { this.elementManager.setEditMode(bool); } - modifyElement(id: string, elemenetProperties: Record) { + modifyElement(id: string, elemenetProperties: Element["properties"]) { this.elementManager.modifyElement(id, elemenetProperties); } diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx new file mode 100644 index 0000000000..d6f2333663 --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx @@ -0,0 +1,75 @@ +import React, { ComponentType, useContext, useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { ErrorBoundary } from "react-error-boundary"; +import JsxParser from "react-jsx-parser"; + +import { PageContext, TaipyContext } from "../../../../src/context/taipyContext"; +import { Element } from "../../renderer/elementManager"; +import useStore from "../../store"; +import { getJsx } from "./utils"; +import { emptyArray } from "../../../../src/utils"; +import ErrorFallback from "../../../../src/utils/ErrorBoundary"; +import { getRegisteredComponents } from "../../../../src/components/Taipy"; +import { renderError, unregisteredRender } from "../../../../src/components/Taipy/Unregistered"; + +interface TaipyElementProps { + editMode: boolean; + element: Element; +} + +const TaipyElement = (props: TaipyElementProps) => { + const { state } = useContext(TaipyContext); + const [module, setModule] = useState(""); + const [jsx, setJsx] = useState(""); + const app = useStore((state) => state.app); + + const renderConfig = useMemo( + () => (props.editMode ? props.element.editModeRenderConfig : props.element.renderConfig), + [props.element, props.editMode], + ); + + const pageState = useMemo(() => { + return { jsx, module }; + }, [jsx, module]); + + useEffect(() => { + app && setModule(app.getContext()); + }, [app]); + + useEffect(() => { + const setJsxAsync = async () => { + if (!app || !renderConfig) { + setJsx(""); + return; + } + const res = await getJsx(app, props.element, props.editMode); + setJsx(`${renderConfig.wrapper[0]}${res}${renderConfig.wrapper[1]}`); + }; + setJsxAsync(); + }, [app, props.editMode, props.element, renderConfig]); + + return renderConfig ? ( + createPortal( + + + } + jsx={pageState.jsx} + renderUnrecognized={unregisteredRender} + allowUnknownElements={false} + renderError={renderError} + blacklistedAttrs={emptyArray} + renderInWrapper={false} + /> + + , + renderConfig.root, + ) + ) : ( + <> + ); +}; + +export default TaipyElement; diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx index 9771cf7939..b50cd8cd3c 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx @@ -11,40 +11,30 @@ * specific language governing permissions and limitations under the License. */ -import React, { ComponentType, useEffect, useMemo, useReducer } from "react"; -import { ErrorBoundary } from "react-error-boundary"; -import JsxParser from "react-jsx-parser"; +import React, { useEffect, useReducer } from "react"; import { ThemeProvider } from "@mui/system"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3"; -import { PageContext, TaipyContext } from "../../../../src/context/taipyContext"; -import { emptyArray } from "../../../../src/utils"; -import ErrorFallback from "../../../../src/utils/ErrorBoundary"; -import { getRegisteredComponents } from "../../../../src/components/Taipy"; -import { renderError, unregisteredRender } from "../../../../src/components/Taipy/Unregistered"; +import { TaipyContext } from "../../../../src/context/taipyContext"; import { INITIAL_STATE, initializeWebSocket, taipyInitialize, taipyReducer, } from "../../../../src/context/taipyReducers"; -import useStore from "../../store"; +import useStore, { getElementAction } from "../../store"; +import TaipyElement from "./TaipyElement"; interface TaipyRenderedProps { - editMode?: boolean; + editMode: boolean; } const TaipyRendered = (props: TaipyRenderedProps) => { - const { editMode } = props; - const jsx = useStore((state) => (editMode ? state.editModeJsx : state.jsx)); - const module = useStore((state) => state.module); - const app = useStore((state) => state.app); - const pageState = useMemo(() => { - return { jsx, module }; - }, [jsx, module]); const [state, dispatch] = useReducer(taipyReducer, INITIAL_STATE, taipyInitialize); + const elements = useStore((state) => state.elements); + const app = useStore((state) => state.app); const themeClass = "taipy-" + state.theme.palette.mode; useEffect(() => { @@ -62,28 +52,16 @@ const TaipyRendered = (props: TaipyRenderedProps) => { }, [themeClass]); useEffect(() => { - app && app.onCanvasReRenderEvent(); - }, [jsx, app]); + app && app.onCanvasReRenderEvent(props.editMode, getElementAction(props.editMode)); + }, [elements, app, props.editMode]); return ( - - - } - jsx={pageState.jsx} - renderUnrecognized={unregisteredRender} - allowUnknownElements={false} - renderError={renderError} - blacklistedAttrs={emptyArray} - renderInWrapper={false} - /> - - + {elements.map((element) => ( + + ))} diff --git a/frontend/taipy-gui/base/src/components/Taipy/utils.ts b/frontend/taipy-gui/base/src/components/Taipy/utils.ts new file mode 100644 index 0000000000..f82d200535 --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/utils.ts @@ -0,0 +1,20 @@ +import axios from "axios"; +import { TaipyApp } from "../../app"; +import { Element } from "../../renderer/elementManager"; + +export const getJsx = async (taipyApp: TaipyApp, element: Element, editMode: boolean): Promise => { + try { + const id = element.id + "-el" + (editMode ? "" : "-active"); + const result = await axios.post<{ jsx: string }>( + `${taipyApp.getBaseUrl()}taipy-element-jsx?client_id=${taipyApp.clientId}`, + { + type: element.type, + properties: { ...element.properties, id }, + context: taipyApp.getContext(), + }, + ); + return result.data.jsx; + } catch (error) { + throw new Error(`Failed to render element '${element.type} - ${element.id}': ${error}`); + } +}; diff --git a/frontend/taipy-gui/base/src/dataManager.ts b/frontend/taipy-gui/base/src/dataManager.ts index 3614fd7e51..8a402fc899 100644 --- a/frontend/taipy-gui/base/src/dataManager.ts +++ b/frontend/taipy-gui/base/src/dataManager.ts @@ -33,7 +33,7 @@ export type RequestDataOptions = { type RequestDataEntry = { options: RequestDataOptions; receivedData: unknown; -} +}; export const getRequestedDataKey = (payload?: unknown) => (!!payload && typeof payload == "object" && "pagekey" in payload && (payload["pagekey"] as string)) || undefined; @@ -41,33 +41,33 @@ export const getRequestedDataKey = (payload?: unknown) => // This class hold the information of variables and real-time value of variables export class DataManager { // key: encoded name, value: real-time value - _data: Record; + #data: Record; // Initial data fetched from taipy-gui backend - _init_data: ModuleData; + #init_data: ModuleData; // key: encodedName -> dataEventKey -> requeste data - _requested_data: Record>; + #requested_data: Record>; constructor(variableModuleData: ModuleData) { - this._data = {}; - this._init_data = {}; - this._requested_data = {}; + this.#data = {}; + this.#init_data = {}; + this.#requested_data = {}; this.init(variableModuleData); } init(variableModuleData: ModuleData) { // Identify changes between the new and old data const changes: ModuleData = {}; - for (const context in this._init_data) { + for (const context in this.#init_data) { if (!(context in variableModuleData)) { - changes[context] = this._init_data[context]; + changes[context] = this.#init_data[context]; continue; } - for (const variable in this._init_data[context]) { + for (const variable in this.#init_data[context]) { if (!(variable in variableModuleData[context])) { if (!(context in changes)) { changes[context] = {}; } - changes[context][variable] = this._init_data[context][variable]; + changes[context][variable] = this.#init_data[context][variable]; } } } @@ -75,29 +75,29 @@ export class DataManager { console.error("Unmatched data tree! Removed changes: ", changes); } // Reset the initial data - this._init_data = variableModuleData; - this._data = {}; - for (const context in this._init_data) { - for (const variable in this._init_data[context]) { - const vData = this._init_data[context][variable]; - this._data[vData["encoded_name"]] = vData.value; + this.#init_data = variableModuleData; + this.#data = {}; + for (const context in this.#init_data) { + for (const variable in this.#init_data[context]) { + const vData = this.#init_data[context][variable]; + this.#data[vData["encoded_name"]] = vData.value; } } return changes; } getEncodedName(varName: string, module: string): string | undefined { - if (module in this._init_data && varName in this._init_data[module]) { - return this._init_data[module][varName].encoded_name; + if (module in this.#init_data && varName in this.#init_data[module]) { + return this.#init_data[module][varName].encoded_name; } return undefined; } // return [name, moduleName] getName(encodedName: string): [string, string] | undefined { - for (const context in this._init_data) { - for (const variable in this._init_data[context]) { - const vData = this._init_data[context][variable]; + for (const context in this.#init_data) { + for (const variable in this.#init_data[context]) { + const vData = this.#init_data[context][variable]; if (vData.encoded_name === encodedName) { return [variable, context]; } @@ -109,35 +109,37 @@ export class DataManager { get(encodedName: string, dataEventKey?: string): unknown { // handle requested data if (dataEventKey) { - if (!(encodedName in this._requested_data)) { + if (!(encodedName in this.#requested_data)) { throw new Error(`Encoded name '${encodedName}' is not available in Taipy GUI`); } - if (!(dataEventKey in this._requested_data[encodedName])) { - throw new Error(`Event key '${dataEventKey}' is not available for encoded name '${encodedName}' in Taipy GUI`); + if (!(dataEventKey in this.#requested_data[encodedName])) { + throw new Error( + `Event key '${dataEventKey}' is not available for encoded name '${encodedName}' in Taipy GUI`, + ); } - return this._requested_data[encodedName][dataEventKey].receivedData; + return this.#requested_data[encodedName][dataEventKey].receivedData; } // handle normal data - if (!(encodedName in this._data)) { + if (!(encodedName in this.#data)) { throw new Error(`${encodedName} is not available in Taipy GUI`); } - return this._data[encodedName]; + return this.#data[encodedName]; } addRequestDataOptions(encodedName: string, dataEventKey: string, options: RequestDataOptions) { - if (!(encodedName in this._requested_data)) { - this._requested_data[encodedName] = {}; + if (!(encodedName in this.#requested_data)) { + this.#requested_data[encodedName] = {}; } // This would overrides object with the same key - this._requested_data[encodedName][dataEventKey] = { options: options, receivedData: undefined }; + this.#requested_data[encodedName][dataEventKey] = { options: options, receivedData: undefined }; } getInfo(encodedName: string): VarData | undefined { - for (const context in this._init_data) { - for (const variable in this._init_data[context]) { - const vData = this._init_data[context][variable]; + for (const context in this.#init_data) { + for (const variable in this.#init_data[context]) { + const vData = this.#init_data[context][variable]; if (vData.encoded_name === encodedName) { - return { ...vData, value: this._data[encodedName] }; + return { ...vData, value: this.#data[encodedName] }; } } } @@ -145,35 +147,41 @@ export class DataManager { } getDataTree(): ModuleData { - return this._init_data; + return this.#init_data; } getAllData(): Record { - return this._data; + return this.#data; } update(encodedName: string, value: unknown, dataEventKey?: string) { // handle requested data if (dataEventKey) { - if (!(encodedName in this._requested_data)) { + if (!(encodedName in this.#requested_data)) { throw new Error(`Encoded name '${encodedName}' is not available in Taipy GUI`); } - if (!(dataEventKey in this._requested_data[encodedName])) { - throw new Error(`Event key '${dataEventKey}' is not available for encoded name '${encodedName}' in Taipy GUI`); + if (!(dataEventKey in this.#requested_data[encodedName])) { + throw new Error( + `Event key '${dataEventKey}' is not available for encoded name '${encodedName}' in Taipy GUI`, + ); } - this._requested_data[encodedName][dataEventKey].receivedData = value; + this.#requested_data[encodedName][dataEventKey].receivedData = value; return; } // handle normal data - if (!(encodedName in this._data)) { + if (!(encodedName in this.#data)) { throw new Error(`${encodedName} is not available in Taipy Gui`); } - this._data[encodedName] = value; + this.#data[encodedName] = value; } deleteRequestedData(encodedName: string, dataEventKey: string) { - if (encodedName in this._requested_data && dataEventKey in this._requested_data[encodedName]) { - delete this._requested_data[encodedName][dataEventKey]; + if (encodedName in this.#requested_data && dataEventKey in this.#requested_data[encodedName]) { + delete this.#requested_data[encodedName][dataEventKey]; } } + + getRequestedData() { + return this.#requested_data; + } } diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index 0eabd4d54f..7efadbece4 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -42,9 +42,7 @@ export type RequestDataEntry = { receivedData: unknown; }; declare class DataManager { - _data: Record; - _init_data: ModuleData; - _requested_data: Record>; + #private; constructor(variableModuleData: ModuleData); init(variableModuleData: ModuleData): ModuleData; getEncodedName(varName: string, module: string): string | undefined; @@ -56,6 +54,7 @@ declare class DataManager { getAllData(): Record; update(encodedName: string, value: unknown, dataEventKey?: string): void; deleteRequestedData(encodedName: string, dataEventKey: string): void; + getRequestedData(): Record>; } export type WsMessageType = | "A" @@ -90,41 +89,44 @@ export declare abstract class WsAdapter { abstract supportedMessageTypes: string[]; abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean; } -declare class CookieHandler { - resourceHandlerId: string; - constructor(); - init(socket: Socket, taipyApp: TaipyApp): Promise; - verifyCookieStatus(): Promise; - addBeforeUnloadListener(): void; - deleteCookie(): Promise; -} -declare class TaipyCanvas { - taipyApp: TaipyApp; - constructor(taipyApp: TaipyApp); - init(domElement: HTMLElement): void; - updateContent(jsx: string): void; -} -declare class ElementRenderer { - taipyApp: TaipyApp; - constructor(taipyApp: TaipyApp); - render(elements: Element[], force?: boolean): Promise; - renderSingle(element: Element): Promise; +export interface CanvasRenderConfig { + rootId: string; + root: HTMLElement; + wrapper: [string, string]; } export interface Element { type: string; - id?: string; - properties?: Record; - wrapperHtml?: [string, string]; - jsx?: string; + id: string; + properties?: Record; + renderConfig?: CanvasRenderConfig; + editModeRenderConfig?: CanvasRenderConfig; +} +declare enum ElementActionEnum { + Add = "add", + Modify = "modify", + Delete = "delete", +} +export interface ElementAction { + action: ElementActionEnum; + id: Element["id"]; + payload?: Record; + editMode?: boolean; } declare class ElementManager { - _elements: Element[]; - _renderer: ElementRenderer; - _canvas: TaipyCanvas; + #private; taipyApp: TaipyApp; constructor(taipyApp: TaipyApp); - init(domElement: HTMLElement): void; - addElement(element: Element): void; + init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement): void; + setEditMode(editMode: boolean): void; + addElement( + type: string, + id: string, + rootId: string, + wrapper: CanvasRenderConfig["wrapper"], + properties?: Element["properties"] | undefined, + ): void; + modifyElement(id: string, elementProperties: Element["properties"]): void; + deleteElement(id: string): void; } export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; @@ -132,7 +134,7 @@ export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void; export type OnWsMessage = (taipyApp: TaipyApp, event: string, payload: unknown) => void; export type OnWsStatusUpdate = (taipyApp: TaipyApp, messageQueue: string[]) => void; -export type OnCanvasReRender = (taipyApp: TaipyApp) => void; +export type OnCanvasReRender = (taipyApp: TaipyApp, isEditMode: boolean, elementAction?: ElementAction) => void; export type Route = [string, string]; export type RequestDataCallback = ( taipyApp: TaipyApp, @@ -141,17 +143,10 @@ export type RequestDataCallback = ( value: unknown, ) => void; export declare class TaipyApp { + #private; socket: Socket; - _onInit: OnInitHandler | undefined; - _onChange: OnChangeHandler | undefined; - _onNotify: OnNotifyHandler | undefined; - _onReload: OnReloadHandler | undefined; - _onWsMessage: OnWsMessage | undefined; - _onWsStatusUpdate: OnWsStatusUpdate | undefined; - _onCanvasReRender: OnCanvasReRender | undefined; - _ackList: string[]; - _rdc: Record>; - _cookieHandler: CookieHandler | undefined; + ackList: string[]; + rdc: Record>; variableData: DataManager | undefined; functionData: DataManager | undefined; appId: string; @@ -189,7 +184,7 @@ export declare class TaipyApp { onWsStatusUpdateEvent(messageQueue: string[]): void; get onCanvasReRender(): OnCanvasReRender | undefined; set onCanvasReRender(handler: OnCanvasReRender | undefined); - onCanvasReRenderEvent(): void; + onCanvasReRenderEvent(canvasIsEditMode: boolean, elementAction?: ElementAction): void; init(): void; initApp(): void; sendWsMessage(type: WsMessageType | string, id: string, payload: unknown, context?: string | undefined): void; @@ -212,13 +207,17 @@ export declare class TaipyApp { getPageMetadata(): Record; getWsStatus(): string[]; getBaseUrl(): string; - createCanvas(domElement: HTMLElement): void; + createCanvas(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement): void; addElement2Canvas( type: string, - properties?: Record | undefined, - wrapperHtml?: [string, string] | undefined, - id?: string | undefined, + id: string, + rootId: string, + wrapper: CanvasRenderConfig["wrapper"], + properties?: Element["properties"] | undefined, ): void; + setCanvasEditMode(bool: boolean): void; + modifyElement(id: string, elemenetProperties: Element["properties"]): void; + deleteElement(id: string): void; } export declare const createApp: ( onInit?: OnInitHandler, diff --git a/frontend/taipy-gui/base/src/renderer/canvas.ts b/frontend/taipy-gui/base/src/renderer/canvas.ts index e1b3a6b842..0ae6166c38 100644 --- a/frontend/taipy-gui/base/src/renderer/canvas.ts +++ b/frontend/taipy-gui/base/src/renderer/canvas.ts @@ -1,78 +1,55 @@ import { TaipyApp } from "../app"; import { createRoot, Root } from "react-dom/client"; -import useStore from "../store"; +import { getStore } from "../store"; import { createElement } from "react"; import TaipyRendered from "../components/Taipy/TaipyRendered"; export class TaipyCanvas { taipyApp: TaipyApp; #root?: Root; - #editModeRoot?: Root; #canvasElement?: HTMLElement; - #canvasEditModeElement?: HTMLElement; constructor(taipyApp: TaipyApp) { this.taipyApp = taipyApp; } init(canvasElement: HTMLElement, canvasEditModeElement?: HTMLElement) { - if (canvasElement) { - this.initCanvas(canvasElement, false); - useStore.getState().setApp(this.taipyApp); - useStore.getState().setModule(this.taipyApp.getContext()); - if (canvasEditModeElement) { - this.initCanvas(canvasEditModeElement, true); - useStore.getState().setEditMode(true); - } - } else { + if (!canvasElement) { console.error("Root element not found!"); + return; + } + getStore().setApp(this.taipyApp); + this.initCanvas(canvasElement, false); + if (canvasEditModeElement) { + this.initCanvas(canvasEditModeElement, true); + getStore().setEditMode(true); } } initCanvas(canvasElement: HTMLElement, editMode: boolean) { - if (editMode) { - this.#canvasEditModeElement = canvasElement; - this.#editModeRoot = createRoot(this.#canvasEditModeElement); - this.#editModeRoot.render(createElement(TaipyRendered, { editMode })); - return; + const root = createRoot(canvasElement); + root.render(createElement(TaipyRendered, { editMode })); + if (!editMode) { + this.#canvasElement = canvasElement; + this.#root = root; } - this.#canvasElement = canvasElement; - this.#root = createRoot(this.#canvasElement); - this.#root.render(createElement(TaipyRendered, { editMode })); } - resetCanvas(editMode: boolean) { - if (editMode && this.#editModeRoot) { - this.#editModeRoot.unmount(); - // remove all elements from canvas - while (this.#canvasEditModeElement?.firstChild) { - this.#canvasEditModeElement.removeChild(this.#canvasEditModeElement.firstChild); - } - useStore.getState().setEditModeJsx(""); - this.#editModeRoot.render(createElement(TaipyRendered, { editMode })); - return; - } - if (!editMode && this.#root && this.#canvasElement) { - // remove all elements from canvas + // only used for view mode canvas + resetMainCanvas() { + if (this.#root && this.#canvasElement) { try { this.#root.unmount(); } catch (error) { console.error("Error while unmounting root canvas", error); } + // remove all elements from canvas while (this.#canvasElement?.firstChild) { this.#canvasElement.removeChild(this.#canvasElement.firstChild); } - useStore.getState().setJsx(""); + getStore().resetMainCanvas(); this.#root = createRoot(this.#canvasElement); - this.#root.render(createElement(TaipyRendered, { editMode })); - } - } - - updateContent(jsx: string, editMode: boolean) { - if (editMode) { - useStore.getState().setEditModeJsx(jsx); - return; + this.#root.render(createElement(TaipyRendered, { editMode: false })); } - useStore.getState().setJsx(jsx); } } diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/renderer/elementManager.ts index dbfce0adf5..ebaea96075 100644 --- a/frontend/taipy-gui/base/src/renderer/elementManager.ts +++ b/frontend/taipy-gui/base/src/renderer/elementManager.ts @@ -1,17 +1,19 @@ -import { nanoid } from "nanoid"; import { TaipyApp } from "../app"; import { TaipyCanvas } from "./canvas"; -import { ElementRenderer } from "./elementRenderer"; -import useStore from "../store"; +import { getStore, isElementExisted } from "../store"; + +export interface CanvasRenderConfig { + rootId: string; + root: HTMLElement; + wrapper: [string, string]; +} export interface Element { type: string; - id?: string; - properties?: Record; - wrapperHtml?: [string, string]; - wrapperHtmlEditMode?: [string, string]; - jsx?: string; - editModeJsx?: string; + id: string; + properties?: Record; + renderConfig?: CanvasRenderConfig; + editModeRenderConfig?: CanvasRenderConfig; } export enum ElementActionEnum { @@ -22,83 +24,74 @@ export enum ElementActionEnum { export interface ElementAction { action: ElementActionEnum; - id: string; - payload?: Record; + id: Element["id"]; + payload?: Record; + editMode?: boolean; } export class ElementManager { - _elements: Element[]; - _renderer: ElementRenderer; - _canvas: TaipyCanvas; - _elementActions: ElementAction[]; + #canvas: TaipyCanvas; taipyApp: TaipyApp; constructor(taipyApp: TaipyApp) { this.taipyApp = taipyApp; - this._elements = []; - this._renderer = new ElementRenderer(taipyApp); - this._canvas = new TaipyCanvas(taipyApp); - this._elementActions = []; + this.#canvas = new TaipyCanvas(taipyApp); } init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement) { - this._canvas.init(canvasDomElement, canvasEditModeCanvas); + this.#canvas.init(canvasDomElement, canvasEditModeCanvas); } setEditMode(editMode: boolean) { - console.log("Setting edit mode to", editMode); - const currentEditMode = useStore.getState().editMode; + const currentEditMode = getStore().editMode; if (currentEditMode === editMode) { return; } - useStore.getState().setEditMode(editMode); + getStore().setEditMode(editMode); // Reset view mode canvas if switched back to edit mode if (editMode) { - this._canvas.resetCanvas(false); - } - // if in view mode -> render it because chalkit will not be rendering it - if (!editMode) { - this.render(true); + this.#canvas.resetMainCanvas(); } } - render(force: boolean = false) { - const { editMode } = useStore.getState(); - this._renderer.render(this._elements, editMode, force).then((jsx) => this._canvas.updateContent(jsx, editMode)); - } - - addElement(element: Element) { - if (element.id === undefined) { - element.id = nanoid(10); + addElement( + type: string, + id: string, + rootId: string, + wrapper: CanvasRenderConfig["wrapper"], + properties: Element["properties"] | undefined = undefined, + ) { + const root = document.getElementById(rootId); + if (!root) { + console.error(`Root element with id '${rootId}' not found!`); + return; } - // check if element already exists based on id - if (this._elements.find((el) => el.id === element.id)) { + const renderConfig = { + [getStore().editMode ? "editModeRenderConfig" : "renderConfig"]: { rootId, root, wrapper }, + }; + // add element if not existed + if (!isElementExisted(id)) { + getStore().addElementAction({ action: ElementActionEnum.Add, id, payload: properties }); + getStore().addElement({ + type, + id, + properties, + ...renderConfig, + }); return; } - this._elements.push(element); - this._elementActions.push({ action: ElementActionEnum.Add, id: element.id }); - this.render(); + // modify element if existed + getStore().addElementAction({ action: ElementActionEnum.Add, id, payload: properties }); + getStore().editElement(id, { ...renderConfig, properties }); } - modifyElement(id: string, elementProperties: Record) { - this._elements = this._elements.map((el) => { - if (el.id !== id) { - return el; - } - const properties = { ...el.properties, ...elementProperties }; - return { ...el, properties, jsx: "", editModeJsx: "" }; - }); - this._elementActions.push({ action: ElementActionEnum.Modify, id, payload: elementProperties }); - this.render(); + modifyElement(id: string, elementProperties: Element["properties"]) { + getStore().addElementAction({ action: ElementActionEnum.Modify, id, payload: elementProperties }); + getStore().editElement(id, elementProperties); } deleteElement(id: string) { - this._elements = this._elements.filter((el) => el.id !== id); - this._elementActions.push({ action: ElementActionEnum.Delete, id }); - this.render(); - } - - getElementActionFromQueue() { - return this._elementActions.shift(); + getStore().addElementAction({ action: ElementActionEnum.Delete, id }); + getStore().deleteElement(id); } } diff --git a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts b/frontend/taipy-gui/base/src/renderer/elementRenderer.ts deleted file mode 100644 index 27e1ff0ccb..0000000000 --- a/frontend/taipy-gui/base/src/renderer/elementRenderer.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TaipyApp } from "../app"; -import { Element } from "./elementManager"; -import axios from "axios"; - -export class ElementRenderer { - taipyApp: TaipyApp; - - constructor(taipyApp: TaipyApp) { - this.taipyApp = taipyApp; - } - - async render(elements: Element[], editMode: boolean, force: boolean = false): Promise { - const renderedElements = await Promise.all( - elements.map(async (element) => { - const jsxMode = editMode ? "editModeJsx" : "jsx"; - const wrapperHtml = editMode ? element.wrapperHtmlEditMode : element.wrapperHtml; - if (force || !element[jsxMode]) { - element[jsxMode] = await this.renderSingle(element, editMode); - } - return `${wrapperHtml?.[0] || ""}${element[jsxMode]}${wrapperHtml?.[1] || ""}`; - }), - ); - return renderedElements.join("\n"); - } - - async renderSingle(element: Element, editMode: boolean): Promise { - try { - const id = element.id + "-el" + (editMode ? "" : "-active"); - const result = await axios.post<{ jsx: string }>( - `${this.taipyApp.getBaseUrl()}taipy-element-jsx?client_id=${this.taipyApp.clientId}`, - { - type: element.type, - properties: { ...element.properties, id, active: !editMode }, - context: this.taipyApp.getContext(), - }, - ); - return result.data.jsx; - } catch (error) { - throw new Error(`Failed to render element '${element.type} - ${element.id}': ${error}`); - } - } -} diff --git a/frontend/taipy-gui/base/src/store.ts b/frontend/taipy-gui/base/src/store.ts index ef9504da2c..1310c3bf99 100644 --- a/frontend/taipy-gui/base/src/store.ts +++ b/frontend/taipy-gui/base/src/store.ts @@ -1,30 +1,54 @@ import { create } from "zustand"; import { TaipyApp } from "./app"; +import { Element, ElementAction } from "./renderer/elementManager"; interface TaipyGuiBaseState { editMode: boolean; - jsx: string; - editModeJsx: string; - module: string; app?: TaipyApp; + elements: Element[]; + elementActions: ElementAction[]; setEditMode: (newEditMode: boolean) => void; - setJsx: (newJsx: string) => void; - setEditModeJsx: (newJsx: string) => void; - setModule: (newModule: string) => void; setApp: (newApp: TaipyApp) => void; + addElement: (newElement: Element) => void; + editElement: (id: string, payload: Element["properties"]) => void; + resetMainCanvas: () => void; + deleteElement: (id: string) => void; + addElementAction: (action: ElementAction) => void; + deleteElementAction: (action: ElementAction) => void; } -const useStore = create()((set) => ({ +export const useStore = create()((set) => ({ editMode: false, - jsx: "", - editModeJsx: "", - module: "", app: undefined, + elements: [], + elementActions: [], setEditMode: (newEditMode: boolean) => set(() => ({ editMode: newEditMode })), - setJsx: (newJsx: string) => set(() => ({ jsx: newJsx })), - setEditModeJsx: (newJsx: string) => set(() => ({ editModeJsx: newJsx })), - setModule: (newModule: string) => set(() => ({ module: newModule })), setApp: (newApp: TaipyApp) => set(() => ({ app: newApp })), + addElement: (newElement: Element) => set((state) => ({ elements: [...state.elements, newElement] })), + editElement: (id: string, payload: Element["properties"]) => + set((state) => ({ + elements: state.elements.map((element) => (element.id === id ? { ...element, ...payload } : element)), + })), + deleteElement: (id: string) => + set((state) => ({ elements: state.elements.filter((element) => element.id !== id) })), + resetMainCanvas: () => + set((state) => ({ elements: state.elements.map((el) => ({ ...el, renderConfig: undefined })) })), + addElementAction: (action: ElementAction) => + set((state) => ({ elementActions: [...state.elementActions, { ...action, editMode: state.editMode }] })), + deleteElementAction: (action: ElementAction) => + set((state) => ({ elementActions: state.elementActions.filter((a) => a !== action) })), })); +export const getStore = () => useStore.getState(); + +export const isElementExisted = (id: string) => getStore().elements.find((element) => element.id === id) !== undefined; + +export const getElementAction = (editMode: boolean) => { + const action = getStore().elementActions.find((a) => a.editMode === editMode); + if (action) { + getStore().deleteElementAction(action); + } + return action; +}; + export default useStore; diff --git a/frontend/taipy-gui/base/src/wsAdapter.ts b/frontend/taipy-gui/base/src/wsAdapter.ts index 5bdc4c6c47..676755db33 100644 --- a/frontend/taipy-gui/base/src/wsAdapter.ts +++ b/frontend/taipy-gui/base/src/wsAdapter.ts @@ -34,9 +34,10 @@ export class TaipyWsAdapter extends WsAdapter { for (const muPayload of message.payload as [MultipleUpdatePayload]) { const encodedName = muPayload.name; const { value } = muPayload.payload; + // eslint-disable-next-line @typescript-eslint/no-explicit-any if (value && (value as any).__taipy_refresh !== undefined) { // refresh all requested data for this encodedName var - const requestDataOptions = taipyApp.variableData?._requested_data[encodedName]; + const requestDataOptions = taipyApp.variableData?.getRequestedData()[encodedName]; for (const dataKey in requestDataOptions) { const requestDataEntry = requestDataOptions[dataKey]; const { options } = requestDataEntry; @@ -47,10 +48,10 @@ export class TaipyWsAdapter extends WsAdapter { const dataKey = getRequestedDataKey(muPayload.payload); taipyApp.variableData?.update(encodedName, value, dataKey); // call the callback if it exists for request data - if (dataKey && (encodedName in taipyApp._rdc && dataKey in taipyApp._rdc[encodedName])) { - const cb = taipyApp._rdc[encodedName]?.[dataKey]; + if (dataKey && encodedName in taipyApp.rdc && dataKey in taipyApp.rdc[encodedName]) { + const cb = taipyApp.rdc[encodedName]?.[dataKey]; cb(taipyApp, encodedName, dataKey, value); - delete taipyApp._rdc[encodedName][dataKey]; + delete taipyApp.rdc[encodedName][dataKey]; } taipyApp.onChangeEvent(encodedName, value, dataKey); } @@ -101,8 +102,8 @@ export class TaipyWsAdapter extends WsAdapter { taipyApp.onNotifyEvent(payload.atype, payload.message); } else if (message.type === "ACK") { const { id } = message as unknown as Record; - taipyApp._ackList = taipyApp._ackList.filter((v) => v !== id); - taipyApp.onWsStatusUpdateEvent(taipyApp._ackList); + taipyApp.ackList = taipyApp.ackList.filter((v) => v !== id); + taipyApp.onWsStatusUpdateEvent(taipyApp.ackList); } this.postWsMessageProcessing(message, taipyApp); return true; From 0ed1c155ac6bd973a02480a438809efc635d5fa1 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Mon, 24 Feb 2025 13:58:35 +0700 Subject: [PATCH 11/24] elementaction behavior update - with action prediction --- frontend/taipy-gui/base/src/app.ts | 8 ++- .../src/components/Taipy/TaipyElement.tsx | 34 +++++++++++- .../src/components/Taipy/TaipyRendered.tsx | 7 +-- .../src/components/Taipy/taipyelements.css | 11 ++++ .../base/src/packaging/taipy-gui-base.d.ts | 4 +- .../base/src/renderer/elementManager.ts | 4 +- frontend/taipy-gui/base/src/store.ts | 10 ++-- frontend/taipy-gui/base/webpack.config.js | 55 ++----------------- frontend/taipy-gui/package-lock.json | 1 + frontend/taipy-gui/package.json | 1 + frontend/taipy-gui/webpack.config.js | 52 ++---------------- 11 files changed, 70 insertions(+), 117 deletions(-) create mode 100644 frontend/taipy-gui/base/src/components/Taipy/taipyelements.css diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index e17b98f182..711d3dba17 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -333,8 +333,12 @@ export class TaipyApp { this.elementManager.setEditMode(bool); } - modifyElement(id: string, elemenetProperties: Element["properties"]) { - this.elementManager.modifyElement(id, elemenetProperties); + modifyElement(id: string, modifiedRecord: Record) { + if ("id" in modifiedRecord) { + console.error(`Error modifying element '${id}'. Cannot modify id of an existing element.`); + return; + } + this.elementManager.modifyElement(id, modifiedRecord); } deleteElement(id: string) { diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx index d6f2333663..5983d96d1b 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx @@ -1,16 +1,17 @@ -import React, { ComponentType, useContext, useEffect, useMemo, useState } from "react"; +import React, { ComponentType, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { ErrorBoundary } from "react-error-boundary"; import JsxParser from "react-jsx-parser"; import { PageContext, TaipyContext } from "../../../../src/context/taipyContext"; -import { Element } from "../../renderer/elementManager"; -import useStore from "../../store"; +import { Element, ElementActionEnum } from "../../renderer/elementManager"; +import useStore, { getElementAction } from "../../store"; import { getJsx } from "./utils"; import { emptyArray } from "../../../../src/utils"; import ErrorFallback from "../../../../src/utils/ErrorBoundary"; import { getRegisteredComponents } from "../../../../src/components/Taipy"; import { renderError, unregisteredRender } from "../../../../src/components/Taipy/Unregistered"; +import "./taipyelements.css"; interface TaipyElementProps { editMode: boolean; @@ -22,6 +23,7 @@ const TaipyElement = (props: TaipyElementProps) => { const [module, setModule] = useState(""); const [jsx, setJsx] = useState(""); const app = useStore((state) => state.app); + const prevElement = useRef(props.element); const renderConfig = useMemo( () => (props.editMode ? props.element.editModeRenderConfig : props.element.renderConfig), @@ -48,6 +50,32 @@ const TaipyElement = (props: TaipyElementProps) => { setJsxAsync(); }, [app, props.editMode, props.element, renderConfig]); + useEffect(() => { + if (prevElement.current === props.element) { + // Initial render + const potentialAction = { action: ElementActionEnum.Add, id: props.element.id, editMode: props.editMode }; + app && app.onCanvasReRenderEvent(props.editMode, getElementAction(potentialAction)); + } else { + // Modify render + const potentialAction = { + action: ElementActionEnum.Modify, + id: props.element.id, + editMode: props.editMode, + }; + app && app.onCanvasReRenderEvent(props.editMode, getElementAction(potentialAction)); + } + prevElement.current = props.element; + return () => { + // ComponenetWillUnmount --> delete element + const potentialAction = { + action: ElementActionEnum.Delete, + id: props.element.id, + editMode: props.editMode, + }; + app && app.onCanvasReRenderEvent(props.editMode, getElementAction(potentialAction)); + }; + }, [props.element, app, props.editMode]); + return renderConfig ? ( createPortal( diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx index b50cd8cd3c..9e3552adc8 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyRendered.tsx @@ -24,7 +24,7 @@ import { taipyInitialize, taipyReducer, } from "../../../../src/context/taipyReducers"; -import useStore, { getElementAction } from "../../store"; +import useStore from "../../store"; import TaipyElement from "./TaipyElement"; interface TaipyRenderedProps { @@ -34,7 +34,6 @@ interface TaipyRenderedProps { const TaipyRendered = (props: TaipyRenderedProps) => { const [state, dispatch] = useReducer(taipyReducer, INITIAL_STATE, taipyInitialize); const elements = useStore((state) => state.elements); - const app = useStore((state) => state.app); const themeClass = "taipy-" + state.theme.palette.mode; useEffect(() => { @@ -51,10 +50,6 @@ const TaipyRendered = (props: TaipyRenderedProps) => { document.body.className = classes.join(" "); }, [themeClass]); - useEffect(() => { - app && app.onCanvasReRenderEvent(props.editMode, getElementAction(props.editMode)); - }, [elements, app, props.editMode]); - return ( diff --git a/frontend/taipy-gui/base/src/components/Taipy/taipyelements.css b/frontend/taipy-gui/base/src/components/Taipy/taipyelements.css new file mode 100644 index 0000000000..9875198a7b --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/taipyelements.css @@ -0,0 +1,11 @@ +.taipy-designer-widget { + width: 100% !important; + height: 100% !important; +} + +.taipy-slider { + width: 100% !important; + height: 100% !important; + padding-left: 10px; + padding-right: 10px; +} diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index 7efadbece4..15533a8c60 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -125,7 +125,7 @@ declare class ElementManager { wrapper: CanvasRenderConfig["wrapper"], properties?: Element["properties"] | undefined, ): void; - modifyElement(id: string, elementProperties: Element["properties"]): void; + modifyElement(id: string, elementProperties: Record): void; deleteElement(id: string): void; } export type OnInitHandler = (taipyApp: TaipyApp) => void; @@ -216,7 +216,7 @@ export declare class TaipyApp { properties?: Element["properties"] | undefined, ): void; setCanvasEditMode(bool: boolean): void; - modifyElement(id: string, elemenetProperties: Element["properties"]): void; + modifyElement(id: string, modifiedRecord: Record): void; deleteElement(id: string): void; } export declare const createApp: ( diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/renderer/elementManager.ts index ebaea96075..d83850e16f 100644 --- a/frontend/taipy-gui/base/src/renderer/elementManager.ts +++ b/frontend/taipy-gui/base/src/renderer/elementManager.ts @@ -81,11 +81,11 @@ export class ElementManager { return; } // modify element if existed - getStore().addElementAction({ action: ElementActionEnum.Add, id, payload: properties }); + getStore().addElementAction({ action: ElementActionEnum.Modify, id, payload: properties }); getStore().editElement(id, { ...renderConfig, properties }); } - modifyElement(id: string, elementProperties: Element["properties"]) { + modifyElement(id: string, elementProperties: Record) { getStore().addElementAction({ action: ElementActionEnum.Modify, id, payload: elementProperties }); getStore().editElement(id, elementProperties); } diff --git a/frontend/taipy-gui/base/src/store.ts b/frontend/taipy-gui/base/src/store.ts index 1310c3bf99..5d44590cb4 100644 --- a/frontend/taipy-gui/base/src/store.ts +++ b/frontend/taipy-gui/base/src/store.ts @@ -10,7 +10,7 @@ interface TaipyGuiBaseState { setEditMode: (newEditMode: boolean) => void; setApp: (newApp: TaipyApp) => void; addElement: (newElement: Element) => void; - editElement: (id: string, payload: Element["properties"]) => void; + editElement: (id: string, payload: Record) => void; resetMainCanvas: () => void; deleteElement: (id: string) => void; addElementAction: (action: ElementAction) => void; @@ -25,7 +25,7 @@ export const useStore = create()((set) => ({ setEditMode: (newEditMode: boolean) => set(() => ({ editMode: newEditMode })), setApp: (newApp: TaipyApp) => set(() => ({ app: newApp })), addElement: (newElement: Element) => set((state) => ({ elements: [...state.elements, newElement] })), - editElement: (id: string, payload: Element["properties"]) => + editElement: (id: string, payload: Record) => set((state) => ({ elements: state.elements.map((element) => (element.id === id ? { ...element, ...payload } : element)), })), @@ -43,8 +43,10 @@ export const getStore = () => useStore.getState(); export const isElementExisted = (id: string) => getStore().elements.find((element) => element.id === id) !== undefined; -export const getElementAction = (editMode: boolean) => { - const action = getStore().elementActions.find((a) => a.editMode === editMode); +export const getElementAction = (elementAction: ElementAction) => { + const action = getStore().elementActions.find( + (a) => elementAction.id === a.id && elementAction.action === a.action && elementAction.editMode == a.editMode, + ); if (action) { getStore().deleteElementAction(action); } diff --git a/frontend/taipy-gui/base/webpack.config.js b/frontend/taipy-gui/base/webpack.config.js index ea376c752f..697bb43cb8 100644 --- a/frontend/taipy-gui/base/webpack.config.js +++ b/frontend/taipy-gui/base/webpack.config.js @@ -1,61 +1,12 @@ const path = require("path"); -const webpack = require("webpack"); 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", - }, - 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: "_", - // }, - // }, - }, { entry: "./base/src/exports.ts", output: { @@ -74,6 +25,10 @@ module.exports = [ use: "ts-loader", exclude: /node_modules/, }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + }, ], }, resolve: { @@ -85,7 +40,7 @@ module.exports = [ }), ], externals: { - "react": { + react: { commonjs: "react", commonjs2: "react", amd: "react", diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json index e703ffbc9f..03411d6b57 100644 --- a/frontend/taipy-gui/package-lock.json +++ b/frontend/taipy-gui/package-lock.json @@ -77,6 +77,7 @@ "lint-staged": "^15.0.2", "mock-socket": "^9.0.7", "mock-xmlhttprequest": "^8.2.0", + "style-loader": "^4.0.0", "ts-jest": "^29.0.0", "ts-jest-mock-import-meta": "^1.2.0", "ts-loader": "^9.2.6", diff --git a/frontend/taipy-gui/package.json b/frontend/taipy-gui/package.json index 19856fea75..a7c8b3ad6c 100644 --- a/frontend/taipy-gui/package.json +++ b/frontend/taipy-gui/package.json @@ -115,6 +115,7 @@ "lint-staged": "^15.0.2", "mock-socket": "^9.0.7", "mock-xmlhttprequest": "^8.2.0", + "style-loader": "^4.0.0", "ts-jest": "^29.0.0", "ts-jest-mock-import-meta": "^1.2.0", "ts-loader": "^9.2.6", diff --git a/frontend/taipy-gui/webpack.config.js b/frontend/taipy-gui/webpack.config.js index 07f1b71bf9..c3cef8f662 100644 --- a/frontend/taipy-gui/webpack.config.js +++ b/frontend/taipy-gui/webpack.config.js @@ -170,54 +170,6 @@ module.exports = (env, options) => { }]), ], }, - { - mode: options.mode, - target: "web", - entry: { - "default": "./base/src/index.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: taipyGuiBaseBundleName, - 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: "_", - // }, - // }, - }, { entry: "./base/src/exports.ts", output: { @@ -236,6 +188,10 @@ module.exports = (env, options) => { use: "ts-loader", exclude: /node_modules/, }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + }, ], }, resolve: { From 7927b1da5eb3e22a4951037b3baa4d22c2260d88 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Tue, 25 Feb 2025 16:52:46 +0700 Subject: [PATCH 12/24] add property editor handler for sidebar --- frontend/taipy-gui/base/src/app.ts | 18 +++++++++++++++--- .../base/src/components/Taipy/TaipyElement.tsx | 2 +- .../components/Taipy/TaipyElementEditor.tsx | 9 +++++++++ .../base/src/components/Taipy/utils.ts | 2 +- .../base/src/{renderer => element}/canvas.ts | 7 ++++++- .../{renderer => element}/elementManager.ts | 14 +++++++++++--- .../base/src/packaging/taipy-gui-base.d.ts | 12 ++++++++++-- frontend/taipy-gui/base/src/store.ts | 6 +++++- 8 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx rename frontend/taipy-gui/base/src/{renderer => element}/canvas.ts (85%) rename frontend/taipy-gui/base/src/{renderer => element}/elementManager.ts (89%) diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index 711d3dba17..6a229beda8 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -10,7 +10,7 @@ import { TaipyWsAdapter, WsAdapter } from "./wsAdapter"; import { WsMessageType } from "../../src/context/wsUtils"; import { getBase } from "./utils"; import { CookieHandler } from "./cookieHandler"; -import { CanvasRenderConfig, Element, ElementAction, ElementManager } from "./renderer/elementManager"; +import { CanvasRenderConfig, Element, ElementAction, ElementManager } from "./element/elementManager"; export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; @@ -315,8 +315,12 @@ export class TaipyApp { } // ElementManager API - createCanvas(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement) { - this.elementManager.init(canvasDomElement, canvasEditModeCanvas); + createCanvas( + canvasDomElement: HTMLElement, + canvasEditModeCanvas?: HTMLElement, + propertyEditorElement?: HTMLElement, + ) { + this.elementManager.init(canvasDomElement, canvasEditModeCanvas, propertyEditorElement); } addElement2Canvas( @@ -344,6 +348,14 @@ export class TaipyApp { deleteElement(id: string) { this.elementManager.deleteElement(id); } + + openPropertyEditor(id: string) { + this.elementManager.openPropertyEditor(id); + } + + closePropertyEditor() { + this.elementManager.closePropertyEditor(); + } } export const createApp = ( diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx index 5983d96d1b..57abaabea0 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElement.tsx @@ -4,7 +4,7 @@ import { ErrorBoundary } from "react-error-boundary"; import JsxParser from "react-jsx-parser"; import { PageContext, TaipyContext } from "../../../../src/context/taipyContext"; -import { Element, ElementActionEnum } from "../../renderer/elementManager"; +import { Element, ElementActionEnum } from "../../element/elementManager"; import useStore, { getElementAction } from "../../store"; import { getJsx } from "./utils"; import { emptyArray } from "../../../../src/utils"; diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx new file mode 100644 index 0000000000..efd283d6f7 --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import useStore from "../../store"; + +const TaipyElementEditor = () => { + const element = useStore((state) => state.selectedElement); + return element ?
{element.id}
:
No Element Selected
; +}; + +export default TaipyElementEditor; diff --git a/frontend/taipy-gui/base/src/components/Taipy/utils.ts b/frontend/taipy-gui/base/src/components/Taipy/utils.ts index f82d200535..eb056607f9 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/utils.ts +++ b/frontend/taipy-gui/base/src/components/Taipy/utils.ts @@ -1,6 +1,6 @@ import axios from "axios"; import { TaipyApp } from "../../app"; -import { Element } from "../../renderer/elementManager"; +import { Element } from "../../element/elementManager"; export const getJsx = async (taipyApp: TaipyApp, element: Element, editMode: boolean): Promise => { try { diff --git a/frontend/taipy-gui/base/src/renderer/canvas.ts b/frontend/taipy-gui/base/src/element/canvas.ts similarity index 85% rename from frontend/taipy-gui/base/src/renderer/canvas.ts rename to frontend/taipy-gui/base/src/element/canvas.ts index 0ae6166c38..1fb244dc8e 100644 --- a/frontend/taipy-gui/base/src/renderer/canvas.ts +++ b/frontend/taipy-gui/base/src/element/canvas.ts @@ -3,6 +3,7 @@ import { createRoot, Root } from "react-dom/client"; import { getStore } from "../store"; import { createElement } from "react"; import TaipyRendered from "../components/Taipy/TaipyRendered"; +import TaipyElementEditor from "../components/Taipy/TaipyElementEditor"; export class TaipyCanvas { taipyApp: TaipyApp; @@ -13,7 +14,7 @@ export class TaipyCanvas { this.taipyApp = taipyApp; } - init(canvasElement: HTMLElement, canvasEditModeElement?: HTMLElement) { + init(canvasElement: HTMLElement, canvasEditModeElement?: HTMLElement, propertyEditorElement?: HTMLElement) { if (!canvasElement) { console.error("Root element not found!"); return; @@ -23,6 +24,10 @@ export class TaipyCanvas { if (canvasEditModeElement) { this.initCanvas(canvasEditModeElement, true); getStore().setEditMode(true); + if (propertyEditorElement) { + const editorRoot = createRoot(propertyEditorElement); + editorRoot.render(createElement(TaipyElementEditor)); + } } } diff --git a/frontend/taipy-gui/base/src/renderer/elementManager.ts b/frontend/taipy-gui/base/src/element/elementManager.ts similarity index 89% rename from frontend/taipy-gui/base/src/renderer/elementManager.ts rename to frontend/taipy-gui/base/src/element/elementManager.ts index d83850e16f..54dbb8abc7 100644 --- a/frontend/taipy-gui/base/src/renderer/elementManager.ts +++ b/frontend/taipy-gui/base/src/element/elementManager.ts @@ -38,8 +38,8 @@ export class ElementManager { this.#canvas = new TaipyCanvas(taipyApp); } - init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement) { - this.#canvas.init(canvasDomElement, canvasEditModeCanvas); + init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement, propertyEditorElement?: HTMLElement) { + this.#canvas.init(canvasDomElement, canvasEditModeCanvas, propertyEditorElement); } setEditMode(editMode: boolean) { @@ -81,7 +81,7 @@ export class ElementManager { return; } // modify element if existed - getStore().addElementAction({ action: ElementActionEnum.Modify, id, payload: properties }); + getStore().addElementAction({ action: ElementActionEnum.Add, id, payload: properties }); getStore().editElement(id, { ...renderConfig, properties }); } @@ -94,4 +94,12 @@ export class ElementManager { getStore().addElementAction({ action: ElementActionEnum.Delete, id }); getStore().deleteElement(id); } + + openPropertyEditor(id: string) { + getStore().setSelectedElement(getStore().elements.find((element) => element.id === id)); + } + + closePropertyEditor() { + getStore().setSelectedElement(undefined); + } } diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts index 15533a8c60..e65f4fa461 100644 --- a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -116,7 +116,7 @@ declare class ElementManager { #private; taipyApp: TaipyApp; constructor(taipyApp: TaipyApp); - init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement): void; + init(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement, propertyEditorElement?: HTMLElement): void; setEditMode(editMode: boolean): void; addElement( type: string, @@ -127,6 +127,8 @@ declare class ElementManager { ): void; modifyElement(id: string, elementProperties: Record): void; deleteElement(id: string): void; + openPropertyEditor(id: string): void; + closePropertyEditor(): void; } export type OnInitHandler = (taipyApp: TaipyApp) => void; export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown, dataEventKey?: string) => void; @@ -207,7 +209,11 @@ export declare class TaipyApp { getPageMetadata(): Record; getWsStatus(): string[]; getBaseUrl(): string; - createCanvas(canvasDomElement: HTMLElement, canvasEditModeCanvas?: HTMLElement): void; + createCanvas( + canvasDomElement: HTMLElement, + canvasEditModeCanvas?: HTMLElement, + propertyEditorElement?: HTMLElement, + ): void; addElement2Canvas( type: string, id: string, @@ -218,6 +224,8 @@ export declare class TaipyApp { setCanvasEditMode(bool: boolean): void; modifyElement(id: string, modifiedRecord: Record): void; deleteElement(id: string): void; + openPropertyEditor(id: string): void; + closePropertyEditor(): void; } export declare const createApp: ( onInit?: OnInitHandler, diff --git a/frontend/taipy-gui/base/src/store.ts b/frontend/taipy-gui/base/src/store.ts index 5d44590cb4..579bf1aaa6 100644 --- a/frontend/taipy-gui/base/src/store.ts +++ b/frontend/taipy-gui/base/src/store.ts @@ -1,12 +1,13 @@ import { create } from "zustand"; import { TaipyApp } from "./app"; -import { Element, ElementAction } from "./renderer/elementManager"; +import { Element, ElementAction } from "./element/elementManager"; interface TaipyGuiBaseState { editMode: boolean; app?: TaipyApp; elements: Element[]; elementActions: ElementAction[]; + selectedElement?: Element; setEditMode: (newEditMode: boolean) => void; setApp: (newApp: TaipyApp) => void; addElement: (newElement: Element) => void; @@ -15,6 +16,7 @@ interface TaipyGuiBaseState { deleteElement: (id: string) => void; addElementAction: (action: ElementAction) => void; deleteElementAction: (action: ElementAction) => void; + setSelectedElement: (action: Element | undefined) => void; } export const useStore = create()((set) => ({ @@ -22,6 +24,7 @@ export const useStore = create()((set) => ({ app: undefined, elements: [], elementActions: [], + selectedElement: undefined, setEditMode: (newEditMode: boolean) => set(() => ({ editMode: newEditMode })), setApp: (newApp: TaipyApp) => set(() => ({ app: newApp })), addElement: (newElement: Element) => set((state) => ({ elements: [...state.elements, newElement] })), @@ -37,6 +40,7 @@ export const useStore = create()((set) => ({ set((state) => ({ elementActions: [...state.elementActions, { ...action, editMode: state.editMode }] })), deleteElementAction: (action: ElementAction) => set((state) => ({ elementActions: state.elementActions.filter((a) => a !== action) })), + setSelectedElement: (element: Element | undefined) => set(() => ({ selectedElement: element })), })); export const getStore = () => useStore.getState(); From 98ce90a2363352416bd2b9cd3e15d78998ffcc19 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Fri, 28 Feb 2025 14:31:52 +0700 Subject: [PATCH 13/24] property selection --- frontend/taipy-gui/base/src/app.ts | 4 + .../components/Taipy/TaipyElementEditor.tsx | 124 +- .../Taipy/components/BindingInput.tsx | 14 + .../Taipy/components/ExpressionInput.tsx | 8 + .../Taipy/components/NumberInput.tsx | 27 + .../Taipy/components/StringInput.tsx | 0 .../Taipy/components/TaipyPropertyHandler.tsx | 90 + .../base/src/element/VisElementParser.ts | 179 ++ .../base/src/element/elementManager.ts | 16 +- .../base/src/element/viselements.json | 2056 +++++++++++++++++ frontend/taipy-gui/base/webpack.config.js | 4 + frontend/taipy-gui/package-lock.json | 138 +- frontend/taipy-gui/package.json | 1 + frontend/taipy-gui/webpack.config.js | 4 + 14 files changed, 2660 insertions(+), 5 deletions(-) create mode 100644 frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx create mode 100644 frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx create mode 100644 frontend/taipy-gui/base/src/components/Taipy/components/NumberInput.tsx create mode 100644 frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx create mode 100644 frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx create mode 100644 frontend/taipy-gui/base/src/element/VisElementParser.ts create mode 100644 frontend/taipy-gui/base/src/element/viselements.json diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index 6a229beda8..fa96ab6a83 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -345,6 +345,10 @@ export class TaipyApp { this.elementManager.modifyElement(id, modifiedRecord); } + modifyElementProperties(id: string, properties: Record) { + this.elementManager.modifyElementProperties(id, properties); + } + deleteElement(id: string) { this.elementManager.deleteElement(id); } diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx index efd283d6f7..686753e96d 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx @@ -1,9 +1,129 @@ -import React from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import useStore from "../../store"; +import VisElementParser, { VisElementDetails } from "../../element/VisElementParser"; +import TaipyPropertyHandler from "./components/TaipyPropertyHandler"; +import { Box, Button, createTheme, CssBaseline, ThemeProvider } from "@mui/material"; + +const darkTheme = createTheme({ + palette: { + mode: "dark", + }, +}); const TaipyElementEditor = () => { const element = useStore((state) => state.selectedElement); - return element ?
{element.id}
:
No Element Selected
; + const app = useStore((state) => state.app); + const [modifiedProperties, setModifiedProperties] = useState>({}); + const elementProperties = useMemo(() => element?.properties, [element]); + const [availableProperties, propertyOrder] = useMemo( + () => + element + ? VisElementParser.getInstance().getDesignerProperty(element.type) + : ([{}, []] as VisElementDetails), + [element], + ); + const defaultProperties = useMemo( + () => + propertyOrder.reduce( + (obj: Record, k: string) => { + obj[k] = undefined; + return obj; + }, + {} as Record, + ), + [propertyOrder], + ); + const actionButtonActiveStatus = useMemo(() => { + // compare modified properties with element properties + if (!elementProperties || !modifiedProperties) { + return false; + } + const elementPropertiesFilled: Record = { ...defaultProperties, ...elementProperties }; + if (modifiedProperties.length !== elementPropertiesFilled.length) { + return false; + } + for (const key in modifiedProperties) { + if (modifiedProperties[key] !== elementPropertiesFilled[key]) { + return true; + } + } + return false; + }, [modifiedProperties, elementProperties, defaultProperties]); + + // USE_EFFECT + // update property selection everytime element property changes + useEffect(() => { + setModifiedProperties({ ...defaultProperties, ...elementProperties }); + }, [elementProperties, defaultProperties]); + + // CALLBACKS + // update local selection + const updateModifiedProperties = useCallback( + (propertyName: string) => (value: unknown) => { + setModifiedProperties((prev) => ({ ...prev, [propertyName]: value })); + }, + [], + ); + + // push modification to main app + const modifyElement = useCallback(() => { + if (element) { + // filter out undefined properties + const filteredModifiedProperties = Object.keys(modifiedProperties).reduce( + (obj: Record, key) => { + if (modifiedProperties[key] !== undefined) { + obj[key] = modifiedProperties[key]; + } + return obj; + }, + {}, + ); + app?.modifyElementProperties(element.id, filteredModifiedProperties); + } + }, [app, modifiedProperties, element]); + + // reset local selection + const resetModifiedProperties = useCallback(() => { + setModifiedProperties({ ...defaultProperties, ...elementProperties }); + }, [elementProperties, defaultProperties]); + + // RENDER + return element ? ( + + + +

Properties for {element.type} widget

+
+ + {propertyOrder.map((propertyName) => ( + + ))} + + + + + +
+ ) : ( +

No Taipy Element Selected

+ ); }; export default TaipyElementEditor; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx new file mode 100644 index 0000000000..ae4d8dec0e --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { TaipyElementInput } from "./TaipyPropertyHandler"; +import { Box } from "@mui/material"; + +const common = (props: TaipyElementInput, bindingInfo: Record) => { + return Input; +}; +export const BindingInput = (props: TaipyElementInput) => { + return common(props, {}); +}; + +export const FunctionBindingInput = (props: TaipyElementInput) => { + return common(props, {}); +}; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx new file mode 100644 index 0000000000..640932dffa --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { TaipyElementInput } from "./TaipyPropertyHandler"; + +const ExpressionInput = (props: TaipyElementInput) => { + return <>BindingInput; +}; + +export default ExpressionInput; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/NumberInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/NumberInput.tsx new file mode 100644 index 0000000000..fb3585e79d --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/components/NumberInput.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { TaipyElementInput } from "./TaipyPropertyHandler"; +import { Box, TextField } from "@mui/material"; + +const NumberInput = (props: TaipyElementInput) => { + const { value, defaultValue, onChange } = props; + const inputOnChange = (event: React.ChangeEvent) => { + onChange(event.target.value); + }; + return ( + + + + ); +}; + +export default NumberInput; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx new file mode 100644 index 0000000000..47730feb08 --- /dev/null +++ b/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx @@ -0,0 +1,90 @@ +import { Box, IconButton, Typography } from "@mui/material"; +import React, { useCallback, useMemo, useState } from "react"; +import Tooltip from "@mui/material/Tooltip"; +import InfoIcon from "@mui/icons-material/Info"; +import ChangeCircleIcon from "@mui/icons-material/ChangeCircle"; +import parse from "html-react-parser"; +import NumberInput from "./NumberInput"; +import { BindingInput, FunctionBindingInput } from "./BindingInput"; +import ExpressionInput from "./ExpressionInput"; + +interface TaipyPropertyHandlerProps { + name: string; + value: unknown; + onChange: (value: unknown) => void; + inputTypes: string[]; + description: string; + defaultValue?: unknown; +} + +export interface TaipyElementInput { + value: TaipyPropertyHandlerProps["value"]; + defaultValue?: TaipyPropertyHandlerProps["defaultValue"]; + onChange: TaipyPropertyHandlerProps["onChange"]; +} + +const elementTypeComponentMap: Record> = { + number: NumberInput, + binding: BindingInput, + expression: ExpressionInput, + functionbinding: FunctionBindingInput, +}; + +const inlineElementTypeComponenetMap: Record> = { + boolean: BindingInput, +}; + +const tooltipPropsSx = { + tooltip: { + sx: { + fontSize: 16, + }, + }, +}; + +const TaipyPropertyHandler = (props: TaipyPropertyHandlerProps) => { + const { name, value, onChange, inputTypes, description, defaultValue } = props; + const [type, setType] = useState(inputTypes[0]); + const ElementTypeComponent = useMemo(() => elementTypeComponentMap[type], [type]); + const InlineElementTypeComponent = useMemo(() => inlineElementTypeComponenetMap[type], [type]); + + const switchType = useCallback(() => { + setType((prev) => { + const index = inputTypes.indexOf(prev); + return inputTypes[(index + 1) % inputTypes.length]; + }); + }, [inputTypes]); + + return ( + <> + + + {name} + + + + + {inputTypes.length > 1 ? ( + + + + + + ) : ( + <> + )} + {InlineElementTypeComponent ? ( + + ) : ( + <> + )} + + {ElementTypeComponent ? ( + + ) : ( + <> + )} + + ); +}; +export default TaipyPropertyHandler; diff --git a/frontend/taipy-gui/base/src/element/VisElementParser.ts b/frontend/taipy-gui/base/src/element/VisElementParser.ts new file mode 100644 index 0000000000..a3f6ef15dc --- /dev/null +++ b/frontend/taipy-gui/base/src/element/VisElementParser.ts @@ -0,0 +1,179 @@ +import viselementsJson from "./viselements.json"; + +interface UnparsedVisElements { + blocks?: [string, ElementDetail][]; + controls?: [string, ElementDetail][]; + undocumented?: [string, ElementDetail][]; +} + +interface ElementDetail { + properties?: ElementProperty[]; + inherits?: string[]; +} + +interface ElementProperty { + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default_property?: any; + default_value?: unknown; + type: string; + doc: string; + signature?: [string, string][]; + designer_input_types: string[]; +} + +type VisElementProperties = Record; + +type VisElementPropertyOrder = ElementProperty["name"][]; + +export type VisElementDetails = [VisElementProperties, VisElementPropertyOrder]; + +type VisElements = Record; + +class VisElementParser { + private static instance: VisElementParser; + private visElements: VisElements; + + private constructor() { + this.visElements = VisElementParser.parseVisualElements(); + } + + private static parseVisualElements(): VisElements { + const unparsedVisElements: UnparsedVisElements = viselementsJson as UnparsedVisElements; + const blocks: Record = (unparsedVisElements["blocks"] || []).reduce( + (obj: Record, v: [string, ElementDetail]) => { + obj[v[0]] = v[1]; + return obj; + }, + {} as Record, + ); + const controls: Record = (unparsedVisElements["controls"] || []).reduce( + (obj: Record, v: [string, ElementDetail]) => { + obj[v[0]] = v[1]; + return obj; + }, + {} as Record, + ); + const undocumented: Record = (unparsedVisElements["undocumented"] || []).reduce( + (obj: Record, v: [string, ElementDetail]) => { + obj[v[0]] = v[1]; + return obj; + }, + {} as Record, + ); + const blocksProperties: Record = {}; + const controlsProperties: Record = {}; + // handle all blocks object + Object.keys(blocks).forEach((v: string) => { + const elementDetail: ElementDetail = blocks[v]; + blocksProperties[v] = VisElementParser.getElementDetailProperties( + elementDetail, + blocks, + controls, + undocumented, + ); + }); + Object.keys(controls).forEach((v: string) => { + const elementDetail: ElementDetail = controls[v]; + controlsProperties[v] = VisElementParser.getElementDetailProperties( + elementDetail, + blocks, + controls, + undocumented, + ); + }); + return { ...blocksProperties, ...controlsProperties }; + } + + private static getElementDetailProperties = ( + elementDetail: ElementDetail, + blocks: Record, + controls: Record, + undocumented: Record, + ): VisElementDetails => { + const [inheritedProperties, inheritedPropertyOrder] = VisElementParser.handleElementDetailInherits( + elementDetail.inherits, + blocks, + controls, + undocumented, + ); + const [propertyList, propertyOrder] = this.parsePropertyList(elementDetail.properties); + return [ + { ...inheritedProperties, ...propertyList }, + [...new Set([...propertyOrder, ...inheritedPropertyOrder])], + ]; + }; + + private static parsePropertyList(propertyList: ElementProperty[] | undefined): VisElementDetails { + if (!propertyList) { + return [{}, []]; + } + return [ + propertyList.reduce((obj: VisElementProperties, v: ElementProperty) => { + obj[v.name] = v; + return obj; + }, {} as VisElementProperties), + propertyList.map((v) => v.name), + ]; + } + + private static handleElementDetailInherits( + inherits: string[] | undefined, + blocks: Record, + controls: Record, + undocumented: Record, + ): VisElementDetails { + if (!inherits) { + return [{}, []]; + } + let properties: VisElementProperties = {}; + let propertyOrder: ElementProperty["name"][] = []; + inherits.forEach((v) => { + let elementDetail: ElementDetail; + if (v in undocumented) { + elementDetail = undocumented[v]; + } else if (v in controls) { + elementDetail = controls[v]; + } else { + elementDetail = blocks[v]; + } + const [elementProperties, elementPropertyOrder] = VisElementParser.getElementDetailProperties( + elementDetail, + blocks, + controls, + undocumented, + ); + propertyOrder = [...propertyOrder, ...elementPropertyOrder]; + properties = { + ...elementProperties, + ...properties, + }; + }); + return [properties, propertyOrder]; + } + + public static getInstance(): VisElementParser { + if (!VisElementParser.instance) { + VisElementParser.instance = new VisElementParser(); + } + return VisElementParser.instance; + } + + public getDesignerProperty(elementType: string): VisElementDetails { + const [properties, propertyOrder] = this.visElements[elementType]; + if (!properties) { + console.error(`Element type ${elementType} not found in visual elements`); + return [{}, []]; + } + const filteredProperties = propertyOrder.reduce((obj: VisElementProperties, key: string) => { + const elementProperty = properties[key]; + if ("designer_input_types" in elementProperty) { + obj[key] = elementProperty; + } + return obj; + }, {} as VisElementProperties); + return [filteredProperties, propertyOrder.filter((v) => v in filteredProperties)]; + } +} + +export default VisElementParser; diff --git a/frontend/taipy-gui/base/src/element/elementManager.ts b/frontend/taipy-gui/base/src/element/elementManager.ts index 54dbb8abc7..e3bc4616ec 100644 --- a/frontend/taipy-gui/base/src/element/elementManager.ts +++ b/frontend/taipy-gui/base/src/element/elementManager.ts @@ -61,6 +61,9 @@ export class ElementManager { wrapper: CanvasRenderConfig["wrapper"], properties: Element["properties"] | undefined = undefined, ) { + if (properties === undefined) { + properties = {}; + } const root = document.getElementById(rootId); if (!root) { console.error(`Root element with id '${rootId}' not found!`); @@ -81,8 +84,12 @@ export class ElementManager { return; } // modify element if existed - getStore().addElementAction({ action: ElementActionEnum.Add, id, payload: properties }); - getStore().editElement(id, { ...renderConfig, properties }); + const editedProperties = { + ...getStore().elements.find((element) => element.id === id)?.properties, + ...properties, + }; + getStore().addElementAction({ action: ElementActionEnum.Add, id, payload: editedProperties }); + getStore().editElement(id, { ...renderConfig, properties: editedProperties }); } modifyElement(id: string, elementProperties: Record) { @@ -90,6 +97,11 @@ export class ElementManager { getStore().editElement(id, elementProperties); } + modifyElementProperties(id: string, payload: Record) { + const properties = { ...getStore().elements.find((element) => element.id === id)?.properties, ...payload }; + this.modifyElement(id, { properties }); + } + deleteElement(id: string) { getStore().addElementAction({ action: ElementActionEnum.Delete, id }); getStore().deleteElement(id); diff --git a/frontend/taipy-gui/base/src/element/viselements.json b/frontend/taipy-gui/base/src/element/viselements.json new file mode 100644 index 0000000000..e917769782 --- /dev/null +++ b/frontend/taipy-gui/base/src/element/viselements.json @@ -0,0 +1,2056 @@ +{ + "controls": [ + [ + "text", + { + "inherits": ["shared"], + "properties": [ + { + "name": "value", + "default_property": true, + "type": "dynamic(Any)", + "default_value": "\"\"", + "doc": "The value displayed as text by this control." + }, + { + "name": "raw", + "type": "bool", + "default_value": "False", + "doc": "If set to True, the component renders as an HTML <span> element without any default style." + }, + { + "name": "mode", + "type": "str", + "doc": "Define the way the text is processed:\n
  • "raw": synonym for setting the raw property to True
  • "pre": keeps spaces and new lines
  • "markdown" or "md": basic support for Markdown
  • "latex": LaTeχ support
" + }, + { + "name": "format", + "type": "str", + "doc": "The format to apply to the value.
See below." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the text element, in CSS units." + } + ] + } + ], + [ + "button", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "label", + "default_property": true, + "type": "dynamic(Union[str,Icon])", + "default_value": "\"\"", + "doc": "The label displayed in the button." + }, + { + "name": "size", + "type": "str", + "default_value": "\"medium\"", + "doc": "The size of the button. Valid values: \"small\", \"medium\", or \"large\"." + }, + { + "name": "variant", + "type": "str", + "default_value": "\"outlined\"", + "doc": "The variant of the button. Valid values: \"contained\", \"outlined\", or \"text\"." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the button is pressed.
This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the button it it has one.
  • payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the button element." + } + ] + } + ], + [ + "input", + { + "inherits": ["sharedInput", "on_change", "propagate"], + "properties": [ + { + "name": "value", + "default_property": true, + "type": "dynamic(Any)", + "default_value": "None", + "doc": "The value represented by this control." + }, + { + "name": "password", + "type": "bool", + "default_value": "False", + "doc": "If True, the text is obscured, and all characters are displayed as asterisks ('*').
This can be useful for sensitive information such as passwords." + }, + { + "name": "label", + "type": "str", + "default_value": "None", + "doc": "The label associated with the input field.
This provides context to the user and improves accessibility." + }, + { + "name": "multiline", + "type": "bool", + "default_value": "False", + "doc": "If True, the input is rendered as a multi-line text area
The default behavior is a single-line input." + }, + { + "name": "lines_shown", + "type": "int", + "default_value": "5", + "doc": "The number of lines displayed in the input control when multiline is True." + }, + { + "name": "type", + "type": "str", + "default_value": "\"text\"", + "doc": "The type of input element, as per HTML input types.
This property enforces specific input formats where applicable. Supported values include \"text\", \"tel\", \"email\", \"url\", etc." + }, + { + "name": "action_on_blur", + "type": "bool", + "default_value": "False", + "doc": "If True, the on_action callback is triggered when the input control looses keyboard focus (e.g., when the user presses the Tab key). When this happens, the key name for the event (set in the args property of the payload parameter to the callback function) is set to \"Tab\"." + } + ] + } + ], + [ + "number", + { + "inherits": ["sharedInput", "on_change", "propagate"], + "properties": [ + { + "name": "value", + "default_property": true, + "type": "dynamic(Any)", + "doc": "The numerical value represented by this control." + }, + { + "name": "label", + "type": "str", + "default_value": "None", + "doc": "The label associated with the number field.
This provides context to the user and improves accessibility." + }, + { + "name": "step", + "type": "dynamic(Union[int,float])", + "default_value": "1", + "doc": "The increment or decrement applied to the value when the user clicks the arrow buttons." + }, + { + "name": "step_multiplier", + "type": "dynamic(Union[int,float])", + "default_value": "10", + "doc": "The factor by which the step value is multiplied when the user holds the Shift key while clicking the arrow buttons." + }, + { + "name": "min", + "type": "dynamic(Union[int,float])", + "doc": "The minimum acceptable value.
Values below this threshold are invalid." + }, + { + "name": "max", + "type": "dynamic(Union[int,float])", + "doc": "The maximum acceptable value.
Values above this threshold are invalid." + }, + { + "name": "action_on_blur", + "type": "bool", + "default_value": "False", + "doc": "If True, the on_action callback is triggered when the number control looses keyboard focus (e.g., when the user presses the Tab key). When this happens, the key name for the event (set in the args property of the payload parameter to the callback function) is set to \"Tab\"." + } + ] + } + ], + [ + "slider", + { + "inherits": ["lovComp", "propagate"], + "properties": [ + { + "name": "value", + "default_property": true, + "type": "dynamic(Union[int,float,str,list[int],list[float],list[str]])", + "doc": "The value that is set for this slider.
If this slider is based on a lov then this property can be set to the lov element.
This value can also hold an array of numbers to indicate that the slider reflects a range (within the [min,max] domain) defined by several knobs that the user can set independently.
If this slider is based on a lov then this property can be set to an array of lov elements. The slider is then represented with several knobs, one for each lov value.", + "designer_input_types": ["binding", "expression"] + }, + { + "name": "min", + "type": "Union[int,float]", + "default_value": "0", + "doc": "The minimum value.
This is ignored when lov is defined.", + "designer_input_types": ["number"] + }, + { + "name": "max", + "type": "Union[int,float]", + "default_value": "100", + "doc": "The maximum value.
This is ignored when lov is defined.", + "designer_input_types": ["number"] + }, + { + "name": "step", + "type": "Union[int,float]", + "default_value": "1", + "doc": "The step value, which is the gap between two consecutive values the slider set. It is a good practice to have (max-min) being divisible by step.
This property is ignored when lov is defined." + }, + { + "name": "text_anchor", + "type": "str", + "default_value": "\"bottom\"", + "doc": "When the lov property is used, this property indicates the location of the label.
Possible values are:\n
    \n
  • \"bottom\"
  • \"top\"
  • \"left\"
  • \"right\"
  • \"none\" (no label is displayed)
" + }, + { + "name": "labels", + "type": "Union[bool,dict[str,str]]", + "doc": "The labels for specific points of the slider.
If set to True, this slider uses the labels of the lov if there are any.
If set to a dictionary, the slider uses the dictionary keys as a lov key or index, and the associated value as the label." + }, + { + "name": "continuous", + "type": "bool", + "default_value": "True", + "doc": "If set to False, the control emits an on_change notification only when the mouse button is released, otherwise notifications are emitted during the cursor movements.
If lov is defined, the default value is False." + }, + { + "name": "change_delay", + "type": "int", + "default_value": "App config", + "doc": "Minimum time between triggering two on_change callbacks.
The default value is defined at the application configuration level by the change_delay configuration option. if None or 0, there's no delay." + }, + { + "name": "width", + "type": "str", + "default_value": "\"300px\"", + "doc": "The width of the slider, in CSS units." + }, + { + "name": "height", + "type": "str", + "doc": "The height of the slider, in CSS units.
It defaults to the value of width when using the vertical orientation." + }, + { + "name": "orientation", + "type": "str", + "default_value": "\"horizontal\"", + "doc": "The orientation of the slider.
Valid values are \"horizontal\" or \"vertical\"." + } + ] + } + ], + [ + "toggle", + { + "inherits": ["lovComp", "propagate"], + "properties": [ + { + "name": "value" + }, + { + "name": "theme", + "type": "bool", + "default_value": "False", + "doc": "If set, this toggle control acts as a way to set the application Theme (dark or light)." + }, + { + "name": "allow_unselect", + "type": "bool", + "default_value": "False", + "doc": "If set, this allows de-selection and the value is set to unselected_value." + }, + { + "name": "mode", + "type": "str", + "doc": "Define the way the toggle is displayed:\n
  • "theme": synonym for setting the theme property to True
" + }, + { + "name": "label", + "type": "str", + "doc": "The label associated with the toggle." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the element." + } + ] + } + ], + [ + "date", + { + "inherits": ["on_change", "propagate"], + "properties": [ + { + "name": "date", + "default_property": true, + "type": "dynamic(datetime)", + "doc": "The date that this control represents and can modify.
It is typically bound to a datetime object." + }, + { + "name": "with_time", + "type": "bool", + "default_value": "False", + "doc": "Whether or not to show the time part of the date." + }, + { + "name": "format", + "type": "str", + "doc": "The format to apply to the value. See below." + }, + { + "name": "editable", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "Shows the date as a formatted string if not editable." + }, + { + "name": "label", + "type": "str", + "doc": "The label associated with the input." + }, + { + "name": "min", + "type": "dynamic(datetime)", + "doc": "The minimum date to accept for this input." + }, + { + "name": "max", + "type": "dynamic(datetime)", + "doc": "The maximum date to accept for this input." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the date element." + }, + { + "name": "analogic", + "type": "bool", + "default_value": "False", + "doc": "Whether or not to show timepicker as a clock." + } + ] + } + ], + [ + "time", + { + "inherits": ["on_change", "propagate"], + "properties": [ + { + "name": "analogic", + "type": "bool", + "default_value": "False", + "doc": "Whether or not to show timepicker as a clock." + }, + { + "name": "format", + "type": "str", + "doc": "The format to apply to the value. See below." + }, + { + "name": "editable", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "Shows the time as a formatted string if not editable." + }, + { + "name": "time", + "default_property": true, + "type": "dynamic(str)", + "doc": "The time that this control represents and can modify" + }, + { + "name": "label", + "type": "str", + "doc": "The label associated with the input." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the time element." + } + ] + } + ], + [ + "date_range", + { + "inherits": ["on_change", "propagate"], + "properties": [ + { + "name": "dates", + "default_property": true, + "type": "dynamic(list[datetime])", + "doc": "The dates that this control represents and can modify.
It is typically bound to a list of two datetime object." + }, + { + "name": "with_time", + "type": "bool", + "default_value": "False", + "doc": "Whether or not to show the time part of the date." + }, + { + "name": "format", + "type": "str", + "doc": "The format to apply to the value. See below." + }, + { + "name": "editable", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "Shows the date as a formatted string if not editable." + }, + { + "name": "label_start", + "type": "str", + "doc": "The label associated with the first input." + }, + { + "name": "label_end", + "type": "str", + "doc": "The label associated with the second input." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the date_range element." + }, + { + "name": "analogic", + "type": "bool", + "default_value": "False", + "doc": "Whether or not to show timepicker as a clock." + } + ] + } + ], + [ + "chart", + { + "inherits": ["on_change", "propagate"], + "properties": [ + { + "name": "data", + "default_property": true, + "required": true, + "type": "indexed(dynamic(Any))", + "doc": "The data object bound to this chart control.
See the section on the data property below for more details." + }, + { + "name": "type", + "type": "indexed(str)", + "default_value": "\"scatter\"", + "doc": "Chart type.
See the Plotly chart type documentation for more details." + }, + { + "name": "mode", + "type": "indexed(str)", + "default_value": "\"lines+markers\"", + "doc": "Chart mode.
See the Plotly chart mode documentation for more details." + }, + { + "name": "x", + "type": "indexed(str)", + "doc": "Column name for the x axis." + }, + { + "name": "y", + "type": "indexed(str)", + "doc": "Column name for the y axis." + }, + { + "name": "z", + "type": "indexed(str)", + "doc": "Column name for the z axis." + }, + { + "name": "lon", + "type": "indexed(str)", + "doc": "Column name for the longitude value, for 'scattergeo' charts. See Plotly Map traces." + }, + { + "name": "lat", + "type": "indexed(str)", + "doc": "Column name for the latitude value, for 'scattergeo' charts. See Plotly Map traces." + }, + { + "name": "r", + "type": "indexed(str)", + "doc": "Column name for the r value, for 'scatterpolar' charts. See Plotly Polar charts." + }, + { + "name": "theta", + "type": "indexed(str)", + "doc": "Column name for the theta value, for 'scatterpolar' charts. See Plotly Polar charts." + }, + { + "name": "high", + "type": "indexed(str)", + "doc": "Column name for the high value, for 'candlestick' charts. See Plotly Candlestick charts." + }, + { + "name": "low", + "type": "indexed(str)", + "doc": "Column name for the low value, for 'candlestick' charts. See Ploty Candlestick charts." + }, + { + "name": "open", + "type": "indexed(str)", + "doc": "Column name for the open value, for 'candlestick' charts. See Plotly Candlestick charts." + }, + { + "name": "close", + "type": "indexed(str)", + "doc": "Column name for the close value, for 'candlestick' charts. See Plotly Candlestick charts." + }, + { + "name": "measure", + "type": "indexed(str)", + "doc": "Column name for the measure value, for 'waterfall' charts. See Plotly Waterfall charts." + }, + { + "name": "locations", + "type": "indexed(str)", + "doc": "Column name for the locations value. See Plotly Choropleth maps." + }, + { + "name": "values", + "type": "indexed(str)", + "doc": "Column name for the values value. See Plotly Pie charts or Plotly Funnel Area charts." + }, + { + "name": "labels", + "type": "indexed(str)", + "doc": "Column name for the labels value. See Plotly Pie charts." + }, + { + "name": "parents", + "type": "indexed(str)", + "doc": "Column name for the parents value. See Plotly Treemap charts." + }, + { + "name": "text", + "type": "indexed(str)", + "doc": "Column name for the text associated to the point for the indicated trace.
This is meaningful only when mode has the text option." + }, + { + "name": "base", + "type": "indexed(str)", + "doc": "Column name for the base value. Used in bar charts only.
See the Plotly bar chart base documentation for more details.\"" + }, + { + "name": "title", + "type": "str", + "doc": "The title of the chart control." + }, + { + "name": "render", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "If True, this chart is visible on the page." + }, + { + "name": "on_range_change", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the visible part of the x axis changes.
This function is invoked with the following parameters:
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the chart control if it has one.
  • payload (dict[str, Any]): the full details on this callback's invocation, as emitted by Plotly.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "columns", + "type": "Union[str,list[str],dict[str,dict[str,str]]]", + "default_value": "All columns", + "doc": "The list of column names to represent.\n
    \n
  • str: ;-separated list of column names
  • list[str]: list of names
  • dict: {\"column_name\": {format: \"format\", index: 1}} if index is specified, it represents the display order of the columns.\nIf not, the list order defines the index

If columns is omitted or set to None, all columns of data are represented." + }, + { + "name": "label", + "type": "indexed(str)", + "doc": "The label for the indicated trace.
This is used when the mouse hovers over a trace." + }, + { + "name": "name", + "type": "indexed(str)", + "doc": "The name of the indicated trace." + }, + { + "name": "selected", + "type": "indexed(dynamic(Union[list[int],str]))", + "doc": "The list of the selected point indices ." + }, + { + "name": "color", + "type": "indexed(str)", + "doc": "The color of the indicated trace (or a column name for scattered)." + }, + { + "name": "selected_color", + "type": "indexed(str)", + "doc": "The color of the selected points for the indicated trace." + }, + { + "name": "marker", + "type": "indexed(dict[str, Any])", + "doc": "The type of markers used for the indicated trace.
See marker for more details.
Color, opacity, size and symbol can be column names." + }, + { + "name": "line", + "type": "indexed(Union[str,dict[str,Any]])", + "doc": "The configuration of the line used for the indicated trace.
See line for more details.
If the value is a string, it must be a dash type or pattern (see dash style of lines for more details)." + }, + { + "name": "selected_marker", + "type": "indexed(dict[str, Any])", + "doc": "The type of markers used for selected points in the indicated trace.
See selected marker for more details." + }, + { + "name": "layout", + "type": "dynamic(dict[str, Any])", + "doc": "The plotly.js compatible layout object." + }, + { + "name": "plot_config", + "type": "dict[str, Any]", + "doc": "The plotly.js compatible configuration options object." + }, + { + "name": "options", + "type": "indexed(dict[str, Any])", + "doc": "The plotly.js compatible data object where dynamic data will be overridden.." + }, + { + "name": "orientation", + "type": "indexed(str)", + "doc": "The orientation of the indicated trace." + }, + { + "name": "text_anchor", + "type": "indexed(str)", + "doc": "Position of the text relative to the point.
Valid values are: top, bottom, left, and right." + }, + { + "name": "xaxis", + "type": "indexed(str)", + "doc": "The x axis identifier for the indicated trace." + }, + { + "name": "yaxis", + "type": "indexed(str)", + "doc": "The y axis identifier for the indicated trace." + }, + { + "name": "width", + "type": "Union[str,int,float]", + "default_value": "\"100%\"", + "doc": "The width of the chart, in CSS units." + }, + { + "name": "height", + "type": "Union[str,int,float]", + "doc": "The height of the chart, in CSS units." + }, + { + "name": "template", + "type": "dict", + "doc": "The Plotly layout template." + }, + { + "name": "template[dark]", + "type": "dict", + "doc": "The Plotly layout template applied over the base template when theme is dark." + }, + { + "name": "template[light]", + "type": "dict", + "doc": "The Plotly layout template applied over the base template when theme is not dark." + }, + { + "name": "decimator", + "type": "indexed(taipy.gui.data.Decimator)", + "doc": "A decimator instance for the indicated trace that reduces the volume of the data being sent back and forth.
If defined as indexed, it impacts only the indicated trace; if not, it applies to the first trace only." + }, + { + "name": "rebuild", + "type": "dynamic(bool)", + "default_value": "False", + "doc": "Allows dynamic config refresh if set to True." + }, + { + "name": "figure", + "type": "dynamic(plotly.graph_objects.Figure)", + "doc": "A figure as produced by Plotly." + }, + { + "name": "on_click", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the user clicks in the chart background.
This function is invoked with the following parameters:
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the chart control if it has one.
  • payload (dict[str, Any]): a dictionary containing the x and y coordinates of the click or latitude and longitude in the case of a map. This feature relies on non-public Plotly structured information.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + } + ] + } + ], + [ + "table", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "data", + "default_property": true, + "required": true, + "type": "dynamic(Any)", + "doc": "The data to be represented in this table. This property can be indexed to define other data for comparison." + }, + { + "name": "page_size", + "type": "int", + "default_value": "100", + "doc": "For a paginated table, the number of visible rows." + }, + { + "name": "allow_all_rows", + "type": "bool", + "default_value": "False", + "doc": "For a paginated table, adds an option to show all the rows." + }, + { + "name": "show_all", + "type": "bool", + "default_value": "False", + "doc": "For a paginated table, show all the rows." + }, + { + "name": "auto_loading", + "type": "bool", + "default_value": "False", + "doc": "If True, the data will be loaded on demand." + }, + { + "name": "width[column_name]", + "type": "str", + "doc": "The width of the indicated column, in CSS units (% values are not supported)." + }, + { + "name": "selected", + "type": "dynamic(Union[list[int],str])", + "doc": "The list of the indices of the rows to be displayed as selected." + }, + { + "name": "page_size_options", + "type": "Union[list[int],str]", + "default_value": "[50, 100, 500]", + "doc": "The list of available page sizes that users can choose from." + }, + { + "name": "columns", + "type": "Union[str,list[str],dict[str,dict[str,Union[str,int]]]]", + "default_value": "All columns", + "doc": "The list of the column names to display.\n
    \n
  • str: semicolon (';')-separated list of column names.
  • list[str]: the list of column names.
  • dict: a dictionary with entries matching: {\"<column_name>\": {\"format\": \"<format>\", \"index\": 1}}.
    \nif index is specified, it represents the display order of this column.\nIf index is not specified, the list order defines the index.
    \nIf format is specified, it is used for numbers or dates.

If columns is omitted or set to None, all columns of data are represented." + }, + { + "name": "date_format", + "type": "str", + "default_value": "\"MM/dd/yyyy\"", + "doc": "The date format used for all date columns when the format is not specifically defined." + }, + { + "name": "number_format", + "type": "str", + "doc": "The number format used for all number columns when the format is not specifically defined." + }, + { + "name": "group_by[column_name]", + "type": "bool", + "default_value": "False", + "doc": "Indicates, if True, that the given column can be aggregated.
See below for more details." + }, + { + "name": "apply[column_name]", + "type": "str", + "default_value": "\"first\"", + "doc": "The name of the aggregation function to use.
This is used only if group_by[column_name] is set to True.
See below for more details." + }, + { + "name": "row_class_name", + "type": "Union[str, Callable]", + "doc": "Allows for styling rows.
This property must be a function or the name of a function that return the name of a CSS class for table rows.
This function is invoked with the following parameters:
  • state (State^): the state instance.
  • index (int): the index of the row.
  • row (Any): all the values for this row.

See below for more details." + }, + { + "name": "cell_class_name[column_name]", + "type": "Union[str, Callable]", + "doc": "Allows for styling cells.
This property must be a function or the name of a function that return the name of a CSS class for table cells.
This function is invoked with the following parameters:
  • state (State^): the state instance.
  • value (Any): the value of the cell.
  • index (int): the index of the row.
  • row (Any): all the values for this row.
  • column_name (str): the name of the column.

See below for more details." + }, + { + "name": "tooltip", + "type": "Union[str, Callable]", + "doc": "Enables tooltips on cells.
This property must be a function or the name of a function that must return a tooltip text for a cell.
See below for more details." + }, + { + "name": "tooltip[column_name]", + "type": "Union[str, Callable]", + "doc": "Enables tooltips on cells at a column level.
This property must be a function or the name of a the function that must return a tooltip text for a cell.
See below for more details." + }, + { + "name": "format_fn[column_name]", + "type": "Union[str, Callable]", + "doc": "Defines custom formatting for table cells. This property must be a function or the name of a function that returns a formatted string for each cell.
The function is invoked when the cells in the specified column (column_name) are rendered. It should return a string that represents the cell value to provide the best user experience.
This function is invoked with the following parameters:
  • state (State^): the state instance.
  • value (Any): the value of the cell.
  • index (int): the index of the row.
  • row (Any): the entire row. The type depends on the type of data.
  • column_name (str): the name of the column.
By default, no custom formatting is applied to the column.
For more details, see the section.", + "signature": [ + ["state", "State"], + ["value", "Any"], + ["index", "int"], + ["row", "Any"], + ["column_name", "str"] + ] + }, + { + "name": "width", + "type": "str", + "default_value": "\"100%\"", + "doc": "The width of the table control, in CSS units." + }, + { + "name": "height", + "type": "str", + "default_value": "\"80vh\"", + "doc": "The height of the table control, in CSS units." + }, + { + "name": "filter", + "type": "bool", + "default_value": "False", + "doc": "Indicates, if True, that all columns can be filtered." + }, + { + "name": "filter[column_name]", + "type": "bool", + "default_value": "False", + "doc": "Indicates, if True, that the indicated column can be filtered." + }, + { + "name": "nan_value", + "type": "str", + "default_value": "\"\"", + "doc": "The replacement text for NaN (not-a-number) values." + }, + { + "name": "nan_value[column_name]", + "type": "str", + "default_value": "\"\"", + "doc": "The replacement text for NaN (not-a-number) values for the indicated column." + }, + { + "name": "editable", + "type": "dynamic(bool)", + "default_value": "False", + "doc": "Indicates, if True, that all cells can be edited." + }, + { + "name": "editable[column_name]", + "type": "bool", + "default_value": "editable", + "doc": "Indicates, if False, that the indicated column cannot be edited, even if editable is True.
By default, all columns are editable or not, depending on the value of the editable property." + }, + { + "name": "on_edit", + "type": "Union[bool, Callable]", + "default_value": "default implementation", + "doc": "A function or the name of a function triggered when an edited cell is validated.
This function is invoked with the following parameters:
  • state (State^): the state instance.
  • var_name (str): the name of the tabular data variable.
  • payload (dict): a dictionary containing details about the callback invocation, with the following keys:
    • index (int): the row index.
    • col (str): the column name.
    • value (Any): the new cell value, cast to the column's data type.
    • user_value (str): the new cell value, as entered by the user.
    • tz (str): the timezone, if the column type is date.
If this property is set to False, the table does not provide the cell editing functionality.
If this property is not set, the table will use the default implementation for editing cells.", + "signature": [ + ["state", "State"], + ["var_name", "str"], + ["payload", "dict"] + ] + }, + { + "name": "on_add", + "type": "Union[bool, Callable]", + "doc": "A function or the name of a function that is triggered when the user requests a row to be added to the table.
This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • var_name (str): the name of the tabular data variable.
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following key:\n
      \n
    • index (int): the row index.

If this property is not set, the table uses the default implementation for adding a new row
If this property is set to False, you cannot add new rows.", + "signature": [ + ["state", "State"], + ["var_name", "str"], + ["payload", "dict"] + ] + }, + { + "name": "on_delete", + "type": "Union[bool, Callable]", + "default_value": "default implementation", + "doc": "A function or the name of a function triggered when a row is deleted.
This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • var_name (str): the name of the tabular data variable.
  • payload (dict): the details on this callback's invocation.
    \nThis dictionary has one key:\n
      \n
    • index (int): the row index.

If this property is not set, the table uses the default implementation for deleting rows.", + "signature": [ + ["state", "State"], + ["var_name", "str"], + ["payload", "dict"] + ] + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the user selects a row.
This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • var_name (str): the name of the tabular data variable.
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • index (int): the row index.
    • col (str): the column name.
    • reason (str): the origin of the action: \"click\", or \"button\" if the cell contains a Markdown link syntax.
    • value (str): the link value indicated in the cell when using a Markdown link syntax (that is, reason is set to \"button\").
.", + "signature": [ + ["state", "State"], + ["var_name", "str"], + ["payload", "dict"] + ] + }, + { + "name": "size", + "type": "str", + "default_value": "\"small\"", + "doc": "The size of the rows.
Valid values are \"small\" and \"medium\"." + }, + { + "name": "rebuild", + "type": "dynamic(bool)", + "default_value": "False", + "doc": "If set to True, this allows to dynamically refresh the columns." + }, + { + "name": "lov[column_name]", + "type": "Union[list[str],str]", + "doc": "The list of values of the indicated column." + }, + { + "name": "downloadable", + "type": "bool", + "default_value": "False", + "doc": "If True, a clickable icon is shown so the user can download the data as CSV." + }, + { + "name": "on_compare", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that compares data. This function should return a structure that identifies the differences between the different data passed as name. The default implementation compares the default data with the data[1] value.", + "signature": [ + ["state", "State"], + ["main_data_name", "str"], + ["compare_data_names", "list[str]"] + ] + }, + { + "name": "use_checkbox", + "type": "bool", + "default_value": "False", + "doc": "If True, boolean values are rendered as a simple HTML checkbox." + }, + { + "name": "sortable", + "type": "bool", + "default_value": "True", + "doc": "If False, the table provides no sorting capability. Individual columns can override this global setting, allowing specific columns to be marked as sortable or non-sortable regardless of value of sortable, by setting the sortable property to True or False accordingly, in the dictionary for that column in the columns property value." + } + ] + } + ], + [ + "selector", + { + "inherits": ["lovComp", "propagate"], + "properties": [ + { + "name": "label", + "type": "str", + "default_value": "None", + "doc": "The label associated with the selector when in dropdown mode." + }, + { + "name": "mode", + "type": "str", + "doc": "Define the way the selector is displayed:\n
  • "radio": as a list of radio buttons
  • "check": as a list of check boxes
  • any other value: a plain list.
" + }, + { + "name": "dropdown", + "type": "bool", + "default_value": "False", + "doc": "If True, the list of items is shown in a dropdown menu.

You cannot use the filter in that situation." + }, + { + "name": "selection_message", + "type": "dynamic(str)", + "doc": "TODO the message shown in the selection area of a dropdown selector when at least one element is selected, list the selected elements if None." + }, + { + "name": "multiple", + "type": "bool", + "default_value": "False", + "doc": "If True, the user can select multiple items." + }, + { + "name": "show_select_all", + "type": "bool", + "default_value": "False", + "doc": "TODO If True and multiple, show a select all option" + }, + { + "name": "filter", + "type": "bool", + "default_value": "False", + "doc": "If True, this control is combined with a filter input area." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "\"360px\"", + "doc": "The width of the selector, in CSS units." + }, + { + "name": "height", + "type": "Union[str,int]", + "doc": "The height of the selector, in CSS units." + } + ] + } + ], + [ + "file_download", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "content", + "default_property": true, + "type": "dynamic(Union[path,file,URL,ReadableBuffer,None])", + "doc": "The content to transfer.
If this is a string, a URL, or a file, then the content is read from this source.
If a readable buffer is provided (such as an array of bytes...), and to prevent the bandwidth from being consumed too much, the way the data is transferred depends on the data_url_max_size parameter of the application configuration (which is set to 50kB by default):\n
    \n
  • If the buffer size is smaller than this setting, then the raw content is generated as a data URL, encoded using base64 (i.e. \"data:<mimetype>;base64,<data>\").
  • If the buffer size exceeds this setting, then it is transferred through a temporary file.
If this property is set to None, that indicates that dynamic content is generated. Please take a look at the examples below for more details on dynamic generation." + }, + { + "name": "label", + "type": "dynamic(str)", + "doc": "The label of the button." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the download is terminated (or on user action if content is None).
This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the button if it has one.
  • payload (dict): the details on this callback's invocation.
    \nThis dictionary has two keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • args: a list of two elements: args[0] reflects the name property and args[1] holds the file URL.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "auto", + "type": "bool", + "default_value": "False", + "doc": "If True, the download starts as soon as the page is loaded." + }, + { + "name": "render", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "If True, the control is displayed.
If False, the control is not displayed." + }, + { + "name": "bypass_preview", + "type": "bool", + "default_value": "True", + "doc": "If False, allows the browser to try to show the content in a different tab.
The file download is always performed." + }, + { + "name": "name", + "type": "str", + "doc": "A name proposition for the file to save, that the user can change." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the element." + } + ] + } + ], + [ + "file_selector", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "content", + "default_property": true, + "type": "dynamic(str)", + "doc": "The path or the list of paths of the uploaded files." + }, + { + "name": "label", + "type": "str", + "doc": "The label of the button." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that will be triggered.
This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the button if it has one.
  • payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "multiple", + "type": "bool", + "default_value": "False", + "doc": "If set to True, multiple files can be uploaded." + }, + { + "name": "selection_type", + "type": "str", + "default_value": "\"file\"", + "doc": "Can be set to \"file\" (with \"f\", \"\" aliases) or \"directory\" (with \"d\", \"dir\", \"folder\" aliases) to upload the respective element with preserved inner structure." + }, + { + "name": "extensions", + "type": "str", + "default_value": "\".csv,.xlsx\"", + "doc": "The list of file extensions that can be uploaded." + }, + { + "name": "drop_message", + "type": "str", + "default_value": "\"Drop here to Upload\"", + "doc": "The message that is displayed when the user drags a file above the button." + }, + { + "name": "notify", + "type": "bool", + "default_value": "True", + "doc": "If set to False, the user won't be notified of upload finish." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the element." + } + ] + } + ], + [ + "image", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "content", + "default_property": true, + "type": "dynamic(Union[path,URL,file,ReadableBuffer])", + "doc": "The image source.
If a buffer is provided (string, array of bytes...), and in order to prevent the bandwidth to be consumed too much, the way the image data is transferred depends on the data_url_max_size parameter of the application configuration (which is set to 50kB by default):\n
    \n
  • If the size of the buffer is smaller than this setting, then the raw content is generated as a\n data URL, encoded using base64 (i.e. \"data:<mimetype>;base64,<data>\").
  • If the size of the buffer is greater than this setting, then it is transferred through a temporary\n file.
" + }, + { + "name": "label", + "type": "dynamic(str)", + "doc": "The label for this image." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the user clicks on the image.
This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the button if it has one.
  • payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "width", + "type": "Union[str,int,float]", + "default_value": "\"300px\"", + "doc": "The width of the image control, in CSS units." + }, + { + "name": "height", + "type": "Union[str,int,float]", + "doc": "The height of the image control, in CSS units." + } + ] + } + ], + [ + "metric", + { + "inherits": ["shared"], + "properties": [ + { + "name": "value", + "default_property": true, + "type": "dynamic(Union[int,float])", + "doc": "The value to represent." + }, + { + "name": "type", + "default_value": "\"circular\"", + "type": "str", + "doc": "The type of the gauge.
Possible values are:\n
    \n
  • \"none\"
  • \"circular\"
  • \"linear\"
Setting this value to \"none\" remove the gauge." + }, + { + "name": "min", + "type": "Union[int,float]", + "default_value": "0", + "doc": "The minimum value of the metric control's gauge." + }, + { + "name": "max", + "type": "Union[int,float]", + "default_value": "100", + "doc": "The maximum value of the metric control's gauge." + }, + { + "name": "delta", + "type": "dynamic(Union[int,float])", + "doc": "The delta value to display." + }, + { + "name": "delta_color", + "type": "str", + "doc": "The color that is used to display the value of the delta property.
If negative_delta_color is set, then this property applies for positive values of delta only.
If this property is set to \"invert\", then values for delta are represented with the color used for negative values if delta is positive and delta is represented with the color used for positive values if it is negative." + }, + { + "name": "title", + "default_value": "None", + "type": "str", + "doc": "The title of the metric." + }, + { + "name": "negative_delta_color", + "type": "str", + "doc": "If set, this represents the color to be used when the value of delta is negative (or positive if delta_color is set to \"invert\")." + }, + { + "name": "threshold", + "type": "dynamic(Union[int,float])", + "doc": "The threshold value to display." + }, + { + "name": "show_value", + "type": "bool", + "default_value": "True", + "doc": "If set to False, the value is not displayed." + }, + { + "name": "format", + "type": "str", + "doc": "The format to use when displaying the value.
This uses the printf syntax." + }, + { + "name": "delta_format", + "type": "str", + "doc": "The format to use when displaying the delta value.
This uses the printf syntax." + }, + { + "name": "bar_color", + "type": "str", + "doc": "The color of the bar in the gauge." + }, + { + "name": "color_map", + "type": "dict", + "doc": "Indicates what colors should be used for different ranges of the metric. The color_map's keys represent the lower bound of each range, which is a number, while the values represent the color for that range.
If the value associated with a key is set to None, the corresponding range is not assigned any color." + }, + { + "name": "width", + "type": "Union[str,number]", + "default_value": "\"20vw\"", + "doc": "The width of the metric control, in CSS units" + }, + { + "name": "height", + "type": "Union[str,number]", + "default_value": "\"20vh\"", + "doc": "The height of the metric control, in CSS units" + }, + { + "name": "layout", + "type": "dynamic(dict[str, Any])", + "doc": "The plotly.js compatible layout object." + }, + { + "name": "template", + "type": "dict", + "doc": "The Plotly layout template." + }, + { + "name": "template[dark]", + "type": "dict", + "doc": "The Plotly layout template applied over the base template when theme is dark." + }, + { + "name": "template[light]", + "type": "dict", + "doc": "The Plotly layout template applied over the base template when theme is not dark." + } + ] + } + ], + [ + "progress", + { + "properties": [ + { + "name": "value", + "type": "dynamic(int)", + "default_value": "None", + "doc": "The progress percentage represented by the control.
If this property is not set or set to None, the progress control represents an indeterminate state.", + "default_property": true + }, + { + "name": "linear", + "type": "bool", + "default_value": "False", + "doc": "If set to True, the control displays a linear progress indicator instead of a circular one." + }, + { + "name": "show_value", + "type": "bool", + "default_value": "False", + "doc": "If set to True, the progress value is shown." + }, + { + "name": "title", + "type": "dynamic(str)", + "doc": "The title of the progress indicator." + }, + { + "name": "title_anchor", + "type": "str", + "default_value": "\"bottom\"", + "doc": "The anchor of the title.
Possible values are:\n
    \n
  • \"bottom\"
  • \"top\"
  • \"left\"
  • \"right\"
  • \"none\" (no title is displayed)
" + }, + { + "name": "render", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "If False, this progress indicator is hidden from the page." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the progress indicator, in CSS units." + } + ] + } + ], + [ + "indicator", + { + "inherits": ["shared"], + "properties": [ + { + "name": "display", + "default_property": true, + "type": "dynamic(Any)", + "doc": "The label to be displayed.
This can be formatted if it is a numerical value." + }, + { + "name": "value", + "type": "dynamic(Union[int, float])", + "default_value": "min", + "doc": "The location of the label on the [min, max] range.
The default value is the min value." + }, + { + "name": "min", + "type": "Union[int, float]", + "default_value": "0", + "doc": "The minimum value of the range." + }, + { + "name": "max", + "type": "Union[int,float]", + "default_value": "100", + "doc": "The maximum value of the range." + }, + { + "name": "format", + "type": "str", + "doc": "The format to use when displaying the value.
This uses the printf syntax." + }, + { + "name": "orientation", + "type": "str", + "default_value": "\"horizontal\"", + "doc": "The orientation of the indicator." + }, + { + "name": "width", + "type": "str", + "default_value": "None", + "doc": "The width of the indicator, in CSS units (used when orientation is horizontal)." + }, + { + "name": "height", + "type": "str", + "default_value": "None", + "doc": "The height of the indicator, in CSS units (used when orientation is vertical)." + }, + { + "name": "hover_text", + "hide": true + } + ] + } + ], + [ + "menu", + { + "inherits": ["active"], + "properties": [ + { + "name": "lov", + "default_property": true, + "type": "dynamic(Union[str,list[Union[str,Icon,Any]]])", + "doc": "The list of menu option values." + }, + { + "name": "label", + "type": "str", + "doc": "The title of the menu." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when a menu option is selected.

This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the button, if it has one.
  • payload (dict): a dictionary containing details about the callback invocation, with the following keys:
      \n
    • action: the name of the action that triggered this callback.
    • args: a list where the first element contains the identifier of the selected option.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "selected", + "type": "dynamic(list[str])", + "doc": "Semicolon (';')-separated list or a list of menu items identifiers that should be selected." + }, + { + "name": "inactive_ids", + "type": "dynamic(Union[str,list[str]])", + "doc": "Semicolon (';')-separated list or a list of menu items identifiers that are disabled." + }, + { + "name": "adapter", + "type": "Union[str, Callable]", + "default_value": "lambda x: str(x)", + "doc": "A function or the name of the function that transforms an element of lov into a tuple(id:str, label:Union[str,Icon]).
The default value is a function that returns the string representation of the lov element." + }, + { + "name": "type", + "type": "str", + "default_value": "Type name of the first lov element", + "doc": "This property is required if lov contains a non-specific type of data (e.g., a dictionary).
Then:
  • value must be of that type
  • lov must be an iterable containing elements of this type
  • The function set to adapter will receive an object of this type.

The default value is the type of the first element in lov." + }, + { + "name": "width", + "type": "str", + "default_value": "\"15vw\"", + "doc": "The width of the menu when unfolded, in CSS units.
Note that when running on a mobile device, the property width[active] is used instead." + }, + { + "name": "width[mobile]", + "type": "str", + "default_value": "\"85vw\"", + "doc": "The width of the menu when unfolded, in CSS units, when running on a mobile device." + } + ] + } + ], + [ + "navbar", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "lov", + "default_property": true, + "type": "dict[str, Any]", + "doc": "The list of pages. The keys should be:\n
    \n
  • page id (start with \"/\")
  • or full URL
\nThe values are labels. See the section on List of Values for more details." + } + ] + } + ], + [ + "alert", + { + "inherits": ["shared"], + "properties": [ + { + "name": "message", + "default_property": true, + "type": "dynamic(str)", + "default_value": "\"\"", + "doc": "The message displayed in the notification. Can be a dynamic string." + }, + { + "name": "severity", + "type": "dynamic(str)", + "default_value": "\"error\"", + "doc": "The severity level of the alert. Valid values: \"error\", \"warning\", \"info\", \"success\".\nThe default is \"error\"." + }, + { + "name": "variant", + "type": "dynamic(str)", + "default_value": "\"filled\"", + "doc": "The variant of the alert. Valid values: \"filled\", \"outlined\".\nThe default is \"filled\"." + }, + { + "name": "render", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "If False, the alert is hidden." + } + ] + } + ], + [ + "status", + { + "inherits": ["shared"], + "properties": [ + { + "name": "value", + "default_property": true, + "type": "Union[tuple,dict,list[dict],list[tuple]]", + "doc": "The different status items to represent. See below." + }, + { + "name": "without_close", + "type": "bool", + "default_value": "False", + "doc": "If True, the user cannot remove the status items from the list." + }, + { + "name": "use_icon", + "type": "indexed(Union[bool,str])", + "default_value": "False", + "doc": "TODO If True, status shows a predefined icon. A string shows as svg content (URL or element). Key from \"info\", \"success\", \"warning\", \"error\"." + } + ] + } + ], + [ + "login", + { + "inherits": ["shared"], + "properties": [ + { + "name": "title", + "default_property": true, + "type": "str", + "default_value": "\"Log in\"", + "doc": "The title of the login dialog." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the dialog button is pressed.

This function is invoked with the following parameters:\n
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the button if it has one.
  • payload (dict): the details on this callback's invocation.
    \nThis dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • args: a list with three elements:\n
      • The first element is the username
      • The second element is the password
      • The third element is the current page name

When the button is pressed, and if this property is not set, Taipy will try to find a callback function called on_login() and invoke it with the parameters listed above.", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "message", + "type": "dynamic(str)", + "doc": "The message shown in the dialog." + }, + { + "name": "labels", + "type": "Union[str,list[str]]", + "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the on_action callback (that index is -1 for the close icon)." + } + ] + } + ], + [ + "chat", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "messages", + "default_property": true, + "required": true, + "type": "dynamic(list[str])", + "doc": "The list of messages. Each item of this list must consist of a list of three strings: a message identifier, a message content, a user identifier, and an image URL." + }, + { + "name": "users", + "type": "dynamic(list[Union[str,Icon]])", + "doc": "The list of users. See the section on List of Values for more details." + }, + { + "name": "sender_id", + "type": "str", + "default_value": "\"taipy\"", + "doc": "The user identifier, as indicated in the users list, associated with all messages sent from the input." + }, + { + "name": "with_input", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "If False, the input field is not rendered." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the user enters a new message.
This function is invoked with the following parameters:
    \n
  • state (State^): the state instance.
  • var_name (str): the name of the variable bound to the messages property.
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • args (list): a list composed of a reason (\"click\" or \"Enter\"), the variable name, the message, and the user identifier of the sender.
", + "signature": [ + ["state", "State"], + ["var_name", "str"], + ["payload", "dict"] + ] + }, + { + "name": "page_size", + "type": "int", + "default_value": "50", + "doc": "The number of messages retrieved from the application and sent to the frontend. Larger values imply more potential latency." + }, + { + "name": "height", + "type": "Union[str,int,float]", + "doc": "The maximum height of the chat control, in CSS units." + }, + { + "name": "show_sender", + "type": "bool", + "default_value": "False", + "doc": "If True, the sender avatar and name are displayed." + }, + { + "name": "mode", + "type": "str", + "default_value": "\"markdown\"", + "doc": "Define the way the messages are processed when they are displayed:\n
  • "raw" no processing
  • "pre": keeps spaces and new lines
  • "markdown" or "md": basic support for Markdown.
" + }, + { + "name": "max_file_size", + "type": "int", + "default_value": "0.8 * 1024 * 1024", + "doc": "The maximum allowable file size, in bytes, for files uploaded to a chat message.\nThe default is 0.8 MB." + }, + { + "name": "allow_send_images", + "type": "bool", + "default_value": "True", + "doc": "TODO if True, an upload image icon is shown." + } + ] + } + ], + [ + "tree", + { + "inherits": ["selector"], + "properties": [ + { + "name": "expanded", + "type": "dynamic(Union[bool,list[str]])", + "default_value": "True", + "doc": "If Boolean and False, only one node can be expanded at one given level. Otherwise this should be set to an array of the node identifiers that need to be expanded." + }, + { + "name": "multiple", + "type": "bool", + "default_value": "False", + "doc": "If True, the user can select multiple items by holding the Ctrl key while clicking on items." + }, + { + "name": "select_leafs_only", + "type": "bool", + "default_value": "False", + "doc": "If True, the user can only select leaf nodes." + }, + { + "name": "row_height", + "type": "str", + "doc": "The height of each row of the tree, in CSS units." + }, + { + "name": "mode", + "hide": true + }, + { + "name": "dropdown", + "hide": true + } + ] + } + ] + ], + "blocks": [ + [ + "part", + { + "inherits": ["partial", "shared"], + "properties": [ + { + "name": "render", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "If True, this part is visible on the page.
If False, the part is hidden and its content is not displayed." + }, + { + "name": "class_name", + "default_property": true, + "type": "dynamic(str)", + "doc": "A list of CSS class names, separated by white spaces, that are associated with the generated HTML Element.
These class names are added to the default taipy-part class name." + }, + { + "name": "page", + "type": "dynamic(str)", + "doc": "The page to show as the content of the block (page name if defined or a URL in an iframe).
This should not be defined if partial is set." + }, + { + "name": "height", + "type": "dynamic(str)", + "doc": "The height of the part, in CSS units." + }, + { + "name": "content", + "type": "dynamic(Any)", + "doc": "The content provided to the part. See the documentation section on content providers." + } + ] + } + ], + [ + "expandable", + { + "inherits": ["partial", "shared", "on_change"], + "properties": [ + { + "name": "title", + "default_property": true, + "type": "dynamic(str)", + "doc": "Title of the expandable block." + }, + { + "name": "expanded", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "If True, the block is expanded, and the content is displayed.
If False, the block is collapsed and its content is hidden." + } + ] + } + ], + [ + "dialog", + { + "inherits": ["partial", "active", "shared"], + "properties": [ + { + "name": "open", + "default_property": true, + "type": "bool", + "default_value": "False", + "doc": "If True, the dialog is visible. If False, it is hidden." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function triggered when a button is pressed.
This function is invoked with the following parameters:
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the dialog if it has one.
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • args: a list where the first element contains the index of the selected label.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "close_label", + "type": "str", + "default_value": "\"Close\"", + "doc": "The tooltip of the top-right close icon button. In the on_action callback, args will be set to -1." + }, + { + "name": "labels", + "type": "Union[str,list[str]]", + "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the on_action callback (that index is -1 for the close icon)." + }, + { + "name": "width", + "type": "Union[str,int,float]", + "doc": "The width of the dialog, in CSS units." + }, + { + "name": "height", + "type": "Union[str,int,float]", + "doc": "The height of the dialog, in CSS units." + }, + { + "name": "ref_id", + "type": "dynamic(str)", + "doc": "TODO an id or a query selector that allows to identify an HTML component that would be the anchor for the dialog." + } + ] + } + ], + [ + "layout", + { + "inherits": ["shared"], + "properties": [ + { + "name": "columns", + "default_property": true, + "type": "str", + "default_value": "\"1 1\"", + "doc": "The list of weights for each column.
For example, \"1 2\" creates a 2 column grid:\n
    \n
  • 1fr
  • 2fr

The creation of multiple same size columns can be simplified by using the multiply sign eg. \"5*1\" is equivalent to \"1 1 1 1 1\"." + }, + { + "name": "columns[mobile]", + "type": "str", + "default_value": "\"1\"", + "doc": "The list of weights for each column, when displayed on a mobile device.
The syntax is the same as for columns." + }, + { + "name": "gap", + "type": "str", + "default_value": "\"0.5rem\"", + "doc": "The size of the gap between the columns." + } + ] + } + ], + [ + "pane", + { + "inherits": ["partial", "on_change", "active", "shared"], + "properties": [ + { + "name": "open", + "default_property": true, + "type": "dynamic(bool)", + "default_value": "False", + "doc": "If True, this pane is visible on the page.
If False, the pane is hidden." + }, + { + "name": "on_close", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when this pane is closed (if the user clicks outside of it or presses the Esc key).
This function is invoked with the following parameters:
    \n
  • state (State^): the state instance.
  • id (optional[str]): the identifier of the close button if it has one.

If this property is not set, no function is called when this pane is closed.", + "signature": [ + ["state", "State"], + ["id", "str"] + ] + }, + { + "name": "anchor", + "type": "str", + "default_value": "\"left\"", + "doc": "Anchor side of the pane.
Valid values are \"left\", \"right\", \"top\", or \"bottom\"." + }, + { + "name": "persistent", + "type": "bool", + "default_value": "False", + "doc": "If False, the pane covers the page where it appeared and disappears if the user clicks in the page.
If True, the pane appears next to the page. Note that the parent section of the pane must have the flex display mode set. See below for an example using the persistent property." + }, + { + "name": "width", + "type": "str", + "default_value": "\"30vw\"", + "doc": "Width of the pane, in CSS units.
This is used only if anchor is \"left\" or \"right\"." + }, + { + "name": "height", + "type": "str", + "default_value": "\"30vh\"", + "doc": "Height of this pane, in CSS units.
This is used only if anchor is \"top\" or \"bottom\"." + }, + { + "name": "show_button", + "type": "bool", + "default_value": "False", + "doc": "If True and when the pane is closed, a button allowing the pane to be opened is shown." + } + ] + } + ] + ], + "undocumented": [ + [ + "active", + { + "properties": [ + { + "name": "active", + "type": "dynamic(bool)", + "default_value": "True", + "doc": "Indicates if this element is active.
If False, the element is disabled, and user interaction is not allowed." + } + ] + } + ], + [ + "lovComp", + { + "inherits": ["on_change"], + "properties": [ + { + "name": "value", + "default_property": true, + "type": "dynamic(Any)", + "doc": "Bound to the selection value.
If this is used for theme toggling, it must be a Boolean value: True indicates that the current theme is set to \"dark\", while False indicates that the theme is set to \"light\"." + }, + { + "name": "lov", + "type": "dict[str, Any]", + "doc": "The list of values. See the section on List of Values for more details." + }, + { + "name": "adapter", + "type": "Union[str, Callable]", + "default_value": "lambda x: str(x)", + "doc": "A function or the name of the function that transforms an element of lov into a tuple(id:str, label:Union[str,Icon]).
The default value is a function that returns the string representation of the lov element." + }, + { + "name": "type", + "type": "str", + "default_value": "Type name of the first lov element", + "doc": "This property is required if lov contains a non-specific type of data (e.g., a dictionary).
Then:
  • value must be of that type
  • lov must be an iterable containing elements of this type
  • The function set to adapter will receive an object of this type.

The default value is the type of the first element in lov." + }, + { + "name": "value_by_id", + "type": "bool", + "default_value": "False", + "doc": "If False, the selection value (in value) is the selected element in lov. If set to True, then value is set to the id of the selected element in lov." + } + ] + } + ], + [ + "on_change", + { + "properties": [ + { + "name": "on_change", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when the value changes.
The callback function receives the following parameters:
    \n
  • state (State^): the state instance.
  • var_name (str): the bound variable name.
  • value (Any): the updated value.
", + "signature": [ + ["state", "State"], + ["var_name", "str"], + ["value", ""] + ] + } + ] + } + ], + [ + "partial", + { + "properties": [ + { + "name": "page", + "type": "str", + "doc": "The page name to show as the content of the block.
This should not be defined if partial is set." + }, + { + "name": "partial", + "type": "taipy.gui.Partial", + "doc": "A Partial object that holds the content of the block.
This should not be defined if page is set." + } + ] + } + ], + [ + "propagate", + { + "inherits": ["active", "shared"], + "properties": [ + { + "name": "propagate", + "type": "bool", + "default_value": "App config", + "doc": "Determines whether the control's value is automatically reflected in the bound application variable.
The default value is defined at the application configuration level by the propagate configuration option.
If True, any change to the control's value is immediately reflected in the variable." + } + ] + } + ], + [ + "sharedInput", + { + "properties": [ + { + "name": "change_delay", + "type": "int", + "default_value": "App config", + "doc": "The minimum interval (in milliseconds) between two consecutive calls to the on_change callback.
The default value is defined at the application configuration level by the change_delay configuration option.
if None, the delay is set to 300 ms.
If set to -1, the callback is triggered only when the user presses the Enter key." + }, + { + "name": "on_action", + "type": "Union[str, Callable]", + "doc": "A function or the name of a function that is triggered when a specific key is pressed.
The callback function is invoked with the following parameters:
    \n
  • state (State^): the state instance.
  • id (str): the identifier of the control if it has one.
  • payload (dict): the callback details
    \nThis dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • args (list):\n
      • The key name pressed.
      • The variable name.
      • The current value of the variable.
", + "signature": [ + ["state", "State"], + ["id", "str"], + ["payload", "dict"] + ] + }, + { + "name": "action_keys", + "type": "str", + "default_value": "\"Enter\"", + "doc": "A semicolon-separated list of keys that can trigger the on_action callback.
Authorized values are Enter, Escape, and function keys F1 to F12." + }, + { + "name": "width", + "type": "Union[str,int]", + "default_value": "None", + "doc": "The width of the element, in CSS units." + } + ] + } + ], + [ + "shared", + { + "properties": [ + { + "name": "id", + "type": "str", + "doc": "The identifier assigned to the rendered HTML component.
This can be used in callbacks or to target the element for styling." + }, + { + "name": "properties", + "type": "dict[str, Any]", + "doc": "A dictionary of additional properties that can be set to the element." + }, + { + "name": "class_name", + "type": "dynamic(str)", + "doc": "A space-separated list of CSS class names to be applied to the generated HTML element.
These classes are added to the default taipy-[element_type] class." + }, + { + "name": "hover_text", + "type": "dynamic(str)", + "doc": "The text that is displayed when the user hovers over the element." + } + ] + } + ] + ] +} diff --git a/frontend/taipy-gui/base/webpack.config.js b/frontend/taipy-gui/base/webpack.config.js index 697bb43cb8..f7860076a5 100644 --- a/frontend/taipy-gui/base/webpack.config.js +++ b/frontend/taipy-gui/base/webpack.config.js @@ -29,6 +29,10 @@ module.exports = [ test: /\.css$/, use: ["style-loader", "css-loader"], }, + { + test: /\.json$/, + type: "json", + }, ], }, resolve: { diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json index 03411d6b57..51cc544a1d 100644 --- a/frontend/taipy-gui/package-lock.json +++ b/frontend/taipy-gui/package-lock.json @@ -19,6 +19,7 @@ "better-react-mathjax": "^2.0.3", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", + "html-react-parser": "^5.2.2", "lodash": "^4.17.21", "nanoid": "^5.0.7", "notistack": "^3.0.0", @@ -5121,7 +5122,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -7201,6 +7201,95 @@ "resolved": "https://registry.npmjs.org/hsluv/-/hsluv-0.0.3.tgz", "integrity": "sha512-08iL2VyCRbkQKBySkSh6m8zMUa3sADAxGVWs3Z1aPcUkTJeK0ETG4Fc27tEmQBGUAXZjIsXOZqBvacuVNSC/fQ==" }, + "node_modules/html-dom-parser": { + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.13.tgz", + "integrity": "sha512-B7JonBuAfG32I7fDouUQEogBrz3jK9gAuN1r1AaXpED6dIhtg/JwiSRhjGL7aOJwRz3HU4efowCjQBaoXiREqg==", + "dependencies": { + "domhandler": "5.0.3", + "htmlparser2": "10.0.0" + } + }, + "node_modules/html-dom-parser/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-dom-parser/node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -7240,6 +7329,40 @@ "node": ">=12" } }, + "node_modules/html-react-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.2.2.tgz", + "integrity": "sha512-yA5012CJGSFWYZsgYzfr6HXJgDap38/AEP4ra8Cw+WHIi2ZRDXRX/QVYdumRf1P8zKyScKd6YOrWYvVEiPfGKg==", + "dependencies": { + "domhandler": "5.0.3", + "html-dom-parser": "5.0.13", + "react-property": "2.0.2", + "style-to-js": "1.1.16" + }, + "peerDependencies": { + "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", + "react": "0.14 || 15 || 16 || 17 || 18 || 19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/html-react-parser/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -11813,6 +11936,11 @@ "react": ">0.13.0" } }, + "node_modules/react-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==" + }, "node_modules/react-router": { "version": "6.28.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", @@ -13121,6 +13249,14 @@ "webpack": "^5.27.0" } }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "dependencies": { + "style-to-object": "1.0.8" + } + }, "node_modules/style-to-object": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", diff --git a/frontend/taipy-gui/package.json b/frontend/taipy-gui/package.json index a7c8b3ad6c..c9ed38c6d3 100644 --- a/frontend/taipy-gui/package.json +++ b/frontend/taipy-gui/package.json @@ -14,6 +14,7 @@ "better-react-mathjax": "^2.0.3", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", + "html-react-parser": "^5.2.2", "lodash": "^4.17.21", "nanoid": "^5.0.7", "notistack": "^3.0.0", diff --git a/frontend/taipy-gui/webpack.config.js b/frontend/taipy-gui/webpack.config.js index c3cef8f662..c03a791f14 100644 --- a/frontend/taipy-gui/webpack.config.js +++ b/frontend/taipy-gui/webpack.config.js @@ -192,6 +192,10 @@ module.exports = (env, options) => { test: /\.css$/, use: ["style-loader", "css-loader"], }, + { + test: /\.json$/, + type: 'json', + }, ], }, resolve: { From 5de04316b79679edcc1b4ea58c5c21c200e36e57 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Fri, 28 Feb 2025 16:39:00 +0700 Subject: [PATCH 14/24] add more input element --- .../components/Taipy/TaipyElementEditor.tsx | 10 +-- .../Taipy/components/BindingInput.tsx | 61 +++++++++++++++++-- .../Taipy/components/ExpressionInput.tsx | 11 +++- .../Taipy/components/StringInput.tsx | 17 ++++++ .../Taipy/components/TaipyPropertyHandler.tsx | 8 +-- .../base/src/element/viselements.json | 6 +- 6 files changed, 96 insertions(+), 17 deletions(-) diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx index 686753e96d..5756513cec 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx @@ -11,10 +11,12 @@ const darkTheme = createTheme({ }); const TaipyElementEditor = () => { - const element = useStore((state) => state.selectedElement); + const elementId = useStore((state) => state.selectedElement?.id); + const elements = useStore((state) => state.elements); + const element = useMemo(() => elements.find((e) => e.id === elementId), [elementId, elements]); const app = useStore((state) => state.app); const [modifiedProperties, setModifiedProperties] = useState>({}); - const elementProperties = useMemo(() => element?.properties, [element]); + const elementProperties = useMemo(() => element?.properties, [element?.properties]); const [availableProperties, propertyOrder] = useMemo( () => element @@ -51,7 +53,7 @@ const TaipyElementEditor = () => { }, [modifiedProperties, elementProperties, defaultProperties]); // USE_EFFECT - // update property selection everytime element property changes + // update property selection every time element property changes useEffect(() => { setModifiedProperties({ ...defaultProperties, ...elementProperties }); }, [elementProperties, defaultProperties]); @@ -78,7 +80,7 @@ const TaipyElementEditor = () => { }, {}, ); - app?.modifyElementProperties(element.id, filteredModifiedProperties); + app?.modifyElement(element.id, { properties: filteredModifiedProperties }); } }, [app, modifiedProperties, element]); diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx index ae4d8dec0e..47e4ff9d8a 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx @@ -1,14 +1,63 @@ -import React from "react"; +import React, { useMemo } from "react"; import { TaipyElementInput } from "./TaipyPropertyHandler"; -import { Box } from "@mui/material"; +import useStore from "../../../store"; +import Box from "@mui/material/Box"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; -const common = (props: TaipyElementInput, bindingInfo: Record) => { - return Input; +const common = (props: TaipyElementInput, bindingInfo: string[][]) => { + const { onChange, value, defaultValue } = props; + const handleChange = (event: SelectChangeEvent) => { + if (event.target.value === "") { + onChange(undefined); + } else { + onChange(event.target.value as string); + } + }; + return ( + + + + + + ); }; export const BindingInput = (props: TaipyElementInput) => { - return common(props, {}); + const app = useStore((state) => state.app); + const data = useMemo(() => { + if (!app) { + return []; + } + const varData = app.getDataTree(); + if (!varData) { + return []; + } + const varDataModule = varData[app.getContext()]; + return Object.keys(varDataModule) + .filter((key: string) => !key.includes("chlkt") && !key.toLowerCase().includes("taipy")) + .map((key: string) => [varDataModule[key].encoded_name, key]); + }, [app]); + return common(props, data); }; export const FunctionBindingInput = (props: TaipyElementInput) => { - return common(props, {}); + const app = useStore((state) => state.app); + const data = useMemo(() => { + if (!app) { + return []; + } + return app + .getFunctionList() + .filter((key: string) => !key.includes("chlkt") && !key.toLowerCase().includes("taipy")) + .map((key: string) => [key, key]); + }, [app]); + return common(props, data); }; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx index 640932dffa..46d6081c52 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx @@ -1,8 +1,17 @@ import React from "react"; import { TaipyElementInput } from "./TaipyPropertyHandler"; +import { Box, TextField } from "@mui/material"; const ExpressionInput = (props: TaipyElementInput) => { - return <>BindingInput; + const { value, defaultValue, onChange } = props; + const inputOnChange = (event: React.ChangeEvent) => { + onChange(event.target.value); + }; + return ( + + + + ); }; export default ExpressionInput; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx index e69de29bb2..4ec342ca80 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { TaipyElementInput } from "./TaipyPropertyHandler"; +import { Box, TextField } from "@mui/material"; + +const StringInput = (props: TaipyElementInput) => { + const { value, defaultValue, onChange } = props; + const inputOnChange = (event: React.ChangeEvent) => { + onChange(event.target.value); + }; + return ( + + + + ); +}; + +export default StringInput; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx index 47730feb08..ba481d759d 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx @@ -37,7 +37,7 @@ const inlineElementTypeComponenetMap: Record const tooltipPropsSx = { tooltip: { sx: { - fontSize: 16, + fontSize: 12, }, }, }; @@ -58,16 +58,16 @@ const TaipyPropertyHandler = (props: TaipyPropertyHandlerProps) => { return ( <> - + {name} - + {inputTypes.length > 1 ? ( - + ) : ( diff --git a/frontend/taipy-gui/base/src/element/viselements.json b/frontend/taipy-gui/base/src/element/viselements.json index e917769782..f9d71a97e1 100644 --- a/frontend/taipy-gui/base/src/element/viselements.json +++ b/frontend/taipy-gui/base/src/element/viselements.json @@ -209,7 +209,8 @@ "name": "step", "type": "Union[int,float]", "default_value": "1", - "doc": "The step value, which is the gap between two consecutive values the slider set. It is a good practice to have (max-min) being divisible by step.
This property is ignored when lov is defined." + "doc": "The step value, which is the gap between two consecutive values the slider set. It is a good practice to have (max-min) being divisible by step.
This property is ignored when lov is defined.", + "designer_input_types": ["number"] }, { "name": "text_anchor", @@ -1954,7 +1955,8 @@ ["state", "State"], ["var_name", "str"], ["value", ""] - ] + ], + "designer_input_types": ["functionbinding"] } ] } From e644a1049170e10ff9bd47e4fb4149cd1b66db74 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Fri, 28 Feb 2025 16:57:38 +0700 Subject: [PATCH 15/24] add docs --- .../taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx index 5756513cec..3f140faf29 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx @@ -11,9 +11,12 @@ const darkTheme = createTheme({ }); const TaipyElementEditor = () => { + // element from the selected item will not be updated since it is not a reference just a copy + // -> need to use the elemnent from the main entry const elementId = useStore((state) => state.selectedElement?.id); const elements = useStore((state) => state.elements); const element = useMemo(() => elements.find((e) => e.id === elementId), [elementId, elements]); + const app = useStore((state) => state.app); const [modifiedProperties, setModifiedProperties] = useState>({}); const elementProperties = useMemo(() => element?.properties, [element?.properties]); From f6595a6d5d7835c96ae40607b76c5d17257d424e Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Fri, 28 Feb 2025 17:48:18 +0700 Subject: [PATCH 16/24] re organize variable fetcher --- .../Taipy/components/BindingInput.tsx | 23 ++++-------------- .../Taipy/components/ExpressionInput.tsx | 2 +- .../Taipy/components/StringInput.tsx | 2 +- .../base/src/components/Taipy/utils.ts | 24 +++++++++++++++++++ 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx index 47e4ff9d8a..43b99b767d 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/BindingInput.tsx @@ -5,9 +5,10 @@ import Box from "@mui/material/Box"; import MenuItem from "@mui/material/MenuItem"; import FormControl from "@mui/material/FormControl"; import Select, { SelectChangeEvent } from "@mui/material/Select"; +import { getFunctionList, getVarList } from "../utils"; const common = (props: TaipyElementInput, bindingInfo: string[][]) => { - const { onChange, value, defaultValue } = props; + const { onChange, value } = props; const handleChange = (event: SelectChangeEvent) => { if (event.target.value === "") { onChange(undefined); @@ -33,17 +34,7 @@ const common = (props: TaipyElementInput, bindingInfo: string[][]) => { export const BindingInput = (props: TaipyElementInput) => { const app = useStore((state) => state.app); const data = useMemo(() => { - if (!app) { - return []; - } - const varData = app.getDataTree(); - if (!varData) { - return []; - } - const varDataModule = varData[app.getContext()]; - return Object.keys(varDataModule) - .filter((key: string) => !key.includes("chlkt") && !key.toLowerCase().includes("taipy")) - .map((key: string) => [varDataModule[key].encoded_name, key]); + return getVarList(app); }, [app]); return common(props, data); }; @@ -51,13 +42,7 @@ export const BindingInput = (props: TaipyElementInput) => { export const FunctionBindingInput = (props: TaipyElementInput) => { const app = useStore((state) => state.app); const data = useMemo(() => { - if (!app) { - return []; - } - return app - .getFunctionList() - .filter((key: string) => !key.includes("chlkt") && !key.toLowerCase().includes("taipy")) - .map((key: string) => [key, key]); + return getFunctionList(app); }, [app]); return common(props, data); }; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx index 46d6081c52..ae6003ded5 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx @@ -3,7 +3,7 @@ import { TaipyElementInput } from "./TaipyPropertyHandler"; import { Box, TextField } from "@mui/material"; const ExpressionInput = (props: TaipyElementInput) => { - const { value, defaultValue, onChange } = props; + const { value, onChange } = props; const inputOnChange = (event: React.ChangeEvent) => { onChange(event.target.value); }; diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx index 4ec342ca80..27d1f002ff 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/StringInput.tsx @@ -9,7 +9,7 @@ const StringInput = (props: TaipyElementInput) => { }; return ( - + ); }; diff --git a/frontend/taipy-gui/base/src/components/Taipy/utils.ts b/frontend/taipy-gui/base/src/components/Taipy/utils.ts index eb056607f9..2f87ab49a9 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/utils.ts +++ b/frontend/taipy-gui/base/src/components/Taipy/utils.ts @@ -18,3 +18,27 @@ export const getJsx = async (taipyApp: TaipyApp, element: Element, editMode: boo throw new Error(`Failed to render element '${element.type} - ${element.id}': ${error}`); } }; + +export const getVarList = (app?: TaipyApp) => { + if (!app) { + return []; + } + const varData = app.getDataTree(); + if (!varData) { + return []; + } + const varDataModule = varData[app.getContext()]; + return Object.keys(varDataModule) + .filter((key: string) => !key.includes("chlkt") && !key.toLowerCase().includes("taipy")) + .map((key: string) => [varDataModule[key].encoded_name, key]); +}; + +export const getFunctionList = (app?: TaipyApp) => { + if (!app) { + return []; + } + return app + .getFunctionList() + .filter((key: string) => !key.includes("chlkt") && !key.toLowerCase().includes("taipy")) + .map((key: string) => [key, key]); +}; From e881a15d877caf096409bcd835b6c717beeaf35a Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Thu, 6 Mar 2025 00:55:19 +0700 Subject: [PATCH 17/24] add expression editor --- .../Taipy/components/ExpressionInput.tsx | 122 +- .../base/src/components/Taipy/utils.ts | 3 +- frontend/taipy-gui/package-lock.json | 33 + frontend/taipy-gui/package.json | 2 + package-lock.json | 1461 ----------------- 5 files changed, 151 insertions(+), 1470 deletions(-) delete mode 100644 package-lock.json diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx index ae6003ded5..aea9b9bd70 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx @@ -1,15 +1,121 @@ -import React from "react"; +import React, { useEffect } from "react"; +import Editor, { useMonaco } from "@monaco-editor/react"; +import * as monaco from "monaco-editor"; import { TaipyElementInput } from "./TaipyPropertyHandler"; -import { Box, TextField } from "@mui/material"; +import { Box } from "@mui/material"; +import useStore from "../../../store"; +import { getVarList } from "../utils"; + +const THEME_NAME = "expression-theme"; +const LANGUAGE_ID = "python-expressions"; const ExpressionInput = (props: TaipyElementInput) => { - const { value, onChange } = props; - const inputOnChange = (event: React.ChangeEvent) => { - onChange(event.target.value); - }; + const app = useStore((state) => state.app); + const monacoInstance = useMonaco(); + + useEffect(() => { + if ( + !app || + !monacoInstance || + monacoInstance.languages.getLanguages().some((lang) => lang.id === LANGUAGE_ID) + ) { + return; + } + + monacoInstance.editor.defineTheme(THEME_NAME, { + base: "vs-dark", + inherit: true, + rules: [{ token: "expression", foreground: "FFA500", fontStyle: "bold" }], + colors: {}, + }); + + monacoInstance.editor.setTheme(THEME_NAME); + + monacoInstance.languages.register({ id: LANGUAGE_ID }); + + monacoInstance.languages.setMonarchTokensProvider(LANGUAGE_ID, { + tokenizer: { + root: [[/\{(?:[^{}]|\{[^{}]*\})*\}/, "expression"]], + }, + }); + + monacoInstance.languages.registerCompletionItemProvider(LANGUAGE_ID, { + triggerCharacters: ["{"], + provideCompletionItems: (model, position, context) => { + const word = model.getWordAtPosition(position); + const range = word + ? new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) + : new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column); + const isCurlyTrigger = + context.triggerKind === monaco.languages.CompletionTriggerKind.TriggerCharacter && + context.triggerCharacter === "{"; + + const suggestions = getVarList(app).map((item) => ({ + label: item[1], + kind: monaco.languages.CompletionItemKind.Variable, + insertText: `${isCurlyTrigger ? "" : "{"}${item[0]}}`, + range, + })); + + return { + suggestions, + }; + }, + }); + }, [app, monacoInstance]); + return ( - - + + { + const model = editor.getModel(); + if (model) { + monacoInstance.editor.setModelLanguage(model, LANGUAGE_ID); + } + editor.onKeyDown((e) => { + if (e.keyCode === monaco.KeyCode.Enter) { + e.preventDefault(); + } + }); + }} + onChange={(value) => { + if (props.onChange) { + props.onChange(value); + } + }} + /> ); }; diff --git a/frontend/taipy-gui/base/src/components/Taipy/utils.ts b/frontend/taipy-gui/base/src/components/Taipy/utils.ts index 2f87ab49a9..9eaa05c452 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/utils.ts +++ b/frontend/taipy-gui/base/src/components/Taipy/utils.ts @@ -30,7 +30,8 @@ export const getVarList = (app?: TaipyApp) => { const varDataModule = varData[app.getContext()]; return Object.keys(varDataModule) .filter((key: string) => !key.includes("chlkt") && !key.toLowerCase().includes("taipy")) - .map((key: string) => [varDataModule[key].encoded_name, key]); + .map((key: string) => [key, key]); + // .map((key: string) => [varDataModule[key].encoded_name, key]); }; export const getFunctionList = (app?: TaipyApp) => { diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json index 51cc544a1d..82d26c196a 100644 --- a/frontend/taipy-gui/package-lock.json +++ b/frontend/taipy-gui/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.10.0", "@emotion/styled": "^11.10.0", + "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^6.0.1", "@mui/material": "^6.0.1", "@mui/x-date-pickers": "^7.0.0", @@ -21,6 +22,7 @@ "date-fns-tz": "^3.1.3", "html-react-parser": "^5.2.2", "lodash": "^4.17.21", + "monaco-editor": "^0.52.2", "nanoid": "^5.0.7", "notistack": "^3.0.0", "plotly.js": "^2.33.0", @@ -1535,6 +1537,27 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.2.1.tgz", @@ -10747,6 +10770,11 @@ "node": ">=16.0.0" } }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==" + }, "node_modules/mouse-change": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz", @@ -12949,6 +12977,11 @@ "node": ">=8" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/static-eval": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", diff --git a/frontend/taipy-gui/package.json b/frontend/taipy-gui/package.json index c9ed38c6d3..a1279729a5 100644 --- a/frontend/taipy-gui/package.json +++ b/frontend/taipy-gui/package.json @@ -5,6 +5,7 @@ "dependencies": { "@emotion/react": "^11.10.0", "@emotion/styled": "^11.10.0", + "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^6.0.1", "@mui/material": "^6.0.1", "@mui/x-date-pickers": "^7.0.0", @@ -16,6 +17,7 @@ "date-fns-tz": "^3.1.3", "html-react-parser": "^5.2.2", "lodash": "^4.17.21", + "monaco-editor": "^0.52.2", "nanoid": "^5.0.7", "notistack": "^3.0.0", "plotly.js": "^2.33.0", diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index cd2788190c..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,1461 +0,0 @@ -{ - "name": "taipy", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", - "@mui/material": "^6.1.6", - "@textea/json-viewer": "^4.0.0", - "react-json-tree": "^0.19.0", - "react-json-view": "^1.21.3" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", - "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.2.0", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.13.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz", - "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.13.3", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", - "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/cache": "^11.13.0", - "@emotion/serialize": "^1.3.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", - "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.1", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", - "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.0", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", - "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.6.tgz", - "integrity": "sha512-nz1SlR9TdBYYPz4qKoNasMPRiGb4PaIHFkzLzhju0YVYS5QSuFF2+n7CsiHMIDcHv3piPu/xDWI53ruhOqvZwQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/material": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.6.tgz", - "integrity": "sha512-1yvejiQ/601l5AK3uIdUlAVElyCxoqKnl7QA+2oFB/2qYPWfRwDgavW/MoywS5Y2gZEslcJKhe0s2F3IthgFgw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.1.6", - "@mui/system": "^6.1.6", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.6", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.11", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^18.3.1", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.6", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.6.tgz", - "integrity": "sha512-ioAiFckaD/fJSnTrUMWgjl9HYBWt7ixCh7zZw7gDZ+Tae7NuprNV6QJK95EidDT7K0GetR2rU3kAeIR61Myttw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.1.6", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.6.tgz", - "integrity": "sha512-I+yS1cSuSvHnZDBO7e7VHxTWpj+R7XlSZvTC4lS/OIbUNJOMMSd3UDP6V2sfwzAdmdDNBi7NGCRv2SZ6O9hGDA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.1", - "@emotion/serialize": "^1.3.2", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.6.tgz", - "integrity": "sha512-qOf1VUE9wK8syiB0BBCp82oNBAVPYdj4Trh+G1s+L+ImYiKlubWhhqlnvWt3xqMevR+D2h1CXzA1vhX2FvA+VQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.1.6", - "@mui/styled-engine": "^6.1.6", - "@mui/types": "^7.2.19", - "@mui/utils": "^6.1.6", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.2.19", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", - "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.6.tgz", - "integrity": "sha512-sBS6D9mJECtELASLM+18WUcXF6RH3zNxBRFeyCRg8wad6NbyNrdxLuwK+Ikvc38sTZwBzAz691HmSofLqHd9sQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "^7.2.19", - "@types/prop-types": "^15.7.13", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^18.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@textea/json-viewer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@textea/json-viewer/-/json-viewer-4.0.0.tgz", - "integrity": "sha512-TOrvWJ3E1Qg8pRu2eTdMSYJl/JwaYtMGD6EIzYsaicFIA0jICZa/WH4D7/0anH5At8eYfubPTfjjBqnair5WIA==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "copy-to-clipboard": "^3.3.3", - "zustand": "^4.5.5" - }, - "peerDependencies": { - "@emotion/react": "^11", - "@emotion/styled": "^11", - "@mui/material": "^6", - "react": "^17 || ^18", - "react-dom": "^17 || ^18" - } - }, - "node_modules/@types/lodash": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", - "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", - "license": "MIT" - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", - "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "license": "MIT" - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/base16": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", - "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==", - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "license": "MIT", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fbemitter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", - "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", - "license": "BSD-3-Clause", - "dependencies": { - "fbjs": "^3.0.0" - } - }, - "node_modules/fbjs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", - "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", - "license": "MIT", - "dependencies": { - "cross-fetch": "^3.1.5", - "fbjs-css-vars": "^1.0.0", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^1.0.35" - } - }, - "node_modules/fbjs-css-vars": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", - "license": "MIT" - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, - "node_modules/flux": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz", - "integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==", - "license": "BSD-3-Clause", - "dependencies": { - "fbemitter": "^3.0.0", - "fbjs": "^3.0.1" - }, - "peerDependencies": { - "react": "^15.0.2 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/lodash.curry": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", - "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==", - "license": "MIT" - }, - "node_modules/lodash.flow": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", - "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "license": "MIT", - "dependencies": { - "asap": "~2.0.3" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/pure-color": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", - "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==", - "license": "MIT" - }, - "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-base16-styling": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.10.0.tgz", - "integrity": "sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.17.0", - "color": "^4.2.3", - "csstype": "^3.1.3", - "lodash-es": "^4.17.21" - } - }, - "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/react-json-tree": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.19.0.tgz", - "integrity": "sha512-PqT1WRVcWP+RROsZPQfNEKIC1iM/ZMfY4g5jN6oDnXp5593PPRAYgoHcgYCDjflAHQMtxl8XGdlTwIBdEGUXvw==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.17.0", - "react-base16-styling": "^0.10.0" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-json-view": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz", - "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==", - "license": "MIT", - "dependencies": { - "flux": "^4.0.1", - "react-base16-styling": "^0.6.0", - "react-lifecycles-compat": "^3.0.4", - "react-textarea-autosize": "^8.3.2" - }, - "peerDependencies": { - "react": "^17.0.0 || ^16.3.0 || ^15.5.4", - "react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4" - } - }, - "node_modules/react-json-view/node_modules/react-base16-styling": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz", - "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==", - "license": "MIT", - "dependencies": { - "base16": "^1.0.0", - "lodash.curry": "^4.0.1", - "lodash.flow": "^3.3.0", - "pure-color": "^1.2.0" - } - }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "license": "MIT" - }, - "node_modules/react-textarea-autosize": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.4.tgz", - "integrity": "sha512-eSSjVtRLcLfFwFcariT77t9hcbVJHQV76b51QjQGarQIHml2+gM2lms0n3XrhnDmgK5B+/Z7TmQk5OHNzqYm/A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "use-composed-ref": "^1.3.0", - "use-latest": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", - "license": "MIT" - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ua-parser-js": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", - "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/use-composed-ref": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", - "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-latest": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", - "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", - "license": "MIT", - "dependencies": { - "use-isomorphic-layout-effect": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/zustand": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", - "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - } - } -} From 82bbb82f6400df5fc94158384206585861531053 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Thu, 6 Mar 2025 01:06:19 +0700 Subject: [PATCH 18/24] add new range calculation --- .../Taipy/components/ExpressionInput.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx index aea9b9bd70..3d7d75fdd1 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/ExpressionInput.tsx @@ -42,10 +42,17 @@ const ExpressionInput = (props: TaipyElementInput) => { monacoInstance.languages.registerCompletionItemProvider(LANGUAGE_ID, { triggerCharacters: ["{"], provideCompletionItems: (model, position, context) => { - const word = model.getWordAtPosition(position); - const range = word - ? new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) - : new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column); + // const word = model.getWordAtPosition(position); + // const range = word + // ? new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) + // : new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column); + const wordUntil = model.getWordUntilPosition(position); + const range = new monaco.Range( + position.lineNumber, + wordUntil.startColumn, + position.lineNumber, + wordUntil.endColumn, + ); const isCurlyTrigger = context.triggerKind === monaco.languages.CompletionTriggerKind.TriggerCharacter && context.triggerCharacter === "{"; From d62ede4357f95e92b088d02b3e88b43f24777309 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Thu, 6 Mar 2025 15:31:33 +0700 Subject: [PATCH 19/24] add client config option to resources --- .../components/Taipy/components/TaipyPropertyHandler.tsx | 2 ++ frontend/taipy-gui/base/src/element/viselements.json | 6 ++++-- taipy/gui/custom/_page.py | 4 +++- taipy/gui/server.py | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx b/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx index ba481d759d..5010f886bf 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/components/TaipyPropertyHandler.tsx @@ -7,6 +7,7 @@ import parse from "html-react-parser"; import NumberInput from "./NumberInput"; import { BindingInput, FunctionBindingInput } from "./BindingInput"; import ExpressionInput from "./ExpressionInput"; +import StringInput from "./StringInput"; interface TaipyPropertyHandlerProps { name: string; @@ -28,6 +29,7 @@ const elementTypeComponentMap: Record> = { binding: BindingInput, expression: ExpressionInput, functionbinding: FunctionBindingInput, + string: StringInput, }; const inlineElementTypeComponenetMap: Record> = { diff --git a/frontend/taipy-gui/base/src/element/viselements.json b/frontend/taipy-gui/base/src/element/viselements.json index f9d71a97e1..8d7c65019b 100644 --- a/frontend/taipy-gui/base/src/element/viselements.json +++ b/frontend/taipy-gui/base/src/element/viselements.json @@ -90,7 +90,8 @@ "default_property": true, "type": "dynamic(Any)", "default_value": "None", - "doc": "The value represented by this control." + "doc": "The value represented by this control.", + "designer_input_types": ["binding", "expression"] }, { "name": "password", @@ -102,7 +103,8 @@ "name": "label", "type": "str", "default_value": "None", - "doc": "The label associated with the input field.
This provides context to the user and improves accessibility." + "doc": "The label associated with the input field.
This provides context to the user and improves accessibility.", + "designer_input_types": ["string"] }, { "name": "multiline", diff --git a/taipy/gui/custom/_page.py b/taipy/gui/custom/_page.py index ba5256b1eb..dfb36eab72 100644 --- a/taipy/gui/custom/_page.py +++ b/taipy/gui/custom/_page.py @@ -60,7 +60,9 @@ def get_id(self) -> str: return self.rh_id if self.rh_id != "" else str(id(self)) @abstractmethod - def get_resources(self, path: str, taipy_resource_path: str, base_url: str) -> t.Any: + def get_resources( + self, path: str, taipy_resource_path: str, base_url: str, client_config: t.Dict[str, t.Any] + ) -> t.Any: raise NotImplementedError diff --git a/taipy/gui/server.py b/taipy/gui/server.py index 65237830ac..21d2c4c8e4 100644 --- a/taipy/gui/server.py +++ b/taipy/gui/server.py @@ -177,7 +177,7 @@ def my_index(path): ) return response try: - return resource_handler.get_resources(path, static_folder, base_url) + return resource_handler.get_resources(path, static_folder, base_url, client_config) except Exception as e: raise RuntimeError("Can't get resources from custom resource handler") from e if path == "" or path == "index.html" or "." not in path: From 2d9817f61fb7815df1d769f6b1b8676eda227bf2 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Thu, 6 Mar 2025 16:29:46 +0700 Subject: [PATCH 20/24] add important taipy config --- taipy/gui/custom/_page.py | 4 +--- taipy/gui/server.py | 10 +++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/taipy/gui/custom/_page.py b/taipy/gui/custom/_page.py index dfb36eab72..d4c361ac88 100644 --- a/taipy/gui/custom/_page.py +++ b/taipy/gui/custom/_page.py @@ -60,9 +60,7 @@ def get_id(self) -> str: return self.rh_id if self.rh_id != "" else str(id(self)) @abstractmethod - def get_resources( - self, path: str, taipy_resource_path: str, base_url: str, client_config: t.Dict[str, t.Any] - ) -> t.Any: + def get_resources(self, path: str, config: t.Dict[str, t.Any]) -> t.Any: raise NotImplementedError diff --git a/taipy/gui/server.py b/taipy/gui/server.py index 21d2c4c8e4..ba3c9eec99 100644 --- a/taipy/gui/server.py +++ b/taipy/gui/server.py @@ -177,7 +177,15 @@ def my_index(path): ) return response try: - return resource_handler.get_resources(path, static_folder, base_url, client_config) + config = { + "base_url": base_url, + "taipy_resource_path": static_folder, + "config": client_config, + "css_vars": css_vars, + "scripts": scripts, + "styles": styles, + } + return resource_handler.get_resources(path, config) except Exception as e: raise RuntimeError("Can't get resources from custom resource handler") from e if path == "" or path == "index.html" or "." not in path: From 041558509c94b78ffd7ca57dc91b44e8afd14207 Mon Sep 17 00:00:00 2001 From: dinhlongviolin1 Date: Fri, 7 Mar 2025 16:03:23 +0700 Subject: [PATCH 21/24] add boolean input + update viselements input slider button --- .../components/Taipy/TaipyElementEditor.tsx | 34 +++---- .../Taipy/components/BindingInput.tsx | 6 +- .../Taipy/components/BooleanInput.tsx | 29 ++++++ .../Taipy/components/NumberInput.tsx | 13 ++- .../Taipy/components/StringInput.tsx | 29 +++++- .../Taipy/components/TaipyPropertyHandler.tsx | 3 +- .../src/components/Taipy/taipyelements.css | 17 ++++ .../base/src/components/Taipy/utils.ts | 2 +- .../base/src/element/viselements.json | 93 ++++++++++++------- 9 files changed, 160 insertions(+), 66 deletions(-) create mode 100644 frontend/taipy-gui/base/src/components/Taipy/components/BooleanInput.tsx diff --git a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx index 3f140faf29..ed238e4609 100644 --- a/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx +++ b/frontend/taipy-gui/base/src/components/Taipy/TaipyElementEditor.tsx @@ -96,23 +96,25 @@ const TaipyElementEditor = () => { return element ? ( - -

Properties for {element.type} widget

+ + +

Properties for {element.type} widget

+
+ + {propertyOrder.map((propertyName) => ( + + ))} +
- - {propertyOrder.map((propertyName) => ( - - ))} - - +