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 ddaba210ed95..d061753b80c1 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js +++ b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js @@ -146,7 +146,7 @@ qx.Class.define("osparc.conversation.AddMessage", { this.__postMessage(); } else { // create new conversation first - osparc.study.Conversations.addConversation(this.__studyData["uuid"]) + osparc.store.Conversations.getInstance().addConversation(this.__studyData["uuid"]) .then(data => { this.__conversationId = data["conversationId"]; this.__postMessage(); @@ -213,7 +213,7 @@ qx.Class.define("osparc.conversation.AddMessage", { this.__postNotify(userGid); } else { // create new conversation first - osparc.study.Conversations.addConversation(this.__studyData["uuid"]) + osparc.store.Conversations.getInstance().addConversation(this.__studyData["uuid"]) .then(data => { this.__conversationId = data["conversationId"]; this.__postNotify(userGid); @@ -225,7 +225,7 @@ qx.Class.define("osparc.conversation.AddMessage", { const commentField = this.getChildControl("comment-field"); const content = commentField.getChildControl("text-area").getValue(); if (content) { - osparc.study.Conversations.addMessage(this.__studyData["uuid"], this.__conversationId, content) + osparc.store.Conversations.getInstance().addMessage(this.__studyData["uuid"], this.__conversationId, content) .then(data => { this.fireDataEvent("messageAdded", data); commentField.getChildControl("text-area").setValue(""); @@ -237,7 +237,7 @@ qx.Class.define("osparc.conversation.AddMessage", { const commentField = this.getChildControl("comment-field"); const content = commentField.getChildControl("text-area").getValue(); if (content) { - osparc.study.Conversations.editMessage(this.__studyData["uuid"], this.__conversationId, this.__message["messageId"], 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(""); @@ -247,7 +247,7 @@ qx.Class.define("osparc.conversation.AddMessage", { __postNotify: function(userGid) { if (userGid) { - osparc.study.Conversations.notifyUser(this.__studyData["uuid"], this.__conversationId, userGid) + osparc.store.Conversations.getInstance().notifyUser(this.__studyData["uuid"], this.__conversationId, userGid) .then(data => { this.fireDataEvent("messageAdded", data); const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators(); 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 8ed290b1e09c..d29bf773c1c6 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js @@ -59,10 +59,6 @@ qx.Class.define("osparc.conversation.Conversation", { }, }, - events: { - "conversationDeleted": "qx.event.type.Event", - }, - members: { __studyData: null, __messages: null, @@ -82,6 +78,7 @@ qx.Class.define("osparc.conversation.Conversation", { }; const renameButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/pencil-alt/10").set({ ...buttonsAesthetics, + visibility: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]) ? "visible" : "excluded", }); renameButton.addListener("execute", () => { const titleEditor = new osparc.widget.Renamer(tabButton.getLabel()); @@ -89,11 +86,11 @@ qx.Class.define("osparc.conversation.Conversation", { titleEditor.close(); const newLabel = e.getData()["newLabel"]; if (this.getConversationId()) { - osparc.study.Conversations.renameConversation(this.__studyData["uuid"], this.getConversationId(), newLabel) + osparc.store.Conversations.getInstance().renameConversation(this.__studyData["uuid"], this.getConversationId(), newLabel) .then(() => this.renameConversation(newLabel)); } else { // create new conversation first - osparc.study.Conversations.addConversation(this.__studyData["uuid"], newLabel) + osparc.store.Conversations.getInstance().addConversation(this.__studyData["uuid"], newLabel) .then(data => { this.setConversationId(data["conversationId"]); this.getChildControl("button").setLabel(newLabel); @@ -112,14 +109,11 @@ qx.Class.define("osparc.conversation.Conversation", { const closeButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/times/12").set({ ...buttonsAesthetics, paddingLeft: 4, // adds spacing between buttons + visibility: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]) ? "visible" : "excluded", }); closeButton.addListener("execute", () => { - const deleteConversation = () => { - osparc.study.Conversations.deleteConversation(this.__studyData["uuid"], this.getConversationId()) - .then(() => this.fireEvent("conversationDeleted")); - } if (this.__messagesList.getChildren().length === 0) { - deleteConversation(); + osparc.store.Conversations.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({ @@ -130,7 +124,7 @@ qx.Class.define("osparc.conversation.Conversation", { confirmationWin.open(); confirmationWin.addListener("close", () => { if (confirmationWin.getConfirmed()) { - deleteConversation(); + osparc.store.Conversations.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); } }, this); } @@ -166,18 +160,18 @@ qx.Class.define("osparc.conversation.Conversation", { this.__loadMoreMessages.addListener("execute", () => this.__reloadMessages(false)); this._add(this.__loadMoreMessages); - if (osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])) { - const addMessages = new osparc.conversation.AddMessage(this.__studyData, this.getConversationId()); - addMessages.setPaddingLeft(10); - addMessages.addListener("messageAdded", e => { - const data = e.getData(); - if (data["conversationId"]) { - this.setConversationId(data["conversationId"]); - this.addMessage(data); - } - }); - this._add(addMessages); - } + const addMessages = new osparc.conversation.AddMessage(this.__studyData, this.getConversationId()).set({ + enabled: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]), + paddingLeft: 10, + }); + addMessages.addListener("messageAdded", e => { + const data = e.getData(); + if (data["conversationId"]) { + this.setConversationId(data["conversationId"]); + this.addMessage(data); + } + }); + this._add(addMessages); }, __getNextRequest: function() { @@ -197,7 +191,8 @@ qx.Class.define("osparc.conversation.Conversation", { const options = { resolveWResponse: true }; - return osparc.data.Resources.fetch("conversations", "getMessagesPage", params, options); + return osparc.data.Resources.fetch("conversations", "getMessagesPage", params, options) + .catch(err => osparc.FlashMessenger.logError(err)); }, __reloadMessages: function(removeMessages = true) { 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 399dfad55d72..8d909d84a8f1 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js @@ -227,7 +227,7 @@ qx.Class.define("osparc.conversation.MessageUI", { win.open(); win.addListener("close", () => { if (win.getConfirmed()) { - osparc.study.Conversations.deleteMessage(message) + osparc.store.Conversations.getInstance().deleteMessage(message) .then(() => this.fireDataEvent("messageDeleted", message)) .catch(err => osparc.FlashMessenger.logError(err)); } 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 6d69b0a04562..781f672feced 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -310,7 +310,7 @@ qx.Class.define("osparc.data.Resources", { } }, "conversations": { - useCache: false, + useCache: false, // It has its own cache handler endpoints: { addConversation: { method: "POST", @@ -320,6 +320,10 @@ qx.Class.define("osparc.data.Resources", { method: "GET", url: statics.API + "/projects/{studyId}/conversations?offset={offset}&limit={limit}" }, + getConversation: { + method: "GET", + url: statics.API + "/projects/{studyId}/conversations/{conversationId}" + }, renameConversation: { method: "PUT", url: statics.API + "/projects/{studyId}/conversations/{conversationId}" diff --git a/services/static-webserver/client/source/class/osparc/data/model/Study.js b/services/static-webserver/client/source/class/osparc/data/model/Study.js index f95c17a80493..1a0ab260ef91 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Study.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Study.js @@ -70,8 +70,8 @@ qx.Class.define("osparc.data.model.Study", { this.setWorkbench(workbench); workbench.setStudy(this); - const workbenchUi = new osparc.data.model.StudyUI(studyData.ui); - this.setUi(workbenchUi); + const studyUI = new osparc.data.model.StudyUI(studyData.ui); + this.setUi(studyUI); this.getWorkbench().buildWorkbench(); }, diff --git a/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js b/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js index 7bf1d260e76f..812dc4a1e62b 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js +++ b/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js @@ -38,7 +38,10 @@ qx.Class.define("osparc.data.model.StudyUI", { }); if ("annotations" in studyDataUI) { - this.__annotationsInitData = studyDataUI["annotations"]; + Object.entries(studyDataUI["annotations"]).forEach(([annotationId, annotationData]) => { + const annotation = new osparc.workbench.Annotation(annotationData, annotationId); + this.addAnnotation(annotation); + }); } }, @@ -99,22 +102,12 @@ qx.Class.define("osparc.data.model.StudyUI", { }, members: { - __annotationsInitData: null, - __applyMode: function(mode) { if (mode === "guided") { this.setMode("app"); } }, - getAnnotationsInitData: function() { - return this.__annotationsInitData; - }, - - nullAnnotationsInitData: function() { - this.__annotationsInitData = null; - }, - addAnnotation: function(annotation) { this.getAnnotations()[annotation.getId()] = annotation; }, diff --git a/services/static-webserver/client/source/class/osparc/desktop/ControlsBar.js b/services/static-webserver/client/source/class/osparc/desktop/ControlsBar.js deleted file mode 100644 index 9c4c9fe9ffe9..000000000000 --- a/services/static-webserver/client/source/class/osparc/desktop/ControlsBar.js +++ /dev/null @@ -1,104 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2018 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -/** - * Widget that shows the play/stop study button. - * - * *Example* - * - * Here is a little example of how to use the widget. - * - *
- * let controlsBar = new osparc.desktop.ControlsBar(); - * this.getRoot().add(controlsBar); - *- */ - -qx.Class.define("osparc.desktop.ControlsBar", { - extend: qx.ui.toolbar.ToolBar, - - construct: function() { - this.base(arguments); - - this.setSpacing(10); - - this.__initDefault(); - this.__attachEventHandlers(); - }, - - events: { - "showWorkbench": "qx.event.type.Event", - "showSettings": "qx.event.type.Event", - "groupSelection": "qx.event.type.Event", - "ungroupSelection": "qx.event.type.Event" - }, - - members: { - __serviceFilters: null, - __viewCtrls: null, - __workbenchViewButton: null, - __settingsViewButton: null, - __iterationCtrls: null, - __parametersButton: null, - - setWorkbenchVisibility: function(isWorkbenchContext) { - this.__serviceFilters.setVisibility(isWorkbenchContext ? "visible" : "excluded"); - }, - - setExtraViewVisibility: function(hasExtraView) { - this.__viewCtrls.setVisibility(hasExtraView ? "visible" : "excluded"); - }, - - __initDefault: function() { - const filterCtrls = new qx.ui.toolbar.Part(); - const serviceFilters = this.__serviceFilters = new osparc.filter.group.ServiceFilterGroup("workbench"); - osparc.filter.UIFilterController.getInstance().registerContainer("workbench", serviceFilters); - filterCtrls.add(serviceFilters); - this.add(filterCtrls); - - this.addSpacer(); - - const viewCtrls = this.__viewCtrls = new qx.ui.toolbar.Part(); - const workbenchViewButton = this.__workbenchViewButton = this.__createWorkbenchButton(); - const settingsViewButton = this.__settingsViewButton = this.__createSettingsButton(); - viewCtrls.add(workbenchViewButton); - viewCtrls.add(settingsViewButton); - this.add(viewCtrls); - const viewRadioGroup = new qx.ui.form.RadioGroup(); - viewRadioGroup.add(workbenchViewButton, settingsViewButton); - }, - - __createWorkbenchButton: function() { - const workbenchButton = this.__createRadioButton(this.tr("Workbench view"), "vector-square", "workbenchViewBtn", "showWorkbench"); - return workbenchButton; - }, - - __createSettingsButton: function() { - const settingsButton = this.__createRadioButton(this.tr("Node view"), "list", "settingsViewBtn", "showSettings"); - return settingsButton; - }, - - __createRadioButton: function(label, icon, widgetId, singalName) { - const button = new qx.ui.toolbar.RadioButton(label); - osparc.utils.Utils.setIdToWidget(button, widgetId); - button.addListener("execute", () => { - this.fireEvent(singalName); - }, this); - return button; - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPanel.js b/services/static-webserver/client/source/class/osparc/desktop/MainPanel.js deleted file mode 100644 index 06161e10d7c4..000000000000 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPanel.js +++ /dev/null @@ -1,85 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2018 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -/* eslint no-underscore-dangle: 0 */ - -/** - * Widget containing a Vertical Box with a MainView and ControlsBar. - * Used as Main View in the study editor. - * - * *Example* - * - * Here is a little example of how to use the widget. - * - *
- * let mainPanel = this.__mainPanel = new osparc.desktop.MainPanel(); - * mainPanel.setMainView(widget); - * this.getRoot().add(mainPanel); - *- */ - -qx.Class.define("osparc.desktop.MainPanel", { - extend: qx.ui.core.Widget, - - construct: function() { - this.base(arguments); - - this._setLayout(new qx.ui.layout.VBox()); - - const wbToolbar = this.__wbToolbar = new osparc.desktop.WorkbenchToolbar(); - this._add(wbToolbar); - - const hBox = this.__mainView = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)).set({ - allowGrowY: true - }); - this._add(hBox, { - flex: 1 - }); - - const controlsBar = this.__controlsBar = new osparc.desktop.ControlsBar(); - this._add(controlsBar); - }, - - properties: { - mainView: { - nullable: false, - check : "qx.ui.core.Widget", - apply : "__applyMainView" - } - }, - - members: { - __wbToolbar: null, - __mainView: null, - __controlsBar: null, - - __applyMainView: function(newWidget) { - this.__mainView.removeAll(); - this.__mainView.add(newWidget, { - flex: 1 - }); - }, - - getToolbar: function() { - return this.__wbToolbar; - }, - - getControls: function() { - return this.__controlsBar; - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js index f4f0bbc9fc67..53b560ad9ce9 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -873,25 +873,34 @@ qx.Class.define("osparc.desktop.WorkbenchView", { __getAnnotationsSection: function() { const annotationsSection = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); - annotationsSection.add(new qx.ui.basic.Label(this.tr("Annotations")).set({ + annotationsSection.add(new qx.ui.basic.Label(this.tr("Add to Workbench")).set({ font: "text-14" })); - const annotationsButtons = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + const annotationsButtons = new qx.ui.container.Composite(new qx.ui.layout.Flow(5, 5)); annotationsSection.add(annotationsButtons); const buttonsHeight = 28; + + const addConversationBtn = new qx.ui.form.Button().set({ + label: this.tr("Conversation"), + icon: "@FontAwesome5Solid/comment/14", + height: buttonsHeight + }); + addConversationBtn.addListener("execute", () => this.__workbenchUI.startConversation(), this); + annotationsButtons.add(addConversationBtn); + const addNoteBtn = new qx.ui.form.Button().set({ label: this.tr("Note"), - icon: "@FontAwesome5Solid/plus/14", + icon: "@FontAwesome5Solid/sticky-note/14", height: buttonsHeight }); addNoteBtn.addListener("execute", () => this.__workbenchUI.startAnnotationsNote(), this); annotationsButtons.add(addNoteBtn); const addRectBtn = new qx.ui.form.Button().set({ - label: this.tr("Rectangle"), - icon: "@FontAwesome5Solid/plus/14", + label: this.tr("Box"), + icon: "@FontAwesome5Regular/square/14", height: buttonsHeight }); addRectBtn.addListener("execute", () => this.__workbenchUI.startAnnotationsRect(), this); @@ -899,7 +908,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { const addTextBtn = new qx.ui.form.Button().set({ label: this.tr("Text"), - icon: "@FontAwesome5Solid/plus/14", + icon: "@FontAwesome5Solid/font/14", height: buttonsHeight }); addTextBtn.addListener("execute", () => this.__workbenchUI.startAnnotationsText(), this); diff --git a/services/static-webserver/client/source/class/osparc/editor/AnnotationEditor.js b/services/static-webserver/client/source/class/osparc/editor/AnnotationEditor.js index f62e7b8ef1e7..d26d6296d89b 100644 --- a/services/static-webserver/client/source/class/osparc/editor/AnnotationEditor.js +++ b/services/static-webserver/client/source/class/osparc/editor/AnnotationEditor.js @@ -129,26 +129,28 @@ qx.Class.define("osparc.editor.AnnotationEditor", { return; } + const annotationTypes = osparc.workbench.Annotation.TYPES; + const attrs = annotation.getAttributes(); - if (annotation.getType() === "text") { + if (annotation.getType() === annotationTypes.TEXT) { const textField = this.getChildControl("text-field").set({ value: attrs.text }); textField.addListener("changeValue", e => annotation.setText(e.getData())); - } else if (annotation.getType() === "note") { + } else if (annotation.getType() === annotationTypes.NOTE) { const textArea = this.getChildControl("text-area").set({ value: attrs.text }); textArea.addListener("changeValue", e => annotation.setText(e.getData())); } - if (["text", "rect"].includes(annotation.getType())) { + if ([annotationTypes.TEXT, annotationTypes.RECT].includes(annotation.getType())) { const colorPicker = this.getChildControl("color-picker"); annotation.bind("color", colorPicker, "value"); colorPicker.bind("value", annotation, "color"); } - if (annotation.getType() === "text") { + if (annotation.getType() === annotationTypes.TEXT) { const fontSizeField = this.getChildControl("font-size").set({ value: attrs.fontSize }) diff --git a/services/static-webserver/client/source/class/osparc/file/FileDrop.js b/services/static-webserver/client/source/class/osparc/file/FileDrop.js index 92b4f15206c9..6411e33276dd 100644 --- a/services/static-webserver/client/source/class/osparc/file/FileDrop.js +++ b/services/static-webserver/client/source/class/osparc/file/FileDrop.js @@ -256,9 +256,9 @@ qx.Class.define("osparc.file.FileDrop", { this._add(this.__dropMe); const svgLayer = this.__svgLayer; if (svgLayer.getReady()) { - this.__dropMe.rect = svgLayer.drawDashedRect(boxWidth, boxHeight); + this.__dropMe["rect"] = svgLayer.drawDashedRect(boxWidth, boxHeight); } else { - svgLayer.addListenerOnce("SvgWidgetReady", () => this.__dropMe.rect = svgLayer.drawDashedRect(boxWidth, boxHeight), this); + svgLayer.addListenerOnce("SvgWidgetReady", () => this.__dropMe["rect"] = svgLayer.drawDashedRect(boxWidth, boxHeight), this); } } const dropMe = this.__dropMe; @@ -269,10 +269,10 @@ qx.Class.define("osparc.file.FileDrop", { top: posY - parseInt(dropMeBounds.height/2)- parseInt(boxHeight/2) }); if ("rect" in dropMe) { - dropMe.rect.stroke({ + dropMe["rect"].stroke({ width: 1 }); - osparc.wrapper.Svg.updateItemPos(dropMe.rect, posX - boxWidth, posY - boxHeight); + osparc.wrapper.Svg.updateItemPos(dropMe["rect"], posX - boxWidth, posY - boxHeight); } }, @@ -280,7 +280,7 @@ qx.Class.define("osparc.file.FileDrop", { const dropMe = this.__dropMe; if (dropMe) { if ("rect" in dropMe) { - dropMe.rect.stroke({ + dropMe["rect"].stroke({ width: 0 }); } diff --git a/services/static-webserver/client/source/class/osparc/store/Conversations.js b/services/static-webserver/client/source/class/osparc/store/Conversations.js new file mode 100644 index 000000000000..0a75ab2acd88 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/store/Conversations.js @@ -0,0 +1,205 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.store.Conversations", { + extend: qx.core.Object, + type: "singleton", + + events: { + "conversationRenamed": "qx.event.type.Data", + "conversationDeleted": "qx.event.type.Data", + }, + + members: { + getConversations: function(studyId) { + const params = { + url: { + studyId, + offset: 0, + limit: 42, + } + }; + return osparc.data.Resources.fetch("conversations", "getConversationsPage", params) + .then(conversations => { + if (conversations.length) { + // Sort conversations by created date, oldest first (the new ones will be next to the plus button) + conversations.sort((a, b) => new Date(a["created"]) - new Date(b["created"])); + } + return conversations; + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + getConversation: function(studyId, conversationId) { + const params = { + url: { + studyId, + conversationId, + } + }; + return osparc.data.Resources.fetch("conversations", "getConversation", params); + }, + + addConversation: function(studyId, name = "new 1", type = osparc.study.Conversations.TYPES.PROJECT_STATIC) { + const params = { + url: { + studyId, + }, + data: { + name, + type, + } + }; + return osparc.data.Resources.fetch("conversations", "addConversation", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + deleteConversation: function(studyId, conversationId) { + const params = { + url: { + studyId, + conversationId, + }, + }; + return osparc.data.Resources.fetch("conversations", "deleteConversation", params) + .then(() => { + this.fireDataEvent("conversationDeleted", { + studyId, + conversationId, + }) + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + renameConversation: function(studyId, conversationId, name) { + const params = { + url: { + studyId, + conversationId, + }, + data: { + name, + } + }; + return osparc.data.Resources.fetch("conversations", "renameConversation", params) + .then(() => { + this.fireDataEvent("conversationRenamed", { + studyId, + conversationId, + name, + }); + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + addMessage: function(studyId, conversationId, message) { + const params = { + url: { + studyId, + conversationId, + }, + data: { + "content": message, + "type": "MESSAGE", + } + }; + return osparc.data.Resources.fetch("conversations", "addMessage", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + editMessage: function(studyId, conversationId, messageId, message) { + const params = { + url: { + studyId, + conversationId, + messageId, + }, + data: { + "content": message, + }, + }; + return osparc.data.Resources.fetch("conversations", "editMessage", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + deleteMessage: function(message) { + const params = { + url: { + studyId: message["projectId"], + conversationId: message["conversationId"], + messageId: message["messageId"], + }, + }; + return osparc.data.Resources.fetch("conversations", "deleteMessage", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + notifyUser: function(studyId, conversationId, userGroupId) { + const params = { + url: { + studyId, + conversationId, + }, + data: { + "content": userGroupId.toString(), // eventually the backend will accept integers + "type": "NOTIFICATION", + } + }; + return osparc.data.Resources.fetch("conversations", "addMessage", 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/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 0839624779e0..e70a62133c1c 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -22,12 +22,13 @@ qx.Class.define("osparc.study.Conversations", { /** * @param studyData {Object} Study Data */ - construct: function(studyData) { + construct: function(studyData, openConversationId = null) { this.base(arguments); this._setLayout(new qx.ui.layout.VBox()); this.__conversations = []; + this.__openConversationId = openConversationId; this.set({ studyData, @@ -46,8 +47,13 @@ qx.Class.define("osparc.study.Conversations", { }, statics: { - popUpInWindow: function(studyData) { - const conversations = new osparc.study.Conversations(studyData); + TYPES: { + PROJECT_STATIC: "PROJECT_STATIC", + PROJECT_ANNOTATION: "PROJECT_ANNOTATION", + }, + + popUpInWindow: function(studyData, openConversationId = null) { + const conversations = new osparc.study.Conversations(studyData, openConversationId); const title = qx.locale.Manager.tr("Conversations"); const viewWidth = 600; const viewHeight = 700; @@ -57,105 +63,10 @@ qx.Class.define("osparc.study.Conversations", { }, this); return win; }, - - addConversation: function(studyId, name = "new 1") { - const params = { - url: { - studyId, - }, - data: { - name, - "type": "PROJECT_STATIC", - } - }; - return osparc.data.Resources.fetch("conversations", "addConversation", params) - .catch(err => osparc.FlashMessenger.logError(err)); - }, - - deleteConversation: function(studyId, conversationId) { - const params = { - url: { - studyId, - conversationId, - }, - }; - return osparc.data.Resources.fetch("conversations", "deleteConversation", params) - .catch(err => osparc.FlashMessenger.logError(err)); - }, - - renameConversation: function(studyId, conversationId, name) { - const params = { - url: { - studyId, - conversationId, - }, - data: { - name, - } - }; - return osparc.data.Resources.fetch("conversations", "renameConversation", params) - .catch(err => osparc.FlashMessenger.logError(err)); - }, - - addMessage: function(studyId, conversationId, message) { - const params = { - url: { - studyId, - conversationId, - }, - data: { - "content": message, - "type": "MESSAGE", - } - }; - return osparc.data.Resources.fetch("conversations", "addMessage", params) - .catch(err => osparc.FlashMessenger.logError(err)); - }, - - editMessage: function(studyId, conversationId, messageId, message) { - const params = { - url: { - studyId, - conversationId, - messageId, - }, - data: { - "content": message, - }, - }; - return osparc.data.Resources.fetch("conversations", "editMessage", params) - .catch(err => osparc.FlashMessenger.logError(err)); - }, - - deleteMessage: function(message) { - const params = { - url: { - studyId: message["projectId"], - conversationId: message["conversationId"], - messageId: message["messageId"], - }, - }; - return osparc.data.Resources.fetch("conversations", "deleteMessage", params) - .catch(err => osparc.FlashMessenger.logError(err)); - }, - - notifyUser: function(studyId, conversationId, userGroupId) { - const params = { - url: { - studyId, - conversationId, - }, - data: { - "content": userGroupId.toString(), // eventually the backend will accept integers - "type": "NOTIFICATION", - } - }; - return osparc.data.Resources.fetch("conversations", "addMessage", params) - .catch(err => osparc.FlashMessenger.logError(err)); - }, }, members: { + __openConversationId: null, __conversations: null, __newConversationButton: null, __wsHandlers: null, @@ -198,7 +109,7 @@ qx.Class.define("osparc.study.Conversations", { this.__updateConversationName(conversation); break; case "conversation:deleted": - this.__removeConversationPage(conversation); + this.__removeConversationPage(conversation["conversationId"]); break; } } @@ -244,17 +155,18 @@ qx.Class.define("osparc.study.Conversations", { const loadMoreButton = this.getChildControl("loading-button"); loadMoreButton.setFetching(true); - const params = { - url: { - studyId: studyData["uuid"], - offset: 0, - limit: 42, - } - }; - osparc.data.Resources.fetch("conversations", "getConversationsPage", params) + osparc.store.Conversations.getInstance().getConversations(studyData["uuid"]) .then(conversations => { if (conversations.length) { conversations.forEach(conversation => this.__addConversationPage(conversation)); + if (this.__openConversationId) { + const conversationsLayout = this.getChildControl("conversations-layout"); + const conversation = conversationsLayout.getSelectables().find(c => c.getConversationId() === this.__openConversationId); + if (conversation) { + conversationsLayout.setSelection([conversation]); + } + this.__openConversationId = null; // reset it so it does not open again + } } else { this.__addTempConversationPage(); } @@ -272,7 +184,12 @@ 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, true)); + osparc.store.Conversations.getInstance().addListener("conversationDeleted", e => { + const data = e.getData(); + if (conversationId === data["conversationId"]) { + this.__removeConversationPage(conversationId, true); + } + }); } else { // create a temporary conversation conversationPage = new osparc.conversation.Conversation(studyData); @@ -307,16 +224,17 @@ qx.Class.define("osparc.study.Conversations", { conversationsLayout.add(conversationPage); if (this.__newConversationButton === null) { + const studyData = this.getStudyData(); // 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", + enabled: osparc.data.model.Study.canIWrite(studyData["accessRights"]), }); newConversationButton.addListener("execute", () => { - const studyData = this.getStudyData(); - osparc.study.Conversations.addConversation(studyData["uuid"], "new " + (this.__conversations.length + 1)) + osparc.store.Conversations.getInstance().addConversation(studyData["uuid"], "new " + (this.__conversations.length + 1)) .then(conversationDt => { this.__addConversationPage(conversationDt); const newConversationPage = this.__getConversation(conversationDt["conversationId"]); @@ -332,8 +250,7 @@ qx.Class.define("osparc.study.Conversations", { conversationsLayout.getChildControl("bar").add(this.__newConversationButton); }, - __removeConversationPage: function(conversationData, changeSelection = false) { - const conversationId = conversationData["conversationId"]; + __removeConversationPage: function(conversationId, changeSelection = false) { const conversation = this.__getConversation(conversationId); if (conversation) { const conversationsLayout = this.getChildControl("conversations-layout"); 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 7444094680a9..746e5d176dd0 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/Annotation.js +++ b/services/static-webserver/client/source/class/osparc/workbench/Annotation.js @@ -19,17 +19,12 @@ qx.Class.define("osparc.workbench.Annotation", { extend: qx.core.Object, /** - * @param svgLayer {Object} SVG canvas * @param data {Object} data containing type, color, attributes and (optional) id * @param id {String} data */ - construct: function(svgLayer, data, id) { + construct: function(data, id) { this.base(); - if (svgLayer) { - this.__svgLayer = svgLayer; - } - if (id === undefined) { id = osparc.utils.Utils.uuidV4(); } @@ -39,14 +34,21 @@ qx.Class.define("osparc.workbench.Annotation", { } this.set({ id, - type: data.type, color, - attributes: data.attributes + attributes: data.attributes, + type: data.type, }); }, statics: { - DEFAULT_COLOR: "#FFFF01" + DEFAULT_COLOR: "#FFFF01", + + TYPES: { + NOTE: "note", + RECT: "rect", + TEXT: "text", + CONVERSATION: "conversation", + }, }, properties: { @@ -56,26 +58,37 @@ qx.Class.define("osparc.workbench.Annotation", { }, type: { - check: ["note", "rect", "text"], - nullable: false + check: [ + "note", // osparc.workbench.Annotation.TYPES.NOTE + "rect", // osparc.workbench.Annotation.TYPES.RECT + "text", // osparc.workbench.Annotation.TYPES.TEXT + "conversation", // osparc.workbench.Annotation.TYPES.CONVERSATION + ], + nullable: false, }, color: { check: "Color", event: "changeColor", init: "#FFFF01", - apply: "__applyColor" + nullable: true, + apply: "__applyColor", }, attributes: { check: "Object", nullable: false, - apply: "__drawAnnotation" + }, + + svgCanvas: { + init: null, + nullable: false, + apply: "__drawAnnotation", }, representation: { init: null - } + }, }, events: { @@ -86,36 +99,66 @@ qx.Class.define("osparc.workbench.Annotation", { }, members: { - __svgLayer: null, - - __drawAnnotation: async function(attrs) { - if (this.__svgLayer === null) { + __drawAnnotation: function(svgLayer) { + if (svgLayer === null) { return; } + const attrs = this.getAttributes(); let representation = null; switch (this.getType()) { - case "note": { + case this.self().TYPES.NOTE: { const user = osparc.store.Groups.getInstance().getUserByGroupId(attrs.recipientGid); - representation = this.__svgLayer.drawAnnotationNote(attrs.x, attrs.y, user ? user.getLabel() : "", attrs.text); + representation = svgLayer.drawAnnotationNote(attrs.x, attrs.y, user ? user.getLabel() : "", attrs.text); break; } - case "rect": - representation = this.__svgLayer.drawAnnotationRect(attrs.width, attrs.height, attrs.x, attrs.y, this.getColor()); + case this.self().TYPES.RECT: + representation = svgLayer.drawAnnotationRect(attrs.width, attrs.height, attrs.x, attrs.y, this.getColor()); break; - case "text": - representation = this.__svgLayer.drawAnnotationText(attrs.x, attrs.y, attrs.text, this.getColor(), attrs.fontSize); + case this.self().TYPES.TEXT: + representation = svgLayer.drawAnnotationText(attrs.x, attrs.y, attrs.text, this.getColor(), attrs.fontSize); break; + case this.self().TYPES.CONVERSATION: { + representation = svgLayer.drawAnnotationConversation(attrs.x, attrs.y, attrs.text); + const conversationId = attrs.conversationId; + if (conversationId) { + osparc.store.Conversations.getInstance().addListener("conversationRenamed", e => { + const data = e.getData(); + if (conversationId === data["conversationId"]) { + this.setText(data.name); + } + }, this); + } + break; + } } + if (representation) { + // handle click events + switch (this.getType()) { + case this.self().TYPES.NOTE: + case this.self().TYPES.RECT: + case this.self().TYPES.TEXT: + representation.node.addEventListener("click", e => { + this.fireDataEvent("annotationClicked", e.ctrlKey); + e.stopPropagation(); + }, this); + break; + case this.self().TYPES.CONVERSATION: + representation["clickables"].forEach(clickable => { + clickable.click(() => { + this.fireDataEvent("annotationClicked", false); + }, this); + }); + break; + } + + // handle moving events osparc.wrapper.Svg.makeDraggable(representation); - representation.node.addEventListener("click", e => { - this.fireDataEvent("annotationClicked", e.ctrlKey); - e.stopPropagation(); - }, this); representation.on("dragstart", () => this.fireEvent("annotationStartedMoving")); representation.on("dragmove", () => this.fireEvent("annotationMoving")); representation.on("dragend", () => this.fireEvent("annotationStoppedMoving")); + this.setRepresentation(representation); } }, @@ -124,10 +167,10 @@ qx.Class.define("osparc.workbench.Annotation", { const representation = this.getRepresentation(); if (representation) { switch (this.getType()) { - case "rect": + case this.self().TYPES.RECT: osparc.wrapper.Svg.updateItemColor(representation, color); break; - case "text": + case this.self().TYPES.TEXT: osparc.wrapper.Svg.updateTextColor(representation, color); break; } @@ -137,11 +180,25 @@ qx.Class.define("osparc.workbench.Annotation", { getRepresentationPosition: function() { const representation = this.getRepresentation(); if (representation) { - const attrs = osparc.wrapper.Svg.getRectAttributes(representation); - return { - x: parseInt(attrs.x), - y: parseInt(attrs.y) - }; + switch (this.getType()) { + case this.self().TYPES.RECT: + case this.self().TYPES.TEXT: + case this.self().TYPES.NOTE: { + const attrs = osparc.wrapper.Svg.getRectAttributes(representation); + return { + x: parseInt(attrs.x), + y: parseInt(attrs.y), + }; + } + case this.self().TYPES.CONVERSATION: { + const x = representation.transform().x; + const y = representation.transform().y; + return { + x, + y, + }; + } + } } return null; }, @@ -185,14 +242,19 @@ qx.Class.define("osparc.workbench.Annotation", { }, setSelected: function(selected) { + const svgCanvas = this.getSvgCanvas(); + if (svgCanvas === null) { + return; + }; + const representation = this.getRepresentation(); if (representation) { switch (this.getType()) { - case "rect": - case "text": { + case this.self().TYPES.RECT: + case this.self().TYPES.TEXT: { if (selected) { if (!("bBox" in representation.node)) { - const bBox = this.__svgLayer.drawBoundingBox(this); + const bBox = svgCanvas.drawBoundingBox(this); representation.node["bBox"] = bBox; } } else if ("bBox" in representation.node) { @@ -206,11 +268,18 @@ qx.Class.define("osparc.workbench.Annotation", { }, serialize: function() { - return { + const serializeData = { type: this.getType(), attributes: this.getAttributes(), - color: this.getColor() }; + if ([ + this.self().TYPES.RECT, + this.self().TYPES.TEXT, + this.self().TYPES.NOTE, + ].includes(this.getType())) { + serializeData.color = this.getColor(); + } + return serializeData; } } }); diff --git a/services/static-webserver/client/source/class/osparc/workbench/BaseNodeUI.js b/services/static-webserver/client/source/class/osparc/workbench/BaseNodeUI.js index bd43406a5183..6ea4228772cd 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/BaseNodeUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/BaseNodeUI.js @@ -17,8 +17,6 @@ qx.Class.define("osparc.workbench.BaseNodeUI", { extend: qx.ui.window.Window, - include: osparc.filter.MFilterable, - implement: osparc.filter.IFilterable, type: "abstract", construct: function() { @@ -43,8 +41,6 @@ qx.Class.define("osparc.workbench.BaseNodeUI", { "border-radius": "4px" }); - this.subscribeToFilterGroup("workbench"); - const captionBar = this.getChildControl("captionbar"); captionBar.set({ cursor: "move", @@ -308,33 +304,5 @@ qx.Class.define("osparc.workbench.BaseNodeUI", { e.stopPropagation(); }, - - // implement osparc.filter.IFilterable - _filter: function() { - this.setOpacity(0.4); - }, - - // implement osparc.filter.IFilterable - _unfilter: function() { - this.setOpacity(1); - }, - - /** - * @abstract - */ - _shouldApplyFilter: function(data) { - throw new Error("Abstract method called!"); - }, - - // implement osparc.filter.IFilterable - _shouldReactToFilter: function(data) { - if (data.text && data.text.length > 1) { - return true; - } - if (data.tags && data.tags.length) { - return true; - } - return false; - } } }); diff --git a/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js b/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js index e6772e9c28f7..80fb1522b45e 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js @@ -666,37 +666,5 @@ qx.Class.define("osparc.workbench.NodeUI", { }); } }, - - __filterText: function(text) { - const label = this.getNode().getLabel() - .trim() - .toLowerCase(); - if (label.indexOf(text) === -1) { - return true; - } - return false; - }, - - __filterTags: function(tags) { - if (tags && tags.length) { - const category = this.getNode().getMetaData().category || ""; - const type = this.getNode().getMetaData().type || ""; - if (!tags.includes(osparc.utils.Utils.capitalize(category.trim())) && !tags.includes(osparc.utils.Utils.capitalize(type.trim()))) { - return true; - } - } - return false; - }, - - // implement osparc.filter.IFilterable - _shouldApplyFilter: function(data) { - if (data.text) { - return this.__filterText(data.text); - } - if (data.tags && data.tags.length) { - return this.__filterTags(data.tags); - } - return false; - } } }); diff --git a/services/static-webserver/client/source/class/osparc/workbench/SvgWidget.js b/services/static-webserver/client/source/class/osparc/workbench/SvgWidget.js index ba8af4fc2f67..b99cb59cd6fa 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/SvgWidget.js +++ b/services/static-webserver/client/source/class/osparc/workbench/SvgWidget.js @@ -105,6 +105,10 @@ qx.Class.define("osparc.workbench.SvgWidget", { return osparc.wrapper.Svg.drawAnnotationRect(this.__canvas, width, height, x, y, color); }, + drawAnnotationConversation: function(x, y, title) { + return osparc.wrapper.Svg.drawAnnotationConversation(this.__canvas, x, y, title); + }, + drawDashedRect: function(width, height, x = 0, y = 0) { return osparc.wrapper.Svg.drawDashedRect(this.__canvas, width, height, x, y); }, 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 a4380c731958..367f470b1d84 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js @@ -132,13 +132,12 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { __dropHereUI: null, __selectionRectInitPos: null, __selectionRectRepr: null, + __rectAnnotationRepr: null, __panning: null, __isDraggingFile: null, __isDraggingLink: null, __annotations: null, - __annotatingNote: null, - __annotatingRect: null, - __annotatingText: null, + __annotating: null, __annotationInitPos: null, __selectedAnnotations: null, __annotationEditor: null, @@ -329,19 +328,19 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { }); temporaryNodeUI.getContentElement().addClass("rotate"); this.__workbenchLayout.add(temporaryNodeUI); - temporaryNodeUI.rect = this.__svgLayer.drawDashedRect(boxWidth, boxHeight); + temporaryNodeUI["rect"] = this.__svgLayer.drawDashedRect(boxWidth, boxHeight); temporaryNodeUI.setLayoutProperties({ left: pos.x + parseInt(boxWidth/2) - parseInt(circleSize/2), top: pos.y + parseInt(boxHeight/2) - parseInt(circleSize/2) }); - osparc.wrapper.Svg.updateItemPos(temporaryNodeUI.rect, pos.x, pos.y); + osparc.wrapper.Svg.updateItemPos(temporaryNodeUI["rect"], pos.x, pos.y); return temporaryNodeUI; }, __removeTemporaryNodeUI: function(temporaryNodeUI) { temporaryNodeUI.exclude(); - osparc.wrapper.Svg.removeItem(temporaryNodeUI.rect); + osparc.wrapper.Svg.removeItem(temporaryNodeUI["rect"]); this.__workbenchLayout.add(temporaryNodeUI); temporaryNodeUI = null; }, @@ -524,7 +523,13 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { __addAnnotationListeners: function(annotation) { annotation.addListener("annotationStartedMoving", () => { - this.__selectAnnotation(annotation); + if ([ + osparc.workbench.Annotation.TYPES.NOTE, + osparc.workbench.Annotation.TYPES.RECT, + osparc.workbench.Annotation.TYPES.TEXT, + ].includes(annotation.getType())) { + this.__selectAnnotation(annotation); + } this.__itemStartedMoving(); }, this); @@ -1183,18 +1188,10 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { }, __renderAnnotations: function(studyUI) { - const initData = studyUI.getAnnotationsInitData(); - const annotations = initData ? initData : studyUI.getAnnotations(); - Object.entries(annotations).forEach(([annotationId, annotation]) => { - if (annotation instanceof osparc.workbench.Annotation) { - this.__addAnnotation(annotation.serialize(), annotationId); - } else { - this.__addAnnotation(annotation, annotationId); - } + const annotations = studyUI.getAnnotations(); + Object.values(annotations).forEach(annotation => { + this.__renderAnnotation(annotation); }); - if (initData) { - studyUI.nullAnnotationsInitData(); - } }, __setSelectedItem: function(newID) { @@ -1215,16 +1212,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { edge.setSelected(true); } else if (this.__isSelectedItemAnAnnotation()) { const annotation = this.__getAnnotation(newID); - this.__setSelectedAnnotations([annotation]); - const annotationEditor = this.__getAnnotationEditorView(); - annotationEditor.setAnnotation(annotation); - annotationEditor.makeItModal(); - annotationEditor.addListener("deleteAnnotation", () => { - annotationEditor.exclude(); - this.__removeAnnotation(annotation.getId()); - this.resetSelection(); - }, this); - annotation.addListener("changeColor", e => this.__annotationLastColor = e.getData()); + this.__annotationSelected(annotation); } else { this.fireDataEvent("changeSelectedNode", newID); } @@ -1246,6 +1234,28 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { return this.__isSelectedItemAnAnnotation() ? this.__annotations[this.__selectedItemId] : null; }, + __annotationSelected: function(annotation) { + this.__setSelectedAnnotations([annotation]); + switch (annotation.getType()) { + case osparc.workbench.Annotation.TYPES.CONVERSATION: { + this.__popUpConversation(annotation.getAttributes()["conversationId"], annotation.getId()); + break; + } + default: { + const annotationEditor = this.__getAnnotationEditorView(); + annotationEditor.setAnnotation(annotation); + annotationEditor.makeItModal(); + annotationEditor.addListener("deleteAnnotation", () => { + annotationEditor.exclude(); + this.__removeAnnotation(annotation.getId()); + this.resetSelection(); + }, this); + annotation.addListener("changeColor", e => this.__annotationLastColor = e.getData()); + break; + } + } + }, + __scaleCoordinates: function(x, y) { return { x: parseInt(x / this.getScale()), @@ -1408,7 +1418,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { __mouseDownOnSVG: function(e) { if (e.isLeftPressed()) { - if (this.__annotatingNote || this.__annotatingRect || this.__annotatingText) { + if (this.__annotating) { this.__annotationInitPos = this.__pointerEventToWorkbenchPos(e); } else { this.__selectionRectInitPos = this.__pointerEventToWorkbenchPos(e); @@ -1419,7 +1429,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { __mouseMove: function(e) { if (this.__isDraggingLink) { this.__draggingLink(e, true); - } else if (this.__tempEdgeRepr === null && (this.__annotatingNote || this.__annotatingRect || this.__annotatingText) && this.__annotationInitPos && e.isLeftPressed()) { + } else if (this.__tempEdgeRepr === null && this.__annotating && this.__annotationInitPos && e.isLeftPressed()) { this.__drawingAnnotation(e); } else if (this.__tempEdgeRepr === null && this.__selectionRectInitPos && e.isLeftPressed()) { this.__drawingSelectionRect(e); @@ -1449,25 +1459,8 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { if (this.__annotationInitPos) { this.__annotationInitPos = null; } - if (this.__annotatingNote || this.__annotatingRect || this.__annotatingText) { - let annotationType = null; - if (this.__annotatingNote) { - annotationType = "note"; - } else if (this.__annotatingRect) { - annotationType = "rect"; - } else if (this.__annotatingText) { - annotationType = "text"; - } - if (this.__consolidateAnnotation(annotationType, annotationInitPos, this.__rectAnnotationRepr)) { - if (this.__rectAnnotationRepr) { - osparc.wrapper.Svg.removeItem(this.__rectAnnotationRepr); - this.__rectAnnotationRepr = null; - } - this.__annotatingNote = false; - this.__annotatingRect = false; - this.__annotatingText = false; - this.__toolHint.setValue(null); - } + if (this.__annotating) { + this.__consolidateAnnotation(annotationInitPos); } if (this.__panning) { @@ -1603,23 +1596,27 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { }, startAnnotationsNote: function() { - this.__annotatingNote = true; - this.__annotatingRect = false; - this.__annotatingText = false; + this.__annotating = osparc.workbench.Annotation.TYPES.NOTE; this.__toolHint.setValue(this.tr("Pick the position")); }, startAnnotationsRect: function() { - this.__annotatingNote = false; - this.__annotatingRect = true; - this.__annotatingText = false; + this.__annotating = osparc.workbench.Annotation.TYPES.RECT; this.__toolHint.setValue(this.tr("Draw a rectangle")); }, startAnnotationsText: function(workbenchPos) { - this.__annotatingNote = false; - this.__annotatingText = true; - this.__annotatingRect = false; + this.__annotating = osparc.workbench.Annotation.TYPES.TEXT; + if (workbenchPos) { + this.__annotationInitPos = workbenchPos; + this.__mouseUp(); + } else { + this.__toolHint.setValue(this.tr("Pick the position")); + } + }, + + startConversation: function(workbenchPos) { + this.__annotating = osparc.workbench.Annotation.TYPES.CONVERSATION; if (workbenchPos) { this.__annotationInitPos = workbenchPos; this.__mouseUp(); @@ -1821,7 +1818,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { }); dropHereNodeUI.exclude(); this.__workbenchLayout.add(dropHereNodeUI); - dropHereNodeUI.rect = this.__svgLayer.drawDashedRect(boxWidth, boxHeight); + dropHereNodeUI["rect"] = this.__svgLayer.drawDashedRect(boxWidth, boxHeight); } let dropHere = this.__dropHereUI; if (show) { @@ -1832,11 +1829,11 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { top: posY - parseInt(dropMeBounds.height/2)- parseInt(boxHeight/2) }); if ("rect" in dropHere) { - osparc.wrapper.Svg.updateItemPos(dropHere.rect, posX - boxWidth, posY - boxHeight); + osparc.wrapper.Svg.updateItemPos(dropHere["rect"], posX - boxWidth, posY - boxHeight); } } else { dropHere.exclude(); - osparc.wrapper.Svg.removeItem(dropHere.rect); + osparc.wrapper.Svg.removeItem(dropHere["rect"]); dropHere = null; } }, @@ -1902,87 +1899,133 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { const y = Math.min(initPos.y, currentPos.y); const width = Math.abs(initPos.x - currentPos.x); const height = Math.abs(initPos.y - currentPos.y); - if ([null, undefined].includes(this.__rectAnnotationRepr)) { + if (this.__rectAnnotationRepr) { + osparc.wrapper.Svg.updateRect(this.__rectAnnotationRepr, width, height, x, y); + } else { const color = this.__annotationLastColor ? this.__annotationLastColor : osparc.workbench.Annotation.DEFAULT_COLOR; this.__rectAnnotationRepr = this.__svgLayer.drawAnnotationRect(width, height, x, y, color); - } else { - osparc.wrapper.Svg.updateRect(this.__rectAnnotationRepr, width, height, x, y); } }, - __consolidateAnnotation: function(type, initPos, annotation) { + __consolidateAnnotation: function(initPos) { + const annotationTypes = osparc.workbench.Annotation.TYPES; + if (type === annotationTypes.RECT && !this.__rectAnnotationRepr) { + osparc.FlashMessenger.logAs(this.tr("Draw a rectangle first"), "WARNING"); + return; + } + + const type = this.__annotating; const color = this.__annotationLastColor ? this.__annotationLastColor : osparc.workbench.Annotation.DEFAULT_COLOR; const serializeData = { type, color, attributes: {} }; - if (type === "rect") { - if ([null, undefined].includes(annotation)) { - osparc.FlashMessenger.logAs(this.tr("Draw a rectangle first"), "WARNING"); - return false; - } - serializeData.attributes = osparc.wrapper.Svg.getRectAttributes(annotation); + if (type === annotationTypes.RECT) { + serializeData.attributes = osparc.wrapper.Svg.getRectAttributes(this.__rectAnnotationRepr); } else { serializeData.attributes = initPos; } - if (type === "note") { - const noteEditor = new osparc.editor.AnnotationNoteCreator(this.getStudy()); - const win = osparc.editor.AnnotationNoteCreator.popUpInWindow(noteEditor); - noteEditor.addListener("addNote", () => { - const gid = noteEditor.getRecipientGid(); - serializeData.attributes.recipientGid = gid; - serializeData.attributes.text = noteEditor.getNote(); - const user = osparc.store.Groups.getInstance().getUserByGroupId(gid) - if (user) { - osparc.notification.Notifications.pushNewAnnotationNote(user.getUserId(), this.getStudy().getUuid()); - } + + switch (type) { + case annotationTypes.NOTE: { + const noteEditor = new osparc.editor.AnnotationNoteCreator(this.getStudy()); + const win = osparc.editor.AnnotationNoteCreator.popUpInWindow(noteEditor); + noteEditor.addListener("addNote", () => { + const gid = noteEditor.getRecipientGid(); + serializeData.attributes.recipientGid = gid; + serializeData.attributes.text = noteEditor.getNote(); + const user = osparc.store.Groups.getInstance().getUserByGroupId(gid) + if (user) { + osparc.notification.Notifications.pushNewAnnotationNote(user.getUserId(), this.getStudy().getUuid()); + } + this.__addAnnotation(serializeData); + win.close(); + }, this); + noteEditor.addListener("cancel", () => win.close()); + break; + } + case annotationTypes.RECT: { this.__addAnnotation(serializeData); - win.close(); - }, this); - noteEditor.addListener("cancel", () => win.close()); - } else if (type === "rect") { - this.__addAnnotation(serializeData); - } else if (type === "text") { - const tempAnnotation = new osparc.workbench.Annotation(null, { - type: "text", - color, - attributes: { - text: "", - fontSize: 12 + if (this.__rectAnnotationRepr) { + osparc.wrapper.Svg.removeItem(this.__rectAnnotationRepr); + this.__rectAnnotationRepr = null; } - }); - const annotationEditor = new osparc.editor.AnnotationEditor(tempAnnotation); - annotationEditor.addAddButtons(); - tempAnnotation.addListener("changeColor", e => this.__annotationLastColor = e.getData()); - annotationEditor.addListener("appear", () => { - const textField = annotationEditor.getChildControl("text-field"); - textField.focus(); - textField.activate(); - }); - const win = osparc.ui.window.Window.popUpInWindow(annotationEditor, "Add Text Annotation", 220, 135).set({ - clickAwayClose: true, - showClose: true - }); - annotationEditor.addListener("addAnnotation", () => { - win.close(); - const form = annotationEditor.getForm(); - serializeData.attributes.text = form.getItem("text").getValue(); - serializeData.attributes.color = form.getItem("color").getValue(); - serializeData.color = form.getItem("color").getValue(); - serializeData.attributes.fontSize = form.getItem("size").getValue(); - this.__addAnnotation(serializeData); - }, this); - win.open(); + break; + } + case annotationTypes.TEXT: { + const tempAnnotation = new osparc.workbench.Annotation({ + type: annotationTypes.TEXT, + color, + attributes: { + text: "", + fontSize: 12 + } + }); + const annotationEditor = new osparc.editor.AnnotationEditor(tempAnnotation); + annotationEditor.addAddButtons(); + tempAnnotation.addListener("changeColor", e => this.__annotationLastColor = e.getData()); + annotationEditor.addListener("appear", () => { + const textField = annotationEditor.getChildControl("text-field"); + textField.focus(); + textField.activate(); + }); + const win = osparc.ui.window.Window.popUpInWindow(annotationEditor, "Add Text Annotation", 220, 135).set({ + clickAwayClose: true, + showClose: true + }); + annotationEditor.addListener("addAnnotation", () => { + win.close(); + const form = annotationEditor.getForm(); + serializeData.attributes.text = form.getItem("text").getValue(); + serializeData.attributes.color = form.getItem("color").getValue(); + serializeData.color = form.getItem("color").getValue(); + serializeData.attributes.fontSize = form.getItem("size").getValue(); + this.__addAnnotation(serializeData); + }, this); + win.open(); + break; + } + case annotationTypes.CONVERSATION: { + const conversationTitle = `${initPos.x}, ${initPos.y}`; + osparc.store.Conversations.getInstance().addConversation(this.getStudy().getUuid(), conversationTitle, osparc.study.Conversations.TYPES.PROJECT_ANNOTATION) + .then(conversationData => { + serializeData.attributes.conversationId = conversationData["conversationId"]; + serializeData.attributes.text = conversationData["name"]; + const annotation = this.__addAnnotation(serializeData); + this.__popUpConversation(conversationData["conversationId"], annotation.getId()); + }); + break; + } } - return true; + + this.__annotating = null; + this.__toolHint.setValue(null); + }, + + __addAnnotation: function(annotationData, id) { + const annotation = new osparc.workbench.Annotation(annotationData, id); + this.getStudy().getUi().addAnnotation(annotation); + + this.__renderAnnotation(annotation); + + return annotation; }, - __addAnnotation: function(data, id) { - const annotation = new osparc.workbench.Annotation(this.__svgLayer, data, id); + __renderAnnotation: function(annotation) { + annotation.setSvgCanvas(this.__svgLayer); + this.__addAnnotationListeners(annotation); this.__annotations[annotation.getId()] = annotation; - this.getStudy().getUi().addAnnotation(annotation); + + if (annotation.getType() === osparc.workbench.Annotation.TYPES.CONVERSATION) { + osparc.store.Conversations.getInstance().addListener("conversationDeleted", e => { + const data = e.getData(); + if (annotation.getAttributes()["conversationId"] === data["conversationId"]) { + this.__removeAnnotation(annotation.getId()); + } + }, this); + } }, __removeAnnotation: function(id) { @@ -1993,6 +2036,28 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { } }, + __popUpConversation: function(conversationId, annotationId) { + 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) + .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({ + caption: this.tr("Conversation not found"), + confirmText: this.tr("Delete"), + confirmAction: "delete", + }); + win.open(); + win.addListener("close", () => { + if (win.getConfirmed()) { + this.__removeAnnotation(annotationId); + } + }); + } + }); + }, + __dropFile: async function(e) { this.__draggingFile(e, false); diff --git a/services/static-webserver/client/source/class/osparc/wrapper/Svg.js b/services/static-webserver/client/source/class/osparc/wrapper/Svg.js index 7177f74480c7..0cecafe73440 100644 --- a/services/static-webserver/client/source/class/osparc/wrapper/Svg.js +++ b/services/static-webserver/client/source/class/osparc/wrapper/Svg.js @@ -274,12 +274,105 @@ qx.Class.define("osparc.wrapper.Svg", { return rect; }, - updateText: function(representation, label) { + drawAnnotationConversation: function(draw, x = 50, y = 50, title = "Conversation") { + const color = qx.theme.manager.Color.getInstance().getTheme().colors["text"]; + const bubbleWidth = 150; + const bubbleHeight = 30; + const padding = 6; + + // Group to keep all elements together + const bubble = draw.group(); + bubble.move(x, y); + + // Rounded rectangle as the base + const rect = draw.rect(bubbleWidth, bubbleHeight) + .radius(4) + .fill("none") + .stroke({ + color, + width: 1.5, + }); + bubble.add(rect); + + // Icon (simple speech bubble using path or text) + const iconSize = 16; + const icon = draw.text('💬') + .font({ + size: iconSize + }) + .attr({ + cursor: "pointer" + }) + .move(padding, (bubbleHeight - iconSize) / 2); + bubble.add(icon); + + // Title text + const titleFontSize = 12; + const defaultFont = osparc.utils.Utils.getDefaultFont(); + const label = draw.text(title) + .font({ + fill: color, + size: titleFontSize, + family: defaultFont["family"], + anchor: 'start' + }) + .attr({ + cursor: "pointer" + }) + .move(padding + iconSize + 8, ((bubbleHeight - titleFontSize) / 2) - 3); + bubble.add(label); + bubble.label = label; // store reference for renaming + + // Compute available width for text + const availableWidth = bubbleWidth - padding * 2 - iconSize - 8; + + // Helper: truncate text with ellipsis + const fitTextWithEllipsis = (fullText, maxWidth) => { + let text = fullText; + label.text(text); + if (label.bbox().width <= maxWidth) { + return text + }; + + const ellipsis = '…'; + let low = 0; + let high = text.length; + // Binary search for the max fitting length + while (low < high) { + const mid = Math.floor((low + high) / 2); + label.text(text.slice(0, mid) + ellipsis); + if (label.bbox().width <= maxWidth) { + low = mid + 1; + } else { + high = mid; + } + } + return text.slice(0, low - 1) + ellipsis; + } + + // Truncate if needed + const fittedText = fitTextWithEllipsis(title, availableWidth); + label.text(fittedText); + + // Move label to proper position + label.move(padding + iconSize + 8, ((bubbleHeight - titleFontSize) / 2) - 3); + + bubble.back(); + + bubble["clickables"] = [icon, label]; + + return bubble; + }, + + updateText: function(representation, newText) { if (representation.type === "text") { - representation.text(label); + representation.text(newText); } else if (representation.type === "svg") { // nested - representation["textChild"].innerText = label; + representation["textChild"].innerText = newText; + } else if (representation.type === "g") { + // group + representation.label.text(newText); } },