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 0d7d8cbde7b5..6663c9f1de8b 100644
--- a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js
+++ b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js
@@ -131,6 +131,20 @@ qx.Class.define("osparc.conversation.AddMessage", {
control.addListener("execute", this.__addCommentPressed, this);
this.getChildControl("add-comment-layout").add(control);
break;
+ case "footer-layout":
+ control = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({
+ alignY: "middle"
+ }));
+ this._add(control);
+ break;
+ case "no-permission-label":
+ control = new qx.ui.basic.Label(this.tr("Only users with write access can add comments.")).set({
+ allowGrowX: true,
+ });
+ this.getChildControl("footer-layout").addAt(control, 0, {
+ flex: 1
+ });
+ break;
case "notify-user-button":
control = new qx.ui.form.Button("🔔 " + this.tr("Notify user")).set({
appearance: "form-button",
@@ -138,7 +152,7 @@ qx.Class.define("osparc.conversation.AddMessage", {
alignX: "right",
});
control.addListener("execute", () => this.__notifyUserTapped());
- this._add(control);
+ this.getChildControl("footer-layout").addAt(control, 1);
break;
}
@@ -152,13 +166,16 @@ qx.Class.define("osparc.conversation.AddMessage", {
},
__applyStudyData: function(studyData) {
+ const noPermissionLabel = this.getChildControl("no-permission-label");
const notifyUserButton = this.getChildControl("notify-user-button");
if (studyData) {
const canIWrite = osparc.data.model.Study.canIWrite(studyData["accessRights"])
this.getChildControl("add-comment-button").setEnabled(canIWrite);
+ noPermissionLabel.setVisibility(canIWrite ? "hidden" : "visible");
notifyUserButton.show();
notifyUserButton.setEnabled(canIWrite);
} else {
+ noPermissionLabel.hide();
notifyUserButton.exclude();
}
},
diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js
index f786949cbf4b..98f27e0b1915 100644
--- a/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js
+++ b/services/static-webserver/client/source/class/osparc/desktop/account/ProfilePage.js
@@ -38,6 +38,7 @@ qx.Class.define("osparc.desktop.account.ProfilePage", {
}
this._add(this.__createPasswordSection());
this._add(this.__createContactSection());
+ this._add(this.__createTransferProjectsSection());
this._add(this.__createDeleteAccount());
this.__userProfileData = {};
@@ -653,6 +654,26 @@ qx.Class.define("osparc.desktop.account.ProfilePage", {
return box;
},
+ __createTransferProjectsSection: function() {
+ const box = this.self().createSectionBox(this.tr("Transfer Projects"));
+ box.addHelper(this.tr("Transfer of your projects to another user."));
+
+ const transferBtn = new qx.ui.form.Button(this.tr("Transfer Projects")).set({
+ appearance: "strong-button",
+ alignX: "right",
+ allowGrowX: false
+ });
+ transferBtn.addListener("execute", () => {
+ const transferProjects = new osparc.desktop.account.TransferProjects();
+ const win = osparc.ui.window.Window.popUpInWindow(transferProjects, qx.locale.Manager.tr("Transfer Projects"), 500, null);
+ transferProjects.addListener("cancel", () => win.close());
+ transferProjects.addListener("transferred", () => win.close());
+ });
+ box.add(transferBtn);
+
+ return box;
+ },
+
__createDeleteAccount: function() {
// layout
const box = this.self().createSectionBox(this.tr("Delete Account"));
diff --git a/services/static-webserver/client/source/class/osparc/desktop/account/TransferProjects.js b/services/static-webserver/client/source/class/osparc/desktop/account/TransferProjects.js
new file mode 100644
index 000000000000..1f0d8f2d3481
--- /dev/null
+++ b/services/static-webserver/client/source/class/osparc/desktop/account/TransferProjects.js
@@ -0,0 +1,266 @@
+/* ************************************************************************
+
+ osparc - the simcore frontend
+
+ https://osparc.io
+
+ Copyright:
+ 2025 IT'IS Foundation, https://itis.swiss
+
+ License:
+ MIT: https://opensource.org/licenses/MIT
+
+ Authors:
+ * Odei Maiz (odeimaiz)
+
+************************************************************************ */
+
+qx.Class.define("osparc.desktop.account.TransferProjects", {
+ extend: qx.ui.core.Widget,
+
+ construct: function() {
+ this.base(arguments, this.tr("Transfer Projects"));
+
+ this._setLayout(new qx.ui.layout.VBox(15));
+
+ this.__buildLayout();
+ },
+
+ events: {
+ "transferred": "qx.event.type.Event",
+ "cancel": "qx.event.type.Event"
+ },
+
+ properties: {
+ targetUser: {
+ check: "osparc.data.model.User",
+ init: null,
+ nullable: true,
+ event: "changeTargetUser",
+ },
+ },
+
+ members: {
+ _createChildControlImpl: function(id) {
+ let control = null;
+ switch (id) {
+ case "intro-text": {
+ const text = this.tr(`\
+ You are about to transfer all your projects to another user.
+ There are two ways to do so:
+ - Share all your projects with the target user and keep the co-ownership.
+ - Share all your projects with the target user and remove yourself as co-owner.
+ `);
+ control = new qx.ui.basic.Label().set({
+ value: text,
+ font: "text-14",
+ rich: true,
+ wrap: true
+ });
+ this._add(control);
+ break;
+ }
+ case "target-user-layout": {
+ control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5).set({
+ alignY: "middle",
+ }));
+ const label = new qx.ui.basic.Label(this.tr("Target user:")).set({
+ font: "text-14"
+ });
+ control.add(label);
+ this._add(control);
+ break;
+ }
+ case "target-user-button":
+ control = new qx.ui.form.Button(this.tr("Select user")).set({
+ appearance: "strong-button",
+ allowGrowX: false,
+ });
+ this.bind("targetUser", control, "label", {
+ converter: targetUser => targetUser ? targetUser.getUserName() : this.tr("Select user")
+ });
+ control.addListener("execute", () => this.__selectTargetUserTapped(), this);
+ this.getChildControl("target-user-layout").add(control);
+ break;
+ case "buttons-container":
+ control = new qx.ui.container.Composite(new qx.ui.layout.HBox(10).set({
+ alignX: "right"
+ }));
+ this._add(control);
+ break;
+ case "cancel-button":
+ control = new qx.ui.form.Button(this.tr("Cancel")).set({
+ appearance: "form-button-text",
+ allowGrowX: false,
+ });
+ control.addListener("execute", () => this.fireEvent("cancel"), this);
+ this.getChildControl("buttons-container").add(control);
+ break;
+ case "share-and-keep-button":
+ control = new osparc.ui.form.FetchButton(this.tr("Share and keep ownership")).set({
+ appearance: "strong-button",
+ allowGrowX: false,
+ });
+ this.bind("targetUser", control, "enabled", {
+ converter: targetUser => targetUser !== null
+ });
+ control.addListener("execute", () => this.__shareAndKeepOwnership(), this);
+ this.getChildControl("buttons-container").add(control);
+ break;
+ case "share-and-leave-button": {
+ control = new osparc.ui.form.FetchButton(this.tr("Share and remove my ownership")).set({
+ appearance: "danger-button",
+ allowGrowX: false,
+ });
+ this.bind("targetUser", control, "enabled", {
+ converter: targetUser => targetUser !== null
+ });
+ control.addListener("execute", () => this.__shareAndLeaveOwnership(), this);
+ this.getChildControl("buttons-container").add(control);
+ break;
+ }
+ }
+ return control || this.base(arguments, id);
+ },
+
+ __buildLayout: function() {
+ this.getChildControl("intro-text");
+ this.getChildControl("target-user-button");
+ this.getChildControl("cancel-button");
+ this.getChildControl("share-and-keep-button");
+ this.getChildControl("share-and-leave-button");
+ },
+
+ __selectTargetUserTapped: function() {
+ const collaboratorsManager = new osparc.share.NewCollaboratorsManager({}, false, false).set({
+ acceptOnlyOne: true
+ });
+ collaboratorsManager.getChildControl("intro-text").set({
+ value: this.tr("Select the user you want to transfer all your projects to.")
+ });
+ collaboratorsManager.setCaption(this.tr("Select target user"));
+ collaboratorsManager.addListener("addCollaborators", e => {
+ collaboratorsManager.close();
+ const selectedUsers = e.getData();
+ if (
+ selectedUsers &&
+ selectedUsers["selectedGids"] &&
+ selectedUsers["selectedGids"].length === 1
+ ) {
+ osparc.store.Users.getInstance().getUser(selectedUsers["selectedGids"][0])
+ .then(user => {
+ if (user.getGroupId() !== osparc.store.Groups.getInstance().getMyGroupId()) {
+ this.setTargetUser(user);
+ } else {
+ osparc.FlashMessenger.logAs(this.tr("You cannot transfer projects to yourself"), "ERROR");
+ }
+ });
+ }
+ }, this);
+ },
+
+ __shareAndKeepOwnership: function() {
+ this.setEnabled(false);
+ this.getChildControl("share-and-keep-button").setFetching(true);
+ this.__shareAllProjects()
+ .then(() => {
+ const msg = this.tr("All projects have been shared with the target user. You still own them.");
+ osparc.FlashMessenger.logAs(msg, "INFO", 10000);
+ this.fireEvent("transferred");
+ })
+ .catch(err => {
+ console.error(err);
+ osparc.FlashMessenger.logError(err);
+ })
+ .finally(() => {
+ this.setEnabled(true);
+ this.getChildControl("share-and-keep-button").setFetching(false);
+ });
+ },
+
+ __shareAndLeaveOwnership: function() {
+ osparc.FlashMessenger.logAs(this.tr("This option is not yet enabled."), "WARNING", 10000);
+ return;
+
+ this.setEnabled(false);
+ this.getChildControl("share-and-leave-button").setFetching(true);
+ this.__shareAllProjects()
+ .then(allMyStudies => {
+ return this.__removeMyOwnerships(allMyStudies);
+ })
+ .then(() => {
+ const msg = this.tr("All projects have been shared with the target user and you have been removed as co-owner.");
+ osparc.FlashMessenger.logAs(msg, "INFO", 10000);
+ this.fireEvent("transferred");
+ })
+ .catch(err => {
+ console.error(err);
+ osparc.FlashMessenger.logError(err);
+ })
+ .finally(() => {
+ this.setEnabled(true);
+ this.getChildControl("share-and-leave-button").setFetching(false);
+ });
+ },
+
+ __filterMyOwnedStudies: function(allMyReadStudies) {
+ // filter those that I don't own (no delete right)
+ const myGroupId = osparc.store.Groups.getInstance().getMyGroupId();
+ const ownerAccess = osparc.data.Roles.STUDY["delete"].accessRights;
+ const allMyStudies = allMyReadStudies.filter(studyData => {
+ return (
+ myGroupId in studyData["accessRights"] &&
+ JSON.stringify(studyData["accessRights"][myGroupId]) === JSON.stringify(ownerAccess)
+ )
+ });
+ return allMyStudies;
+ },
+
+ __shareAllProjects: function() {
+ const targetUser = this.getTargetUser();
+ if (targetUser === null) {
+ return;
+ }
+ const targetGroupId = targetUser.getGroupId();
+
+ return osparc.store.Study.getInstance().getAllMyStudies()
+ .then(allMyReadStudies => {
+ // filter those that I don't own (no delete right)
+ const allMyStudies = this.__filterMyOwnedStudies(allMyReadStudies);
+ const ownerAccess = osparc.data.Roles.STUDY["delete"].accessRights;
+ const newAccessRights = {
+ [targetGroupId]: ownerAccess
+ };
+ const promises = [];
+ allMyStudies.forEach(studyData => {
+ // first check it's not already shared with the target user
+ if (targetGroupId in studyData["accessRights"]) {
+ if (JSON.stringify(studyData["accessRights"][targetGroupId]) !== JSON.stringify(ownerAccess)) {
+ // update access rights to owner
+ promises.push(osparc.store.Study.getInstance().updateCollaborator(studyData, targetGroupId, ownerAccess));
+ }
+ } else {
+ // add as new collaborator with owner rights
+ promises.push(osparc.store.Study.getInstance().addCollaborators(studyData, newAccessRights));
+ }
+ });
+ // return only those projects that were shared
+ return Promise.all(promises)
+ .then(() => {
+ return allMyStudies;
+ })
+ .catch(err => {
+ console.error("Error sharing projects:", err);
+ });
+ });
+ },
+
+ __removeMyOwnerships: function(studies) {
+ const myGroupId = osparc.store.Groups.getInstance().getMyGroupId();
+ const promises = studies.map(study => {
+ return osparc.store.Study.getInstance().removeCollaborator(study, myGroupId);
+ });
+ return Promise.all(promises);
+ },
+ }
+});
diff --git a/services/static-webserver/client/source/class/osparc/info/StudyLarge.js b/services/static-webserver/client/source/class/osparc/info/StudyLarge.js
index 8061b5e47d1e..cfc4b9ec604e 100644
--- a/services/static-webserver/client/source/class/osparc/info/StudyLarge.js
+++ b/services/static-webserver/client/source/class/osparc/info/StudyLarge.js
@@ -60,9 +60,9 @@ qx.Class.define("osparc.info.StudyLarge", {
if (
this.__canIWrite() &&
this.getStudy().getTemplateType() &&
- osparc.data.Permissions.getInstance().isProductOwner()
+ osparc.data.Permissions.getInstance().isTester()
) {
- // let product owners change the template type
+ // let testers change the template type
const hBox = new qx.ui.container.Composite(new qx.ui.layout.HBox(5).set({
alignY: "middle",
}));
diff --git a/services/static-webserver/client/source/class/osparc/store/Study.js b/services/static-webserver/client/source/class/osparc/store/Study.js
index f7150275133d..f791f667baf7 100644
--- a/services/static-webserver/client/source/class/osparc/store/Study.js
+++ b/services/static-webserver/client/source/class/osparc/store/Study.js
@@ -63,6 +63,23 @@ qx.Class.define("osparc.store.Study", {
return osparc.data.Resources.fetch("studies", "getOne", params)
},
+ getAllMyStudies: function() {
+ const params = {
+ url: {
+ orderBy: JSON.stringify({
+ field: "last_change_date",
+ direction: "desc"
+ }),
+ text: "",
+ }
+ };
+ // getPageSearch with no text filter returns all studies
+ return osparc.data.Resources.getInstance().getAllPages("studies", params, "getPageSearch")
+ .then(allStudies => {
+ return allStudies;
+ });
+ },
+
openStudy: function(studyId, autoStart = true) {
const params = {
url: {