diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index 8a268861358..70b9abda163 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -812,7 +812,7 @@ qx.Class.define("osparc.dashboard.CardBase", { this.setBlocked(projectLocked ? "IN_USE" : false); if (projectLocked) { - this.__showBlockedCardFromStatus("IN_USE", state["shareState"]); + this.__showBlockedCardFromStatus("IN_USE", state); } if (pipelineState) { @@ -896,10 +896,10 @@ qx.Class.define("osparc.dashboard.CardBase", { avatarGroup.setUserGroupIds(currentUserGroupIds); }, - __showBlockedCardFromStatus: function(reason, shareState) { + __showBlockedCardFromStatus: function(reason, state) { switch (reason) { case "IN_USE": - this.__blockedInUse(shareState); + this.__blockedInUse(state); break; case "IN_DEBT": this.__blockedInDebt(); @@ -907,9 +907,9 @@ qx.Class.define("osparc.dashboard.CardBase", { } }, - __blockedInUse: function(shareState) { - const status = shareState["status"]; - const currentUserGroupIds = shareState["currentUserGroupids"]; + __blockedInUse: function(state) { + const projectStatus = osparc.study.Utils.state.getProjectStatus(state); + const currentUserGroupIds = osparc.study.Utils.state.getCurrentGroupIds(state); const usersStore = osparc.store.Users.getInstance(); const userPromises = currentUserGroupIds.map(userGroupId => usersStore.getUser(userGroupId)); const usernames = []; @@ -925,7 +925,7 @@ qx.Class.define("osparc.dashboard.CardBase", { console.error("Failed to fetch user data for avatars:", error); }) .finally(() => { - switch (status) { + switch (projectStatus) { case "CLOSING": image = "@FontAwesome5Solid/key/"; toolTip += this.tr("Closing..."); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 5855044dc11..acc86c34cdd 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -81,9 +81,10 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__resourceModel["resourceType"] = resourceData["resourceType"]; this.__addPages(); } - if (resourceData["functionClass"] === osparc.data.model.Function.FUNCTION_CLASS.PROJECT) { + // use latestResourceData, resourceData doesn't have the functionClass nor the templateId + if (latestResourceData["functionClass"] === osparc.data.model.Function.FUNCTION_CLASS.PROJECT) { // this is only required for functions that have a template linked - osparc.store.Templates.fetchTemplate(resourceData["templateId"]) + osparc.store.Templates.fetchTemplate(latestResourceData["templateId"]) .then(templateData => { // prefetch function's underlying template's services metadata osparc.store.Services.getStudyServicesMetadata(templateData) @@ -592,7 +593,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { page.setEnabled(enabled); const lazyLoadContent = () => { - const resourceModel = osparc.utils.Resources.isFunction(this.__resourceData) ? this.__resourceModel.getTemplate() : this.__resourceData; + const resourceModel = osparc.utils.Resources.isFunction(this.__resourceData) ? this.__resourceModel.getTemplate() : this.__resourceModel; const preview = new osparc.study.StudyPreview(resourceModel); page.addToContent(preview); this.__widgets.push(preview); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Node.js b/services/static-webserver/client/source/class/osparc/data/model/Node.js index 048f69ebfc0..0e40eface10 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Node.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Node.js @@ -98,11 +98,6 @@ qx.Class.define("osparc.data.model.Node", { event: "changeLabel" }, - inputAccess: { - check: "Object", - nullable: true - }, - dynamicV2: { check: "Boolean", init: false, @@ -171,7 +166,8 @@ qx.Class.define("osparc.data.model.Node", { check: "qx.core.Object", init: null, nullable: true, - event: "changeMarker" + event: "changeMarker", + apply: "__applyMarker", }, inputConnected: { @@ -198,6 +194,7 @@ qx.Class.define("osparc.data.model.Node", { events: { "updateStudyDocument": "qx.event.type.Event", + "projectDocumentChanged": "qx.event.type.Data", "reloadModel": "qx.event.type.Event", "retrieveInputs": "qx.event.type.Data", "keyChanged": "qx.event.type.Event", @@ -213,6 +210,33 @@ qx.Class.define("osparc.data.model.Node", { }, statics: { + // Properties of the Node class that should not be listened to + ListenChangesProps: [ + // "study", // immutable + "key", + "version", + // "nodeId", // immutable + "label", + "inputs", // own listener + "inputsUnits", // own listener + // "dynamicV2", // frontend only + // "serviceUrl", // frontend only + // "portsConnected", // frontend only + "outputs", // listen to changes only if this is a frontend node + // "status", // backend driven + // "errors", // frontend only + "bootOptions", + // "propsForm", // frontend only + // "outputsForm", // frontend only + // "marker", // own listener + // "inputConnected", // frontend only + // "outputConnected", // frontend only + // "logger", // frontend only + "inputNodes", // !! not a property but goes into the model + "inputsRequired", // !! not a property but goes into the model + "progress", // !! not a property but goes into the model + ], + isFrontend: function(metadata) { return (metadata && metadata.key && metadata.key.includes("/frontend/")); }, @@ -406,15 +430,19 @@ qx.Class.define("osparc.data.model.Node", { return this.__metaData; }, + hasPropsForm: function() { + return this.isPropertyInitialized("propsForm") && this.getPropsForm(); + }, + __getInputData: function() { - if (this.isPropertyInitialized("propsForm") && this.getPropsForm()) { + if (this.hasPropsForm()) { return this.getPropsForm().getValues(); } return {}; }, __getInputUnits: function() { - if (this.isPropertyInitialized("propsForm") && this.getPropsForm()) { + if (this.hasPropsForm()) { const changedUnits = this.getPropsForm().getChangedXUnits(); if (Object.keys(changedUnits).length) { return changedUnits; @@ -503,8 +531,11 @@ qx.Class.define("osparc.data.model.Node", { populateInputOutputData: function(nodeData) { this.__setInputData(nodeData.inputs); this.__setInputUnits(nodeData.inputsUnits); - this.__setInputDataAccess(nodeData.inputAccess); if (this.getPropsForm()) { + const study = this.getStudy(); + if (study && study.isReadOnly()) { + this.getPropsForm().setEnabled(false); + } this.getPropsForm().makeInputsDynamic(); } this.setOutputData(nodeData.outputs); @@ -669,6 +700,31 @@ qx.Class.define("osparc.data.model.Node", { this.setMarker(null); }, + __applyMarker: function(marker) { + if (marker) { + this.fireDataEvent("projectDocumentChanged", { + "op": "add", + "path": `/ui/workbench/${this.getNodeId()}/marker`, + "value": marker.getColor(), + "osparc-resource": "ui", + }); + marker.addListener("changeColor", e => { + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/ui/workbench/${this.getNodeId()}/marker`, + "value": e.getData(), + "osparc-resource": "ui", + }); + }); + } else { + this.fireDataEvent("projectDocumentChanged", { + "op": "delete", + "path": `/ui/workbench/${this.getNodeId()}/marker`, + "osparc-resource": "ui", + }); + } + }, + __setInputData: function(inputs) { if (this.__settingsForm && inputs) { const inputData = {}; @@ -692,18 +748,6 @@ qx.Class.define("osparc.data.model.Node", { } }, - __setInputDataAccess: function(inputAccess) { - if (inputAccess) { - this.setInputAccess(inputAccess); - this.getPropsForm().setAccessLevel(inputAccess); - } - - const study = this.getStudy(); - if (study && study.isReadOnly() && this.getPropsForm()) { - this.getPropsForm().setEnabled(false); - } - }, - setOutputData: function(outputs) { if (outputs) { let hasOutputs = false; @@ -733,7 +777,8 @@ qx.Class.define("osparc.data.model.Node", { this.getStatus().setModified(false); } - this.fireDataEvent("changeOutputs", this.getOutputs()); + // event was fired in the outputs setter + // this.fireDataEvent("changeOutputs", this.getOutputs()); } }, @@ -880,7 +925,7 @@ qx.Class.define("osparc.data.model.Node", { if (index > -1) { // remove node connection this.__inputNodes.splice(index, 1); - this.fireDataEvent("changeInputNodes"); + this.fireEvent("changeInputNodes"); return true; } return false; @@ -1186,13 +1231,25 @@ qx.Class.define("osparc.data.model.Node", { }, setPosition: function(pos) { - const { - x, - y - } = pos; + const {x, y} = pos; + if (x === this.__posX && y === this.__posY) { + return; // no change + } + // keep positions positive this.__posX = parseInt(x) < 0 ? 0 : parseInt(x); this.__posY = parseInt(y) < 0 ? 0 : parseInt(y); + + const nodeId = this.getNodeId(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/ui/workbench/${nodeId}/position`, + "value": { + "x": this.__posX, + "y": this.__posY, + }, + "osparc-resource": "ui", + }); }, getPosition: function() { @@ -1241,7 +1298,121 @@ qx.Class.define("osparc.data.model.Node", { } }, - serialize: function(clean = true) { + listenToChanges: function() { + const nodeId = this.getNodeId(); + const propertyKeys = Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.Node)); + this.self().ListenChangesProps.forEach(key => { + switch (key) { + case "inputs": + if (this.hasPropsForm()) { + // listen to changes in the props form + this.getPropsForm().addListener("changeData", () => { + const data = this.__getInputData(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/inputs`, + "value": data, + "osparc-resource": "node", + }); + }); + // listen to changes in link and unlink of ports + this.getPropsForm().addListener("linkFieldModified", () => { + const data = this.__getInputData(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/inputs`, + "value": data, + "osparc-resource": "node", + }); + }); + } + break; + case "inputsUnits": + if (this.hasPropsForm()) { + this.getPropsForm().addListener("unitChanged", () => { + const data = this.__getInputUnits(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/inputsUnits`, + "value": data, + "osparc-resource": "node", + }); + }); + } + break; + case "inputNodes": + this.addListener("changeInputNodes", () => { + const data = this.getInputNodes(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/inputNodes`, + "value": data, + "osparc-resource": "node", + }); + }, this); + break; + case "inputsRequired": + this.addListener("changeInputsRequired", () => { + const data = this.getInputsRequired(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/inputsRequired`, + "value": data, + "osparc-resource": "node", + }); + }, this); + break; + case "progress": + if (this.isFilePicker()) { + this.getStatus().addListener("changeProgress", e => { + const data = e.getData(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/progress`, + "value": data, + "osparc-resource": "node", + }); + }); + } + break; + case "outputs": + if (this.isFilePicker() || this.isParameter()) { + this.addListener("changeOutputs", e => { + let data = e.getData(); + if (this.isFilePicker()) { + data = osparc.file.FilePicker.serializeOutput(this.getOutputs()); + } else if (this.isParameter()) { + data = this.__getOutputsData(); + } + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/outputs`, + "value": data, + "osparc-resource": "node", + }); + }, this); + } + break; + default: + if (propertyKeys.includes(key)) { + this.addListener("change" + qx.lang.String.firstUp(key), e => { + const data = e.getData(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/` + key, + "value": data, + "osparc-resource": "node", + }); + }, this); + } else { + console.error(`Property "${key}" is not a valid property of osparc.data.model.Node`); + } + break; + } + }); + }, + + serialize: function() { // node generic let nodeEntry = { key: this.getKey(), @@ -1249,16 +1420,10 @@ qx.Class.define("osparc.data.model.Node", { label: this.getLabel(), inputs: this.__getInputData(), inputsUnits: this.__getInputUnits(), - inputAccess: this.getInputAccess(), inputNodes: this.getInputNodes(), inputsRequired: this.getInputsRequired(), bootOptions: this.getBootOptions() }; - if (!clean) { - nodeEntry.progress = this.getStatus().getProgress(); - nodeEntry.outputs = this.__getOutputsData(); - nodeEntry.state = this.getStatus().serialize(); - } if (this.isFilePicker()) { nodeEntry.outputs = osparc.file.FilePicker.serializeOutput(this.getOutputs()); @@ -1276,6 +1441,18 @@ qx.Class.define("osparc.data.model.Node", { } return filteredNodeEntry; - } + }, + + serializeUI: function() { + const uiInfo = {} + uiInfo["position"] = this.getPosition(); + const marker = this.getMarker(); + if (marker) { + uiInfo["marker"] = { + "color": marker.getColor() + }; + } + return uiInfo; + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Service.js b/services/static-webserver/client/source/class/osparc/data/model/Service.js index d7f37db2e00..d260144d4c0 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Service.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Service.js @@ -147,7 +147,6 @@ qx.Class.define("osparc.data.model.Service", { nullable: false }, - // ------ ignore for serializing ------ xType: { check: "String", nullable: true, @@ -160,30 +159,6 @@ qx.Class.define("osparc.data.model.Service", { init: 0, event: "changeHits", nullable: false - } - // ------ ignore for serializing ------ - }, - - statics: { - IgnoreSerializationProps: [ - "xType", - "hits", - ] + }, }, - - members: { - __serviceData: null, - - serialize: function() { - let jsonObject = {}; - const propertyKeys = this.self().getProperties(); - propertyKeys.forEach(key => { - if (this.self().IgnoreSerializationProps.includes(key)) { - return; - } - jsonObject[key] = this.get(key); - }); - return jsonObject; - }, - } }); 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 984fa029f5a..7e9cc12053b 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 @@ -215,7 +215,6 @@ qx.Class.define("osparc.data.model.Study", { event: "changeTemplateType" }, - // ------ ignore for serializing ------ state: { check: "Object", nullable: true, @@ -256,10 +255,44 @@ qx.Class.define("osparc.data.model.Study", { event: "changeSavePending", init: false }, - // ------ ignore for serializing ------ + }, + + events: { + "projectDocumentChanged": "qx.event.type.Data", }, statics: { + // Properties of the Study class that should not be listened to + ListenChangesProps: [ + // "uuid", // immutable + // "workspaceId", // own patch + // "folderId", // own patch + "name", + "description", + // "prjOwner", // immutable + // "accessRights", // own patch + // "creationDate", // immutable + // "lastChangeDate", // backend sets it + "thumbnail", + "workbench", // own listener + "ui", // own listener + // "tags", // own patch + // "classifiers", // own patch + // "quality", // own patch + // "permalink", // backend sets it + "dev", + // "type", // immutable + "templateType", + // "state", // backend sets it + // "pipelineRunning", // backend sets it + // "readOnly", // frontend only + // "trashedAt", // backend sets it + // "trashedBy", // backend sets it + // "savePending", // frontend only + ], + + // Properties of the Study class that should not be serialized + // when serializing the study object to send it to the backend IgnoreSerializationProps: [ "permalink", "state", @@ -269,10 +302,6 @@ qx.Class.define("osparc.data.model.Study", { "savePending", ], - IgnoreModelizationProps: [ - "dev" - ], - OwnPatch: [ "accessRights", "workbench" @@ -361,21 +390,46 @@ qx.Class.define("osparc.data.model.Study", { } return overallProgress/nCompNodes; }, - - isRunning: function(state) { - return [ - "PUBLISHED", - "PENDING", - "WAITING_FOR_RESOURCES", - "WAITING_FOR_CLUSTER", - "STARTED", - "RETRY" - ].includes(state); - }, }, members: { - serialize: function(clean = true) { + listenToChanges: function() { + const propertyKeys = this.self().getProperties(); + this.self().ListenChangesProps.forEach(key => { + switch (key) { + case "workbench": + this.getWorkbench().addListener("projectDocumentChanged", e => { + const data = e.getData(); + this.fireDataEvent("projectDocumentChanged", data); + }, this); + break; + case "ui": + this.getUi().listenToChanges(); + this.getUi().addListener("projectDocumentChanged", e => { + const data = e.getData(); + this.fireDataEvent("projectDocumentChanged", data); + }, this); + break; + default: + if (propertyKeys.includes(key)) { + this.addListener("change" + qx.lang.String.firstUp(key), e => { + const data = e.getData(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": "/" + key, + "value": data, + "osparc-resource": "study", + }); + }, this); + } else { + console.error(`Property "${key}" is not a valid property of osparc.data.model.Study`); + } + break; + } + }); + }, + + serialize: function() { let jsonObject = {}; const propertyKeys = this.self().getProperties(); propertyKeys.forEach(key => { @@ -383,7 +437,7 @@ qx.Class.define("osparc.data.model.Study", { return; } if (key === "workbench") { - jsonObject[key] = this.getWorkbench().serialize(clean); + jsonObject[key] = this.getWorkbench().serialize(); return; } if (key === "ui") { @@ -575,12 +629,7 @@ qx.Class.define("osparc.data.model.Study", { }, __applyState: function(value) { - if (value && "state" in value) { - const isRunning = this.self().isRunning(value["state"]["value"]); - this.setPipelineRunning(isRunning); - } else { - this.setPipelineRunning(false); - } + this.setPipelineRunning(osparc.study.Utils.state.isPipelineRunning(value)); }, getDisableServiceAutoStart: function() { 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 04733b429ff..54ce78cde8d 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 @@ -42,6 +42,15 @@ qx.Class.define("osparc.data.model.StudyUI", { this.addAnnotation(annotation); }); } + + this.getSlideshow().addListener("changeSlideshow", () => { + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": "/ui/slideshow", + "value": this.getSlideshow().serialize(), + "osparc-resource": "study-ui", + }); + }, this); }, properties: { @@ -85,12 +94,24 @@ qx.Class.define("osparc.data.model.StudyUI", { }, }, + events: { + "projectDocumentChanged": "qx.event.type.Data", + }, + statics: { TEMPLATE_TYPE: "TEMPLATE", TUTORIAL_TYPE: "TUTORIAL", HYPERTOOL_TYPE: "HYPERTOOL", HYPERTOOL_ICON: "https://raw.githubusercontent.com/ZurichMedTech/s4l-assets/refs/heads/main/app/icons/hypertool.png", PIPELINE_ICON: "https://raw.githubusercontent.com/ZurichMedTech/s4l-assets/refs/heads/main/app/icons/diagram.png", + + ListenChangesProps: [ + // "workbench", it's handled by osparc.data.model.Workbench + "slideshow", + "currentNodeId", // eventually don't patch it, it is personal, only the last closing sets it + "mode", // eventually don't patch it, it is personal, only the last closing sets it + "annotations", // TODO + ], }, members: { @@ -102,10 +123,30 @@ qx.Class.define("osparc.data.model.StudyUI", { addAnnotation: function(annotation) { this.getAnnotations()[annotation.getId()] = annotation; + this.fireDataEvent("projectDocumentChanged", { + "op": "add", + "path": `/ui/annotations/${annotation.getId()}`, + "value": annotation.serialize(), + "osparc-resource": "study-ui", + }); + annotation.addListener("annotationChanged", () => { + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/ui/annotations/${annotation.getId()}`, + "value": annotation.serialize(), + "osparc-resource": "study-ui", + }); + }, this); }, removeAnnotation: function(annotationId) { if (annotationId in this.getAnnotations()) { + const annotation = this.getAnnotations()[annotationId] + this.fireDataEvent("projectDocumentChanged", { + "op": "delete", + "path": `/ui/annotations/${annotation.getId()}`, + "osparc-resource": "study-ui", + }); delete this.getAnnotations()[annotationId]; } }, @@ -115,6 +156,29 @@ qx.Class.define("osparc.data.model.StudyUI", { this.getSlideshow().removeNode(nodeId); }, + listenToChanges: function() { + const propertyKeys = Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.StudyUI)); + this.self().ListenChangesProps.forEach(key => { + switch (key) { + default: + if (propertyKeys.includes(key)) { + this.addListener(`change${qx.lang.String.firstUp(key)}`, () => { + const data = this.serialize(); + this.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/ui/${key}`, + "value": data, + "osparc-resource": "study-ui", + }); + }, this); + } else { + console.error(`Property "${key}" is not a valid property of osparc.data.model.StudyUI`); + } + break; + } + }); + }, + serialize: function() { const currentStudy = osparc.store.Store.getInstance().getCurrentStudy(); let jsonObject = {}; diff --git a/services/static-webserver/client/source/class/osparc/data/model/Workbench.js b/services/static-webserver/client/source/class/osparc/data/model/Workbench.js index 3e0d3eea769..605e9bd77f2 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Workbench.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Workbench.js @@ -51,6 +51,7 @@ qx.Class.define("osparc.data.model.Workbench", { events: { "updateStudyDocument": "qx.event.type.Event", + "projectDocumentChanged": "qx.event.type.Data", "restartAutoSaveTimer": "qx.event.type.Event", "pipelineChanged": "qx.event.type.Event", "reloadModel": "qx.event.type.Event", @@ -273,6 +274,10 @@ qx.Class.define("osparc.data.model.Workbench", { __createNode: function(study, metadata, uuid) { const node = new osparc.data.model.Node(study, metadata, uuid); + if (osparc.utils.Utils.eventDrivenPatch()) { + node.listenToChanges(); + node.addListener("projectDocumentChanged", e => this.fireDataEvent("projectDocumentChanged", e.getData()), this); + } node.addListener("keyChanged", () => this.fireEvent("reloadModel"), this); node.addListener("changeInputNodes", () => this.fireDataEvent("pipelineChanged"), this); node.addListener("reloadModel", () => this.fireEvent("reloadModel"), this); @@ -541,9 +546,9 @@ qx.Class.define("osparc.data.model.Workbench", { return false; } + this.fireEvent("restartAutoSaveTimer"); let node = this.getNode(nodeId); if (node) { - this.fireEvent("restartAutoSaveTimer"); // remove the node in the backend first const removed = await node.removeNode(); if (removed) { @@ -621,8 +626,10 @@ qx.Class.define("osparc.data.model.Workbench", { const rightNode = this.getNode(rightNodeId); if (rightNode) { // no need to make any changes to a just removed node (it would trigger a patch call) - rightNode.removeInputNode(leftNodeId); + // first remove the port connections rightNode.removeNodePortConnections(leftNodeId); + // then the node connection + rightNode.removeInputNode(leftNodeId); } delete this.__edges[edgeId]; @@ -743,16 +750,15 @@ qx.Class.define("osparc.data.model.Workbench", { } }, - serialize: function(clean = true) { + serialize: function() { if (this.__workbenchInitData !== null) { // workbench is not initialized return this.__workbenchInitData; } - let workbench = {}; - const allModels = this.getNodes(); - const nodes = Object.values(allModels); + const workbench = {}; + const nodes = Object.values(this.getNodes()); for (const node of nodes) { - const data = node.serialize(clean); + const data = node.serialize(); if (data) { workbench[node.getNodeId()] = data; } @@ -765,17 +771,12 @@ qx.Class.define("osparc.data.model.Workbench", { // workbenchUI is not initialized return this.__workbenchUIInitData; } - let workbenchUI = {}; - const nodes = this.getNodes(); - for (const nodeId in nodes) { - const node = nodes[nodeId]; - workbenchUI[nodeId] = {}; - workbenchUI[nodeId]["position"] = node.getPosition(); - const marker = node.getMarker(); - if (marker) { - workbenchUI[nodeId]["marker"] = { - color: marker.getColor() - }; + const workbenchUI = {}; + const nodes = Object.values(this.getNodes()); + for (const node of nodes) { + const data = node.serializeUI(); + if (data) { + workbenchUI[node.getNodeId()] = data; } } return workbenchUI; diff --git a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js index 440ea8db6ee..177fef9a788 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js @@ -81,6 +81,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { }); this.__updatingStudy = 0; + this.__throttledPatchPending = false; }, events: { @@ -106,6 +107,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { statics: { AUTO_SAVE_INTERVAL: 3000, DIFF_CHECK_INTERVAL: 300, + THROTTLE_PATCH_TIME: 500, READ_ONLY_TEXT: qx.locale.Manager.tr("You do not have writing permissions.
Your changes will not be saved."), }, @@ -121,6 +123,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { __updatingStudy: null, __updateThrottled: null, __nodesSlidesTree: null, + __throttledPatchPending: null, setStudyData: function(studyData) { if (this.__settingStudy) { @@ -246,8 +249,13 @@ qx.Class.define("osparc.desktop.StudyEditor", { this.nodeSelected(nodeId); }, this); - workbench.addListener("updateStudyDocument", () => this.updateStudyDocument()); - workbench.addListener("restartAutoSaveTimer", () => this.__restartAutoSaveTimer()); + if (osparc.utils.Utils.eventDrivenPatch()) { + study.listenToChanges(); // this includes the listener on the workbench and ui + study.addListener("projectDocumentChanged", e => this.projectDocumentChanged(e.getData()), this); + } else { + workbench.addListener("updateStudyDocument", () => this.updateStudyDocument()); + workbench.addListener("restartAutoSaveTimer", () => this.__restartAutoSaveTimer()); + } }, __setStudyDataInBackend: function(studyData) { @@ -802,6 +810,11 @@ qx.Class.define("osparc.desktop.StudyEditor", { // ------------------ AUTO SAVER ------------------ __startAutoSaveTimer: function() { + if (osparc.utils.Utils.eventDrivenPatch()) { + // If event driven patch is enabled, auto save is not needed + return; + } + // Save every 3 seconds const timer = this.__autoSaveTimer = new qx.event.Timer(this.self().AUTO_SAVE_INTERVAL); timer.addListener("interval", () => { @@ -829,6 +842,11 @@ qx.Class.define("osparc.desktop.StudyEditor", { // ---------------- SAVING TIMER ------------------ __startSavingTimer: function() { + if (osparc.utils.Utils.eventDrivenPatch()) { + // If event driven patch is enabled, saving timer indicator is not needed + return; + } + const timer = this.__savingTimer = new qx.event.Timer(this.self().DIFF_CHECK_INTERVAL); timer.addListener("interval", () => { if (!osparc.wrapper.WebSocket.getInstance().isConnected()) { @@ -872,7 +890,9 @@ qx.Class.define("osparc.desktop.StudyEditor", { // didStudyChange takes around 0.5ms didStudyChange: function() { const studyDiffs = this.__getStudyDiffs(); - return Boolean(Object.keys(studyDiffs.delta).length); + const changed = Boolean(Object.keys(studyDiffs.delta).length); + this.getStudy().setSavePending(changed); + return changed; }, __checkStudyChanges: function() { @@ -886,6 +906,27 @@ qx.Class.define("osparc.desktop.StudyEditor", { } }, + /** + * @param {JSON Patch} data It will soon be used to patch the project document https://datatracker.ietf.org/doc/html/rfc6902 + */ + projectDocumentChanged: function(data) { + data["userGroupId"] = osparc.auth.Data.getInstance().getGroupId(); + if (osparc.utils.Utils.isDevelopmentPlatform()) { + console.log("projectDocumentChanged", data); + } + + this.getStudy().setSavePending(true); + // throttling: do not update study document right after a change, wait for THROTTLE_PATCH_TIME + if (!this.__throttledPatchPending) { + this.__throttledPatchPending = true; + + setTimeout(() => { + this.updateStudyDocument(); + this.__throttledPatchPending = false; + }, this.self().THROTTLE_PATCH_TIME); + } + }, + updateStudyDocument: function() { if (!osparc.data.model.Study.canIWrite(this.getStudy().getAccessRights())) { return new Promise(resolve => { @@ -893,6 +934,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { }); } + this.getStudy().setSavePending(true); this.__updatingStudy++; const studyDiffs = this.__getStudyDiffs(); return this.getStudy().patchStudyDelayed(studyDiffs.delta, studyDiffs.sourceStudy) @@ -908,6 +950,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { throw error; }) .finally(() => { + this.getStudy().setSavePending(false); this.__updatingStudy--; if (this.__updateThrottled && this.__updatingStudy === 0) { this.__updateThrottled = false; 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 aa01d10340c..0d9e6183018 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -1042,7 +1042,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { vBox.add(header); // INPUTS FORM - if (node.isPropertyInitialized("propsForm") && node.getPropsForm()) { + if (node.hasPropsForm()) { const inputsForm = node.getPropsForm(); const inputs = new osparc.desktop.PanelView(this.tr("Inputs"), inputsForm); inputs._innerContainer.set({ 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 08eedabdd1e..c50fba4456e 100644 --- a/services/static-webserver/client/source/class/osparc/editor/AnnotationEditor.js +++ b/services/static-webserver/client/source/class/osparc/editor/AnnotationEditor.js @@ -166,6 +166,8 @@ qx.Class.define("osparc.editor.AnnotationEditor", { const colorPicker = this.getChildControl("color-picker"); marker.bind("color", colorPicker, "value"); colorPicker.bind("value", marker, "color"); + + this.getChildControl("delete-btn").exclude(); }, addDeleteButton: function() { diff --git a/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js b/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js index 6600cf4706a..c3a89731b08 100644 --- a/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js +++ b/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js @@ -41,7 +41,6 @@ qx.Class.define("osparc.form.renderer.PropForm", { "fileRequested": "qx.event.type.Data", "filePickerRequested": "qx.event.type.Data", "parameterRequested": "qx.event.type.Data", - "changeChildVisibility": "qx.event.type.Event" }, properties: { @@ -545,29 +544,6 @@ qx.Class.define("osparc.form.renderer.PropForm", { }); }, - // overridden - setAccessLevel: function(data) { - const entry = this.self().GRID_POS; - const disableables = osparc.form.renderer.PropFormBase.getDisableables(); - Object.entries(data).forEach(([portId, visibility]) => { - Object.values(entry).forEach(entryPos => { - const layoutElement = this._getLayoutChild(portId, entryPos); - if (layoutElement && layoutElement.child) { - const control = layoutElement.child; - if (control) { - const vis = visibility === this._visibility.hidden ? "excluded" : "visible"; - const enabled = visibility === this._visibility.readWrite; - control.setVisibility(vis); - if (disableables.includes(entryPos)) { - control.setEnabled(enabled); - } - } - } - }); - }); - this.fireEvent("changeChildVisibility"); - }, - setPortErrorMessage: function(portId, msg) { const infoButton = this._getInfoFieldChild(portId); if (infoButton && "child" in infoButton) { diff --git a/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js b/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js index fbc0a9a4077..dce44818824 100644 --- a/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js +++ b/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js @@ -48,6 +48,8 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { grid.setColumnFlex(this.self().GRID_POS.FIELD_LINK_UNLINK, 0); grid.setColumnMinWidth(this.self().GRID_POS.CTRL_FIELD, 50); Object.keys(this.self().GRID_POS).forEach((_, idx) => grid.setColumnAlign(idx, "left", "middle")); + + form.addListener("changeData", e => this.fireDataEvent("changeData", e.getData()), this); }, properties: { @@ -57,6 +59,11 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { } }, + events: { + "changeData": "qx.event.type.Data", + "unitChanged": "qx.event.type.Data", + }, + statics: { GRID_POS: { LABEL: 0, @@ -68,13 +75,6 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { ROW_HEIGHT: 28, - getDisableables: function() { - return [ - this.GRID_POS.LABEL, - this.GRID_POS.CTRL_FIELD - ]; - }, - updateUnitLabelPrefix: function(item) { const { unitShort, @@ -319,13 +319,6 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { return false; }, - /** - * @abstract - */ - setAccessLevel: function() { - throw new Error("Abstract method called!"); - }, - __createInfoWHint: function(hint) { const infoWHint = new osparc.form.PortInfoHint(hint); return infoWHint; @@ -376,6 +369,10 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { } item.setValue(newValue); this.self().updateUnitLabelPrefix(item); + this.fireDataEvent("unitChanged", { + portId: item.key, + prefix: newPrefix, + }); }, _getLayoutChild: function(portId, column) { diff --git a/services/static-webserver/client/source/class/osparc/form/renderer/PropFormEditor.js b/services/static-webserver/client/source/class/osparc/form/renderer/PropFormEditor.js deleted file mode 100644 index 83cef4b5996..00000000000 --- a/services/static-webserver/client/source/class/osparc/form/renderer/PropFormEditor.js +++ /dev/null @@ -1,201 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2020 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -/** - * An extension of the PropFormBase that is able to handle the Access Level of each entry. - */ - -qx.Class.define("osparc.form.renderer.PropFormEditor", { - extend: osparc.form.renderer.PropFormBase, - - /** - * @param form {osparc.form.Auto} form widget to embed - * @param node {osparc.data.model.Node} Node owning the widget - */ - construct: function(form, node) { - this.base(arguments, form, node); - - this.__ctrlRadioButtonsMap = {}; - this.__addAccessLevelRBs(); - }, - - statics: { - GRID_POS: { - ...osparc.form.renderer.PropFormBase.GRID_POS, - ACCESS_LEVEL: Object.keys(osparc.form.renderer.PropFormBase.GRID_POS).length - } - }, - - // eslint-disable-next-line qx-rules/no-refs-in-members - members: { - _accessLevel: { - hidden: 0, - readOnly: 1, - readAndWrite: 2 - }, - - __ctrlRadioButtonsMap: null, - - // overridden - setAccessLevel: function(data) { - for (const key in data) { - const control = this.__getRadioButtonsFieldChild(key); - if (control) { - const group = this.__ctrlRadioButtonsMap[key]; - switch (data[key]) { - case this._visibility.hidden: { - group.setSelection([group.getSelectables()[0]]); - break; - } - case this._visibility.readOnly: { - group.setSelection([group.getSelectables()[1]]); - break; - } - case this._visibility.readWrite: { - group.setSelection([group.getSelectables()[2]]); - break; - } - } - } - } - }, - - linkAdded: function(portId, controlLink) { - let data = this._getCtrlFieldChild(portId); - if (data) { - let child = data.child; - let idx = data.idx; - const layoutProps = child.getLayoutProperties(); - controlLink.oldCtrl = child; - this._removeAt(idx); - this._addAt(controlLink, idx, { - row: layoutProps.row, - column: this.self().GRID_POS.CTRL_FIELD - }); - } - }, - - linkRemoved: function(portId) { - let data = this._getCtrlFieldChild(portId); - if (data) { - let child = data.child; - let idx = data.idx; - const layoutProps = child.getLayoutProperties(); - this._removeAt(idx); - this._addAt(child.oldCtrl, idx, { - row: layoutProps.row, - column: this.self().GRID_POS.CTRL_FIELD - }); - } - }, - - __addAccessLevelRBs: function() { - Object.keys(this._form.getControls()).forEach(portId => { - this.__addAccessLevelRB(portId); - }); - }, - - __addAccessLevelRB: function(portId) { - const rbHidden = new qx.ui.form.RadioButton(this.tr("Not Visible")); - rbHidden.accessLevel = this._visibility.hidden; - rbHidden.portId = portId; - const rbReadOnly = new qx.ui.form.RadioButton(this.tr("Read Only")); - rbReadOnly.accessLevel = this._visibility.readOnly; - rbReadOnly.portId = portId; - const rbEditable = new qx.ui.form.RadioButton(this.tr("Editable")); - rbEditable.accessLevel = this._visibility.readWrite; - rbEditable.portId = portId; - - const groupBox = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); - groupBox.add(rbHidden); - groupBox.add(rbReadOnly); - groupBox.add(rbEditable); - - const group = new qx.ui.form.RadioGroup(rbHidden, rbReadOnly, rbEditable); - group.setSelection([rbEditable]); - this.__ctrlRadioButtonsMap[portId] = group; - group.addListener("changeSelection", this.__onAccessLevelChanged, this); - - const ctrlField = this._getCtrlFieldChild(portId); - if (ctrlField) { - const idx = ctrlField.idx; - const child = ctrlField.child; - const layoutProps = child.getLayoutProperties(); - this._addAt(groupBox, idx, { - row: layoutProps.row, - column: this.self().GRID_POS.ACCESS_LEVEL - }); - } - }, - - __onAccessLevelChanged: function(e) { - const selectedButton = e.getData()[0]; - const { - accessLevel, - portId - } = selectedButton; - - const data = {}; - data[portId] = accessLevel; - - this.__setAccessLevel(data); - - let inputAccess = this.getNode().getInputAccess(); - if (inputAccess === null) { - inputAccess = {}; - } - inputAccess[portId] = accessLevel; - this.getNode().setInputAccess(inputAccess); - - const propsForm = this.getNode().getPropsForm(); - propsForm.setAccessLevel(data); - }, - - __addDelTag: function(label) { - const newLabel = "" + label + ""; - return newLabel; - }, - - __removeDelTag: function(label) { - let newLabel = label.replace("", ""); - newLabel = newLabel.replace("", ""); - return newLabel; - }, - - __setAccessLevel: function(data) { - for (const key in data) { - const label = this._getLabelFieldChild(key).child; - const newLabel = data[key] === this._visibility.hidden ? this.__addDelTag(label.getValue()) : this.__removeDelTag(label.getValue()); - label.setValue(newLabel); - - const enabled = data[key] === this._visibility.readWrite; - const disableables = osparc.form.renderer.PropFormBase.getDisableables(); - const ctrls = []; - disableables.forEach(disableable => ctrls.push(this._getLayoutChild(key, disableable))); - ctrls.forEach(ctrl => { - if (ctrl) { - ctrl.child.setEnabled(enabled); - } - }); - } - }, - - __getRadioButtonsFieldChild: function(portId) { - return this._getLayoutChild(portId, this.self().GRID_POS.ACCESS_LEVEL); - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js index 3b2dc6fb4a4..7799830bade 100644 --- a/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js +++ b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js @@ -101,13 +101,10 @@ qx.Class.define("osparc.jobs.JobsButton", { __attachSocketListener: function() { const socket = osparc.wrapper.WebSocket.getInstance(); - socket.on("projectStateUpdated", content => { - // for now, we can only access the activity of my user, not the whole project... - if (osparc.study.Utils.amIRunningTheStudy(content)) { - // we know that I am running at least one study + socket.on("projectStateUpdated", data => { + if (osparc.study.Utils.state.isPipelineRunning(data["data"])) { this.__updateJobsButton(true); } - // ...in the next iteration: listen to main store's "studyStateChanged", which will cover all users }, this); }, diff --git a/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js b/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js index 291c028422d..762f7134351 100644 --- a/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js +++ b/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js @@ -50,13 +50,23 @@ qx.Class.define("osparc.node.BootOptionsView", { if (selection.length) { buttonsLayout.setEnabled(false); const newBootModeId = selection[0].bootModeId; - node.setBootOptions({ + const data = { "boot_mode": newBootModeId - }); + }; + node.setBootOptions(data); node.fireEvent("updateStudyDocument"); + node.fireDataEvent("projectDocumentChanged", { + "op": "replace", + "path": `/workbench/${nodeId}/bootOptions`, + "value": data, + "osparc-resource": "node", + }); + // add timeout to make sure the node is saved before starting it setTimeout(() => { buttonsLayout.setEnabled(true); - node.requestStartNode(); + if (!node.getStudy().getDisableServiceAutoStart()) { + node.requestStartNode(); + } }, osparc.desktop.StudyEditor.AUTO_SAVE_INTERVAL); } }, this); diff --git a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js index ea6ed0fbb28..23a390106f4 100644 --- a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js +++ b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js @@ -111,16 +111,32 @@ qx.Class.define("osparc.node.LifeCycleView", { updateButton.addListener("execute", () => { updateButton.setFetching(true); const latestCompatible = osparc.store.Services.getLatestCompatible(node.getKey(), node.getVersion()); + const newData = {}; if (node.getKey() !== latestCompatible["key"]) { - node.setKey(latestCompatible["key"]); + newData["key"] = latestCompatible["key"]; } if (node.getVersion() !== latestCompatible["version"]) { - node.setVersion(latestCompatible["version"]); + newData["version"] = latestCompatible["version"]; } + node.set(newData); node.fireEvent("updateStudyDocument"); + node.fireDataEvent("projectDocumentChanged", [{ + "op": "replace", + "path": `/workbench/${nodeId}/key`, + "value": latestCompatible["key"], + "osparc-resource": "node", + }, { + "op": "replace", + "path": `/workbench/${nodeId}/version`, + "value": latestCompatible["version"], + "osparc-resource": "node", + }]); + // add timeout to make sure the node is saved before starting it setTimeout(() => { updateButton.setFetching(false); - node.requestStartNode(); + if (!node.getStudy().getDisableServiceAutoStart()) { + node.requestStartNode(); + } }, osparc.desktop.StudyEditor.AUTO_SAVE_INTERVAL); }); diff --git a/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js b/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js index cee5d1fb130..a0f26f40645 100644 --- a/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js +++ b/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js @@ -57,7 +57,6 @@ qx.Class.define("osparc.node.slideshow.NodeView", { const node = this.getNode(); const propsForm = node.getPropsForm(); if (propsForm && node.hasInputs()) { - propsForm.addListener("changeChildVisibility", () => this.__checkSettingsVisibility(), this); this._settingsLayout.add(propsForm); } this.__checkSettingsVisibility(); diff --git a/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js b/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js index 5f3a3866bad..0d643fa6a22 100644 --- a/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js +++ b/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js @@ -133,7 +133,7 @@ qx.Class.define("osparc.snapshots.IterationsView", { const iteration = new osparc.data.model.Study(iterationData); iteration.setReadOnly(true); iteration.nodeUpdated(dataUpdate); - const iterationDataUpdated = iteration.serialize(false); + const iterationDataUpdated = iteration.serialize(); this.__iterations.splice(idx, 1, iterationDataUpdated); // update maximum once every 2" @@ -180,7 +180,7 @@ qx.Class.define("osparc.snapshots.IterationsView", { this.__iterationsSection.remove(this.__iterationsTable); } - const iterationsTable = this.__iterationsTable = new osparc.snapshots.Iterations(this.__study.serialize(false)); + const iterationsTable = this.__iterationsTable = new osparc.snapshots.Iterations(this.__study.serialize()); iterationsTable.populateTable(this.__iterations); iterationsTable.addListener("cellTap", e => { const selectedRow = e.getRow(); diff --git a/services/static-webserver/client/source/class/osparc/study/Utils.js b/services/static-webserver/client/source/class/osparc/study/Utils.js index c32211ba728..800becc4380 100644 --- a/services/static-webserver/client/source/class/osparc/study/Utils.js +++ b/services/static-webserver/client/source/class/osparc/study/Utils.js @@ -419,23 +419,24 @@ qx.Class.define("osparc.study.Utils", { } return undefined; }, - }, - // used in the "projectStateUpdated" socket event - amIRunningTheStudy: function(content) { - if (content && "data" in content) { - const state = content["data"]; - const currentGroupIds = this.state.getCurrentGroupIds(state); - if (currentGroupIds.includes(osparc.auth.Data.getInstance().getGroupId())) { - const pipelineState = this.state.getPipelineState(state); - return [ - "PUBLISHED", - "STARTED", - "STOPPING", - ].includes(pipelineState); + PIPELINE_RUNNING_STATES: [ + "PUBLISHED", + "PENDING", + "WAITING_FOR_RESOURCES", + "WAITING_FOR_CLUSTER", + "STARTED", + "STOPPING", + "RETRY", + ], + + isPipelineRunning: function(state) { + const pipelineState = this.getPipelineState(state); + if (pipelineState) { + return this.PIPELINE_RUNNING_STATES.includes(pipelineState); } - } - return false; + return false; + }, }, __getBlockedState: function(studyData) { @@ -533,7 +534,7 @@ qx.Class.define("osparc.study.Utils", { resolve(osparc.data.model.StudyUI.PIPELINE_ICON); } else { const productIcon = osparc.dashboard.CardBase.PRODUCT_ICON; - const wbServices = this.self().getNonFrontendNodes(studyData); + const wbServices = this.getNonFrontendNodes(studyData); if (wbServices.length === 1) { const wbService = wbServices[0]; osparc.store.Services.getService(wbService.key, wbService.version) 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 846c1d6e98a..03898c10a63 100644 --- a/services/static-webserver/client/source/class/osparc/utils/Utils.js +++ b/services/static-webserver/client/source/class/osparc/utils/Utils.js @@ -496,6 +496,10 @@ qx.Class.define("osparc.utils.Utils", { return (["dev", "master"].includes(platformName)); }, + eventDrivenPatch: function() { + return osparc.utils.DisabledPlugins.isRTCEnabled(); + }, + getEditButton: function(isVisible = true) { return new qx.ui.form.Button(null, "@FontAwesome5Solid/pencil-alt/12").set({ appearance: "form-button-outlined", 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 746e5d176dd..4b98e89c6c6 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/Annotation.js +++ b/services/static-webserver/client/source/class/osparc/workbench/Annotation.js @@ -77,7 +77,7 @@ qx.Class.define("osparc.workbench.Annotation", { attributes: { check: "Object", - nullable: false, + nullable: false }, svgCanvas: { @@ -95,7 +95,8 @@ qx.Class.define("osparc.workbench.Annotation", { "annotationClicked": "qx.event.type.Data", "annotationStartedMoving": "qx.event.type.Event", "annotationMoving": "qx.event.type.Event", - "annotationStoppedMoving": "qx.event.type.Event" + "annotationStoppedMoving": "qx.event.type.Event", + "annotationChanged": "qx.event.type.Event", }, members: { @@ -174,6 +175,7 @@ qx.Class.define("osparc.workbench.Annotation", { osparc.wrapper.Svg.updateTextColor(representation, color); break; } + this.fireEvent("annotationChanged"); } }, @@ -223,6 +225,7 @@ qx.Class.define("osparc.workbench.Annotation", { this.getAttributes().x = x; this.getAttributes().y = y; } + this.fireEvent("annotationChanged"); }, setText: function(newText) { @@ -230,6 +233,7 @@ qx.Class.define("osparc.workbench.Annotation", { const representation = this.getRepresentation(); if (representation) { osparc.wrapper.Svg.updateText(representation, newText); + this.fireEvent("annotationChanged"); } }, @@ -238,6 +242,7 @@ qx.Class.define("osparc.workbench.Annotation", { const representation = this.getRepresentation(); if (representation) { osparc.wrapper.Svg.updateTextSize(representation, fontSize); + this.fireEvent("annotationChanged"); } }, 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 b0f2bc4bec0..476a3583322 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js @@ -709,10 +709,14 @@ qx.Class.define("osparc.workbench.NodeUI", { }; }, + moveNodeTo: function(pos) { + this.moveTo(pos.x, pos.y); + }, + setPosition: function(pos) { const node = this.getNode(); node.setPosition(pos); - this.moveTo(node.getPosition().x, node.getPosition().y); + this.moveNodeTo(pos); }, snapToGrid: function() { @@ -724,11 +728,10 @@ qx.Class.define("osparc.workbench.NodeUI", { const snapGrid = 20; const snapX = Math.round(x/snapGrid)*snapGrid; const snapY = Math.round(y/snapGrid)*snapGrid; - node.setPosition({ + this.setPosition({ x: snapX, y: snapY }); - this.moveTo(node.getPosition().x, node.getPosition().y); }, __applyThumbnail: function(thumbnailSrc) { 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 4114221d7c0..df48c91120e 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js @@ -443,7 +443,8 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { __itemMoving: function(itemId, xDiff, yDiff) { this.getSelectedNodeUIs().forEach(selectedNodeUI => { if (itemId !== selectedNodeUI.getNodeId()) { - selectedNodeUI.setPosition({ + // do not touch the position, just move the node, this will happen in __itemStoppedMoving + selectedNodeUI.moveNodeTo({ x: selectedNodeUI.initPos.x + xDiff, y: selectedNodeUI.initPos.y + yDiff }); @@ -464,10 +465,24 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { this.getSelectedNodeUIs().forEach(selectedNodeUI => delete selectedNodeUI["initPos"]); this.getSelectedAnnotations().forEach(selectedAnnotation => delete selectedAnnotation["initPos"]); - if (nodeUI && osparc.Preferences.getInstance().isSnapNodeToGrid()) { - this.getSelectedNodeUIs().forEach(selectedNodeUI => selectedNodeUI.snapToGrid()); - // make sure nodeUI is moved, then update edges - setTimeout(() => this.__updateNodeUIPos(nodeUI), 10); + // the moving item could be an annotation, so we need to check if it is a nodeUI + if (nodeUI) { + this.getSelectedNodeUIs().forEach(selectedNodeUI => { + if (nodeUI !== selectedNodeUI) { + // now set the position + const layoutProps = selectedNodeUI.getLayoutProperties(); + selectedNodeUI.setPosition({ + x: layoutProps.left, + y: layoutProps.top, + }); + } + }); + + if (osparc.Preferences.getInstance().isSnapNodeToGrid()) { + this.getSelectedNodeUIs().forEach(selectedNodeUI => selectedNodeUI.snapToGrid()); + // make sure nodeUI is moved, then update edges + setTimeout(() => this.__updateNodeUIPos(nodeUI), 10); + } } this.__updateWorkbenchBounds();