Implementation for Displaying Assets Associated with New Definition with React Components - (Part 4)

👋 Welcome back to Part 4 of our series on Displaying and Managing Entities from a New Definition in Sitecore Content Hub! In this part, we will see how to display the assets of our MarketingCampaign using a React component (external component)


Understanding the Schema

In our current schema, we have a relation called MarketingAssets, where MarketingCampaign is the parent of M.Asset. This relationship helps us understand how entities interact within our Content Hub.





Creating a Marketing Campaign

When you create a new marketing campaign using the creation component, you will notice the Marketing Assets field. This field allows you to select various assets for your marketing campaign.







For example, let’s say you selected an asset labeled Asset A during the creation process. Once the entity is created, a new marketing campaign entity will be generated with the asset associated with it. If you check the JSON response of Asset A, you will find that it includes the MarketingAssets relation, and in that relation, you will see the associated campaign ID of the marketing campaign.


Relation Binding in the Selection Component

It's important to note that since this relation is designated as the parent of M.Asset, it won't be visible for binding in the selection component. You can learn more in detail in Part 2 of our series, where we explored the JSON structure of the new marketing campaign entities. In those relations, you may find that there are no values because of the relation and its chosen cardinality. Now, all the assets selected in the creation component will have the MarketingAssets relation with the campaign ID.


Creating an External Component

In this part of the series, I will show how to create an external component that displays the associated assets of marketing campaigns. Additionally, I have included a preview URL for viewing your assets.


Fetching Assets

In React, we generally use the EntityLoadConfiguration for assets with various load options, such as default, none, or all. For example:


    const assetLoadConfig = new EntityLoadConfiguration(
        CultureLoadOption.Default,
        new PropertyLoadOption([
            AssetDefinition.Properties.Title,
            AssetDefinition.Properties.FileName
        ]),
        new RelationLoadOption([
            AssetDefinition.Relations.MarketingAssets
        ])
    );


However, in this case, I opted to directly fetch the assets using the Raw client. To know more, you can refer to the Sitecore documentation. I am taking the assets in batches and binding the data to the marketing campaigns and their associated assets.


Code in Action

Here’s a snippet of the code that showcases how we fetch and bind the data:

  import React, { useEffect, useState } from "react";
  import { ContentHubClient } from "@sitecore/sc-contenthub-webclient-sdk";

  interface Props {
    client: ContentHubClient;
  }

  interface Campaign {
    id: number;
    properties: {
      CampaignName: string;
      StartDate: string;
      EndDate: string;
      Location: string;
      CampaignStatusActive: boolean;
    };
    assets: AssetInfo[];
  }

  interface AssetInfo {
    id: number;
    fileName: string;
    previewUrl: string;
  }

  const ListMarketingCampaigns: React.FC<Props> = ({ client }) => {
    const [campaigns, setCampaigns] = useState<Campaign[]>([]);
    const [error, setError] = useState<string | null>(null);
    const [assetsMap, setAssetsMap] = useState<{ [key: number]:
                                                 AssetInfo[]}>({});
    const [loading, setLoading] = useState<boolean>(true);
    const renditionBaseUrl = `https://${location.hostname}/api/gateway`;
    const take = 100;

    // Function to extract the campaign ID from a URL
    const extractIdFromUrl = (url: string): number => {
      const parts = url.split('/');
      return parseInt(parts[parts.length - 1], 10);
    };

    // Function to fetch campaign ID from MarketingAssets relation
    const fetchCampaignIdFromAssetRelation = async (href: string):
     Promise<number[] | null> => {
      try {
        const relationResponse = await client.raw.getAsync<any>(href);
        const relatedCampaigns = relationResponse.content?.parents || [];
        const campaignIds = relatedCampaigns.map((parent: any) =>     
        extractIdFromUrl(parent.href));
        if (campaignIds.length > 0) {
          return campaignIds;
        } else {
          console.log("No related campaigns found in MarketingAssets relation
            response.");
          return [];
        }
      } catch (error) {
        console.error("Error fetching MarketingAssets relation:", error);
        return [];
      }
    };

  useEffect(() => {
   const fetchCampaignsAndAssets = async () => {
    try {
      // Fetch Marketing Campaigns
      const campaignsResponse = await client.raw.getAsync<any>(
       `/api/entitydefinitions/MarketingCampaign/entities?skip=0&take=100
         &sort=CreatedOn&order=desc&culture=en-US`
      );
      const campaignsData: Campaign[] = campaignsResponse.content?.items;

      if (!Array.isArray(campaignsData)) {
       setError("Unexpected response format for campaigns.");
       console.error("Campaigns Data Error:", campaignsData);
       return;
      }

      setCampaigns(campaignsData);
      const campaignIds = campaignsData.map((campaign) => campaign.id);
      const campaignAssetsMap: { [key: number]: AssetInfo[] } = {};

      // Fetch assets in batches
      let allAssetsFetched = false;
      let skip = 0;

      while (!allAssetsFetched) {
      console.log(`Fetching assets with skip=${skip} and take=${take}...`);
      const assetsResponse = await client.raw.getAsync<any>(
         `/api/entitydefinitions/M.Asset/entities?skip=${skip}&take=${take}
          &sort=CreatedOn&order=desc&culture=en-US`
      );

     if (!assetsResponse || !assetsResponse.content?.items ||
     assetsResponse.content.items.length === 0) {
     allAssetsFetched = true; // No more assets to fetch
     console.log("No more assets to fetch.");
     break;
     }

     // Process assets and check their MarketingAssets relation
     for (const assetEntity of assetsResponse.content.items) {
      const marketingAssetsHref = assetEntity.relations?.MarketingAssets?.href;
         if (marketingAssetsHref) {
          const relatedCampaignIds  = await fetchCampaignIdFromAssetRelation
          (marketingAssetsHref);
          relatedCampaignIds!.forEach(campaignId => {
           if (campaignIds.includes(campaignId)) {
           const assetInfo: AssetInfo = {
            id: assetEntity.id,
            fileName: assetEntity.properties.FileName || "Unnamed Asset",
            previewUrl: `${renditionBaseUrl}/${assetEntity.id}/preview`,
            };
           
            if (!campaignAssetsMap[campaignId]) {
            campaignAssetsMap[campaignId] = [];
              }
              campaignAssetsMap[campaignId].push(assetInfo);
              }
            });
         } else {
            console.log("No MarketingAssets relation found for asset:",
            assetEntity.id);
           }
       }
       skip += take;
    }

        setAssetsMap(prevMap => ({ ...prevMap, ...campaignAssetsMap }));
        setLoading(false);
      } catch (err) {
        setError(`Failed to fetch campaigns or assets. Error: ${err}`);
        console.error("Error fetching campaigns or assets:", err);
        setLoading(false);
      }
    };

    fetchCampaignsAndAssets();
  }, [client]);

  return (
    <div style={{ margin: '20px' }}>
      <h1>Marketing Campaigns</h1>
      {error && <p>{error}</p>}
      {loading ? (
        <p>Loading assets...</p>
      ) : (
        campaigns.length > 0 ? (
          <div
            style={{
              overflowX: 'auto',
              boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
              borderRadius: '10px',
            }}
          >
            <table style={{ width: '100%', borderCollapse: 'collapse',
                borderRadius: '10px', overflow: 'hidden' }}>
              <thead>
                <tr style={{ backgroundColor: '#f8f8f8', color: '#333',
                    textAlign: 'left' }}>
                  <th style={{ padding: '12px 15px', fontWeight: '600',
                    borderBottom: '1px solid #ddd' }}>Name</th>
                  <th style={{ padding: '12px 15px', fontWeight: '600',
                    borderBottom: '1px solid #ddd' }}>Start Date</th>
                  <th style={{ padding: '12px 15px', fontWeight: '600',
                    borderBottom: '1px solid #ddd' }}>End Date</th>
                  <th style={{ padding: '12px 15px', fontWeight: '600',
                    borderBottom: '1px solid #ddd' }}>Location</th>
                  <th style={{ padding: '12px 15px', fontWeight: '600',
                    borderBottom: '1px solid #ddd' }}>Status</th>
                  <th style={{ padding: '12px 15px', fontWeight: '600',
                    borderBottom: '1px solid #ddd' }}>Marketing Assets</th>
                </tr>
              </thead>
              <tbody>
                {campaigns.map((campaign, index) => (
                  <tr key={campaign.id} style={{ backgroundColor:
                    index % 2 === 0 ? '#f9f9f9' : '#fff',
                    transition: 'background-color 0.3s ease' }}>
                    <td style={{ padding: '12px 15px',
                     borderBottom: '1px solid #ddd'
                     }}>{campaign.properties.CampaignName}</td>
                    <td style={{ padding: '12px 15px',
                    borderBottom: '1px solid #ddd'}}>
                    {new Date(campaign.properties.StartDate).
                     toLocaleDateString()}
                    </td>
                    <td style={{ padding: '12px 15px',
                    borderBottom: '1px solid #ddd' }}>
                    {new Date(campaign.properties.EndDate).
                     toLocaleDateString()}</td>
                    <td style={{ padding: '12px 15px',
                    borderBottom: '1px solid #ddd' }}>
                    {campaign.properties.Location}</td>
                    <td style={{ padding: '12px 15px',
                    borderBottom: '1px solid #ddd' }}>
                    {campaign.properties.CampaignStatusActive
                        ? "Active" : "Inactive"}</td>
                    <td style={{ padding: '12px 15px',
                       borderBottom: '1px solid #ddd' }}>
                      {assetsMap[campaign.id] &&
                        assetsMap[campaign.id].length > 0 ? (
                        <ul>
                          {assetsMap[campaign.id].map((asset) => (
                            <li key={asset.id}>
                              <a href={asset.previewUrl} target="_blank"
                                rel="noopener noreferrer">{asset.fileName}</a>
                            </li>
                          ))}
                        </ul>
                      ) : (
                        <span>No assets</span>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        ) : (
          <p>No campaigns found.</p>
        )
      )}
    </div>
    );
  };

  export default ListMarketingCampaigns;


In this code, we’re making a raw API call to fetch the assets associated with a specific marketing campaign. The returned data can then be utilized within our component to render the assets along with their details.

Here is the Index.tsx file:
  import { ContentHubClient } from "@sitecore/sc-contenthub-webclient-sdk";
  import { createRoot } from "react-dom/client";
  import ListMarketingCampaigns from "./ListMarketingCampaigns";

  interface Context {
      client: ContentHubClient
  }

  export default function createExternalRoot(container: HTMLElement) {
    const root = createRoot(container);

    return {
      render(context: Context) {
        root.render(
          <ListMarketingCampaigns
          client={context.client}
          />
        );
      },

      unmount() {
        root.unmount();
      },
    }
  }

Component Functionality

The external component will display all entities created from the marketing campaign definition, including their associated assets. In the Marketing Assets column, you will see the filenames of the assets, which are clickable links that will open the asset previews. This setup provides a clear and convenient way for users to access and view the assets linked to their marketing campaigns.










Conclusion

In summary, it's important to understand the relationship and its cardinality, as well as its associated definitions for managing and displaying marketing campaigns in Sitecore Content Hub. By creating an external component to showcase these assets, we can enhance our workflow and provide users with a comprehensive view of their marketing campaigns.

Stay tuned for the next part of our series, where we will continue to explore more functionalities and enhancements within Sitecore Content Hub! 😊👋

Post a Comment (0)
Previous Post Next Post