diff --git a/services/static-webserver/client/source/class/osparc/data/model/User.js b/services/static-webserver/client/source/class/osparc/data/model/User.js index a8f39a3779f4..47d665f847d1 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/User.js +++ b/services/static-webserver/client/source/class/osparc/data/model/User.js @@ -30,7 +30,7 @@ qx.Class.define("osparc.data.model.User", { const userId = ("id" in userData) ? parseInt(userData["id"]) : parseInt(userData["userId"]); const groupId = ("gid" in userData) ? parseInt(userData["gid"]) : parseInt(userData["groupId"]); - const username = userData["userName"]; + const username = userData["userName"] || "-"; const email = ("login" in userData) ? userData["login"] : userData["email"]; let firstName = ""; if (userData["first_name"]) { @@ -60,7 +60,7 @@ qx.Class.define("osparc.data.model.User", { lastName, email, thumbnail, - label: username, + label: userData["userName"] || description, description, }); }, 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 2f40fee70b64..e10cc5f62fdd 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -442,7 +442,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { appearance: "form-button-outlined", label: this.tr("App Mode"), toolTipText: this.tr("Start App Mode"), - icon: "@FontAwesome5Solid/play/14", + icon: osparc.dashboard.CardBase.MODE_APP, marginRight: 10, marginTop: 7, ...osparc.navigation.NavigationBar.BUTTON_OPTIONS @@ -837,7 +837,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { const startAppBtn = this.__startAppButton = new qx.ui.form.Button().set({ label: this.tr("Start"), - icon: "@FontAwesome5Solid/play/14", + icon: osparc.dashboard.CardBase.MODE_APP, toolTipText: this.tr("Start App Mode"), height: buttonsHeight }); 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 f74cb9aed5aa..07a32d3358b5 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 @@ -31,6 +31,9 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { this._setLayout(new qx.ui.layout.VBox(15)); + this.__userProfileData = {}; + this.__userPrivacyData = {}; + this.__fetchProfile(); this._add(this.__createProfileUser()); @@ -45,8 +48,11 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { members: { __userProfileData: null, __userProfileModel: null, + __userProfileRenderer: null, + __updateProfileBtn: null, __userPrivacyData: null, __userPrivacyModel: null, + __updatePrivacyBtn: null, __userProfileForm: null, __fetchProfile: function() { @@ -69,16 +75,29 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { "expirationDate": data["expirationDate"] || null, }); } + this.__updateProfileBtn.setEnabled(false); }, __setDataToPrivacy: function(privacyData) { if (privacyData) { this.__userPrivacyData = privacyData; this.__userPrivacyModel.set({ + "hideUsername": "hideUsername" in privacyData ? privacyData["hideUsername"] : false, "hideFullname": "hideFullname" in privacyData ? privacyData["hideFullname"] : true, "hideEmail": "hideEmail" in privacyData ? privacyData["hideEmail"] : true, }); + + const visibleIcon = "@FontAwesome5Solid/eye/12"; + const hiddenIcon = "@FontAwesome5Solid/eye-slash/12"; + const icons = { + 0: this.__userPrivacyModel.getHideUsername() ? hiddenIcon : visibleIcon, + 1: this.__userPrivacyModel.getHideFullname() ? hiddenIcon : visibleIcon, + 2: this.__userPrivacyModel.getHideFullname() ? hiddenIcon : visibleIcon, + 3: this.__userPrivacyModel.getHideEmail() ? hiddenIcon : visibleIcon, + }; + this.__userProfileRenderer.setIcons(icons); } + this.__updatePrivacyBtn.setEnabled(false); }, __createProfileUser: function() { @@ -105,12 +124,13 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { readOnly: true }); - const form = this.__userProfileForm = new qx.ui.form.Form(); - form.add(username, "Username", null, "username"); - form.add(firstName, "First Name", null, "firstName"); - form.add(lastName, "Last Name", null, "lastName"); - form.add(email, "Email", null, "email"); - box.add(new qx.ui.form.renderer.Single(form)); + const profileForm = this.__userProfileForm = new qx.ui.form.Form(); + profileForm.add(username, "Username", null, "username"); + profileForm.add(firstName, "First Name", null, "firstName"); + profileForm.add(lastName, "Last Name", null, "lastName"); + profileForm.add(email, "Email", null, "email"); + const singleWithIcon = this.__userProfileRenderer = new osparc.ui.form.renderer.SingleWithIcon(profileForm); + box.add(singleWithIcon); const expirationLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)).set({ paddingLeft: 16, @@ -167,21 +187,23 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { namesValidator.add(firstName, qx.util.Validate.regExp(/[^\.\d]+/), this.tr("Avoid dots or numbers in text")); namesValidator.add(lastName, qx.util.Validate.regExp(/^$|[^\.\d]+/), this.tr("Avoid dots or numbers in text")); // allow also empty last name - const updateBtn = new qx.ui.form.Button("Update Profile").set({ + const updateProfileBtn = this.__updateProfileBtn = new qx.ui.form.Button().set({ + label: this.tr("Update Profile"), appearance: "form-button", alignX: "right", - allowGrowX: false + allowGrowX: false, + enabled: false, }); - box.add(updateBtn); + box.add(updateProfileBtn); - updateBtn.addListener("execute", () => { + updateProfileBtn.addListener("execute", () => { if (!osparc.data.Permissions.getInstance().canDo("user.user.update", true)) { this.__resetUserData(); return; } const patchData = {}; - if (this.__userProfileData["username"] !== model.getUsername()) { + if (this.__userProfileData["userName"] !== model.getUsername()) { patchData["userName"] = model.getUsername(); } if (this.__userProfileData["first_name"] !== model.getFirstName()) { @@ -211,49 +233,73 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { } }); + const profileFields = [ + username, + firstName, + lastName, + ] + const valueChanged = () => { + const anyChanged = + username.getValue() !== this.__userProfileData["userName"] || + firstName.getValue() !== this.__userProfileData["first_name"] || + lastName.getValue() !== this.__userProfileData["last_name"]; + updateProfileBtn.setEnabled(anyChanged); + }; + valueChanged(); + profileFields.forEach(privacyField => privacyField.addListener("changeValue", () => valueChanged())); + return box; }, __createPrivacySection: function() { + // binding to a model + const defaultModel = { + "hideUsername": false, + "hideFullname": true, + "hideEmail": true, + }; + + const privacyModel = this.__userPrivacyModel = qx.data.marshal.Json.createModel(defaultModel, true); + const box = osparc.ui.window.TabbedView.createSectionBox(this.tr("Privacy")); box.set({ alignX: "left", maxWidth: 500 }); - const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr("For Privacy reasons, you might want to hide your First and Last Names and/or the Email to other users")); + const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr("For Privacy reasons, you might want to hide some personal data.")); box.add(label); + const hideUsername = new qx.ui.form.CheckBox().set({ + value: defaultModel.hideUsername + }); const hideFullname = new qx.ui.form.CheckBox().set({ - value: true + value: defaultModel.hideFullname }); const hideEmail = new qx.ui.form.CheckBox().set({ - value: true + value: defaultModel.hideEmail }); - const form = new qx.ui.form.Form(); - form.add(hideFullname, "Hide Full Name", null, "hideFullname"); - form.add(hideEmail, "Hide Email", null, "hideEmail"); - box.add(new qx.ui.form.renderer.Single(form)); - - // binding to a model - const raw = { - "hideFullname": true, - "hideEmail": true, - }; + const privacyForm = new qx.ui.form.Form(); + privacyForm.add(hideUsername, "Hide Username", null, "hideUsername"); + privacyForm.add(hideFullname, "Hide Full Name", null, "hideFullname"); + privacyForm.add(hideEmail, "Hide Email", null, "hideEmail"); + box.add(new qx.ui.form.renderer.Single(privacyForm)); - const model = this.__userPrivacyModel = qx.data.marshal.Json.createModel(raw); - const controller = new qx.data.controller.Object(model); - controller.addTarget(hideFullname, "value", "hideFullname", true); - controller.addTarget(hideEmail, "value", "hideEmail", true); + const privacyModelCtrl = new qx.data.controller.Object(privacyModel); + privacyModelCtrl.addTarget(hideUsername, "value", "hideUsername", true); + privacyModelCtrl.addTarget(hideFullname, "value", "hideFullname", true); + privacyModelCtrl.addTarget(hideEmail, "value", "hideEmail", true); - const privacyBtn = new qx.ui.form.Button("Update Privacy").set({ + const updatePrivacyBtn = this.__updatePrivacyBtn = new qx.ui.form.Button().set({ + label: this.tr("Update Privacy"), appearance: "form-button", alignX: "right", - allowGrowX: false + allowGrowX: false, + enabled: false, }); - box.add(privacyBtn); - privacyBtn.addListener("execute", () => { + box.add(updatePrivacyBtn); + updatePrivacyBtn.addListener("execute", () => { if (!osparc.data.Permissions.getInstance().canDo("user.user.update", true)) { this.__resetPrivacyData(); return; @@ -261,11 +307,14 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { const patchData = { "privacy": {} }; - if (this.__userPrivacyData["hideFullname"] !== model.getHideFullname()) { - patchData["privacy"]["hideFullname"] = model.getHideFullname(); + if (this.__userPrivacyData["hideUsername"] !== privacyModel.getHideUsername()) { + patchData["privacy"]["hideUsername"] = privacyModel.getHideUsername(); + } + if (this.__userPrivacyData["hideFullname"] !== privacyModel.getHideFullname()) { + patchData["privacy"]["hideFullname"] = privacyModel.getHideFullname(); } - if (this.__userPrivacyData["hideEmail"] !== model.getHideEmail()) { - patchData["privacy"]["hideEmail"] = model.getHideEmail(); + if (this.__userPrivacyData["hideEmail"] !== privacyModel.getHideEmail()) { + patchData["privacy"]["hideEmail"] = privacyModel.getHideEmail(); } if ( @@ -298,6 +347,36 @@ qx.Class.define("osparc.desktop.account.ProfilePage", { } }); + const optOutMessage = new qx.ui.basic.Atom().set({ + label: this.tr("If all searchable fields are hidden, you will not be findable."), + icon: "@FontAwesome5Solid/exclamation-triangle/14", + gap: 8, + allowGrowX: false, + }); + optOutMessage.getChildControl("icon").setTextColor("warning-yellow") + box.add(optOutMessage); + + const privacyFields = [ + hideUsername, + hideFullname, + hideEmail, + ] + const valueChanged = () => { + const anyChanged = + hideUsername.getValue() !== this.__userPrivacyData["hideUsername"] || + hideFullname.getValue() !== this.__userPrivacyData["hideFullname"] || + hideEmail.getValue() !== this.__userPrivacyData["hideEmail"]; + updatePrivacyBtn.setEnabled(anyChanged); + + if (privacyFields.every(privacyField => privacyField.getValue())) { + optOutMessage.show(); + } else { + optOutMessage.exclude(); + } + }; + valueChanged(); + privacyFields.forEach(privacyField => privacyField.addListener("changeValue", () => valueChanged())); + return box; }, diff --git a/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js b/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js new file mode 100644 index 000000000000..a8252d6040c2 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/ui/form/renderer/SingleWithIcon.js @@ -0,0 +1,62 @@ +/* ************************************************************************ + + 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.ui.form.renderer.SingleWithIcon", { + extend: qx.ui.form.renderer.Single, + + construct: function(form, icons) { + if (icons) { + this.__icons = icons; + } else { + this.__icons = {}; + } + + this.base(arguments, form); + }, + + members: { + __icons: null, + + setIcons: function(icons) { + this.__icons = icons; + + this._onFormChange(); + }, + + // overridden + addItems: function(items, names, title, itemOptions, headerOptions) { + this.base(arguments, items, names, title, itemOptions, headerOptions); + + // header + let row = title === null ? 0 : 1; + + for (let i = 0; i < items.length; i++) { + if (i in this.__icons) { + const image = new qx.ui.basic.Image(this.__icons[i]).set({ + alignY: "middle", + }); + this._add(image, { + row, + column: 2, + }); + } + + row++; + } + }, + } +}); diff --git a/tests/e2e/tests/startupCalls.js b/tests/e2e/tests/startupCalls.js index cadffad1ef74..78d080c1367c 100644 --- a/tests/e2e/tests/startupCalls.js +++ b/tests/e2e/tests/startupCalls.js @@ -11,6 +11,9 @@ module.exports = { const responses = { me: null, + tags: null, + tasks: null, + uiConfig: null, studies: null, templates: null, services: null, @@ -23,6 +26,12 @@ module.exports = { const url = response.url(); if (url.endsWith('/me')) { responses.me = response.json(); + } else if (url.endsWith('/tags')) { + responses.tags = response.json(); + } else if (url.endsWith('/tasks')) { + responses.tasks = response.json(); + } else if (url.endsWith('/ui')) { + responses.uiConfig = response.json(); } else if (url.includes('projects?type=user')) { responses.studies = response.json(); } else if (url.includes('projects?type=template')) { @@ -52,6 +61,26 @@ module.exports = { expect(responseEnv.data["login"]).toBe(user); }, ourTimeout); + test('Tags', async () => { + const responseEnv = await responses.tags; + expect(Array.isArray(responseEnv.data)).toBeTruthy(); + }, ourTimeout); + + /* + test('Tasks', async () => { + const responseEnv = await responses.tasks; + expect(Array.isArray(responseEnv.data)).toBeTruthy(); + }, ourTimeout); + */ + + test('UI Config', async () => { + const responseEnv = await responses.uiConfig; + expect(responseEnv.data["productName"]).toBe("osparc"); + const uiConfig = responseEnv.data["ui"]; + const isObject = typeof uiConfig === 'object' && !Array.isArray(uiConfig) && uiConfig !== null; + expect(isObject).toBeTruthy(); + }, ourTimeout); + test('Studies', async () => { const responseEnv = await responses.studies; expect(Array.isArray(responseEnv.data)).toBeTruthy();