From 14171c5ff0a410452c0c08125b111f4af071d150 Mon Sep 17 00:00:00 2001 From: namnguyen Date: Wed, 9 Oct 2024 02:45:44 +0700 Subject: [PATCH 01/24] Init commit --- .../taipy-gui/src/components/Taipy/Menu.tsx | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Menu.tsx b/frontend/taipy-gui/src/components/Taipy/Menu.tsx index 917b41c8cc..38d485e539 100644 --- a/frontend/taipy-gui/src/components/Taipy/Menu.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Menu.tsx @@ -11,7 +11,7 @@ * specific language governing permissions and limitations under the License. */ -import React, { useCallback, useMemo, useState, MouseEvent, CSSProperties } from "react"; +import React, { useCallback, useMemo, useState, MouseEvent, CSSProperties, useEffect } from "react"; import MenuIco from "@mui/icons-material/Menu"; import ListItemButton from "@mui/material/ListItemButton"; import Drawer from "@mui/material/Drawer"; @@ -20,14 +20,15 @@ import Avatar from "@mui/material/Avatar"; import CardHeader from "@mui/material/CardHeader"; import ListItemAvatar from "@mui/material/ListItemAvatar"; import Box from "@mui/material/Box"; -import Tooltip from '@mui/material/Tooltip'; +import Tooltip from "@mui/material/Tooltip"; import { Theme, useTheme } from "@mui/system"; import { SingleItem } from "./lovUtils"; import { createSendActionNameAction } from "../../context/taipyReducers"; import { MenuProps } from "../../utils/lov"; import { useClassNames, useDispatch, useModule } from "../../utils/hooks"; -import { emptyArray } from "../../utils"; +import { emptyArray, getBaseURL } from "../../utils"; +import { useLocation } from "react-router"; const boxDrawerStyle = { overflowX: "hidden" } as CSSProperties; const headerSx = { padding: 0 }; @@ -41,7 +42,7 @@ const Menu = (props: MenuProps) => { const dispatch = useDispatch(); const theme = useTheme(); const module = useModule(); - + const location = useLocation(); const className = useClassNames(props.libClassName, props.dynamicClassName, props.className); const clickHandler = useCallback( @@ -64,19 +65,31 @@ const Menu = (props: MenuProps) => { const [drawerSx, titleProps] = useMemo(() => { const drawerWidth = opened ? width : `calc(${theme.spacing(9)} + 1px)`; - const titleWidth = opened ? `calc(${width} - ${theme.spacing(10)})`: undefined; - return [{ - width: drawerWidth, - flexShrink: 0, - "& .MuiDrawer-paper": { + const titleWidth = opened ? `calc(${width} - ${theme.spacing(10)})` : undefined; + return [ + { width: drawerWidth, - boxSizing: "border-box", + flexShrink: 0, + "& .MuiDrawer-paper": { + width: drawerWidth, + boxSizing: "border-box", + transition: "width 0.3s", + }, transition: "width 0.3s", }, - transition: "width 0.3s", - }, {...baseTitleProps, width: titleWidth}]; + { ...baseTitleProps, width: titleWidth }, + ]; }, [opened, width, theme]); + useEffect(() => { + if (lov && lov.length) { + const value = lov.find((it) => getBaseURL() + it.id === location.pathname); + if (value) { + setSelectedValue(value.id); + } + } + }, [location.pathname, lov]); + return lov && lov.length ? ( @@ -86,9 +99,11 @@ const Menu = (props: MenuProps) => { - - + + + + + } title={label} titleTypographyProps={titleProps} From 15f4f0650791d8faa5c159e6842deb32a44a5c2a Mon Sep 17 00:00:00 2001 From: namnguyen Date: Thu, 10 Oct 2024 04:25:08 +0700 Subject: [PATCH 02/24] refactoring Menu test cases --- .../src/components/Taipy/Menu.spec.tsx | 90 ++++++++++++------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx index bed03eb69b..1adc7b5d18 100644 --- a/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx @@ -1,20 +1,8 @@ -/* - * Copyright 2021-2024 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 from "react"; import { render } from "@testing-library/react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; +import { BrowserRouter as Router } from "react-router-dom"; import Menu from "./Menu"; import { INITIAL_STATE, TaipyState } from "../../context/taipyReducers"; @@ -22,65 +10,107 @@ import { TaipyContext } from "../../context/taipyContext"; import { LovItem } from "../../utils/lov"; const lov: LovItem[] = [ - {id: "id1", item: "Item 1"}, - {id: "id2", item:"Item 2"}, - {id: "id3", item:"Item 3"}, - {id: "id4", item:"Item 4"}, + { id: "id1", item: "Item 1" }, + { id: "id2", item: "Item 2" }, + { id: "id3", item: "Item 3" }, + { id: "id4", item: "Item 4" }, ]; -const imageItem: LovItem = {id: "ii1", item: { path: "/img/fred.png", text: "Image" }}; +const imageItem: LovItem = { id: "ii1", item: { path: "/img/fred.png", text: "Image" } }; describe("Menu Component", () => { it("renders", async () => { - const { getByText } = render(); + const { getByText } = render( + + + + ); const elt = getByText("Item 1"); expect(elt.tagName).toBe("SPAN"); }); + it("uses the class", async () => { - const { getByText } = render(); + const { getByText } = render( + + + + ); const elt = getByText("Item 1"); expect(elt.closest(".taipy-menu")).not.toBeNull(); }); + it("can display an avatar with initials", async () => { const lovWithImage = [...lov, imageItem]; - const { getByText } = render(); + const { getByText } = render( + + + + ); const elt = getByText("I2"); expect(elt.tagName).toBe("DIV"); }); + it("can display an image", async () => { const lovWithImage = [...lov, imageItem]; - const { getByAltText } = render(); + const { getByAltText } = render( + + + + ); const elt = getByAltText("Image"); expect(elt.tagName).toBe("IMG"); }); + it("is disabled", async () => { - const { getAllByRole } = render(); + const { getAllByRole } = render( + + + + ); const elts = getAllByRole("button"); elts.forEach((elt, idx) => idx > 0 && expect(elt).toHaveClass("Mui-disabled")); }); + it("is enabled by default", async () => { - const { getAllByRole } = render(); + const { getAllByRole } = render( + + + + ); const elts = getAllByRole("button"); elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled")); }); + it("is enabled by active", async () => { - const { getAllByRole } = render(); + const { getAllByRole } = render( + + + + ); const elts = getAllByRole("button"); elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled")); }); + it("can disable a specific item", async () => { - const { getByText } = render(); + const { getByText } = render( + + + + ); const elt = getByText(lov[0].item as string); - const button = elt.closest('[role="button"]') + const button = elt.closest('[role="button"]'); expect(button).toHaveClass("Mui-disabled"); }); + it("dispatch a well formed message", async () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByText } = render( - - - + + + + + ); const elt = getByText(lov[0].item as string); await userEvent.click(elt); From d2704a367b3d038ba8c32f583e0f3247606fd65e Mon Sep 17 00:00:00 2001 From: namnguyen Date: Sat, 12 Oct 2024 23:14:36 +0700 Subject: [PATCH 03/24] Based on new requirement for Menu --- .../src/components/Taipy/Menu.spec.tsx | 57 ++++--------------- .../taipy-gui/src/components/Taipy/Menu.tsx | 18 ++---- .../src/components/Taipy/MenuCtl.tsx | 21 +++++-- .../src/components/Taipy/lovUtils.tsx | 6 +- frontend/taipy-gui/src/utils/lov.ts | 1 + taipy/gui/_renderers/factory.py | 1 + taipy/gui/viselements.json | 5 ++ 7 files changed, 43 insertions(+), 66 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx index 1adc7b5d18..313431c9ad 100644 --- a/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Menu.spec.tsx @@ -2,7 +2,6 @@ import React from "react"; import { render } from "@testing-library/react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; -import { BrowserRouter as Router } from "react-router-dom"; import Menu from "./Menu"; import { INITIAL_STATE, TaipyState } from "../../context/taipyReducers"; @@ -20,83 +19,51 @@ const imageItem: LovItem = { id: "ii1", item: { path: "/img/fred.png", text: "Im describe("Menu Component", () => { it("renders", async () => { - const { getByText } = render( - - - - ); + const { getByText } = render(); const elt = getByText("Item 1"); expect(elt.tagName).toBe("SPAN"); }); it("uses the class", async () => { - const { getByText } = render( - - - - ); + const { getByText } = render(); const elt = getByText("Item 1"); expect(elt.closest(".taipy-menu")).not.toBeNull(); }); it("can display an avatar with initials", async () => { const lovWithImage = [...lov, imageItem]; - const { getByText } = render( - - - - ); + const { getByText } = render(); const elt = getByText("I2"); expect(elt.tagName).toBe("DIV"); }); it("can display an image", async () => { const lovWithImage = [...lov, imageItem]; - const { getByAltText } = render( - - - - ); + const { getByAltText } = render(); const elt = getByAltText("Image"); expect(elt.tagName).toBe("IMG"); }); it("is disabled", async () => { - const { getAllByRole } = render( - - - - ); + const { getAllByRole } = render(); const elts = getAllByRole("button"); elts.forEach((elt, idx) => idx > 0 && expect(elt).toHaveClass("Mui-disabled")); }); it("is enabled by default", async () => { - const { getAllByRole } = render( - - - - ); + const { getAllByRole } = render(); const elts = getAllByRole("button"); elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled")); }); it("is enabled by active", async () => { - const { getAllByRole } = render( - - - - ); + const { getAllByRole } = render(); const elts = getAllByRole("button"); elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled")); }); it("can disable a specific item", async () => { - const { getByText } = render( - - - - ); + const { getByText } = render(); const elt = getByText(lov[0].item as string); const button = elt.closest('[role="button"]'); expect(button).toHaveClass("Mui-disabled"); @@ -106,11 +73,9 @@ describe("Menu Component", () => { const dispatch = jest.fn(); const state: TaipyState = INITIAL_STATE; const { getByText } = render( - - - - - + + + ); const elt = getByText(lov[0].item as string); await userEvent.click(elt); diff --git a/frontend/taipy-gui/src/components/Taipy/Menu.tsx b/frontend/taipy-gui/src/components/Taipy/Menu.tsx index edf7df8183..3f9f686a42 100644 --- a/frontend/taipy-gui/src/components/Taipy/Menu.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Menu.tsx @@ -11,7 +11,7 @@ * specific language governing permissions and limitations under the License. */ -import React, { useCallback, useMemo, useState, MouseEvent, CSSProperties, useEffect } from "react"; +import React, { useCallback, useMemo, useState, MouseEvent, CSSProperties } from "react"; import MenuIco from "@mui/icons-material/Menu"; import ListItemButton from "@mui/material/ListItemButton"; import Drawer from "@mui/material/Drawer"; @@ -28,8 +28,7 @@ import { createSendActionNameAction } from "../../context/taipyReducers"; import { MenuProps } from "../../utils/lov"; import { useClassNames, useDispatch, useModule } from "../../utils/hooks"; import { getComponentClassName } from "./TaipyStyle"; -import { emptyArray, getBaseURL } from "../../utils"; -import { useLocation } from "react-router"; +import { emptyArray } from "../../utils"; const boxDrawerStyle = { overflowX: "hidden" } as CSSProperties; const headerSx = { padding: 0 }; @@ -37,13 +36,12 @@ const avatarSx = { bgcolor: (theme: Theme) => theme.palette.text.primary }; const baseTitleProps = { noWrap: true, variant: "h6" } as const; const Menu = (props: MenuProps) => { - const { label, onAction = "", lov, width, inactiveIds = emptyArray, active = true } = props; + const { label, onAction = "", lov, selectedItems, width, inactiveIds = emptyArray, active = true } = props; const [selectedValue, setSelectedValue] = useState(""); const [opened, setOpened] = useState(false); const dispatch = useDispatch(); const theme = useTheme(); const module = useModule(); - const location = useLocation(); const className = useClassNames(props.libClassName, props.dynamicClassName, props.className); const clickHandler = useCallback( @@ -82,15 +80,6 @@ const Menu = (props: MenuProps) => { ]; }, [opened, width, theme]); - useEffect(() => { - if (lov && lov.length) { - const value = lov.find((it) => getBaseURL() + it.id === location.pathname); - if (value) { - setSelectedValue(value.id); - } - } - }, [location.pathname, lov]); - return lov && lov.length ? ( @@ -121,6 +110,7 @@ const Menu = (props: MenuProps) => { disabled={!active || inactiveIds.includes(elt.id)} withAvatar={true} titleTypographyProps={titleProps} + isSelected={selectedItems?.some((item) => item.id === elt.id)} /> ))} diff --git a/frontend/taipy-gui/src/components/Taipy/MenuCtl.tsx b/frontend/taipy-gui/src/components/Taipy/MenuCtl.tsx index 4119b945dc..ed189ddabb 100644 --- a/frontend/taipy-gui/src/components/Taipy/MenuCtl.tsx +++ b/frontend/taipy-gui/src/components/Taipy/MenuCtl.tsx @@ -11,12 +11,19 @@ * specific language governing permissions and limitations under the License. */ -import React, { useMemo, useEffect } from "react"; +import React, {useMemo, useEffect} from "react"; -import { LovProps, useLovListMemo } from "./lovUtils"; -import { useClassNames, useDispatch, useDispatchRequestUpdateOnFirstRender, useDynamicProperty, useIsMobile, useModule } from "../../utils/hooks"; -import { createSetMenuAction } from "../../context/taipyReducers"; -import { MenuProps } from "../../utils/lov"; +import {LovProps, useLovListMemo} from "./lovUtils"; +import { + useClassNames, + useDispatch, + useDispatchRequestUpdateOnFirstRender, + useDynamicProperty, + useIsMobile, + useModule +} from "../../utils/hooks"; +import {createSetMenuAction} from "../../context/taipyReducers"; +import {MenuProps} from "../../utils/lov"; interface MenuCtlProps extends LovProps { label?: string; @@ -35,6 +42,7 @@ const MenuCtl = (props: MenuCtlProps) => { defaultLov = "", width = "15vw", width_Mobile_ = "85vw", + defaultSelectedItems = "", } = props; const dispatch = useDispatch(); const isMobile = useIsMobile(); @@ -46,6 +54,7 @@ const MenuCtl = (props: MenuCtlProps) => { useDispatchRequestUpdateOnFirstRender(dispatch, id, module, props.updateVars, props.updateVarName); const lovList = useLovListMemo(props.lov, defaultLov, true); + const lovSelectedItems = useLovListMemo(props.selectedItems, defaultSelectedItems, true); const inactiveIds = useMemo(() => { if (props.inactiveIds) { @@ -71,6 +80,7 @@ const MenuCtl = (props: MenuCtlProps) => { inactiveIds: inactiveIds, width: isMobile ? width_Mobile_ : width, className: className, + selectedItems: lovSelectedItems, } as MenuProps) ); return () => dispatch(createSetMenuAction({})); @@ -85,6 +95,7 @@ const MenuCtl = (props: MenuCtlProps) => { isMobile, className, dispatch, + lovSelectedItems ]); return <>; diff --git a/frontend/taipy-gui/src/components/Taipy/lovUtils.tsx b/frontend/taipy-gui/src/components/Taipy/lovUtils.tsx index cfe76f3554..6f7c244613 100644 --- a/frontend/taipy-gui/src/components/Taipy/lovUtils.tsx +++ b/frontend/taipy-gui/src/components/Taipy/lovUtils.tsx @@ -41,6 +41,8 @@ export interface LovProps extends TaipyActive defaultValue?: U; height?: string | number; valueById?: boolean; + selectedItems?: LoV; + defaultSelectedItems?: U; } /** @@ -148,6 +150,7 @@ export interface ItemProps { disabled: boolean; withAvatar?: boolean; titleTypographyProps?: TypographyProps<"span", { component?: "span"; }>; + isSelected?: boolean; } export const SingleItem = ({ @@ -158,11 +161,12 @@ export const SingleItem = ({ disabled, withAvatar = false, titleTypographyProps, + isSelected, }: ItemProps) => ( {typeof item === "string" ? ( diff --git a/frontend/taipy-gui/src/utils/lov.ts b/frontend/taipy-gui/src/utils/lov.ts index 544651fbcc..7005394f92 100644 --- a/frontend/taipy-gui/src/utils/lov.ts +++ b/frontend/taipy-gui/src/utils/lov.ts @@ -33,4 +33,5 @@ export interface MenuProps extends TaipyBaseProps { inactiveIds?: string[]; lov?: LovItem[]; active?: boolean; + selectedItems?: LovItem[]; } diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index 6ae813d338..3b85563097 100644 --- a/taipy/gui/_renderers/factory.py +++ b/taipy/gui/_renderers/factory.py @@ -343,6 +343,7 @@ class _Factory: ("inactive_ids", PropertyType.dynamic_list), ("hover_text", PropertyType.dynamic_string), ("lov", PropertyType.lov), + ("selected_items", PropertyType.single_lov), ] ) ._set_propagate(), diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index c4f87e533f..556a377770 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1512,6 +1512,11 @@ "type": "dynamic(Union[str,list[str]])", "doc": "Semicolon (';')-separated list or a list of menu items identifiers that are disabled." }, + { + "name": "selected_items", + "type": "dynamic(Union[str,list[str]])", + "doc": "Semicolon (';')-separated list or a list of menu items identifiers that are selected." + }, { "name": "width", "type": "str", From f03894be4f90db5d3126e01a1636d1c007c923ff Mon Sep 17 00:00:00 2001 From: namnguyen Date: Sat, 12 Oct 2024 23:26:04 +0700 Subject: [PATCH 04/24] Fix unit test --- frontend/taipy-gui/src/components/Taipy/MenuCtl.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/taipy-gui/src/components/Taipy/MenuCtl.spec.tsx b/frontend/taipy-gui/src/components/Taipy/MenuCtl.spec.tsx index eb777294cb..254d30ec0b 100644 --- a/frontend/taipy-gui/src/components/Taipy/MenuCtl.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/MenuCtl.spec.tsx @@ -87,6 +87,7 @@ describe("MenuCtl Component", () => { ], onAction: "on_action", width: "15vw", + selectedItems: [], }, type: "SET_MENU", }); From 5bb01cb046ce80c161587261f8ea1c21e10dfadb Mon Sep 17 00:00:00 2001 From: namnguyen Date: Tue, 15 Oct 2024 14:21:28 +0700 Subject: [PATCH 05/24] add example for menu --- doc/gui/examples/controls/menu_inactive.py | 26 ++++++ .../examples/controls/menu_inactive_ids.py | 27 +++++++ doc/gui/examples/controls/menu_label.py | 27 +++++++ doc/gui/examples/controls/menu_on_action.py | 36 +++++++++ .../examples/controls/menu_selected_ids.py | 27 +++++++ doc/gui/examples/controls/menu_simple.py | 26 ++++++ .../taipy-gui/src/components/Taipy/Menu.tsx | 80 +++++++++++++------ .../src/components/Taipy/MenuCtl.spec.tsx | 2 +- .../src/components/Taipy/MenuCtl.tsx | 18 ++--- .../src/components/Taipy/lovUtils.tsx | 16 ++-- frontend/taipy-gui/src/utils/lov.ts | 2 +- taipy/gui/_renderers/factory.py | 14 ++-- taipy/gui/viselements.json | 68 ++++++++-------- 13 files changed, 283 insertions(+), 86 deletions(-) create mode 100644 doc/gui/examples/controls/menu_inactive.py create mode 100644 doc/gui/examples/controls/menu_inactive_ids.py create mode 100644 doc/gui/examples/controls/menu_label.py create mode 100644 doc/gui/examples/controls/menu_on_action.py create mode 100644 doc/gui/examples/controls/menu_selected_ids.py create mode 100644 doc/gui/examples/controls/menu_simple.py diff --git a/doc/gui/examples/controls/menu_inactive.py b/doc/gui/examples/controls/menu_inactive.py new file mode 100644 index 0000000000..81fb37af73 --- /dev/null +++ b/doc/gui/examples/controls/menu_inactive.py @@ -0,0 +1,26 @@ +# Copyright 2021-2024 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. +# ----------------------------------------------------------------------------------------- +# To execute this script, make sure that the taipy-gui package is installed in your +# Python environment and run: +# python