diff --git a/package-lock.json b/package-lock.json index 55935a1..5b29bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "version": "3.0.24", "license": "MIT", "dependencies": { - "@eluvio/elv-client-js": "^4.0.63", + "@eluvio/elv-client-js": "^4.0.66", "bignumber.js": "^8.1.1", "browser-cancelable-events": "^1.0.1", "browser-solc": "^1.0.0", @@ -2019,9 +2019,9 @@ } }, "node_modules/@eluvio/elv-client-js": { - "version": "4.0.63", - "resolved": "https://registry.npmjs.org/@eluvio/elv-client-js/-/elv-client-js-4.0.63.tgz", - "integrity": "sha512-v6QMrqX75B3Js9tig1dnHsyCUCfMhVw/eGEjnDbWdwVj1l/WYYwwnlbWpoWP7/lxWiiDjxSTkVMKJjYymW6O/g==", + "version": "4.0.66", + "resolved": "https://registry.npmjs.org/@eluvio/elv-client-js/-/elv-client-js-4.0.66.tgz", + "integrity": "sha512-aWl2/x9eCNCNRwCgHYkfBZD4LStJZi0i4CKNJd4W7e6d3j2zCXRgMbED8SH36Rp0tyrUV5ASWvW2tuJsNKU2aw==", "dependencies": { "@babel/runtime": "^7.8.4", "@eluvio/crypto": "^1.1.1", @@ -23320,9 +23320,9 @@ } }, "@eluvio/elv-client-js": { - "version": "4.0.63", - "resolved": "https://registry.npmjs.org/@eluvio/elv-client-js/-/elv-client-js-4.0.63.tgz", - "integrity": "sha512-v6QMrqX75B3Js9tig1dnHsyCUCfMhVw/eGEjnDbWdwVj1l/WYYwwnlbWpoWP7/lxWiiDjxSTkVMKJjYymW6O/g==", + "version": "4.0.66", + "resolved": "https://registry.npmjs.org/@eluvio/elv-client-js/-/elv-client-js-4.0.66.tgz", + "integrity": "sha512-aWl2/x9eCNCNRwCgHYkfBZD4LStJZi0i4CKNJd4W7e6d3j2zCXRgMbED8SH36Rp0tyrUV5ASWvW2tuJsNKU2aw==", "requires": { "@babel/runtime": "^7.8.4", "@eluvio/crypto": "^1.1.1", diff --git a/package.json b/package.json index c156748..24ed971 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "last 2 versions" ], "dependencies": { - "@eluvio/elv-client-js": "^4.0.63", + "@eluvio/elv-client-js": "^4.0.66", "bignumber.js": "^8.1.1", "browser-cancelable-events": "^1.0.1", "browser-solc": "^1.0.0", diff --git a/src/clients/Fabric.js b/src/clients/Fabric.js index 1051185..f03b346 100644 --- a/src/clients/Fabric.js +++ b/src/clients/Fabric.js @@ -311,7 +311,8 @@ const Fabric = { currentAccountAddress, imageUrl, kmsAddress, - tenantId + tenantId, + isManager ] = await Promise.all([ client.ContentLibrary({libraryId}), // libraryInfo Fabric.ListLibraryContentTypes({libraryId}), // types @@ -333,6 +334,10 @@ const Fabric = { methodArgs: [ "_tenantId" ] + }), + client.CallContractMethod({ + contractAddress: client.utils.HashToAddress(libraryId), + methodName: "canEdit", }) ]); @@ -354,7 +359,6 @@ const Fabric = { */ - return { ...libraryInfo, libraryId, @@ -371,7 +375,8 @@ const Fabric = { owner, ownerName, isOwner: EqualAddress(owner, currentAccountAddress), - isContentSpaceLibrary: libraryId === Fabric.contentSpaceLibraryId + isContentSpaceLibrary: libraryId === Fabric.contentSpaceLibraryId, + isManager }; }, @@ -428,7 +433,34 @@ const Fabric = { /* Library Groups */ ListContentLibraryGroups: async ({libraryId, type, params={}}) => { - return await Fabric.ListAccessGroups({params: {libraryId, type, ...params}}); + if(["accessor", "contributor"].includes(type)) { + return await Fabric.ListAccessGroups({params: {libraryId, type, ...params}}); + } else if(type === "manage") { + const accessGroups = {}; + const accessGroupList = await Fabric.ListContentLibraryManagerGroups({libraryId}); + + for(let i = 0; i < accessGroupList.length; i++) { + const address = accessGroupList[i]; + accessGroups[address] = await Fabric.GetAccessGroup({contractAddress: address, publicOnly: false}); + } + + return { + accessGroups, + count: accessGroupList.length + }; + } + }, + + ListContentLibraryManagerGroups: async ({libraryId}) => { + const response = client.utils.FromHex( + await client.CallContractMethod({ + contractAddress: Fabric.utils.HashToAddress(libraryId), + methodName: "getMeta", + methodArgs: ["managerGroups"] + }) + ); + + return response && JSON.parse(response) ? JSON.parse(response) : []; }, AddContentLibraryGroup: async ({libraryId, address, groupType}) => { @@ -443,6 +475,89 @@ const Fabric = { } }, + AddContentLibraryManagerGroup: async ({libraryId, address}) => { + const contractAddress = client.utils.HashToAddress(libraryId); + const event = await client.CallContractMethodAndWait({ + contractAddress, + methodName: "setRights", + methodArgs: [ + FormatAddress(address), + 2, // TYPE_SEE = 0; TYPE_ACCESS = 1; TYPE_EDIT = 2 + 1 // adding + ] + }); + + const managerGroupsList = await Fabric.ListContentLibraryManagerGroups({libraryId}); + + if(!managerGroupsList.includes(address)) { + managerGroupsList.push(address); + } + + await client.ReplaceContractMetadata({ + contractAddress, + metadataKey: "managerGroups", + metadata: managerGroupsList + }); + + if(!event.logs || event.logs.length === 0) { + throw Error(`Failed to add manage group ${address}`); + } + }, + + CheckLibraryCanContribute: async ({libraryId}) => { + const contractAddress = client.utils.HashToAddress(libraryId); + return client.CallContractMethod({ + contractAddress, + methodName: "canContribute", + methodArgs: [ + FormatAddress(Fabric.currentAccountAddress) + ] + }); + }, + + RemoveContentLibraryManagerGroup: async ({libraryId, address}) => { + const contractAddress = client.utils.HashToAddress(libraryId); + await client.CallContractMethodAndWait({ + contractAddress, + methodName: "setRights", + methodArgs: [ + FormatAddress(address), + 2, // TYPE_SEE = 0; TYPE_ACCESS = 1; TYPE_EDIT = 2 + 0 // revoking + ] + }); + + let managerGroupList = await Fabric.ListContentLibraryManagerGroups({libraryId}); + + managerGroupList = managerGroupList.filter(groupAddress => groupAddress !== address); + + await client.ReplaceContractMetadata({ + contractAddress, + metadataKey: "managerGroups", + metadata: managerGroupList + }); + }, + + SetContentLibraryRights: async ({libraryId, address, type="SEE", access}) => { + // access - 1 add | 0 revoke + const typeMap = { + "SEE": 0, + "ACCESS": 1, + "EDIT": 2 + }; + const accessType = typeMap[type]; + + await client.CallContractMethodAndWait({ + contractAddress: client.utils.HashToAddress(libraryId), + methodName: "setRights", + methodArgs: [ + FormatAddress(address), + accessType, + access + ] + }); + }, + RemoveContentLibraryGroup: async ({libraryId, address, groupType}) => { const event = await client.CallContractMethodAndWait({ contractAddress: client.utils.HashToAddress(libraryId), @@ -630,6 +745,7 @@ const Fabric = { const isContentLibraryObject = client.utils.EqualHash(libraryId, objectId); const isContentType = libraryId === Fabric.contentSpaceLibraryId && !isContentLibraryObject; const isNormalObject = !isContentLibraryObject && !isContentType; + const {isV3} = await client.ContractInfo({id: objectId}); const latestVersionHash = await client.LatestVersionHash({objectId}); @@ -791,7 +907,8 @@ const Fabric = { isContentLibraryObject, isContentType, isNormalObject, - accessType + accessType, + isV3 }; }, diff --git a/src/components/pages/access_groups/AccessGroup.js b/src/components/pages/access_groups/AccessGroup.js index 92135a8..d640e77 100644 --- a/src/components/pages/access_groups/AccessGroup.js +++ b/src/components/pages/access_groups/AccessGroup.js @@ -283,7 +283,10 @@ class AccessGroup extends React.Component { {}} + LoadGroupPermissions={async () => { + await this.props.groupStore.AccessGroupGroupPermissions({contractAddress: this.props.groupStore.contractAddress}); + await this.props.groupStore.SetInheritedGroupAccess(); + }} /> ); } else { diff --git a/src/components/pages/content/ContentLibrary.js b/src/components/pages/content/ContentLibrary.js index 8db85bf..1bab0b4 100644 --- a/src/components/pages/content/ContentLibrary.js +++ b/src/components/pages/content/ContentLibrary.js @@ -6,7 +6,7 @@ import ContentIcon from "../../../static/icons/content.svg"; import {LabelledField} from "../../components/LabelledField"; import ClippedText from "../../components/ClippedText"; import {PageHeader} from "../../components/Page"; -import {Action, Tabs} from "elv-components-js"; +import {Action, Confirm, IconButton, Tabs} from "elv-components-js"; import AsyncComponent from "../../components/AsyncComponent"; import Listing from "../../components/Listing"; @@ -18,6 +18,7 @@ import ContentLibraryGroupForm from "./ContentLibraryGroupForm"; import {ContentBrowserModal} from "../../components/ContentBrowser"; import {Redirect} from "react-router"; import ActionsToolbar from "../../components/ActionsToolbar"; +import RemoveIcon from "../../../static/icons/close.svg"; @inject("libraryStore") @inject("groupStore") @@ -73,6 +74,23 @@ class ContentLibrary extends React.Component { ); } + async RemoveAccessGroupPermission({groupAddress}) { + await Confirm({ + message: "Are you sure you want to remove this group's permissions on this library?", + onConfirm: async () => { + await this.props.libraryStore.RemoveContentLibraryGroupPermission({ + libraryId: this.props.libraryStore.libraryId, + groupAddress, + type: this.state.groupsView + }); + } + }); + + if(this.state.listingRef) { + this.state.listingRef.Load(); + } + } + AccessGroups() { const groups = this.props.libraryStore.library[`${this.state.groupsView}Groups`]; @@ -86,7 +104,17 @@ class ContentLibrary extends React.Component { sortKey: (group.name || "zz").toLowerCase(), title: group.name || address, description: group.description, - link: UrlJoin("/access-groups", address) + status: ( +
+ this.RemoveAccessGroupPermission({groupAddress: address})} + > + Remove + +
+ ) }; }); @@ -102,27 +130,35 @@ class ContentLibrary extends React.Component { }} render={() => ( -
- this.setState({showGroupForm: true})}> - Manage Group Permissions - -
+ { + this.props.libraryStore.library.isManager && +
+ this.setState({showGroupForm: true})}> + Manage Group Permissions + +
+ } this.setState({groupsView: value})} /> { + if(!this.state.listingRef) { + this.setState({listingRef: ref}); + } + }} key={`library-access-group-listing-${this.state.groupsView}`} className="compact" pageId="LibraryAccessGroups" noIcon={true} - noStatus={true} + noLink={true} paginate={true} count={this.props.libraryStore.library[`${type}GroupsCount`] || 0} LoadContent={async ({params}) => { @@ -316,7 +352,7 @@ class ContentLibrary extends React.Component { { label: "Manage", type: "link", - hidden: !this.props.libraryStore.library.isOwner, + hidden: !(this.props.libraryStore.library.isOwner || this.props.libraryStore.library.isManager), path: UrlJoin(this.props.match.url, "edit"), }, { @@ -334,7 +370,7 @@ class ContentLibrary extends React.Component { { label: "Create From Existing", type: "button", - hidden: this.props.libraryStore.library.isContentSpaceLibrary || !(this.props.libraryStore.library.isOwner && this.props.libraryStore.library.canContribute), + hidden: this.props.libraryStore.library.isContentSpaceLibrary || !(this.props.libraryStore.library.isManager && this.props.libraryStore.library.canContribute), onClick: () => this.setState({showCopyObjectModal: true}) } ]} diff --git a/src/components/pages/content/ContentLibraryGroupForm.js b/src/components/pages/content/ContentLibraryGroupForm.js index 3e42ab5..a1d9a89 100644 --- a/src/components/pages/content/ContentLibraryGroupForm.js +++ b/src/components/pages/content/ContentLibraryGroupForm.js @@ -13,9 +13,7 @@ class ContentLibraryGroupForm extends React.Component { this.state = { groupAddress: "", - accessor: false, - reviewer: false, - contributor: false + selectedPermission: "" }; this.PageContent = this.PageContent.bind(this); @@ -28,9 +26,9 @@ class ContentLibraryGroupForm extends React.Component { this.setState({ groupAddress: event.target.value, - accessor: !!permissions.accessor, - contributor: !!permissions.contributor, - reviewer: !!permissions.reviewer + selectedPermission: ( + permissions.manage ? "MANAGE" : permissions.contributor ? "CONTRIBUTE" : permissions.accessor ? "VIEW" : "" + ) }); } @@ -38,9 +36,9 @@ class ContentLibraryGroupForm extends React.Component { await this.props.libraryStore.UpdateContentLibraryGroup({ libraryId: this.props.libraryStore.libraryId, groupAddress: this.state.groupAddress, - accessor: this.state.accessor, - reviewer: this.state.reviewer, - contributor: this.state.contributor + accessor: this.state.selectedPermission === "VIEW", + manage: this.state.selectedPermission === "MANAGE", + contributor: this.state.selectedPermission === "CONTRIBUTE" }); await this.props.LoadGroups(); @@ -102,27 +100,54 @@ class ContentLibraryGroupForm extends React.Component { >
{ this.Groups() } +
- +
this.setState({accessor: !this.state.accessor})} + type="radio" + id="view" + checked={this.state.selectedPermission === "VIEW"} + value={this.state.selectedPermission === "VIEW"} + onChange={() => this.setState({selectedPermission: "VIEW"})} /> +
+ +
+ List content objects in the library. View library metadata. +
+
+
- +
this.setState({contributor: !this.state.contributor})} + type="radio" + id="contribute" + checked={this.state.selectedPermission === "CONTRIBUTE"} + value={this.state.selectedPermission === "CONTRIBUTE"} + onChange={() => this.setState({selectedPermission: "CONTRIBUTE"})} /> +
+ +
+ List and create new content objects in the library. View library metadata. +
+
+
- +
this.setState({reviewer: !this.state.reviewer})} + type="radio" + id="manage" + checked={this.state.selectedPermission === "MANAGE"} + value={this.state.selectedPermission === "MANAGE"} + onChange={() => this.setState({selectedPermission: "MANAGE"})} /> +
+ +
+ List, create and delete content objects in the library. Edit library metadata. +
+
)} diff --git a/src/components/pages/content/ContentObject.js b/src/components/pages/content/ContentObject.js index 5a3a87f..0d765c6 100644 --- a/src/components/pages/content/ContentObject.js +++ b/src/components/pages/content/ContentObject.js @@ -866,7 +866,11 @@ class ContentObject extends React.Component { { label: "Delete", type: "button", - hidden: this.props.objectStore.object.isContentLibraryObject || !this.props.objectStore.object.canEdit || !this.props.objectStore.object.isOwner, + hidden: ( + this.props.objectStore.object.isContentLibraryObject || + (this.props.objectStore.object.isV3 && !this.props.libraryStore.library.isManager) || + (!this.props.objectStore.object.isV3 && !this.props.objectStore.object.isOwner) + ), onClick: () => this.DeleteContentObject(), className: "danger", dividerAbove: true diff --git a/src/components/pages/content/ContentObjectGroups.js b/src/components/pages/content/ContentObjectGroups.js index 1d456ee..5210d89 100644 --- a/src/components/pages/content/ContentObjectGroups.js +++ b/src/components/pages/content/ContentObjectGroups.js @@ -1,11 +1,11 @@ import React from "react"; -import UrlJoin from "url-join"; -import {Action, AsyncComponent, Tabs} from "elv-components-js"; +import {Action, AsyncComponent, Confirm, IconButton, Tabs} from "elv-components-js"; import Listing from "../../components/Listing"; import {inject, observer} from "mobx-react"; import PropTypes from "prop-types"; import ContentObjectGroupForm from "./ContentObjectGroupForm"; +import RemoveIcon from "../../../static/icons/close.svg"; @inject("objectStore") @inject("groupStore") @@ -32,8 +32,9 @@ class ContentObjectGroups extends React.Component { const page = filterOptions.params.page; const perPage = filterOptions.params.perPage; + const groupsObject = (this.props.currentPage === "contentObject" ? (this.props.objectStore.objectGroupPermissions || {}) : this.props.groupStore.accessGroup.groupPermissions || {}); let groups = Object - .values(this.props.currentPage === "contentObject" ? (this.props.objectStore.objectGroupPermissions || {}) : this.props.groupStore.accessGroup.groupPermissions || {}) + .values(groupsObject) .filter(group => group.permissions.includes(this.state.view)) .sort((a, b) => (a.name || "zz").toLowerCase() > (b.name || "zz").toLowerCase() ? 1 : -1) .slice((page - 1) * perPage, page * perPage); @@ -46,6 +47,34 @@ class ContentObjectGroups extends React.Component { return groups; } + async RemoveAccessGroupPermission({groupAddress}) { + let itemText; + if(this.props.currentPage === "accessGroup") { + itemText = "group"; + } else if(this.props.objectStore.object.isContentType) { + itemText = "content type"; + } else { + itemText = "object"; + } + + await Confirm({ + message: `Are you sure you want to remove this group's permissions on this ${itemText}?`, + onConfirm: async () => { + await this.props.objectStore.RemoveContentObjectGroupPermission({ + objectId: this.props.objectStore.objectId, + groupAddress, + permission: this.state.view + }); + } + }); + + if(this.state.listingRef) { + this.state.listingRef.Load(); + } + + this.setState({pageVersion: this.state.pageVersion + 1}); + } + AccessGroups() { const groupsInfo = this.FilterGroups(this.state.filterOptions).map(group => { return { @@ -53,7 +82,17 @@ class ContentObjectGroups extends React.Component { sortKey: (group.name || "zz").toLowerCase(), title: group.name || group.address, description: group.description, - link: UrlJoin("/access-groups", group.address) + status: ( +
+ this.RemoveAccessGroupPermission({groupAddress: group.address})} + > + Remove + +
+ ) }; }); @@ -98,7 +137,6 @@ class ContentObjectGroups extends React.Component { params: {}, publicOnly: true }); - await this.props.LoadGroupPermissions(); } }} render={() => ( @@ -111,14 +149,22 @@ class ContentObjectGroups extends React.Component { onChange={(value) => this.setState({view: value})} /> { + if(!this.state.listingRef) { + this.setState({listingRef: ref}); + } + }} key={`object-access-group-listing-${this.state.view}-${this.state.pageVersion}`} className="compact" pageId="ObjectAccessGroups" noIcon={true} - noStatus={true} + noLink={true} paginate={true} count={GroupCount()} - LoadContent={(filterOptions => this.setState(filterOptions))} + LoadContent={async (filterOptions) => { + await this.props.LoadGroupPermissions(); + this.setState(filterOptions); + }} RenderContent={() => this.AccessGroups()} /> { diff --git a/src/static/stylesheets/forms.scss b/src/static/stylesheets/forms.scss index 25cb5c1..03ed07a 100644 --- a/src/static/stylesheets/forms.scss +++ b/src/static/stylesheets/forms.scss @@ -108,6 +108,32 @@ form { } } +.-elv-form { + input { + &[type="radio"] { + cursor: pointer; + height: 100%; + margin: 3px 0 0; + width: 1rem; + } + } +} + +.radio-item { + align-items: flex-start; + display: flex; + flex-direction: row; + gap: 2rem; // sass-lint:disable-line no-misspelled-properties + margin-bottom: 1rem; + + .radio-helper-text { + color: $elv-color-text-lighter; + font-size: $elv-font-m; + margin-top: $elv-spacing-xxs; + } +} + + fieldset { display: flex; flex-direction: column; diff --git a/src/stores/Library.js b/src/stores/Library.js index 4d3c3b6..37f981a 100644 --- a/src/stores/Library.js +++ b/src/stores/Library.js @@ -57,9 +57,11 @@ class LibraryStore { Fabric.AccessGroupAddresses() ]); + const canContribute = yield Fabric.CheckLibraryCanContribute({libraryId}); + this.libraries[libraryId].canContribute = this.libraries[libraryId].isOwner || - (contributorGroups.filter(address => userGroups.includes(address))).length > 0; + (contributorGroups.filter(address => userGroups.includes(address))).length > 0 || canContribute; } }); @@ -221,8 +223,15 @@ class LibraryStore { ContentLibraryGroupPermissions = flow(function * ({libraryId}) { let permissions = {}; + const SetPermission = ({address, type}) => { + permissions[address] = { + [type]: true, + ...(permissions[address] || {}) + }; + }; + yield Promise.all( - ["accessor", "contributor", "reviewer"].map(async type => { + ["accessor", "contributor"].map(async type => { const {accessGroups} = await Fabric.ListContentLibraryGroups({ libraryId, type, @@ -230,31 +239,83 @@ class LibraryStore { }); Object.keys(accessGroups).forEach(address => { - permissions[address] = { - [type]: true, - ...(permissions[address] || {}) - }; + SetPermission({address, type}); }); }) ); + const managerGroups = yield Fabric.ListContentLibraryManagerGroups({libraryId}); + managerGroups.forEach(address => { + SetPermission({address, type: "manage"}); + }); + this.libraries[libraryId].groupPermissions = permissions; }); @action.bound - UpdateContentLibraryGroup = flow(function * ({libraryId, groupAddress, accessor, contributor, reviewer}) { - const options = { accessor, contributor, reviewer}; + RemoveContentLibraryGroupPermission = flow(function * ({libraryId, groupAddress, type}) { + if(type === "manage") { + yield Fabric.RemoveContentLibraryManagerGroup({libraryId, address: groupAddress}); + } else { + yield Fabric.RemoveContentLibraryGroup({ + libraryId, + address: groupAddress, + groupType: type + }); + } + + this.rootStore.notificationStore.SetNotificationMessage({ + message: "Successfully removed library group permissions", + redirect: true + }); + }); + + @action.bound + UpdateContentLibraryGroup = flow(function * ({ + libraryId, + groupAddress, + accessor, + contributor, + manage + }) { + const options = { accessor, contributor, manage }; const permissions = this.libraries[libraryId].groupPermissions[groupAddress] || {}; yield Promise.all( - ["accessor", "contributor", "reviewer"].map(async type => { + ["accessor", "contributor", "manage"].map(async type => { if(!permissions[type] && options[type]) { // Add group - await Fabric.AddContentLibraryGroup({libraryId, address: groupAddress, groupType: type}); + if(type === "manage") { + await Fabric.AddContentLibraryManagerGroup({libraryId, address: groupAddress}); + } else { + await Fabric.AddContentLibraryGroup({libraryId, address: groupAddress, groupType: type}); + } + + if(type === "contributor") { + await Fabric.SetContentLibraryRights({ + libraryId, + address: groupAddress, + type: "ACCESS", + access: 1 + }); + } } else if(permissions[type] && !options[type]) { // Remove group - await Fabric.RemoveContentLibraryGroup({libraryId, address: groupAddress, groupType: type}); + if(type === "manage") { + await Fabric.RemoveContentLibraryManagerGroup({libraryId, address: groupAddress}); + } else { + await Fabric.RemoveContentLibraryGroup({libraryId, address: groupAddress, groupType: type}); + } + + if(type === "contributor") { + await Fabric.SetContentLibraryRights({ + libraryId, + address: groupAddress, + type: "ACCESS", + access: 0 + }); + } } }) ); diff --git a/src/stores/Object.js b/src/stores/Object.js index 984603d..9982bed 100644 --- a/src/stores/Object.js +++ b/src/stores/Object.js @@ -26,7 +26,10 @@ class ObjectStore { } @computed get objectGroupPermissions() { - return this.objects[this.objectId].groupPermissions; + const object = this.objects[this.objectId]; + // eslint-disable-next-line no-unused-vars + + return object ? object.groupPermissions : undefined; } @computed get currentAccountAddress() { @@ -595,6 +598,20 @@ MergeMetadata = flow(function * ({ }); }); + @action.bound + RemoveContentObjectGroupPermission = flow(function * ({objectId, groupAddress, permission}) { + yield Fabric.RemoveContentObjectGroupPermission({ + objectId, + groupAddress, + permission + }); + + this.rootStore.notificationStore.SetNotificationMessage({ + message: "Successfully removed object group permissions", + redirect: false + }); + }); + @action.bound UpdateContentObjectGroupPermissions = flow(function * ({ objectId,