Beyond Permissions: Customizing Asset Action Visibility in Sitecore Content Hub - Part2
Hello everyone! I hope you’re doing well.
Let’s continue exploring how we can customize asset action visibility in a Content Hub external component.
In my previous blog, Beyond Permissions: Customizing Asset Actions in Content Hub, we discussed how to hide the Download button on the asset search page for assets belonging to a specific source system.
However, there’s another important aspect we haven’t covered yet: What happens when a user tries to download the same asset using a bulk operation?
Hiding the Download button from the asset card is one thing, but ensuring that the same restriction applies to bulk operations is equally important. In this blog, we’ll explore how we can handle this scenario using a Content Hub external component.
When policies cannot fully address this requirement, we can still rely on our trump card: an external component. In this scenario, we’ll continue using the same source system, KOL, to demonstrate how we can control asset actions for assets belonging to that source during bulk operation. I have added a screenshot for the reference.
Before diving into the implementation details, let’s first understand the Search and Selection components and how they work together. This will help us better understand the logic behind controlling asset actions in bulk operations.
Search Component: The Search component allows users to search and filter assets in Content Hub based on criteria such as keywords, metadata, and source systems. It displays the matching assets in the asset search page.
Selection Component: The Selection component allows users to select one or more assets from the search results and perform actions on them, such as downloading, editing, or other bulk operations.
How they work together: The Search component provides the assets, while the Selection component manages the selection and actions performed on those assets.You can refer the below screenshot for the reference.
In simple terms, whenever we select assets from the search results on the Asset Search page, those selections are internally stored in a common pool or space shared by the Search and Selection components. This shared space acts as the link between the two components, allowing the Selection component to access the assets selected through the Search component. When we add selection component we get an option to link to a search component shown below.
Now that we have a clear understanding of how the Search and Selection components work together, let’s focus on the implementation logic.
The idea is simple. When a user attempts to download assets through a bulk operation, we first intercept the download action and use the Selection Pool API, along with the selection pool identifier, to retrieve all the entities currently selected.
Once we have the selected entities, we inspect the Source System property of each asset. If any of the selected assets belong to the KOL source system, we prevent the download and display a modal popup informing the user that they are not authorized to download the selected asset(s).
To stop the download operation, we’ll use the browser’s native window.alert() as part of the validation flow.
In below references, you can find more about creating an external component in content hub.I will not go in deep explaining all steps.
Step 1: Create an external component in Content Hub.
Step 2: Add the required configuration to the Content Configuration section of the external component. These configurations will allow our frontend component to identify the relevant Search, Selection, and Selection Pool components.
{
"selectioncomponent": "{Selection component identifier}",
"selectionpool": "{Selection Pool Id}",
"searchcomponent": "{Search component identifier}",
"needSubpoolId": false,
"message": "{Customize message you need}"
}
Step 3: Create the corresponding external component in your frontend repository and implement the logic to handle the bulk download validation.Let's create and index.tsx file and RestrictDownloadButtonOperation.tsx file
index.tsx
/* This popup is used to show a modal pop up based on entity property exist or not in selection operations*/
import { ContentHubClient } from "@sitecore/sc-contenthub-webclient-sdk/dist/clients/content-hub-client";
import { createRoot } from "react-dom/client";
import BulkDownloadButtonModalPopup from "./BulkDownloadButtonModalPopup";
interface Context {
client: ContentHubClient;
options: {
entityId?: number;
};
api: {
search: {
getEventSearchIdentifier: (searchIdentifier: string) => string;
activate: (searchIdentifier: string) => void;
};
};
config: {
searchSettings: Array<{
selectionpool: string;
searchcomponent: string;
selectioncomponent: string;
needSubpoolId: boolean;
message: string;
}>;
};
}
export default function createExternalRoot(container: HTMLElement) {
const root = createRoot(container);
return {
render(context: Context) {
root.render(
<>
{context.config.searchSettings.map((el, index) => (
<BulkDownloadButtonModalPopup
key={index}
subpoolid={el.needSubpoolId ? context.options.entityId : undefined}
searchIdentifier={el.searchcomponent}
selectionPool={el.selectionpool}
selectioncomponent={el.selectioncomponent}
getEventSearchIdentifier={context.api.search.getEventSearchIdentifier}
activate={context.api.search.activate}
message={el.message}
/>
))}
</>
);
},
unmount() {
root.unmount();
},
};
}
RestrictDownloadButtonOperation.tsx
import { FunctionComponent, useEffect, useState } from "react";
import { waitForElemAlways } from "../../Utils";
interface BulkDownloadButtonModalPopupProps {
selectionPool: string;
subpoolid?: number;
selectioncomponent: string;
searchIdentifier: string;
getEventSearchIdentifier: (searchIdentifier: string) => string;
activate: any;
message: string;
}
const BulkDownloadButtonModalPopup: FunctionComponent<BulkDownloadButtonModalPopupProps> = ({
selectionPool,
subpoolid,
selectioncomponent,
searchIdentifier,
getEventSearchIdentifier,
activate,
message
}) => {
const orderButtonSelector = `[id="${selectioncomponent}"] [data-testid="selection-operations"] button[data-testid="Order"]`;
const [searchFinished, setSearchFinished] = useState<boolean>(false);
const onSearchFinished = async (evt: Event): Promise<void> => {
const { searchIdentifier: eventSearchIdentifier } = (
evt as CustomEvent<{ searchIdentifier: string }>
).detail;
const formattedIdentifier = getEventSearchIdentifier(searchIdentifier);
if (eventSearchIdentifier === formattedIdentifier) {
setSearchFinished(true);
}
};
useEffect(() => {
window.addEventListener("SEARCH_FINISHED", onSearchFinished);
activate(searchIdentifier);
return () => {
window.removeEventListener("SEARCH_FINISHED", onSearchFinished);
};
}, []);
useEffect(() => {
const hasKOLSourceSystemAssets = async (): Promise<boolean> => {
const selectionPoolApi =
`/api/selection/${selectionPool}` +
`/?definitionnames=m.asset&subpoolid=${subpoolid ?? ""}`;
const response = await fetch(selectionPoolApi, {
credentials: "same-origin",
});
if (!response.ok) return false;
const items: number[] =
(await response.json())?.["m.asset"]?.items || [];
if (!items.length) return false;
const entities = await Promise.all(
items.map(id =>
fetch(`/api/entities/${id}`, {
credentials: "same-origin",
}).then(r => r.json())
)
);
return entities.some(
entity =>
entity?.properties?.SourceSystem?.identifier ===
"KOL"
);
};
const addOrderButtonInterceptor = (): MutationObserver => {
return waitForElemAlways(orderButtonSelector, (element) => {
if (element.hasAttribute("data-kol-intercept")) return;
element.setAttribute("data-kol-intercept", "true");
element.addEventListener(
"click",
async (event) => {
if (element.hasAttribute("data-kol-bypass")) return;
event.stopImmediatePropagation();
event.preventDefault();
const hasKOL = await hasKOLSourceSystemAssets();
if (hasKOL) {
alert(message);
} else {
element.setAttribute("data-kol-bypass", "true");
element.click();
element.removeAttribute("data-kol-bypass");
}
},
true
);
});
};
const observer = addOrderButtonInterceptor();
return () => {
observer.disconnect();
};
}, [searchFinished]);
return null;
};
export default BulkDownloadButtonModalPopup;
So on click of download option, we will show them, how the below popup.
Thanks for reading and keep learning !!!
You can check my other blogs too if interested. Blog Website
References
- https://sitecoreforu.blogspot.com/2024/05/understanding-external-component-in-content-hub-with-use-case-using-react.html
- https://sitecoreforu.blogspot.com/2023/10/creating-custom-component-in-content-hub-using-external-component.html
- https://doc.sitecore.com/ch/en/users/content-hub/permissions.html






Comments
Post a Comment