Skip to content

Feature: #2471 notification on_close #2519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from

Conversation

Andre-Pestana0
Copy link

@Andre-Pestana0 Andre-Pestana0 commented Mar 31, 2025

What type of PR is this?

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Description

Implementation of a way to know if a notification was closed or not, including a callback function for the notification and the reason behind it.

Related Issue

Checklist

  • This solution meets the acceptance criteria of the related issue.
  • The related issue checklist is completed.
  • This PR adds unit tests for the developed code. If not, why?
  • End-to-End tests have been added or updated. If not, why?
  • The documentation has been updated, or a dedicated issue created. (If applicable)
  • The release notes have been updated? (If applicable)

@Andre-Pestana0
Copy link
Author

Andre-Pestana0 commented Mar 31, 2025

Hello there, I have some doubts that I would like to share.

First and foremost this is my work until now:

  • I have added a callback to notify "on_close" and inside of it Im expecting a user to send the state, notification_id and reason for closing the notification.
def notify(
    state: State,
    notification_type: str = "info",
    message: str = "",
    system_notification: t.Optional[bool] = None,
    duration: t.Optional[int] = None,
    id: str = "",
    on_close: t.Optional[t.Callable[[State, str, str], None]] = None,
) -> t.Optional[str]:

....


    def _notify(
        self,
        notification_type: str = "I",
        message: str = "",
        system_notification: t.Optional[bool] = None,
        duration: t.Optional[int] = None,
        notification_id: t.Optional[str] = None,
        on_close: t.Optional[t.Callable[[State, str, str], None]] = None,
    ):
        if notification_id and on_close:
            self._notification_callbacks[notification_id] = on_close  # A dictonary was created in order to store the callback

  • I have added the reason to on_close:
def close_notification(state: State, id: str, reason:str) -> None:
...



    def _close_notification(
        self,
        notification_id: str,
        reason: str = "forced", 
    ):
        if notification_id:
            self.__send_ws_notification(
                type="",  # Empty string indicates closing
                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,
            )

            if notification_id in self._notification_callbacks:
                callback = self._notification_callbacks.pop(notification_id)  
                callback(self, notification_id, reason)

The final result can be seen by the following code and prints:

from taipy.gui import Gui, notify, close_notification

#Callback function
def on_close(state, notification_id, reason):
    print(f"Notification {notification_id} closed due to {reason}")

# Function to trigger a notification
def send_notification(state):
    notify(state, "info", "This is a test notification!", None, None, "3", on_close)

# Function to close the notification
def close_test_notification(state):
    close_notification(state, "3", "forced")

if __name__ == "__main__":
    page = """
# Notification Demo

Click the button to trigger a notification:

<|button|text=Send Notification|on_action=send_notification|>

Click the button to close the notification:

<|button|text=Close Notification|on_action=close_test_notification|>
"""
Gui(page).run()

Image

Image

My question

I don't understand if the changes requested are these or if the reasons"forced" and "timeout" should be something that the user never has to actually state

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please do not commit file that are not related to the PR
I suppose this PR is missing a front-end part ?
How should that part be handled ?

@Andre-Pestana0
Copy link
Author

Andre-Pestana0 commented Mar 31, 2025

please do not commit file that are not related to the PR I suppose this PR is missing a front-end part ? How should that part be handled ?

Hello there @FredLL-Avaiga, thank you for the reply
Regarding the files that are not meant to be committed I will make sure that in the final version those will not be included.
I sent this PR in order to clarify this doubt:

I don't understand if the changes requested are these or if the reasons"forced" and "timeout" should be something that the user never has to actually state

For now there are no front-end modifications, I just wanted to clarify my previous question so that I am aware on what needs to be done

@Andre-Pestana0
Copy link
Author

Hello there @FabienLelaquais,
First and foremost sorry for bothering,
I have a question regarding this issue and I was wondering if you could give some insight.

I don't understand if the changes requested are the ones that I made (the user inputs the reason whenever he closes the notification), or if the reasons "forced" and "timeout" should be something that the user never has to actually state.

You also said this:

It could make sense for the app to know about this, in order to potentially show other notifications that would be unreadable should too many notifications be visible yet.

However, when I try create multiple notifications there is a limit of 5 notifications that are displayed simultaneously, and whenever there is a sixth notification, one of the older notifications will be removed (in a First In First Out manner) .
I don't know if i am missing something, but it seems that that problem would not occur (but again, I might be missing something)

Looking forward to hearing from you

@FabienLelaquais
Copy link
Member

FabienLelaquais commented Apr 1, 2025

Hello there

Hi @Andre-Pestana0
And thank you for your involvement in this thing. And also, don't apologize for asking questions: that's the only path to a proposal that you and us can agree on.

Now.
"I don't understand if the changes requested are the ones that I made"
Well the point of this issue is to let the application know if and when a notification was closed, and why.
When the app creates a notification, now it appears on the front-end, and the app can forget about it.
I tried to come up with a rationale as to why the application would want to know when closing a notification happens, and you pointed out that my use case makes little sense (since no more than five consecutive notifications are visible anyway).
So let's forget about that use case and imagine another one, such as a notification that a developer really wants to make sure the user acknowledges and interacts with (by closing it). That could be a notification about a severe problem that the app needs the user to know about and fix.
So when the user closes (manually) the notification (that in this case would have been created with an illimited time out period), then the application needs to be notified that this happens. The "why" is the cherry on the cake: because the front-end side 'knows' why a notification is closed (timeout vs manual close), it's easy to let the back end know as well. That is what I called the "reason": depending on the cause of the notification being closed, the "reason" is set - by the front-end code - to "timeout" or "forced" (indicating a manual interaction).
At the end of the day, it is the user interaction (click the close box, or wait for timeout) that sets the 'reason', not the programmer.

Does that makes sense?

@Andre-Pestana0
Copy link
Author

it is the user interaction (click the close box, or wait for timeout) that sets the 'reason', not the programm

@FabienLelaquais
It makes total sense!
Thank you so much for the clarification!

@Andre-Pestana0
Copy link
Author

Andre-Pestana0 commented Apr 10, 2025

Hello there @FredLL-Avaiga @FabienLelaquais
First and foremost apologies for including the main.py and the package-lock.json. I guarantee they will not be present in the final version

I think I'm almost at the solution, but I'm missing something...
I already have defined in the frontend the reason for the deletion of the notification (forced or timeout) and I'm using the disptach function to send this info to the backend.

const notificationClosed = useCallback(
        (event: SyntheticEvent | null, reason: CloseReason, key?: SnackbarKey) => {
            const final_reason = reason === "timeout" ? "timeout" : "forced";
            if (key) {
                dispatch(createDeleteNotificationAction(key.toString(), final_reason));
            }
            snackbarIds.current = Object.fromEntries(
                Object.entries(snackbarIds.current).filter(([id]) => id !== key)
            );
        },
        [dispatch] 
    );

I have been trying to understand how to to gather this data from the backend, but with no success.
Could you give some guidance?
Thank you very much for your time

@FredLL-Avaiga FredLL-Avaiga changed the title Issue 2471 notification Feature: #2471 notification on_close Apr 14, 2025
@FredLL-Avaiga FredLL-Avaiga self-requested a review April 14, 2025 16:45
@Andre-Pestana0
Copy link
Author

Andre-Pestana0 commented Apr 14, 2025

Hello @FredLL-Avaiga,

Thank you so much for your reply. I will make sure to implement the requested changes.

Sorry for bringing this up again, but I'm still having some difficulty understanding how the communication between the frontend and backend is happening. I understand that by using the dispatch function, I'll be sending information to the frontend, but I'm not quite sure how to inspect what data is being sent and how to retrieve that same data from the backend (but understanding how is it being sent will help me figure out this part).

Could you please help me understand how I can view the data being sent from the frontend to the backend?

I've tried using console.log, which helped a bit, but I'm still missing some details. I appreciate your help and hope I’m not burdening you with too many questions.

Thank you again!

@FredLL-Avaiga
Copy link
Member

Hello @FredLL-Avaiga,

Thank you so much for your reply. I will make sure to implement the requested changes.

Sorry for bringing this up again, but I'm still having some difficulty understanding how the communication between the frontend and backend is happening. I understand that by using the dispatch function, I'll be sending information to the frontend, but I'm not quite sure how to inspect what data is being sent.

Could you please help me understand how I can view the data being sent from the frontend to the backend?

I've tried using console.log, which helped a bit, but I'm still missing some details. I appreciate your help and hope I’m not burdening you with too many questions.

Thank you again!

In this case, you want to trigger a back-end callback from the front-end.
we provide a action for this: createSendActionNameAction

You can find an example of its use in the Button component

@Andre-Pestana0
Copy link
Author

Hello there @FredLL-Avaiga,

I've been working on the notification callback for on_close and this is what I’ve done so far:

Backend:

  • The callback function is now saved properly in _notification_callbacks, and the backend accepts both strings and callables for on_close.

Frontend:

  • The notificationClosed function fires as expected when a notification is closed.
  • However, the callback argument in notificationClosed is always undefined, even though I believe the backend is sending a valid string for on_close.

At one point, I was able to make this work, but after making some changes, the callback stopped being passed correctly. The on_close value in the frontend is always undefined.

I suspect the issue might be happening during the transition from the backend to the frontend. I’ve checked the backend and confirmed that the on_close_str is not None before sending the notification.

Would appreciate any pointers on what might be causing this or what I might have missed.

Thank you so much for your time!

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to change the createNotificationAction to handle the on_close callback name

@Andre-Pestana0
Copy link
Author

Hello there.

I believe I did almost all of the requested changes.
I had not realized that I removed the dispatch, and that was the reason the connection front-end backend stopped working.
Now i can the callback is activated from the frontend.

image

My only problem now is regarding the callback that is always undefined

Notification.tsx

const notificationClosed = useCallback(
        (event: SyntheticEvent | null, reason: CloseReason, key?: SnackbarKey, callback?: string) => {
            const final_reason = reason === "timeout" ? "timeout" : "forced";
            console.log(callback)
            if (key) {
                if (true) { //Should be if(callback) but callback is always undefined
                    dispatch(createSendActionNameAction(notification?.notificationId, module, "on_notification_closed", final_reason));               
                }
                dispatch(createDeleteNotificationAction(key.toString()));
            }
            snackbarIds.current = Object.fromEntries(
                Object.entries(snackbarIds.current).filter(([id]) => id !== key)
            );
        },
        [dispatch, module, notification?.notificationId] 
    );

gui.py

def _notify(
        self,
        notification_type: str = "I",
        message: str = "",
        system_notification: t.Optional[bool] = None,
        duration: t.Optional[int] = None,
        notification_id: t.Optional[str] = None,
        on_close: t.Optional[t.Union[str, t.Callable[[State, str, str], None]]] = "",
        ):
        on_close_str = None

        if notification_id and on_close:
            if isinstance(on_close, str):
                func = self._get_user_function(on_close)
                if callable(func):
                    on_close_str = on_close
                else:
                    _warn(f"Notification on_close callback '{on_close}' is not a valid function.")
            else:
                _warn(f"Invalid on_close value for notification {notification_id}: {on_close}")

        self.__send_ws_notification(
            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,
            on_close_str,  
        )
        return notification_id

In the gui.py i have done some prints and I am positive that "on_close_str" is indeed a valid string.

I have tried to use notification?.on_close (on the front-end) but that also comes as undefined

Is there anything else I'm missing?
Thank you so much for your time

@Andre-Pestana0
Copy link
Author

Andre-Pestana0 commented Apr 17, 2025

Hello there, thank you for the reply.
I believe the issues that were pointed out were tackled.

  • on_close will handle functions
  • default value of reason: changed to user_action
  • NotificationMessage has the requested parameters
  • on_close_str was added to __send_ws_notification

Im trying to call notification.on_close on the front-end but it still is undefined. Once again I checked that on_close_str is a valid string that is sent in the backend

I added a print("payload", payload) just to check if the payload had the correct information, and it has. My callback is called on_notification_closed and you can see it at the end.

image

Thank you so much for your time.

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose you still need to change the case Types.SetNotification ...

@Andre-Pestana0
Copy link
Author

Hello there,

Thank you so much for your help, i had to do that and add on_close here:

export const createNotificationAction = (notification: NotificationMessage): TaipyNotificationAction => ({
    type: Types.SetNotification,
    nType: getNotificationType(notification.nType),
    message: notification.message,
    system: notification.system,
    duration: notification.duration,
    notificationId: notification.notificationId,
    snackbarId: notification.snackbarId,
    on_close: notification?.on_close,
});

Now everything is working!
If you just could please check if the the last changes are to your liking, then I would update my branch, add tests and finally remove everything that shouldn't be commited (main.py, etc...)

Thank you so much for your time

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there
Well done

@Andre-Pestana0
Copy link
Author

Andre-Pestana0 commented Apr 23, 2025

Hello there @FredLL-Avaiga
I think I tackled everything.
Thank you so much for your time!

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any test ?

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there but you really need to remove those files from the PR ...

@Andre-Pestana0
Copy link
Author

Hello there @FredLL-Avaiga, thank you for the reply!

I think i did the change to the backend test.
Sorry for including the files you don't want, I don't know a quick way to exclude all the undesired changes so I will do it when I'm sure everything else is to your liking.

Thank you so much for your time!

Copy link
Contributor

This PR has been labelled as "🥶Waiting for contributor" because it has been inactive for more than 14 days. If you would like to continue working on this PR, then please add new commit or another comment, otherwise this PR will be closed in 14 days. For more information please refer to the contributing guidelines.

@github-actions github-actions bot added the 🥶Waiting for contributor Issues or PRs waiting for a long time label May 15, 2025
@Andre-Pestana0
Copy link
Author

Andre-Pestana0 commented May 17, 2025

Hello there! @FredLL-Avaiga

Is this version of the tests acceptable?
I removed every change on the unwanted files

Thank you very much for your time

@github-actions github-actions bot removed the 🥶Waiting for contributor Issues or PRs waiting for a long time label May 18, 2025
Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, can you fix the errors ?

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and check the linter errors

@Andre-Pestana0
Copy link
Author

Hello there! @FredLL-Avaiga
I believe that everything was sorted out.

Thank you for your time.

Copy link
Member

@FredLL-Avaiga FredLL-Avaiga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, tx a lot @Andre-Pestana0
any input @FabienLelaquais ?

Copy link
Contributor

github-actions bot commented Jun 6, 2025

This PR has been labelled as "🥶Waiting for contributor" because it has been inactive for more than 14 days. If you would like to continue working on this PR, then please add new commit or another comment, otherwise this PR will be closed in 14 days. For more information please refer to the contributing guidelines.

@github-actions github-actions bot added the 🥶Waiting for contributor Issues or PRs waiting for a long time label Jun 6, 2025
@FredLL-Avaiga FredLL-Avaiga added 🖰 GUI Related to GUI 🟨 Priority: Medium Not blocking but should be addressed ✨New feature and removed 🥶Waiting for contributor Issues or PRs waiting for a long time labels Jun 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🖰 GUI Related to GUI ✨New feature 🟨 Priority: Medium Not blocking but should be addressed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Let the application know that a notification was closed
3 participants