Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c556fd9
TransferProjects
odeimaiz Oct 10, 2025
1a4546b
buttons workflow
odeimaiz Oct 10, 2025
30a68b0
__shareAllProjects
odeimaiz Oct 10, 2025
b6d7bc7
minor
odeimaiz Oct 10, 2025
33b111a
Merge branch 'master' into feature/transfer-projects
odeimaiz Oct 10, 2025
50b0c89
Merge branch 'feature/transfer-projects' of github.com:odeimaiz/ospar…
odeimaiz Oct 10, 2025
1b53c5c
let testers change the template type
odeimaiz Oct 10, 2025
3b68e37
minor
odeimaiz Oct 10, 2025
4ae4036
intro text
odeimaiz Oct 10, 2025
d7282d1
updateCollaborator
odeimaiz Oct 10, 2025
ace3973
shareAndKeepOwnership
odeimaiz Oct 10, 2025
998fa96
undo
odeimaiz Oct 10, 2025
0708751
undo
odeimaiz Oct 10, 2025
b51fa84
return all my studies
odeimaiz Oct 10, 2025
dc5e063
This option is not yet enabled.
odeimaiz Oct 10, 2025
14bac36
warning message
odeimaiz Oct 10, 2025
193f84b
"no-permission-label"
odeimaiz Oct 10, 2025
bf77238
Update services/static-webserver/client/source/class/osparc/store/Stu…
odeimaiz Oct 10, 2025
cad5de5
Update services/static-webserver/client/source/class/osparc/store/Stu…
odeimaiz Oct 10, 2025
6821805
Merge branch 'feature/transfer-projects' of github.com:odeimaiz/ospar…
odeimaiz Oct 10, 2025
9d1fc38
Merge branch 'master' into feature/transfer-projects
odeimaiz Oct 10, 2025
b505370
Merge branch 'feature/transfer-projects' of github.com:odeimaiz/ospar…
odeimaiz Oct 10, 2025
625ccdb
minor
odeimaiz Oct 10, 2025
dcc7932
__removeMyOwnership
odeimaiz Oct 10, 2025
3bb9b89
__removeMyOwnerships
odeimaiz Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,28 @@ 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",
allowGrowX: false,
alignX: "right",
});
control.addListener("execute", () => this.__notifyUserTapped());
this._add(control);
this.getChildControl("footer-layout").addAt(control, 1);
break;
}

Expand All @@ -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();
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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.<br>
There are two ways to do so:<br>
- Share all your projects with the target user and keep the co-ownership. <br>
- Share all your projects with the target user and remove yourself as co-owner. <br>
`);
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);
},
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading