diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index eef057256137..fecd1b5c09eb 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -1266,7 +1266,23 @@ qx.Class.define("osparc.data.Resources", { delete: { method: "DELETE", url: statics.API + "/tags/{tagId}" - } + }, + getAccessRights: { + method: "GET", + url: statics.API + "/tags/{tagId}/groups" + }, + putAccessRights: { + method: "PUT", + url: statics.API + "/tags/{tagId}/groups/{groupId}" + }, + postAccessRights: { + method: "POST", + url: statics.API + "/tags/{tagId}/groups/{groupId}" + }, + deleteAccessRights: { + method: "DELETE", + url: statics.API + "/tags/{tagId}/groups/{groupId}" + }, } }, diff --git a/services/static-webserver/client/source/class/osparc/data/model/Tag.js b/services/static-webserver/client/source/class/osparc/data/model/Tag.js index fc7e00a5fcc6..94d3cfe8a538 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Tag.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Tag.js @@ -33,7 +33,7 @@ qx.Class.define("osparc.data.model.Tag", { name: tagData.name, description: tagData.description, color: tagData.color, - accessRights: tagData.accessRights, + myAccessRights: tagData.accessRights, }); }, @@ -65,6 +65,13 @@ qx.Class.define("osparc.data.model.Tag", { init: "#303030" }, + myAccessRights: { + check: "Object", + nullable: false, + init: null, + event: "changeMyAccessRights" + }, + accessRights: { check: "Object", nullable: false, @@ -73,6 +80,12 @@ qx.Class.define("osparc.data.model.Tag", { }, }, + statics: { + getProperties: function() { + return Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.Tag)); + } + }, + members: { serialize: function() { const jsonObject = {}; 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 9d70f9a85e0c..871afbf33b24 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -288,7 +288,9 @@ qx.Class.define("osparc.desktop.WorkbenchView", { converter: val => val ? "tab-button-selected" : "tab-button" }); if (widget) { - tabPage.add(widget, { + const scrollView = new qx.ui.container.Scroll(); + scrollView.add(widget); + tabPage.add(scrollView, { flex: 1 }); } diff --git a/services/static-webserver/client/source/class/osparc/desktop/preferences/pages/TagsPage.js b/services/static-webserver/client/source/class/osparc/desktop/preferences/pages/TagsPage.js index add2f2f3040a..f69ae38e4ee8 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/preferences/pages/TagsPage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/preferences/pages/TagsPage.js @@ -19,7 +19,7 @@ qx.Class.define("osparc.desktop.preferences.pages.TagsPage", { const studiesLabel = osparc.product.Utils.getStudyAlias({plural: true}); const studyLabel = osparc.product.Utils.getStudyAlias(); const msg = this.tr("\ - Tags are annotations to help users with grouping ") + studiesLabel + this.tr(" in the Dashboard. \ + Tags help you organize the ") + studiesLabel + this.tr(" in the Dashboard by categorizing topics, making it easier to search and filter. \ Once the tags are created, they can be assigned to the ") + studyLabel + this.tr(" via 'More options...' on the ") + studyLabel + this.tr(" cards."); const intro = osparc.ui.window.TabbedView.createHelpLabel(msg); this._add(intro); diff --git a/services/static-webserver/client/source/class/osparc/form/tag/TagItem.js b/services/static-webserver/client/source/class/osparc/form/tag/TagItem.js index 77282a5db7f9..97600c48bd41 100644 --- a/services/static-webserver/client/source/class/osparc/form/tag/TagItem.js +++ b/services/static-webserver/client/source/class/osparc/form/tag/TagItem.js @@ -15,7 +15,6 @@ qx.Class.define("osparc.form.tag.TagItem", { this.base(arguments); this._setLayout(new qx.ui.layout.HBox(5)); this.__validationManager = new qx.ui.form.validation.Manager(); - this.__renderLayout(); }, statics: { @@ -57,18 +56,23 @@ qx.Class.define("osparc.form.tag.TagItem", { init: "#303030" }, + myAccessRights: { + check: "Object", + nullable: false, + event: "changeMyAccessRights", + }, + accessRights: { check: "Object", nullable: false, event: "changeAccessRights", - apply: "__renderLayout", }, mode: { check: "String", init: "display", nullable: false, - apply: "_applyMode" + apply: "__applyMode" }, appearance: { @@ -84,83 +88,62 @@ qx.Class.define("osparc.form.tag.TagItem", { }, members: { - __tag: null, - __description: null, - __nameInput: null, - __descriptionInput: null, - __colorInput: null, - __colorButton: null, - __loadingIcon: null, __validationManager: null, _createChildControlImpl: function(id) { let control; switch (id) { case "tag": - // Tag sample on display mode - if (this.__tag === null) { - this.__tag = new osparc.ui.basic.Tag(); - this.bind("name", this.__tag, "value"); - this.bind("color", this.__tag, "color"); - } - control = this.__tag; + control = new osparc.ui.basic.Tag(); + this.bind("name", control, "value"); + this.bind("color", control, "color"); break; case "description": - // Description label on display mode - if (this.__description === null) { - this.__description = new qx.ui.basic.Label().set({ - rich: true - }); - this.bind("description", this.__description, "value"); - } - control = this.__description; + control = new qx.ui.basic.Label().set({ + rich: true, + allowGrowX: true, + }); + this.bind("description", control, "value"); + break; + case "shared-icon": + control = new qx.ui.basic.Image().set({ + minWidth: 30, + alignY: "middle", + cursor: "pointer", + }); + osparc.dashboard.CardBase.populateShareIcon(control, this.getAccessRights()) + control.addListener("tap", () => this.__openAccessRights(), this); break; case "name-input": - // Tag name input in edit mode - if (this.__nameInput === null) { - this.__nameInput = new qx.ui.form.TextField().set({ - required: true - }); - this.__validationManager.add(this.__nameInput); - this.__nameInput.getContentElement().setAttribute("autocomplete", "off"); - } - control = this.__nameInput; + control = new qx.ui.form.TextField().set({ + required: true + }); + this.__validationManager.add(control); + control.getContentElement().setAttribute("autocomplete", "off"); break; case "description-input": - // Tag description input in edit mode - if (this.__descriptionInput === null) { - this.__descriptionInput = new qx.ui.form.TextArea().set({ - autoSize: true, - minimalLineHeight: 1 - }); - } - control = this.__descriptionInput; + control = new qx.ui.form.TextArea().set({ + autoSize: true, + minimalLineHeight: 1 + }); break; case "color-input": - // Color input in edit mode - if (this.__colorInput === null) { - this.__colorInput = new qx.ui.form.TextField().set({ - value: this.getColor(), - width: 60, - required: true - }); - this.__colorInput.bind("value", this.getChildControl("color-button"), "backgroundColor"); - this.__colorInput.bind("value", this.getChildControl("color-button"), "textColor", { - converter: value => osparc.utils.Utils.getContrastedBinaryColor(value) - }); - this.__validationManager.add(this.__colorInput, osparc.utils.Validators.hexColor); - } - control = this.__colorInput; + control = new qx.ui.form.TextField().set({ + value: this.getColor(), + width: 60, + required: true + }); + control.bind("value", this.getChildControl("color-button"), "backgroundColor"); + control.bind("value", this.getChildControl("color-button"), "textColor", { + converter: value => osparc.utils.Utils.getContrastedBinaryColor(value) + }); + this.__validationManager.add(control, osparc.utils.Validators.hexColor); break; case "color-button": - // Random color generator button in edit mode - if (this.__colorButton === null) { - this.__colorButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/sync-alt/12"); - this.__colorButton.addListener("execute", () => { - this.getChildControl("color-input").setValue(osparc.utils.Utils.getRandomColor()); - }, this); - } - control = this.__colorButton; + control = new qx.ui.form.Button(null, "@FontAwesome5Solid/sync-alt/12"); + control.addListener("execute", () => { + this.getChildControl("color-input").setValue(osparc.utils.Utils.getRandomColor()); + }, this); break; } return control || this.base(arguments, id); @@ -171,7 +154,10 @@ qx.Class.define("osparc.form.tag.TagItem", { tag.bind("name", this, "name"); tag.bind("description", this, "description"); tag.bind("color", this, "color"); + tag.bind("myAccessRights", this, "myAccessRights"); tag.bind("accessRights", this, "accessRights"); + + this.__renderLayout(); }, /** @@ -212,40 +198,48 @@ qx.Class.define("osparc.form.tag.TagItem", { }, __renderDisplayMode: function() { - const tagContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox()).set({ - width: 100 - }); - tagContainer.add(this.getChildControl("tag")); - this._add(tagContainer); - const descriptionContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox()); - descriptionContainer.add(this.getChildControl("description"), { - width: "100%" - }); - this._add(descriptionContainer, { + this._add(this.getChildControl("tag")); + this._add(this.getChildControl("description"), { flex: 1 }); + this._add(this.getChildControl("shared-icon")); this._add(this.__tagItemButtons()); this.resetBackgroundColor(); }, + __openAccessRights: function() { + const permissionsView = new osparc.share.CollaboratorsTag(this.getTag()); + const title = this.tr("Share Tag"); + osparc.ui.window.Window.popUpInWindow(permissionsView, title, 600, 600); + + permissionsView.addListener("updateAccessRights", () => { + const accessRights = this.getTag().getAccessRights(); + if (accessRights) { + const sharedIcon = this.getChildControl("shared-icon"); + osparc.dashboard.CardBase.populateShareIcon(sharedIcon, accessRights); + } + }, this); + }, + /** * Generates and returns the buttons for deleting and editing an existing label (display mode) */ __tagItemButtons: function() { + const canIWrite = osparc.share.CollaboratorsTag.canIWrite(this.getMyAccessRights()); + const canIDelete = osparc.share.CollaboratorsTag.canIDelete(this.getMyAccessRights()); + const buttonContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox()); const editButton = new qx.ui.form.Button().set({ icon: "@FontAwesome5Solid/pencil-alt/12", - toolTipText: this.tr("Edit") + toolTipText: this.tr("Edit"), + enabled: canIWrite, }); const deleteButton = new osparc.ui.form.FetchButton().set({ appearance: "danger-button", icon: "@FontAwesome5Solid/trash/12", - toolTipText: this.tr("Delete") + toolTipText: this.tr("Delete"), + enabled: canIDelete, }); - if (this.isPropertyInitialized("accessRights")) { - editButton.setEnabled(this.getAccessRights()["write"]); - deleteButton.setEnabled(this.getAccessRights()["delete"]); - } buttonContainer.add(editButton); buttonContainer.add(deleteButton); editButton.addListener("execute", () => this.setMode(this.self().modes.EDIT), this); @@ -279,20 +273,31 @@ qx.Class.define("osparc.form.tag.TagItem", { if (this.__validationManager.validate()) { const data = this.__serializeData(); saveButton.setFetching(true); - let fetch; + const tagsStore = osparc.store.Tags.getInstance(); if (this.isPropertyInitialized("id")) { - fetch = osparc.store.Tags.getInstance().putTag(this.getId(), data); + tagsStore.putTag(this.getId(), data) + .then(tag => this.setTag(tag)) + .catch(console.error) + .finally(() => { + this.fireEvent("tagSaved"); + this.setMode(this.self().modes.DISPLAY); + saveButton.setFetching(false); + }); } else { - fetch = osparc.store.Tags.getInstance().postTag(data); + let newTag = null; + tagsStore.postTag(data) + .then(tag => { + newTag = tag; + return tagsStore.fetchAccessRights(tag); + }) + .then(() => this.setTag(newTag)) + .catch(console.error) + .finally(() => { + this.fireEvent("tagSaved"); + this.setMode(this.self().modes.DISPLAY); + saveButton.setFetching(false); + }); } - fetch - .then(tag => this.setTag(tag)) - .catch(console.error) - .finally(() => { - this.fireEvent("tagSaved"); - this.setMode(this.self().modes.DISPLAY); - saveButton.setFetching(false); - }); } }, this); cancelButton.addListener("execute", () => { @@ -334,7 +339,7 @@ qx.Class.define("osparc.form.tag.TagItem", { color: color }; }, - _applyMode: function() { + __applyMode: function() { this.__renderLayout(); } } diff --git a/services/static-webserver/client/source/class/osparc/share/Collaborators.js b/services/static-webserver/client/source/class/osparc/share/Collaborators.js index b012119b0254..cdeeb99c419b 100644 --- a/services/static-webserver/client/source/class/osparc/share/Collaborators.js +++ b/services/static-webserver/client/source/class/osparc/share/Collaborators.js @@ -189,16 +189,22 @@ qx.Class.define("osparc.share.Collaborators", { // Access Rights are set at workspace level return false; } + let canIShare = false; switch (this._resourceType) { case "study": case "template": + canIShare = osparc.study.Utils.canIWrite(this._serializedDataCopy["accessRights"]); + break; case "service": canIShare = osparc.service.Utils.canIWrite(this._serializedDataCopy["accessRights"]); break; case "workspace": canIShare = osparc.share.CollaboratorsWorkspace.canIDelete(this._serializedDataCopy["myAccessRights"]); break; + case "tag": + canIShare = osparc.share.CollaboratorsTag.canIWrite(this._serializedDataCopy["myAccessRights"]); + break; } return canIShare; }, @@ -220,6 +226,9 @@ qx.Class.define("osparc.share.Collaborators", { case "workspace": fullOptions = osparc.share.CollaboratorsWorkspace.canIDelete(this._serializedDataCopy["myAccessRights"]); break; + case "tag": + fullOptions = osparc.share.CollaboratorsTag.canIDelete(this._serializedDataCopy["myAccessRights"]); + break; } return fullOptions; }, @@ -227,13 +236,17 @@ qx.Class.define("osparc.share.Collaborators", { __createRolesLayout: function() { let rolesLayout = null; switch (this._resourceType) { + case "study": + case "template": + rolesLayout = osparc.data.Roles.createRolesStudyInfo(); + break; case "service": rolesLayout = osparc.data.Roles.createRolesServicesInfo(); break; case "workspace": rolesLayout = osparc.data.Roles.createRolesWorkspaceInfo(); break; - default: + case "tag": rolesLayout = osparc.data.Roles.createRolesStudyInfo(); break; } diff --git a/services/static-webserver/client/source/class/osparc/share/CollaboratorsTag.js b/services/static-webserver/client/source/class/osparc/share/CollaboratorsTag.js new file mode 100644 index 000000000000..0327bf29589d --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/share/CollaboratorsTag.js @@ -0,0 +1,172 @@ +/* ************************************************************************ + + 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.share.CollaboratorsTag", { + extend: osparc.share.Collaborators, + + /** + * @param tag {osparc.data.model.Tag} + */ + construct: function(tag) { + this.__tag = tag; + this._resourceType = "tag"; + + const tagDataCopy = tag.serialize(); + this.base(arguments, tagDataCopy, []); + }, + + statics: { + canIWrite: function(myAccessRights) { + return myAccessRights["write"]; + }, + + canIDelete: function(myAccessRights) { + return myAccessRights["delete"]; + }, + + getViewerAccessRight: function() { + return { + "read": true, + "write": false, + "delete": false + }; + }, + + getCollaboratorAccessRight: function() { + return { + "read": true, + "write": true, + "delete": false + }; + }, + + getOwnerAccessRight: function() { + return { + "read": true, + "write": true, + "delete": true + }; + } + }, + + members: { + __tag: null, + + _addEditors: function(gids) { + if (gids.length === 0) { + return; + } + + const newCollaborators = {}; + gids.forEach(gid => newCollaborators[gid] = this.self().getViewerAccessRight()); + osparc.store.Tags.getInstance().addCollaborators(this.__tag.getTagId(), newCollaborators) + .then(() => { + const text = this.tr("Tag successfully shared"); + osparc.FlashMessenger.getInstance().logAs(text); + this.fireDataEvent("updateAccessRights", this.__tag.serialize()); + this._reloadCollaboratorsList(); + }) + .catch(err => { + console.error(err); + osparc.FlashMessenger.getInstance().logAs(this.tr("Something went wrong sharing the Tag"), "ERROR"); + }); + }, + + _deleteMember: function(collaborator, item) { + if (item) { + item.setEnabled(false); + } + + osparc.store.Tags.getInstance().removeCollaborator(this.__tag.getTagId(), collaborator["gid"]) + .then(() => { + this.fireDataEvent("updateAccessRights", this.__tag.serialize()); + osparc.FlashMessenger.getInstance().logAs(collaborator["name"] + this.tr(" successfully removed")); + this._reloadCollaboratorsList(); + }) + .catch(err => { + console.error(err); + osparc.FlashMessenger.getInstance().logAs(this.tr("Something went wrong removing ") + collaborator["name"], "ERROR"); + }) + .finally(() => { + if (item) { + item.setEnabled(true); + } + }); + }, + + __make: function(collaboratorGId, newAccessRights, successMsg, failureMsg, item) { + item.setEnabled(false); + + osparc.store.Tags.getInstance().updateCollaborator(this.__tag.getTagId(), collaboratorGId, newAccessRights) + .then(() => { + this.fireDataEvent("updateAccessRights", this.__tag.serialize()); + osparc.FlashMessenger.getInstance().logAs(successMsg); + this._reloadCollaboratorsList(); + }) + .catch(err => { + console.error(err); + osparc.FlashMessenger.getInstance().logAs(failureMsg, "ERROR"); + }) + .finally(() => { + if (item) { + item.setEnabled(true); + } + }); + }, + + _promoteToEditor: function(collaborator, item) { + this.__make( + collaborator["gid"], + this.self().getCollaboratorAccessRight(), + this.tr(`Successfully promoted to ${osparc.data.Roles.STUDY[2].label}`), + this.tr(`Something went wrong promoting to ${osparc.data.Roles.STUDY[2].label}`), + item + ); + }, + + _promoteToOwner: function(collaborator, item) { + this.__make( + collaborator["gid"], + this.self().getOwnerAccessRight(), + this.tr(`Successfully promoted to ${osparc.data.Roles.STUDY[3].label}`), + this.tr(`Something went wrong promoting to ${osparc.data.Roles.STUDY[3].label}`), + item + ); + }, + + _demoteToUser: async function(collaborator, item) { + this.__make( + collaborator["gid"], + this.self().getViewerAccessRight(), + this.tr(`Successfully demoted to ${osparc.data.Roles.STUDY[1].label}`), + this.tr(`Something went wrong demoting to ${osparc.data.Roles.STUDY[1].label}`), + item + ); + }, + + _demoteToEditor: function(collaborator, item) { + this.__make( + collaborator["gid"], + this.self().getCollaboratorAccessRight(), + this.tr(`Successfully demoted to ${osparc.data.Roles.STUDY[2].label}`), + this.tr(`Something went wrong demoting to ${osparc.data.Roles.STUDY[2].label}`), + item + ); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/store/Tags.js b/services/static-webserver/client/source/class/osparc/store/Tags.js index c07e9d8d8aee..dcc0c15cbd64 100644 --- a/services/static-webserver/client/source/class/osparc/store/Tags.js +++ b/services/static-webserver/client/source/class/osparc/store/Tags.js @@ -45,6 +45,7 @@ qx.Class.define("osparc.store.Tags", { tagsData.forEach(tagData => { const tag = this.__addToCache(tagData); tags.push(tag); + this.fetchAccessRights(tag); }); return tags; }); @@ -54,6 +55,10 @@ qx.Class.define("osparc.store.Tags", { return this.tagsCached; }, + getTag: function(tagId = null) { + return this.tagsCached.find(f => f.getTagId() === tagId); + }, + postTag: function(newTagData) { const params = { data: newTagData @@ -97,8 +102,84 @@ qx.Class.define("osparc.store.Tags", { .catch(console.error); }, - getTag: function(tagId = null) { - return this.tagsCached.find(f => f.getTagId() === tagId); + fetchAccessRights: function(tag) { + const params = { + url: { + "tagId": tag.getTagId() + } + }; + return osparc.data.Resources.fetch("tags", "getAccessRights", params) + .then(accessRightsArray => { + const accessRights = {}; + accessRightsArray.forEach(ar => accessRights[ar.gid] = ar); + tag.setAccessRights(accessRights) + }) + .catch(err => console.error(err)); + }, + + addCollaborators: function(tagId, newCollaborators) { + const promises = []; + Object.keys(newCollaborators).forEach(groupId => { + const params = { + url: { + tagId, + groupId, + }, + data: newCollaborators[groupId] + }; + promises.push(osparc.data.Resources.fetch("tags", "postAccessRights", params)); + }); + return Promise.all(promises) + .then(() => { + const tag = this.getTag(tagId); + const newAccessRights = tag.getAccessRights(); + Object.keys(newCollaborators).forEach(gid => { + newAccessRights[gid] = newCollaborators[gid]; + }); + tag.set({ + accessRights: newAccessRights, + }); + }) + .catch(console.error); + }, + + removeCollaborator: function(tagId, groupId) { + const params = { + url: { + tagId, + groupId, + } + }; + return osparc.data.Resources.fetch("tags", "deleteAccessRights", params) + .then(() => { + const tag = this.getTag(tagId); + const newAccessRights = tag.getAccessRights(); + delete newAccessRights[groupId]; + tag.set({ + accessRights: newAccessRights, + }); + }) + .catch(console.error); + }, + + updateCollaborator: function(tagId, groupId, newPermissions) { + const params = { + url: { + tagId, + groupId, + }, + data: newPermissions + }; + return osparc.data.Resources.fetch("tags", "putAccessRights", params) + .then(() => { + const tag = this.getTag(tagId); + const newAccessRights = tag.getAccessRights(); + newAccessRights[groupId] = newPermissions; + tag.set({ + accessRights: tag.newAccessRights, + }); + }) + .catch(console.error); }, __addToCache: function(tagData) { diff --git a/services/static-webserver/client/source/class/osparc/ui/list/CollaboratorListItem.js b/services/static-webserver/client/source/class/osparc/ui/list/CollaboratorListItem.js index eb881568b389..c51147869fb1 100644 --- a/services/static-webserver/client/source/class/osparc/ui/list/CollaboratorListItem.js +++ b/services/static-webserver/client/source/class/osparc/ui/list/CollaboratorListItem.js @@ -82,6 +82,8 @@ qx.Class.define("osparc.ui.list.CollaboratorListItem", { return osparc.data.Roles.SERVICES[i]; } else if (resource === "workspace") { return osparc.data.Roles.WORKSPACE[i]; + } else if (resource === "tag") { + return osparc.data.Roles.STUDY[i]; } return undefined; },