From db3aacf3341acde346bf3b6e93c4f164f9480a93 Mon Sep 17 00:00:00 2001 From: Odei Maiz <33152403+odeimaiz@users.noreply.github.com> Date: Wed, 6 Nov 2024 09:18:19 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20[Frontend]=20Improve=20Notificat?= =?UTF-8?q?ion=20texts=20and=20Bell's=20UX=20(#6661)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../class/osparc/dashboard/StudyBrowser.js | 6 + .../class/osparc/desktop/MainPageDesktop.js | 48 +++--- .../source/class/osparc/info/CommentUI.js | 14 +- .../class/osparc/navigation/NavigationBar.js | 12 +- .../class/osparc/notification/Notification.js | 16 ++ .../osparc/notification/NotificationUI.js | 141 ++++++++++++------ .../osparc/notification/Notifications.js | 37 ++++- .../notification/NotificationsButton.js | 7 +- .../client/source/class/osparc/store/Store.js | 21 ++- .../api/v0/openapi.yaml | 32 ++++ .../users/_notifications.py | 6 + .../unit/isolated/test_user_notifications.py | 4 + .../with_dbs/03/test_users__notifications.py | 4 + 13 files changed, 246 insertions(+), 102 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js index 804986735a7..ab29d814808 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js @@ -115,6 +115,12 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { } // "Starting..." page this._hideLoadingPage(); + + // since all the resources (templates, users, orgs...) were already loaded, notifications can be built + osparc.data.Resources.get("notifications") + .then(notifications => { + osparc.notification.Notifications.getInstance().addNotifications(notifications); + }); }) .catch(err => { console.error(err); diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js b/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js index e39b9c72357..93f5f50c74d 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js @@ -26,31 +26,29 @@ qx.Class.define("osparc.desktop.MainPageDesktop", { this._add(osparc.notification.RibbonNotifications.getInstance()); const navBar = new osparc.navigation.NavigationBar(); - navBar.populateLayout() - .then(() => { - // exclude some items from the navigation bar - navBar.getChildControl("dashboard-label").exclude(); - navBar.getChildControl("dashboard-button").exclude(); - navBar.getChildControl("notifications-button").exclude(); - navBar.getChildControl("help").exclude(); - - // exclude all the menu entries except "log-out" from user menu - const userMenuButton = navBar.getChildControl("user-menu"); - const userMenu = userMenuButton.getMenu(); - // eslint-disable-next-line no-underscore-dangle - const userMenuEntries = userMenu._getCreatedChildControls(); - Object.entries(userMenuEntries).forEach(([id, userMenuEntry]) => { - if (!["mini-profile-view", "po-center", "log-out"].includes(id)) { - userMenuEntry.exclude(); - } - }); - // exclude also the separators - userMenu.getChildren().forEach(child => { - if (child.classname === "qx.ui.menu.Separator") { - child.exclude(); - } - }); - }); + navBar.populateLayout(); + // exclude some items from the navigation bar + navBar.getChildControl("dashboard-label").exclude(); + navBar.getChildControl("dashboard-button").exclude(); + navBar.getChildControl("notifications-button").exclude(); + navBar.getChildControl("help").exclude(); + + // exclude all the menu entries except "log-out" from user menu + const userMenuButton = navBar.getChildControl("user-menu"); + const userMenu = userMenuButton.getMenu(); + // eslint-disable-next-line no-underscore-dangle + const userMenuEntries = userMenu._getCreatedChildControls(); + Object.entries(userMenuEntries).forEach(([id, userMenuEntry]) => { + if (!["mini-profile-view", "po-center", "log-out"].includes(id)) { + userMenuEntry.exclude(); + } + }); + // exclude also the separators + userMenu.getChildren().forEach(child => { + if (child.classname === "qx.ui.menu.Separator") { + child.exclude(); + } + }); this._add(navBar); osparc.MaintenanceTracker.getInstance().startTracker(); diff --git a/services/static-webserver/client/source/class/osparc/info/CommentUI.js b/services/static-webserver/client/source/class/osparc/info/CommentUI.js index 47a005cdb12..62871860da6 100644 --- a/services/static-webserver/client/source/class/osparc/info/CommentUI.js +++ b/services/static-webserver/client/source/class/osparc/info/CommentUI.js @@ -117,14 +117,12 @@ qx.Class.define("osparc.info.CommentUI", { const commentContent = this.getChildControl("comment-content"); commentContent.setValue(this.__comment["contents"]); - osparc.store.Store.getInstance().getUser(this.__comment["user_id"]) - .then(user => { - if (user) { - const userSource = osparc.utils.Avatar.getUrl(user["login"], 32); - thumbnail.setSource(userSource); - userName.setValue(user["label"]); - } - }); + const user = osparc.store.Store.getInstance().getUser(this.__comment["user_id"]) + if (user) { + const userSource = osparc.utils.Avatar.getUrl(user["login"], 32); + thumbnail.setSource(userSource); + userName.setValue(user["label"]); + } } } }); diff --git a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js index d3b00b12978..9397aed32e8 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js +++ b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js @@ -92,16 +92,8 @@ qx.Class.define("osparc.navigation.NavigationBar", { __tabButtons: null, populateLayout: function() { - return new Promise(resolve => { - osparc.data.Resources.get("notifications") - .then(notifications => { - osparc.notification.Notifications.getInstance().addNotifications(notifications); - this.__buildLayout(); - osparc.WindowSizeTracker.getInstance().addListener("changeCompactVersion", () => this.__navBarResized(), this); - resolve(); - }) - .catch(err => console.error(err)); - }); + this.__buildLayout(); + osparc.WindowSizeTracker.getInstance().addListener("changeCompactVersion", () => this.__navBarResized(), this); }, __buildLayout: function() { diff --git a/services/static-webserver/client/source/class/osparc/notification/Notification.js b/services/static-webserver/client/source/class/osparc/notification/Notification.js index 37f102e466f..af219894c0f 100644 --- a/services/static-webserver/client/source/class/osparc/notification/Notification.js +++ b/services/static-webserver/client/source/class/osparc/notification/Notification.js @@ -23,10 +23,12 @@ qx.Class.define("osparc.notification.Notification", { this.set({ id: notificationObj.id, + resourceId: notificationObj.resource_id ? notificationObj.resource_id : null, category: notificationObj.category, actionablePath: notificationObj.actionable_path, title: notificationObj.title, text: notificationObj.text, + userFromId: notificationObj.user_from_id ? notificationObj.user_from_id : null, date: new Date(notificationObj.date), read: ["true", "True", true].includes(notificationObj.read) }); @@ -40,6 +42,13 @@ qx.Class.define("osparc.notification.Notification", { event: "changeId" }, + resourceId: { + check: "String", + init: null, + nullable: true, + event: "changeResourceId" + }, + category: { check: [ "NEW_ORGANIZATION", @@ -74,6 +83,13 @@ qx.Class.define("osparc.notification.Notification", { event: "changeText" }, + userFromId: { + check: "Number", + init: null, + nullable: true, + event: "changeUserFromId" + }, + date: { check: "Date", init: null, diff --git a/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js b/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js index 6ed2ee9356a..b2b002a4f80 100644 --- a/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js +++ b/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js @@ -112,37 +112,102 @@ qx.Class.define("osparc.notification.NotificationUI", { return control || this.base(arguments, id); }, - __applyNotification: function(notification) { - const icon = this.getChildControl("icon"); - notification.bind("category", icon, "source", { - converter: value => { - let source = ""; - switch (value) { - case "NEW_ORGANIZATION": - source = "@FontAwesome5Solid/users/14"; - break; - case "STUDY_SHARED": - source = "@FontAwesome5Solid/file/14"; - break; - case "TEMPLATE_SHARED": - source = "@FontAwesome5Solid/copy/14"; - break; - case "ANNOTATION_NOTE": - source = "@FontAwesome5Solid/file/14"; - break; - case "WALLET_SHARED": - source = "@MaterialIcons/account_balance_wallet/14"; - break; + __applyNotification: async function(notification) { + console.log("notification", notification); + let resourceId = null; + if (notification.getResourceId()) { + resourceId = notification.getResourceId(); + } else if (notification.getActionablePath()) { + // extract it from the actionable path + const actionablePath = notification.getActionablePath(); + resourceId = actionablePath.split("/")[1]; + } + const userFromId = notification.getUserFromId(); + + let source = ""; + let title = ""; + let description = ""; + switch (notification.getCategory()) { + case "NEW_ORGANIZATION": + source = "@FontAwesome5Solid/users/14"; + if (resourceId) { + const group = await osparc.store.Store.getInstance().getGroup(resourceId); + description = "You're now member of '" + group["name"] + "'"; } - return source; - } - }); + break; + case "STUDY_SHARED": + source = "@FontAwesome5Solid/file/14"; + if (resourceId) { + const params = { + url: { + "studyId": resourceId + } + }; + const study = await osparc.data.Resources.getOne("studies", params); + const studyAlias = osparc.product.Utils.getStudyAlias({ + firstUpperCase: true + }); + if (study) { + title = `${studyAlias} '${study["name"]}'`; + } + } + if (userFromId) { + const user = osparc.store.Store.getInstance().getUser(userFromId); + if (user) { + description = "was shared by " + user["label"]; + } + } + break; + case "TEMPLATE_SHARED": + source = "@FontAwesome5Solid/copy/14"; + if (resourceId) { + const template = osparc.store.Store.getInstance().getTemplate(resourceId); + const templateAlias = osparc.product.Utils.getTemplateAlias({ + firstUpperCase: true + }); + if (template) { + title = `${templateAlias} '${template["name"]}'`; + } + } + if (userFromId) { + const user = osparc.store.Store.getInstance().getUser(userFromId); + if (user) { + description = "was shared by " + user["label"]; + } + } + break; + case "ANNOTATION_NOTE": + source = "@FontAwesome5Solid/file/14"; + if (resourceId) { + const params = { + url: { + "studyId": resourceId + } + }; + const study = await osparc.data.Resources.getOne("studies", params); + if (study) { + title = `Note added in '${study["name"]}'`; + } + } + if (userFromId) { + const user = osparc.store.Store.getInstance().getUser(userFromId); + if (user) { + description = "was added by " + user["label"]; + } + } + break; + case "WALLET_SHARED": + source = "@MaterialIcons/account_balance_wallet/14"; + break; + } + const icon = this.getChildControl("icon"); + icon.setSource(source); - const title = this.getChildControl("title"); - notification.bind("title", title, "value"); + const titleLabel = this.getChildControl("title"); + titleLabel.setValue(title ? title : notification.getTitle()); - const text = this.getChildControl("text"); - notification.bind("text", text, "value"); + const descriptionLabel = this.getChildControl("text"); + descriptionLabel.setValue(description ? description : notification.getText()); const date = this.getChildControl("date"); notification.bind("date", date, "value", { @@ -166,23 +231,11 @@ qx.Class.define("osparc.notification.NotificationUI", { } this.fireEvent("notificationTapped"); + osparc.notification.Notifications.markAsRead(notification); + this.__openActionablePath(notification); + }, - if (notification.isRead() === false) { - // set as read - const params = { - url: { - notificationId: notification.getId() - }, - data: { - "read": true - } - }; - osparc.data.Resources.fetch("notifications", "patch", params) - .then(() => notification.setRead(true)) - .catch(() => notification.setRead(false)); - } - - // open actionable path + __openActionablePath: function(notification) { const actionablePath = notification.getActionablePath(); const items = actionablePath.split("/"); const resourceId = items.pop(); diff --git a/services/static-webserver/client/source/class/osparc/notification/Notifications.js b/services/static-webserver/client/source/class/osparc/notification/Notifications.js index 2f7ab34c146..a34a0e2886c 100644 --- a/services/static-webserver/client/source/class/osparc/notification/Notifications.js +++ b/services/static-webserver/client/source/class/osparc/notification/Notifications.js @@ -28,8 +28,9 @@ qx.Class.define("osparc.notification.Notifications", { __newNotificationBase: function(userId) { return { "user_id": userId.toString(), + "user_from_id": osparc.auth.Data.getInstance().getUserId(), "date": new Date().toISOString(), - "product": osparc.product.Utils.getProductName() + "product": osparc.product.Utils.getProductName(), }; }, @@ -38,6 +39,7 @@ qx.Class.define("osparc.notification.Notifications", { const specNotification = { "category": "NEW_ORGANIZATION", "actionable_path": "organization/"+orgId, + "resource_id": orgId, "title": "New organization", "text": "You're now member of a new Organization" }; @@ -55,6 +57,7 @@ qx.Class.define("osparc.notification.Notifications", { const specNotification = { "category": "STUDY_SHARED", "actionable_path": "study/"+studyId, + "resource_id": studyId, "title": `${study} shared`, "text": `A ${study} was shared with you` }; @@ -72,6 +75,7 @@ qx.Class.define("osparc.notification.Notifications", { const specNotification = { "category": "TEMPLATE_SHARED", "actionable_path": "template/"+templateId, + "resource_id": templateId, "title": `${template} shared`, "text": `A ${template} was shared with you` }; @@ -86,6 +90,7 @@ qx.Class.define("osparc.notification.Notifications", { const specNotification = { "category": "ANNOTATION_NOTE", "actionable_path": "study/"+studyId, + "resource_id": studyId, "title": "Note added", "text": "A Note was added for you" }; @@ -100,6 +105,7 @@ qx.Class.define("osparc.notification.Notifications", { const specNotification = { "category": "WALLET_SHARED", "actionable_path": "wallet/"+walletId, + "resource_id": walletId, "title": "Credits shared", "text": "A Credit Account was shared with you" }; @@ -142,7 +148,24 @@ qx.Class.define("osparc.notification.Notifications", { data: this.__newWalletObj(userId, studyId) }; return osparc.data.Resources.fetch("notifications", "post", params); - } + }, + + markAsRead: function(notification) { + if (notification.isRead() === false) { + // set as read + const params = { + url: { + notificationId: notification.getId() + }, + data: { + "read": true + } + }; + osparc.data.Resources.fetch("notifications", "patch", params) + .then(() => notification.setRead(true)) + .catch(() => notification.setRead(false)); + } + }, }, members: { @@ -165,6 +188,14 @@ qx.Class.define("osparc.notification.Notifications", { getNotifications: function() { return this.__notifications; - } + }, + + markAllAsRead: function() { + this.__notifications.forEach(notification => { + if (notification.isRead() === false) { + osparc.notification.Notifications.markAsRead(notification); + } + }); + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js b/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js index 0b58a2b4e3f..dd8d4b543d4 100644 --- a/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js +++ b/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js @@ -82,7 +82,8 @@ qx.Class.define("osparc.notification.NotificationsButton", { }, __updateButton: function() { - const notifications = osparc.notification.Notifications.getInstance().getNotifications(); + const notificationManager = osparc.notification.Notifications.getInstance(); + const notifications = notificationManager.getNotifications(); notifications.forEach(notification => notification.addListener("changeRead", () => this.__updateButton(), this)); const nUnreadNotifications = notifications.filter(notification => notification.getRead() === false).length; @@ -117,6 +118,10 @@ qx.Class.define("osparc.notification.NotificationsButton", { // Show the container this.__notificationsContainer.show(); + // mark all notifications as read + const notificationManager = osparc.notification.Notifications.getInstance(); + notificationManager.markAllAsRead(); + // Add listener for taps outside the container to hide it document.addEventListener("mousedown", this.__onTapOutside.bind(this), true); }, diff --git a/services/static-webserver/client/source/class/osparc/store/Store.js b/services/static-webserver/client/source/class/osparc/store/Store.js index 3a180897e28..0e015ed7811 100644 --- a/services/static-webserver/client/source/class/osparc/store/Store.js +++ b/services/static-webserver/client/source/class/osparc/store/Store.js @@ -427,6 +427,11 @@ qx.Class.define("osparc.store.Store", { } }, + getTemplate: function(templateId) { + const templates = this.getTemplates(); + return templates.find(template => template["uuid"] === templateId); + }, + deleteStudy: function(studyId) { const params = { url: { @@ -626,17 +631,11 @@ qx.Class.define("osparc.store.Store", { }, getUser: function(uid) { - return new Promise(resolve => { - if (uid) { - this.getReachableMembers() - .then(visibleMembers => { - resolve(Object.values(visibleMembers).find(member => member.id === uid)); - }) - .catch(() => resolve(null)); - } else { - resolve(null); - } - }); + if (uid) { + const visibleMembers = this.getReachableMembers(); + return Object.values(visibleMembers).find(member => member.id === uid); + } + return null; }, reloadCreditPrice: function() { diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 82a18aebb3b..a49c71acf17 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -12589,6 +12589,22 @@ components: read: title: Read type: boolean + resource_id: + title: Resource ID + anyOf: + - enum: + - "" + type: string + - type: string + default: "" + user_from_id: + title: User ID of the one creating it + anyOf: + - enum: + - None + type: integer + - type: integer + default: None UserNotificationCreate: title: UserNotificationCreate required: @@ -12628,6 +12644,22 @@ components: type: string - type: string default: UNDEFINED + resource_id: + title: Resource ID + anyOf: + - enum: + - "" + type: string + - type: string + default: "" + user_from_id: + title: User ID of the one creating it + anyOf: + - enum: + - None + type: integer + - type: integer + default: None UserNotificationPatch: title: UserNotificationPatch required: diff --git a/services/web/server/src/simcore_service_webserver/users/_notifications.py b/services/web/server/src/simcore_service_webserver/users/_notifications.py index 256e521f89c..885371f7f65 100644 --- a/services/web/server/src/simcore_service_webserver/users/_notifications.py +++ b/services/web/server/src/simcore_service_webserver/users/_notifications.py @@ -32,6 +32,8 @@ class BaseUserNotification(BaseModel): text: str date: datetime product: Literal["UNDEFINED"] | ProductName = "UNDEFINED" + resource_id: Literal[""] | str = "" + user_from_id: Literal[None] | UserID = None @validator("category", pre=True) @classmethod @@ -106,6 +108,8 @@ class Config: "date": "2023-02-23T16:28:13.122Z", "product": "s4l", "read": False, + "resource_id": "3fb96d89-ff5d-4d27-b5aa-d20d46e20e12", + "user_from_id": "2", }, { "id": "390053c9-3931-40e1-839f-585268f6fd3e", @@ -117,6 +121,8 @@ class Config: "date": "2023-09-29T16:28:13.122Z", "product": "tis", "read": False, + "resource_id": "3fb96d89-ff5d-4d27-b5aa-d20d46e20e13", + "user_from_id": "2", }, ] } diff --git a/services/web/server/tests/unit/isolated/test_user_notifications.py b/services/web/server/tests/unit/isolated/test_user_notifications.py index d606a84297f..7faf71f0aaf 100644 --- a/services/web/server/tests/unit/isolated/test_user_notifications.py +++ b/services/web/server/tests/unit/isolated/test_user_notifications.py @@ -80,6 +80,8 @@ def test_get_notification_key(user_id: UserID): "text": "You're now member of a new Organization", "date": "2023-02-23T16:23:13.122Z", "product": "s4l", + "resource_id": "other_id", + "user_from_id": "2", } ), id="category_from_string", @@ -95,6 +97,8 @@ def test_get_notification_key(user_id: UserID): "text": "You're now member of a new Organization", "date": "2023-02-23T16:23:13.122Z", "product": "tis", + "resource_id": "other_id", + "user_from_id": "2", } ), id="category_from_lower_case_string", diff --git a/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py b/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py index 77aaccade51..0aef84ee328 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users__notifications.py @@ -196,6 +196,8 @@ async def test_list_user_notifications( "date": "2023-02-23T16:23:13.122Z", "product": "osparc", "read": True, + "resource_id": "3fb96d89-ff5d-4d27-b5aa-d20d46e20e12", + "user_from_id": "2", }, id="with_extra_params_that_will_get_overwritten", ), @@ -265,6 +267,8 @@ async def test_create_user_notification_capped_list_length( "text": "You're now member of a new Organization", "date": "2023-02-23T16:23:13.122Z", "product": "osparc", + "resource_id": "3fb96d89-ff5d-4d27-b5aa-d20d46e20e12", + "user_from_id": "2", }, ) for _ in range(notification_count)