Browse Source

Based on new requirement for Menu

namnguyen 7 months ago
parent
commit
d2704a367b

+ 11 - 46
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(
-            <Router>
-                <Menu lov={lov} />
-            </Router>
-        );
+        const { getByText } = render(<Menu lov={lov} />);
         const elt = getByText("Item 1");
         expect(elt.tagName).toBe("SPAN");
     });
 
     it("uses the class", async () => {
-        const { getByText } = render(
-            <Router>
-                <Menu lov={lov} className="taipy-menu" />
-            </Router>
-        );
+        const { getByText } = render(<Menu lov={lov} className="taipy-menu" />);
         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(
-            <Router>
-                <Menu lov={lovWithImage} />
-            </Router>
-        );
+        const { getByText } = render(<Menu lov={lovWithImage} />);
         const elt = getByText("I2");
         expect(elt.tagName).toBe("DIV");
     });
 
     it("can display an image", async () => {
         const lovWithImage = [...lov, imageItem];
-        const { getByAltText } = render(
-            <Router>
-                <Menu lov={lovWithImage} />
-            </Router>
-        );
+        const { getByAltText } = render(<Menu lov={lovWithImage} />);
         const elt = getByAltText("Image");
         expect(elt.tagName).toBe("IMG");
     });
 
     it("is disabled", async () => {
-        const { getAllByRole } = render(
-            <Router>
-                <Menu lov={lov} active={false} />
-            </Router>
-        );
+        const { getAllByRole } = render(<Menu lov={lov} active={false} />);
         const elts = getAllByRole("button");
         elts.forEach((elt, idx) => idx > 0 && expect(elt).toHaveClass("Mui-disabled"));
     });
 
     it("is enabled by default", async () => {
-        const { getAllByRole } = render(
-            <Router>
-                <Menu lov={lov} />
-            </Router>
-        );
+        const { getAllByRole } = render(<Menu lov={lov} />);
         const elts = getAllByRole("button");
         elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled"));
     });
 
     it("is enabled by active", async () => {
-        const { getAllByRole } = render(
-            <Router>
-                <Menu lov={lov} active={true} />
-            </Router>
-        );
+        const { getAllByRole } = render(<Menu lov={lov} active={true} />);
         const elts = getAllByRole("button");
         elts.forEach((elt) => expect(elt).not.toHaveClass("Mui-disabled"));
     });
 
     it("can disable a specific item", async () => {
-        const { getByText } = render(
-            <Router>
-                <Menu lov={lov} inactiveIds={[lov[0].id]} />
-            </Router>
-        );
+        const { getByText } = render(<Menu lov={lov} inactiveIds={[lov[0].id]} />);
         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(
-            <Router>
-                <TaipyContext.Provider value={{ state, dispatch }}>
-                    <Menu lov={lov} onAction="action" />
-                </TaipyContext.Provider>
-            </Router>
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Menu lov={lov} onAction="action" />
+            </TaipyContext.Provider>
         );
         const elt = getByText(lov[0].item as string);
         await userEvent.click(elt);

+ 4 - 14
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 ? (
         <Drawer variant="permanent" anchor="left" sx={drawerSx} className={`${className} ${getComponentClassName(props.children)}`}>
             <Box style={boxDrawerStyle}>
@@ -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)}
                         />
                     ))}
                 </List>

+ 16 - 5
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<string> {
     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 <></>;

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

@@ -41,6 +41,8 @@ export interface LovProps<T = string | string[], U = string> 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) => (
     <ListItemButton
         onClick={clickHandler}
         data-id={value}
-        selected={Array.isArray(selectedValue) ? selectedValue.indexOf(value) !== -1 : selectedValue === value}
+        selected={Array.isArray(selectedValue) ? selectedValue.indexOf(value) !== -1 : selectedValue === value || isSelected}
         disabled={disabled}
     >
         {typeof item === "string" ? (

+ 1 - 0
frontend/taipy-gui/src/utils/lov.ts

@@ -33,4 +33,5 @@ export interface MenuProps extends TaipyBaseProps {
     inactiveIds?: string[];
     lov?: LovItem[];
     active?: boolean;
+    selectedItems?: LovItem[];
 }

+ 1 - 0
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(),

+ 5 - 0
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",