From bc9d07be8bda26012899b7702f5ad116186260a9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 18 Dec 2024 12:33:59 +0100 Subject: [PATCH] Ensure data models are reused ensuring that notification state persists (#7560) --- CHANGELOG.md | 2 + doc/about/releases.md | 2 + panel/io/datamodel.py | 5 +- panel/io/notifications.py | 61 +++++++++++++------------ panel/reactive.py | 21 +++++++++ panel/tests/ui/io/test_notifications.py | 17 ++++++- 6 files changed, 76 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17acdb8283..4bb80f5d35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,13 @@ This release fixes a regression causing .node_modules to be bundled into our rel ### Bug fixes +- Ensure Notifications are cleaned up correctly ([#4964](https://github.com/holoviz/panel/pull/4964)) - Ensure `FileDownload` label text updates correctly ([#7489](https://github.com/holoviz/panel/pull/7489)) - Fix `Tabulator` aggregation behavior ([#7450](https://github.com/holoviz/panel/pull/7450)) - Fix typing for `.servable` method ([#7530](https://github.com/holoviz/panel/pull/7530)) - Ensure `NestedSelect` respects `disabled` parameter ([#7533](https://github.com/holoviz/panel/pull/7533)) - Ensure errors in hooks aren't masked by fallback to different signature ([#7502](https://github.com/holoviz/panel/pull/7502)) +- Ensure Notifications are only shown once if scheduled onload ([#7504](https://github.com/holoviz/panel/pull/7504)) ### Documentation diff --git a/doc/about/releases.md b/doc/about/releases.md index 610549bae8..ec6bf67459 100644 --- a/doc/about/releases.md +++ b/doc/about/releases.md @@ -16,11 +16,13 @@ This release fixes a regression causing .node_modules to be bundled into our rel ### Bug fixes +- Ensure Notifications are cleaned up correctly ([#4964](https://github.com/holoviz/panel/pull/4964)) - Ensure `FileDownload` label text updates correctly ([#7489](https://github.com/holoviz/panel/pull/7489)) - Fix `Tabulator` aggregation behavior ([#7450](https://github.com/holoviz/panel/pull/7450)) - Fix typing for `.servable` method ([#7530](https://github.com/holoviz/panel/pull/7530)) - Ensure `NestedSelect` respects `disabled` parameter ([#7533](https://github.com/holoviz/panel/pull/7533)) - Ensure errors in hooks aren't masked by fallback to different signature ([#7502](https://github.com/holoviz/panel/pull/7502)) +- Ensure Notifications are only shown once if scheduled onload ([#7504](https://github.com/holoviz/panel/pull/7504)) ### Documentation diff --git a/panel/io/datamodel.py b/panel/io/datamodel.py index 445408c961..7a6bd521ea 100644 --- a/panel/io/datamodel.py +++ b/panel/io/datamodel.py @@ -199,7 +199,10 @@ def create_linked_datamodel(obj, root=None): else: _DATA_MODELS[cls] = model = construct_data_model(obj) properties = model.properties() - model = model(**{k: v for k, v in obj.param.values().items() if k in properties}) + props = {k: v for k, v in obj.param.values().items() if k in properties} + if root: + props['name'] = f"{root.ref['id']}-{id(obj)}" + model = model(**props) _changing = [] def cb_bokeh(attr, old, new): diff --git a/panel/io/notifications.py b/panel/io/notifications.py index abbd0cfb62..83c2a25db7 100644 --- a/panel/io/notifications.py +++ b/panel/io/notifications.py @@ -34,6 +34,8 @@ class Notification(param.Parameterized): notification_type = param.String(default=None, constant=True, label='type') + _rendered = param.Boolean(default=False) + _destroyed = param.Boolean(default=False) def destroy(self) -> None: @@ -194,46 +196,45 @@ def __css__(cls): }) """, "notifications": """ - var notification = state.current || data.notifications[data.notifications.length-1] - if (notification._destroyed) { - return - } - var config = { - duration: notification.duration, - type: notification.notification_type, - message: notification.message - } - if (notification.background != null) { - config.background = notification.background; - } - if (notification.icon != null) { - config.icon = notification.icon; - } - var toast = state.toaster.open(config); - function destroy() { - if (state.current !== notification) { + for (notification of data.notifications) { + if (notification._destroyed || notification._rendered) { + return + } + var config = { + duration: notification.duration, + type: notification.notification_type, + message: notification.message + } + if (notification.background != null) { + config.background = notification.background; + } + if (notification.icon != null) { + config.icon = notification.icon; + } + let toast = state.toaster.open(config); + function destroy() { notification._destroyed = true; } + notification._rendered = true + toast.on('dismiss', destroy) + if (notification.duration) { + setTimeout(destroy, notification.duration) + } + if (notification.properties === undefined) + return + view.connect(notification.properties._destroyed.change, function () { + state.toaster.dismiss(toast) + }) } - toast.on('dismiss', destroy) - if (notification.duration) { - setTimeout(destroy, notification.duration) - } - if (notification.properties === undefined) - return - view.connect(notification.properties._destroyed.change, function () { - state.toaster.dismiss(toast) - }) """, "_clear": "state.toaster.dismissAll()", "position": """ script('_clear'); script('render'); for (notification of data.notifications) { - state.current = notification; - script('notifications'); + notification._rendered = false; } - state.current = undefined + script('notifications'); """ } diff --git a/panel/reactive.py b/panel/reactive.py index 340a59c916..7fc101cec2 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -2170,6 +2170,27 @@ def _update_model( ) ): continue + elif isinstance(v, list) and all(isinstance(vs, param.Parameterized) for vs in v): + from .io.datamodel import create_linked_datamodel + old = getattr(model.data, prop) + if isinstance(old, list): + mapping = {o.name: o for o in old} + vals = [] + for vs in v: + if (vname:=f"{root.ref['id']}-{id(vs)}") in mapping: + vals.append(mapping[vname]) + else: + vals.append(create_linked_datamodel(vs, root)) + v = vals + data_msg[prop] = v + elif isinstance(v, param.Parameterized): + from .io.datamodel import create_linked_datamodel + old = getattr(model.data, prop) + if old.name == f"{root.ref['id']}-{id(v)}": + v = old + else: + v = create_linked_datamodel(vs, root) + data_msg[prop] = v elif isinstance(v, str): data_msg[prop] = HTML_SANITIZER.clean(v) else: diff --git a/panel/tests/ui/io/test_notifications.py b/panel/tests/ui/io/test_notifications.py index d830046b67..ac532c309f 100644 --- a/panel/tests/ui/io/test_notifications.py +++ b/panel/tests/ui/io/test_notifications.py @@ -70,4 +70,19 @@ def app(): page.click('.bk-btn') - expect(page.locator('.notyf__message')).to_have_text('Disconnected!') + +def test_onload_notification(page): + def onload_callback(): + state.notifications.warning("Warning", duration=0) + state.notifications.info("Info", duration=0) + + def app(): + config.notifications = True + state.onload(onload_callback) + return Markdown("# Hello world") + + serve_component(page, app) + + expect(page.locator('.notyf__message')).to_have_count(2) + expect(page.locator('.notyf__message').nth(0)).to_have_text("Warning") + expect(page.locator('.notyf__message').nth(1)).to_have_text("Info")