Skip to content

adding NotificationContainer to be more inline with Mantine#539

Merged
AnnMarieW merged 24 commits intosnehilvj:masterfrom
BSd3v:notifications-rework
Jun 1, 2025
Merged

adding NotificationContainer to be more inline with Mantine#539
AnnMarieW merged 24 commits intosnehilvj:masterfrom
BSd3v:notifications-rework

Conversation

@BSd3v
Copy link
Copy Markdown
Contributor

@BSd3v BSd3v commented Mar 20, 2025

utilizes the api more closely (https://mantine.dev/x/notifications/)
exposes the api and store at appNotifications in the dash_mantine_components namespace

fixes #525

utilizes the api more closely
exposes the api and store at `appNotifications` in the `dash_mantine_components` namespace
@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented Mar 20, 2025

I cant figure out how to tie the store together to be able to see what's in the queue vs the active notifications.

https://mantine.dev/x/notifications/#subscribe-to-notifications-state


Figured out a workaround for this, its not their describe way

@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented Mar 20, 2025

Here is an example app to show the difference in the API:


import dash_mantine_components as dmc
from dash import Output, Input, html, callback, Dash, ctx, _dash_renderer
from dash.exceptions import PreventUpdate
from dash_iconify import DashIconify

# this is optional when using dash >3.0.0
_dash_renderer._set_react_version("18.2.0")

app = Dash(__name__, external_stylesheets=dmc.styles.ALL)

app.layout = dmc.MantineProvider(
    [
        dmc.NotificationContainer(id='test-container'),
        dmc.Group(
            children=[
                dmc.Button(
                    "Load Data",
                    id="show-notification",
                ),
                dmc.Button(
                    "Update",
                    id="update-notification",
                ),
            ],
        ),
    ]
)


@callback(
    Output("test-container", "sendNotifications"),
    Input("show-notification", "n_clicks"),
    Input("update-notification", "n_clicks"),
    prevent_initial_call=True,
)
def notify(nc1, nc2):
    if not ctx.triggered:
        raise PreventUpdate
    else:
        button_id = ctx.triggered_id
        if "show" in button_id:
            return {'show': [
                dict(
                    id=f"my-notification-{nc1}",
                    title="Process initiated",
                    message="The process has started.",
                    loading=True,
                    color="orange",
                    autoClose=False,
                )
                ]
            }
        else:
            return {'update': [
                dict(
                    id=f"my-notification-{nc2}",
                    title="Data loaded",
                    message="Notification closing in 2 seconds",
                    color="green",
                    loading=False,
                    action="update",
                    autoClose=2000,
                    icon=DashIconify(icon="akar-icons:circle-check"),
                )
                ]
            }


if __name__ == "__main__":
    app.run(debug=True)

…ations available at `dash_mantine_components.appNotifications.store`
@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented Mar 20, 2025

fixes #525

@AnnMarieW AnnMarieW requested a review from alexcjohnson March 21, 2025 23:30
@AnnMarieW
Copy link
Copy Markdown
Collaborator

Hey Alex - this PR is ready for review. It works well and looks like it resolves the issue from the original component.

Would like to get your thoughts on the API and how to handle the transition from the current method to this one in order to avoid breaking changes (and confusion)

Comment on lines +125 to +128
return () => {
delete appNotifications['api']
delete appNotifications['store']
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it important to remove these on unmount? Thinking about two scenarios:

  1. A clientside callback that's set to perhaps clear the notifications in several situations, one of which also removes the NotificationContainer entirely, depending on the order of operations the clientside callback may be executed after the NotificationContainer has already unmounted. Feels like we could leave the api and store there even if accessing them isn't going to do anything visible.
  2. Changing layouts/pages, if the NotificationContainer is in both layout contents rather than in the page skeleton. Can we always guarantee that the new one will be created after the previous one's unmount hooks have executed? If for any reason these happened in the opposite order we'd end up deleting them after creating them the second time and they'd end up gone. This one seems unlikely, unless you do something really weird like put the two layout chunks into different parts of the page skeleton so they're both briefly on screen simultaneously... nevertheless I feel like we could just leave these items where they are and avoid this possibility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I guess we could leave them, just not sure what error handlers that Mantine provides on them.

It's not recommended to have multiple notification providers in the app, so both of these scenarios shouldnt really happen.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not recommended is different from won't happen 😉

The user's goal in these scenarios is not to have multiple notification providers at the same time, it's to move the notification provider from one place to another - perhaps on one page you want them at the top level, so extending vertically down from the top right corner on top of other content, but on a different page you want something else always visible in the top right corner so you put the notifications inside a smaller container.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The notification itself can be placed in different places, or you could have a clientside update based upon the layout you are loading. :)

Comment thread src/ts/components/extensions/notifications/NotificationContainer.tsx Outdated
Comment thread src/ts/components/extensions/notifications/NotificationContainer.tsx Outdated
/** Determines whether notifications container should be rendered inside `Portal`, `true` by default */
withinPortal?: boolean;
/** Notifications to be passed to the API */
sendNotifications?: NotificationsFormat;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall I like this pattern, but most of the time you're just going to use {'show': [...]} - I wonder if it would be better as three separate props, like showNotifications, hideNotifications, updateNotifications?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, we could do that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So, when working with this, its kinda frustrating have to split this into multiple props. I'll push the changes and let you look at it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Frustrating when you have logic that needs to decide between show and update or some such? I can see that, though I still feel like this is an edge case so I'm not too bothered by that being less convenient. Do you agree or do you think it's a common need?

An alternative would be to make one list of notifications, but give each one an extra optional key like action ?= 'show', 'update', 'hide'. That means the object no longer quite matches the Mantine notification format, but one extra key isn't so bad. Then the code to handle them would be something like:

useEffect(() => {
    if (notifications) {
        notifications.forEach((notification) => {
            const {action, ...realNotification} = notification;
            notifications[action || 'show'](newRenderDashComponents(realNotification, ['message', 'icon', 'title']));
        });
        setProps({ notifications: [] }); // Avoid duplicate processing
    }
}, [notifications]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That is how the original dmc api was done.

I think it feels better this way, especially with the default of show.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actaully, I also think it would be better to have separate props for show update and hide (rather than sendNotificaitons) It's a closer match to the Mantine API. It also makes it easier to know what props each action requires. For example, hide only needs the notification id, show and update can take any of the valid Notification props. Currently if you use an invalid action you get a very hard to debug error message. Separate props would ensure only the valid Mantine function is used.

Bryan has made a good case that it's more convenient if it's all in one prop (sendNotifications), but a workaround for that could be using setProps or the api directly in a clientside callback using the new appNotifications

Copy link
Copy Markdown
Contributor Author

@BSd3v BSd3v May 21, 2025

Choose a reason for hiding this comment

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

This is the current way that I have set_props working for my app, notice that it just targets the children prop:

def app_notification(notification_type, message, ctx=None, nid=None):
    """
    Adds a notification to the app's notification context.

    Parameters:
    - notification_type: The type of notification (e.g., 'success', 'error').
    - message: The message to display in the notification.
    - ctx: The context to use for the notification. Defaults to Dash's callback context if not provided.
    - nid: An optional notification ID.
    """
    # Use the provided context or default to Dash's callback context
    context = ctx or dash._callback_context

    # Safely retrieve the current children from the context
    try:
        updated_props = context._get_context_value().updated_props
        children = updated_props.get('app_notification', {}).get('children', [])
    except AttributeError:
        # Handle cases where context or updated_props might be None
        children = []

    # Append the new notification
    children.append(notification(notification_type, message, ctx, nid))

    # Update the properties with the new list of children
    set_props("app_notification", {"children": children})

Updating this would require that I pull the props for show, update, hide and update them as needed, whereas having the notification provide the action is easier from this standpoint.

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Also note that Mantine is releasing V8 next month. If the best solution has breaking changes, we could include them with the others when we upgrade to V8.

@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented Apr 28, 2025

The notifications wont be breaking changes as this is a new component. We should really deprecate the old version though.


// Define the Notification interface based on your requirements
interface Notification {
color?: MantineColor;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • Need to add docstrings for each prop.
  • Missing style, styles, className, classNames although currently dash does not do any error checking and it's possible to include these props anyway, but it would be good to add for completeness.
  • If there is an invalid option used for position there is a hard to debug error message generated by Mantine. We should validate the position prop before it's sent to Mantine.

/** Notifications container z-index, `400` by default */
zIndex?: string | number;
/** Determines whether notifications container should be rendered inside `Portal`, `true` by default */
withinPortal?: boolean;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

missing the portalProps prop.
Also, do we need the store prop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

store as in what? Is this to be from the notifications? If so, we'd have to have a hook from the store to know when it updated.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ok, let's skip the store prop


const componentPath = getContextPath()

useEffect(() => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a reason for using useEffect rather than Mantine's UseDidUpdate ? https://mantine.dev/hooks/use-did-update/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

useDidUpdate only runs on the prop updates, this would not capture sendNotifications initially sent to the component.

Comment thread src/ts/components/extensions/notifications/Notification.tsx Outdated
Comment thread src/ts/components/extensions/notifications/NotificationProvider.tsx Outdated
BSd3v and others added 3 commits May 21, 2025 15:53
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
…r.tsx

Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
Comment thread src/ts/utils/dash3.ts
@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented May 22, 2025

Should add a couple tests for hideNotifications and also checking with a couple of pattern-matching callbacks.

@AnnMarieW
Copy link
Copy Markdown
Collaborator

Here are the new Notification docs snehilvj/dmc-docs#199

@AnnMarieW
Copy link
Copy Markdown
Collaborator

@alexcjohnson This PR is ready for a final review. We are planning on including this in V2 so that people can refer to the V1 docs if they are still using the deprecated components.

The new Notification docs and a detailed migration guide are ready (with some final edits to do)

The only open item is whether to rename the component NotificationsContainer (with an s) any thoughts on that?

const { setProps, loading_state, ...others } = props;

useEffect(() => {
console.warn('This method of Notifications is deprecated and will be removed in a future major release. Instead, use `NotificationContainer')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
console.warn('This method of Notifications is deprecated and will be removed in a future major release. Instead, use `NotificationContainer')
console.warn('`NotificationProvider` is deprecated and will be removed in a future major release. Instead, use `NotificationContainer`')

Might not be obvious from the console what "this method" is (and "method" has a more precise meaning anyway, ie a function bound to a class)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good point - that's better. But should probably be NotificationProvider and Notification are deprecated....

Copy link
Copy Markdown
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

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

LGTM! I'd stick with NotificationContainer, I see the point about it holding multiple notifications, but it's grammatically funny. A shoe store sells many shoes, a bike lane can have many bikes...

@AnnMarieW AnnMarieW merged commit 155785c into snehilvj:master Jun 1, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rework Notifications into NotificationContainer and Expose notifications API from Mantine

3 participants