From c501b3a205b50c1cab5d687fa99405cbf0cd2bff Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 11:08:36 +0200 Subject: [PATCH 01/26] simpler --- .../client/source/class/osparc/conversation/MessageUI.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js index fdddc051d6b..399dfad55d7 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js @@ -101,10 +101,7 @@ qx.Class.define("osparc.conversation.MessageUI", { break; case "message-content": control = new osparc.ui.markdown.Markdown().set({ - decorator: "rounded", noMargin: true, - paddingLeft: 8, - paddingRight: 8, allowGrowX: true, }); control.getContentElement().setStyles({ From 0743bd81ff619d95a434fff17744b3cc361c7ec0 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 25 Jun 2025 11:15:37 +0200 Subject: [PATCH 02/26] feat: add conversation events --- .../_conversation_message_service.py | 14 +---- .../conversations/_conversation_service.py | 24 ++++++- .../conversations/_socketio.py | 62 +++++++++++++++++-- 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/conversations/_conversation_message_service.py b/services/web/server/src/simcore_service_webserver/conversations/_conversation_message_service.py index caeec8b030c..5b4f8397648 100644 --- a/services/web/server/src/simcore_service_webserver/conversations/_conversation_message_service.py +++ b/services/web/server/src/simcore_service_webserver/conversations/_conversation_message_service.py @@ -16,12 +16,10 @@ from models_library.rest_pagination import PageTotalCount from models_library.users import UserID -from ..projects._groups_repository import list_project_groups -from ..users._users_service import get_users_in_group - # Import or define SocketMessageDict from ..users.api import get_user_primary_group_id from . import _conversation_message_repository +from ._conversation_service import _get_recipients from ._socketio import ( notify_conversation_message_created, notify_conversation_message_deleted, @@ -31,16 +29,6 @@ _logger = logging.getLogger(__name__) -async def _get_recipients(app: web.Application, project_id: ProjectID) -> set[UserID]: - groups = await list_project_groups(app, project_id=project_id) - return { - user - for group in groups - if group.read - for user in await get_users_in_group(app, gid=group.gid) - } - - async def create_message( app: web.Application, *, diff --git a/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py b/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py index e4541f56c3f..1e342d1881e 100644 --- a/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py +++ b/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py @@ -16,12 +16,25 @@ from models_library.rest_pagination import PageTotalCount from models_library.users import UserID +from ..conversations._socketio import notify_conversation_created +from ..projects._groups_repository import list_project_groups +from ..users._users_service import get_users_in_group from ..users.api import get_user_primary_group_id from . import _conversation_repository _logger = logging.getLogger(__name__) +async def _get_recipients(app: web.Application, project_id: ProjectID) -> set[UserID]: + groups = await list_project_groups(app, project_id=project_id) + return { + user + for group in groups + if group.read + for user in await get_users_in_group(app, gid=group.gid) + } + + async def create_conversation( app: web.Application, *, @@ -37,7 +50,7 @@ async def create_conversation( _user_group_id = await get_user_primary_group_id(app, user_id=user_id) - return await _conversation_repository.create( + created_conversation = await _conversation_repository.create( app, name=name, project_uuid=project_uuid, @@ -46,6 +59,15 @@ async def create_conversation( product_name=product_name, ) + await notify_conversation_created( + app, + recipients=await _get_recipients(app, project_uuid), + project_id=project_uuid, + conversation=created_conversation, + ) + + return created_conversation + async def get_conversation( app: web.Application, diff --git a/services/web/server/src/simcore_service_webserver/conversations/_socketio.py b/services/web/server/src/simcore_service_webserver/conversations/_socketio.py index 03761ca4961..021fb792e39 100644 --- a/services/web/server/src/simcore_service_webserver/conversations/_socketio.py +++ b/services/web/server/src/simcore_service_webserver/conversations/_socketio.py @@ -3,12 +3,15 @@ from aiohttp import web from models_library.conversations import ( + ConversationGetDB, ConversationID, ConversationMessageGetDB, ConversationMessageID, ConversationMessageType, + ConversationType, ) from models_library.groups import GroupID +from models_library.products import ProductName from models_library.projects import ProjectID from models_library.socketio import SocketMessageDict from models_library.users import UserID @@ -20,6 +23,10 @@ _MAX_CONCURRENT_SENDS: Final[int] = 3 +SOCKET_IO_CONVERSATION_CREATED_EVENT: Final[str] = "conversation:created" +SOCKET_IO_CONVERSATION_DELETED_EVENT: Final[str] = "conversation:deleted" +SOCKET_IO_CONVERSATION_UPDATED_EVENT: Final[str] = "conversation:updated" + SOCKET_IO_CONVERSATION_MESSAGE_CREATED_EVENT: Final[str] = ( "conversation:message:created" ) @@ -31,7 +38,30 @@ ) -class BaseConversationMessage(BaseModel): +class BaseEvent(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), + ) + + +class BaseConversationEvent(BaseEvent): + product_name: ProductName + project_id: ProjectID | None + user_group_id: GroupID + conversation_id: ConversationID + type: ConversationType + + +class ConversationCreatedOrUpdatedEvent(BaseConversationEvent): + created: datetime.datetime + modified: datetime.datetime + + +class BaseConversationMessageEvent(BaseEvent): conversation_id: ConversationID message_id: ConversationMessageID user_group_id: GroupID @@ -46,13 +76,13 @@ class BaseConversationMessage(BaseModel): ) -class ConversationMessageCreatedOrUpdated(BaseConversationMessage): +class ConversationMessageCreatedOrUpdatedEvent(BaseConversationMessageEvent): content: str created: datetime.datetime modified: datetime.datetime -class ConversationMessageDeleted(BaseConversationMessage): ... +class ConversationMessageDeletedEvent(BaseConversationMessageEvent): ... async def _send_message_to_recipients( @@ -72,6 +102,26 @@ async def _send_message_to_recipients( ... +async def notify_conversation_created( + app: web.Application, + *, + recipients: set[UserID], + project_id: ProjectID, + conversation: ConversationGetDB, +) -> None: + notification_message = SocketMessageDict( + event_type=SOCKET_IO_CONVERSATION_CREATED_EVENT, + data={ + "projectId": project_id, + **ConversationCreatedOrUpdatedEvent(**conversation.model_dump()).model_dump( + mode="json", by_alias=True + ), + }, + ) + + await _send_message_to_recipients(app, recipients, notification_message) + + async def notify_conversation_message_created( app: web.Application, *, @@ -83,7 +133,7 @@ async def notify_conversation_message_created( event_type=SOCKET_IO_CONVERSATION_MESSAGE_CREATED_EVENT, data={ "projectId": project_id, - **ConversationMessageCreatedOrUpdated( + **ConversationMessageCreatedOrUpdatedEvent( **conversation_message.model_dump() ).model_dump(mode="json", by_alias=True), }, @@ -104,7 +154,7 @@ async def notify_conversation_message_updated( event_type=SOCKET_IO_CONVERSATION_MESSAGE_UPDATED_EVENT, data={ "projectId": project_id, - **ConversationMessageCreatedOrUpdated( + **ConversationMessageCreatedOrUpdatedEvent( **conversation_message.model_dump() ).model_dump(mode="json", by_alias=True), }, @@ -127,7 +177,7 @@ async def notify_conversation_message_deleted( event_type=SOCKET_IO_CONVERSATION_MESSAGE_DELETED_EVENT, data={ "projectId": project_id, - **ConversationMessageDeleted( + **ConversationMessageDeletedEvent( conversation_id=conversation_id, message_id=message_id, user_group_id=user_group_id, From 8de8b2ced18c1dfd20ea610498b2ce8fdcbe0715 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:01:26 +0200 Subject: [PATCH 03/26] minor --- .../source/class/osparc/conversation/Conversation.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js index 9bb2cab5b76..bc00386db8c 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js @@ -90,9 +90,7 @@ qx.Class.define("osparc.conversation.Conversation", { const newLabel = e.getData()["newLabel"]; if (this.getConversationId()) { osparc.study.Conversations.renameConversation(this.__studyData["uuid"], this.getConversationId(), newLabel) - .then(() => { - this.getChildControl("button").setLabel(newLabel); - }); + .then(() => this.renameConversation(newLabel)); } else { // create new conversation first osparc.study.Conversations.addConversation(this.__studyData["uuid"], newLabel) @@ -147,6 +145,10 @@ qx.Class.define("osparc.conversation.Conversation", { }); }, + renameConversation: function(newName) { + this.getChildControl("button").setLabel(newName); + }, + __buildLayout: function() { this.__messagesTitle = new qx.ui.basic.Label(); this._add(this.__messagesTitle); From f34e5083c6392425f56dbb3aa4c08206088c4f6d Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:01:47 +0200 Subject: [PATCH 04/26] event handlers --- .../class/osparc/study/Conversations.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 7d8a5800796..7b2b99588a1 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -167,6 +167,31 @@ qx.Class.define("osparc.study.Conversations", { this.__wsHandlers = []; const socket = osparc.wrapper.WebSocket.getInstance(); + + [ + "conversation:created", + "conversation:updated", + "conversation:deleted", + ].forEach(eventName => { + const eventHandler = conversation => { + if (conversation) { + switch (eventName) { + case "conversation:created": + conversation.addMessage(conversation); + break; + case "conversation:updated": + conversation.updateMessage(conversation); + break; + case "conversation:deleted": + conversation.deleteMessage(conversation); + break; + } + } + }; + socket.on(eventName, eventHandler, this); + this.__wsHandlers.push({ eventName, handler: eventHandler }); + }); + [ "conversation:message:created", "conversation:message:updated", From bfbbfea2877407e8fae2656743261ae86496b01f Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:28:46 +0200 Subject: [PATCH 05/26] minor --- .../source/class/osparc/conversation/Conversation.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js index bc00386db8c..8ed290b1e09 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js @@ -109,11 +109,11 @@ qx.Class.define("osparc.conversation.Conversation", { column: 3 }); - const trashButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/times/12").set({ + const closeButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/times/12").set({ ...buttonsAesthetics, paddingLeft: 4, // adds spacing between buttons }); - trashButton.addListener("execute", () => { + closeButton.addListener("execute", () => { const deleteConversation = () => { osparc.study.Conversations.deleteConversation(this.__studyData["uuid"], this.getConversationId()) .then(() => this.fireEvent("conversationDeleted")); @@ -136,11 +136,11 @@ qx.Class.define("osparc.conversation.Conversation", { } }); // eslint-disable-next-line no-underscore-dangle - tabButton._add(trashButton, { + tabButton._add(closeButton, { row: 0, column: 4 }); - this.bind("conversationId", trashButton, "visibility", { + this.bind("conversationId", closeButton, "visibility", { converter: value => value ? "visible" : "excluded" }); }, From c6049298467e4fe0622dbde1011cfcce5b2bc160 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:31:40 +0200 Subject: [PATCH 06/26] prop --- .../class/osparc/study/Conversations.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 7b2b99588a1..924a2c7aa88 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -20,7 +20,7 @@ qx.Class.define("osparc.study.Conversations", { extend: qx.ui.core.Widget, /** - * @param studyData {String} Study Data + * @param studyData {Object} Study Data */ construct: function(studyData) { this.base(arguments); @@ -29,11 +29,22 @@ qx.Class.define("osparc.study.Conversations", { this.__conversations = []; - this.fetchConversations(studyData); + this.set({ + studyData, + }); this.__listenToConversationWS(); }, + properties: { + studyData: { + check: "Object", + init: null, + nullable: false, + apply: "__applyStudyData", + }, + }, + statics: { popUpInWindow: function(studyData) { const conversations = new osparc.study.Conversations(studyData); @@ -177,13 +188,13 @@ qx.Class.define("osparc.study.Conversations", { if (conversation) { switch (eventName) { case "conversation:created": - conversation.addMessage(conversation); + this.__addConversation(conversation); break; case "conversation:updated": - conversation.updateMessage(conversation); + this.__updateConversation(conversation); break; case "conversation:deleted": - conversation.deleteMessage(conversation); + this.__deleteConversation(conversation); break; } } @@ -225,7 +236,7 @@ qx.Class.define("osparc.study.Conversations", { return this.__conversations.find(conversation => conversation.getConversationId() === conversationId); }, - fetchConversations: function(studyData) { + __applyStudyData: function(studyData) { const loadMoreButton = this.getChildControl("loading-button"); loadMoreButton.setFetching(true); From 4cc9fab48de2a311085682a6efc9c252ca6b9766 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 25 Jun 2025 13:35:02 +0200 Subject: [PATCH 07/26] feat: add events --- .../src/models_library/conversations.py | 12 ++-- .../conversations/_conversation_service.py | 32 +++++++++- .../conversations/_socketio.py | 62 ++++++++++++++++--- .../projects/_conversations_service.py | 10 ++- 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/packages/models-library/src/models_library/conversations.py b/packages/models-library/src/models_library/conversations.py index 5d33a0fcd45..e8e22ebd559 100644 --- a/packages/models-library/src/models_library/conversations.py +++ b/packages/models-library/src/models_library/conversations.py @@ -1,16 +1,20 @@ from datetime import datetime from enum import auto -from typing import TypeAlias +from typing import Annotated, TypeAlias from uuid import UUID from models_library.groups import GroupID from models_library.projects import ProjectID -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, StringConstraints from .products import ProductName from .utils.enums import StrAutoEnum ConversationID: TypeAlias = UUID +ConversationName: TypeAlias = Annotated[ + str, StringConstraints(strip_whitespace=True, min_length=1, max_length=255) +] + ConversationMessageID: TypeAlias = UUID @@ -36,7 +40,7 @@ class ConversationMessageType(StrAutoEnum): class ConversationGetDB(BaseModel): conversation_id: ConversationID product_name: ProductName - name: str + name: ConversationName project_uuid: ProjectID | None user_group_id: GroupID type: ConversationType @@ -63,7 +67,7 @@ class ConversationMessageGetDB(BaseModel): class ConversationPatchDB(BaseModel): - name: str | None = None + name: ConversationName | None = None class ConversationMessagePatchDB(BaseModel): diff --git a/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py b/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py index 1e342d1881e..a14b81b800a 100644 --- a/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py +++ b/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py @@ -16,7 +16,11 @@ from models_library.rest_pagination import PageTotalCount from models_library.users import UserID -from ..conversations._socketio import notify_conversation_created +from ..conversations._socketio import ( + notify_conversation_created, + notify_conversation_deleted, + notify_conversation_updated, +) from ..projects._groups_repository import list_project_groups from ..users._users_service import get_users_in_group from ..users.api import get_user_primary_group_id @@ -83,20 +87,33 @@ async def get_conversation( async def update_conversation( app: web.Application, *, + project_id: ProjectID, conversation_id: ConversationID, # Update attributes updates: ConversationPatchDB, ) -> ConversationGetDB: - return await _conversation_repository.update( + updated_conversation = await _conversation_repository.update( app, conversation_id=conversation_id, updates=updates, ) + await notify_conversation_updated( + app, + recipients=await _get_recipients(app, project_id), + project_id=project_id, + conversation=updated_conversation, + ) + + return updated_conversation + async def delete_conversation( app: web.Application, *, + product_name: ProductName, + project_id: ProjectID, + user_id: UserID, conversation_id: ConversationID, ) -> None: await _conversation_repository.delete( @@ -104,6 +121,17 @@ async def delete_conversation( conversation_id=conversation_id, ) + _user_group_id = await get_user_primary_group_id(app, user_id=user_id) + + await notify_conversation_deleted( + app, + recipients=await _get_recipients(app, project_id), + product_name=product_name, + user_group_id=_user_group_id, + project_id=project_id, + conversation_id=conversation_id, + ) + async def list_conversations_for_project( app: web.Application, diff --git a/services/web/server/src/simcore_service_webserver/conversations/_socketio.py b/services/web/server/src/simcore_service_webserver/conversations/_socketio.py index 021fb792e39..2d095243aad 100644 --- a/services/web/server/src/simcore_service_webserver/conversations/_socketio.py +++ b/services/web/server/src/simcore_service_webserver/conversations/_socketio.py @@ -8,6 +8,7 @@ ConversationMessageGetDB, ConversationMessageID, ConversationMessageType, + ConversationName, ConversationType, ) from models_library.groups import GroupID @@ -57,10 +58,14 @@ class BaseConversationEvent(BaseEvent): class ConversationCreatedOrUpdatedEvent(BaseConversationEvent): + name: ConversationName created: datetime.datetime modified: datetime.datetime +class ConversationDeletedEvent(BaseConversationEvent): ... + + class BaseConversationMessageEvent(BaseEvent): conversation_id: ConversationID message_id: ConversationMessageID @@ -92,9 +97,7 @@ async def _send_message_to_recipients( ): async for _ in limited_as_completed( ( - send_message_to_user( - app, recipient, notification_message, ignore_queue=True - ) + send_message_to_user(app, recipient, notification_message) for recipient in recipients ), limit=_MAX_CONCURRENT_SENDS, @@ -112,10 +115,55 @@ async def notify_conversation_created( notification_message = SocketMessageDict( event_type=SOCKET_IO_CONVERSATION_CREATED_EVENT, data={ - "projectId": project_id, - **ConversationCreatedOrUpdatedEvent(**conversation.model_dump()).model_dump( - mode="json", by_alias=True - ), + **ConversationCreatedOrUpdatedEvent( + project_id=project_id, + **conversation.model_dump(), + ).model_dump(mode="json", by_alias=True), + }, + ) + + await _send_message_to_recipients(app, recipients, notification_message) + + +async def notify_conversation_updated( + app: web.Application, + *, + recipients: set[UserID], + project_id: ProjectID, + conversation: ConversationGetDB, +) -> None: + notification_message = SocketMessageDict( + event_type=SOCKET_IO_CONVERSATION_UPDATED_EVENT, + data={ + **ConversationCreatedOrUpdatedEvent( + project_id=project_id, + **conversation.model_dump(), + ).model_dump(mode="json", by_alias=True), + }, + ) + + await _send_message_to_recipients(app, recipients, notification_message) + + +async def notify_conversation_deleted( + app: web.Application, + *, + recipients: set[UserID], + product_name: ProductName, + project_id: ProjectID, + user_group_id: GroupID, + conversation_id: ConversationID, +) -> None: + notification_message = SocketMessageDict( + event_type=SOCKET_IO_CONVERSATION_DELETED_EVENT, + data={ + **ConversationDeletedEvent( + product_name=product_name, + project_id=project_id, + conversation_id=conversation_id, + user_group_id=user_group_id, + type=ConversationType.PROJECT_STATIC, + ).model_dump(mode="json", by_alias=True), }, ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_conversations_service.py b/services/web/server/src/simcore_service_webserver/projects/_conversations_service.py index b415f694bcf..48aeae02cc6 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_conversations_service.py +++ b/services/web/server/src/simcore_service_webserver/projects/_conversations_service.py @@ -8,6 +8,7 @@ ConversationMessageID, ConversationMessagePatchDB, ConversationMessageType, + ConversationName, ConversationPatchDB, ConversationType, ) @@ -87,7 +88,7 @@ async def update_project_conversation( project_uuid: ProjectID, conversation_id: ConversationID, # attributes - name: str, + name: ConversationName, ) -> ConversationGetDB: await check_user_project_permission( app, @@ -98,6 +99,7 @@ async def update_project_conversation( ) return await conversations_service.update_conversation( app, + project_id=project_uuid, conversation_id=conversation_id, updates=ConversationPatchDB(name=name), ) @@ -119,7 +121,11 @@ async def delete_project_conversation( permission="read", ) await conversations_service.delete_conversation( - app, conversation_id=conversation_id + app, + product_name=product_name, + project_id=project_uuid, + user_id=user_id, + conversation_id=conversation_id, ) From eb1fabd472ca014e3ad0530541ed317db1b08e29 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:49:44 +0200 Subject: [PATCH 08/26] refactor --- .../class/osparc/study/Conversations.js | 98 +++++++++++-------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 924a2c7aa88..609eebb0705 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -154,6 +154,7 @@ qx.Class.define("osparc.study.Conversations", { members: { __conversations: null, + __conversationPages: null, __wsHandlers: null, _createChildControlImpl: function(id) { @@ -248,57 +249,76 @@ qx.Class.define("osparc.study.Conversations", { } }; osparc.data.Resources.fetch("conversations", "getConversationsPage", params) - .then(conversations => this.__addConversations(conversations, studyData)) + .then(conversations => { + this.__conversationPages = []; + if (conversations.length === 0) { + conversations.forEach(conversation => this.__addConversation(conversation)); + } else { + this.__addTemporaryConversation(); + } + }) .finally(() => { loadMoreButton.setFetching(false); loadMoreButton.exclude(); }); }, - __addConversations: function(conversations, studyData) { - const conversationPages = []; + __createConversation: function(conversationData) { + const studyData = this.getStudyData(); + let conversation = null; + if (conversationData) { + const conversationId = conversationData["conversationId"]; + conversation = new osparc.conversation.Conversation(studyData, conversationId); + conversation.setLabel(conversationData["name"]); + conversation.addListener("conversationDeleted", () => { + console.log("Conversation deleted"); + }); + } else { + // create a temporary conversation + conversation = new osparc.conversation.Conversation(studyData); + conversation.setLabel(this.tr("new")); + } + }, + + __addTemporaryConversation: function() { + const temporaryConversation = this.__createConversation(); + const conversationsLayout = this.getChildControl("conversations-layout"); + conversationsLayout.add(temporaryConversation); - const newConversationButton = new qx.ui.form.Button().set({ - icon: "@FontAwesome5Solid/plus/12", - toolTipText: this.tr("Add new conversation"), - allowGrowX: false, - backgroundColor: "transparent", - }); + this.__conversationPages.push(temporaryConversation); + }, - const reloadConversations = () => { - conversationPages.forEach(conversationPage => conversationPage.fireDataEvent("close", conversationPage)); - conversationsLayout.getChildControl("bar").remove(newConversationButton); - this.fetchConversations(studyData); - }; + __addConversation: function(conversationData) { + const conversation = this.__createConversation(conversationData); - this.__conversations = []; - if (conversations.length === 0) { - const noConversationTab = new osparc.conversation.Conversation(studyData); - conversationPages.push(noConversationTab); - noConversationTab.setLabel(this.tr("new")); - noConversationTab.addListener("conversationDeleted", () => reloadConversations()); - conversationsLayout.add(noConversationTab); - } else { - conversations.forEach(conversationData => { - const conversationId = conversationData["conversationId"]; - const conversation = new osparc.conversation.Conversation(studyData, conversationId); - this.__conversations.push(conversation); - conversationPages.push(conversation); - conversation.setLabel(conversationData["name"]); - conversation.addListener("conversationDeleted", () => reloadConversations()); - conversationsLayout.add(conversation); - }); - } + const conversationsLayout = this.getChildControl("conversations-layout"); + conversationsLayout.add(conversation); - newConversationButton.addListener("execute", () => { - osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (conversations.length + 1)) - .then(() => { - reloadConversations(); - }); - }); - conversationsLayout.getChildControl("bar").add(newConversationButton); + + this.__conversations.push(conversation); + this.__conversationPages.push(conversation); + + if (this.__newConversationButton === null) { + // initialize the new button only once + const newConversationButton = this.__newConversationButton = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/plus/12", + toolTipText: this.tr("Add new conversation"), + allowGrowX: false, + backgroundColor: "transparent", + }); + newConversationButton.addListener("execute", () => { + osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (conversations.length + 1)) + .then(conversation => { + this.__addConversation(conversation); + }); + }); + conversationsLayout.getChildControl("bar").add(newConversationButton); + } + // remove and add to move to last position + conversationsLayout.getChildControl("bar").remove(this.__newConversationButton); + conversationsLayout.getChildControl("bar").add(this.__newConversationButton); }, }, From 9934c4e21bb6a341e765a45f8d25e81c8bb86ee7 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:53:37 +0200 Subject: [PATCH 09/26] not needed --- .../client/source/class/osparc/study/Conversations.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 609eebb0705..c0f0b509db6 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -154,7 +154,6 @@ qx.Class.define("osparc.study.Conversations", { members: { __conversations: null, - __conversationPages: null, __wsHandlers: null, _createChildControlImpl: function(id) { @@ -250,7 +249,6 @@ qx.Class.define("osparc.study.Conversations", { }; osparc.data.Resources.fetch("conversations", "getConversationsPage", params) .then(conversations => { - this.__conversationPages = []; if (conversations.length === 0) { conversations.forEach(conversation => this.__addConversation(conversation)); } else { @@ -285,8 +283,6 @@ qx.Class.define("osparc.study.Conversations", { const conversationsLayout = this.getChildControl("conversations-layout"); conversationsLayout.add(temporaryConversation); - - this.__conversationPages.push(temporaryConversation); }, __addConversation: function(conversationData) { @@ -295,10 +291,7 @@ qx.Class.define("osparc.study.Conversations", { const conversationsLayout = this.getChildControl("conversations-layout"); conversationsLayout.add(conversation); - - this.__conversations.push(conversation); - this.__conversationPages.push(conversation); if (this.__newConversationButton === null) { // initialize the new button only once From 0b7464e5781d7a60c0d0e91e56f07b59d11911f8 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:58:27 +0200 Subject: [PATCH 10/26] update and delete --- .../class/osparc/study/Conversations.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index c0f0b509db6..e6a8998d040 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -315,6 +315,25 @@ qx.Class.define("osparc.study.Conversations", { }, }, + __deleteConversation: function(conversationData) { + const conversationId = conversationData["conversationId"]; + const conversation = this.__getConversation(conversationId); + if (conversation) { + const conversationsLayout = this.getChildControl("conversations-layout"); + conversationsLayout.remove(conversation); + this.__conversations = this.__conversations.filter(c => c !== conversation); + } + }, + + // it can only be renamed, not updated + __updateConversation: function(conversationData) { + const conversationId = conversationData["conversationId"]; + const conversation = this.__getConversation(conversationId); + if (conversation) { + conversation.renameConversation(conversationData["name"]); + } + }, + destruct: function() { const socket = osparc.wrapper.WebSocket.getInstance(); if (this.__wsHandlers) { From 671c5443698c0aed0d13ed9032061464a2b56833 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 13:59:22 +0200 Subject: [PATCH 11/26] minor --- .../client/source/class/osparc/study/Conversations.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index e6a8998d040..bdd8f5e09c4 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -302,9 +302,9 @@ qx.Class.define("osparc.study.Conversations", { backgroundColor: "transparent", }); newConversationButton.addListener("execute", () => { - osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (conversations.length + 1)) - .then(conversation => { - this.__addConversation(conversation); + osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (this.__conversations.length + 1)) + .then(conversationDt => { + this.__addConversation(conversationDt); }); }); conversationsLayout.getChildControl("bar").add(newConversationButton); From 525d1f0bb6be4e755aeadce3dd097bef4112ef06 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 25 Jun 2025 14:19:51 +0200 Subject: [PATCH 12/26] tests: add conversation events --- .../test_projects_conversations_handlers.py | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py index 42f1960a9f9..2f8a43c4d8a 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py @@ -12,6 +12,7 @@ import pytest import simcore_service_webserver.conversations._conversation_message_service +import simcore_service_webserver.conversations._conversation_service import sqlalchemy as sa from aiohttp.test_utils import TestClient from models_library.api_schemas_webserver.projects_conversations import ( @@ -33,10 +34,10 @@ @pytest.fixture -def mock_notify_function(mocker: MockerFixture) -> Callable[[str], MagicMock]: - def _mock(function_name: str) -> MagicMock: +def mock_notify_function(mocker: MockerFixture) -> Callable[[object, str], MagicMock]: + def _mock(target: object, function_name: str) -> MagicMock: return mocker.patch.object( - simcore_service_webserver.conversations._conversation_message_service, + target, function_name, ) @@ -81,7 +82,21 @@ async def test_project_conversations_full_workflow( logged_user: UserInfoDict, user_project: ProjectDict, expected: HTTPStatus, + mock_notify_function: Callable[[object, str], MagicMock], ): + mocked_notify_conversation_created = mock_notify_function( + simcore_service_webserver.conversations._conversation_service, + "notify_conversation_created", + ) + mocked_notify_conversation_updated = mock_notify_function( + simcore_service_webserver.conversations._conversation_service, + "notify_conversation_updated", + ) + mocked_notify_conversation_deleted = mock_notify_function( + simcore_service_webserver.conversations._conversation_service, + "notify_conversation_deleted", + ) + base_url = client.app.router["list_project_conversations"].url_for( project_id=user_project["uuid"] ) @@ -106,6 +121,12 @@ async def test_project_conversations_full_workflow( assert ConversationRestGet.model_validate(data) _first_conversation_id = data["conversationId"] + assert mocked_notify_conversation_created.call_count == 1 + kwargs = mocked_notify_conversation_created.call_args.kwargs + + assert f"{kwargs['project_id']}" == user_project["uuid"] + assert kwargs["conversation"].name == "My first conversation" + # Now we will create second conversation body = {"name": "My conversation", "type": "PROJECT_ANNOTATION"} resp = await client.post(f"{base_url}", json=body) @@ -115,6 +136,12 @@ async def test_project_conversations_full_workflow( ) assert ConversationRestGet.model_validate(data) + assert mocked_notify_conversation_created.call_count == 2 + kwargs = mocked_notify_conversation_created.call_args.kwargs + + assert f"{kwargs['project_id']}" == user_project["uuid"] + assert kwargs["conversation"].name == "My conversation" + # Now we will list all conversations for the project resp = await client.get(f"{base_url}") data, _, meta, links = await assert_status( @@ -145,6 +172,12 @@ async def test_project_conversations_full_workflow( ) assert data["name"] == updated_name + assert mocked_notify_conversation_updated.call_count == 1 + kwargs = mocked_notify_conversation_updated.call_args.kwargs + + assert f"{kwargs['project_id']}" == user_project["uuid"] + assert kwargs["conversation"].name == updated_name + # Now we will delete the first conversation resp = await client.delete(f"{base_url}/{_first_conversation_id}") data, _ = await assert_status( @@ -152,6 +185,11 @@ async def test_project_conversations_full_workflow( status.HTTP_204_NO_CONTENT, ) + assert mocked_notify_conversation_deleted.call_count == 1 + kwargs = mocked_notify_conversation_deleted.call_args.kwargs + + assert kwargs["conversation"].conversation_id == _first_conversation_id + # Now we will list all conversations for the project resp = await client.get(f"{base_url}") data, _, meta = await assert_status( @@ -178,16 +216,19 @@ async def test_project_conversation_messages_full_workflow( user_project: ProjectDict, expected: HTTPStatus, postgres_db: sa.engine.Engine, - mock_notify_function: Callable[[str], MagicMock], + mock_notify_function: Callable[[object, str], MagicMock], ): mocked_notify_conversation_message_created = mock_notify_function( - "notify_conversation_message_created" + simcore_service_webserver.conversations._conversation_message_service, + "notify_conversation_message_created", ) mocked_notify_conversation_message_updated = mock_notify_function( - "notify_conversation_message_updated" + simcore_service_webserver.conversations._conversation_message_service, + "notify_conversation_message_updated", ) mocked_notify_conversation_message_deleted = mock_notify_function( - "notify_conversation_message_deleted" + simcore_service_webserver.conversations._conversation_message_service, + "notify_conversation_message_deleted", ) base_project_url = client.app.router["list_project_conversations"].url_for( From 9b8c46553febd1d8af8ecf21fb2efb0390d09a36 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 14:48:38 +0200 Subject: [PATCH 13/26] minor --- .../class/osparc/study/Conversations.js | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index bdd8f5e09c4..d57176c7f72 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -276,6 +276,7 @@ qx.Class.define("osparc.study.Conversations", { conversation = new osparc.conversation.Conversation(studyData); conversation.setLabel(this.tr("new")); } + return conversation; }, __addTemporaryConversation: function() { @@ -313,34 +314,34 @@ qx.Class.define("osparc.study.Conversations", { conversationsLayout.getChildControl("bar").remove(this.__newConversationButton); conversationsLayout.getChildControl("bar").add(this.__newConversationButton); }, - }, - __deleteConversation: function(conversationData) { - const conversationId = conversationData["conversationId"]; - const conversation = this.__getConversation(conversationId); - if (conversation) { - const conversationsLayout = this.getChildControl("conversations-layout"); - conversationsLayout.remove(conversation); - this.__conversations = this.__conversations.filter(c => c !== conversation); - } - }, + __deleteConversation: function(conversationData) { + const conversationId = conversationData["conversationId"]; + const conversation = this.__getConversation(conversationId); + if (conversation) { + const conversationsLayout = this.getChildControl("conversations-layout"); + conversationsLayout.remove(conversation); + this.__conversations = this.__conversations.filter(c => c !== conversation); + } + }, - // it can only be renamed, not updated - __updateConversation: function(conversationData) { - const conversationId = conversationData["conversationId"]; - const conversation = this.__getConversation(conversationId); - if (conversation) { - conversation.renameConversation(conversationData["name"]); - } - }, + // it can only be renamed, not updated + __updateConversation: function(conversationData) { + const conversationId = conversationData["conversationId"]; + const conversation = this.__getConversation(conversationId); + if (conversation) { + conversation.renameConversation(conversationData["name"]); + } + }, - destruct: function() { - const socket = osparc.wrapper.WebSocket.getInstance(); - if (this.__wsHandlers) { - this.__wsHandlers.forEach(({ eventName }) => { - socket.removeSlot(eventName); - }); - this.__wsHandlers = null; - } - }, + destruct: function() { + const socket = osparc.wrapper.WebSocket.getInstance(); + if (this.__wsHandlers) { + this.__wsHandlers.forEach(({ eventName }) => { + socket.removeSlot(eventName); + }); + this.__wsHandlers = null; + } + }, + } }); From ee73ea91224c3c1010bc3d9eaab2367dd69fbb8f Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 15:00:23 +0200 Subject: [PATCH 14/26] refactor --- .../class/osparc/study/Conversations.js | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index d57176c7f72..468772178ac 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -154,6 +154,7 @@ qx.Class.define("osparc.study.Conversations", { members: { __conversations: null, + __newConversationButton: null, __wsHandlers: null, _createChildControlImpl: function(id) { @@ -188,7 +189,7 @@ qx.Class.define("osparc.study.Conversations", { if (conversation) { switch (eventName) { case "conversation:created": - this.__addConversation(conversation); + this.__addConversationPage(conversation); break; case "conversation:updated": this.__updateConversation(conversation); @@ -249,10 +250,10 @@ qx.Class.define("osparc.study.Conversations", { }; osparc.data.Resources.fetch("conversations", "getConversationsPage", params) .then(conversations => { - if (conversations.length === 0) { - conversations.forEach(conversation => this.__addConversation(conversation)); + if (conversations.length) { + conversations.forEach(conversation => this.__addConversationPage(conversation)); } else { - this.__addTemporaryConversation(); + this.__addTempConversationPage(); } }) .finally(() => { @@ -261,38 +262,41 @@ qx.Class.define("osparc.study.Conversations", { }); }, - __createConversation: function(conversationData) { + __createConversationPage: function(conversationData) { const studyData = this.getStudyData(); - let conversation = null; + let conversationPage = null; if (conversationData) { const conversationId = conversationData["conversationId"]; - conversation = new osparc.conversation.Conversation(studyData, conversationId); - conversation.setLabel(conversationData["name"]); - conversation.addListener("conversationDeleted", () => { - console.log("Conversation deleted"); + conversationPage = new osparc.conversation.Conversation(studyData, conversationId); + conversationPage.setLabel(conversationData["name"]); + conversationPage.addListener("conversationDeleted", () => { + this.__deleteConversation(conversationData); }); } else { // create a temporary conversation - conversation = new osparc.conversation.Conversation(studyData); - conversation.setLabel(this.tr("new")); + conversationPage = new osparc.conversation.Conversation(studyData); + conversationPage.setLabel(this.tr("new")); } - return conversation; + return conversationPage; }, - __addTemporaryConversation: function() { - const temporaryConversation = this.__createConversation(); - - const conversationsLayout = this.getChildControl("conversations-layout"); - conversationsLayout.add(temporaryConversation); + __addTempConversationPage: function() { + const temporaryConversationPage = this.__createConversationPage(); + this.__addToPages(temporaryConversationPage); }, - __addConversation: function(conversationData) { - const conversation = this.__createConversation(conversationData); + __addConversationPage: function(conversationData) { + const conversationPage = this.__createConversationPage(conversationData); + this.__addToPages(conversationPage); - const conversationsLayout = this.getChildControl("conversations-layout"); - conversationsLayout.add(conversation); + this.__conversations.push(conversationPage); + + return conversationPage; + }, - this.__conversations.push(conversation); + __addToPages: function(conversationPage) { + const conversationsLayout = this.getChildControl("conversations-layout"); + conversationsLayout.add(conversationPage); if (this.__newConversationButton === null) { // initialize the new button only once @@ -303,9 +307,11 @@ qx.Class.define("osparc.study.Conversations", { backgroundColor: "transparent", }); newConversationButton.addListener("execute", () => { + const studyData = this.getStudyData(); osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (this.__conversations.length + 1)) .then(conversationDt => { - this.__addConversation(conversationDt); + const newConversationPage = this.__addConversationPage(conversationDt); + conversationsLayout.setSelection([newConversationPage]); }); }); conversationsLayout.getChildControl("bar").add(newConversationButton); @@ -322,6 +328,14 @@ qx.Class.define("osparc.study.Conversations", { const conversationsLayout = this.getChildControl("conversations-layout"); conversationsLayout.remove(conversation); this.__conversations = this.__conversations.filter(c => c !== conversation); + + const conversationPages = conversationsLayout.getSelectables(); + if (conversationPages.length) { + conversationsLayout.setSelection([conversationPages[0]]); + } else { + // no conversations left, add a temporary one + this.__addTempConversationPage(); + } } }, From 532ccf4ad549bef97aff60def4a748f9fbd266b6 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 15:02:14 +0200 Subject: [PATCH 15/26] rename --- .../source/class/osparc/study/Conversations.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 468772178ac..1dedf7e8fca 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -192,10 +192,10 @@ qx.Class.define("osparc.study.Conversations", { this.__addConversationPage(conversation); break; case "conversation:updated": - this.__updateConversation(conversation); + this.__updateConversationName(conversation); break; case "conversation:deleted": - this.__deleteConversation(conversation); + this.__removeConversationPage(conversation); break; } } @@ -269,9 +269,7 @@ qx.Class.define("osparc.study.Conversations", { const conversationId = conversationData["conversationId"]; conversationPage = new osparc.conversation.Conversation(studyData, conversationId); conversationPage.setLabel(conversationData["name"]); - conversationPage.addListener("conversationDeleted", () => { - this.__deleteConversation(conversationData); - }); + conversationPage.addListener("conversationDeleted", () => this.__removeConversationPage(conversationData)); } else { // create a temporary conversation conversationPage = new osparc.conversation.Conversation(studyData); @@ -321,7 +319,7 @@ qx.Class.define("osparc.study.Conversations", { conversationsLayout.getChildControl("bar").add(this.__newConversationButton); }, - __deleteConversation: function(conversationData) { + __removeConversationPage: function(conversationData) { const conversationId = conversationData["conversationId"]; const conversation = this.__getConversation(conversationId); if (conversation) { @@ -340,7 +338,7 @@ qx.Class.define("osparc.study.Conversations", { }, // it can only be renamed, not updated - __updateConversation: function(conversationData) { + __updateConversationName: function(conversationData) { const conversationId = conversationData["conversationId"]; const conversation = this.__getConversation(conversationId); if (conversation) { From 663d9bbd2ed27ccbb6c3969f2a9ba12c650e1193 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 15:25:16 +0200 Subject: [PATCH 16/26] extra checks --- .../client/source/class/osparc/study/Conversations.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 1dedf7e8fca..152e9a15921 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -284,6 +284,13 @@ qx.Class.define("osparc.study.Conversations", { }, __addConversationPage: function(conversationData) { + // ignore it if it was already there + const conversationId = conversationData["conversationId"]; + const conversation = this.__getConversation(conversationId); + if (conversation) { + return null; + } + const conversationPage = this.__createConversationPage(conversationData); this.__addToPages(conversationPage); @@ -309,7 +316,9 @@ qx.Class.define("osparc.study.Conversations", { osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (this.__conversations.length + 1)) .then(conversationDt => { const newConversationPage = this.__addConversationPage(conversationDt); - conversationsLayout.setSelection([newConversationPage]); + if (newConversationPage) { + conversationsLayout.setSelection([newConversationPage]); + } }); }); conversationsLayout.getChildControl("bar").add(newConversationButton); From 4807e051962c6476501d24e11311058f88e417a2 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 15:47:01 +0200 Subject: [PATCH 17/26] minor --- .../client/source/class/osparc/study/Conversations.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 152e9a15921..3d8c9f85ac6 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -315,7 +315,8 @@ qx.Class.define("osparc.study.Conversations", { const studyData = this.getStudyData(); osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (this.__conversations.length + 1)) .then(conversationDt => { - const newConversationPage = this.__addConversationPage(conversationDt); + this.__addConversationPage(conversationDt); + const newConversationPage = this.__getConversation(conversationDt["conversationId"]); if (newConversationPage) { conversationsLayout.setSelection([newConversationPage]); } From 93af2591f37c233d0a930ff35d4ed3323308904a Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Wed, 25 Jun 2025 15:49:20 +0200 Subject: [PATCH 18/26] UX --- .../client/source/class/osparc/study/Conversations.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 3d8c9f85ac6..a0aba854066 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -269,7 +269,7 @@ qx.Class.define("osparc.study.Conversations", { const conversationId = conversationData["conversationId"]; conversationPage = new osparc.conversation.Conversation(studyData, conversationId); conversationPage.setLabel(conversationData["name"]); - conversationPage.addListener("conversationDeleted", () => this.__removeConversationPage(conversationData)); + conversationPage.addListener("conversationDeleted", () => this.__removeConversationPage(conversationData, true)); } else { // create a temporary conversation conversationPage = new osparc.conversation.Conversation(studyData); @@ -329,7 +329,7 @@ qx.Class.define("osparc.study.Conversations", { conversationsLayout.getChildControl("bar").add(this.__newConversationButton); }, - __removeConversationPage: function(conversationData) { + __removeConversationPage: function(conversationData, changeSelection = false) { const conversationId = conversationData["conversationId"]; const conversation = this.__getConversation(conversationId); if (conversation) { @@ -337,9 +337,14 @@ qx.Class.define("osparc.study.Conversations", { conversationsLayout.remove(conversation); this.__conversations = this.__conversations.filter(c => c !== conversation); + changeSelection + const conversationPages = conversationsLayout.getSelectables(); if (conversationPages.length) { - conversationsLayout.setSelection([conversationPages[0]]); + if (changeSelection) { + // change selection to the first conversation + conversationsLayout.setSelection([conversationPages[0]]); + } } else { // no conversations left, add a temporary one this.__addTempConversationPage(); From 20a0fe2d602f66d9bef374b51dae41ab72c04e51 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Wed, 25 Jun 2025 17:32:37 +0200 Subject: [PATCH 19/26] tests: fix --- .../test_projects_conversations_handlers.py | 102 +++++++++--------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py index 2f8a43c4d8a..606f5368120 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_conversations_handlers.py @@ -6,13 +6,13 @@ # pylint: disable=too-many-statements -from collections.abc import Callable +from collections.abc import Callable, Iterable from http import HTTPStatus -from unittest.mock import MagicMock +from types import SimpleNamespace import pytest -import simcore_service_webserver.conversations._conversation_message_service -import simcore_service_webserver.conversations._conversation_service +import simcore_service_webserver.conversations._conversation_message_service as conversation_message_service +import simcore_service_webserver.conversations._conversation_service as conversation_service import sqlalchemy as sa from aiohttp.test_utils import TestClient from models_library.api_schemas_webserver.projects_conversations import ( @@ -34,14 +34,18 @@ @pytest.fixture -def mock_notify_function(mocker: MockerFixture) -> Callable[[object, str], MagicMock]: - def _mock(target: object, function_name: str) -> MagicMock: - return mocker.patch.object( - target, - function_name, +def mock_functions_factory( + mocker: MockerFixture, +) -> Callable[[Iterable[tuple[object, str]]], SimpleNamespace]: + def _patch(targets_and_names: Iterable[tuple[object, str]]) -> SimpleNamespace: + return SimpleNamespace( + **{ + name: mocker.patch.object(target, name) + for target, name in targets_and_names + } ) - return _mock + return _patch @pytest.mark.parametrize( @@ -82,19 +86,14 @@ async def test_project_conversations_full_workflow( logged_user: UserInfoDict, user_project: ProjectDict, expected: HTTPStatus, - mock_notify_function: Callable[[object, str], MagicMock], + mock_functions_factory: Callable[[Iterable[tuple[object, str]]], SimpleNamespace], ): - mocked_notify_conversation_created = mock_notify_function( - simcore_service_webserver.conversations._conversation_service, - "notify_conversation_created", - ) - mocked_notify_conversation_updated = mock_notify_function( - simcore_service_webserver.conversations._conversation_service, - "notify_conversation_updated", - ) - mocked_notify_conversation_deleted = mock_notify_function( - simcore_service_webserver.conversations._conversation_service, - "notify_conversation_deleted", + mocks = mock_functions_factory( + [ + (conversation_service, "notify_conversation_created"), + (conversation_service, "notify_conversation_updated"), + (conversation_service, "notify_conversation_deleted"), + ] ) base_url = client.app.router["list_project_conversations"].url_for( @@ -121,11 +120,11 @@ async def test_project_conversations_full_workflow( assert ConversationRestGet.model_validate(data) _first_conversation_id = data["conversationId"] - assert mocked_notify_conversation_created.call_count == 1 - kwargs = mocked_notify_conversation_created.call_args.kwargs + assert mocks.notify_conversation_created.call_count == 1 + kwargs = mocks.notify_conversation_created.call_args.kwargs assert f"{kwargs['project_id']}" == user_project["uuid"] - assert kwargs["conversation"].name == "My first conversation" + assert kwargs["conversation"].name == "My conversation" # Now we will create second conversation body = {"name": "My conversation", "type": "PROJECT_ANNOTATION"} @@ -136,8 +135,8 @@ async def test_project_conversations_full_workflow( ) assert ConversationRestGet.model_validate(data) - assert mocked_notify_conversation_created.call_count == 2 - kwargs = mocked_notify_conversation_created.call_args.kwargs + assert mocks.notify_conversation_created.call_count == 2 + kwargs = mocks.notify_conversation_created.call_args.kwargs assert f"{kwargs['project_id']}" == user_project["uuid"] assert kwargs["conversation"].name == "My conversation" @@ -172,8 +171,8 @@ async def test_project_conversations_full_workflow( ) assert data["name"] == updated_name - assert mocked_notify_conversation_updated.call_count == 1 - kwargs = mocked_notify_conversation_updated.call_args.kwargs + assert mocks.notify_conversation_updated.call_count == 1 + kwargs = mocks.notify_conversation_updated.call_args.kwargs assert f"{kwargs['project_id']}" == user_project["uuid"] assert kwargs["conversation"].name == updated_name @@ -185,10 +184,10 @@ async def test_project_conversations_full_workflow( status.HTTP_204_NO_CONTENT, ) - assert mocked_notify_conversation_deleted.call_count == 1 - kwargs = mocked_notify_conversation_deleted.call_args.kwargs + assert mocks.notify_conversation_deleted.call_count == 1 + kwargs = mocks.notify_conversation_deleted.call_args.kwargs - assert kwargs["conversation"].conversation_id == _first_conversation_id + assert f"{kwargs['conversation_id']}" == _first_conversation_id # Now we will list all conversations for the project resp = await client.get(f"{base_url}") @@ -216,19 +215,14 @@ async def test_project_conversation_messages_full_workflow( user_project: ProjectDict, expected: HTTPStatus, postgres_db: sa.engine.Engine, - mock_notify_function: Callable[[object, str], MagicMock], + mock_functions_factory: Callable[[Iterable[tuple[object, str]]], SimpleNamespace], ): - mocked_notify_conversation_message_created = mock_notify_function( - simcore_service_webserver.conversations._conversation_message_service, - "notify_conversation_message_created", - ) - mocked_notify_conversation_message_updated = mock_notify_function( - simcore_service_webserver.conversations._conversation_message_service, - "notify_conversation_message_updated", - ) - mocked_notify_conversation_message_deleted = mock_notify_function( - simcore_service_webserver.conversations._conversation_message_service, - "notify_conversation_message_deleted", + mocks = mock_functions_factory( + [ + (conversation_message_service, "notify_conversation_message_created"), + (conversation_message_service, "notify_conversation_message_updated"), + (conversation_message_service, "notify_conversation_message_deleted"), + ] ) base_project_url = client.app.router["list_project_conversations"].url_for( @@ -258,8 +252,8 @@ async def test_project_conversation_messages_full_workflow( assert ConversationMessageRestGet.model_validate(data) _first_message_id = data["messageId"] - assert mocked_notify_conversation_message_created.call_count == 1 - kwargs = mocked_notify_conversation_message_created.call_args.kwargs + assert mocks.notify_conversation_message_created.call_count == 1 + kwargs = mocks.notify_conversation_message_created.call_args.kwargs assert f"{kwargs['project_id']}" == user_project["uuid"] assert kwargs["conversation_message"].content == "My first message" @@ -274,8 +268,8 @@ async def test_project_conversation_messages_full_workflow( assert ConversationMessageRestGet.model_validate(data) _second_message_id = data["messageId"] - assert mocked_notify_conversation_message_created.call_count == 2 - kwargs = mocked_notify_conversation_message_created.call_args.kwargs + assert mocks.notify_conversation_message_created.call_count == 2 + kwargs = mocks.notify_conversation_message_created.call_args.kwargs assert user_project["uuid"] == f"{kwargs['project_id']}" assert kwargs["conversation_message"].content == "My second message" @@ -306,8 +300,8 @@ async def test_project_conversation_messages_full_workflow( expected, ) - assert mocked_notify_conversation_message_updated.call_count == 1 - kwargs = mocked_notify_conversation_message_updated.call_args.kwargs + assert mocks.notify_conversation_message_updated.call_count == 1 + kwargs = mocks.notify_conversation_message_updated.call_args.kwargs assert user_project["uuid"] == f"{kwargs['project_id']}" assert kwargs["conversation_message"].content == updated_content @@ -342,8 +336,8 @@ async def test_project_conversation_messages_full_workflow( status.HTTP_204_NO_CONTENT, ) - assert mocked_notify_conversation_message_deleted.call_count == 1 - kwargs = mocked_notify_conversation_message_deleted.call_args.kwargs + assert mocks.notify_conversation_message_deleted.call_count == 1 + kwargs = mocks.notify_conversation_message_deleted.call_args.kwargs assert f"{kwargs['project_id']}" == user_project["uuid"] assert f"{kwargs['conversation_id']}" == _conversation_id @@ -440,8 +434,8 @@ async def test_project_conversation_messages_full_workflow( status.HTTP_204_NO_CONTENT, ) - assert mocked_notify_conversation_message_deleted.call_count == 2 - kwargs = mocked_notify_conversation_message_deleted.call_args.kwargs + assert mocks.notify_conversation_message_deleted.call_count == 2 + kwargs = mocks.notify_conversation_message_deleted.call_args.kwargs assert f"{kwargs['project_id']}" == user_project["uuid"] assert f"{kwargs['conversation_id']}" == _conversation_id From 71231193e90a64ac66040a6b2985598c2aaa8562 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Thu, 26 Jun 2025 08:33:34 +0200 Subject: [PATCH 20/26] refactor --- .../source/class/osparc/dashboard/CardBase.js | 13 ++++++++----- .../class/osparc/dashboard/NewPlusMenu.js | 9 ++++++--- .../osparc/dashboard/ResourceBrowserBase.js | 15 +++++++++------ .../class/osparc/dashboard/ResourceDetails.js | 19 +++++++++++-------- .../desktop/organizations/ServicesList.js | 6 ++++-- .../desktop/organizations/TutorialsList.js | 6 ++++-- .../osparc/notification/NotificationUI.js | 9 ++++++--- .../class/osparc/share/ShareePermissions.js | 9 ++++----- .../class/osparc/widget/PersistentIframe.js | 9 ++++++--- 9 files changed, 58 insertions(+), 37 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index 9cefd0c9aff..6f335b73b3d 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -1024,13 +1024,16 @@ qx.Class.define("osparc.dashboard.CardBase", { __openResourceDetails: function(openWindowCB) { const resourceData = this.getResourceData(); - const resourceDetails = new osparc.dashboard.ResourceDetails(resourceData); + const { + resourceDetails, + window, + } = osparc.dashboard.ResourceDetails.popUpInWindow(resourceData); + resourceDetails.addListenerOnce("pagesAdded", () => { if (openWindowCB in resourceDetails) { resourceDetails[openWindowCB](); } - }) - const win = osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); + }); [ "updateStudy", "updateTemplate", @@ -1041,11 +1044,11 @@ qx.Class.define("osparc.dashboard.CardBase", { resourceDetails.addListener(ev, e => this.fireDataEvent(ev, e.getData())); }); resourceDetails.addListener("publishTemplate", e => { - win.close(); + window.close(); this.fireDataEvent("publishTemplate", e.getData()); }); resourceDetails.addListener("openStudy", e => { - const openCB = () => win.close(); + const openCB = () => window.close(); const studyId = e.getData()["uuid"]; const isStudyCreation = false; this._startStudyById(studyId, openCB, null, isStudyCreation); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js b/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js index 9de7d11a173..d645af47b55 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js @@ -360,10 +360,13 @@ qx.Class.define("osparc.dashboard.NewPlusMenu", { // so that is not consumed by the menu button itself e.stopPropagation(); latestMetadata["resourceType"] = "service"; - const resourceDetails = new osparc.dashboard.ResourceDetails(latestMetadata); - const win = osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); + const { + resourceDetails, + window, + } = osparc.dashboard.ResourceDetails.popUpInWindow(latestMetadata); + resourceDetails.addListener("openService", ev => { - win.close(); + window.close(); const openServiceData = ev.getData(); this.fireDataEvent("newStudyFromServiceClicked", { serviceMetadata: openServiceData, diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js index b9666fec810..eb58ca7fed8 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js @@ -939,19 +939,22 @@ qx.Class.define("osparc.dashboard.ResourceBrowserBase", { }, _openResourceDetails: function(resourceData) { - const resourceDetails = new osparc.dashboard.ResourceDetails(resourceData); - const win = osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); + const { + resourceDetails, + window, + } = osparc.dashboard.ResourceDetails.popUpInWindow(resourceData); + resourceDetails.addListener("updateStudy", e => this._updateStudyData(e.getData())); resourceDetails.addListener("updateTemplate", e => this._updateTemplateData(e.getData())); resourceDetails.addListener("updateTutorial", e => this._updateTutorialData(e.getData())); resourceDetails.addListener("updateService", e => this._updateServiceData(e.getData())); resourceDetails.addListener("updateHypertool", e => this._updateHypertoolData(e.getData())); resourceDetails.addListener("publishTemplate", e => { - win.close(); + window.close(); this.fireDataEvent("publishTemplate", e.getData()); }); resourceDetails.addListener("openStudy", e => { - const openCB = () => win.close(); + const openCB = () => window.close(); const studyId = e.getData()["uuid"]; const isStudyCreation = false; this._startStudyById(studyId, openCB, null, isStudyCreation); @@ -962,13 +965,13 @@ qx.Class.define("osparc.dashboard.ResourceBrowserBase", { "openHypertool", ].forEach(eventName => { resourceDetails.addListener(eventName, e => { - win.close(); + window.close(); const templateData = e.getData(); this._createStudyFromTemplate(templateData); }); }); resourceDetails.addListener("openService", e => { - win.close(); + window.close(); const openServiceData = e.getData(); this._createStudyFromService(openServiceData["key"], openServiceData["version"]); }); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index bd5f1785227..7865d1b010d 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -107,21 +107,24 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { WIDTH: 830, HEIGHT: 700, - popUpInWindow: function(resourceDetails) { - // eslint-disable-next-line no-underscore-dangle - const title = resourceDetails.__resourceData.name; - const win = osparc.ui.window.Window.popUpInWindow(resourceDetails, title, this.WIDTH, this.HEIGHT).set({ + popUpInWindow: function(resourceData) { + const resourceDetails = new osparc.dashboard.ResourceDetails(resourceData); + const title = resourceData.name; + const window = osparc.ui.window.Window.popUpInWindow(resourceDetails, title, this.WIDTH, this.HEIGHT).set({ layout: new qx.ui.layout.Grow(), }); - win.set(osparc.ui.window.TabbedWindow.DEFAULT_PROPS); - win.set({ + window.set(osparc.ui.window.TabbedWindow.DEFAULT_PROPS); + window.set({ width: this.WIDTH, height: this.HEIGHT, }); resourceDetails.addListener("closeWindow", () => { - win.close(); + window.close(); }); - return win; + return { + resourceDetails, + window, + }; }, createToolbar: function() { diff --git a/services/static-webserver/client/source/class/osparc/desktop/organizations/ServicesList.js b/services/static-webserver/client/source/class/osparc/desktop/organizations/ServicesList.js index 2265026c55a..58b1bc96195 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/organizations/ServicesList.js +++ b/services/static-webserver/client/source/class/osparc/desktop/organizations/ServicesList.js @@ -92,10 +92,12 @@ qx.Class.define("osparc.desktop.organizations.ServicesList", { .then(serviceData => { if (serviceData) { serviceData["resourceType"] = "service"; - const resourceDetails = new osparc.dashboard.ResourceDetails(serviceData).set({ + const { + resourceDetails, + } = osparc.dashboard.ResourceDetails.popUpInWindow(serviceData); + resourceDetails.set({ showOpenButton: false }); - osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); } }); }); diff --git a/services/static-webserver/client/source/class/osparc/desktop/organizations/TutorialsList.js b/services/static-webserver/client/source/class/osparc/desktop/organizations/TutorialsList.js index 6636b4bd918..c735aefe30a 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/organizations/TutorialsList.js +++ b/services/static-webserver/client/source/class/osparc/desktop/organizations/TutorialsList.js @@ -90,10 +90,12 @@ qx.Class.define("osparc.desktop.organizations.TutorialsList", { .then(templateData => { if (templateData) { templateData["resourceType"] = "tutorial"; - const resourceDetails = new osparc.dashboard.ResourceDetails(templateData).set({ + const { + resourceDetails, + } = osparc.dashboard.ResourceDetails.popUpInWindow(templateData); + resourceDetails.set({ showOpenButton: false }); - osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); } }); }); 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 7cd46cbae75..6e5233ffa8b 100644 --- a/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js +++ b/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js @@ -332,11 +332,14 @@ qx.Class.define("osparc.notification.NotificationUI", { if (studyData) { const studyDataCopy = osparc.data.model.Study.deepCloneStudyObject(studyData); studyDataCopy["resourceType"] = notification.getCategory() === "TEMPLATE_SHARED" ? "template" : "study"; - const resourceDetails = new osparc.dashboard.ResourceDetails(studyDataCopy); - const win = osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); + const { + resourceDetails, + window, + } = osparc.dashboard.ResourceDetails.popUpInWindow(studyDataCopy); + resourceDetails.addListener("openStudy", () => { if (notification.getCategory() === "STUDY_SHARED") { - const openCB = () => win.close(); + const openCB = () => window.close(); osparc.dashboard.ResourceBrowserBase.startStudyById(studyId, openCB); } }); diff --git a/services/static-webserver/client/source/class/osparc/share/ShareePermissions.js b/services/static-webserver/client/source/class/osparc/share/ShareePermissions.js index 2a2a2bf9909..cd516e76833 100644 --- a/services/static-webserver/client/source/class/osparc/share/ShareePermissions.js +++ b/services/static-webserver/client/source/class/osparc/share/ShareePermissions.js @@ -90,12 +90,11 @@ qx.Class.define("osparc.share.ShareePermissions", { hBox.add(infoButton); hBox.add(label); osparc.store.Services.getService(inaccessibleService.key, inaccessibleService.version) - .then(metadata => { - label.setValue(metadata["name"] + " : " + metadata["version"]) + .then(serviceMetadata => { + label.setValue(serviceMetadata["name"] + " : " + serviceMetadata["version"]) infoButton.addListener("execute", () => { - metadata["resourceType"] = "service"; - const resourceDetails = new osparc.dashboard.ResourceDetails(metadata); - osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); + serviceMetadata["resourceType"] = "service"; + osparc.dashboard.ResourceDetails.popUpInWindow(serviceMetadata); }, this); }) diff --git a/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js b/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js index b4293f56ca1..81f4d9bb7ec 100644 --- a/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js +++ b/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js @@ -338,11 +338,14 @@ qx.Class.define("osparc.widget.PersistentIframe", { "uuid": templateId, "resourceType": "function", }; - const resourceDetails = new osparc.dashboard.ResourceDetails(functionData).set({ + const { + resourceDetails, + window, + } = osparc.dashboard.ResourceDetails.popUpInWindow(functionData); + resourceDetails.set({ showOpenButton: false, }); - const win = osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); - win.setCaption("Function Details"); + window.setCaption("Function Details"); } break; } From e21761a35dc2fd0f5ad107a2120c0f560a7cb482 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Thu, 26 Jun 2025 08:39:52 +0200 Subject: [PATCH 21/26] minor --- .../client/source/class/osparc/ui/window/Window.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/ui/window/Window.js b/services/static-webserver/client/source/class/osparc/ui/window/Window.js index c8f8c304d30..8f8cf31aa71 100644 --- a/services/static-webserver/client/source/class/osparc/ui/window/Window.js +++ b/services/static-webserver/client/source/class/osparc/ui/window/Window.js @@ -73,8 +73,8 @@ qx.Class.define("osparc.ui.window.Window", { showMinimize: false, showMaximize: false, resizable: true, - width: width, - minHeight: minHeight, + width, + minHeight, maxHeight: Math.max(minHeight, document.documentElement.clientHeight), modal: true, clickAwayClose: true From e4b3cd4ac2e8fbeb1ea01663a0ed17eeb4101c46 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Thu, 26 Jun 2025 08:40:07 +0200 Subject: [PATCH 22/26] simplify --- .../source/class/osparc/dashboard/ResourceDetails.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 7865d1b010d..ca3684abc9c 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -112,15 +112,9 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const title = resourceData.name; const window = osparc.ui.window.Window.popUpInWindow(resourceDetails, title, this.WIDTH, this.HEIGHT).set({ layout: new qx.ui.layout.Grow(), + ...osparc.ui.window.TabbedWindow.DEFAULT_PROPS, }); - window.set(osparc.ui.window.TabbedWindow.DEFAULT_PROPS); - window.set({ - width: this.WIDTH, - height: this.HEIGHT, - }); - resourceDetails.addListener("closeWindow", () => { - window.close(); - }); + resourceDetails.addListener("closeWindow", () => window.close()); return { resourceDetails, window, From c8d0d337c6b60790d13918ac070db138d07a8f4c Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Thu, 26 Jun 2025 09:10:24 +0200 Subject: [PATCH 23/26] refactor --- .../class/osparc/dashboard/ResourceDetails.js | 152 +++++++++--------- 1 file changed, 75 insertions(+), 77 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index ca3684abc9c..04c39817728 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -376,42 +376,29 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { if (this.__resourceData["resourceType"] === "function") { // for now, we only want the preview page - const page = this.__getPreviewPage(); - if (page) { - tabsView.add(page); - } + this.__addPreviewPage(); this.fireEvent("pagesAdded"); return; } - // add Open service button - [ - this.__getInfoPage, - this.__getBillingPage, - this.__getServicesUpdatePage, - this.__getServicesBootOptionsPage, - this.__getConversationsPage, - this.__getPermissionsPage, - this.__getPublishPage, - this.__getCreateTemplatePage, - this.__getCreateFunctionsPage, - this.__getTagsPage, - this.__getQualityPage, - this.__getClassifiersPage, - this.__getPreviewPage - ].forEach(pageCallee => { - if (pageCallee) { - const page = pageCallee.call(this); - if (page) { - tabsView.add(page); - } - } - }); + this.__addInfoPage(); + this.__addBillingPage(); + this.__addServicesUpdatePage(); + this.__addServicesBootOptionsPage(); + this.__addConversationsPage(); + this.__addPermissionsPage(); + this.__addPublishPage(); + this.__addCreateTemplatePage(); + this.__addCreateFunctionsPage(); + this.__addTagsPage(); + this.__addQualityPage(); + this.__addClassifiersPage(); + this.__addPreviewPage(); if (osparc.product.Utils.showComputationalActivity()) { - this.__getActivityOverviewPopUp(); + this.__addActivityOverviewPopUp(); } - this.__getProjectFilesPopUp(); + this.__addProjectFilesPopUp(); if (selectedTabId) { const pageFound = tabsView.getChildren().find(page => page.tabId === selectedTabId); @@ -443,7 +430,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } }, - __getInfoPage: function() { + __addInfoPage: function() { const id = "Information"; const title = this.tr("Overview"); const iconSrc = "@FontAwesome5Solid/info/22"; @@ -475,12 +462,13 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getBillingPage: function() { + __addBillingPage: function() { if (!osparc.desktop.credits.Utils.areWalletsEnabled()) { - return null; + return; } const resourceData = this.__resourceData; @@ -513,7 +501,8 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); } else if (osparc.utils.Resources.isService(resourceData)) { const id = "Tiers"; const title = this.tr("Tiers"); @@ -528,12 +517,12 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); } - return null; }, - __getPreviewPage: function() { + __addPreviewPage: function() { const resourceData = this.__resourceData; if ( osparc.utils.Resources.isService(resourceData) || @@ -541,7 +530,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { ["app", "guided", "standalone"].includes(osparc.study.Utils.getUiMode(resourceData)) ) { // there is no pipelining or don't show it - return null; + return; } const id = "Pipeline"; @@ -561,13 +550,14 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getConversationsPage: function() { + __addConversationsPage: function() { const resourceData = this.__resourceData; if (osparc.utils.Resources.isService(resourceData)) { - return null; + return; } const id = "Conversations"; @@ -582,10 +572,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getPermissionsPage: function() { + __addPermissionsPage: function() { const id = "Permissions"; const title = this.tr("Sharing"); const iconSrc = "@FontAwesome5Solid/share-alt/22"; @@ -619,16 +610,17 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getClassifiersPage: function() { + __addClassifiersPage: function() { if (!osparc.product.Utils.showClassifiers()) { - return null; + return; } const id = "Classifiers"; if (!osparc.data.Permissions.getInstance().canDo("study.classifier")) { - return null; + return; } const title = this.tr("Classifiers"); const iconSrc = "@FontAwesome5Solid/search/22"; @@ -654,12 +646,13 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getQualityPage: function() { + __addQualityPage: function() { if (!osparc.product.Utils.showQuality()) { - return null; + return; } const resourceData = this.__resourceData; @@ -683,18 +676,18 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); } - return null; }, - __getTagsPage: function() { + __addTagsPage: function() { const resourceData = this.__resourceData; if (osparc.utils.Resources.isService(resourceData)) { - return null; + return; } if (!osparc.data.model.Study.canIWrite(resourceData["accessRights"])) { - return null; + return; } const id = "Tags"; @@ -714,13 +707,14 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getServicesUpdatePage: function() { + __addServicesUpdatePage: function() { const resourceData = this.__resourceData; if (osparc.utils.Resources.isService(resourceData)) { - return null; + return; } const id = "ServicesUpdate"; @@ -743,16 +737,17 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getServicesBootOptionsPage: function() { + __addServicesBootOptionsPage: function() { const resourceData = this.__resourceData; if ( osparc.utils.Resources.isService(resourceData) || !osparc.data.Permissions.getInstance().canDo("study.node.bootOptions.read") ) { - return null; + return; } const id = "ServicesBootOptions"; @@ -793,15 +788,16 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getPublishPage: function() { + __addPublishPage: function() { if ( !osparc.utils.Resources.isStudy(this.__resourceData) || !osparc.product.Utils.showPublicProjects() ) { - return null; + return; } const canIWrite = osparc.data.model.Study.canIWrite(this.__resourceData["accessRights"]); @@ -830,17 +826,17 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); } - return null; }, - __getCreateTemplatePage: function() { + __addCreateTemplatePage: function() { if ( !osparc.utils.Resources.isStudy(this.__resourceData) || !osparc.product.Utils.showTemplates() ) { - return null; + return; } const canIWrite = osparc.data.model.Study.canIWrite(this.__resourceData["accessRights"]); @@ -869,22 +865,22 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - return page; + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); } - return null; }, - __getCreateFunctionsPage: function() { + __addCreateFunctionsPage: function() { if (!osparc.data.Permissions.getInstance().checkFunctionPermissions("writeFunctions")) { - return null; + return; } if (!osparc.utils.Resources.isStudy(this.__resourceData)) { - return null; + return; } if (!osparc.study.Utils.canCreateFunction(this.__resourceData["workbench"])) { - return null; + return; } const id = "CreateFunction"; @@ -898,10 +894,12 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { toolbar.add(createFunctionButton); page.addToHeader(toolbar); page.addToContent(createFunction); - return page; + + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); }, - __getProjectFilesPopUp: function() { + __addProjectFilesPopUp: function() { const resourceData = this.__resourceData; if (!osparc.utils.Resources.isService(resourceData)) { const title = osparc.product.Utils.resourceTypeToAlias(resourceData["resourceType"], {firstUpperCase: true}) + this.tr(" Files"); @@ -925,7 +923,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } }, - __getActivityOverviewPopUp: function() { + __addActivityOverviewPopUp: function() { const resourceData = this.__resourceData; if (osparc.utils.Resources.isStudy(resourceData)) { const title = this.tr("Activity Overview..."); From bc08fa3c05f5e6a1bada08952058a54ef1caa0b9 Mon Sep 17 00:00:00 2001 From: Giancarlo Romeo Date: Thu, 26 Jun 2025 09:38:16 +0200 Subject: [PATCH 24/26] fix: params order --- .../conversations/_conversation_service.py | 2 +- .../src/simcore_service_webserver/conversations/_socketio.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py b/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py index a14b81b800a..fda9dde006a 100644 --- a/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py +++ b/services/web/server/src/simcore_service_webserver/conversations/_conversation_service.py @@ -112,8 +112,8 @@ async def delete_conversation( app: web.Application, *, product_name: ProductName, - project_id: ProjectID, user_id: UserID, + project_id: ProjectID, conversation_id: ConversationID, ) -> None: await _conversation_repository.delete( diff --git a/services/web/server/src/simcore_service_webserver/conversations/_socketio.py b/services/web/server/src/simcore_service_webserver/conversations/_socketio.py index 2d095243aad..ad232f639e8 100644 --- a/services/web/server/src/simcore_service_webserver/conversations/_socketio.py +++ b/services/web/server/src/simcore_service_webserver/conversations/_socketio.py @@ -150,8 +150,8 @@ async def notify_conversation_deleted( *, recipients: set[UserID], product_name: ProductName, - project_id: ProjectID, user_group_id: GroupID, + project_id: ProjectID, conversation_id: ConversationID, ) -> None: notification_message = SocketMessageDict( From 85a5117695da70287c9ac6a77a86749f93deacb1 Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Thu, 26 Jun 2025 09:40:47 +0200 Subject: [PATCH 25/26] collect garbage --- .../class/osparc/dashboard/ResourceDetails.js | 73 ++++++++++++------- .../class/osparc/study/Conversations.js | 10 ++- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 04c39817728..c164ab94d79 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -21,6 +21,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { construct: function(resourceData) { this.base(arguments); + this.__widgets = []; this.__resourceData = resourceData; let latestPromise = null; @@ -115,6 +116,10 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { ...osparc.ui.window.TabbedWindow.DEFAULT_PROPS, }); resourceDetails.addListener("closeWindow", () => window.close()); + window.addListener("close", () => { + // trigger children's destroy functions + resourceDetails.destroy(); + }); return { resourceDetails, window, @@ -144,6 +149,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { members: { __resourceData: null, __resourceModel: null, + __widgets: null, __infoPage: null, __servicesUpdatePage: null, __conversationsPage: null, @@ -430,6 +436,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } }, + __addPage: function(page) { + const tabsView = this.getChildControl("tabs-view"); + tabsView.add(page); + }, + __addInfoPage: function() { const id = "Information"; const title = this.tr("Overview"); @@ -459,11 +470,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { infoCard.addListener("openClassifiers", () => this.openClassifiers()); infoCard.addListener("openQuality", () => this.openQuality()); page.addToContent(infoCard); + this.__widgets.push(infoCard); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addBillingPage: function() { @@ -498,11 +509,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { }, this); const billingScroll = new qx.ui.container.Scroll(billingSettings); page.addToContent(billingScroll); + this.__widgets.push(billingSettings); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); } else if (osparc.utils.Resources.isService(resourceData)) { const id = "Tiers"; const title = this.tr("Tiers"); @@ -514,11 +525,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const pricingUnitsList = new osparc.service.PricingUnitsList(resourceData); const pricingUnitsListScroll = new qx.ui.container.Scroll(pricingUnitsList); page.addToContent(pricingUnitsListScroll); + this.__widgets.push(pricingUnitsList); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); } }, @@ -547,11 +558,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const resourceModel = this.__resourceModel; const preview = new osparc.study.StudyPreview(resourceModel); page.addToContent(preview); + this.__widgets.push(preview); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addConversationsPage: function() { @@ -569,11 +580,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const lazyLoadContent = () => { const conversations = new osparc.study.Conversations(resourceData); page.addToContent(conversations); + this.__widgets.push(conversations); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addPermissionsPage: function() { @@ -607,11 +618,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { }, this); } page.addToContent(collaboratorsView); + this.__widgets.push(collaboratorsView); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addClassifiersPage: function() { @@ -643,11 +654,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { classifiers = new osparc.metadata.ClassifiersViewer(resourceData); } page.addToContent(classifiers); + this.__widgets.push(classifiers); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addQualityPage: function() { @@ -673,11 +684,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__fireUpdateEvent(updatedData); }); page.addToContent(qualityEditor); + this.__widgets.push(qualityEditor); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); } }, @@ -704,11 +715,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__fireUpdateEvent(resourceData, updatedData); }, this); page.addToContent(tagManager); + this.__widgets.push(tagManager); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addServicesUpdatePage: function() { @@ -734,11 +745,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__fireUpdateEvent(resourceData, updatedData); }); page.addToContent(servicesUpdate); + this.__widgets.push(servicesUpdate); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addServicesBootOptionsPage: function() { @@ -767,6 +778,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__fireUpdateEvent(resourceData, updatedData); }); page.addToContent(servicesBootOpts); + this.__widgets.push(servicesBootOpts); if ( osparc.utils.Resources.isStudy(resourceData) || @@ -788,8 +800,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addPublishPage: function() { @@ -823,11 +834,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { toolbar.add(publishTemplateButton); page.addToHeader(toolbar); page.addToContent(saveAsTemplate); + this.__widgets.push(saveAsTemplate); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); } }, @@ -862,11 +873,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { toolbar.add(createTemplateButton); page.addToHeader(toolbar); page.addToContent(saveAsTemplate); + this.__widgets.push(saveAsTemplate); } page.addListenerOnce("appear", lazyLoadContent, this); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); } }, @@ -894,9 +905,9 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { toolbar.add(createFunctionButton); page.addToHeader(toolbar); page.addToContent(createFunction); + this.__widgets.push(createFunction); - const tabsView = this.getChildControl("tabs-view"); - tabsView.add(page); + this.__addPage(page); }, __addProjectFilesPopUp: function() { @@ -941,5 +952,11 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.addWidgetToTabs(dataAccess); } }, + + // overridden + destroy: function() { + this.__widgets.forEach(w => w.destroy()); + this.base(arguments); + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index a0aba854066..5105a544087 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -52,6 +52,9 @@ qx.Class.define("osparc.study.Conversations", { const viewWidth = 600; const viewHeight = 700; const win = osparc.ui.window.Window.popUpInWindow(conversations, title, viewWidth, viewHeight); + win.addListener("close", () => { + conversations.destroy(); + }, this); return win; }, @@ -361,7 +364,8 @@ qx.Class.define("osparc.study.Conversations", { } }, - destruct: function() { + // overridden + destroy: function() { const socket = osparc.wrapper.WebSocket.getInstance(); if (this.__wsHandlers) { this.__wsHandlers.forEach(({ eventName }) => { @@ -369,6 +373,8 @@ qx.Class.define("osparc.study.Conversations", { }); this.__wsHandlers = null; } + + this.base(arguments); }, - } + }, }); From a2782624605db8d635c7e199fda25cba7d4b183c Mon Sep 17 00:00:00 2001 From: odeimaiz Date: Thu, 26 Jun 2025 10:21:18 +0200 Subject: [PATCH 26/26] minor --- .../client/source/class/osparc/study/Conversations.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 5105a544087..0839624779e 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -339,9 +339,6 @@ qx.Class.define("osparc.study.Conversations", { const conversationsLayout = this.getChildControl("conversations-layout"); conversationsLayout.remove(conversation); this.__conversations = this.__conversations.filter(c => c !== conversation); - - changeSelection - const conversationPages = conversationsLayout.getSelectables(); if (conversationPages.length) { if (changeSelection) {