diff --git a/services/static-webserver/client/source/class/osparc/Application.js b/services/static-webserver/client/source/class/osparc/Application.js index e2e0eb594835..7f2150b47697 100644 --- a/services/static-webserver/client/source/class/osparc/Application.js +++ b/services/static-webserver/client/source/class/osparc/Application.js @@ -135,7 +135,8 @@ qx.Class.define("osparc.Application", { osparc.auth.Manager.getInstance().validateToken() .then(() => { const studyId = urlFragment.nav[1]; - this.__loadMainPage(studyId); + const loadAfterLogin = { studyId }; + this.__loadMainPage(loadAfterLogin); }) .catch(() => this.__loadLoginPage()); } @@ -153,12 +154,27 @@ qx.Class.define("osparc.Application", { if (["anonymous", "guest"].includes(data.role.toLowerCase())) { this.__loadNodeViewerPage(studyId, viewerNodeId); } else { - this.__loadMainPage(studyId); + const loadAfterLogin = { studyId }; + this.__loadMainPage(loadAfterLogin); } }); } break; } + case "conversation": { + // Route: /#/conversation/{id} + if (urlFragment.nav.length > 1) { + osparc.utils.Utils.cookie.deleteCookie("user"); + osparc.auth.Manager.getInstance().validateToken() + .then(() => { + const conversationId = urlFragment.nav[1]; + const loadAfterLogin = { conversationId }; + this.__loadMainPage(loadAfterLogin); + }) + .catch(() => this.__loadLoginPage()); + } + break; + } case "registration": { // Route: /#/registration/?invitation={token} if (urlFragment.params && urlFragment.params.invitation) { @@ -201,9 +217,9 @@ qx.Class.define("osparc.Application", { } break; } - case "form-sandbox": { + case "form-sandbox": this.__loadView(new osparc.desktop.FormSandboxPage(), {}, false); - } + break; } }, @@ -450,7 +466,7 @@ qx.Class.define("osparc.Application", { view.addListener("done", () => this.__restart(), this); }, - __loadMainPage: function(studyId = null) { + __loadMainPage: function(loadAfterLogin = null) { // logged in osparc.WindowSizeTracker.getInstance().evaluateTooSmallDialog(); osparc.data.Resources.getOne("profile") @@ -497,10 +513,17 @@ qx.Class.define("osparc.Application", { }); } - if (studyId) { + if (loadAfterLogin && loadAfterLogin["studyId"]) { + const studyId = loadAfterLogin["studyId"]; osparc.store.Store.getInstance().setCurrentStudyId(studyId); } + if (loadAfterLogin && loadAfterLogin["conversationId"]) { + const conversationId = loadAfterLogin["conversationId"]; + const supportCenterWindow = osparc.support.SupportCenter.openWindow(); + supportCenterWindow.openConversation(conversationId); + } + const loadViewerPage = () => { const mainPage = new osparc.desktop.MainPage(); this.__mainPage = mainPage; diff --git a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js index 8109cfad7b3f..527e6924f761 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js +++ b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js @@ -19,41 +19,48 @@ qx.Class.define("osparc.conversation.AddMessage", { extend: qx.ui.core.Widget, - /** - * @param studyData {Object} serialized Study Data - * @param conversationId {String} Conversation Id - */ - construct: function(studyData, conversationId = null, message = null) { + construct: function() { this.base(arguments); - this.__studyData = studyData; - this.__conversationId = conversationId; - this.__message = message; - this._setLayout(new qx.ui.layout.VBox(5)); this.__buildLayout(); }, + properties: { + conversationId: { + check: "String", + init: null, + nullable: true, + event: "changeConversationId", + }, + + studyData: { + check: "Object", + init: null, + nullable: true, + event: "changeStudyData", + apply: "__applyStudyData", + }, + + message: { + check: "Object", + init: null, + nullable: true, + event: "changeMessage", + apply: "__applyMessage", + } + }, + events: { "messageAdded": "qx.event.type.Data", "messageUpdated": "qx.event.type.Data", }, members: { - __studyData: null, - __conversationId: null, - __message: null, - _createChildControlImpl: function(id) { let control; switch (id) { - case "add-comment-label": - control = new qx.ui.basic.Label().set({ - value: this.tr("Add comment") - }); - this._add(control); - break; case "add-comment-layout": { const grid = new qx.ui.layout.Grid(8, 5); grid.setColumnWidth(0, 32); @@ -65,17 +72,7 @@ qx.Class.define("osparc.conversation.AddMessage", { break; } case "thumbnail": { - control = new qx.ui.basic.Image().set({ - alignY: "middle", - scale: true, - allowGrowX: true, - allowGrowY: true, - allowShrinkX: true, - allowShrinkY: true, - maxWidth: 32, - maxHeight: 32, - decorator: "rounded", - }); + control = osparc.utils.Utils.createThumbnail(32); const authData = osparc.auth.Data.getInstance(); const myUsername = authData.getUsername(); const myEmail = authData.getEmail(); @@ -111,7 +108,7 @@ qx.Class.define("osparc.conversation.AddMessage", { allowGrowX: false, alignX: "right" }); - control.setEnabled(osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])); + control.addListener("execute", this.__addCommentPressed, this); this._add(control); break; case "notify-user-button": @@ -120,7 +117,7 @@ qx.Class.define("osparc.conversation.AddMessage", { allowGrowX: false, alignX: "right" }); - control.setEnabled(osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])); + control.addListener("execute", () => this.__notifyUserTapped()); this._add(control); break; } @@ -130,41 +127,113 @@ qx.Class.define("osparc.conversation.AddMessage", { __buildLayout: function() { this.getChildControl("thumbnail"); - const commentField = this.getChildControl("comment-field"); - - const addMessageButton = this.getChildControl("add-comment-button"); - if (this.__message) { - // edit mode - addMessageButton.setLabel(this.tr("Edit message")); - addMessageButton.addListener("execute", () => this.__editComment()); + this.getChildControl("comment-field"); + this.getChildControl("add-comment-button"); + }, - commentField.setText(this.__message["content"]); + __applyStudyData: function(studyData) { + const notifyUserButton = this.getChildControl("notify-user-button"); + if (studyData) { + const canIWrite = osparc.data.model.Study.canIWrite(studyData["accessRights"]) + this.getChildControl("add-comment-button").setEnabled(canIWrite); + notifyUserButton.show(); + notifyUserButton.setEnabled(canIWrite); } else { - // new message - addMessageButton.addListener("execute", () => this.__addComment()); + notifyUserButton.exclude(); + } + }, - const notifyUserButton = this.getChildControl("notify-user-button"); - notifyUserButton.addListener("execute", () => this.__notifyUserTapped()); + __applyMessage: function(message) { + if (message) { + // edit mode + const commentField = this.getChildControl("comment-field"); + commentField.setText(message["content"]); + + const addMessageButton = this.getChildControl("add-comment-button"); + addMessageButton.setLabel(this.tr("Edit message")); } }, + __addCommentPressed: function() { + this.getMessage() ? this.__editComment() : this.__addComment(); + }, + __addComment: function() { - if (this.__conversationId) { + const conversationId = this.getConversationId(); + if (conversationId) { this.__postMessage(); } else { - // create new conversation first - osparc.store.Conversations.getInstance().addConversation(this.__studyData["uuid"]) + const studyData = this.getStudyData(); + let promise = null; + if (studyData) { + // create new project conversation first + promise = osparc.store.ConversationsProject.getInstance().postConversation(studyData["uuid"]) + } else { + // support conversation + const extraContext = {}; + const currentStudy = osparc.store.Store.getInstance().getCurrentStudy() + if (currentStudy) { + extraContext["projectId"] = currentStudy.getUuid(); + } + promise = osparc.store.ConversationsSupport.getInstance().postConversation(extraContext); + } + promise .then(data => { - this.__conversationId = data["conversationId"]; + this.setConversationId(data["conversationId"]); this.__postMessage(); - }) + }); + } + }, + + __postMessage: function() { + const commentField = this.getChildControl("comment-field"); + const content = commentField.getChildControl("text-area").getValue(); + let promise = null; + if (content) { + const studyData = this.getStudyData(); + const conversationId = this.getConversationId(); + if (studyData) { + promise = osparc.store.ConversationsProject.getInstance().postMessage(studyData["uuid"], conversationId, content); + } else { + promise = osparc.store.ConversationsSupport.getInstance().postMessage(conversationId, content); + } + promise + .then(data => { + this.fireDataEvent("messageAdded", data); + commentField.getChildControl("text-area").setValue(""); + }); } }, + __editComment: function() { + const commentField = this.getChildControl("comment-field"); + const content = commentField.getChildControl("text-area").getValue(); + if (content) { + const studyData = this.getStudyData(); + const conversationId = this.getConversationId(); + const message = this.getMessage(); + if (studyData) { + promise = osparc.store.ConversationsProject.getInstance().editMessage(studyData["uuid"], conversationId, message["messageId"], content); + } else { + promise = osparc.store.ConversationsSupport.getInstance().editMessage(conversationId, message["messageId"], content); + } + promise.then(data => { + this.fireDataEvent("messageUpdated", data); + commentField.getChildControl("text-area").setValue(""); + }); + } + }, + + /* NOTIFY USERS */ __notifyUserTapped: function() { + const studyData = this.getStudyData(); + if (!studyData) { + return; + } + const showOrganizations = false; const showAccessRights = false; - const userManager = new osparc.share.NewCollaboratorsManager(this.__studyData, showOrganizations, showAccessRights).set({ + const userManager = new osparc.share.NewCollaboratorsManager(studyData, showOrganizations, showAccessRights).set({ acceptOnlyOne: true, }); userManager.setCaption(this.tr("Notify user")); @@ -181,10 +250,15 @@ qx.Class.define("osparc.conversation.AddMessage", { }, __notifyUser: function(userGid) { + const studyData = this.getStudyData(); + if (!studyData) { + return; + } + // Note! // This check only works if the project is directly shared with the user. // If it's shared through a group, it might be a bit confusing - if (userGid in this.__studyData["accessRights"]) { + if (userGid in studyData["accessRights"]) { this.__addNotify(userGid); } else { const msg = this.tr("This user has no access to the project. Do you want to share it?"); @@ -200,13 +274,13 @@ qx.Class.define("osparc.conversation.AddMessage", { const newCollaborators = { [userGid]: osparc.data.Roles.STUDY["write"].accessRights }; - osparc.store.Study.getInstance().addCollaborators(this.__studyData, newCollaborators) + osparc.store.Study.getInstance().addCollaborators(studyData, newCollaborators) .then(() => { this.__addNotify(userGid); const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators() if (userGid in potentialCollaborators && "getUserId" in potentialCollaborators[userGid]) { const uid = potentialCollaborators[userGid].getUserId(); - osparc.notification.Notifications.pushStudyShared(uid, this.__studyData["uuid"]); + osparc.notification.Notifications.pushStudyShared(uid, studyData["uuid"]); } }) .catch(err => osparc.FlashMessenger.logError(err)); @@ -216,52 +290,40 @@ qx.Class.define("osparc.conversation.AddMessage", { }, __addNotify: function(userGid) { - if (this.__conversationId) { + const studyData = this.getStudyData(); + if (!studyData) { + return; + } + + const conversationId = this.getConversationId(); + if (conversationId) { this.__postNotify(userGid); } else { // create new conversation first - osparc.store.Conversations.getInstance().addConversation(this.__studyData["uuid"]) + osparc.store.ConversationsProject.getInstance().postConversation(studyData["uuid"]) .then(data => { - this.__conversationId = data["conversationId"]; + this.setConversationId(data["conversationId"]); this.__postNotify(userGid); }); } }, - __postMessage: function() { - const commentField = this.getChildControl("comment-field"); - const content = commentField.getChildControl("text-area").getValue(); - if (content) { - osparc.store.Conversations.getInstance().addMessage(this.__studyData["uuid"], this.__conversationId, content) - .then(data => { - this.fireDataEvent("messageAdded", data); - commentField.getChildControl("text-area").setValue(""); - }); - } - }, - - __editComment: function() { - const commentField = this.getChildControl("comment-field"); - const content = commentField.getChildControl("text-area").getValue(); - if (content) { - osparc.store.Conversations.getInstance().editMessage(this.__studyData["uuid"], this.__conversationId, this.__message["messageId"], content) - .then(data => { - this.fireDataEvent("messageUpdated", data); - commentField.getChildControl("text-area").setValue(""); - }); + __postNotify: function(userGid) { + const studyData = this.getStudyData(); + if (!studyData) { + return; } - }, - __postNotify: function(userGid) { if (userGid) { - osparc.store.Conversations.getInstance().notifyUser(this.__studyData["uuid"], this.__conversationId, userGid) + const conversationId = this.getConversationId(); + osparc.store.ConversationsProject.getInstance().notifyUser(studyData["uuid"], conversationId, userGid) .then(data => { this.fireDataEvent("messageAdded", data); const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators(); if (userGid in potentialCollaborators) { if ("getUserId" in potentialCollaborators[userGid]) { const uid = potentialCollaborators[userGid].getUserId(); - osparc.notification.Notifications.pushConversationNotification(uid, this.__studyData["uuid"]); + osparc.notification.Notifications.pushConversationNotification(uid, studyData["uuid"]); } const msg = "getLabel" in potentialCollaborators[userGid] ? potentialCollaborators[userGid].getLabel() + this.tr(" was notified") : this.tr("Notification sent"); osparc.FlashMessenger.logAs(msg, "INFO"); @@ -269,5 +331,6 @@ qx.Class.define("osparc.conversation.AddMessage", { }); } }, + /* NOTIFY USERS */ } }); 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 8d909d84a8f1..274140943006 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js @@ -20,7 +20,7 @@ qx.Class.define("osparc.conversation.MessageUI", { extend: qx.ui.core.Widget, /** - * @param message {Object} message + * @param message {Object} message data * @param studyData {Object?null} serialized Study Data */ construct: function(message, studyData = null) { @@ -28,7 +28,7 @@ qx.Class.define("osparc.conversation.MessageUI", { this.__studyData = studyData; - const layout = new qx.ui.layout.Grid(12, 4); + const layout = new qx.ui.layout.Grid(12, 2); layout.setColumnFlex(1, 1); // content this._setLayout(layout); this.setPadding(5); @@ -64,11 +64,7 @@ qx.Class.define("osparc.conversation.MessageUI", { let control; switch (id) { case "thumbnail": - control = new qx.ui.basic.Image().set({ - scale: true, - maxWidth: 32, - maxHeight: 32, - decorator: "rounded", + control = osparc.utils.Utils.createThumbnail(32).set({ marginTop: 4, }); this._add(control, { @@ -89,13 +85,15 @@ qx.Class.define("osparc.conversation.MessageUI", { break; case "user-name": control = new qx.ui.basic.Label().set({ - font: "text-12" + font: "text-12", + textColor: "text-disabled", }); this.getChildControl("header-layout").addAt(control, isMyMessage ? 2 : 0); break; case "last-updated": control = new qx.ui.basic.Label().set({ - font: "text-12" + font: "text-12", + textColor: "text-disabled", }); this.getChildControl("header-layout").addAt(control, isMyMessage ? 0 : 2); break; @@ -203,7 +201,11 @@ qx.Class.define("osparc.conversation.MessageUI", { __editMessage: function() { const message = this.getMessage(); - const addMessage = new osparc.conversation.AddMessage(this.__studyData, message["conversationId"], message); + const addMessage = new osparc.conversation.AddMessage().set({ + studyData: this.__studyData, + conversationId: message["conversationId"], + message, + }); const title = this.tr("Edit message"); const win = osparc.ui.window.Window.popUpInWindow(addMessage, title, 570, 135).set({ clickAwayClose: false, @@ -227,7 +229,13 @@ qx.Class.define("osparc.conversation.MessageUI", { win.open(); win.addListener("close", () => { if (win.getConfirmed()) { - osparc.store.Conversations.getInstance().deleteMessage(message) + let promise = null; + if (this.__studyData) { + promise = osparc.store.ConversationsProject.getInstance().deleteMessage(message); + } else { + promise = osparc.store.ConversationsSupport.getInstance().deleteMessage(message); + } + promise .then(() => this.fireDataEvent("messageDeleted", message)) .catch(err => osparc.FlashMessenger.logError(err)); } diff --git a/services/static-webserver/client/source/class/osparc/dashboard/GroupedCardContainer.js b/services/static-webserver/client/source/class/osparc/dashboard/GroupedCardContainer.js index a919ffe61881..b33655305bbd 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/GroupedCardContainer.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/GroupedCardContainer.js @@ -100,15 +100,7 @@ qx.Class.define("osparc.dashboard.GroupedCardContainer", { paddingBottom: 5, allowGrowX: false }); - control.getChildControl("icon").set({ - scale: true, - allowGrowX: true, - allowGrowY: true, - allowShrinkX: true, - allowShrinkY: true, - maxWidth: 32, - maxHeight: 32 - }); + control.getChildControl("icon").set(this.getThumbnailProps(32)); control.getChildControl("label").set({ rich: true, wrap: true diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserFilter.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserFilter.js index df60183ed3d6..54857e7db2d7 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserFilter.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserFilter.js @@ -105,10 +105,18 @@ qx.Class.define("osparc.dashboard.ResourceBrowserFilter", { }); this.__workspacesAndFoldersTree.contextChanged(context); - this.__templatesButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.TEMPLATES); - this.__publicProjectsButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.PUBLIC_TEMPLATES); - this.__functionsButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.FUNCTIONS); - this.__trashButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.TRASH); + if (this.__templatesButton) { + this.__templatesButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.TEMPLATES); + } + if (this.__publicProjectsButton) { + this.__publicProjectsButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.PUBLIC_TEMPLATES); + } + if (this.__functionsButton) { + this.__functionsButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.FUNCTIONS); + } + if (this.__trashButton) { + this.__trashButton.setValue(context === osparc.dashboard.StudyBrowser.CONTEXT.TRASH); + } }, /* WORKSPACES AND FOLDERS */ diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 77789fdcbc29..9f28be3bd152 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -305,10 +305,10 @@ qx.Class.define("osparc.data.Resources", { }, } }, - "conversations": { + "conversationsStudies": { useCache: false, // It has its own cache handler endpoints: { - addConversation: { + postConversation: { method: "POST", url: statics.API + "/projects/{studyId}/conversations" }, @@ -328,7 +328,7 @@ qx.Class.define("osparc.data.Resources", { method: "DELETE", url: statics.API + "/projects/{studyId}/conversations/{conversationId}" }, - addMessage: { + postMessage: { method: "POST", url: statics.API + "/projects/{studyId}/conversations/{conversationId}/messages" }, @@ -1469,7 +1469,52 @@ qx.Class.define("osparc.data.Resources", { url: statics.API + "/wallets/{walletId}/licensed-items-checkouts?offset={offset}&limit={limit}" }, } - } + }, + + /* + * SUPPORT CONVERSATIONS + */ + "conversationsSupport": { + useCache: false, // It has its own cache handler + endpoints: { + postConversation: { + method: "POST", + url: statics.API + "/conversations" + }, + getConversationsPage: { + method: "GET", + url: statics.API + "/conversations?type=SUPPORT&offset={offset}&limit={limit}" + }, + getConversation: { + method: "GET", + url: statics.API + "/conversations/{conversationId}?type=SUPPORT" + }, + renameConversation: { + method: "PATCH", + url: statics.API + "/conversations/{conversationId}?type=SUPPORT" + }, + deleteConversation: { + method: "DELETE", + url: statics.API + "/conversations/{conversationId}" + }, + postMessage: { + method: "POST", + url: statics.API + "/conversations/{conversationId}/messages" + }, + editMessage: { + method: "PUT", + url: statics.API + "/conversations/{conversationId}/messages/{messageId}" + }, + deleteMessage: { + method: "DELETE", + url: statics.API + "/conversations/{conversationId}/messages/{messageId}" + }, + getMessagesPage: { + method: "GET", + url: statics.API + "/conversations/{conversationId}/messages?offset={offset}&limit={limit}" + }, + } + }, }; }, diff --git a/services/static-webserver/client/source/class/osparc/data/model/Conversation.js b/services/static-webserver/client/source/class/osparc/data/model/Conversation.js new file mode 100644 index 000000000000..187f2eed3fe0 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/data/model/Conversation.js @@ -0,0 +1,218 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +/** + * Class that stores Conversation data. + */ + +qx.Class.define("osparc.data.model.Conversation", { + extend: qx.core.Object, + + /** + * @param conversationData {Object} Object containing the serialized Conversation Data + * */ + construct: function(conversationData) { + this.base(arguments); + + this.set({ + conversationId: conversationData.conversationId, + name: conversationData.name, + userGroupId: conversationData.userGroupId, + type: conversationData.type, + created: new Date(conversationData.created), + modified: new Date(conversationData.modified), + projectId: conversationData.projectUuid || null, + extraContext: conversationData.extraContext || null, + }); + + this.__messages = []; + this.__fetchLastMessage(); + }, + + properties: { + conversationId: { + check: "String", + nullable: false, + init: null, + event: "changeConversationId", + }, + + name: { + check: "String", + nullable: false, + init: null, + event: "changeName", + apply: "__applyName", + }, + + userGroupId: { + check: "Number", + nullable: false, + init: null, + event: "changeUserGroupId", + }, + + type: { + check: [ + "PROJECT_STATIC", + "PROJECT_ANNOTATION", + "SUPPORT", + ], + nullable: false, + init: null, + event: "changeType", + }, + + created: { + check: "Date", + nullable: false, + init: null, + event: "changeCreated", + }, + + modified: { + check: "Date", + nullable: false, + init: null, + event: "changeModified", + }, + + projectId: { + check: "String", + nullable: true, + init: null, + event: "changeProjectId", + }, + + extraContext: { + check: "Object", + nullable: true, + init: null, + event: "changeExtraContext", + }, + + nameAlias: { + check: "String", + nullable: false, + init: "", + event: "changeNameAlias", + }, + + lastMessage: { + check: "Object", + nullable: true, + init: null, + event: "changeLastMessage", + apply: "__applyLastMessage", + }, + }, + + members: { + __fetchLastMessagePromise: null, + __nextRequestParams: null, + __messages: null, + + __applyName: function(name) { + if (name && name !== "null") { + this.setNameAlias(name); + } + }, + + __applyLastMessage: function(lastMessage) { + const name = this.getName(); + if (!name || name === "null") { + this.setNameAlias(lastMessage ? lastMessage.content : ""); + } + }, + + __fetchLastMessage: function() { + if (this.__fetchLastMessagePromise) { + return this.__fetchLastMessagePromise; + } + + let promise = osparc.store.ConversationsSupport.getInstance().fetchLastMessage(this.getConversationId()); + promise + .then(lastMessage => { + this.addMessage(lastMessage); + promise = null; + return lastMessage; + }) + .finally(() => { + this.__fetchLastMessagePromise = null; + }); + + this.__fetchLastMessagePromise = promise; + return promise; + }, + + amIOwner: function() { + return this.getUserGroupId() === osparc.auth.Data.getInstance().getGroupId(); + }, + + getNextMessages: function() { + const params = { + url: { + conversationId: this.getConversationId(), + offset: 0, + limit: 42 + } + }; + const nextRequestParams = this.__nextRequestParams; + if (nextRequestParams) { + params.url.offset = nextRequestParams.offset; + params.url.limit = nextRequestParams.limit; + } + const options = { + resolveWResponse: true + }; + return osparc.data.Resources.fetch("conversationsSupport", "getMessagesPage", params, options) + .then(resp => { + const messages = resp["data"]; + messages.forEach(message => this.addMessage(message)); + this.__nextRequestParams = resp["_links"]["next"]; + return resp; + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + renameConversation: function(newName) { + osparc.store.ConversationsSupport.getInstance().renameConversation(this.getConversationId(), newName) + .then(() => { + this.setNameAlias(newName); + }); + }, + + addMessage: function(message) { + if (message) { + const found = this.__messages.find(msg => msg["messageId"] === message["messageId"]); + if (!found) { + this.__messages.push(message); + } + // latest first + this.__messages.sort((a, b) => new Date(b.created) - new Date(a.created)); + this.setLastMessage(this.__messages[0]); + } + }, + + getContextProjectId: function() { + if (this.getExtraContext() && "projectId" in this.getExtraContext()) { + return this.getExtraContext()["projectId"]; + } + return null; + } + }, +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js index a797f06a79aa..18e309fd2c6d 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js @@ -50,7 +50,6 @@ qx.Class.define("osparc.desktop.credits.Utils", { }; }, - areWalletsEnabled: function() { const statics = osparc.store.Store.getInstance().get("statics"); return Boolean(statics && statics["isPaymentEnabled"]); diff --git a/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js b/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js index 0aa96f7781c4..79c8e94f5e51 100644 --- a/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js +++ b/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js @@ -34,6 +34,12 @@ qx.Class.define("osparc.editor.MarkdownEditor", { value: this.tr("Markdown supported"), url: "https://en.wikipedia.org/wiki/Markdown", }); + + this.getChildControl("text-area").set({ + minimalLineHeight: 2, // defaults to 4 lines + maxHeight: 100, // 5 lines + autoSize: true, + }); }, members: { diff --git a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js index 0e5cbad6414e..c33ea53745f6 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js +++ b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js @@ -158,25 +158,23 @@ qx.Class.define("osparc.navigation.NavigationBar", { case "left-items": control = new qx.ui.container.Composite(new qx.ui.layout.HBox(20).set({ alignY: "middle", - alignX: "left" + alignX: "left", })); - this._addAt(control, 0); + this._addAt(control, 0, { flex: 1 }); break; case "center-items": control = new qx.ui.container.Composite(new qx.ui.layout.HBox(10).set({ alignY: "middle", - alignX: "center" + alignX: "center", })); - this._addAt(control, 1, { - flex: 1 - }); + this._addAt(control, 1); break; case "right-items": control = new qx.ui.container.Composite(new qx.ui.layout.HBox(6).set({ alignY: "middle", - alignX: "right" + alignX: "right", })); - this._addAt(control, 2); + this._addAt(control, 2, { flex: 1 }); break; case "logo": control = osparc.navigation.LogoOnOff.getInstance().set({ @@ -369,12 +367,15 @@ qx.Class.define("osparc.navigation.NavigationBar", { position: "top-right", appearance: "menu-wider", }); - const menuButton = new qx.ui.form.MenuButton(null, "@FontAwesome5Regular/question-circle/22", menu).set({ + const menuButton = new qx.ui.form.MenuButton(null, "@FontAwesome5Regular/question-circle/24", menu).set({ backgroundColor: "transparent" }); osparc.utils.Utils.setIdToWidget(menu, "helpNavigationMenu"); + // add support conversations + osparc.store.Support.addSupportConversationsToMenu(menu); + // quick starts and manuals osparc.store.Support.addQuickStartToMenu(menu); osparc.store.Support.addGuidedToursToMenu(menu); diff --git a/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js b/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js index 26250a85f8dd..ec9fba511872 100644 --- a/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js +++ b/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js @@ -427,8 +427,8 @@ qx.Class.define("osparc.node.slideshow.BaseNodeView", { // and show Flash Message const outputs = this.getNode().getOutputs(); if (outputs && Object.keys(outputs).length > 0) { - const flashMsg = this.tr("New Outputs received"); - osparc.FlashMessenger.getInstance().logAs(flashMsg, "INFO"); + const flashMsg = this.tr("New Outputs generated"); + osparc.FlashMessenger.getInstance().logAs(flashMsg, "INFO", 2000); } }); diff --git a/services/static-webserver/client/source/class/osparc/product/Utils.js b/services/static-webserver/client/source/class/osparc/product/Utils.js index 7dc624cbd719..965f50e8ec0c 100644 --- a/services/static-webserver/client/source/class/osparc/product/Utils.js +++ b/services/static-webserver/client/source/class/osparc/product/Utils.js @@ -417,5 +417,9 @@ qx.Class.define("osparc.product.Utils", { groupServices: function() { return Boolean(osparc.store.Products.getInstance().getGroupedServicesUiConfig()); }, + + isSupportEnabled: function() { + return Boolean(osparc.store.Products.getInstance().getSupportGroupId()); + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/store/Conversations.js b/services/static-webserver/client/source/class/osparc/store/ConversationsProject.js similarity index 58% rename from services/static-webserver/client/source/class/osparc/store/Conversations.js rename to services/static-webserver/client/source/class/osparc/store/ConversationsProject.js index 0a75ab2acd88..fc513663524c 100644 --- a/services/static-webserver/client/source/class/osparc/store/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/store/ConversationsProject.js @@ -15,7 +15,7 @@ ************************************************************************ */ -qx.Class.define("osparc.store.Conversations", { +qx.Class.define("osparc.store.ConversationsProject", { extend: qx.core.Object, type: "singleton", @@ -24,6 +24,13 @@ qx.Class.define("osparc.store.Conversations", { "conversationDeleted": "qx.event.type.Data", }, + statics: { + TYPES: { + PROJECT_STATIC: "PROJECT_STATIC", + PROJECT_ANNOTATION: "PROJECT_ANNOTATION", + }, + }, + members: { getConversations: function(studyId) { const params = { @@ -33,7 +40,7 @@ qx.Class.define("osparc.store.Conversations", { limit: 42, } }; - return osparc.data.Resources.fetch("conversations", "getConversationsPage", params) + return osparc.data.Resources.fetch("conversationsStudies", "getConversationsPage", params) .then(conversations => { if (conversations.length) { // Sort conversations by created date, oldest first (the new ones will be next to the plus button) @@ -51,10 +58,10 @@ qx.Class.define("osparc.store.Conversations", { conversationId, } }; - return osparc.data.Resources.fetch("conversations", "getConversation", params); + return osparc.data.Resources.fetch("conversationsStudies", "getConversation", params); }, - addConversation: function(studyId, name = "new 1", type = osparc.study.Conversations.TYPES.PROJECT_STATIC) { + postConversation: function(studyId, name = "new 1", type = osparc.store.ConversationsProject.TYPES.PROJECT_STATIC) { const params = { url: { studyId, @@ -64,7 +71,7 @@ qx.Class.define("osparc.store.Conversations", { type, } }; - return osparc.data.Resources.fetch("conversations", "addConversation", params) + return osparc.data.Resources.fetch("conversationsStudies", "postConversation", params) .catch(err => osparc.FlashMessenger.logError(err)); }, @@ -75,7 +82,7 @@ qx.Class.define("osparc.store.Conversations", { conversationId, }, }; - return osparc.data.Resources.fetch("conversations", "deleteConversation", params) + return osparc.data.Resources.fetch("conversationsStudies", "deleteConversation", params) .then(() => { this.fireDataEvent("conversationDeleted", { studyId, @@ -95,7 +102,7 @@ qx.Class.define("osparc.store.Conversations", { name, } }; - return osparc.data.Resources.fetch("conversations", "renameConversation", params) + return osparc.data.Resources.fetch("conversationsStudies", "renameConversation", params) .then(() => { this.fireDataEvent("conversationRenamed", { studyId, @@ -106,7 +113,7 @@ qx.Class.define("osparc.store.Conversations", { .catch(err => osparc.FlashMessenger.logError(err)); }, - addMessage: function(studyId, conversationId, message) { + postMessage: function(studyId, conversationId, message) { const params = { url: { studyId, @@ -117,7 +124,7 @@ qx.Class.define("osparc.store.Conversations", { "type": "MESSAGE", } }; - return osparc.data.Resources.fetch("conversations", "addMessage", params) + return osparc.data.Resources.fetch("conversationsStudies", "postMessage", params) .catch(err => osparc.FlashMessenger.logError(err)); }, @@ -132,7 +139,7 @@ qx.Class.define("osparc.store.Conversations", { "content": message, }, }; - return osparc.data.Resources.fetch("conversations", "editMessage", params) + return osparc.data.Resources.fetch("conversationsStudies", "editMessage", params) .catch(err => osparc.FlashMessenger.logError(err)); }, @@ -144,7 +151,7 @@ qx.Class.define("osparc.store.Conversations", { messageId: message["messageId"], }, }; - return osparc.data.Resources.fetch("conversations", "deleteMessage", params) + return osparc.data.Resources.fetch("conversationsStudies", "deleteMessage", params) .catch(err => osparc.FlashMessenger.logError(err)); }, @@ -159,47 +166,8 @@ qx.Class.define("osparc.store.Conversations", { "type": "NOTIFICATION", } }; - return osparc.data.Resources.fetch("conversations", "addMessage", params) + return osparc.data.Resources.fetch("conversationsStudies", "postMessage", params) .catch(err => osparc.FlashMessenger.logError(err)); }, - - __addToCache: function(pricingPlanData) { - let pricingPlan = this.__pricingPlansCached.find(f => f.getPricingPlanId() === pricingPlanData["pricingPlanId"]); - if (pricingPlan) { - // put - pricingPlan.set({ - pricingPlanKey: pricingPlanData["pricingPlanKey"], - name: pricingPlanData["displayName"], - description: pricingPlanData["description"], - classification: pricingPlanData["classification"], - isActive: pricingPlanData["isActive"], - }); - } else { - // get and post - pricingPlan = new osparc.data.model.PricingPlan(pricingPlanData); - this.__pricingPlansCached.unshift(pricingPlan); - } - return pricingPlan; - }, - - __addPricingUnitToCache: function(pricingPlan, pricingUnitData) { - const pricingUnits = pricingPlan.getPricingUnits(); - let pricingUnit = pricingUnits ? pricingUnits.find(unit => ("getPricingUnitId" in unit) && unit.getPricingUnitId() === pricingUnitData["pricingUnitId"]) : null; - if (pricingUnit) { - const props = Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.PricingPlan)); - // put - Object.keys(pricingUnitData).forEach(key => { - if (props.includes(key)) { - pricingPlan.set(key, pricingUnitData[key]); - } - }); - } else { - // get and post - pricingUnit = new osparc.data.model.PricingUnit(pricingUnitData); - pricingPlan.bind("classification", pricingUnit, "classification"); - pricingUnits.push(pricingUnit); - } - return pricingUnit; - }, } }); diff --git a/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js b/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js new file mode 100644 index 000000000000..dcf769038536 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js @@ -0,0 +1,205 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.store.ConversationsSupport", { + extend: qx.core.Object, + type: "singleton", + + construct: function() { + this.base(arguments); + + this.__conversationsCached = {}; + }, + + events: { + "conversationCreated": "qx.event.type.Data", + "conversationDeleted": "qx.event.type.Data", + }, + + statics: { + TYPES: { + SUPPORT: "SUPPORT", + }, + }, + + members: { + getConversations: function() { + const params = { + url: { + offset: 0, + limit: 42, + } + }; + return osparc.data.Resources.fetch("conversationsSupport", "getConversationsPage", params) + .then(conversationsData => { + const conversations = []; + if (conversationsData.length) { + // Sort conversations by created date, newest first (the new ones will be next to the plus button) + conversationsData.sort((a, b) => new Date(b["created"]) - new Date(a["created"])); + } + conversationsData.forEach(conversationData => { + const conversation = new osparc.data.model.Conversation(conversationData); + this.__addToCache(conversation); + conversations.push(conversation); + }); + return conversations; + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + getConversation: function(conversationId) { + if (conversationId in this.__conversationsCached) { + return Promise.resolve(this.__conversationsCached[conversationId]); + } + + const params = { + url: { + conversationId, + } + }; + return osparc.data.Resources.fetch("conversationsSupport", "getConversation", params) + .then(conversationData => { + const conversation = new osparc.data.model.Conversation(conversationData); + this.__addToCache(conversation); + return conversation; + }); + }, + + postConversation: function(extraContext = {}) { + const url = window.location.href; + extraContext["deployment"] = url; + extraContext["product"] = osparc.product.Utils.getProductName(); + const params = { + data: { + name: "null", + type: osparc.store.ConversationsSupport.TYPES.SUPPORT, + extraContext, + } + }; + return osparc.data.Resources.fetch("conversationsSupport", "postConversation", params) + .then(conversationData => { + const conversation = new osparc.data.model.Conversation(conversationData); + this.__addToCache(conversation); + this.fireDataEvent("conversationCreated", conversation); + return conversationData; + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + deleteConversation: function(conversationId) { + const params = { + url: { + conversationId, + }, + }; + return osparc.data.Resources.fetch("conversationsSupport", "deleteConversation", params) + .then(() => { + this.fireDataEvent("conversationDeleted", { + conversationId, + }) + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + renameConversation: function(conversationId, name) { + const params = { + url: { + conversationId, + }, + data: { + name, + } + }; + return osparc.data.Resources.fetch("conversationsSupport", "renameConversation", params); + }, + + fetchLastMessage: function(conversationId) { + if ( + conversationId in this.__conversationsCached && + this.__conversationsCached[conversationId].getLastMessage() + ) { + return Promise.resolve(this.__conversationsCached[conversationId].getLastMessage()); + } + + const params = { + url: { + conversationId, + offset: 0, + limit: 1, + } + }; + return osparc.data.Resources.fetch("conversationsSupport", "getMessagesPage", params) + .then(messagesData => { + if (messagesData && messagesData.length) { + const lastMessage = messagesData[0]; + this.__addMessageToCache(conversationId, lastMessage); + return lastMessage; + } + return null; + }); + }, + + postMessage: function(conversationId, message) { + const params = { + url: { + conversationId, + }, + data: { + "content": message, + "type": "MESSAGE", + } + }; + return osparc.data.Resources.fetch("conversationsSupport", "postMessage", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + editMessage: function(conversationId, messageId, message) { + const params = { + url: { + conversationId, + messageId, + }, + data: { + "content": message, + }, + }; + return osparc.data.Resources.fetch("conversationsSupport", "editMessage", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + deleteMessage: function(message) { + const params = { + url: { + conversationId: message["conversationId"], + messageId: message["messageId"], + }, + }; + return osparc.data.Resources.fetch("conversationsSupport", "deleteMessage", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + __addToCache: function(conversation) { + this.__conversationsCached[conversation.getConversationId()] = conversation; + }, + + __addMessageToCache: function(conversationId, messageData) { + if (conversationId in this.__conversationsCached) { + this.__conversationsCached[conversationId].addMessage(messageData); + } + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/store/Groups.js b/services/static-webserver/client/source/class/osparc/store/Groups.js index 2aa6794a6871..b040129d5a83 100644 --- a/services/static-webserver/client/source/class/osparc/store/Groups.js +++ b/services/static-webserver/client/source/class/osparc/store/Groups.js @@ -38,7 +38,8 @@ qx.Class.define("osparc.store.Groups", { organizations: { check: "Object", - init: {} + init: {}, + event: "organizationsChanged", }, groupMe: { @@ -47,11 +48,6 @@ qx.Class.define("osparc.store.Groups", { }, }, - events: { - "groupAdded": "qx.event.type.Data", - "groupRemoved": "qx.event.type.Data", - }, - statics: { curateOrderBy: function(orderBy) { const curatedOrderBy = osparc.utils.Utils.deepCloneObject(orderBy); diff --git a/services/static-webserver/client/source/class/osparc/store/Products.js b/services/static-webserver/client/source/class/osparc/store/Products.js index c62da0be5ee5..586c7c8ee5be 100644 --- a/services/static-webserver/client/source/class/osparc/store/Products.js +++ b/services/static-webserver/client/source/class/osparc/store/Products.js @@ -125,5 +125,20 @@ qx.Class.define("osparc.store.Products", { getGroupedServicesUiConfig: function() { return this.__uiConfig["groupedServices"]; }, + + getSupportGroupId: function() { + const statics = osparc.store.Store.getInstance().get("statics"); + if (statics["supportStandardGroupId"]) { + return statics["supportStandardGroupId"]; + } + return null; + }, + + amIASupportUser: function() { + const supportGroupId = this.getSupportGroupId(); + const groupsStore = osparc.store.Groups.getInstance(); + const myGroupIds = groupsStore.getOrganizationIds().map(gId => parseInt(gId)); + return (supportGroupId && myGroupIds.includes(supportGroupId)); + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/store/Store.js b/services/static-webserver/client/source/class/osparc/store/Store.js index 145febf17548..3958026cb6c3 100644 --- a/services/static-webserver/client/source/class/osparc/store/Store.js +++ b/services/static-webserver/client/source/class/osparc/store/Store.js @@ -103,7 +103,7 @@ qx.Class.define("osparc.store.Store", { check: "Array", init: [] }, - conversations: { + conversationsStudies: { check: "Array", init: [] }, @@ -248,6 +248,10 @@ qx.Class.define("osparc.store.Store", { check: "Array", init: null, }, + conversationsSupport: { + check: "Array", + init: [] + }, }, members: { diff --git a/services/static-webserver/client/source/class/osparc/store/Support.js b/services/static-webserver/client/source/class/osparc/store/Support.js index 13c8cbd8d051..475a6b3d922a 100644 --- a/services/static-webserver/client/source/class/osparc/store/Support.js +++ b/services/static-webserver/client/source/class/osparc/store/Support.js @@ -19,6 +19,26 @@ qx.Class.define("osparc.store.Support", { return osparc.store.VendorInfo.getInstance().getManuals(); }, + addSupportConversationsToMenu: function(menu) { + if (osparc.product.Utils.isSupportEnabled()) { + const supportCenterButton = new qx.ui.menu.Button().set({ + icon: "@FontAwesome5Regular/question-circle/16", + }); + const amISupporter = () => { + const isSupportUser = osparc.store.Products.getInstance().amIASupportUser(); + supportCenterButton.set({ + label: isSupportUser ? qx.locale.Manager.tr("Support Center") : qx.locale.Manager.tr("Support"), + }); + }; + amISupporter(); + osparc.store.Groups.getInstance().addListener("organizationsChanged", () => amISupporter()); + supportCenterButton.addListener("execute", () => { + osparc.support.SupportCenter.openWindow(); + }); + menu.add(supportCenterButton); + } + }, + addQuickStartToMenu: function(menu) { const quickStart = osparc.product.quickStart.Utils.getQuickStart(); if (quickStart) { diff --git a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js b/services/static-webserver/client/source/class/osparc/study/Conversation.js similarity index 92% rename from services/static-webserver/client/source/class/osparc/conversation/Conversation.js rename to services/static-webserver/client/source/class/osparc/study/Conversation.js index f60771f8bf57..e1a776be99d1 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversation.js @@ -16,7 +16,7 @@ ************************************************************************ */ -qx.Class.define("osparc.conversation.Conversation", { +qx.Class.define("osparc.study.Conversation", { extend: qx.ui.tabview.Page, /** @@ -87,11 +87,11 @@ qx.Class.define("osparc.conversation.Conversation", { titleEditor.close(); const newLabel = e.getData()["newLabel"]; if (this.getConversationId()) { - osparc.store.Conversations.getInstance().renameConversation(this.__studyData["uuid"], this.getConversationId(), newLabel) + osparc.store.ConversationsProject.getInstance().renameConversation(this.__studyData["uuid"], this.getConversationId(), newLabel) .then(() => this.renameConversation(newLabel)); } else { // create new conversation first - osparc.store.Conversations.getInstance().addConversation(this.__studyData["uuid"], newLabel) + osparc.store.ConversationsProject.getInstance().postConversation(this.__studyData["uuid"], newLabel) .then(data => { this.setConversationId(data["conversationId"]); this.getChildControl("button").setLabel(newLabel); @@ -114,7 +114,7 @@ qx.Class.define("osparc.conversation.Conversation", { }); closeButton.addListener("execute", () => { if (this.__messagesList.getChildren().length === 0) { - osparc.store.Conversations.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); + osparc.store.ConversationsProject.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); } else { const msg = this.tr("Are you sure you want to delete the conversation?"); const confirmationWin = new osparc.ui.window.Confirmation(msg).set({ @@ -125,7 +125,7 @@ qx.Class.define("osparc.conversation.Conversation", { confirmationWin.open(); confirmationWin.addListener("close", () => { if (confirmationWin.getConfirmed()) { - osparc.store.Conversations.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); + osparc.store.ConversationsProject.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); } }, this); } @@ -166,7 +166,9 @@ qx.Class.define("osparc.conversation.Conversation", { this.__loadMoreMessages.addListener("execute", () => this.__reloadMessages(false)); this._add(this.__loadMoreMessages); - const addMessages = new osparc.conversation.AddMessage(this.__studyData, this.getConversationId()).set({ + const addMessages = new osparc.conversation.AddMessage().set({ + studyData: this.__studyData, + conversationId: this.getConversationId(), enabled: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]), paddingLeft: 10, }); @@ -197,7 +199,7 @@ qx.Class.define("osparc.conversation.Conversation", { const options = { resolveWResponse: true }; - return osparc.data.Resources.fetch("conversations", "getMessagesPage", params, options) + return osparc.data.Resources.fetch("conversationsStudies", "getMessagesPage", params, options) .catch(err => osparc.FlashMessenger.logError(err)); }, 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 c8dd29f9ae93..e032314d7090 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -47,11 +47,6 @@ qx.Class.define("osparc.study.Conversations", { }, statics: { - TYPES: { - PROJECT_STATIC: "PROJECT_STATIC", - PROJECT_ANNOTATION: "PROJECT_ANNOTATION", - }, - CHANNELS: { CONVERSATION_CREATED: "conversation:created", CONVERSATION_UPDATED: "conversation:updated", @@ -125,7 +120,9 @@ qx.Class.define("osparc.study.Conversations", { if (conversation) { switch (eventName) { case this.self().CHANNELS.CONVERSATION_CREATED: - this.__addConversationPage(conversation); + if (conversation["projectId"] === this.getStudyData()["uuid"]) { + this.__addConversationPage(conversation); + } break; case this.self().CHANNELS.CONVERSATION_UPDATED: this.__updateConversationName(conversation); @@ -177,7 +174,7 @@ qx.Class.define("osparc.study.Conversations", { const loadMoreButton = this.getChildControl("loading-button"); loadMoreButton.setFetching(true); - osparc.store.Conversations.getInstance().getConversations(studyData["uuid"]) + osparc.store.ConversationsProject.getInstance().getConversations(studyData["uuid"]) .then(conversations => { if (conversations.length) { conversations.forEach(conversation => this.__addConversationPage(conversation)); @@ -204,9 +201,9 @@ qx.Class.define("osparc.study.Conversations", { let conversationPage = null; if (conversationData) { const conversationId = conversationData["conversationId"]; - conversationPage = new osparc.conversation.Conversation(studyData, conversationId); + conversationPage = new osparc.study.Conversation(studyData, conversationId); conversationPage.setLabel(conversationData["name"]); - osparc.store.Conversations.getInstance().addListener("conversationDeleted", e => { + osparc.store.ConversationsProject.getInstance().addListener("conversationDeleted", e => { const data = e.getData(); if (conversationId === data["conversationId"]) { this.__removeConversationPage(conversationId, true); @@ -214,7 +211,7 @@ qx.Class.define("osparc.study.Conversations", { }); } else { // create a temporary conversation - conversationPage = new osparc.conversation.Conversation(studyData); + conversationPage = new osparc.study.Conversation(studyData); conversationPage.setLabel(this.tr("new")); } return conversationPage; @@ -262,7 +259,7 @@ qx.Class.define("osparc.study.Conversations", { enabled: osparc.data.model.Study.canIWrite(studyData["accessRights"]), }); newConversationButton.addListener("execute", () => { - osparc.store.Conversations.getInstance().addConversation(studyData["uuid"], "new " + (this.__conversationsPages.length + 1)) + osparc.store.ConversationsProject.getInstance().postConversation(studyData["uuid"], "new " + (this.__conversationsPages.length + 1)) .then(conversationDt => { this.__addConversationPage(conversationDt); const newConversationPage = this.__getConversationPage(conversationDt["conversationId"]); diff --git a/services/static-webserver/client/source/class/osparc/support/Conversation.js b/services/static-webserver/client/source/class/osparc/support/Conversation.js new file mode 100644 index 000000000000..7477601c0e34 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/support/Conversation.js @@ -0,0 +1,313 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.support.Conversation", { + extend: qx.ui.core.Widget, + + /** + * @param conversation {osparc.data.model.Conversation} Conversation + */ + construct: function(conversation) { + this.base(arguments); + + this.__messages = []; + + this._setLayout(new qx.ui.layout.VBox(5)); + + this.__buildLayout(); + + if (conversation) { + this.setConversation(conversation); + } + }, + + properties: { + conversation: { + check: "osparc.data.model.Conversation", + init: null, + nullable: true, + event: "changeConversation", + apply: "__applyConversation", + }, + + studyId: { + check: "String", + init: null, + nullable: true, + event: "changeStudyId", + apply: "__applyStudyId", + }, + }, + + members: { + __messages: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "spacer-top": + control = new qx.ui.core.Spacer(); + this._addAt(control, 0, { + flex: 100 // high number to keep even a one message list at the bottom + }); + break; + case "messages-container-scroll": + control = new qx.ui.container.Scroll(); + this._addAt(control, 1, { + flex: 1 + }); + break; + case "messages-container": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({ + alignY: "middle" + }); + this.getChildControl("messages-container-scroll").add(control); + break; + case "load-more-button": + control = new osparc.ui.form.FetchButton(this.tr("Load more messages...")); + control.addListener("execute", () => this.__reloadMessages(false)); + this._addAt(control, 2); + break; + case "support-suggestion": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({ + alignY: "middle" + }); + this._addAt(control, 3); + break; + case "add-message": + control = new osparc.conversation.AddMessage().set({ + padding: 5, + }); + this.bind("conversation", control, "conversationId", { + converter: conversation => conversation ? conversation.getConversationId() : null + }); + // make it more compact + control.getChildControl("comment-field").getChildControl("tabs").getChildControl("bar").exclude(); + control.getChildControl("comment-field").getChildControl("subtitle").exclude(); + this._addAt(control, 4); + break; + case "share-project-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox()).set({ + backgroundColor: "strong-main", + decorator: "rounded", + }); + this._addAt(control, 5); + break; + case "share-project-checkbox": + control = new qx.ui.form.CheckBox().set({ + value: false, + label: this.tr("Share Project with Support"), + textColor: "white", + padding: 3, + }); + this.getChildControl("share-project-layout").add(new qx.ui.core.Spacer(), { flex: 1 }); + this.getChildControl("share-project-layout").add(control); + this.getChildControl("share-project-layout").add(new qx.ui.core.Spacer(), { flex: 1 }); + break; + } + return control || this.base(arguments, id); + }, + + __buildLayout: function() { + this.getChildControl("spacer-top"); + this.getChildControl("messages-container"); + const addMessages = this.getChildControl("add-message"); + addMessages.addListener("messageAdded", e => { + const data = e.getData(); + if (data["conversationId"] && this.getConversation() === null) { + osparc.store.ConversationsSupport.getInstance().getConversation(data["conversationId"]) + .then(conversation => { + this.setConversation(conversation); + }); + } else { + this.getConversation().addMessage(data); + this.addMessage(data); + } + }); + }, + + __applyConversation: function(conversation) { + this.__reloadMessages(true); + + const shareProjectCB = this.getChildControl("share-project-checkbox"); + const shareProjectLayout = this.getChildControl("share-project-layout"); + const currentStudy = osparc.store.Store.getInstance().getCurrentStudy(); + let showCB = false; + let enabledCB = false; + if (conversation === null && currentStudy) { + // initiating conversation + showCB = true; + enabledCB = true; + } else if (conversation) { + // it was already set + showCB = conversation.getContextProjectId(); + enabledCB = conversation.amIOwner(); + } + shareProjectLayout.set({ + visibility: showCB ? "visible" : "excluded", + enabled: enabledCB, + }); + + if (conversation && conversation.getContextProjectId()) { + const projectId = conversation.getContextProjectId(); + osparc.store.Study.getInstance().getOne(projectId) + .then(studyData => { + let isAlreadyShared = false; + const accessRights = studyData["accessRights"]; + const supportGroupId = osparc.store.Products.getInstance().getSupportGroupId(); + if (supportGroupId && supportGroupId in accessRights) { + isAlreadyShared = true; + } else { + isAlreadyShared = false; + } + shareProjectCB.setValue(isAlreadyShared); + shareProjectCB.removeListener("changeValue", this.__shareProjectWithSupport, this); + if (showCB) { + shareProjectCB.addListener("changeValue", this.__shareProjectWithSupport, this); + } + }); + } + }, + + __shareProjectWithSupport: function(e) { + const share = e.getData(); + const supportGroupId = osparc.store.Products.getInstance().getSupportGroupId(); + const projectId = this.getConversation().getContextProjectId(); + osparc.store.Study.getInstance().getOne(projectId) + .then(studyData => { + if (share) { + const newCollaborators = { + [supportGroupId]: osparc.data.Roles.STUDY["write"].accessRights + }; + osparc.store.Study.getInstance().addCollaborators(studyData, newCollaborators) + } else { + osparc.store.Study.getInstance().removeCollaborator(studyData, supportGroupId); + } + }); + }, + + __reloadMessages: function(removeMessages = true) { + const messagesContainer = this.getChildControl("messages-container"); + const loadMoreMessages = this.getChildControl("load-more-button"); + if (this.getConversation() === null) { + messagesContainer.hide(); + loadMoreMessages.hide(); + return; + } + + messagesContainer.show(); + loadMoreMessages.show(); + loadMoreMessages.setFetching(true); + + if (removeMessages) { + this.__messages = []; + messagesContainer.removeAll(); + } + + this.getConversation().getNextMessages() + .then(resp => { + const messages = resp["data"]; + messages.forEach(message => this.addMessage(message)); + if (resp["_links"]["next"] === null && loadMoreMessages) { + loadMoreMessages.exclude(); + } + }) + .finally(() => loadMoreMessages.setFetching(false)); + }, + + addMessage: function(message) { + // ignore it if it was already there + const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]); + if (messageIndex !== -1) { + return; + } + + // determine insertion index for latest‐first order + const newTime = new Date(message["created"]); + let insertAt = this.__messages.findIndex(m => new Date(m["created"]) > newTime); + if (insertAt === -1) { + insertAt = this.__messages.length; + } + + // Insert the message in the messages array + this.__messages.splice(insertAt, 0, message); + + // Add the UI element to the messages list + let control = null; + switch (message["type"]) { + case "MESSAGE": + control = new osparc.conversation.MessageUI(message); + control.addListener("messageUpdated", e => this.updateMessage(e.getData())); + control.addListener("messageDeleted", e => this.deleteMessage(e.getData())); + break; + case "NOTIFICATION": + control = new osparc.conversation.NotificationUI(message); + break; + } + if (control) { + // insert into the UI at the same position + const messagesContainer = this.getChildControl("messages-container"); + messagesContainer.addAt(control, insertAt); + } + + // scroll to bottom + // add timeout to ensure the scroll happens after the UI is updated + setTimeout(() => { + const messagesScroll = this.getChildControl("messages-container-scroll"); + messagesScroll.scrollToY(messagesScroll.getChildControl("pane").getScrollMaxY()); + }, 50); + }, + + deleteMessage: function(message) { + // remove it from the messages array + const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]); + if (messageIndex === -1) { + return; + } + this.__messages.splice(messageIndex, 1); + + // Remove the UI element from the messages list + const messagesContainer = this.getChildControl("messages-container"); + const children = messagesContainer.getChildren(); + const controlIndex = children.findIndex( + ctrl => ("getMessage" in ctrl && ctrl.getMessage()["messageId"] === message["messageId"]) + ); + if (controlIndex > -1) { + messagesContainer.remove(children[controlIndex]); + } + }, + + updateMessage: function(message) { + // Replace the message in the messages array + const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]); + if (messageIndex === -1) { + return; + } + this.__messages[messageIndex] = message; + + // Update the UI element from the messages list + const messagesContainer = this.getChildControl("messages-container"); + messagesContainer.getChildren().forEach(control => { + if ("getMessage" in control && control.getMessage()["messageId"] === message["messageId"]) { + control.setMessage(message); + return; + } + }); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js b/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js new file mode 100644 index 000000000000..727525770f61 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js @@ -0,0 +1,75 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.support.ConversationListItem", { + extend: osparc.ui.list.ListItem, + + construct: function() { + this.base(arguments); + + const layout = this._getLayout(); + layout.setSpacingX(10); + layout.setSpacingY(0); + + // decorate + this.getChildControl("thumbnail").getContentElement().setStyles({ + "border-radius": "16px" + }); + this.getChildControl("subtitle").set({ + textColor: "text-disabled", + }); + }, + + properties: { + conversation: { + check: "osparc.data.model.Conversation", + init: null, + nullable: false, + event: "changeConversation", + apply: "__applyConversation", + }, + }, + + members: { + __applyConversation: function(conversation) { + conversation.bind("nameAlias", this, "title"); + + this.__populateWithLastMessage(); + conversation.addListener("changeLastMessage", this.__populateWithLastMessage, this); + }, + + __populateWithLastMessage: function() { + const lastMessage = this.getConversation().getLastMessage(); + if (lastMessage) { + const date = osparc.utils.Utils.formatDateAndTime(new Date(lastMessage.created)); + this.set({ + subtitle: date, + }); + const userGroupId = lastMessage.userGroupId; + osparc.store.Users.getInstance().getUser(userGroupId) + .then(user => { + if (user) { + this.set({ + thumbnail: user.getThumbnail(), + subtitle: user.getLabel() + " - " + date, + }); + } + }); + } + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/support/ConversationPage.js b/services/static-webserver/client/source/class/osparc/support/ConversationPage.js new file mode 100644 index 000000000000..d01e3cfb06e3 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/support/ConversationPage.js @@ -0,0 +1,211 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.support.ConversationPage", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this.__messages = []; + + this._setLayout(new qx.ui.layout.VBox(5)); + + this.getChildControl("back-button"); + + const conversation = this.getChildControl("conversation-content"); + this.bind("conversation", conversation, "conversation"); + conversation.bind("conversation", this, "conversation"); + }, + + properties: { + conversation: { + check: "osparc.data.model.Conversation", + init: null, + nullable: true, + event: "changeConversation", + apply: "__applyConversation", + }, + }, + + events: { + "showConversations": "qx.event.type.Event", + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "conversation-header-layout": { + const headerLayout = new qx.ui.layout.HBox(5).set({ + alignY: "middle", + }) + control = new qx.ui.container.Composite(headerLayout).set({ + padding: 5, + }); + this._add(control); + break; + } + case "back-button": + control = new qx.ui.form.Button().set({ + toolTipText: this.tr("Return to Messages"), + icon: "@FontAwesome5Solid/arrow-left/16", + backgroundColor: "transparent" + }); + control.addListener("execute", () => this.fireEvent("showConversations")); + this.getChildControl("conversation-header-layout").addAt(control, 0); + break; + case "conversation-header-center-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)); + this.getChildControl("conversation-header-layout").addAt(control, 1, { + flex: 1, + }); + break; + case "conversation-title": + control = new qx.ui.basic.Label().set({ + font: "text-14", + alignY: "middle", + allowGrowX: true, + }); + this.getChildControl("conversation-header-center-layout").addAt(control, 0); + break; + case "conversation-extra-content": + control = new qx.ui.basic.Label().set({ + font: "text-12", + textColor: "text-disabled", + rich: true, + allowGrowX: true, + selectable: true, + }); + this.getChildControl("conversation-header-center-layout").addAt(control, 1); + break; + case "open-project-button": + control = new qx.ui.form.Button().set({ + maxWidth: 26, + maxHeight: 24, + alignX: "center", + alignY: "middle", + icon: "@FontAwesome5Solid/external-link-alt/12", + }); + control.addListener("execute", () => this.__openProjectDetails()); + this.getChildControl("conversation-header-layout").addAt(control, 2); + break; + case "conversation-options": { + control = new qx.ui.form.MenuButton().set({ + maxWidth: 24, + maxHeight: 24, + alignX: "center", + alignY: "middle", + icon: "@FontAwesome5Solid/ellipsis-v/12", + }); + const menu = new qx.ui.menu.Menu().set({ + position: "bottom-right", + }); + control.setMenu(menu); + const renameButton = new qx.ui.menu.Button().set({ + label: this.tr("Rename"), + icon: "@FontAwesome5Solid/i-cursor/10" + }); + renameButton.addListener("execute", () => this.__renameConversation()); + menu.add(renameButton); + this.getChildControl("conversation-header-layout").addAt(control, 3); + break; + } + case "conversation-content": + control = new osparc.support.Conversation(); + const scroll = new qx.ui.container.Scroll(); + scroll.add(control); + this._add(scroll, { + flex: 1, + }); + break; + } + return control || this.base(arguments, id); + }, + + __applyConversation: function(conversation) { + const title = this.getChildControl("conversation-title"); + if (conversation) { + conversation.bind("nameAlias", title, "value"); + } else { + title.setValue(this.tr("Ask a Question")); + } + + const extraContextLabel = this.getChildControl("conversation-extra-content"); + const amISupporter = osparc.store.Products.getInstance().amIASupportUser(); + if (conversation && amISupporter) { + const extraContext = conversation.getExtraContext(); + if (extraContext && Object.keys(extraContext).length) { + let extraContextText = `Support ID: ${conversation.getConversationId()}`; + const contextProjectId = conversation.getContextProjectId(); + if (contextProjectId) { + extraContextText += `
Project ID: ${contextProjectId}`; + } + extraContextLabel.setValue(extraContextText); + } + extraContextLabel.show(); + } else { + extraContextLabel.exclude(); + } + + const openButton = this.getChildControl("open-project-button"); + if (conversation && conversation.getContextProjectId()) { + openButton.show(); + } else { + openButton.exclude(); + } + + const options = this.getChildControl("conversation-options"); + if (conversation && conversation.amIOwner()) { + options.show(); + } else { + options.exclude(); + } + }, + + __openProjectDetails: function() { + const projectId = this.getConversation().getContextProjectId(); + if (projectId) { + osparc.store.Study.getInstance().getOne(projectId) + .then(studyData => { + if (studyData) { + const studyDataCopy = osparc.data.model.Study.deepCloneStudyObject(studyData); + studyDataCopy["resourceType"] = "study"; + osparc.dashboard.ResourceDetails.popUpInWindow(studyDataCopy); + } + }) + .catch(err => console.warn(err)); + } + }, + + __renameConversation: function() { + let oldName = this.getConversation().getName(); + if (oldName === "null") { + oldName = ""; + } + const renamer = new osparc.widget.Renamer(oldName); + renamer.addListener("labelChanged", e => { + renamer.close(); + const newLabel = e.getData()["newLabel"]; + this.getConversation().renameConversation(newLabel); + }, this); + renamer.center(); + renamer.open(); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/support/Conversations.js b/services/static-webserver/client/source/class/osparc/support/Conversations.js new file mode 100644 index 000000000000..076656d13760 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/support/Conversations.js @@ -0,0 +1,116 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.support.Conversations", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(10)); + + this.__noConversationsLabel = new qx.ui.basic.Label("No conversations yet — your messages will appear here.").set({ + padding: 5, + }); + this.__conversationListItems = []; + + this.__fetchConversations(); + + this.__listenToNewConversations(); + }, + + events: { + "openConversation": "qx.event.type.Data", + }, + + members: { + __noConversationsLabel: null, + __conversationListItems: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "loading-button": + control = new osparc.ui.form.FetchButton(); + this._add(control); + break; + case "conversations-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)); + this._add(control, { + flex: 1 + }); + break; + } + + return control || this.base(arguments, id); + }, + + __getConversationItem: function(conversationId) { + return this.__conversationListItems.find(conversation => conversation.getConversation().getConversationId() === conversationId); + }, + + __fetchConversations: function() { + const loadMoreButton = this.getChildControl("loading-button"); + loadMoreButton.setFetching(true); + + osparc.store.ConversationsSupport.getInstance().getConversations() + .then(conversations => { + if (conversations.length) { + conversations.forEach(conversation => this.__addConversation(conversation)); + } else { + // No conversations found + this.getChildControl("conversations-layout").add(this.__noConversationsLabel); + } + }) + .finally(() => { + loadMoreButton.setFetching(false); + loadMoreButton.exclude(); + }); + }, + + __listenToNewConversations: function() { + osparc.store.ConversationsSupport.getInstance().addListener("conversationCreated", e => { + const conversation = e.getData(); + this.__addConversation(conversation); + }); + }, + + __addConversation: function(conversation) { + const conversationsLayout = this.getChildControl("conversations-layout"); + // remove the noConversationsLabel + if (conversationsLayout && conversationsLayout.getChildren().indexOf(this.__noConversationsLabel) > -1) { + conversationsLayout.remove(this.__noConversationsLabel); + } + + // ignore it if it was already there + const conversationId = conversation.getConversationId(); + const conversationItemFound = this.__getConversationItem(conversationId); + if (conversationItemFound) { + return null; + } + + const conversationListItem = new osparc.support.ConversationListItem(); + conversationListItem.setConversation(conversation); + conversationListItem.addListener("tap", () => this.fireDataEvent("openConversation", conversationId, this)); + conversationsLayout.add(conversationListItem); + this.__conversationListItems.push(conversationListItem); + + return conversationListItem; + }, + }, +}); diff --git a/services/static-webserver/client/source/class/osparc/support/SuggestedQuestion.js b/services/static-webserver/client/source/class/osparc/support/SuggestedQuestion.js new file mode 100644 index 000000000000..0ae7b8dbd526 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/support/SuggestedQuestion.js @@ -0,0 +1,84 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.support.SuggestedQuestion", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + const layout = new qx.ui.layout.Grid(12, 4); + layout.setColumnFlex(1, 1); // content + this._setLayout(layout); + this.setPadding(5); + }, + + events: { + "questionAnswered": "qx.event.type.Data", + }, + + members: { + __addProductThumbnail: function() { + const thumbnail = osparc.utils.Utils.createThumbnail(32).set({ + source: osparc.product.Utils.getIconUrl(), + }); + this._add(thumbnail, { + row: 0, + column: 0, + }); + }, + + __addQuestionLabel: function(text) { + const question = new qx.ui.basic.Label(text); + this._add(question, { + row: 0, + column: 1, + }); + }, + + __addAnswers: function(answers) { + const answersContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + answers.forEach(answer => { + const button = new qx.ui.form.Button(answer.label).set({ + appearance: "strong-button", + allowGrowX: false, + }); + button.addListener("execute", () => this.fireDataEvent("questionAnswered", answer.key)); + answersContainer.add(button); + }); + this._add(answersContainer, { + row: 1, + column: 1, + }); + }, + + isProjectRelated: function(answers) { + this._removeAll(); + this.__addProductThumbnail(); + this.__addQuestionLabel(this.tr("Is your question related to the current project?")); + this.__addAnswers(answers); + }, + + shareProject: function() { + this._removeAll(); + this.__addProductThumbnail(); + this.__addQuestionLabel(this.tr("Do you want to share this project with Support?")); + this.__addAnswers(answers); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/support/SupportCenter.js b/services/static-webserver/client/source/class/osparc/support/SupportCenter.js new file mode 100644 index 000000000000..ef5eee883faf --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/support/SupportCenter.js @@ -0,0 +1,158 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.support.SupportCenter", { + extend: osparc.ui.window.SingletonWindow, + + construct: function() { + this.base(arguments, "support-center", "Support"); + + this.getChildControl("title").set({ + textAlign: "center", + }); + + this.set({ + layout: new qx.ui.layout.VBox(10), + width: osparc.support.SupportCenter.WINDOW_WIDTH, + height: osparc.support.SupportCenter.getMaxHeight(), + modal: false, + showMaximize: false, + showMinimize: false, + showClose: true, + }); + + this.getChildControl("conversations-intro-text"); + this.getChildControl("conversations-list"); + if (!osparc.store.Products.getInstance().amIASupportUser()) { + this.getChildControl("ask-a-question-button"); + } + }, + + statics: { + WINDOW_WIDTH: 430, + + getMaxHeight: function() { + // height: max 80% of screen, or 600px + const clientHeight = document.documentElement.clientHeight; + return Math.min(600, parseInt(clientHeight * 0.8)); + }, + + openWindow: function() { + const supportCenterWindow = new osparc.support.SupportCenter(); + + const positionWindow = () => { + supportCenterWindow.set({ + height: osparc.support.SupportCenter.getMaxHeight(), + }); + // bottom right + const clientWidth = document.documentElement.clientWidth; + const clientHeight = document.documentElement.clientHeight; + const posX = clientWidth - osparc.support.SupportCenter.WINDOW_WIDTH - 4; + const posY = clientHeight - supportCenterWindow.getHeight() - 4; + supportCenterWindow.moveTo(posX, posY); + }; + supportCenterWindow.open(); + positionWindow(); + window.addEventListener("resize", positionWindow); + + return supportCenterWindow; + } + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "stack-layout": + control = new qx.ui.container.Stack(); + this.add(control, { + flex: 1 + }); + break; + case "conversations-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(15)); + this.getChildControl("stack-layout").add(control); + break; + case "conversations-intro-text": { + control = new qx.ui.basic.Label().set({ + rich: true, + font: "text-14", + }); + const isSupportUser = osparc.store.Products.getInstance().amIASupportUser(); + control.set({ + value: isSupportUser ? + this.tr("Thanks for being here! Let's help every user feel supported.") : + this.tr("Need help or want to share feedback? You're in the right place."), + }); + this.getChildControl("conversations-layout").add(control); + break; + } + case "conversations-list": { + control = new osparc.support.Conversations(); + control.addListener("openConversation", e => { + const conversationId = e.getData(); + this.openConversation(conversationId); + }, this); + const scroll = new qx.ui.container.Scroll(); + scroll.add(control); + this.getChildControl("conversations-layout").add(scroll, { + flex: 1, + }); + break; + } + case "ask-a-question-button": + control = new osparc.ui.form.FetchButton(this.tr("Ask a Question")).set({ + appearance: "strong-button", + allowGrowX: false, + center: true, + alignX: "center", + }); + control.addListener("execute", () => this.openConversation(null), this); + this.getChildControl("conversations-layout").add(control); + break; + case "conversation-page": + control = new osparc.support.ConversationPage(); + control.addListener("showConversations", () => this.__showConversations(), this); + this.getChildControl("stack-layout").add(control); + break; + } + return control || this.base(arguments, id); + }, + + __showConversations: function() { + this.getChildControl("stack-layout").setSelection([this.getChildControl("conversations-layout")]); + }, + + __showConversation: function() { + this.getChildControl("stack-layout").setSelection([this.getChildControl("conversation-page")]); + }, + + openConversation: function(conversationId) { + const conversationPage = this.getChildControl("conversation-page"); + if (conversationId) { + osparc.store.ConversationsSupport.getInstance().getConversation(conversationId) + .then(conversation => { + conversationPage.setConversation(conversation); + this.__showConversation(); + }); + } else { + conversationPage.setConversation(null); + this.__showConversation(); + } + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js b/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js index 0f9130d755a4..462845a7144b 100644 --- a/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js +++ b/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js @@ -142,16 +142,7 @@ qx.Class.define("osparc.ui.list.ListItem", { let control; switch (id) { case "thumbnail": - control = new qx.ui.basic.Image().set({ - alignY: "middle", - scale: true, - allowGrowX: true, - allowGrowY: true, - allowShrinkX: true, - allowShrinkY: true, - maxWidth: 32, - maxHeight: 32 - }); + control = osparc.utils.Utils.createThumbnail(32); this._add(control, { row: 0, column: 0, 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 8f8cf31aa719..22af331a5d04 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 @@ -36,8 +36,10 @@ qx.Class.define("osparc.ui.window.Window", { ); if (modalFrame) { modalFrame.addEventListener("click", () => { - if (this.isModal() && this.isClickAwayClose() && - parseInt(modalFrame.style.zIndex) === parseInt(thisDom.style.zIndex) - 1) { + if ( + this.isClickAwayClose() && + parseInt(modalFrame.style.zIndex) === parseInt(thisDom.style.zIndex) - 1 + ) { this.close(); } }); @@ -116,6 +118,8 @@ qx.Class.define("osparc.ui.window.Window", { } } }, 1); + // keep it centered + window.addEventListener("resize", () => this.center()); } else { this.base(arguments); } diff --git a/services/static-webserver/client/source/class/osparc/utils/Utils.js b/services/static-webserver/client/source/class/osparc/utils/Utils.js index beb742fbb667..f3b6b6938209 100644 --- a/services/static-webserver/client/source/class/osparc/utils/Utils.js +++ b/services/static-webserver/client/source/class/osparc/utils/Utils.js @@ -130,6 +130,24 @@ qx.Class.define("osparc.utils.Utils", { } }, + getThumbnailProps: function(size = 32) { + return { + alignY: "middle", + scale: true, + allowGrowX: true, + allowGrowY: true, + allowShrinkX: true, + allowShrinkY: true, + decorator: "rounded", + maxWidth: size, + maxHeight: size, + }; + }, + + createThumbnail: function(size = 32) { + return new qx.ui.basic.Image().set(this.getThumbnailProps(size)); + }, + disableAutocomplete: function(control) { if (control && control.getContentElement()) { control.getContentElement().setAttribute("autocomplete", "off"); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemDetails.js b/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemDetails.js index 509924c3ae79..cf6aaaca5c54 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemDetails.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemDetails.js @@ -195,12 +195,7 @@ qx.Class.define("osparc.vipMarket.LicensedItemDetails", { iconPosition: "right", cursor: "pointer", }); - manufacturerLink.getChildControl("icon").set({ - maxWidth: 32, - maxHeight: 32, - scale: true, - decorator: "rounded", - }); + manufacturerLink.getChildControl("icon").set(osparc.utils.Utils.getThumbnailProps(32)); manufacturerLink.addListener("tap", () => window.open(licensedResource.getManufacturerLink())); headerLayout.add(manufacturerLink, { column: 1, diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemListItem.js b/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemListItem.js index 9756cc5931ad..755b977d481d 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemListItem.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/LicensedItemListItem.js @@ -127,16 +127,7 @@ qx.Class.define("osparc.vipMarket.LicensedItemListItem", { let control; switch (id) { case "thumbnail": - control = new qx.ui.basic.Image().set({ - alignY: "middle", - scale: true, - allowGrowX: true, - allowGrowY: true, - allowShrinkX: true, - allowShrinkY: true, - maxWidth: 32, - maxHeight: 32 - }); + control = osparc.utils.Utils.createThumbnail(32); this._add(control, { row: 0, column: 0, diff --git a/services/static-webserver/client/source/class/osparc/workbench/Annotation.js b/services/static-webserver/client/source/class/osparc/workbench/Annotation.js index 2ef15ebc9bbb..09583b40cc99 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/Annotation.js +++ b/services/static-webserver/client/source/class/osparc/workbench/Annotation.js @@ -123,7 +123,7 @@ qx.Class.define("osparc.workbench.Annotation", { representation = svgLayer.drawAnnotationConversation(attrs.x, attrs.y, attrs.text); const conversationId = attrs.conversationId; if (conversationId) { - osparc.store.Conversations.getInstance().addListener("conversationRenamed", e => { + osparc.store.ConversationsProject.getInstance().addListener("conversationRenamed", e => { const data = e.getData(); if (conversationId === data["conversationId"]) { this.setText(data.name); diff --git a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js index 6ca961dd2755..1de3b6979fe6 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js @@ -2049,7 +2049,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { } case annotationTypes.CONVERSATION: { const conversationTitle = `${initPos.x}, ${initPos.y}`; - osparc.store.Conversations.getInstance().addConversation(this.getStudy().getUuid(), conversationTitle, osparc.study.Conversations.TYPES.PROJECT_ANNOTATION) + osparc.store.ConversationsProject.getInstance().postConversation(this.getStudy().getUuid(), conversationTitle, osparc.store.ConversationsProject.TYPES.PROJECT_ANNOTATION) .then(conversationData => { serializeData.attributes.conversationId = conversationData["conversationId"]; serializeData.attributes.text = conversationData["name"]; @@ -2079,7 +2079,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { this.__annotations[annotation.getId()] = annotation; if (annotation.getType() === osparc.workbench.Annotation.TYPES.CONVERSATION) { - osparc.store.Conversations.getInstance().addListener("conversationDeleted", e => { + osparc.store.ConversationsProject.getInstance().addListener("conversationDeleted", e => { const data = e.getData(); if (annotation.getAttributes()["conversationId"] === data["conversationId"]) { this.__removeAnnotation(annotation.getId()); @@ -2100,7 +2100,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { osparc.study.Conversations.popUpInWindow(this.getStudy().serialize(), conversationId); // Check if conversation still exists, if not, ask to remove annotation - osparc.store.Conversations.getInstance().getConversation(this.getStudy().getUuid(), conversationId) + osparc.store.ConversationsProject.getInstance().getConversation(this.getStudy().getUuid(), conversationId) .catch(err => { if ("status" in err && err.status === 404) { const win = new osparc.ui.window.Confirmation(this.tr("Do you want to remove the annotation?")).set({