فهرست منبع

#1801 Added close a specific notification feature (#1985)

* Added close a specific notification feature

* ruff space removal

* removed new __send_ws_alert

* comments removed

* add frontend and changed in notification id

* changed notification id by nanoid

* sapce correction

* resolved multiple issue

* return state._gui

* fixed useref error and added test for deleteAlert

* changed nanoid to random string

---------

Co-authored-by: Fred Lefévère-Laoide <90181748+FredLL-Avaiga@users.noreply.github.com>
Adesh Ghadage 6 ماه پیش
والد
کامیت
95dabf5844

+ 9 - 11
frontend/taipy-gui/src/components/Taipy/Alert.tsx

@@ -11,10 +11,11 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useCallback, useEffect, useMemo, useRef } from "react";
+import React, { useCallback, useEffect, useMemo } from "react";
 import { SnackbarKey, useSnackbar, VariantType } from "notistack";
 import IconButton from "@mui/material/IconButton";
 import CloseIcon from "@mui/icons-material/Close";
+import { nanoid } from 'nanoid';
 
 import { AlertMessage, createDeleteAlertAction } from "../../context/taipyReducers";
 import { useDispatch } from "../../utils/hooks";
@@ -25,7 +26,6 @@ interface AlertProps {
 
 const Alert = ({ alerts }: AlertProps) => {
     const alert = alerts.length ? alerts[0] : undefined;
-    const lastKey = useRef<SnackbarKey>("");
     const { enqueueSnackbar, closeSnackbar } = useSnackbar();
     const dispatch = useDispatch();
 
@@ -57,23 +57,21 @@ const Alert = ({ alerts }: AlertProps) => {
 
     useEffect(() => {
         if (alert) {
+            const notificationId = nanoid(); 
             if (alert.atype === "") {
-                if (lastKey.current) {
-                    closeSnackbar(lastKey.current);
-                    lastKey.current = "";
-                }
+                closeSnackbar(notificationId);  
             } else {
-                lastKey.current = enqueueSnackbar(alert.message, {
+                enqueueSnackbar(alert.message, {
                     variant: alert.atype as VariantType,
-                    action: notifAction,
+                    action: notifAction,  
                     autoHideDuration: alert.duration,
+                    key: notificationId,  
                 });
                 alert.system && new Notification(document.title || "Taipy", { body: alert.message, icon: faviconUrl });
             }
-            dispatch(createDeleteAlertAction());
+            dispatch(createDeleteAlertAction(notificationId));
         }
     }, [alert, enqueueSnackbar, closeSnackbar, notifAction, faviconUrl, dispatch]);
-
     useEffect(() => {
         alert?.system && window.Notification && Notification.requestPermission();
     }, [alert?.system]);
@@ -81,4 +79,4 @@ const Alert = ({ alerts }: AlertProps) => {
     return null;
 };
 
-export default Alert;
+export default Alert;

+ 32 - 6
frontend/taipy-gui/src/context/taipyReducers.spec.ts

@@ -50,6 +50,7 @@ import { Socket } from "socket.io-client";
 import { Dispatch } from "react";
 import { parseData } from "../utils/dataFormat";
 import * as wsUtils from "./wsUtils";
+import { nanoid } from 'nanoid';
 
 jest.mock("./utils", () => ({
     ...jest.requireActual("./utils"),
@@ -575,6 +576,7 @@ describe("taipyReducer function", () => {
             message: "some error message",
             system: true,
             duration: 3000,
+            notificationId: nanoid(),
         };
         const newState = taipyReducer({ ...INITIAL_STATE }, action);
         expect(newState.alerts).toContainEqual({
@@ -582,19 +584,37 @@ describe("taipyReducer function", () => {
             message: action.message,
             system: action.system,
             duration: action.duration,
+            notificationId: action.notificationId,
         });
     });
     it("should handle DELETE_ALERT action", () => {
+        const notificationId1 = "id-1234";
+        const notificationId2 = "id-5678";
         const initialState = {
             ...INITIAL_STATE,
             alerts: [
-                { atype: "error", message: "First Alert", system: true, duration: 5000 },
-                { atype: "warning", message: "Second Alert", system: false, duration: 3000 },
+                { atype: "error", message: "First Alert", system: true, duration: 5000, notificationId: notificationId1 },
+                { atype: "warning", message: "Second Alert", system: false, duration: 3000, notificationId: notificationId2 },
             ],
         };
-        const action = { type: Types.DeleteAlert };
+        const action = { type: Types.DeleteAlert, notificationId: notificationId1 };
+        const newState = taipyReducer(initialState, action);
+        expect(newState.alerts).toEqual([{ atype: "warning", message: "Second Alert", system: false, duration: 3000, notificationId: notificationId2 }]);
+    });
+    it('should not modify state if DELETE_ALERT does not match any notificationId', () => {
+        const notificationId1 = "id-1234";
+        const notificationId2 = "id-5678";
+        const nonExistentId = "000000";
+        const initialState = {
+            ...INITIAL_STATE,
+            alerts: [
+                { atype: "error", message: "First Alert", system: true, duration: 5000, notificationId: notificationId1 },
+                { atype: "warning", message: "Second Alert", system: false, duration: 3000, notificationId: notificationId2 },
+            ],
+        };
+        const action = { type: Types.DeleteAlert, notificationId: nonExistentId };
         const newState = taipyReducer(initialState, action);
-        expect(newState.alerts).toEqual([{ atype: "warning", message: "Second Alert", system: false, duration: 3000 }]);
+        expect(newState).toEqual(initialState);
     });
     it("should not modify state if no alerts are present", () => {
         const initialState = { ...INITIAL_STATE, alerts: [] };
@@ -602,7 +622,10 @@ describe("taipyReducer function", () => {
         const newState = taipyReducer(initialState, action);
         expect(newState).toEqual(initialState);
     });
-    it("should handle DELETE_ALERT action", () => {
+    it("should handle DELETE_ALERT action even when no notificationId is passed", () => {
+        const notificationId1 = "id-1234";
+        const notificationId2 = "id-5678";
+
         const initialState = {
             ...INITIAL_STATE,
             alerts: [
@@ -611,16 +634,18 @@ describe("taipyReducer function", () => {
                     atype: "type1",
                     system: true,
                     duration: 5000,
+                    notificationId: notificationId1,
                 },
                 {
                     message: "alert2",
                     atype: "type2",
                     system: false,
                     duration: 3000,
+                    notificationId: notificationId2,
                 },
             ],
         };
-        const action = { type: Types.DeleteAlert };
+        const action = { type: Types.DeleteAlert, notificationId: notificationId1 };
         const newState = taipyReducer(initialState, action);
         expect(newState.alerts).toEqual([
             {
@@ -628,6 +653,7 @@ describe("taipyReducer function", () => {
                 atype: "type2",
                 system: false,
                 duration: 3000,
+                notificationId: notificationId2,
             },
         ]);
     });

+ 15 - 5
frontend/taipy-gui/src/context/taipyReducers.ts

@@ -16,6 +16,7 @@ import { createTheme, Theme } from "@mui/material/styles";
 import merge from "lodash/merge";
 import { Dispatch } from "react";
 import { io, Socket } from "socket.io-client";
+import { nanoid } from 'nanoid';
 
 import { FilterDesc } from "../components/Taipy/tableUtils";
 import { stylekitModeThemes, stylekitTheme } from "../themes/stylekit";
@@ -91,6 +92,7 @@ export interface AlertMessage {
     message: string;
     system: boolean;
     duration: number;
+    notificationId?: string;
 }
 
 interface TaipyAction extends NamePayload, TaipyBaseAction {
@@ -108,6 +110,10 @@ interface TaipyMultipleMessageAction extends TaipyBaseAction {
 
 interface TaipyAlertAction extends TaipyBaseAction, AlertMessage {}
 
+interface TaipyDeleteAlertAction extends TaipyBaseAction {
+    notificationId: string;
+}
+
 export const BLOCK_CLOSE = { action: "", message: "", close: true, noCancel: false } as BlockMessage;
 
 export interface BlockMessage {
@@ -379,14 +385,16 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta
                         message: alertAction.message,
                         system: alertAction.system,
                         duration: alertAction.duration,
+                        notificationId: alertAction.notificationId || nanoid(),
                     },
                 ],
             };
         case Types.DeleteAlert:
-            if (state.alerts.length) {
-                return { ...state, alerts: state.alerts.filter((_, i) => i) };
-            }
-            return state;
+            const deleteAlertAction = action as unknown as TaipyAlertAction;
+            return {
+                ...state,
+                alerts: state.alerts.filter(alert => alert.notificationId !== deleteAlertAction.notificationId),
+            };
         case Types.SetBlock:
             const blockAction = action as unknown as TaipyBlockAction;
             if (blockAction.close) {
@@ -818,10 +826,12 @@ export const createAlertAction = (alert: AlertMessage): TaipyAlertAction => ({
     message: alert.message,
     system: alert.system,
     duration: alert.duration,
+    notificationId: alert.notificationId,
 });
 
-export const createDeleteAlertAction = (): TaipyBaseAction => ({
+export const createDeleteAlertAction = (notificationId: string): TaipyDeleteAlertAction => ({
     type: Types.DeleteAlert,
+    notificationId,
 });
 
 export const createBlockAction = (block: BlockMessage): TaipyBlockAction => ({

+ 40 - 8
taipy/gui/gui.py

@@ -21,6 +21,7 @@ import sys
 import tempfile
 import time
 import typing as t
+import uuid
 import warnings
 from importlib import metadata, util
 from importlib.util import find_spec
@@ -1330,15 +1331,26 @@ class Gui:
             send_back_only=True,
         )
 
-    def __send_ws_alert(self, type: str, message: str, system_notification: bool, duration: int) -> None:
+    def __send_ws_alert(
+            self, type: str,
+            message: str,
+            system_notification: bool,
+            duration: int,
+            notification_id: t.Optional[str] = None
+        ) -> None:
+        payload = {
+            "type": _WsType.ALERT.value,
+            "atype": type,
+            "message": message,
+            "system": system_notification,
+            "duration": duration,
+        }
+
+        if notification_id:
+            payload["notificationId"] = notification_id
+
         self.__send_ws(
-            {
-                "type": _WsType.ALERT.value,
-                "atype": type,
-                "message": message,
-                "system": system_notification,
-                "duration": duration,
-            }
+            payload,
         )
 
     def __send_ws_partial(self, partial: str):
@@ -2242,13 +2254,33 @@ class Gui:
         message: str = "",
         system_notification: t.Optional[bool] = None,
         duration: t.Optional[int] = None,
+        notification_id: t.Optional[str] = None,
     ):
+        if not notification_id:
+            notification_id = str(uuid.uuid4())
+
         self.__send_ws_alert(
             notification_type,
             message,
             self._get_config("system_notification", False) if system_notification is None else system_notification,
             self._get_config("notification_duration", 3000) if duration is None else duration,
+            notification_id,
         )
+        return notification_id
+
+    def _close_notification(
+        self,
+        notification_id: str,
+    ):
+        if notification_id:
+            self.__send_ws_alert(
+                type="",  # Since you're closing, set type to an empty string or a predefined "close" type
+                message="",  # No need for a message when closing
+                system_notification=False,  # System notification not needed for closing
+                duration=0,  # No duration since it's an immediate close
+                notification_id=notification_id
+            )
+
 
     def _hold_actions(
         self,

+ 11 - 1
taipy/gui/gui_actions.py

@@ -67,6 +67,7 @@ def notify(
     message: str = "",
     system_notification: t.Optional[bool] = None,
     duration: t.Optional[int] = None,
+    notification_id: str = "",
 ):
     """Send a notification to the user interface.
 
@@ -96,11 +97,20 @@ def notify(
     feature.
     """
     if state and isinstance(state._gui, Gui):
-        state._gui._notify(notification_type, message, system_notification, duration)
+        return state._gui._notify(notification_type, message, system_notification, duration, notification_id)
     else:
         _warn("'notify()' must be called in the context of a callback.")
 
 
+def close_notification(state: State, notification_id: str):
+    """Close a specific notification by ID."""
+    if state and isinstance(state._gui, Gui):
+        # Send the close command with the notification_id
+        state._gui._close_notification(notification_id)
+    else:
+        _warn("'close_notification()' must be called in the context of a callback.")
+
+
 def hold_control(
     state: State,
     callback: t.Optional[t.Union[str, t.Callable]] = None,